From 710ad7b5d6538ca2531e7aba5312efd247279a56 Mon Sep 17 00:00:00 2001 From: Julian Knight <1591850+TotallyInformation@users.noreply.github.com> Date: Thu, 23 Nov 2023 17:00:36 +0000 Subject: [PATCH 01/78] Fix for #232 --- CHANGELOG.md | 13 ++++++++++++- front-end/utils/uibrouter.esm.js | 3 +-- front-end/utils/uibrouter.esm.min.js | 4 ++-- front-end/utils/uibrouter.esm.min.js.map | 4 ++-- front-end/utils/uibrouter.iife.js | 3 +-- front-end/utils/uibrouter.iife.min.js | 4 ++-- front-end/utils/uibrouter.iife.min.js.map | 4 ++-- src/front-end-module/uibrouter.js | 2 +- 8 files changed, 23 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e917257..80d755af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,11 +6,22 @@ typora-root-url: docs/images Please see the documentation for archived changelogs - a new archive is produced for each major version. Check the [roadmap](./docs/roadmap.md) for future developments. +## To Fix + + + ------------ ## [Unreleased](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v6.7.0...main) -Nothing currently. + + +### `uibrouter` + +* [Issue #232](https://github.com/TotallyInformation/node-red-contrib-uibuilder/issues/232) - Ensure origin script is removed after re-applying to ensure only 1 instance shows instead of 2. + + + ## [v6.7.0](https://github.com/TotallyInformation/node-red-contrib-uibuilder/compare/v6.6.0...v6.7.0) diff --git a/front-end/utils/uibrouter.esm.js b/front-end/utils/uibrouter.esm.js index 06cfcf95..675b01ba 100644 --- a/front-end/utils/uibrouter.esm.js +++ b/front-end/utils/uibrouter.esm.js @@ -157,11 +157,10 @@ var UibRouter = class { _applyScripts(tempContainer) { const scripts = tempContainer.querySelectorAll("script"); scripts.forEach((scr) => { - if (scr.parentElement) - scr.parentElement.removeChild(scr); const newScript = document.createElement("script"); newScript.textContent = scr.innerText; tempContainer.append(newScript); + scr.remove(); }); } //#endregion --- ----- -- diff --git a/front-end/utils/uibrouter.esm.min.js b/front-end/utils/uibrouter.esm.min.js index 464a9143..d5271010 100644 --- a/front-end/utils/uibrouter.esm.min.js +++ b/front-end/utils/uibrouter.esm.min.js @@ -1,3 +1,3 @@ -var E=Object.defineProperty;var b=(n,e,t)=>e in n?E(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var u=(n,e,t)=>(b(n,typeof e!="symbol"?e+"":e,t),t),h=(n,e,t)=>{if(!e.has(n))throw TypeError("Cannot "+t)};var f=(n,e,t)=>(h(n,e,"read from private field"),t?t.call(n):e.get(n)),p=(n,e,t)=>{if(e.has(n))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(n):e.set(n,t)},m=(n,e,t,r)=>(h(n,e,"write to private field"),r?r.call(n,t):e.set(n,t),t);var d,s=class{constructor(e){u(this,"config");u(this,"routeContainerEl");u(this,"currentRouteId");u(this,"previousRouteId");p(this,d,!1);if(!fetch)throw new Error("[uibrouter:constructor] UibRouter requires `fetch`. Please use a current browser or load a fetch polyfill.");if(!e)throw new Error("[uibrouter:constructor] No config provided");if(!e.routes)throw new Error("[uibrouter:constructor] No routes provided in routerConfig");e.routeContainer||(e.routeContainer="#uibroutecontainer"),this.config=e,this._setRouteContainer(),Promise.all(Object.values(e.routes).filter(t=>t.type==="url").map(this._loadExternal)).then(this._appendExternalTemplates).then(()=>{this._start()})}_setRouteContainer(){let e=document.getElementsByTagName("body")[0],t=this.routeContainerEl=document.querySelector(this.config.routeContainer);if(!t){let r=document.createElement("div");r.setAttribute("id",this.config.routeContainer.replace("#","")),e.append(r),t=this.routeContainerEl=document.querySelector(this.config.routeContainer)}}_appendExternalTemplates(e){let t=document.getElementsByTagName("head")[0];e.forEach(r=>{Array.isArray(r)?console.error(...r):t.append(r)})}_start(){f(this,d)!==!0&&(window.addEventListener("hashchange",e=>this._hashChange(e)),this.doRoute(),document.dispatchEvent(new CustomEvent("uibrouter:loaded")),uibuilder&&uibuilder.set("uibrouter","loaded"),m(this,d,!0))}_hashChange(e){this.doRoute(e)}_createRouteContent(e){let t=document.querySelector(`#${e}`);if(!t)return console.error(`[uibrouter:createRouteContent] No route template found for route selector '#${e}'. Does the link url match a defined route id?`),!1;let r=t.content.cloneNode(!0);this.isRouteExternal(e)&&this._applyScripts(r);let o=document.createElement("div");o.dataset.route=e,o.append(r);try{this.routeContainerEl.append(o)}catch(i){return console.error(`[uibrouter:createRouteContent] Failed to apply route id '${e}'. - ${i.message}`),!1}return!0}_loadExternal(e){let t=e.id;return fetch(e.src).then(r=>r.ok===!1?[e.id,e.src,r.status,r.statusText]:r.text()).then(r=>{if(Array.isArray(r))return r;try{let i=document.querySelector(`#${t}`);i&&i.remove()}catch{}let o=document.createElement("template");return o.innerHTML=r,o.setAttribute("id",t),o}).catch(r=>{console.error(`[uibrouter:loadHTML] Error loading route template HTML from ${e.src}:`,r)})}_applyScripts(e){e.querySelectorAll("script").forEach(r=>{r.parentElement&&r.parentElement.removeChild(r);let o=document.createElement("script");o.textContent=r.innerText,e.append(o)})}doRoute(e){let t=this.routeContainerEl;if(!t)throw new Error("[uibrouter:doRoute] Cannot route, has router.setup() been called yet?");let r=this.keepHashFromUrl(window.location.hash);e||(e=r);let o,i;if(typeof e=="string"){if(o=this.keepHashFromUrl(e),i=r,o===""&&this.config.defaultRoute&&(o=this.config.defaultRoute),o!==r){window.location.hash=`#${o}`;return}}else if(e.type==="hashchange"){let a=e.newURL;if(a.includes("#"))i=this.keepHashFromUrl(e.oldURL),o=this.keepHashFromUrl(a);else return}else{i=r;try{o=this.keepHashFromUrl(e.target.attributes.href.value)}catch{throw new Error("[uibrouter:doRoute] No valid route found. Event.target does not have an href attribute")}}let l=!1;if(!o||!this.routeList().includes(o))throw document.dispatchEvent(new CustomEvent("uibrouter:route-change-failed",{detail:{newRouteId:o,oldRouteId:i}})),uibuilder&&uibuilder.set("uibrouter","route change failed"),window.location.hash=i?`#${i}`:"",new Error(`[uibrouter:doRoute] No valid route found. Either pass a valid route name or an event from an element having an href of '#routename'. Route id requested: '${o}'`);if(this.config.hide){if(i){let c=document.querySelector(`div[data-route="${i}"]`);c&&(c.style.display="none")}let a=document.querySelector(`div[data-route="${o}"]`);a?(a.style.removeProperty("display"),l=!0):l=this._createRouteContent(o)}else t.replaceChildren(),l=this._createRouteContent(o);if(l===!1){document.dispatchEvent(new CustomEvent("uibrouter:route-change-failed",{detail:{newRouteId:o,oldRouteId:i}})),uibuilder&&uibuilder.set("uibrouter","route change failed"),window.location.hash=i?`#${i}`:"";return}this.currentRouteId=o,this.previousRouteId=i,t.dataset.currentRoute=o,document.dispatchEvent(new CustomEvent("uibrouter:route-changed",{detail:{newRouteId:o,oldRouteId:i}})),uibuilder&&uibuilder.set("uibrouter","route changed")}getRouteConfigById(e){return Object.values(this.config.routes).filter(t=>t.id===e)[0]}isRouteExternal(e){let t=this.getRouteConfigById(e);return!!(t&&t.type==="url")}defaultRoute(){this.config.defaultRoute&&this.doRoute(this.config.defaultRoute)}removeHash(){history.pushState("",document.title,window.location.pathname+window.location.search)}noRoute(){this.removeHash(),this.routeContainerEl.replaceChildren()}keepHashFromUrl(e){return e?e.replace(/^.*#(.*)/,"$1").replace(/\?.*$/,""):""}routeList(e){return Object.values(this.config.routes).map(t=>e===!0?`#${t.id}`:t.id)}addRoutes(e){Array.isArray(e)||(e=[e]),Promise.all(Object.values(e).filter(t=>t.type==="url").map(this._loadExternal)).then(this._appendExternalTemplates).then(()=>{this.config.routes.push(...e),document.dispatchEvent(new CustomEvent("uibrouter:routes-added",{detail:e})),uibuilder&&uibuilder.set("uibrouter","routes added")})}};d=new WeakMap,u(s,"version","1.0.0");var y=s;window.UibRouter?console.warn("`UibRouter` already assigned to window. Have you tried to load it more than once?"):window.UibRouter=s;export{s as UibRouter,y as default}; +var b=Object.defineProperty;var w=(n,e,t)=>e in n?b(n,e,{enumerable:!0,configurable:!0,writable:!0,value:t}):n[e]=t;var u=(n,e,t)=>(w(n,typeof e!="symbol"?e+"":e,t),t),h=(n,e,t)=>{if(!e.has(n))throw TypeError("Cannot "+t)};var f=(n,e,t)=>(h(n,e,"read from private field"),t?t.call(n):e.get(n)),p=(n,e,t)=>{if(e.has(n))throw TypeError("Cannot add the same private member more than once");e instanceof WeakSet?e.add(n):e.set(n,t)},m=(n,e,t,r)=>(h(n,e,"write to private field"),r?r.call(n,t):e.set(n,t),t);var d,s=class{constructor(e){u(this,"config");u(this,"routeContainerEl");u(this,"currentRouteId");u(this,"previousRouteId");p(this,d,!1);if(!fetch)throw new Error("[uibrouter:constructor] UibRouter requires `fetch`. Please use a current browser or load a fetch polyfill.");if(!e)throw new Error("[uibrouter:constructor] No config provided");if(!e.routes)throw new Error("[uibrouter:constructor] No routes provided in routerConfig");e.routeContainer||(e.routeContainer="#uibroutecontainer"),this.config=e,this._setRouteContainer(),Promise.all(Object.values(e.routes).filter(t=>t.type==="url").map(this._loadExternal)).then(this._appendExternalTemplates).then(()=>{this._start()})}_setRouteContainer(){let e=document.getElementsByTagName("body")[0],t=this.routeContainerEl=document.querySelector(this.config.routeContainer);if(!t){let r=document.createElement("div");r.setAttribute("id",this.config.routeContainer.replace("#","")),e.append(r),t=this.routeContainerEl=document.querySelector(this.config.routeContainer)}}_appendExternalTemplates(e){let t=document.getElementsByTagName("head")[0];e.forEach(r=>{Array.isArray(r)?console.error(...r):t.append(r)})}_start(){f(this,d)!==!0&&(window.addEventListener("hashchange",e=>this._hashChange(e)),this.doRoute(),document.dispatchEvent(new CustomEvent("uibrouter:loaded")),uibuilder&&uibuilder.set("uibrouter","loaded"),m(this,d,!0))}_hashChange(e){this.doRoute(e)}_createRouteContent(e){let t=document.querySelector(`#${e}`);if(!t)return console.error(`[uibrouter:createRouteContent] No route template found for route selector '#${e}'. Does the link url match a defined route id?`),!1;let r=t.content.cloneNode(!0);this.isRouteExternal(e)&&this._applyScripts(r);let o=document.createElement("div");o.dataset.route=e,o.append(r);try{this.routeContainerEl.append(o)}catch(i){return console.error(`[uibrouter:createRouteContent] Failed to apply route id '${e}'. + ${i.message}`),!1}return!0}_loadExternal(e){let t=e.id;return fetch(e.src).then(r=>r.ok===!1?[e.id,e.src,r.status,r.statusText]:r.text()).then(r=>{if(Array.isArray(r))return r;try{let i=document.querySelector(`#${t}`);i&&i.remove()}catch{}let o=document.createElement("template");return o.innerHTML=r,o.setAttribute("id",t),o}).catch(r=>{console.error(`[uibrouter:loadHTML] Error loading route template HTML from ${e.src}:`,r)})}_applyScripts(e){e.querySelectorAll("script").forEach(r=>{let o=document.createElement("script");o.textContent=r.innerText,e.append(o),r.remove()})}doRoute(e){let t=this.routeContainerEl;if(!t)throw new Error("[uibrouter:doRoute] Cannot route, has router.setup() been called yet?");let r=this.keepHashFromUrl(window.location.hash);e||(e=r);let o,i;if(typeof e=="string"){if(o=this.keepHashFromUrl(e),i=r,o===""&&this.config.defaultRoute&&(o=this.config.defaultRoute),o!==r){window.location.hash=`#${o}`;return}}else if(e.type==="hashchange"){let a=e.newURL;if(a.includes("#"))i=this.keepHashFromUrl(e.oldURL),o=this.keepHashFromUrl(a);else return}else{i=r;try{o=this.keepHashFromUrl(e.target.attributes.href.value)}catch{throw new Error("[uibrouter:doRoute] No valid route found. Event.target does not have an href attribute")}}let l=!1;if(!o||!this.routeList().includes(o))throw document.dispatchEvent(new CustomEvent("uibrouter:route-change-failed",{detail:{newRouteId:o,oldRouteId:i}})),uibuilder&&uibuilder.set("uibrouter","route change failed"),window.location.hash=i?`#${i}`:"",new Error(`[uibrouter:doRoute] No valid route found. Either pass a valid route name or an event from an element having an href of '#routename'. Route id requested: '${o}'`);if(this.config.hide){if(i){let c=document.querySelector(`div[data-route="${i}"]`);c&&(c.style.display="none")}let a=document.querySelector(`div[data-route="${o}"]`);a?(a.style.removeProperty("display"),l=!0):l=this._createRouteContent(o)}else t.replaceChildren(),l=this._createRouteContent(o);if(l===!1){document.dispatchEvent(new CustomEvent("uibrouter:route-change-failed",{detail:{newRouteId:o,oldRouteId:i}})),uibuilder&&uibuilder.set("uibrouter","route change failed"),window.location.hash=i?`#${i}`:"";return}this.currentRouteId=o,this.previousRouteId=i,t.dataset.currentRoute=o,document.dispatchEvent(new CustomEvent("uibrouter:route-changed",{detail:{newRouteId:o,oldRouteId:i}})),uibuilder&&uibuilder.set("uibrouter","route changed")}getRouteConfigById(e){return Object.values(this.config.routes).filter(t=>t.id===e)[0]}isRouteExternal(e){let t=this.getRouteConfigById(e);return!!(t&&t.type==="url")}defaultRoute(){this.config.defaultRoute&&this.doRoute(this.config.defaultRoute)}removeHash(){history.pushState("",document.title,window.location.pathname+window.location.search)}noRoute(){this.removeHash(),this.routeContainerEl.replaceChildren()}keepHashFromUrl(e){return e?e.replace(/^.*#(.*)/,"$1").replace(/\?.*$/,""):""}routeList(e){return Object.values(this.config.routes).map(t=>e===!0?`#${t.id}`:t.id)}addRoutes(e){Array.isArray(e)||(e=[e]),Promise.all(Object.values(e).filter(t=>t.type==="url").map(this._loadExternal)).then(this._appendExternalTemplates).then(()=>{this.config.routes.push(...e),document.dispatchEvent(new CustomEvent("uibrouter:routes-added",{detail:e})),uibuilder&&uibuilder.set("uibrouter","routes added")})}};d=new WeakMap,u(s,"version","1.0.0");var y=s;window.UibRouter?console.warn("`UibRouter` already assigned to window. Have you tried to load it more than once?"):window.UibRouter=s;export{s as UibRouter,y as default}; //# sourceMappingURL=uibrouter.esm.min.js.map diff --git a/front-end/utils/uibrouter.esm.min.js.map b/front-end/utils/uibrouter.esm.min.js.map index ba09ff5e..d5501547 100644 --- a/front-end/utils/uibrouter.esm.min.js.map +++ b/front-end/utils/uibrouter.esm.min.js.map @@ -1,7 +1,7 @@ { "version": 3, "sources": ["src/front-end-module/uibrouter.js"], - "sourcesContent": ["// @ts-nocheck\n/** A simple, vanilla JavaScript front-end router class\n * Included in node-red-contrib-uibuilder but is not dependent on it.\n * May be used in other contexts as desired.\n * \n * Copyright (c) 2023-2023 Julian Knight (Totally Information)\n * https://it.knightnet.org.uk\n *\n * Licensed under the Apache License, Version 2.0 (the 'License');\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an 'AS IS' BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/** ---------------------------------------------------------------------\n * TODO\n * Methods needed:\n * Delete route\n * Update/reload route\n * shutdown - that removes all elements\n * delete templates - unloads a list of (or all) templates\n * reload templates - to facilitate updates of a list of (or all) templates\n * Additional options:\n * unload templates after they are added to the route container. Only if hide=true. `unload: true`\n * Maybe: options to auto-load js and css files with the same name as a template file.\n * --------------------------------------------------------------------- */\n\n/** Type definitions\n * routeConfig\n * @typedef {object} routeDefinition Single route configuration\n * @property {string} id REQUIRED. Route ID\n * @property {string} src REQUIRED. CSS Selector for template tag routes, url for external routes\n * @property {\"url\"|undefined} [type] OPTIONAL. \"url\" for external routes\n * UibRouterConfig\n * @typedef {object} UibRouterConfig Configuration for the UiBRouter class instances\n * @property {routeDefinition[]} routes REQUIRED. Array of route definitions\n * @property {string} [defaultRoute] OPTIONAL. If set to a route id, that route will be automatically shown on load\n * @property {string} [routeContainer] OPTIONAL. CSS Selector for an HTML Element containing routes\n * @property {boolean} [hide] OPTIONAL. If TRUE, routes will be hidden/shown on change instead of removed/added\n * @property {boolean} [unload] OPTIONAL. If TRUE, route templates will be unloaded from DOM after access. Only useful with the `hide` option\n */\n\nclass UibRouter { // eslint-disable-line no-unused-vars\n //#region --- Variables ---\n /** Class version */\n static version = '1.0.0'\n\n /** Configuration settings @type {UibRouterConfig} */\n config\n /** Reference to the container DOM element - set in setup() @type {HTMLDivElement} */\n routeContainerEl\n /** The current route id after doRoute() has been called */\n currentRouteId\n /** The previous route id after doRoute() has been called */\n previousRouteId\n\n /** Internal only. Set to true when the _start() method has been called */\n #startDone = false\n //#endregion --- ----- ---\n\n //#region --- Internal Methods ---\n /** Class constructor\n * @param {UibRouterConfig} routerConfig Configuration object\n */\n constructor(routerConfig) {\n // Fetch is on desktop browsers since 2017 at latest. Not so much on mobile (Android!)\n // May need a polyfill on mobile or old browsers.\n if (!fetch) throw new Error('[uibrouter:constructor] UibRouter requires `fetch`. Please use a current browser or load a fetch polyfill.')\n\n if (!routerConfig) throw new Error('[uibrouter:constructor] No config provided')\n if (!routerConfig.routes) throw new Error('[uibrouter:constructor] No routes provided in routerConfig')\n // Add a default route container uf needed\n if (!routerConfig.routeContainer)routerConfig.routeContainer = '#uibroutecontainer'\n\n // Save the config\n this.config = routerConfig\n // Create/access the route container element, sets this.routeContainerEl\n this._setRouteContainer()\n\n // Load all external route templates async in parallel - NB: Object.values works on both arrays and objects\n // Note that final `then` is called even if no external routes are given\n Promise.all(Object.values(routerConfig.routes).filter(r => r.type === 'url').map(this._loadExternal)) // eslint-disable-line promise/catch-or-return\n .then(this._appendExternalTemplates)\n .then( () => { // eslint-disable-line promise/always-return\n // Everything is loaded so we can start\n this._start()\n } )\n }\n\n /** Save a reference to, and create if necessary, the HTML element to hold routes */\n _setRouteContainer() {\n const body = document.getElementsByTagName('body')[0]\n // Get reference to route container or create it\n let routeContainerEl = this.routeContainerEl = document.querySelector(this.config.routeContainer)\n if (!routeContainerEl) {\n // throw new Error(`Route container element with CSS selector '${routerConfig.routeContainer}' not found in HTML`)\n const tempContainer = document.createElement('div')\n tempContainer.setAttribute('id', this.config.routeContainer.replace('#', ''))\n body.append(tempContainer)\n routeContainerEl = this.routeContainerEl = document.querySelector(this.config.routeContainer)\n }\n }\n\n /** Load fetched external elements to templates tags under the head tag\n * @param {HTMLElement[]} loadedElements Array of loaded external elements to add as templates to the head tag\n */\n _appendExternalTemplates(loadedElements) {\n const head = document.getElementsByTagName('head')[0]\n // Append the loaded content to the main container\n loadedElements.forEach(element => {\n if (Array.isArray(element)) {\n console.error(...element)\n } else {\n head.append(element)\n }\n })\n }\n\n /** Called once all external templates have been loaded */\n _start() {\n if (this.#startDone === true) return // Don't run this again\n\n // Listen for url hash changes and process route change\n window.addEventListener('hashchange', (event) => this._hashChange(event) )\n // Go to default route if no route in url and if a default is defined or ensure current route is shown\n this.doRoute()\n // Events on fully loaded ...\n document.dispatchEvent(new CustomEvent('uibrouter:loaded'))\n if (uibuilder) uibuilder.set('uibrouter', 'loaded') // eslint-disable-line no-undef\n\n this.#startDone = true // Don't run this again\n }\n\n /** Called when the URL Hash changes\n * @param {HashChangeEvent} event URL Hash change event object\n */\n _hashChange(event) {\n // console.log(`[uibrouter] hashchange: ${this.keepHashFromUrl(event.oldURL)} => ${this.keepHashFromUrl(event.newURL)}` )\n this.doRoute(event)\n }\n\n /** Create DOM route content from a route template (internal or external)\n * Route templates have to be a `