Skip to content

Commit b74a7cb

Browse files
authored
Merge pull request #189 from WebCoder49/support-all-event-handlers
Support IDL and content attribute event handlers, and event handlers on fallback element (graceful degradation)
2 parents 6b904b1 + 63f4b4b commit b74a7cb

File tree

2 files changed

+131
-30
lines changed

2 files changed

+131
-30
lines changed

code-input.js

Lines changed: 67 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,8 @@ var codeInput = {
716716
// Synchronise attributes to textarea
717717
for(let i = 0; i < this.attributes.length; i++) {
718718
let attribute = this.attributes[i].name;
719-
if (codeInput.textareaSyncAttributes.includes(attribute) || attribute.substring(0, 5) == "aria-") {
719+
if (codeInput.textareaSyncAttributes.includes(attribute)
720+
|| attribute.substring(0, 5) == "aria-") {
720721
textarea.setAttribute(attribute, this.getAttribute(attribute));
721722
}
722723
}
@@ -726,6 +727,7 @@ var codeInput = {
726727
// Save element internally
727728
this.textareaElement = textarea;
728729
this.append(textarea);
730+
this.setupTextareaSyncEvents(this.textareaElement);
729731

730732
// Create result element
731733
let code = document.createElement("code");
@@ -837,7 +839,24 @@ var codeInput = {
837839
} else {
838840
this.setup();
839841
}
840-
}
842+
}
843+
844+
// Graceful degradation: make events still work without template being
845+
// registered
846+
if (document.readyState === 'loading') {
847+
// Children not yet present - wait until they are
848+
window.addEventListener("DOMContentLoaded", () => {
849+
const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
850+
if(fallbackTextarea) {
851+
this.setupTextareaSyncEvents(fallbackTextarea);
852+
}
853+
})
854+
} else {
855+
const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
856+
if(fallbackTextarea) {
857+
this.setupTextareaSyncEvents(fallbackTextarea);
858+
}
859+
}
841860
}
842861

843862
mutationObserverCallback(mutationList, observer) {
@@ -855,7 +874,7 @@ var codeInput = {
855874
if (mutation.attributeName == codeInput.textareaSyncAttributes[i]) {
856875
return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
857876
}
858-
}
877+
}
859878
if (mutation.attributeName.substring(0, 5) == "aria-") {
860879
return this.attributeChangedCallback(mutation.attributeName, mutation.oldValue, super.getAttribute(mutation.attributeName));
861880
}
@@ -949,12 +968,31 @@ var codeInput = {
949968

950969
}
951970

952-
/* ------------------------------------
953-
* -----------Overrides----------------
954-
* ------------------------------------
955-
* Override/Implement ordinary HTML textarea functionality so that the <code-input>
956-
* element acts just like a <textarea>. */
971+
//-------------------------------------------
972+
//----------- Textarea interface ------------
973+
//-------------------------------------------
974+
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement
975+
// Attributes defined at codeInput.textareaSyncAttributes
976+
// Event listener added to pass to code-input element
957977

978+
/**
979+
* Capture all events from textareaSyncEvents triggered on the given textarea
980+
* element and pass them to the code-input element.
981+
*/
982+
setupTextareaSyncEvents(textarea) {
983+
for(let i = 0; i < codeInput.textareaSyncEvents.length; i++) {
984+
const evtName = codeInput.textareaSyncEvents[i];
985+
textarea.addEventListener(evtName, (evt) => {
986+
if(!evt.bubbles) { // Don't duplicate the callback
987+
this.dispatchEvent(new evt.constructor(evt.type, evt)); // Thanks to
988+
}
989+
});
990+
}
991+
}
992+
993+
// addEventListener and removeEventListener overrides are still used
994+
// for backwards compatibility - unlike the solution above, they keep
995+
// the event's isTrusted as true.
958996
/**
959997
* @override
960998
*/
@@ -975,12 +1013,22 @@ var codeInput = {
9751013

9761014
if (options === undefined) {
9771015
if(this.textareaElement == null) {
1016+
// Unregistered
1017+
const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
1018+
if(fallbackTextarea) {
1019+
fallbackTextarea.addEventListener(type, boundCallback);
1020+
}
9781021
this.addEventListener("code-input_load", () => { this.textareaElement.addEventListener(type, boundCallback); });
9791022
} else {
9801023
this.textareaElement.addEventListener(type, boundCallback);
9811024
}
9821025
} else {
9831026
if(this.textareaElement == null) {
1027+
// Unregistered
1028+
const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
1029+
if(fallbackTextarea) {
1030+
fallbackTextarea.addEventListener(type, boundCallback, options);
1031+
}
9841032
this.addEventListener("code-input_load", () => { this.textareaElement.addEventListener(type, boundCallback, options); });
9851033
} else {
9861034
this.textareaElement.addEventListener(type, boundCallback, options);
@@ -1000,19 +1048,29 @@ var codeInput = {
10001048
* @override
10011049
*/
10021050
removeEventListener(type, listener, options = undefined) {
1003-
// Save a copy of the callback where `this` refers to the code-input element
1051+
// Save a copy of the callback where `this` refers to the code-input element
10041052
let boundCallback = this.boundEventCallbacks[listener];
10051053

10061054
if (codeInput.textareaSyncEvents.includes(type)) {
10071055
// Synchronise with textarea
10081056
if (options === undefined) {
10091057
if(this.textareaElement == null) {
1058+
// Unregistered
1059+
const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
1060+
if(fallbackTextarea) {
1061+
fallbackTextarea.removeEventListener(type, boundCallback);
1062+
}
10101063
this.addEventListener("code-input_load", () => { this.textareaElement.removeEventListener(type, boundCallback); });
10111064
} else {
10121065
this.textareaElement.removeEventListener(type, boundCallback);
10131066
}
10141067
} else {
10151068
if(this.textareaElement == null) {
1069+
// Unregistered
1070+
const fallbackTextarea = this.querySelector("textarea[data-code-input-fallback]");
1071+
if(fallbackTextarea) {
1072+
fallbackTextarea.removeEventListener(type, boundCallback, options);
1073+
}
10161074
this.addEventListener("code-input_load", () => { this.textareaElement.removeEventListener(type, boundCallback, options); });
10171075
} else {
10181076
this.textareaElement.removeEventListener(type, boundCallback, options);
@@ -1028,12 +1086,6 @@ var codeInput = {
10281086
}
10291087
}
10301088

1031-
//-------------------------------------------
1032-
//----------- Textarea interface ------------
1033-
//-------------------------------------------
1034-
// See https://developer.mozilla.org/en-US/docs/Web/API/HTMLTextAreaElement
1035-
// Attributes defined at codeInput.textareaSyncAttributes
1036-
10371089
/**
10381090
* Get the JavaScript property from the internal textarea
10391091
* element, given its name and a defaultValue to return

tests/tester.js

Lines changed: 64 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -226,20 +226,51 @@ console.log("I've got another line!", 2 &lt; 3, "should be true.");
226226
assertEqual("Core", "Programmatically-created element rendered value", programmaticCodeInput.preElement.textContent, "Hello, World!\n");
227227

228228
// Event Listener Tests
229-
// Function type listeners
230-
let numTimesInputCalled = 0;
231-
let numTimesChangeCalled = 0;
232229

233-
let inputListener = (evt) => {
234-
if(!evt.isTrusted) { // To prevent duplicate calling due to allowInputEvents hack
235-
numTimesInputCalled++;
230+
let numTimesInputCalled = {"listener": 0, "idl": 0, "content": 0};
231+
let numTimesChangeCalled = {"listener": 0, "idl": 0, "content": 0};
232+
let numTimesFocusCalled = {"listener": 0, "idl": 0, "content": 0};
233+
let numTimesBlurCalled = {"listener": 0, "idl": 0, "content": 0};
234+
235+
let inputListener = (type, evt) => {
236+
if(!evt.isTrusted) { // To prevent duplicate calling due to allowInputEvents hack, used just in this test
237+
numTimesInputCalled[type]++;
236238
}
237239
};
238-
codeInputElement.addEventListener("input", inputListener);
239-
let changeListener = () => {
240-
numTimesChangeCalled++;
240+
let changeListener = (type) => {
241+
numTimesChangeCalled[type]++;
241242
};
242-
codeInputElement.addEventListener("change", changeListener);
243+
let focusListener = (type, evt) => {
244+
numTimesFocusCalled[type]++;
245+
};
246+
let blurListener = (type, evt) => {
247+
numTimesBlurCalled[type]++;
248+
};
249+
250+
codeInputElement.addEventListener("input", inputListener.bind(null, "listener"));
251+
codeInputElement.addEventListener("change", changeListener.bind(null, "listener"));
252+
codeInputElement.addEventListener("focus", focusListener.bind(null, "listener"));
253+
codeInputElement.addEventListener("blur", blurListener.bind(null, "listener"));
254+
255+
codeInputElement.oninput = inputListener.bind(null, "idl");
256+
codeInputElement.onchange = changeListener.bind(null, "idl");
257+
codeInputElement.onfocus = focusListener.bind(null, "idl");
258+
codeInputElement.onblur = blurListener.bind(null, "idl");
259+
260+
// Make listeners be called - first time
261+
textarea.focus(); // Focus textarea
262+
addText(textarea, " // Hi");
263+
textarea.blur(); // Unfocus textarea - calls change event
264+
textarea.focus();
265+
266+
window.content_listener_oninput = inputListener.bind(null, "content");
267+
codeInputElement.setAttribute("oninput", "content_listener_oninput(event)");
268+
window.content_listener_onchange = changeListener.bind(null, "content");
269+
codeInputElement.setAttribute("onchange", "content_listener_onchange(event)");
270+
window.content_listener_onfocus = focusListener.bind(null, "content");
271+
codeInputElement.setAttribute("onfocus", "content_listener_onfocus(event)");
272+
window.content_listener_onblur = blurListener.bind(null, "content");
273+
codeInputElement.setAttribute("onblur", "content_listener_onblur(event)");
243274

244275
let inputDeletedListenerCalled = false;
245276
let deletedListener = () => {
@@ -248,15 +279,33 @@ console.log("I've got another line!", 2 &lt; 3, "should be true.");
248279
codeInputElement.addEventListener("input", deletedListener);
249280
codeInputElement.removeEventListener("input", deletedListener);
250281

251-
// Make listeners be called
252-
textarea.focus(); // Focus textarea
282+
// Make listeners be called - second time
253283
addText(textarea, " // Hi");
254284
textarea.blur(); // Unfocus textarea - calls change event
255285
textarea.focus();
256286

257-
assertEqual("Core", "Function Event Listeners: Input Called Right Number of Times", numTimesInputCalled, 6);
258-
assertEqual("Core", "Function Event Listeners: Change Called Right Number of Times", numTimesChangeCalled, 1);
259-
testAssertion("Core", "Function Event Listeners: Input Removed Listener Not Called", !inputDeletedListenerCalled, "(code-input element).removeEventListener did not work.");
287+
// Function type listeners
288+
// Never overriden
289+
assertEqual("Core", "addEventListener: Input Called Right Number of Times", numTimesInputCalled["listener"], 12);
290+
assertEqual("Core", "addEventListener: Change Called Right Number of Times", numTimesChangeCalled["listener"], 2);
291+
assertEqual("Core", "addEventListener: Focus Called Right Number of Times", numTimesFocusCalled["listener"], 3);
292+
assertEqual("Core", "addEventListener: Blur Called Right Number of Times", numTimesBlurCalled["listener"], 2);
293+
294+
// IDL attribute (JavaScript property) type listeners
295+
// Overriden by content attributes before second set of interaction
296+
assertEqual("Core", "IDL attribute (JavaScript .oninput) event handler: Input Called Right Number of Times then Overriden by Content Attr", numTimesInputCalled["idl"], 6);
297+
assertEqual("Core", "IDL attribute (JavaScript .onchange) event handler: Change Called Right Number of Times then Overriden by Content Attr", numTimesChangeCalled["idl"], 1);
298+
assertEqual("Core", "IDL attribute (JavaScript .onfocus) event handler: Focus Called Right Number of Times then Overriden by Content Attr", numTimesFocusCalled["idl"], 2);
299+
assertEqual("Core", "IDL attribute (JavaScript .onblur) event handler: Blur Called Right Number of Times then Overriden by Content Attr", numTimesBlurCalled["idl"], 1);
300+
301+
// Content attribute (HTML attribute) type listeners
302+
// Only registered before second set of interaction
303+
assertEqual("Core", "Content attribute (HTML oninput=\"...\") event handler: Input Called Right Number of Times", numTimesInputCalled["content"], 6);
304+
assertEqual("Core", "Content attribute (HTML onchange=\"...\") event handler: Change Called Right Number of Times", numTimesChangeCalled["content"], 1);
305+
assertEqual("Core", "Content attribute (HTML onfocus=\"...\") event handler: Focus Called Right Number of Times", numTimesFocusCalled["content"], 1);
306+
assertEqual("Core", "Content attribute (HTML onblur=\"...\") event handler: Blur Called Right Number of Times", numTimesBlurCalled["content"], 1);
307+
308+
testAssertion("Core", "addEventListener, removeEventListener: Input Removed Listener Not Called", !inputDeletedListenerCalled, "(code-input element).removeEventListener did not work.");
260309

261310
codeInputElement.removeEventListener("input", inputListener);
262311
codeInputElement.removeEventListener("change", changeListener);

0 commit comments

Comments
 (0)