Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
116 changes: 87 additions & 29 deletions code-input.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
},

Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -660,24 +679,22 @@ 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 {
// Doesn't exist - add to queue
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;
}
}
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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();

Expand Down
48 changes: 48 additions & 0 deletions tests/tester.js
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down