diff --git a/qt/aqt/data/web/js/editor.ts b/qt/aqt/data/web/js/editor.ts index d56b71071..85483078c 100644 --- a/qt/aqt/data/web/js/editor.ts +++ b/qt/aqt/data/web/js/editor.ts @@ -6,22 +6,23 @@ 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 () { - 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: string): void => { + const match = m.match(/\d+/); + + return match ? args[match[0]] : ""; }); }; -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 +37,7 @@ function saveNow(keepFocus) { } } -function triggerKeyTimer() { +function triggerKeyTimer(): void { clearChangeTimer(); changeTimer = setTimeout(function () { updateButtonState(); @@ -48,15 +49,15 @@ 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.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 +74,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; @@ -87,9 +88,9 @@ function onKey(evt: KeyboardEvent) { triggerKeyTimer(); } -function onKeyUp(evt: KeyboardEvent) { +function onKeyUp(evt: KeyboardEvent): void { // 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 ( @@ -105,7 +106,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 +124,7 @@ function inListItem(): boolean { return inList; } -function insertNewline() { +function insertNewline(): void { if (!inPreEnvironment()) { setFormat("insertText", "\n"); return; @@ -156,34 +157,28 @@ 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)) { - $("#" + name).addClass("highlighted"); - } else { - $("#" + name).removeClass("highlighted"); - } + const elem = document.querySelector(`#${name}`) as HTMLElement; + elem.classList.toggle("highlighted", document.queryCommandState(name)); } // fixme: forecolor // 'col': document.queryCommandValue("forecolor") } -function toggleEditorButton(buttonid) { - if ($(buttonid).hasClass("highlighted")) { - $(buttonid).removeClass("highlighted"); - } else { - $(buttonid).addClass("highlighted"); - } +function toggleEditorButton(buttonid: string): void { + const button = $(buttonid)[0]; + button.classList.toggle("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,33 +186,30 @@ 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; } currentField = elem; - pycmd("focus:" + currentFieldOrdinal()); + 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 - 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 +222,14 @@ function onFocus(elem) { } } -function focusField(n) { +function focusField(n: number): void { if (n === null) { return; } - $("#f" + n).focus(); + $(`#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 +244,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 +258,7 @@ function caretToEnd() { s.addRange(r); } -function onBlur() { +function onBlur(): void { if (!currentField) { return; } @@ -281,7 +273,7 @@ function onBlur() { } } -function saveField(type) { +function saveField(type: "blur" | "key"): void { clearChangeTimer(); if (!currentField) { // no field has been focused yet @@ -289,35 +281,37 @@ function saveField(type) { } // type is either 'blur' or 'key' pycmd( - type + - ":" + - currentFieldOrdinal() + - ":" + - currentNoteId + - ":" + - currentField.innerHTML + `${type}:${currentFieldOrdinal()}:${currentNoteId}:${currentField.innerHTML}` ); } -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 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); } -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 +319,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 +351,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 @@ -397,44 +391,44 @@ function setFields(fields) { `; } - $("#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"); - } else { - $("#f" + i).removeClass("dupe"); - } + const element = document.querySelector(`#f${i}`); + element.classList.toggle("dupe", cols[i] === "dupe"); } } -function setFonts(fonts) { +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"; } } -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,9 +436,15 @@ 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; + const top = document.createElement("ankitop"); + top.innerHTML = html; + if (internal) { filterInternalNode(top); } else { @@ -516,38 +516,30 @@ 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 (const attr of [...elem.attributes]) { 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 (const name of [...elem.style]) { + 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 +547,33 @@ 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 (const child of [...node.children]) { filterNode(child, extendedMode); } @@ -590,12 +581,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,59 +597,40 @@ let filterNode = function (node, extendedMode) { tag(node); } else { // allowed, filter out attributes - const toRemove = []; - for (const attr of node.attributes) { + for (const attr of [...node.attributes]) { 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"; + document.getElementById("fields").style.marginTop = `${margin}px`; }; -let mouseDown = 0; - -$(function () { - document.body.onmousedown = function () { - mouseDown++; - }; - - document.body.onmouseup = function () { - mouseDown--; - }; - - document.onclick = function (evt: MouseEvent) { - 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 (e) { - e.preventDefault(); - }); - - window.onresize = function () { - adjustFieldsTopMargin(); - }; + } +}); +window.addEventListener("resize", () => { + adjustFieldsTopMargin(); +}); + +$(function (): void { adjustFieldsTopMargin(); }); 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/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 ###################################################################### 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/*"]