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;
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());
}
}
}

View File

@ -37,11 +37,8 @@
public override List<Resource> Resources => new List<Resource>()
{
// 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")
};
}

View File

@ -17,11 +17,9 @@ namespace Oqtane.Themes.OqtaneTheme
Resources = new List<Resource>()
{
// 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")
}
};
}

View File

@ -62,17 +62,17 @@ namespace Oqtane.Themes
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());
}
}
}

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)
{
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
{
_jsRuntime.InvokeVoidAsync(
"Oqtane.Interop.includeScript",
id, src, integrity, crossorigin, type, content, location);
id, src, integrity, crossorigin, type, content, location, dataAttributes);
return Task.CompletedTask;
}
catch

View File

@ -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("</script>") - index), location.ToString().ToLower());
var pos = script.IndexOf(">") + 1;
await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("</script>") - pos), location.ToString().ToLower(), dataAttributes);
}
index = content.IndexOf("<script", index + 1);
}

View File

@ -554,11 +554,22 @@
if (!resource.Reload)
{
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 + "\"" +
((resource.ES6Module) ? " type=\"module\"" : "") +
((!string.IsNullOrEmpty(resource.Type)) ? " type=\"" + resource.Type + "\"" : "") +
((!string.IsNullOrEmpty(resource.Integrity)) ? " integrity=\"" + resource.Integrity + "\"" : "") +
((!string.IsNullOrEmpty(resource.CrossOrigin)) ? " crossorigin=\"" + resource.CrossOrigin + "\"" : "") +
"></script>";
((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") +
"></script>";
}
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);
}
},
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;
return new Promise((res, rej) => {
script.onload = res();
script.onerror = rej();
script.onload = () => resolve();
script.onerror = (error) => reject(error);
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]);
}

View File

@ -1,4 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Oqtane.Shared;
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>
/// Integrity checks to increase the security of resources accessed. Especially common in CDN resources.
/// </summary>
@ -52,11 +59,6 @@ namespace Oqtane.Models
/// </summary>
public ResourceLocation Location { get; set; }
/// <summary>
/// For Scripts this allows type="module" registrations - not applicable to Stylesheets
/// </summary>
public bool ES6Module { get; set; }
/// <summary>
/// Allows specification of inline script - not applicable to Stylesheets
/// </summary>
@ -72,6 +74,11 @@ namespace Oqtane.Models
/// </summary>
public bool Reload { get; set; }
/// <summary>
/// Cusotm data-* attributes for scripts - not applicable to Stylesheets
/// </summary>
public Dictionary<string, string> DataAttributes { get; set; }
/// <summary>
/// The namespace of the component that declared the resource - only used in SiteRouter
/// </summary>
@ -82,14 +89,22 @@ namespace Oqtane.Models
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<string, string>();
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 @@ namespace Oqtane.Models
[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";
};
}
}
}
}

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;
}
}
}