From d8321082dd2300858e3f6fcc527ccfca55739ed3 Mon Sep 17 00:00:00 2001 From: John Pavek Date: Tue, 21 May 2024 16:56:31 -0500 Subject: [PATCH 1/4] Adds common tag helpers --- .../TagHelpers/AnchorTagHelper.cs | 69 +++++++++++++++++++ .../TagHelpers/ClassListHelper.cs | 32 +++++++++ .../TagHelpers/ConditionalWrapperHelper.cs | 36 ++++++++++ src/Blend.Optimizely/TagHelpers/IImageFile.cs | 16 +++++ .../TagHelpers/ImageHelper.cs | 62 +++++++++++++++++ .../TagHelpers/SourceHelper.cs | 61 ++++++++++++++++ 6 files changed, 276 insertions(+) create mode 100644 src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs create mode 100644 src/Blend.Optimizely/TagHelpers/ClassListHelper.cs create mode 100644 src/Blend.Optimizely/TagHelpers/ConditionalWrapperHelper.cs create mode 100644 src/Blend.Optimizely/TagHelpers/IImageFile.cs create mode 100644 src/Blend.Optimizely/TagHelpers/ImageHelper.cs create mode 100644 src/Blend.Optimizely/TagHelpers/SourceHelper.cs diff --git a/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs b/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs new file mode 100644 index 0000000..32a55e8 --- /dev/null +++ b/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs @@ -0,0 +1,69 @@ +using EPiServer.ServiceLocation; +using Microsoft.AspNetCore.Razor.TagHelpers; + +namespace Blend.Optimizely.TagHelpers +{ + [HtmlTargetElement("a", Attributes = "content-link")] + public class AnchorTagHelper : TagHelper + { + private Injected LinkResolver { get; } + + /// + /// Some kind of link that can be resolved via Optimizely. This includes: + /// IResolvable for special cases + /// IContent objects + /// Url objects + /// LinkItem objects + /// ContentReference objects + /// + [HtmlAttributeName("content-link")] + public object ContentLink { get; set; } + + [HtmlAttributeName("link-options")] + public LinkOptions LinkOptions { get; set; } + + [HtmlAttributeName("output-options")] + public OutputOptions OutputOptions { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + var resolvedLink = LinkResolver.Service.TryResolveLink(ContentLink, LinkOptions); + + if (resolvedLink is null) + return; + + if (resolvedLink.Href.HasValue()) + { + output.Attributes.SetAttribute("href", resolvedLink.Href); + } + else if (OutputOptions is OutputOptions.ConvertLinkToSpan) + { + output.TagName = "span"; + } + else if (OutputOptions is OutputOptions.SuppressOutput) + { + output.SuppressOutput(); + return; + } + + if (resolvedLink.Target.HasValue() && !context.AllAttributes.TryGetAttribute("target", out _)) + output.Attributes.SetAttribute("target", resolvedLink.Target); + } + } + + + public enum OutputOptions + { + None = 0, + + /// + /// If url is blank, convert the wrapping to a + /// + ConvertLinkToSpan = 1, + + /// + /// If url is blank, suppress output + /// + SuppressOutput = 2 + } +} diff --git a/src/Blend.Optimizely/TagHelpers/ClassListHelper.cs b/src/Blend.Optimizely/TagHelpers/ClassListHelper.cs new file mode 100644 index 0000000..a3cd408 --- /dev/null +++ b/src/Blend.Optimizely/TagHelpers/ClassListHelper.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; +using System.Linq; + +namespace Blend.Optimizely.TagHelpers +{ + [HtmlTargetElement("*", Attributes = "classes")] + public class ClassListHelper : TagHelper + { + public IEnumerable? Classes { get; set; } + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (Classes.HasValue() && context != null && output != null) + { + var classes = Classes.ToList(); + var existingClasses = context.AllAttributes["class"]?.Value.ToString(); + if (existingClasses != null) + { + var existingClassList = existingClasses.Split(" ", System.StringSplitOptions.RemoveEmptyEntries); + classes.AddRange(existingClassList); + } + if (output.Attributes.TryGetAttribute("class", out var outputClass)) + { + var outputClassList = outputClass?.Value?.ToString()?.Split(" ", System.StringSplitOptions.RemoveEmptyEntries); + if (outputClassList != null && outputClassList.HasValue()) + classes.AddRange(outputClassList); + } + output.Attributes.SetAttribute("class", string.Join(" ", classes)); + } + } + } +} diff --git a/src/Blend.Optimizely/TagHelpers/ConditionalWrapperHelper.cs b/src/Blend.Optimizely/TagHelpers/ConditionalWrapperHelper.cs new file mode 100644 index 0000000..59eccc7 --- /dev/null +++ b/src/Blend.Optimizely/TagHelpers/ConditionalWrapperHelper.cs @@ -0,0 +1,36 @@ +using Microsoft.AspNetCore.Razor.TagHelpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Blend.Optimizely.TagHelpers +{ + [HtmlTargetElement("*", Attributes = "wrap-if")] + [HtmlTargetElement("*", Attributes = "suppress-if")] + public class ConditionalWrapperHelper : TagHelper + { + [HtmlAttributeName("wrap-if")] + public bool WrapIfCondition { get; set; } + + [HtmlAttributeName("suppress-if")] + public bool SuppressIfCondition { get; set; } + + + [HtmlAttributeName("link-options")] + public LinkOptions LinkOptions { get; set; } + + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (SuppressIfCondition) + { + output.SuppressOutput(); + } + if (!WrapIfCondition) + { + output.TagName = null; + } + } + } +} diff --git a/src/Blend.Optimizely/TagHelpers/IImageFile.cs b/src/Blend.Optimizely/TagHelpers/IImageFile.cs new file mode 100644 index 0000000..6b5b817 --- /dev/null +++ b/src/Blend.Optimizely/TagHelpers/IImageFile.cs @@ -0,0 +1,16 @@ +using EPiServer.Core; +using System.Collections.Generic; + +namespace Blend.Optimizely.TagHelpers +{ + internal interface IImageFile :IContent + { + public int Width { get; set; } + public int Height { get; set; } + + public string GetAltText(); + + public IEnumerable ImageOptions(); + + } +} diff --git a/src/Blend.Optimizely/TagHelpers/ImageHelper.cs b/src/Blend.Optimizely/TagHelpers/ImageHelper.cs new file mode 100644 index 0000000..8894e15 --- /dev/null +++ b/src/Blend.Optimizely/TagHelpers/ImageHelper.cs @@ -0,0 +1,62 @@ +using EPiServer; +using EPiServer.Core; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Linq; + +namespace Blend.Optimizely.TagHelpers +{ + + [HtmlTargetElement("Image", Attributes = "src", TagStructure = TagStructure.WithoutEndTag)] + public class ImageHelper : TagHelper + { + public ContentReference? src { get; set; } + + [HtmlAttributeName("eager-loading")] + public bool EagerLoading { get; set; } + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (!src.HasValue() && output != null) + { + output.SuppressOutput(); + return; + } + + var image = src.Get(); // NOTE: This is likely to be a breaking change - Switching from `ImageFile` to `IImageFile` + var imgUrl = src.ResolveUrl(); + + if (image == null || !imgUrl.HasValue()) + { + output.SuppressOutput(); + return; + } + + output.TagName = "img"; + output.TagMode = TagMode.SelfClosing; + output.Attributes.SetAttribute("src", imgUrl); + output.Attributes.SetAttribute("alt", image.GetAltText()); // NOTE: This is likely to be a breaking change - Switching from `ContentReference` to `IImageFile` + + if (image.Width > 0) + output.Attributes.SetAttribute("width", image.Width); + + if (image.Height > 0) + output.Attributes.SetAttribute("height", image.Height); + + var imageVariations = image.ImageOptions().Where(x => x != image.Width).Select(x => + { + var url = new UrlBuilder(imgUrl); + url.QueryCollection.Add("width", x.ToString()); + return (string)url + $" {x}w"; + }).ToList(); + + if (imageVariations.HasValue()) + { + imageVariations.Add($"{imgUrl} {image.Width}w"); + output.Attributes.SetAttribute("srcset", string.Join($", ", imageVariations)); + output.Attributes.SetAttribute("sizes", "100vw"); + } + + if (!EagerLoading) + output.Attributes.SetAttribute("loading", "lazy"); + } + } +} \ No newline at end of file diff --git a/src/Blend.Optimizely/TagHelpers/SourceHelper.cs b/src/Blend.Optimizely/TagHelpers/SourceHelper.cs new file mode 100644 index 0000000..7af1565 --- /dev/null +++ b/src/Blend.Optimizely/TagHelpers/SourceHelper.cs @@ -0,0 +1,61 @@ +using EPiServer; +using EPiServer.Core; +using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Linq; + +namespace Blend.Optimizely.TagHelpers +{ + + [HtmlTargetElement("Source", Attributes = "srcset")] + public class SourceHelper : TagHelper + { + public ContentReference? src { get; set; } + + public bool EagerLoading { get; set; } + public override void Process(TagHelperContext context, TagHelperOutput output) + { + if (!src.HasValue() && output != null) + { + output.SuppressOutput(); + return; + } + + var image = src.Get(); // NOTE: This is likely to be a breaking change - Switching from `ImageFile` to `IImageFile` + var imgUrl = src.ResolveUrl(); + + if (image == null || !imgUrl.HasValue()) + { + output.SuppressOutput(); + return; + } + + output.TagName = "img"; + output.TagMode = TagMode.SelfClosing; + output.Attributes.SetAttribute("src", imgUrl); + output.Attributes.SetAttribute("alt", image.GetAltText()); // NOTE: This is likely to be a breaking change - Switching from `ContentReference` to `IImageFile` + + if (image.Width > 0) + output.Attributes.SetAttribute("width", image.Width); + + if (image.Height > 0) + output.Attributes.SetAttribute("height", image.Height); + + var imageVariations = image.ImageOptions().Where(x => x != image.Width).Select(x => + { + var url = new UrlBuilder(imgUrl); + url.QueryCollection.Add("width", x.ToString()); + return (string)url + $" {x}w"; + }).ToList(); + + if (imageVariations.HasValue()) + { + imageVariations.Add($"{imgUrl} {image.Width}w"); + output.Attributes.SetAttribute("srcset", string.Join($", ", imageVariations)); + output.Attributes.SetAttribute("sizes", "100vw"); + } + + if (!EagerLoading) + output.Attributes.SetAttribute("loading", "lazy"); + } + } +} \ No newline at end of file From e39df185f6bc3bd2d7448931f21a8632e82f151a Mon Sep 17 00:00:00 2001 From: John Pavek Date: Thu, 20 Jun 2024 10:45:52 -0500 Subject: [PATCH 2/4] Anchor helper update --- .../TagHelpers/AnchorTagHelper.cs | 54 +++++++++++++------ 1 file changed, 37 insertions(+), 17 deletions(-) diff --git a/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs b/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs index 32a55e8..68f7178 100644 --- a/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs +++ b/src/Blend.Optimizely/TagHelpers/AnchorTagHelper.cs @@ -1,4 +1,5 @@ -using EPiServer.ServiceLocation; +using Blend.Optimizely; +using EPiServer.ServiceLocation; using Microsoft.AspNetCore.Razor.TagHelpers; namespace Blend.Optimizely.TagHelpers @@ -19,51 +20,70 @@ public class AnchorTagHelper : TagHelper [HtmlAttributeName("content-link")] public object ContentLink { get; set; } + /// + /// How should the anchor tag be handle when there is no valid href or the condition is false. + /// Options are: + /// None, ConvertLinkToSpan, SuppressOutput, KeepInnerContent + /// [HtmlAttributeName("link-options")] public LinkOptions LinkOptions { get; set; } - [HtmlAttributeName("output-options")] - public OutputOptions OutputOptions { get; set; } + [HtmlAttributeName("fallback-option")] + public AnchorFallbackOptions FallbackOption { get; set; } + + /// + /// Only output an anchor tag with href when condition is true and a valid link exists. Default is true + /// + [HtmlAttributeName("condition")] + public bool Condition { get; set; } = true; public override void Process(TagHelperContext context, TagHelperOutput output) { - var resolvedLink = LinkResolver.Service.TryResolveLink(ContentLink, LinkOptions); - - if (resolvedLink is null) + if (context is null || output is null) return; - if (resolvedLink.Href.HasValue()) + var resolvedLink = LinkResolver.Service.TryResolveLink(ContentLink, LinkOptions); + if (resolvedLink is not null && resolvedLink.Href.HasValue() && Condition) { output.Attributes.SetAttribute("href", resolvedLink.Href); + + if (resolvedLink.Target.HasValue() && !context.AllAttributes.TryGetAttribute("target", out _)) + output.Attributes.SetAttribute("target", resolvedLink.Target); } - else if (OutputOptions is OutputOptions.ConvertLinkToSpan) + else if (FallbackOption is AnchorFallbackOptions.ConvertLinkToSpan) { output.TagName = "span"; } - else if (OutputOptions is OutputOptions.SuppressOutput) + else if (FallbackOption is AnchorFallbackOptions.SuppressOutput) { output.SuppressOutput(); return; } - - if (resolvedLink.Target.HasValue() && !context.AllAttributes.TryGetAttribute("target", out _)) - output.Attributes.SetAttribute("target", resolvedLink.Target); + else if (FallbackOption is AnchorFallbackOptions.KeepInnerContent) + { + output.TagName = string.Empty; + return; + } } } - - public enum OutputOptions + public enum AnchorFallbackOptions { None = 0, /// - /// If url is blank, convert the wrapping to a + /// If url is blank, convert the wrapping a tag to a span /// ConvertLinkToSpan = 1, /// /// If url is blank, suppress output /// - SuppressOutput = 2 + SuppressOutput = 2, + + /// + /// If url is blank, remove wrapping tag + /// + KeepInnerContent = 3 } -} +} \ No newline at end of file From a53cca50dc348bff3a13559fbe1d3bab62add002 Mon Sep 17 00:00:00 2001 From: John Pavek Date: Thu, 20 Jun 2024 14:25:55 -0500 Subject: [PATCH 3/4] Removes IImageFile as requirement --- .../TagHelpers/ImageHelper.cs | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/Blend.Optimizely/TagHelpers/ImageHelper.cs b/src/Blend.Optimizely/TagHelpers/ImageHelper.cs index 8894e15..4f17e94 100644 --- a/src/Blend.Optimizely/TagHelpers/ImageHelper.cs +++ b/src/Blend.Optimizely/TagHelpers/ImageHelper.cs @@ -1,11 +1,12 @@ using EPiServer; using EPiServer.Core; +using EPiServer.ServiceLocation; using Microsoft.AspNetCore.Razor.TagHelpers; +using System.Collections.Generic; using System.Linq; namespace Blend.Optimizely.TagHelpers { - [HtmlTargetElement("Image", Attributes = "src", TagStructure = TagStructure.WithoutEndTag)] public class ImageHelper : TagHelper { @@ -21,40 +22,45 @@ public override void Process(TagHelperContext context, TagHelperOutput output) return; } - var image = src.Get(); // NOTE: This is likely to be a breaking change - Switching from `ImageFile` to `IImageFile` var imgUrl = src.ResolveUrl(); - - if (image == null || !imgUrl.HasValue()) + if (!imgUrl.HasValue()) { output.SuppressOutput(); return; } - output.TagName = "img"; - output.TagMode = TagMode.SelfClosing; - output.Attributes.SetAttribute("src", imgUrl); - output.Attributes.SetAttribute("alt", image.GetAltText()); // NOTE: This is likely to be a breaking change - Switching from `ContentReference` to `IImageFile` + var imageVariations = new List(); - if (image.Width > 0) - output.Attributes.SetAttribute("width", image.Width); + var loader = ServiceLocator.Current.GetInstance(); + if (loader.TryGet(src, out var image)) + { + output.Attributes.SetAttribute("alt", image.GetAltText()); - if (image.Height > 0) - output.Attributes.SetAttribute("height", image.Height); + if (image.Width > 0) + output.Attributes.SetAttribute("width", image.Width); - var imageVariations = image.ImageOptions().Where(x => x != image.Width).Select(x => - { - var url = new UrlBuilder(imgUrl); - url.QueryCollection.Add("width", x.ToString()); - return (string)url + $" {x}w"; - }).ToList(); + if (image.Height > 0) + output.Attributes.SetAttribute("height", image.Height); - if (imageVariations.HasValue()) - { - imageVariations.Add($"{imgUrl} {image.Width}w"); - output.Attributes.SetAttribute("srcset", string.Join($", ", imageVariations)); - output.Attributes.SetAttribute("sizes", "100vw"); + imageVariations = image.ImageOptions().Where(x => x != image.Width).Select(x => + { + var url = new UrlBuilder(imgUrl); + url.QueryCollection.Add("width", x.ToString()); + return (string)url + $" {x}w"; + }).ToList(); + + if (imageVariations.HasValue()) + { + imageVariations.Add($"{imgUrl} {image.Width}w"); + output.Attributes.SetAttribute("srcset", string.Join($", ", imageVariations)); + output.Attributes.SetAttribute("sizes", "100vw"); + } } + output.TagName = "img"; + output.TagMode = TagMode.SelfClosing; + output.Attributes.SetAttribute("src", imgUrl); + if (!EagerLoading) output.Attributes.SetAttribute("loading", "lazy"); } From 9fe22fc7d7854402f954e8ba2f7deda59a1b6709 Mon Sep 17 00:00:00 2001 From: John Pavek Date: Thu, 20 Jun 2024 14:47:47 -0500 Subject: [PATCH 4/4] Removes ClassList tag helper --- .../TagHelpers/ClassListHelper.cs | 32 ------------------- 1 file changed, 32 deletions(-) delete mode 100644 src/Blend.Optimizely/TagHelpers/ClassListHelper.cs diff --git a/src/Blend.Optimizely/TagHelpers/ClassListHelper.cs b/src/Blend.Optimizely/TagHelpers/ClassListHelper.cs deleted file mode 100644 index a3cd408..0000000 --- a/src/Blend.Optimizely/TagHelpers/ClassListHelper.cs +++ /dev/null @@ -1,32 +0,0 @@ -using Microsoft.AspNetCore.Razor.TagHelpers; -using System.Collections.Generic; -using System.Linq; - -namespace Blend.Optimizely.TagHelpers -{ - [HtmlTargetElement("*", Attributes = "classes")] - public class ClassListHelper : TagHelper - { - public IEnumerable? Classes { get; set; } - public override void Process(TagHelperContext context, TagHelperOutput output) - { - if (Classes.HasValue() && context != null && output != null) - { - var classes = Classes.ToList(); - var existingClasses = context.AllAttributes["class"]?.Value.ToString(); - if (existingClasses != null) - { - var existingClassList = existingClasses.Split(" ", System.StringSplitOptions.RemoveEmptyEntries); - classes.AddRange(existingClassList); - } - if (output.Attributes.TryGetAttribute("class", out var outputClass)) - { - var outputClassList = outputClass?.Value?.ToString()?.Split(" ", System.StringSplitOptions.RemoveEmptyEntries); - if (outputClassList != null && outputClassList.HasValue()) - classes.AddRange(outputClassList); - } - output.Attributes.SetAttribute("class", string.Join(" ", classes)); - } - } - } -}