From ca0fb05baaa527d82290db4e735725d4d0bdea84 Mon Sep 17 00:00:00 2001 From: sbwalker Date: Wed, 18 Dec 2024 15:15:54 -0500 Subject: [PATCH] Improvements to add support for script type and data-* attributes. Also added Script and Stylesheet classes to simplify Resource declarations. --- Oqtane.Client/Modules/ModuleBase.cs | 6 +- .../Themes/BlazorTheme/Themes/Default.razor | 9 +-- Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs | 8 +-- Oqtane.Client/Themes/ThemeBase.cs | 6 +- Oqtane.Client/UI/Interop.cs | 7 +- Oqtane.Client/UI/ThemeBuilder.razor | 6 +- Oqtane.Server/Components/App.razor | 15 +++- Oqtane.Server/wwwroot/js/interop.js | 72 ++++++++++--------- Oqtane.Shared/Models/Resource.cs | 40 +++++++++-- Oqtane.Shared/Models/Script.cs | 53 ++++++++++++++ Oqtane.Shared/Models/Stylesheet.cs | 30 ++++++++ 11 files changed, 191 insertions(+), 61 deletions(-) create mode 100644 Oqtane.Shared/Models/Script.cs create mode 100644 Oqtane.Shared/Models/Stylesheet.cs diff --git a/Oqtane.Client/Modules/ModuleBase.cs b/Oqtane.Client/Modules/ModuleBase.cs index 4a794630d..b99e7d294 100644 --- a/Oqtane.Client/Modules/ModuleBase.cs +++ b/Oqtane.Client/Modules/ModuleBase.cs @@ -98,17 +98,17 @@ protected override async Task OnAfterRenderAsync(bool firstRender) var inline = 0; foreach (Resource resource in resources) { - if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) + if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload) { if (!string.IsNullOrEmpty(resource.Url)) { var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url; - scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module, location = resource.Location.ToString().ToLower() }); + scripts.Add(new { href = url, type = resource.Type ?? "", bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", location = resource.Location.ToString().ToLower(), dataAttributes = resource.DataAttributes }); } else { inline += 1; - await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower()); + await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Type ?? "", resource.Content, resource.Location.ToString().ToLower()); } } } diff --git a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor index 2682018f5..2c684bb80 100644 --- a/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor +++ b/Oqtane.Client/Themes/BlazorTheme/Themes/Default.razor @@ -37,11 +37,8 @@ public override List Resources => new List() { // obtained from https://cdnjs.com/libraries - new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", - Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", - CrossOrigin = "anonymous" }, - new Resource { ResourceType = ResourceType.Stylesheet, Url = ThemePath() + "Theme.css" }, - new Resource { ResourceType = ResourceType.Script, Url = Constants.BootstrapScriptUrl, Integrity = Constants.BootstrapScriptIntegrity, CrossOrigin = "anonymous", Location = ResourceLocation.Body }, + new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", "anonymous"), + new Stylesheet(ThemePath() + "Theme.css"), + new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") }; - } diff --git a/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs b/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs index bb528190e..f0197b717 100644 --- a/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs +++ b/Oqtane.Client/Themes/OqtaneTheme/ThemeInfo.cs @@ -17,11 +17,9 @@ public class ThemeInfo : ITheme Resources = new List() { // obtained from https://cdnjs.com/libraries - new Resource { ResourceType = ResourceType.Stylesheet, Url = "https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", - Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", - CrossOrigin = "anonymous" }, - new Resource { ResourceType = ResourceType.Stylesheet, Url = "~/Theme.css" }, - new Resource { ResourceType = ResourceType.Script, Url = Constants.BootstrapScriptUrl, Integrity = Constants.BootstrapScriptIntegrity, CrossOrigin = "anonymous", Location = ResourceLocation.Body } + new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", "anonymous"), + new Stylesheet("~/Theme.css"), + new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "anonymous") } }; } diff --git a/Oqtane.Client/Themes/ThemeBase.cs b/Oqtane.Client/Themes/ThemeBase.cs index d6f9789e4..6db6a0288 100644 --- a/Oqtane.Client/Themes/ThemeBase.cs +++ b/Oqtane.Client/Themes/ThemeBase.cs @@ -62,17 +62,17 @@ protected override async Task OnAfterRenderAsync(bool firstRender) var inline = 0; foreach (Resource resource in resources) { - if (string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) + if ((string.IsNullOrEmpty(resource.RenderMode) || resource.RenderMode == RenderModes.Interactive) && !resource.Reload) { if (!string.IsNullOrEmpty(resource.Url)) { var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + resource.Url; - scripts.Add(new { href = url, bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", es6module = resource.ES6Module, location = resource.Location.ToString().ToLower() }); + scripts.Add(new { href = url, type = resource.Type ?? "", bundle = resource.Bundle ?? "", integrity = resource.Integrity ?? "", crossorigin = resource.CrossOrigin ?? "", location = resource.Location.ToString().ToLower(), dataAttributes = resource.DataAttributes }); } else { inline += 1; - await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Content, resource.Location.ToString().ToLower()); + await interop.IncludeScript(GetType().Namespace.ToLower() + inline.ToString(), "", "", "", resource.Type ?? "", resource.Content, resource.Location.ToString().ToLower()); } } } diff --git a/Oqtane.Client/UI/Interop.cs b/Oqtane.Client/UI/Interop.cs index c5964c962..8183aff5f 100644 --- a/Oqtane.Client/UI/Interop.cs +++ b/Oqtane.Client/UI/Interop.cs @@ -117,12 +117,17 @@ public Task IncludeScript(string id, string src, string integrity, string crosso } public Task IncludeScript(string id, string src, string integrity, string crossorigin, string type, string content, string location) + { + return IncludeScript(id, src, integrity, crossorigin, type, content, location, null); + } + + public Task IncludeScript(string id, string src, string integrity, string crossorigin, string type, string content, string location, Dictionary dataAttributes) { try { _jsRuntime.InvokeVoidAsync( "Oqtane.Interop.includeScript", - id, src, integrity, crossorigin, type, content, location); + id, src, integrity, crossorigin, type, content, location, dataAttributes); return Task.CompletedTask; } catch diff --git a/Oqtane.Client/UI/ThemeBuilder.razor b/Oqtane.Client/UI/ThemeBuilder.razor index 3058f4f43..71543e036 100644 --- a/Oqtane.Client/UI/ThemeBuilder.razor +++ b/Oqtane.Client/UI/ThemeBuilder.razor @@ -176,7 +176,7 @@ if (!string.IsNullOrEmpty(src)) { src = (src.Contains("://")) ? src : PageState.Alias.BaseUrl + src; - scripts.Add(new { href = src, bundle = "", integrity = integrity, crossorigin = crossorigin, es6module = (type == "module"), location = location.ToString().ToLower(), dataAttributes = dataAttributes }); + scripts.Add(new { href = src, type = type, bundle = "", integrity = integrity, crossorigin = crossorigin, location = location.ToString().ToLower(), dataAttributes = dataAttributes }); } else { @@ -186,8 +186,8 @@ count += 1; id = $"page{PageState.Page.PageId}-script{count}"; } - index = script.IndexOf(">") + 1; - await interop.IncludeScript(id, "", "", "", "", script.Substring(index, script.IndexOf("") - index), location.ToString().ToLower()); + var pos = script.IndexOf(">") + 1; + await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("") - pos), location.ToString().ToLower(), dataAttributes); } index = content.IndexOf(" 0) + { + foreach (var attribute in resource.DataAttributes) + { + dataAttributes += " " + attribute.Key + "=\"" + attribute.Value + "\""; + } + } + return ""; + ((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") + + ">"; } else { diff --git a/Oqtane.Server/wwwroot/js/interop.js b/Oqtane.Server/wwwroot/js/interop.js index 675cebcae..ee81109c0 100644 --- a/Oqtane.Server/wwwroot/js/interop.js +++ b/Oqtane.Server/wwwroot/js/interop.js @@ -120,13 +120,22 @@ Oqtane.Interop = { this.includeLink(links[i].id, links[i].rel, links[i].href, links[i].type, links[i].integrity, links[i].crossorigin, links[i].insertbefore); } }, - includeScript: function (id, src, integrity, crossorigin, type, content, location) { + includeScript: function (id, src, integrity, crossorigin, type, content, location, dataAttributes) { var script; if (src !== "") { script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); } else { - script = document.getElementById(id); + if (id !== "") { + script = document.getElementById(id); + } else { + const scripts = document.querySelectorAll("script:not([src])"); + for (let i = 0; i < scripts.length; i++) { + if (scripts[i].textContent.includes(content)) { + script = scripts[i]; + } + } + } } if (script !== null) { script.remove(); @@ -152,37 +161,36 @@ Oqtane.Interop = { else { script.innerHTML = content; } - script.async = false; - this.addScript(script, location) - .then(() => { - if (src !== "") { - console.log(src + ' loaded'); - } - else { - console.log(id + ' loaded'); - } - }) - .catch(() => { - if (src !== "") { - console.error(src + ' failed'); - } - else { - console.error(id + ' failed'); - } - }); + if (dataAttributes !== null) { + for (var key in dataAttributes) { + script.setAttribute(key, dataAttributes[key]); + } + } + + try { + this.addScript(script, location); + } catch (error) { + if (src !== "") { + console.error("Failed to load external script: ${src}", error); + } else { + console.error("Failed to load inline script: ${content}", error); + } + } } }, addScript: function (script, location) { - if (location === 'head') { - document.head.appendChild(script); - } - if (location === 'body') { - document.body.appendChild(script); - } + return new Promise((resolve, reject) => { + script.async = false; + script.defer = false; + + script.onload = () => resolve(); + script.onerror = (error) => reject(error); - return new Promise((res, rej) => { - script.onload = res(); - script.onerror = rej(); + if (location === 'head') { + document.head.appendChild(script); + } else { + document.body.appendChild(script); + } }); }, includeScripts: async function (scripts) { @@ -222,10 +230,10 @@ Oqtane.Interop = { if (scripts[s].crossorigin !== '') { element.crossOrigin = scripts[s].crossorigin; } - if (scripts[s].es6module === true) { - element.type = "module"; + if (scripts[s].type !== '') { + element.type = scripts[s].type; } - if (typeof scripts[s].dataAttributes !== "undefined" && scripts[s].dataAttributes !== null) { + if (scripts[s].dataAttributes !== null) { for (var key in scripts[s].dataAttributes) { element.setAttribute(key, scripts[s].dataAttributes[key]); } diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 177475d04..92b64bef7 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -1,4 +1,6 @@ using System; +using System.Collections.Generic; +using System.Linq; using Oqtane.Shared; namespace Oqtane.Models @@ -27,6 +29,11 @@ public string Url } } + /// + /// For Scripts this allows type to be specified - not applicable to Stylesheets + /// + public string Type { get; set; } + /// /// Integrity checks to increase the security of resources accessed. Especially common in CDN resources. /// @@ -52,11 +59,6 @@ public string Url /// public ResourceLocation Location { get; set; } - /// - /// For Scripts this allows type="module" registrations - not applicable to Stylesheets - /// - public bool ES6Module { get; set; } - /// /// Allows specification of inline script - not applicable to Stylesheets /// @@ -72,6 +74,11 @@ public string Url /// public bool Reload { get; set; } + /// + /// Cusotm data-* attributes for scripts - not applicable to Stylesheets + /// + public Dictionary DataAttributes { get; set; } + /// /// The namespace of the component that declared the resource - only used in SiteRouter /// @@ -82,14 +89,22 @@ public Resource Clone(ResourceLevel level, string name) var resource = new Resource(); resource.ResourceType = ResourceType; resource.Url = Url; + resource.Type = Type; resource.Integrity = Integrity; resource.CrossOrigin = CrossOrigin; resource.Bundle = Bundle; resource.Location = Location; - resource.ES6Module = ES6Module; resource.Content = Content; resource.RenderMode = RenderMode; resource.Reload = Reload; + resource.DataAttributes = new Dictionary(); + if (DataAttributes != null && DataAttributes.Count > 0) + { + foreach (var kvp in DataAttributes) + { + resource.DataAttributes.Add(kvp.Key, kvp.Value); + } + } resource.Level = level; resource.Namespace = name; return resource; @@ -97,5 +112,18 @@ public Resource Clone(ResourceLevel level, string name) [Obsolete("ResourceDeclaration is deprecated", false)] public ResourceDeclaration Declaration { get; set; } + + [Obsolete("ES6Module is deprecated. Use Type property instead for scripts.", false)] + public bool ES6Module + { + get => (Type == "module"); + set + { + if (value) + { + Type = "module"; + }; + } + } } } diff --git a/Oqtane.Shared/Models/Script.cs b/Oqtane.Shared/Models/Script.cs new file mode 100644 index 000000000..de44a5ced --- /dev/null +++ b/Oqtane.Shared/Models/Script.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using Oqtane.Shared; + +namespace Oqtane.Models +{ + /// + /// Script inherits from Resource and offers constructors with parameters specific to Scripts + /// + public class Script : Resource + { + private void SetDefaults() + { + this.ResourceType = ResourceType.Script; + this.Location = ResourceLocation.Body; + } + + public Script(string Src) + { + SetDefaults(); + this.Url = Src; + } + + public Script(string Content, string Type) + { + SetDefaults(); + this.Content = Content; + this.Type = Type; + } + + public Script(string Src, string Integrity, string CrossOrigin) + { + SetDefaults(); + this.Url = Src; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + } + + public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, bool Reload, Dictionary DataAttributes, string RenderMode) + { + SetDefaults(); + this.Url = Src; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + this.Type = Type; + this.Content = Content; + this.Location = Location; + this.Bundle = Bundle; + this.Reload = Reload; + this.DataAttributes = DataAttributes; + this.RenderMode = RenderMode; + } + } +} diff --git a/Oqtane.Shared/Models/Stylesheet.cs b/Oqtane.Shared/Models/Stylesheet.cs new file mode 100644 index 000000000..b3b8c5fe4 --- /dev/null +++ b/Oqtane.Shared/Models/Stylesheet.cs @@ -0,0 +1,30 @@ +using Oqtane.Shared; + +namespace Oqtane.Models +{ + /// + /// Stylesheet inherits from Resource and offers constructors with parameters specific to Stylesheets + /// + public class Stylesheet : Resource + { + private void SetDefaults() + { + this.ResourceType = ResourceType.Stylesheet; + this.Location = ResourceLocation.Head; + } + + public Stylesheet(string Href) + { + SetDefaults(); + this.Url = Href; + } + + public Stylesheet(string Href, string Integrity, string CrossOrigin) + { + SetDefaults(); + this.Url = Href; + this.Integrity = Integrity; + this.CrossOrigin = CrossOrigin; + } + } +}