diff --git a/Oqtane.Client/UI/Head.razor b/Oqtane.Client/UI/Head.razor
index 0d72ce98..c3b0f667 100644
--- a/Oqtane.Client/UI/Head.razor
+++ b/Oqtane.Client/UI/Head.razor
@@ -58,27 +58,22 @@
var index = headcontent.IndexOf("") + 9 - index);
+ var script = headcontent.Substring(index, headcontent.IndexOf("", index) - index + 9);
if (RenderMode == RenderModes.Interactive)
{
// remove scripts when interactive rendering
headcontent = headcontent.Remove(index, script.Length);
+ index = headcontent.IndexOf("";
}
else
{
- return "";
+ return "";
}
}
diff --git a/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js b/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js
index 41d385cb..508f42a0 100644
--- a/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js
+++ b/Oqtane.Server/wwwroot/Oqtane.Server.lib.module.js
@@ -1,194 +1,88 @@
const pageScriptInfoBySrc = new Map();
-function getKey(element) {
- if (element.src !== "") {
- return element.src;
- } else {
- return element.content;
+function registerPageScriptElement(src) {
+ if (!src) {
+ throw new Error('Must provide a non-empty value for the "src" attribute.');
}
-}
-function registerPageScript(element) {
- let key = getKey(element);
- let pageScriptInfo = pageScriptInfoBySrc.get(key);
+ let pageScriptInfo = pageScriptInfoBySrc.get(src);
if (pageScriptInfo) {
pageScriptInfo.referenceCount++;
} else {
- 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(key, pageScriptInfo);
+ pageScriptInfo = { referenceCount: 1, module: null };
+ pageScriptInfoBySrc.set(src, pageScriptInfo);
+ initializePageScriptModule(src, pageScriptInfo);
}
}
-async function initializePageScript(key, pageScriptInfo) {
- if (pageScriptInfo.reload) {
- const module = await import(pageScriptInfo.src);
- pageScriptInfo.module = module;
- module.onLoad?.();
- module.onUpdate?.();
- } else {
- if (!scriptExists(pageScriptInfo)) {
- try {
- injectScript(pageScriptInfo);
- } catch (error) {
- if (pageScriptInfo.src !== "") {
- console.error("Failed to load external script: ${pageScriptInfo.src}", error);
- } else {
- console.error("Failed to load inline script: ${pageScriptInfo.content}", error);
- }
- }
- }
- }
- removePageScript(key, pageScriptInfo);
-}
-
-function onEnhancedLoad() {
- for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
- if (pageScriptInfo.referenceCount <= 0) {
- if (pageScriptInfo.module) {
- pageScriptInfo.module.onDispose?.();
- }
- pageScriptInfoBySrc.delete(key);
- }
+function unregisterPageScriptElement(src) {
+ if (!src) {
+ return;
}
- for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
- if (pageScriptInfo.module) {
- pageScriptInfo.module.onUpdate?.();
- } else {
- if (!scriptExists(pageScriptInfo)) {
- try {
- injectScript(pageScriptInfo);
- } catch (error) {
- if (pageScriptInfo.src !== "") {
- console.error("Failed to load external script: ${pageScriptInfo.src}", error);
- } else {
- console.error("Failed to load inline script: ${pageScriptInfo.content}", error);
- }
- }
- }
- }
- }
-
- for (const [key, pageScriptInfo] of pageScriptInfoBySrc) {
- removePageScript(key, pageScriptInfo);
- }
-}
-
-function scriptExists(pageScriptInfo) {
- if (pageScriptInfo.src !== "") {
- return document.querySelector("script[src=\"" + pageScriptInfo.src + "\"]");
- } else {
- const scripts = document.querySelectorAll('script:not([src])');
- for (let i = 0; i < scripts.length; i++) {
- if (scripts[i].textContent.includes(pageScriptInfo.content)) {
- return true;
- }
- }
- return false;
- }
-}
-
-function injectScript(pageScriptInfo) {
- return new Promise((resolve, reject) => {
- var script = document.createElement("script");
-
- script.async = false;
- if (pageScriptInfo.type !== "") {
- script.type = pageScriptInfo.type;
- }
-
- if (pageScriptInfo.src !== "") {
- script.src = pageScriptInfo.src;
- if (pageScriptInfo.integrity !== "") {
- script.integrity = pageScriptInfo.integrity;
- }
- if (pageScriptInfo.crossorigin !== "") {
- script.crossOrigin = pageScriptInfo.crossorigin;
- }
- } else {
- script.innerHTML = pageScriptInfo.content;
- }
-
- script.onload = () => resolve();
- script.onerror = (error) => reject(error);
-
- // add script to page
- document.head.appendChild(script);
- });
-}
-
-function removePageScript(key, pageScriptInfo) {
- var pageScript;
-
- if (pageScriptInfo.src !== "") {
- pageScript = document.querySelector("page-script[src=\"" + key + "\"]");
- } else {
- pageScript = document.querySelector("page-script[content=\"" + CSS.escape(pageScriptInfo.content) + "\"]");
- }
-
- if (pageScript) {
- pageScript.remove();
- }
-}
-
-function unregisterPageScript(element) {
- const pageScriptInfo = pageScriptInfoBySrc.get(getKey(element));
+ const pageScriptInfo = pageScriptInfoBySrc.get(src);
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();
+ }
+
+ 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);
+ }
+ }
+
+ // Then invoke 'onUpdate' on the remaining modules.
+ for (const { module } of pageScriptInfoBySrc.values()) {
+ module?.onUpdate?.();
+ }
+}
+
export function afterWebStarted(blazor) {
customElements.define('page-script', class extends HTMLElement {
- static observedAttributes = ['src', 'type', 'integrity', 'crossorigin', 'content', 'reload'];
-
- constructor() {
- super();
-
- this.src = "";
- this.type = "";
- this.integrity = "";
- this.crossorigin = "";
- this.content = "";
- this.reload = false;
- }
+ static observedAttributes = ['src'];
+ // We use attributeChangedCallback instead of connectedCallback
+ // because a page-script element might get reused between enhanced
+ // navigations.
attributeChangedCallback(name, oldValue, newValue) {
- 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 "reload":
- this.reload = newValue;
- break;
+ if (name !== 'src') {
+ return;
}
- // if last attribute for element has been processed
- if (this.attributes[this.attributes.length - 1].name === name) {
- registerPageScript(this);
- }
+ this.src = newValue;
+ unregisterPageScriptElement(oldValue);
+ registerPageScriptElement(newValue);
}
disconnectedCallback() {
- unregisterPageScript(this);
+ unregisterPageScriptElement(this.src);
}
});
diff --git a/Oqtane.Server/wwwroot/js/reload.js b/Oqtane.Server/wwwroot/js/reload.js
new file mode 100644
index 00000000..b30e8f9a
--- /dev/null
+++ b/Oqtane.Server/wwwroot/js/reload.js
@@ -0,0 +1,87 @@
+const scriptInfoBySrc = new Map();
+
+function getKey(script) {
+ if (script.hasAttribute("src") && script.src !== "") {
+ return script.src;
+ } 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 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);
+ }
+ }
+ }
+
+ // remove scripts that are no longer referenced
+ for (const [key, scriptInfo] of scriptInfoBySrc) {
+ if (scriptInfo.timestamp !== timestamp) {
+ scriptInfoBySrc.delete(key);
+ }
+ }
+}
+
+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 replaceScript(script) {
+ return new Promise((resolve, reject) => {
+ var newScript = document.createElement("script");
+
+ newScript.async = false;
+ if (script.type !== "") {
+ newScript.type = script.type;
+ }
+
+ if (script.src !== "") {
+ newScript.src = script.src;
+ if (script.integrity !== "") {
+ newScript.integrity = script.integrity;
+ }
+ if (script.crossorigin !== "") {
+ newScript.crossOrigin = script.crossOrigin;
+ }
+ } else {
+ newScript.innerHTML = script.innerHTML;
+ }
+
+ newScript.onload = () => resolve();
+ newScript.onerror = (error) => reject(error);
+
+ // remove existing newScript script
+ script.remove();
+
+ // replace with new newScript to force reload in Blazor
+ document.head.appendChild(newScript);
+ });
+}
+