diff --git a/code-input.js b/code-input.js index b02c381..efde97e 100644 --- a/code-input.js +++ b/code-input.js @@ -130,25 +130,31 @@ 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.removeTemporaryFallback() + } } - } + 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 +484,10 @@ 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) { let plugin = this.templateObject.plugins[i]; if (eventName in plugin) { @@ -525,12 +535,19 @@ 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; + } + this.pluginEvt("beforeHighlight"); // Syntax Highlight @@ -543,7 +560,9 @@ var codeInput = { } getStyledHighlightingElement() { - if(this.templateObject.preElementStyled) { + // 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 { return this.codeElement; @@ -660,16 +679,12 @@ var codeInput = { /** * Get the template object this code-input element is using. - * @returns {Object} - Template object + * @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() { - let templateName; - if (this.getAttribute("template") == undefined) { - // Default - templateName = codeInput.defaultTemplate; - } else { - templateName = this.getAttribute("template"); - } + getTemplate(templateName=undefined) { + if (templateName == undefined) templateName = this.getAttribute("template") || codeInput.defaultTemplate; + if (templateName in codeInput.usedTemplates) { return codeInput.usedTemplates[templateName]; } else { @@ -677,7 +692,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; } } @@ -905,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. */ @@ -966,13 +998,19 @@ 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) { + // 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) { + 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); } } @@ -1022,9 +1060,29 @@ 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) { + // 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]; + } + } + + 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"); // 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"); + 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..194b1b8 100644 --- a/tests/tester.js +++ b/tests/tester.js @@ -305,6 +305,54 @@ 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 uses fallback textarea styling", switchableCodeInput.textareaElement.hasAttribute("data-code-input-fallback"), true); + + switchableCodeInput.setAttribute("template", recoveredTemplateName); // TODO why here not later + + 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"); + }); + 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 styling", 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};