Improvements to add support for script type and data-* attributes. Also added Script and Stylesheet classes to simplify Resource declarations.

This commit is contained in:
sbwalker 2024-12-18 15:15:54 -05:00
parent 3a1244bddc
commit ca0fb05baa
11 changed files with 191 additions and 61 deletions

View File

@ -98,17 +98,17 @@ namespace Oqtane.Modules
var inline = 0; var inline = 0;
foreach (Resource resource in resources) 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)) if (!string.IsNullOrEmpty(resource.Url))
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + 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 else
{ {
inline += 1; 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());
} }
} }
} }

View File

@ -37,11 +37,8 @@
public override List<Resource> Resources => new List<Resource>() public override List<Resource> Resources => new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // 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", new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootstrap/5.3.3/css/bootstrap.min.css", "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", "anonymous"),
Integrity = "sha512-jnSuA4Ss2PkkikSOLtYs8BlYIeeIK1h99ty4YfvRPAlzr377vr3CXDb7sb7eEEBYjDtcYj+AjBH3FLv5uSJuXg==", new Stylesheet(ThemePath() + "Theme.css"),
CrossOrigin = "anonymous" }, new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "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 },
}; };
} }

View File

@ -17,11 +17,9 @@ namespace Oqtane.Themes.OqtaneTheme
Resources = new List<Resource>() Resources = new List<Resource>()
{ {
// obtained from https://cdnjs.com/libraries // 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", new Stylesheet("https://cdnjs.cloudflare.com/ajax/libs/bootswatch/5.3.3/cyborg/bootstrap.min.css", "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", "anonymous"),
Integrity = "sha512-M+Wrv9LTvQe81gFD2ZE3xxPTN5V2n1iLCXsldIxXvfs6tP+6VihBCwCMBkkjkQUZVmEHBsowb9Vqsq1et1teEg==", new Stylesheet("~/Theme.css"),
CrossOrigin = "anonymous" }, new Script(Constants.BootstrapScriptUrl, Constants.BootstrapScriptIntegrity, "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 }
} }
}; };
} }

View File

@ -62,17 +62,17 @@ namespace Oqtane.Themes
var inline = 0; var inline = 0;
foreach (Resource resource in resources) 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)) if (!string.IsNullOrEmpty(resource.Url))
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : PageState.Alias.BaseUrl + 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 else
{ {
inline += 1; 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());
} }
} }
} }

View File

@ -117,12 +117,17 @@ namespace Oqtane.UI
} }
public Task IncludeScript(string id, string src, string integrity, string crossorigin, string type, string content, string location) 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<string, string> dataAttributes)
{ {
try try
{ {
_jsRuntime.InvokeVoidAsync( _jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.includeScript", "Oqtane.Interop.includeScript",
id, src, integrity, crossorigin, type, content, location); id, src, integrity, crossorigin, type, content, location, dataAttributes);
return Task.CompletedTask; return Task.CompletedTask;
} }
catch catch

View File

@ -176,7 +176,7 @@
if (!string.IsNullOrEmpty(src)) if (!string.IsNullOrEmpty(src))
{ {
src = (src.Contains("://")) ? src : PageState.Alias.BaseUrl + 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 else
{ {
@ -186,8 +186,8 @@
count += 1; count += 1;
id = $"page{PageState.Page.PageId}-script{count}"; id = $"page{PageState.Page.PageId}-script{count}";
} }
index = script.IndexOf(">") + 1; var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", "", script.Substring(index, script.IndexOf("</script>") - index), location.ToString().ToLower()); await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
} }
index = content.IndexOf("<script", index + 1); index = content.IndexOf("<script", index + 1);
} }

View File

@ -554,10 +554,21 @@
if (!resource.Reload) if (!resource.Reload)
{ {
var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
var dataAttributes = "";
if (resource.DataAttributes != null && resource.DataAttributes.Count > 0)
{
foreach (var attribute in resource.DataAttributes)
{
dataAttributes += " " + attribute.Key + "=\"" + attribute.Value + "\"";
}
}
return "<script src=\"" + url + "\"" + return "<script src=\"" + url + "\"" +
((resource.ES6Module) ? " type=\"module\"" : "") + ((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") + ((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") + ((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>"; "></script>";
} }
else else

View File

@ -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); 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; var script;
if (src !== "") { if (src !== "") {
script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]"); script = document.querySelector("script[src=\"" + CSS.escape(src) + "\"]");
} }
else { else {
if (id !== "") {
script = document.getElementById(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) { if (script !== null) {
script.remove(); script.remove();
@ -152,37 +161,36 @@ Oqtane.Interop = {
else { else {
script.innerHTML = content; script.innerHTML = content;
} }
script.async = false; if (dataAttributes !== null) {
this.addScript(script, location) for (var key in dataAttributes) {
.then(() => { script.setAttribute(key, dataAttributes[key]);
}
}
try {
this.addScript(script, location);
} catch (error) {
if (src !== "") { if (src !== "") {
console.log(src + ' loaded'); console.error("Failed to load external script: ${src}", error);
} else {
console.error("Failed to load inline script: ${content}", error);
} }
else {
console.log(id + ' loaded');
} }
})
.catch(() => {
if (src !== "") {
console.error(src + ' failed');
}
else {
console.error(id + ' failed');
}
});
} }
}, },
addScript: function (script, location) { addScript: function (script, location) {
return new Promise((resolve, reject) => {
script.async = false;
script.defer = false;
script.onload = () => resolve();
script.onerror = (error) => reject(error);
if (location === 'head') { if (location === 'head') {
document.head.appendChild(script); document.head.appendChild(script);
} } else {
if (location === 'body') {
document.body.appendChild(script); document.body.appendChild(script);
} }
return new Promise((res, rej) => {
script.onload = res();
script.onerror = rej();
}); });
}, },
includeScripts: async function (scripts) { includeScripts: async function (scripts) {
@ -222,10 +230,10 @@ Oqtane.Interop = {
if (scripts[s].crossorigin !== '') { if (scripts[s].crossorigin !== '') {
element.crossOrigin = scripts[s].crossorigin; element.crossOrigin = scripts[s].crossorigin;
} }
if (scripts[s].es6module === true) { if (scripts[s].type !== '') {
element.type = "module"; 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) { for (var key in scripts[s].dataAttributes) {
element.setAttribute(key, scripts[s].dataAttributes[key]); element.setAttribute(key, scripts[s].dataAttributes[key]);
} }

View File

@ -1,4 +1,6 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using Oqtane.Shared; using Oqtane.Shared;
namespace Oqtane.Models namespace Oqtane.Models
@ -27,6 +29,11 @@ namespace Oqtane.Models
} }
} }
/// <summary>
/// For Scripts this allows type to be specified - not applicable to Stylesheets
/// </summary>
public string Type { get; set; }
/// <summary> /// <summary>
/// Integrity checks to increase the security of resources accessed. Especially common in CDN resources. /// Integrity checks to increase the security of resources accessed. Especially common in CDN resources.
/// </summary> /// </summary>
@ -52,11 +59,6 @@ namespace Oqtane.Models
/// </summary> /// </summary>
public ResourceLocation Location { get; set; } public ResourceLocation Location { get; set; }
/// <summary>
/// For Scripts this allows type="module" registrations - not applicable to Stylesheets
/// </summary>
public bool ES6Module { get; set; }
/// <summary> /// <summary>
/// Allows specification of inline script - not applicable to Stylesheets /// Allows specification of inline script - not applicable to Stylesheets
/// </summary> /// </summary>
@ -72,6 +74,11 @@ namespace Oqtane.Models
/// </summary> /// </summary>
public bool Reload { get; set; } public bool Reload { get; set; }
/// <summary>
/// Cusotm data-* attributes for scripts - not applicable to Stylesheets
/// </summary>
public Dictionary<string, string> DataAttributes { get; set; }
/// <summary> /// <summary>
/// The namespace of the component that declared the resource - only used in SiteRouter /// The namespace of the component that declared the resource - only used in SiteRouter
/// </summary> /// </summary>
@ -82,14 +89,22 @@ namespace Oqtane.Models
var resource = new Resource(); var resource = new Resource();
resource.ResourceType = ResourceType; resource.ResourceType = ResourceType;
resource.Url = Url; resource.Url = Url;
resource.Type = Type;
resource.Integrity = Integrity; resource.Integrity = Integrity;
resource.CrossOrigin = CrossOrigin; resource.CrossOrigin = CrossOrigin;
resource.Bundle = Bundle; resource.Bundle = Bundle;
resource.Location = Location; resource.Location = Location;
resource.ES6Module = ES6Module;
resource.Content = Content; resource.Content = Content;
resource.RenderMode = RenderMode; resource.RenderMode = RenderMode;
resource.Reload = Reload; resource.Reload = Reload;
resource.DataAttributes = new Dictionary<string, string>();
if (DataAttributes != null && DataAttributes.Count > 0)
{
foreach (var kvp in DataAttributes)
{
resource.DataAttributes.Add(kvp.Key, kvp.Value);
}
}
resource.Level = level; resource.Level = level;
resource.Namespace = name; resource.Namespace = name;
return resource; return resource;
@ -97,5 +112,18 @@ namespace Oqtane.Models
[Obsolete("ResourceDeclaration is deprecated", false)] [Obsolete("ResourceDeclaration is deprecated", false)]
public ResourceDeclaration Declaration { get; set; } 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";
};
}
}
} }
} }

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
using Oqtane.Shared;
namespace Oqtane.Models
{
/// <summary>
/// Script inherits from Resource and offers constructors with parameters specific to Scripts
/// </summary>
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<string, string> 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;
}
}
}

View File

@ -0,0 +1,30 @@
using Oqtane.Shared;
namespace Oqtane.Models
{
/// <summary>
/// Stylesheet inherits from Resource and offers constructors with parameters specific to Stylesheets
/// </summary>
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;
}
}
}