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] 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};