From a898224d3d8c69cb65bcf75041dbdf4e4f59e205 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sat, 16 Jan 2021 20:23:46 +0100 Subject: [PATCH 1/9] Switch to KeyboardEvent.code rather than KeyboardEvent.which, which is deprecated --- qt/aqt/data/web/js/editor.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index d56b71071..079a1135c 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -50,13 +50,13 @@ interface Selection { function onKey(evt: KeyboardEvent) { // esc clears focus, allowing dialog to close - if (evt.which === 27) { + if (evt.code === "Escape") { currentField.blur(); return; } // prefer
instead of
- if (evt.which === 13 && !inListItem()) { + if (evt.code === "Enter" && !inListItem()) { evt.preventDefault(); document.execCommand("insertLineBreak"); return; @@ -73,11 +73,11 @@ function onKey(evt: KeyboardEvent) { if (evt.shiftKey) { alter = "extend"; } - if (evt.which === 39) { + if (evt.code === "ArrowRight") { selection.modify(alter, "right", granularity); evt.preventDefault(); return; - } else if (evt.which === 37) { + } else if (evt.code === "ArrowLeft") { selection.modify(alter, "left", granularity); evt.preventDefault(); return; @@ -89,7 +89,7 @@ function onKey(evt: KeyboardEvent) { function onKeyUp(evt: KeyboardEvent) { // Avoid div element on remove - if (evt.which === 8 || evt.which === 13) { + if (evt.code === "Enter" || evt.code === "Backspace") { const anchor = window.getSelection().anchorNode; if ( From 150de7a683348198183166ddda5791eb5ff6bc28 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Mon, 18 Jan 2021 17:05:05 +0100 Subject: [PATCH 2/9] Use more strict TypeScript in editor.ts --- qt/aqt/data/web/js/editor.ts | 179 +++++++++++++++++------------------ 1 file changed, 85 insertions(+), 94 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index 079a1135c..8ee02f2ef 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -10,18 +10,18 @@ declare interface String { } /* kept for compatibility with add-ons */ -String.prototype.format = function () { +String.prototype.format = function (): string { const args = arguments; return this.replace(/\{\d+\}/g, function (m) { return args[m.match(/\d+/)]; }); }; -function setFGButton(col) { +function setFGButton(col: string): void { $("#forecolor")[0].style.backgroundColor = col; } -function saveNow(keepFocus) { +function saveNow(keepFocus: boolean): void { if (!currentField) { return; } @@ -36,7 +36,7 @@ function saveNow(keepFocus) { } } -function triggerKeyTimer() { +function triggerKeyTimer(): void { clearChangeTimer(); changeTimer = setTimeout(function () { updateButtonState(); @@ -48,7 +48,7 @@ interface Selection { modify(s: string, t: string, u: string): void; } -function onKey(evt: KeyboardEvent) { +function onKey(evt: KeyboardEvent): void { // esc clears focus, allowing dialog to close if (evt.code === "Escape") { currentField.blur(); @@ -87,7 +87,7 @@ function onKey(evt: KeyboardEvent) { triggerKeyTimer(); } -function onKeyUp(evt: KeyboardEvent) { +function onKeyUp(evt: KeyboardEvent): void { // Avoid div element on remove if (evt.code === "Enter" || evt.code === "Backspace") { const anchor = window.getSelection().anchorNode; @@ -105,7 +105,7 @@ function onKeyUp(evt: KeyboardEvent) { } function nodeIsElement(node: Node): node is Element { - return node.nodeType == Node.ELEMENT_NODE; + return node.nodeType === Node.ELEMENT_NODE; } function inListItem(): boolean { @@ -123,7 +123,7 @@ function inListItem(): boolean { return inList; } -function insertNewline() { +function insertNewline(): void { if (!inPreEnvironment()) { setFormat("insertText", "\n"); return; @@ -156,12 +156,12 @@ function inPreEnvironment(): boolean { return window.getComputedStyle(n).whiteSpace.startsWith("pre"); } -function onInput() { +function onInput(): void { // make sure IME changes get saved triggerKeyTimer(); } -function updateButtonState() { +function updateButtonState(): void { const buts = ["bold", "italic", "underline", "superscript", "subscript"]; for (const name of buts) { if (document.queryCommandState(name)) { @@ -175,15 +175,16 @@ function updateButtonState() { // 'col': document.queryCommandValue("forecolor") } -function toggleEditorButton(buttonid) { - if ($(buttonid).hasClass("highlighted")) { - $(buttonid).removeClass("highlighted"); +function toggleEditorButton(buttonid: string): void { + const button = $(buttonid); + if (button.hasClass("highlighted")) { + button.removeClass("highlighted"); } else { - $(buttonid).addClass("highlighted"); + button.addClass("highlighted"); } } -function setFormat(cmd: string, arg?: any, nosave: boolean = false) { +function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { document.execCommand(cmd, false, arg); if (!nosave) { saveField("key"); @@ -191,14 +192,14 @@ function setFormat(cmd: string, arg?: any, nosave: boolean = false) { } } -function clearChangeTimer() { +function clearChangeTimer(): void { if (changeTimer) { clearTimeout(changeTimer); changeTimer = null; } } -function onFocus(elem) { +function onFocus(elem: HTMLElement): void { if (currentField === elem) { // anki window refocused; current element unchanged return; @@ -213,11 +214,12 @@ function onFocus(elem) { // do this twice so that there's no flicker on newer versions caretToEnd(); // scroll if bottom of element off the screen - function pos(obj) { + function pos(elem: HTMLElement): number { let cur = 0; do { - cur += obj.offsetTop; - } while ((obj = obj.offsetParent)); + cur += elem.offsetTop; + elem = elem.offsetParent as HTMLElement; + } while (elem); return cur; } @@ -230,14 +232,14 @@ function onFocus(elem) { } } -function focusField(n) { +function focusField(n: number): void { if (n === null) { return; } $("#f" + n).focus(); } -function focusIfField(x, y) { +function focusIfField(x: number, y: number): boolean { const elements = document.elementsFromPoint(x, y); for (let i = 0; i < elements.length; i++) { let elem = elements[i] as HTMLElement; @@ -252,12 +254,12 @@ function focusIfField(x, y) { return false; } -function onPaste(elem) { +function onPaste(): void { pycmd("paste"); window.event.preventDefault(); } -function caretToEnd() { +function caretToEnd(): void { const r = document.createRange(); r.selectNodeContents(currentField); r.collapse(false); @@ -266,7 +268,7 @@ function caretToEnd() { s.addRange(r); } -function onBlur() { +function onBlur(): void { if (!currentField) { return; } @@ -281,7 +283,7 @@ function onBlur() { } } -function saveField(type) { +function saveField(type: "blur" | "key"): void { clearChangeTimer(); if (!currentField) { // no field has been focused yet @@ -299,25 +301,25 @@ function saveField(type) { ); } -function currentFieldOrdinal() { +function currentFieldOrdinal(): string { return currentField.id.substring(1); } -function wrappedExceptForWhitespace(text, front, back) { +function wrappedExceptForWhitespace(text: string, front: string, back: string): string { const match = text.match(/^(\s*)([^]*?)(\s*)$/); return match[1] + front + match[2] + back + match[3]; } -function disableButtons() { +function disableButtons(): void { $("button.linkb:not(.perm)").prop("disabled", true); } -function enableButtons() { +function enableButtons(): void { $("button.linkb").prop("disabled", false); } // disable the buttons if a field is not currently focused -function maybeDisableButtons() { +function maybeDisableButtons(): void { if (!document.activeElement || document.activeElement.className !== "field") { disableButtons(); } else { @@ -325,16 +327,16 @@ function maybeDisableButtons() { } } -function wrap(front, back) { +function wrap(front: string, back: string): void { wrapInternal(front, back, false); } /* currently unused */ -function wrapIntoText(front, back) { +function wrapIntoText(front: string, back: string): void { wrapInternal(front, back, true); } -function wrapInternal(front, back, plainText) { +function wrapInternal(front: string, back: string, plainText: boolean): void { const s = window.getSelection(); let r = s.getRangeAt(0); const content = r.cloneContents(); @@ -357,12 +359,12 @@ function wrapInternal(front, back, plainText) { } } -function onCutOrCopy() { +function onCutOrCopy(): boolean { pycmd("cutOrCopy"); return true; } -function setFields(fields) { +function setFields(fields: [string, string][]): void { let txt = ""; // webengine will include the variable after enter+backspace // if we don't convert it to a literal colour @@ -414,7 +416,7 @@ function setBackgrounds(cols) { } } -function setFonts(fonts) { +function setFonts(fonts: [string, number, boolean][]): void { for (let i = 0; i < fonts.length; i++) { const n = $("#f" + i); n.css("font-family", fonts[i][0]).css("font-size", fonts[i][1]); @@ -422,19 +424,19 @@ function setFonts(fonts) { } } -function setNoteId(id) { +function setNoteId(id: number): void { currentNoteId = id; } -function showDupes() { +function showDupes(): void { $("#dupes").show(); } -function hideDupes() { +function hideDupes(): void { $("#dupes").hide(); } -let pasteHTML = function (html, internal, extendedMode) { +let pasteHTML = function (html: string, internal: boolean, extendedMode: boolean): void { html = filterHTML(html, internal, extendedMode); if (html !== "") { @@ -442,7 +444,7 @@ let pasteHTML = function (html, internal, extendedMode) { } }; -let filterHTML = function (html, internal, extendedMode) { +let filterHTML = function (html: string, internal: boolean, extendedMode: boolean): string { // wrap it in as we aren't allowed to change top level elements const top = $.parseHTML("" + html + "")[0] as Element; if (internal) { @@ -516,38 +518,32 @@ let isNightMode = function (): boolean { return document.body.classList.contains("nightMode"); }; -let filterExternalSpan = function (node) { +let filterExternalSpan = function (elem: HTMLElement) { // filter out attributes - let toRemove = []; - for (const attr of node.attributes) { + for (let i = 0; i < elem.attributes.length; i++) { + const attr = elem.attributes.item(i); const attrName = attr.name.toUpperCase(); + if (attrName !== "STYLE") { - toRemove.push(attr); + elem.removeAttributeNode(attr); } } - for (const attributeToRemove of toRemove) { - node.removeAttributeNode(attributeToRemove); - } + // filter styling - toRemove = []; - for (const name of node.style) { - if (!allowedStyling.hasOwnProperty(name)) { - toRemove.push(name); - } - if (name === "background-color" && node.style[name] === "transparent") { + for (let i = 0; i < elem.style.length; i++) { + const name = elem.style.item(i); + const value = elem.style.getPropertyValue(name) + + if ( + !allowedStyling.hasOwnProperty(name) || // google docs adds this unnecessarily - toRemove.push(name); - } - if (isNightMode()) { + name === "background-color" && value === "transparent" || // ignore coloured text in night mode for now - if (name === "background-color" || name == "color") { - toRemove.push(name); - } + isNightMode() && (name === "background-color" || name === "color") + ) { + elem.style.removeProperty(name); } } - for (let name of toRemove) { - node.style.removeProperty(name); - } }; allowedTagsExtended["SPAN"] = filterExternalSpan; @@ -555,34 +551,34 @@ allowedTagsExtended["SPAN"] = filterExternalSpan; // add basic tags to extended Object.assign(allowedTagsExtended, allowedTagsBasic); +function isHTMLElement(elem: Element): elem is HTMLElement { + return elem instanceof HTMLElement; +} + // filtering from another field -let filterInternalNode = function (node) { - if (node.style) { - node.style.removeProperty("background-color"); - node.style.removeProperty("font-size"); - node.style.removeProperty("font-family"); +let filterInternalNode = function (elem: Element) { + if (isHTMLElement(elem)) { + elem.style.removeProperty("background-color"); + elem.style.removeProperty("font-size"); + elem.style.removeProperty("font-family"); } // recurse - for (const child of node.childNodes) { + for (let i = 0; i < elem.children.length; i++) { + const child = elem.children[i]; filterInternalNode(child); } }; // filtering from external sources -let filterNode = function (node, extendedMode) { - // text node? - if (node.nodeType === 3) { +let filterNode = function (node: Node, extendedMode: boolean): void { + if (!nodeIsElement(node)) { return; } // descend first, and take a copy of the child nodes as the loop will skip // elements due to node modifications otherwise - - const nodes = []; - for (const child of node.childNodes) { - nodes.push(child); - } - for (const child of nodes) { + for (let i = 0; i < node.children.length; i++) { + const child = node.children[i]; filterNode(child, extendedMode); } @@ -590,12 +586,10 @@ let filterNode = function (node, extendedMode) { return; } - let tag; - if (extendedMode) { - tag = allowedTagsExtended[node.tagName]; - } else { - tag = allowedTagsBasic[node.tagName]; - } + const tag = extendedMode + ? allowedTagsExtended[node.tagName] + : allowedTagsBasic[node.tagName]; + if (!tag) { if (!node.innerHTML || node.tagName === "TITLE") { node.parentNode.removeChild(node); @@ -608,21 +602,18 @@ let filterNode = function (node, extendedMode) { tag(node); } else { // allowed, filter out attributes - const toRemove = []; - for (const attr of node.attributes) { + for (let i = 0; i < node.attributes.length; i++) { + const attr = node.attributes.item(i); const attrName = attr.name.toUpperCase(); if (tag.attrs.indexOf(attrName) === -1) { - toRemove.push(attr); + node.removeAttributeNode(attr); } } - for (const attributeToRemove of toRemove) { - node.removeAttributeNode(attributeToRemove); - } } } }; -let adjustFieldsTopMargin = function () { +let adjustFieldsTopMargin = function (): void { const topHeight = $("#topbuts").height(); const margin = topHeight + 8; document.getElementById("fields").style.marginTop = margin + "px"; @@ -630,7 +621,7 @@ let adjustFieldsTopMargin = function () { let mouseDown = 0; -$(function () { +$(function (): void { document.body.onmousedown = function () { mouseDown++; }; @@ -639,7 +630,7 @@ $(function () { mouseDown--; }; - document.onclick = function (evt: MouseEvent) { + document.onclick = function (evt: MouseEvent): void { const src = evt.target as Element; if (src.tagName === "IMG") { // image clicked; find contenteditable parent From 4deeb798cae9d2604812ab2c66c241458a7b4ac3 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Mon, 18 Jan 2021 17:42:29 +0100 Subject: [PATCH 3/9] Prefer template string and `addEventListener` over string concatenation and .on --- qt/aqt/data/web/js/editor.ts | 73 +++++++++++++----------------------- 1 file changed, 27 insertions(+), 46 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index 8ee02f2ef..ef7597f01 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -6,15 +6,12 @@ let changeTimer = null; let currentNoteId = null; declare interface String { - format(...args): string; + format(...args: string[]): string; } /* kept for compatibility with add-ons */ -String.prototype.format = function (): string { - const args = arguments; - return this.replace(/\{\d+\}/g, function (m) { - return args[m.match(/\d+/)]; - }); +String.prototype.format = function (...args: string[]): string { + return this.replace(/\{\d+\}/g, (m) => args[m.match(/\d+/)]); }; function setFGButton(col: string): void { @@ -165,9 +162,9 @@ function updateButtonState(): void { const buts = ["bold", "italic", "underline", "superscript", "subscript"]; for (const name of buts) { if (document.queryCommandState(name)) { - $("#" + name).addClass("highlighted"); + $(`#${name}`).addClass("highlighted"); } else { - $("#" + name).removeClass("highlighted"); + $(`#${name}`).removeClass("highlighted"); } } @@ -205,7 +202,7 @@ function onFocus(elem: HTMLElement): void { return; } currentField = elem; - pycmd("focus:" + currentFieldOrdinal()); + pycmd(`focus:${currentFieldOrdinal()}`); enableButtons(); // don't adjust cursor on mouse clicks if (mouseDown) { @@ -236,7 +233,7 @@ function focusField(n: number): void { if (n === null) { return; } - $("#f" + n).focus(); + $(`#f${n}`).focus(); } function focusIfField(x: number, y: number): boolean { @@ -290,15 +287,7 @@ function saveField(type: "blur" | "key"): void { return; } // type is either 'blur' or 'key' - pycmd( - type + - ":" + - currentFieldOrdinal() + - ":" + - currentNoteId + - ":" + - currentField.innerHTML - ); + pycmd(`${type}:${currentFieldOrdinal()}:${currentNoteId}:${currentField.innerHTML}`) } function currentFieldOrdinal(): string { @@ -399,26 +388,23 @@ function setFields(fields: [string, string][]): void { `; } - $("#fields").html(` - -${txt} -
`); + $("#fields").html(`${txt}
`); maybeDisableButtons(); } -function setBackgrounds(cols) { +function setBackgrounds(cols: ("dupe")[]) { for (let i = 0; i < cols.length; i++) { - if (cols[i] == "dupe") { - $("#f" + i).addClass("dupe"); + if (cols[i] === "dupe") { + $(`#f${i}`).addClass("dupe"); } else { - $("#f" + i).removeClass("dupe"); + $(`#f${i}`).removeClass("dupe"); } } } function setFonts(fonts: [string, number, boolean][]): void { for (let i = 0; i < fonts.length; i++) { - const n = $("#f" + i); + const n = $(`#f${i}`); n.css("font-family", fonts[i][0]).css("font-size", fonts[i][1]); n[0].dir = fonts[i][2] ? "rtl" : "ltr"; } @@ -446,7 +432,7 @@ let pasteHTML = function (html: string, internal: boolean, extendedMode: boolean let filterHTML = function (html: string, internal: boolean, extendedMode: boolean): string { // wrap it in as we aren't allowed to change top level elements - const top = $.parseHTML("" + html + "")[0] as Element; + const top = $.parseHTML(`${html}`)[0] as Element; if (internal) { filterInternalNode(top); } else { @@ -616,42 +602,37 @@ let filterNode = function (node: Node, extendedMode: boolean): void { let adjustFieldsTopMargin = function (): void { const topHeight = $("#topbuts").height(); const margin = topHeight + 8; - document.getElementById("fields").style.marginTop = margin + "px"; + document.getElementById("fields").style.marginTop = `${margin}px`; }; let mouseDown = 0; $(function (): void { - document.body.onmousedown = function () { - mouseDown++; - }; + document.body.addEventListener("mousedown", () => (mouseDown++)); + document.body.addEventListener("mouseup", () => (mouseDown--)); - document.body.onmouseup = function () { - mouseDown--; - }; - - document.onclick = function (evt: MouseEvent): void { + document.addEventListener("click", (evt: MouseEvent): void => { const src = evt.target as Element; if (src.tagName === "IMG") { // image clicked; find contenteditable parent let p = src; while ((p = p.parentNode as Element)) { if (p.className === "field") { - $("#" + p.id).focus(); + $(`#${p.id}`).focus(); break; } } } - }; - - // prevent editor buttons from taking focus - $("button.linkb").on("mousedown", function (e) { - e.preventDefault(); }); - window.onresize = function () { + // prevent editor buttons from taking focus + $("button.linkb").on("mousedown", function (evt: Event) { + evt.preventDefault(); + }); + + window.addEventListener("resize", () => { adjustFieldsTopMargin(); - }; + }); adjustFieldsTopMargin(); }); From a4921a36dd85423c2b09cca1821a4573931e6781 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 19 Jan 2021 01:08:15 +0100 Subject: [PATCH 4/9] Satisfy formatter --- qt/aqt/data/web/js/editor.ts | 36 ++++++++++++++++++++++++++---------- 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index ef7597f01..e87a11cad 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -287,7 +287,9 @@ function saveField(type: "blur" | "key"): void { return; } // type is either 'blur' or 'key' - pycmd(`${type}:${currentFieldOrdinal()}:${currentNoteId}:${currentField.innerHTML}`) + pycmd( + `${type}:${currentFieldOrdinal()}:${currentNoteId}:${currentField.innerHTML}` + ); } function currentFieldOrdinal(): string { @@ -388,11 +390,13 @@ function setFields(fields: [string, string][]): void { `; } - $("#fields").html(`${txt}
`); + $("#fields").html( + `${txt}
` + ); maybeDisableButtons(); } -function setBackgrounds(cols: ("dupe")[]) { +function setBackgrounds(cols: "dupe"[]) { for (let i = 0; i < cols.length; i++) { if (cols[i] === "dupe") { $(`#f${i}`).addClass("dupe"); @@ -422,7 +426,11 @@ function hideDupes(): void { $("#dupes").hide(); } -let pasteHTML = function (html: string, internal: boolean, extendedMode: boolean): void { +let pasteHTML = function ( + html: string, + internal: boolean, + extendedMode: boolean +): void { html = filterHTML(html, internal, extendedMode); if (html !== "") { @@ -430,7 +438,11 @@ let pasteHTML = function (html: string, internal: boolean, extendedMode: boolean } }; -let filterHTML = function (html: string, internal: boolean, extendedMode: boolean): string { +let filterHTML = function ( + html: string, + internal: boolean, + extendedMode: boolean +): string { // wrap it in as we aren't allowed to change top level elements const top = $.parseHTML(`${html}`)[0] as Element; if (internal) { @@ -518,14 +530,14 @@ let filterExternalSpan = function (elem: HTMLElement) { // filter styling for (let i = 0; i < elem.style.length; i++) { const name = elem.style.item(i); - const value = elem.style.getPropertyValue(name) + const value = elem.style.getPropertyValue(name); if ( !allowedStyling.hasOwnProperty(name) || // google docs adds this unnecessarily - name === "background-color" && value === "transparent" || + (name === "background-color" && value === "transparent") || // ignore coloured text in night mode for now - isNightMode() && (name === "background-color" || name === "color") + (isNightMode() && (name === "background-color" || name === "color")) ) { elem.style.removeProperty(name); } @@ -608,8 +620,12 @@ let adjustFieldsTopMargin = function (): void { let mouseDown = 0; $(function (): void { - document.body.addEventListener("mousedown", () => (mouseDown++)); - document.body.addEventListener("mouseup", () => (mouseDown--)); + document.body.addEventListener("mousedown", (): void => { + mouseDown++; + }); + document.body.addEventListener("mouseup", (): void => { + mouseDown--; + }); document.addEventListener("click", (evt: MouseEvent): void => { const src = evt.target as Element; From 47d26126e7239ab0ab644f16f1c7b965788e6cd1 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 19 Jan 2021 02:48:41 +0100 Subject: [PATCH 5/9] Switch to iterables for elem.style and elem.attributes --- qt/aqt/data/web/js/editor.ts | 20 +++++++++++--------- qt/aqt/data/web/js/tsconfig.json | 2 +- ts/tsconfig.json | 2 +- 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index e87a11cad..750363def 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -11,7 +11,13 @@ declare interface String { /* kept for compatibility with add-ons */ String.prototype.format = function (...args: string[]): string { - return this.replace(/\{\d+\}/g, (m) => args[m.match(/\d+/)]); + return this.replace(/\{\d+\}/g, (m: string): void => { + const match = m.match(/\d+/) + + return match + ? args[match[0]] + : ""; + }) }; function setFGButton(col: string): void { @@ -518,8 +524,7 @@ let isNightMode = function (): boolean { let filterExternalSpan = function (elem: HTMLElement) { // filter out attributes - for (let i = 0; i < elem.attributes.length; i++) { - const attr = elem.attributes.item(i); + for (const attr of [...elem.attributes]) { const attrName = attr.name.toUpperCase(); if (attrName !== "STYLE") { @@ -528,8 +533,7 @@ let filterExternalSpan = function (elem: HTMLElement) { } // filter styling - for (let i = 0; i < elem.style.length; i++) { - const name = elem.style.item(i); + for (const name of [...elem.style]) { const value = elem.style.getPropertyValue(name); if ( @@ -575,8 +579,7 @@ let filterNode = function (node: Node, extendedMode: boolean): void { // descend first, and take a copy of the child nodes as the loop will skip // elements due to node modifications otherwise - for (let i = 0; i < node.children.length; i++) { - const child = node.children[i]; + for (const child of [...node.children]) { filterNode(child, extendedMode); } @@ -600,8 +603,7 @@ let filterNode = function (node: Node, extendedMode: boolean): void { tag(node); } else { // allowed, filter out attributes - for (let i = 0; i < node.attributes.length; i++) { - const attr = node.attributes.item(i); + for (const attr of [...node.attributes]) { const attrName = attr.name.toUpperCase(); if (tag.attrs.indexOf(attrName) === -1) { node.removeAttributeNode(attr); diff --git a/qt/aqt/data/web/js/tsconfig.json b/qt/aqt/data/web/js/tsconfig.json index fab6760c2..e15ecfe4f 100644 --- a/qt/aqt/data/web/js/tsconfig.json +++ b/qt/aqt/data/web/js/tsconfig.json @@ -2,7 +2,7 @@ "compilerOptions": { "target": "es6", "module": "commonjs", - "lib": ["es6", "dom"], + "lib": ["es6", "dom", "dom.iterable"], "strict": true, "noImplicitAny": false, "strictNullChecks": false, diff --git a/ts/tsconfig.json b/ts/tsconfig.json index d25f28372..4e8f96d8c 100644 --- a/ts/tsconfig.json +++ b/ts/tsconfig.json @@ -3,7 +3,7 @@ "compilerOptions": { "target": "es6", "module": "es6", - "lib": ["es2016", "es2019.array", "dom"], + "lib": ["es2016", "es2019.array", "dom", "dom.iterable"], "baseUrl": ".", "paths": { "anki/*": ["../bazel-bin/ts/lib/*"] From 27a1e81088721b70d2d6ec32b7fd74527315a681 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 19 Jan 2021 02:54:15 +0100 Subject: [PATCH 6/9] Remove code which supposedly fixing focus, but which is not functional --- qt/aqt/data/web/js/editor.ts | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index 750363def..7eafd2abb 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -210,10 +210,6 @@ function onFocus(elem: HTMLElement): void { currentField = elem; pycmd(`focus:${currentFieldOrdinal()}`); enableButtons(); - // don't adjust cursor on mouse clicks - if (mouseDown) { - return; - } // do this twice so that there's no flicker on newer versions caretToEnd(); // scroll if bottom of element off the screen @@ -622,13 +618,6 @@ let adjustFieldsTopMargin = function (): void { let mouseDown = 0; $(function (): void { - document.body.addEventListener("mousedown", (): void => { - mouseDown++; - }); - document.body.addEventListener("mouseup", (): void => { - mouseDown--; - }); - document.addEventListener("click", (evt: MouseEvent): void => { const src = evt.target as Element; if (src.tagName === "IMG") { From 5b24e5c522e1719c0be11cb270f9ccc20ecf3072 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 19 Jan 2021 03:06:44 +0100 Subject: [PATCH 7/9] Remove some unnecessary jQuery, replace some toggles with classList.toggle --- qt/aqt/data/web/js/editor.ts | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index 7eafd2abb..54b703ac3 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -167,11 +167,8 @@ function onInput(): void { function updateButtonState(): void { const buts = ["bold", "italic", "underline", "superscript", "subscript"]; for (const name of buts) { - if (document.queryCommandState(name)) { - $(`#${name}`).addClass("highlighted"); - } else { - $(`#${name}`).removeClass("highlighted"); - } + const elem = document.querySelector(`#${name}`) as HTMLElement; + elem.classList.toggle("highlighted", document.queryCommandState(name)); } // fixme: forecolor @@ -179,12 +176,8 @@ function updateButtonState(): void { } function toggleEditorButton(buttonid: string): void { - const button = $(buttonid); - if (button.hasClass("highlighted")) { - button.removeClass("highlighted"); - } else { - button.addClass("highlighted"); - } + const button = $(buttonid)[0]; + button.classList.toggle("highlighted"); } function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { @@ -400,11 +393,8 @@ function setFields(fields: [string, string][]): void { function setBackgrounds(cols: "dupe"[]) { for (let i = 0; i < cols.length; i++) { - if (cols[i] === "dupe") { - $(`#f${i}`).addClass("dupe"); - } else { - $(`#f${i}`).removeClass("dupe"); - } + const element = document.querySelector(`#f${i}`) + element.classList.toggle("dupe", cols[i] === "dupe") } } @@ -446,7 +436,9 @@ let filterHTML = function ( extendedMode: boolean ): string { // wrap it in as we aren't allowed to change top level elements - const top = $.parseHTML(`${html}`)[0] as Element; + const top = document.createElement("ankitop") + top.innerHTML = html; + if (internal) { filterInternalNode(top); } else { From cfc8e34cf019d09f7c7e4440237c37755805d046 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Tue, 19 Jan 2021 03:23:29 +0100 Subject: [PATCH 8/9] Remove jQuery from most top functions, and avoid waiting for jquery load --- qt/aqt/data/web/js/editor.ts | 54 +++++++++++++++++------------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index 54b703ac3..af89b4e7c 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -12,12 +12,10 @@ declare interface String { /* kept for compatibility with add-ons */ String.prototype.format = function (...args: string[]): string { return this.replace(/\{\d+\}/g, (m: string): void => { - const match = m.match(/\d+/) + const match = m.match(/\d+/); - return match - ? args[match[0]] - : ""; - }) + return match ? args[match[0]] : ""; + }); }; function setFGButton(col: string): void { @@ -393,8 +391,8 @@ function setFields(fields: [string, string][]): void { function setBackgrounds(cols: "dupe"[]) { for (let i = 0; i < cols.length; i++) { - const element = document.querySelector(`#f${i}`) - element.classList.toggle("dupe", cols[i] === "dupe") + const element = document.querySelector(`#f${i}`); + element.classList.toggle("dupe", cols[i] === "dupe"); } } @@ -436,7 +434,7 @@ let filterHTML = function ( extendedMode: boolean ): string { // wrap it in as we aren't allowed to change top level elements - const top = document.createElement("ankitop") + const top = document.createElement("ankitop"); top.innerHTML = html; if (internal) { @@ -607,31 +605,31 @@ let adjustFieldsTopMargin = function (): void { document.getElementById("fields").style.marginTop = `${margin}px`; }; -let mouseDown = 0; - -$(function (): void { - document.addEventListener("click", (evt: MouseEvent): void => { - const src = evt.target as Element; - if (src.tagName === "IMG") { - // image clicked; find contenteditable parent - let p = src; - while ((p = p.parentNode as Element)) { - if (p.className === "field") { - $(`#${p.id}`).focus(); - break; - } +document.addEventListener("click", (evt: MouseEvent): void => { + const src = evt.target as Element; + if (src.tagName === "IMG") { + // image clicked; find contenteditable parent + let p = src; + while ((p = p.parentNode as Element)) { + if (p.className === "field") { + document.getElementById(p.id).focus(); + break; } } - }); + } +}); - // prevent editor buttons from taking focus - $("button.linkb").on("mousedown", function (evt: Event) { +// prevent editor buttons from taking focus +for (const element of document.querySelectorAll("button.linkb")) { + element.addEventListener("mousedown", (evt: Event) => { evt.preventDefault(); }); +} - window.addEventListener("resize", () => { - adjustFieldsTopMargin(); - }); - +window.addEventListener("resize", () => { + adjustFieldsTopMargin(); +}); + +$(function (): void { adjustFieldsTopMargin(); }); From 62629975993476e0920f9206d056be4f6f88d2a0 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Wed, 20 Jan 2021 17:01:16 +0100 Subject: [PATCH 9/9] Explicitly execute code deactivating button focus from editor.py --- qt/aqt/data/web/js/editor.ts | 15 ++++++++------- qt/aqt/editor.py | 1 + 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index af89b4e7c..85483078c 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -294,6 +294,14 @@ function wrappedExceptForWhitespace(text: string, front: string, back: string): return match[1] + front + match[2] + back + match[3]; } +function preventButtonFocus(): void { + for (const element of document.querySelectorAll("button.linkb")) { + element.addEventListener("mousedown", (evt: Event) => { + evt.preventDefault(); + }); + } +} + function disableButtons(): void { $("button.linkb:not(.perm)").prop("disabled", true); } @@ -619,13 +627,6 @@ document.addEventListener("click", (evt: MouseEvent): void => { } }); -// prevent editor buttons from taking focus -for (const element of document.querySelectorAll("button.linkb")) { - element.addEventListener("mousedown", (evt: Event) => { - evt.preventDefault(); - }); -} - window.addEventListener("resize", () => { adjustFieldsTopMargin(); }); diff --git a/qt/aqt/editor.py b/qt/aqt/editor.py index bbe3a4add..342bdb877 100644 --- a/qt/aqt/editor.py +++ b/qt/aqt/editor.py @@ -222,6 +222,7 @@ class Editor: js=["js/vendor/jquery.min.js", "js/editor.js"], context=self, ) + self.web.eval("preventButtonFocus();") # Top buttons ######################################################################