From 6b4f0a8614a491bd4cb3c7403dea1c1767e12dc9 Mon Sep 17 00:00:00 2001 From: "Joey@macstudio" <4296411@qq.com> Date: Thu, 16 Jul 2026 19:34:47 +0800 Subject: [PATCH 1/2] Handle unregistered template changes gracefully --- code-input.js | 89 +++++++++++++++++++++++++++++++++---------------- tests/tester.js | 45 +++++++++++++++++++++++++ 2 files changed, 105 insertions(+), 29 deletions(-) diff --git a/code-input.js b/code-input.js index b02c381..31d60f7 100644 --- a/code-input.js +++ b/code-input.js @@ -130,25 +130,35 @@ var codeInput = { codeInput.usedTemplates[templateName] = template; - // Add waiting code-input elements wanting this template from queue - if (templateName in codeInput.templateNotYetRegisteredQueue) { - for (let i in codeInput.templateNotYetRegisteredQueue[templateName]) { - const elem = codeInput.templateNotYetRegisteredQueue[templateName][i]; + const activateQueuedElements = function (queueName) { + if (!(queueName in codeInput.templateNotYetRegisteredQueue)) return; + + for (let i in codeInput.templateNotYetRegisteredQueue[queueName]) { + const elem = codeInput.templateNotYetRegisteredQueue[queueName][i]; + const requestedTemplateName = elem.getAttribute("template") || codeInput.defaultTemplate; + if (requestedTemplateName != templateName) continue; + elem.templateObject = template; - elem.setup(); + if (elem.textareaElement == null) { + elem.setup(); + } else { + elem.textareaElement.removeAttribute("data-code-input-fallback"); + elem.classList.add("code-input_loaded"); + if (template.preElementStyled) elem.classList.add("code-input_pre-element-styled"); + else elem.classList.remove("code-input_pre-element-styled"); + elem.scheduleHighlight(); + } } - } + delete codeInput.templateNotYetRegisteredQueue[queueName]; + }; + + // Add waiting code-input elements wanting this template from queue + activateQueuedElements(templateName); if (codeInput.defaultTemplate == undefined) { codeInput.defaultTemplate = templateName; // Add elements with default template from queue - if (undefined in codeInput.templateNotYetRegisteredQueue) { - for (let i in codeInput.templateNotYetRegisteredQueue[undefined]) { - const elem = codeInput.templateNotYetRegisteredQueue[undefined][i]; - elem.templateObject = template; - elem.setup(); - } - } + activateQueuedElements(undefined); } }, @@ -478,6 +488,8 @@ var codeInput = { * @param {Array} args - the arguments to pass into the event callback in the template after the code-input element. Normally left empty */ pluginEvt(eventName, args) { + if (this.templateObject == undefined) return; + for (let i in this.templateObject.plugins) { let plugin = this.templateObject.plugins[i]; if (eventName in plugin) { @@ -531,6 +543,11 @@ var codeInput = { // Update code resultElement.innerHTML = this.escapeHtml(value); + if (this.templateObject == undefined) { + this.syncSize(); + return; + } + this.pluginEvt("beforeHighlight"); // Syntax Highlight @@ -543,7 +560,7 @@ var codeInput = { } getStyledHighlightingElement() { - if(this.templateObject.preElementStyled) { + if(this.templateObject != undefined && this.templateObject.preElementStyled) { return this.preElement; } else { return this.codeElement; @@ -660,16 +677,12 @@ var codeInput = { /** * Get the template object this code-input element is using. - * @returns {Object} - Template object + * @param {string} [templateName] - Optional template name to use instead of the current attribute/default. + * @returns {Object|undefined} - Template object, or undefined while waiting for it to be registered. */ - getTemplate() { - let templateName; - if (this.getAttribute("template") == undefined) { - // Default - templateName = codeInput.defaultTemplate; - } else { - templateName = this.getAttribute("template"); - } + getTemplate(templateName) { + if (templateName == undefined) templateName = this.getAttribute("template") || codeInput.defaultTemplate; + if (templateName in codeInput.usedTemplates) { return codeInput.usedTemplates[templateName]; } else { @@ -677,7 +690,9 @@ var codeInput = { if (!(templateName in codeInput.templateNotYetRegisteredQueue)) { codeInput.templateNotYetRegisteredQueue[templateName] = []; } - codeInput.templateNotYetRegisteredQueue[templateName].push(this); + if (!codeInput.templateNotYetRegisteredQueue[templateName].includes(this)) { + codeInput.templateNotYetRegisteredQueue[templateName].push(this); + } return undefined; } } @@ -966,13 +981,13 @@ var codeInput = { // Children not yet present - wait until they are window.addEventListener("DOMContentLoaded", () => { const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]"); - if(fallbackTextarea) { + if(fallbackTextarea && this.textareaElement == null) { this.setupTextareaSyncEvents(fallbackTextarea); } }) } else { const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]"); - if(fallbackTextarea) { + if(fallbackTextarea && this.textareaElement == null) { this.setupTextareaSyncEvents(fallbackTextarea); } } @@ -1022,9 +1037,25 @@ var codeInput = { this.value = newValue; break; case "template": - this.templateObject = codeInput.usedTemplates[newValue || codeInput.defaultTemplate]; - if (this.templateObject.preElementStyled) this.classList.add("code-input_pre-element-styled"); - else this.classList.remove("code-input_pre-element-styled"); + const previousTemplateName = oldValue || codeInput.defaultTemplate; + if (previousTemplateName in codeInput.templateNotYetRegisteredQueue) { + codeInput.templateNotYetRegisteredQueue[previousTemplateName] = codeInput.templateNotYetRegisteredQueue[previousTemplateName].filter((elem) => elem != this); + if (codeInput.templateNotYetRegisteredQueue[previousTemplateName].length == 0) { + delete codeInput.templateNotYetRegisteredQueue[previousTemplateName]; + } + } + + this.templateObject = this.getTemplate(newValue || codeInput.defaultTemplate); + if (this.templateObject == undefined) { + this.classList.remove("code-input_pre-element-styled"); + this.classList.remove("code-input_loaded"); + this.textareaElement.setAttribute("data-code-input-fallback", ""); + } else { + this.textareaElement.removeAttribute("data-code-input-fallback"); + this.classList.add("code-input_loaded"); + if (this.templateObject.preElementStyled) this.classList.add("code-input_pre-element-styled"); + else this.classList.remove("code-input_pre-element-styled"); + } // Syntax Highlight this.scheduleHighlight(); diff --git a/tests/tester.js b/tests/tester.js index f619b12..e6007e9 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -305,6 +305,51 @@ console.log("I've got another line!", 2 < 3, "should be true."); await waitAsync(50); assertEqual("Core", "Programmatically-created element rendered value", programmaticCodeInput.preElement.textContent, "Hello, World!\n"); + const switchableCodeInput = document.createElement("code-input"); + switchableCodeInput.setAttribute("template", "code-editor"); + switchableCodeInput.textContent = "before"; + document.body.append(switchableCodeInput); + await waitAsync(50); + + const staleTemplateName = "test-unregistered-stale"; + const recoveredTemplateName = "test-unregistered-recovered"; + switchableCodeInput.setAttribute("template", staleTemplateName); + switchableCodeInput.value = "after"; + await waitAsync(50); + assertEqual("Core", "Unregistered template has no active template object", switchableCodeInput.templateObject, undefined); + assertEqual("Core", "Unregistered template preserves editable plain-text rendering", switchableCodeInput.codeElement.textContent, "after\n"); + assertEqual("Core", "Unregistered template returns to fallback display", switchableCodeInput.classList.contains("code-input_loaded"), false); + assertEqual("Core", "Unregistered template keeps its textarea editable", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), true); + + switchableCodeInput.setAttribute("template", recoveredTemplateName); + await waitAsync(50); + assertEqual("Core", "Stale unregistered template is removed from the waiting queue", staleTemplateName in codeInput.templateNotYetRegisteredQueue, false); + const staleTemplate = new codeInput.Template((codeElement) => { + codeElement.classList.add("test-stale-template-highlighted"); + }); + codeInput.registerTemplate(staleTemplateName, staleTemplate); + await waitAsync(50); + assertEqual("Core", "Stale queued template does not replace current missing template", switchableCodeInput.templateObject, undefined); + assertEqual("Core", "Stale queued template does not highlight the element", switchableCodeInput.codeElement.classList.contains("test-stale-template-highlighted"), false); + + const recoveredTemplate = new codeInput.Template((codeElement) => { + codeElement.classList.add("test-recovered-template-highlighted"); + }, true, true, false, [new codeInput.plugins.Indent()]); + codeInput.registerTemplate(recoveredTemplateName, recoveredTemplate); + await waitAsync(50); + assertEqual("Core", "Registered missing template becomes active", switchableCodeInput.templateObject, recoveredTemplate); + assertEqual("Core", "Registered missing template resumes highlighting", switchableCodeInput.codeElement.classList.contains("test-recovered-template-highlighted"), true); + assertEqual("Core", "Registered missing template preserves the edited value", switchableCodeInput.value, "after"); + assertEqual("Core", "Registered missing template is removed from the waiting queue", recoveredTemplateName in codeInput.templateNotYetRegisteredQueue, false); + assertEqual("Core", "Registered missing template restores the loaded display", switchableCodeInput.classList.contains("code-input_loaded"), true); + assertEqual("Core", "Registered missing template removes the fallback marker", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), false); + + switchableCodeInput.setAttribute("template", "code-editor"); + await waitAsync(50); + assertEqual("Core", "Existing registered template can be selected after recovery", switchableCodeInput.templateObject, codeInput.usedTemplates["code-editor"]); + assertEqual("Core", "Existing registered template keeps the loaded display", switchableCodeInput.classList.contains("code-input_loaded"), true); + switchableCodeInput.remove(); + // Event Listener Tests let numTimesInputCalled = {"listener": 0, "idl": 0, "content": 0}; From 88fb342a05458368e82b09a82c0897c256e0174d Mon Sep 17 00:00:00 2001 From: Oliver Geer Date: Mon, 20 Jul 2026 16:46:31 +0100 Subject: [PATCH 2/2] Add some comments (including some TODO notes for before merge) --- code-input.js | 43 +++++++++++++++++++++++++++++++++++-------- tests/tester.js | 9 ++++++--- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/code-input.js b/code-input.js index 31d60f7..efde97e 100644 --- a/code-input.js +++ b/code-input.js @@ -142,11 +142,7 @@ var codeInput = { if (elem.textareaElement == null) { elem.setup(); } else { - elem.textareaElement.removeAttribute("data-code-input-fallback"); - elem.classList.add("code-input_loaded"); - if (template.preElementStyled) elem.classList.add("code-input_pre-element-styled"); - else elem.classList.remove("code-input_pre-element-styled"); - elem.scheduleHighlight(); + elem.removeTemporaryFallback() } } delete codeInput.templateNotYetRegisteredQueue[queueName]; @@ -488,6 +484,8 @@ var codeInput = { * @param {Array} args - the arguments to pass into the event callback in the template after the code-input element. Normally left empty */ pluginEvt(eventName, args) { + // To handle already-loaded code-input elements whose template has since been + // not found if (this.templateObject == undefined) return; for (let i in this.templateObject.plugins) { @@ -537,12 +535,14 @@ var codeInput = { * Update the text value to the result element, after the textarea contents have changed. */ update() { + let resultElement = this.codeElement; let value = this.value; value += "\n"; // Placeholder for next line // Update code resultElement.innerHTML = this.escapeHtml(value); + // Fallback view if (this.templateObject == undefined) { this.syncSize(); return; @@ -560,6 +560,8 @@ var codeInput = { } getStyledHighlightingElement() { + // this.templateObject == undefined when code-input element has been loaded + // and then changed to a nonexistent template (so shows fallback textarea). if(this.templateObject != undefined && this.templateObject.preElementStyled) { return this.preElement; } else { @@ -677,10 +679,10 @@ var codeInput = { /** * Get the template object this code-input element is using. - * @param {string} [templateName] - Optional template name to use instead of the current attribute/default. + * @param {string|undefined} [templateName] - Optional template name to use instead of the current attribute/default. * @returns {Object|undefined} - Template object, or undefined while waiting for it to be registered. */ - getTemplate(templateName) { + getTemplate(templateName=undefined) { if (templateName == undefined) templateName = this.getAttribute("template") || codeInput.defaultTemplate; if (templateName in codeInput.usedTemplates) { @@ -920,6 +922,21 @@ var codeInput = { this.classList.add("code-input_loaded"); } + + /** + * Set up and initialise the textarea with a new template, a second or later time. + * Assumes the element has already been loaded once using setup, but that the + * template attribute was then changed to refer to no existing template and thus + * force fallback styling again. + */ + removeTemporaryFallback() { + this.textareaElement.removeAttribute("data-code-input-fallback"); + this.classList.add("code-input_loaded"); + if (this.templateObject.preElementStyled) this.classList.add("code-input_pre-element-styled"); + else this.classList.remove("code-input_pre-element-styled"); + this.scheduleHighlight(); + } + /** * @deprecated This shouldn't have been accessed as part of the library's public interface (to enable more flexibility in backwards-compatible versions), but is still here just in case it was. */ @@ -982,12 +999,18 @@ var codeInput = { window.addEventListener("DOMContentLoaded", () => { const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]"); if(fallbackTextarea && this.textareaElement == null) { + // Check this.textareaElement == null to check the code-input + // element is not loaded, rather than a post-load change to an + // unregistered template. this.setupTextareaSyncEvents(fallbackTextarea); } }) } else { const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]"); if(fallbackTextarea && this.textareaElement == null) { + // Check this.textareaElement == null to check the code-input + // element is not loaded, rather than a post-load change to an + // unregistered template. this.setupTextareaSyncEvents(fallbackTextarea); } } @@ -1039,6 +1062,8 @@ var codeInput = { case "template": const previousTemplateName = oldValue || codeInput.defaultTemplate; if (previousTemplateName in codeInput.templateNotYetRegisteredQueue) { + // This element is no longer waiting for its old template. + // TODO is this the best codeInput.templateNotYetRegisteredQueue[previousTemplateName] = codeInput.templateNotYetRegisteredQueue[previousTemplateName].filter((elem) => elem != this); if (codeInput.templateNotYetRegisteredQueue[previousTemplateName].length == 0) { delete codeInput.templateNotYetRegisteredQueue[previousTemplateName]; @@ -1047,10 +1072,12 @@ var codeInput = { this.templateObject = this.getTemplate(newValue || codeInput.defaultTemplate); if (this.templateObject == undefined) { + // Load fallback template this.classList.remove("code-input_pre-element-styled"); - this.classList.remove("code-input_loaded"); + this.classList.remove("code-input_loaded"); // TODO Can we keep code-input_loaded here since technically is loaded this.textareaElement.setAttribute("data-code-input-fallback", ""); } else { + // Load existing template this.textareaElement.removeAttribute("data-code-input-fallback"); this.classList.add("code-input_loaded"); if (this.templateObject.preElementStyled) this.classList.add("code-input_pre-element-styled"); diff --git a/tests/tester.js b/tests/tester.js index e6007e9..194b1b8 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -319,10 +319,13 @@ console.log("I've got another line!", 2 < 3, "should be true."); assertEqual("Core", "Unregistered template has no active template object", switchableCodeInput.templateObject, undefined); assertEqual("Core", "Unregistered template preserves editable plain-text rendering", switchableCodeInput.codeElement.textContent, "after\n"); assertEqual("Core", "Unregistered template returns to fallback display", switchableCodeInput.classList.contains("code-input_loaded"), false); - assertEqual("Core", "Unregistered template keeps its textarea editable", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), true); + assertEqual("Core", "Unregistered template uses fallback textarea styling", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), true); + + switchableCodeInput.setAttribute("template", recoveredTemplateName); // TODO why here not later - switchableCodeInput.setAttribute("template", recoveredTemplateName); await waitAsync(50); + + // TODO do we want this assertEqual("Core", "Stale unregistered template is removed from the waiting queue", staleTemplateName in codeInput.templateNotYetRegisteredQueue, false); const staleTemplate = new codeInput.Template((codeElement) => { codeElement.classList.add("test-stale-template-highlighted"); @@ -342,7 +345,7 @@ console.log("I've got another line!", 2 < 3, "should be true."); assertEqual("Core", "Registered missing template preserves the edited value", switchableCodeInput.value, "after"); assertEqual("Core", "Registered missing template is removed from the waiting queue", recoveredTemplateName in codeInput.templateNotYetRegisteredQueue, false); assertEqual("Core", "Registered missing template restores the loaded display", switchableCodeInput.classList.contains("code-input_loaded"), true); - assertEqual("Core", "Registered missing template removes the fallback marker", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), false); + assertEqual("Core", "Registered missing template removes the fallback styling", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), false); switchableCodeInput.setAttribute("template", "code-editor"); await waitAsync(50);