diff --git a/Oqtane.Client/UI/Head.razor b/Oqtane.Client/UI/Head.razor
index fa06fc0e..1b030789 100644
--- a/Oqtane.Client/UI/Head.razor
+++ b/Oqtane.Client/UI/Head.razor
@@ -41,7 +41,7 @@
}
break;
case "HeadContent":
- var content = RemoveScripts(SiteState.Properties.HeadContent) + "\n";
+ var content = FormatScripts(SiteState.Properties.HeadContent) + "\n";
if (content != _content)
{
_content = content;
@@ -51,14 +51,30 @@
}
}
- private string RemoveScripts(string headcontent)
+ private string FormatScripts(string headcontent)
{
- if (!string.IsNullOrEmpty(headcontent) && RenderMode == RenderModes.Interactive)
+ if (!string.IsNullOrEmpty(headcontent))
{
var index = headcontent.IndexOf("") + 9 - index);
+ var script = headcontent.Substring(index, headcontent.IndexOf("") + 9 - index);
+ if (RenderMode == RenderModes.Interactive)
+ {
+ // remove scripts when interactive rendering
+ headcontent = headcontent.Remove(index, script.Length);
+ }
+ else
+ {
+ // transform scripts into page-scripts when static rendering
+ var pagescript = script.Replace("script", "page-script");
+ if (!pagescript.Contains("><"))
+ {
+ var content = pagescript.Substring(pagescript.IndexOf(">") + 1, pagescript.LastIndexOf("<") - pagescript.IndexOf(">") - 1);
+ pagescript = pagescript.Replace(">" + content, " content=\"" + content.Replace("\"","'") + "\">");
+ }
+ headcontent = headcontent.Replace(script, pagescript);
+ }
index = headcontent.IndexOf(""; // src at end of element due to enhanced navigation patch algorithm
- }
- else
- {
- // use custom element which can execute script on every page transition
- @if (string.IsNullOrEmpty(resource.Integrity) && string.IsNullOrEmpty(resource.CrossOrigin))
- {
- return "";
- }
- else
- {
- // use modulepreload for external resources
- return "\n" +
- "";
- }
- }
- }
- else
- {
- // inline script
- return "";
+ src = (resource.Url.Contains("://")) ? resource.Url : alias.BaseUrl + resource.Url;
}
+
+ return "";
}
private void SetLocalizationCookie(string cookieValue)
diff --git a/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js b/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js
index 508f42a0..25d1e4b8 100644
--- a/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js
+++ b/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js
@@ -1,88 +1,170 @@
const pageScriptInfoBySrc = new Map();
-function registerPageScriptElement(src) {
- if (!src) {
- throw new Error('Must provide a non-empty value for the "src" attribute.');
+function getKey(element) {
+ if (element.src !== "") {
+ return element.src;
+ } else {
+ return element.content;
}
+}
- let pageScriptInfo = pageScriptInfoBySrc.get(src);
+function registerPageScriptElement(element) {
+ let key = getKey(element);
+ let pageScriptInfo = pageScriptInfoBySrc.get(key);
if (pageScriptInfo) {
pageScriptInfo.referenceCount++;
} else {
- pageScriptInfo = { referenceCount: 1, module: null };
- pageScriptInfoBySrc.set(src, pageScriptInfo);
- initializePageScriptModule(src, pageScriptInfo);
+ if (element.src.startsWith("./")) {
+ element.src = new URL(element.src.substr(2), document.baseURI).toString();
+ }
+ pageScriptInfo = { src: element.src, type: element.type, integrity: element.integrity, crossorigin: element.crossorigin, content: element.content, location: element.location, reload: element.reload, module: null, referenceCount: 1 };
+ pageScriptInfoBySrc.set(key, pageScriptInfo);
+ initializePageScript(pageScriptInfo);
}
}
-function unregisterPageScriptElement(src) {
- if (!src) {
- return;
- }
-
- const pageScriptInfo = pageScriptInfoBySrc.get(src);
+function unregisterPageScriptElement(element) {
+ const pageScriptInfo = pageScriptInfoBySrc.get(getKey(element));
if (!pageScriptInfo) {
return;
}
-
pageScriptInfo.referenceCount--;
}
-async function initializePageScriptModule(src, pageScriptInfo) {
- // If the path is relative, normalize it by by making it an absolute URL
- // with document's the base HREF.
- if (src.startsWith("./")) {
- src = new URL(src.substr(2), document.baseURI).toString();
+async function initializePageScript(pageScriptInfo) {
+ if (pageScriptInfo.reload) {
+ const module = await import(pageScriptInfo.src);
+ pageScriptInfo.module = module;
+ module.onLoad?.();
+ module.onUpdate?.();
+ } else {
+ try {
+ injectScript(pageScriptInfo);
+ } catch (error) {
+ console.error("Failed to load script: ${pageScriptInfo.src}", error);
+ }
}
-
- const module = await import(src);
-
- if (pageScriptInfo.referenceCount <= 0) {
- // All page-script elements with the same 'src' were
- // unregistered while we were loading the module.
- return;
- }
-
- pageScriptInfo.module = module;
- module.onLoad?.();
- module.onUpdate?.();
}
function onEnhancedLoad() {
- // Start by invoking 'onDispose' on any modules that are no longer referenced.
- for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
- if (referenceCount <= 0) {
- module?.onDispose?.();
- pageScriptInfoBySrc.delete(src);
+ for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
+ if (pageScriptInfo.referenceCount <= 0) {
+ if (pageScriptInfo.module) {
+ pageScriptInfo.module.onDispose?.();
+ }
+ pageScriptInfoBySrc.delete(key);
}
}
- // Then invoke 'onUpdate' on the remaining modules.
- for (const { module } of pageScriptInfoBySrc.values()) {
- module?.onUpdate?.();
+ for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
+ if (pageScriptInfo.module) {
+ pageScriptInfo.module.onUpdate?.();
+ } else {
+ try {
+ injectScript(pageScriptInfo);
+ } catch (error) {
+ if (pageScriptInfo.src !== "") {
+ console.error("Failed to load script library: ${pageScriptInfo.src}", error);
+ } else {
+ console.error("Failed to load inline script: ${pageScriptInfo.content}", error);
+ }
+ }
+ }
}
}
+function injectScript(pageScriptInfo) {
+ return new Promise((resolve, reject) => {
+ var pageScript;
+ var script = document.createElement("script");
+ script.async = false;
+
+ if (pageScriptInfo.src !== "") {
+ pageScript = document.querySelector("page-script[src=\"" + pageScriptInfo.src + "\"]");
+ script.src = pageScriptInfo.src;
+ if (pageScriptInfo.type !== "") {
+ script.type = pageScriptInfo.type;
+ }
+ if (pageScriptInfo.integrity !== "") {
+ script.integrity = pageScriptInfo.integrity;
+ }
+ if (pageScriptInfo.crossorigin !== "") {
+ script.crossOrigin = pageScriptInfo.crossorigin;
+ }
+ } else {
+ pageScript = document.querySelector("page-script[content=\"" + CSS.escape(pageScriptInfo.content) + "\"]");
+ script.innerHTML = pageScriptInfo.content;
+ }
+
+ script.onload = () => resolve();
+ script.onerror = (error) => reject(error);
+
+ // add script to page
+ if (pageScriptInfo.location === "head") {
+ document.head.appendChild(script);
+ } else {
+ document.body.appendChild(script);
+ // note this throws an exception when page-script is on a page which has interactive components (ie Counter.razor)
+ // Error: Uncaught (in promise) TypeError: can't access property "attributes" of null
+ // this seems to be related to Blazor-Server-Component-State which is also injected at the end of the body
+ }
+
+ // remove page-script element from page
+ if (pageScript !== null) {
+ pageScript.remove();
+ }
+ });
+}
+
export function afterWebStarted(blazor) {
customElements.define('page-script', class extends HTMLElement {
- static observedAttributes = ['src'];
+ static observedAttributes = ['src', 'type', 'integrity', 'crossorigin', 'content', 'location', 'reload'];
+
+ constructor() {
+ super();
+
+ this.src = "";
+ this.type = "";
+ this.integrity = "";
+ this.crossorigin = "";
+ this.content = "";
+ this.location = "head";
+ this.reload = false;
+ }
- // We use attributeChangedCallback instead of connectedCallback
- // because a page-script element might get reused between enhanced
- // navigations.
attributeChangedCallback(name, oldValue, newValue) {
- if (name !== 'src') {
- return;
+ switch (name) {
+ case "src":
+ this.src = newValue;
+ break;
+ case "type":
+ this.type = newValue;
+ break;
+ case "integrity":
+ this.integrity = newValue;
+ break;
+ case "crossorigin":
+ this.crossorigin = newValue;
+ break;
+ case "content":
+ this.content = newValue;
+ break;
+ case "location":
+ this.location = newValue;
+ break;
+ case "reload":
+ this.reload = newValue;
+ break;
}
+ }
- this.src = newValue;
- unregisterPageScriptElement(oldValue);
- registerPageScriptElement(newValue);
+ connectedCallback() {
+ registerPageScriptElement(this);
}
disconnectedCallback() {
- unregisterPageScriptElement(this.src);
+ unregisterPageScriptElement(this);
}
});