diff --git a/Oqtane.Client/UI/Head.razor b/Oqtane.Client/UI/Head.razor index c3b0f667..5ab1bc26 100644 --- a/Oqtane.Client/UI/Head.razor +++ b/Oqtane.Client/UI/Head.razor @@ -70,7 +70,7 @@ if (!script.Contains("><") && !script.Contains("data-reload")) { // add data-reload attribute to inline script - headcontent = headcontent.Replace(script, script.Replace("") + 1; - await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("") - pos), location.ToString().ToLower(), dataAttributes); + count += 1; + id = $"page{PageState.Page.PageId}-script{count}"; } + var pos = script.IndexOf(">") + 1; + await interop.IncludeScript(id, "", "", "", type, script.Substring(pos, script.IndexOf("") - pos), location.ToString().ToLower(), dataAttributes); } index = content.IndexOf(""; + } + else { var url = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url; var dataAttributes = ""; + if (!resource.DataAttributes.ContainsKey("data-reload")) + { + switch (resource.LoadBehavior) + { + case ResourceLoadBehavior.Once: + dataAttributes += " data-reload=\"once\""; + break; + case ResourceLoadBehavior.Always: + dataAttributes += " data-reload=\"always\""; + break; + } + } if (resource.DataAttributes != null && resource.DataAttributes.Count > 0) { foreach (var attribute in resource.DataAttributes) @@ -552,10 +568,6 @@ ((!string.IsNullOrEmpty(dataAttributes)) ? dataAttributes : "") + ">"; } - else - { - return ""; - } } private void SetLocalizationCookie(string cookieValue) diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js index bb9ad076..66ac9a4c 100644 --- a/Oqtane.Server/wwwroot/js/reload.js +++ b/Oqtane.Server/wwwroot/js/reload.js @@ -1,67 +1,74 @@ -const scriptInfoBySrc = new Map(); +const scriptKeys = new Set(); + +export function onUpdate() { + // determine if this is an enhanced navigation + let enhancedNavigation = scriptKeys.size !== 0; + + // iterate over all script elements in document + const scripts = document.getElementsByTagName('script'); + for (const script of Array.from(scripts)) { + // only process scripts that include a data-reload attribute + if (script.hasAttribute('data-reload')) { + let key = getKey(script); + + if (enhancedNavigation) { + // reload the script if data-reload is always or if the script has not been loaded previously and data-reload is once + let dataReload = script.getAttribute('data-reload'); + if (dataReload === 'always' || (!scriptKeys.has(key) && dataReload == 'once')) { + reloadScript(script); + } + } + + // save the script key + if (!scriptKeys.has(key)) { + scriptKeys.add(key); + } + } + } +} function getKey(script) { - if (script.hasAttribute("src") && script.src !== "") { + if (script.src) { return script.src; + } else if (script.id) { + return script.id; } else { return script.innerHTML; } } -export function onUpdate() { - let timestamp = Date.now(); - let enhancedNavigation = scriptInfoBySrc.size !== 0; - - // iterate over all script elements in page - const scripts = document.getElementsByTagName("script"); - for (const script of Array.from(scripts)) { - let key = getKey(script); - let scriptInfo = scriptInfoBySrc.get(key); - if (!scriptInfo) { - // new script added - scriptInfo = { timestamp: timestamp }; - scriptInfoBySrc.set(key, scriptInfo); - if (enhancedNavigation) { - reloadScript(script); - } - } else { - // existing script - scriptInfo.timestamp = timestamp; - if (script.hasAttribute("data-reload") && script.getAttribute("data-reload") === "true") { - reloadScript(script); - } +function reloadScript(script) { + try { + if (isValid(script)) { + replaceScript(script); } - } - - // remove scripts that are no longer referenced - for (const [key, scriptInfo] of scriptInfoBySrc) { - if (scriptInfo.timestamp !== timestamp) { - scriptInfoBySrc.delete(key); + } catch (error) { + if (script.src) { + console.error(`Script Reload failed to load external script: ${script.src}`, error); + } else { + console.error(`Script Reload failed to load inline script: ${script.innerHTML}`, error); } } } -function reloadScript(script) { - try { - replaceScript(script); - } catch (error) { - if (script.hasAttribute("src") && script.src !== "") { - console.error("Failed to load external script: ${script.src}", error); - } else { - console.error("Failed to load inline script: ${script.innerHtml}", error); - } +function isValid(script) { + if (script.innerHTML.includes('document.write(')) { + console.log(`Script using document.write() not supported by Script Reload: ${script.innerHTML}`); + return false; } + return true; } function replaceScript(script) { return new Promise((resolve, reject) => { - var newScript = document.createElement("script"); + var newScript = document.createElement('script'); // replicate attributes and content for (let i = 0; i < script.attributes.length; i++) { newScript.setAttribute(script.attributes[i].name, script.attributes[i].value); } newScript.innerHTML = script.innerHTML; + newScript.removeAttribute('data-reload'); // dynamically injected scripts cannot be async or deferred newScript.async = false; @@ -70,11 +77,10 @@ function replaceScript(script) { newScript.onload = () => resolve(); newScript.onerror = (error) => reject(error); - // remove existing script + // remove existing script element script.remove(); - // replace with new script to force reload in Blazor + // replace with new script element to force reload in Blazor document.head.appendChild(newScript); }); -} - +} \ No newline at end of file diff --git a/Oqtane.Shared/Enums/ResourceLoadBehavior.cs b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs new file mode 100644 index 00000000..091a014f --- /dev/null +++ b/Oqtane.Shared/Enums/ResourceLoadBehavior.cs @@ -0,0 +1,10 @@ +namespace Oqtane.Shared +{ + public enum ResourceLoadBehavior + { + Once, + Always, + Never, + BlazorPageScript + } +} diff --git a/Oqtane.Shared/Models/Resource.cs b/Oqtane.Shared/Models/Resource.cs index 92b64bef..f3ee3349 100644 --- a/Oqtane.Shared/Models/Resource.cs +++ b/Oqtane.Shared/Models/Resource.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Linq; using Oqtane.Shared; namespace Oqtane.Models @@ -13,7 +12,7 @@ namespace Oqtane.Models private string _url; /// - /// A so the Interop can properly create `script` or `link` tags + /// A to define the type of resource ie. Script or Stylesheet /// public ResourceType ResourceType { get; set; } @@ -45,7 +44,7 @@ namespace Oqtane.Models public string CrossOrigin { get; set; } /// - /// For Scripts a Bundle can be used to identify dependencies and ordering in the script loading process + /// For Scripts a Bundle can be used to identify dependencies and ordering in the script loading process (for Interactive rendering only) /// public string Bundle { get; set; } @@ -60,7 +59,7 @@ namespace Oqtane.Models public ResourceLocation Location { get; set; } /// - /// Allows specification of inline script - not applicable to Stylesheets + /// For Scripts this allows for the specification of inline script - not applicable to Stylesheets /// public string Content { get; set; } @@ -70,9 +69,9 @@ namespace Oqtane.Models public string RenderMode { get; set; } /// - /// Indicates that a script should be reloaded on every page transition - not applicable to Stylesheets + /// Specifies how a script should be loaded in Static rendering - not applicable to Stylesheets /// - public bool Reload { get; set; } + public ResourceLoadBehavior LoadBehavior { get; set; } /// /// Cusotm data-* attributes for scripts - not applicable to Stylesheets @@ -96,7 +95,7 @@ namespace Oqtane.Models resource.Location = Location; resource.Content = Content; resource.RenderMode = RenderMode; - resource.Reload = Reload; + resource.LoadBehavior = LoadBehavior; resource.DataAttributes = new Dictionary(); if (DataAttributes != null && DataAttributes.Count > 0) { @@ -125,5 +124,18 @@ namespace Oqtane.Models }; } } + + [Obsolete("Reload is deprecated. Use LoadBehavior property instead for scripts.", false)] + public bool Reload + { + get => (LoadBehavior == ResourceLoadBehavior.BlazorPageScript); + set + { + if (value) + { + LoadBehavior = ResourceLoadBehavior.BlazorPageScript; + }; + } + } } } diff --git a/Oqtane.Shared/Models/Script.cs b/Oqtane.Shared/Models/Script.cs index de44a5ce..1fd5d612 100644 --- a/Oqtane.Shared/Models/Script.cs +++ b/Oqtane.Shared/Models/Script.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using Oqtane.Shared; @@ -27,6 +28,13 @@ namespace Oqtane.Models this.Type = Type; } + public Script(string Content, ResourceLoadBehavior LoadBehavior) + { + SetDefaults(); + this.Content = Content; + this.LoadBehavior = LoadBehavior; + } + public Script(string Src, string Integrity, string CrossOrigin) { SetDefaults(); @@ -35,6 +43,22 @@ namespace Oqtane.Models this.CrossOrigin = CrossOrigin; } + public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, ResourceLoadBehavior LoadBehavior, 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.LoadBehavior = LoadBehavior; + this.DataAttributes = DataAttributes; + this.RenderMode = RenderMode; + } + + [Obsolete("This constructor is deprecated. Use constructor with LoadBehavior parameter instead.", false)] public Script(string Src, string Integrity, string CrossOrigin, string Type, string Content, ResourceLocation Location, string Bundle, bool Reload, Dictionary DataAttributes, string RenderMode) { SetDefaults(); @@ -45,9 +69,10 @@ namespace Oqtane.Models this.Content = Content; this.Location = Location; this.Bundle = Bundle; - this.Reload = Reload; + this.LoadBehavior = (Reload) ? ResourceLoadBehavior.BlazorPageScript : ResourceLoadBehavior.Once; this.DataAttributes = DataAttributes; this.RenderMode = RenderMode; } + } }