Avoid making currentField a global

This commit is contained in:
Henrik Giesel 2021-02-08 17:00:27 +01:00
parent 2637a4955b
commit ef14000afd
2 changed files with 55 additions and 60 deletions

View file

@ -10,7 +10,8 @@ html {
flex-direction: column; flex-direction: column;
margin: 5px; margin: 5px;
& > *, & > * > * { & > *,
& > * > * {
margin: 1px 0; margin: 1px 0;
&:first-child { &:first-child {

View file

@ -5,7 +5,6 @@ import { filterHTML } from "./filterHtml";
import { nodeIsElement, nodeIsInline } from "./helpers"; import { nodeIsElement, nodeIsInline } from "./helpers";
import { bridgeCommand } from "./lib"; import { bridgeCommand } from "./lib";
let currentField: EditingArea | null = null;
let changeTimer: number | null = null; let changeTimer: number | null = null;
let currentNoteId: number | null = null; let currentNoteId: number | null = null;
@ -19,18 +18,18 @@ declare global {
} }
export function getCurrentField(): EditingArea | null { export function getCurrentField(): EditingArea | null {
return currentField; return document.activeElement instanceof EditingArea
} ? document.activeElement
: null;
export function getCurrentNoteId(): number | null {
return currentNoteId;
} }
export function setFGButton(col: string): void { export function setFGButton(col: string): void {
document.getElementById("forecolor").style.backgroundColor = col; document.getElementById("forecolor")!.style.backgroundColor = col;
} }
export function saveNow(keepFocus: boolean): void { export function saveNow(keepFocus: boolean): void {
const currentField = getCurrentField();
if (!currentField) { if (!currentField) {
return; return;
} }
@ -38,22 +37,24 @@ export function saveNow(keepFocus: boolean): void {
clearChangeTimer(); clearChangeTimer();
if (keepFocus) { if (keepFocus) {
saveField("key"); saveField(currentField, "key");
} else { } else {
// triggers onBlur, which saves // triggers onBlur, which saves
currentField.blurEditable(); currentField.blurEditable();
} }
} }
function triggerKeyTimer(): void { function triggerKeyTimer(currentField: EditingArea): void {
clearChangeTimer(); clearChangeTimer();
changeTimer = setTimeout(function () { changeTimer = setTimeout(function () {
updateButtonState(); updateButtonState();
saveField("key"); saveField(currentField, "key");
}, 600); }, 600);
} }
function onKey(evt: KeyboardEvent): void { function onKey(evt: KeyboardEvent): void {
const currentField = evt.currentTarget as EditingArea;
// esc clears focus, allowing dialog to close // esc clears focus, allowing dialog to close
if (evt.code === "Escape") { if (evt.code === "Escape") {
currentField.blurEditable(); currentField.blurEditable();
@ -61,7 +62,7 @@ function onKey(evt: KeyboardEvent): void {
} }
// prefer <br> instead of <div></div> // prefer <br> instead of <div></div>
if (evt.code === "Enter" && !inListItem()) { if (evt.code === "Enter" && !inListItem(currentField)) {
evt.preventDefault(); evt.preventDefault();
document.execCommand("insertLineBreak"); document.execCommand("insertLineBreak");
} }
@ -84,13 +85,15 @@ function onKey(evt: KeyboardEvent): void {
} }
} }
triggerKeyTimer(); triggerKeyTimer(currentField);
} }
function onKeyUp(evt: KeyboardEvent): void { function onKeyUp(evt: KeyboardEvent): void {
const currentField = evt.currentTarget as EditingArea;
// Avoid div element on remove // Avoid div element on remove
if (evt.code === "Enter" || evt.code === "Backspace") { if (evt.code === "Enter" || evt.code === "Backspace") {
const anchor = currentField.getSelection().anchorNode; const anchor = currentField.getSelection().anchorNode as Node;
if ( if (
nodeIsElement(anchor) && nodeIsElement(anchor) &&
@ -104,8 +107,8 @@ function onKeyUp(evt: KeyboardEvent): void {
} }
} }
function inListItem(): boolean { function inListItem(currentField: EditingArea): boolean {
const anchor = currentField.getSelection().anchorNode; const anchor = currentField.getSelection()!.anchorNode!;
let inList = false; let inList = false;
let n = nodeIsElement(anchor) ? anchor : anchor.parentElement; let n = nodeIsElement(anchor) ? anchor : anchor.parentElement;
@ -117,9 +120,9 @@ function inListItem(): boolean {
return inList; return inList;
} }
function onInput(): void { function onInput(event: Event): void {
// make sure IME changes get saved // make sure IME changes get saved
triggerKeyTimer(); triggerKeyTimer(event.currentTarget as EditingArea);
} }
function updateButtonState(): void { function updateButtonState(): void {
@ -141,7 +144,7 @@ export function toggleEditorButton(buttonid: string): void {
export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void { export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
document.execCommand(cmd, false, arg); document.execCommand(cmd, false, arg);
if (!nosave) { if (!nosave) {
saveField("key"); saveField(getCurrentField() as EditingArea, "key");
updateButtonState(); updateButtonState();
} }
} }
@ -154,17 +157,12 @@ function clearChangeTimer(): void {
} }
function onFocus(evt: FocusEvent): void { function onFocus(evt: FocusEvent): void {
const elem = evt.currentTarget as EditingArea; const currentField = evt.currentTarget as EditingArea;
if (currentField === elem) { currentField.focusEditable();
// anki window refocused; current element unchanged
return;
}
elem.focusEditable();
currentField = elem;
bridgeCommand(`focus:${currentField.ord}`); bridgeCommand(`focus:${currentField.ord}`);
enableButtons(); enableButtons();
// do this twice so that there's no flicker on newer versions // do this twice so that there's no flicker on newer versions
caretToEnd(); caretToEnd(currentField);
// scroll if bottom of element off the screen // scroll if bottom of element off the screen
function pos(elem: HTMLElement): number { function pos(elem: HTMLElement): number {
let cur = 0; let cur = 0;
@ -175,12 +173,12 @@ function onFocus(evt: FocusEvent): void {
return cur; return cur;
} }
const y = pos(elem); const y = pos(currentField);
if ( if (
window.pageYOffset + window.innerHeight < y + elem.offsetHeight || window.pageYOffset + window.innerHeight < y + currentField.offsetHeight ||
window.pageYOffset > y window.pageYOffset > y
) { ) {
window.scroll(0, y + elem.offsetHeight - window.innerHeight); window.scroll(0, y + currentField.offsetHeight - window.innerHeight);
} }
} }
@ -198,21 +196,18 @@ export function focusIfField(x: number, y: number): boolean {
let elem = elements[i] as EditingArea; let elem = elements[i] as EditingArea;
if (elem instanceof EditingArea) { if (elem instanceof EditingArea) {
elem.focusEditable(); elem.focusEditable();
// the focus event may not fire if the window is not active, so make sure
// the current field is set
currentField = elem;
return true; return true;
} }
} }
return false; return false;
} }
function onPaste(event: ClipboardEvent): void { function onPaste(evt: ClipboardEvent): void {
bridgeCommand("paste"); bridgeCommand("paste");
event.preventDefault(); evt.preventDefault();
} }
function caretToEnd(): void { function caretToEnd(currentField: EditingArea): void {
const range = document.createRange(); const range = document.createRange();
range.selectNodeContents(currentField.editable); range.selectNodeContents(currentField.editable);
range.collapse(false); range.collapse(false);
@ -221,17 +216,14 @@ function caretToEnd(): void {
selection.addRange(range); selection.addRange(range);
} }
function onBlur(): void { function onBlur(evt: FocusEvent): void {
if (!currentField) { const currentField = evt.currentTarget as EditingArea;
return;
}
if (document.activeElement === currentField) { if (document.activeElement === currentField) {
// other widget or window focused; current field unchanged // other widget or window focused; current field unchanged
saveField("key"); saveField(currentField, "key");
} else { } else {
saveField("blur"); saveField(currentField, "blur");
currentField = null;
disableButtons(); disableButtons();
} }
} }
@ -251,20 +243,15 @@ function containsInlineContent(field: Element): boolean {
return true; return true;
} }
function saveField(type: "blur" | "key"): void { function saveField(currentField: EditingArea, type: "blur" | "key"): void {
clearChangeTimer(); clearChangeTimer();
if (!currentField) {
// no field has been focused yet
return;
}
bridgeCommand( bridgeCommand(
`${type}:${currentField.ord}:${currentNoteId}:${currentField.fieldHTML}` `${type}:${currentField.ord}:${getCurrentNoteId()}:${currentField.fieldHTML}`
); );
} }
function wrappedExceptForWhitespace(text: string, front: string, back: string): string { function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
const match = text.match(/^(\s*)([^]*?)(\s*)$/); const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
return match[1] + front + match[2] + back + match[3]; return match[1] + front + match[2] + back + match[3];
} }
@ -303,11 +290,13 @@ export function wrapIntoText(front: string, back: string): void {
} }
function wrapInternal(front: string, back: string, plainText: boolean): void { function wrapInternal(front: string, back: string, plainText: boolean): void {
const currentField = getCurrentField()!;
const s = currentField.getSelection(); const s = currentField.getSelection();
let r = s.getRangeAt(0); let r = s.getRangeAt(0);
const content = r.cloneContents(); const content = r.cloneContents();
const span = document.createElement("span"); const span = document.createElement("span");
span.appendChild(content); span.appendChild(content);
if (plainText) { if (plainText) {
const new_ = wrappedExceptForWhitespace(span.innerText, front, back); const new_ = wrappedExceptForWhitespace(span.innerText, front, back);
setFormat("inserttext", new_); setFormat("inserttext", new_);
@ -315,6 +304,7 @@ function wrapInternal(front: string, back: string, plainText: boolean): void {
const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back); const new_ = wrappedExceptForWhitespace(span.innerHTML, front, back);
setFormat("inserthtml", new_); setFormat("inserthtml", new_);
} }
if (!span.innerHTML) { if (!span.innerHTML) {
// run with an empty selection; move cursor back past postfix // run with an empty selection; move cursor back past postfix
r = s.getRangeAt(0); r = s.getRangeAt(0);
@ -364,14 +354,14 @@ class EditingArea extends HTMLDivElement {
const rootStyle = document.createElement("link"); const rootStyle = document.createElement("link");
rootStyle.setAttribute("rel", "stylesheet"); rootStyle.setAttribute("rel", "stylesheet");
rootStyle.setAttribute("href", "./_anki/css/editable.css"); rootStyle.setAttribute("href", "./_anki/css/editable.css");
this.shadowRoot.appendChild(rootStyle); this.shadowRoot!.appendChild(rootStyle);
this.baseStyle = document.createElement("style"); this.baseStyle = document.createElement("style");
this.baseStyle.setAttribute("rel", "stylesheet"); this.baseStyle.setAttribute("rel", "stylesheet");
this.shadowRoot.appendChild(this.baseStyle); this.shadowRoot!.appendChild(this.baseStyle);
this.editable = document.createElement("anki-editable") as Editable; this.editable = document.createElement("anki-editable") as Editable;
this.shadowRoot.appendChild(this.editable); this.shadowRoot!.appendChild(this.editable);
} }
get ord(): number { get ord(): number {
@ -435,7 +425,7 @@ class EditingArea extends HTMLDivElement {
} }
getSelection(): Selection { getSelection(): Selection {
return this.shadowRoot.getSelection(); return this.shadowRoot!.getSelection()!;
} }
focusEditable(): void { focusEditable(): void {
@ -498,7 +488,7 @@ class EditorField extends HTMLDivElement {
customElements.define("anki-editor-field", EditorField, { extends: "div" }); customElements.define("anki-editor-field", EditorField, { extends: "div" });
function adjustFieldAmount(amount: number): void { function adjustFieldAmount(amount: number): void {
const fieldsContainer = document.getElementById("fields"); const fieldsContainer = document.getElementById("fields")!;
while (fieldsContainer.childElementCount < amount) { while (fieldsContainer.childElementCount < amount) {
const newField = document.createElement("div", { const newField = document.createElement("div", {
@ -509,12 +499,12 @@ function adjustFieldAmount(amount: number): void {
} }
while (fieldsContainer.childElementCount > amount) { while (fieldsContainer.childElementCount > amount) {
fieldsContainer.removeChild(fieldsContainer.lastElementChild); fieldsContainer.removeChild(fieldsContainer.lastElementChild as Node);
} }
} }
export function getEditorField(n: number): EditorField | null { export function getEditorField(n: number): EditorField | null {
const fields = document.getElementById("fields").children; const fields = document.getElementById("fields")!.children;
return (fields[n] as EditorField) ?? null; return (fields[n] as EditorField) ?? null;
} }
@ -522,7 +512,7 @@ export function forEditorField<T>(
values: T[], values: T[],
func: (field: EditorField, value: T) => void func: (field: EditorField, value: T) => void
): void { ): void {
const fields = document.getElementById("fields").children; const fields = document.getElementById("fields")!.children;
for (let i = 0; i < fields.length; i++) { for (let i = 0; i < fields.length; i++) {
const field = fields[i] as EditorField; const field = fields[i] as EditorField;
func(field, values[i]); func(field, values[i]);
@ -549,7 +539,7 @@ export function setBackgrounds(cols: ("dupe" | "")[]) {
field.editingArea.classList.toggle("dupe", value === "dupe") field.editingArea.classList.toggle("dupe", value === "dupe")
); );
document document
.querySelector("#dupes") .getElementById("dupes")!
.classList.toggle("is-inactive", !cols.includes("dupe")); .classList.toggle("is-inactive", !cols.includes("dupe"));
} }
@ -563,6 +553,10 @@ export function setNoteId(id: number): void {
currentNoteId = id; currentNoteId = id;
} }
export function getCurrentNoteId(): number | null {
return currentNoteId;
}
export let pasteHTML = function ( export let pasteHTML = function (
html: string, html: string,
internal: boolean, internal: boolean,