From 2f46c69ed6e0fdc8e4a285d5c2c86826d0d112f4 Mon Sep 17 00:00:00 2001 From: Henrik Giesel Date: Sun, 28 Feb 2021 14:12:48 +0100 Subject: [PATCH] Separate index.ts into editable/editingArea/editorField/labelContainer.ts --- ts/editor/changeTimer.ts | 2 +- ts/editor/editable.ts | 36 +++++ ts/editor/editingArea.ts | 115 ++++++++++++++++ ts/editor/editorField.ts | 45 +++++++ ts/editor/focusHandlers.ts | 4 +- ts/editor/index.ts | 254 ++---------------------------------- ts/editor/inputHandlers.ts | 2 +- ts/editor/labelContainer.ts | 45 +++++++ 8 files changed, 256 insertions(+), 247 deletions(-) create mode 100644 ts/editor/editable.ts create mode 100644 ts/editor/editingArea.ts create mode 100644 ts/editor/editorField.ts create mode 100644 ts/editor/labelContainer.ts diff --git a/ts/editor/changeTimer.ts b/ts/editor/changeTimer.ts index 7a123773b..fc7c22bbe 100644 --- a/ts/editor/changeTimer.ts +++ b/ts/editor/changeTimer.ts @@ -1,7 +1,7 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import type { EditingArea } from "."; +import type { EditingArea } from "./editingArea"; import { getCurrentField } from "."; import { bridgeCommand } from "./lib"; diff --git a/ts/editor/editable.ts b/ts/editor/editable.ts new file mode 100644 index 000000000..69eec5624 --- /dev/null +++ b/ts/editor/editable.ts @@ -0,0 +1,36 @@ +import { nodeIsInline } from "./helpers"; + +function containsInlineContent(field: Element): boolean { + if (field.childNodes.length === 0) { + // for now, for all practical purposes, empty fields are in block mode + return false; + } + + for (const child of field.children) { + if (!nodeIsInline(child)) { + return false; + } + } + + return true; +} + +export class Editable extends HTMLElement { + set fieldHTML(content: string) { + this.innerHTML = content; + + if (containsInlineContent(this)) { + this.appendChild(document.createElement("br")); + } + } + + get fieldHTML(): string { + return containsInlineContent(this) && this.innerHTML.endsWith("
") + ? this.innerHTML.slice(0, -4) // trim trailing
+ : this.innerHTML; + } + + connectedCallback() { + this.setAttribute("contenteditable", ""); + } +} diff --git a/ts/editor/editingArea.ts b/ts/editor/editingArea.ts new file mode 100644 index 000000000..a6b3d74f7 --- /dev/null +++ b/ts/editor/editingArea.ts @@ -0,0 +1,115 @@ +import type { Editable } from "./editable"; + +import { bridgeCommand } from "./lib"; +import { onInput, onKey, onKeyUp } from "./inputHandlers"; +import { onFocus, onBlur } from "./focusHandlers"; +import { updateButtonState } from "./toolbar"; + +function onPaste(evt: ClipboardEvent): void { + bridgeCommand("paste"); + evt.preventDefault(); +} + +function onCutOrCopy(): boolean { + bridgeCommand("cutOrCopy"); + return true; +} + +export class EditingArea extends HTMLDivElement { + editable: Editable; + baseStyle: HTMLStyleElement; + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.className = "field"; + + const rootStyle = document.createElement("link"); + rootStyle.setAttribute("rel", "stylesheet"); + rootStyle.setAttribute("href", "./_anki/css/editable.css"); + this.shadowRoot!.appendChild(rootStyle); + + this.baseStyle = document.createElement("style"); + this.baseStyle.setAttribute("rel", "stylesheet"); + this.shadowRoot!.appendChild(this.baseStyle); + + this.editable = document.createElement("anki-editable") as Editable; + this.shadowRoot!.appendChild(this.editable); + } + + get ord(): number { + return Number(this.getAttribute("ord")); + } + + set fieldHTML(content: string) { + this.editable.fieldHTML = content; + } + + get fieldHTML(): string { + return this.editable.fieldHTML; + } + + connectedCallback(): void { + this.addEventListener("keydown", onKey); + this.addEventListener("keyup", onKeyUp); + this.addEventListener("input", onInput); + this.addEventListener("focus", onFocus); + this.addEventListener("blur", onBlur); + this.addEventListener("paste", onPaste); + this.addEventListener("copy", onCutOrCopy); + this.addEventListener("oncut", onCutOrCopy); + this.addEventListener("mouseup", updateButtonState); + + const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; + baseStyleSheet.insertRule("anki-editable {}", 0); + } + + disconnectedCallback(): void { + this.removeEventListener("keydown", onKey); + this.removeEventListener("keyup", onKeyUp); + this.removeEventListener("input", onInput); + this.removeEventListener("focus", onFocus); + this.removeEventListener("blur", onBlur); + this.removeEventListener("paste", onPaste); + this.removeEventListener("copy", onCutOrCopy); + this.removeEventListener("oncut", onCutOrCopy); + this.removeEventListener("mouseup", updateButtonState); + } + + initialize(color: string, content: string): void { + this.setBaseColor(color); + this.editable.fieldHTML = content; + } + + setBaseColor(color: string): void { + const styleSheet = this.baseStyle.sheet as CSSStyleSheet; + const firstRule = styleSheet.cssRules[0] as CSSStyleRule; + firstRule.style.color = color; + } + + setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { + const styleSheet = this.baseStyle.sheet as CSSStyleSheet; + const firstRule = styleSheet.cssRules[0] as CSSStyleRule; + firstRule.style.fontFamily = fontFamily; + firstRule.style.fontSize = fontSize; + firstRule.style.direction = direction; + } + + isRightToLeft(): boolean { + const styleSheet = this.baseStyle.sheet as CSSStyleSheet; + const firstRule = styleSheet.cssRules[0] as CSSStyleRule; + return firstRule.style.direction === "rtl"; + } + + getSelection(): Selection { + return this.shadowRoot!.getSelection()!; + } + + focusEditable(): void { + this.editable.focus(); + } + + blurEditable(): void { + this.editable.blur(); + } +} diff --git a/ts/editor/editorField.ts b/ts/editor/editorField.ts new file mode 100644 index 000000000..eb0b3005e --- /dev/null +++ b/ts/editor/editorField.ts @@ -0,0 +1,45 @@ +import type { EditingArea } from "./editingArea"; +import type { LabelContainer } from "./labelContainer"; + +export class EditorField extends HTMLDivElement { + labelContainer: LabelContainer; + editingArea: EditingArea; + + constructor() { + super(); + this.labelContainer = document.createElement("div", { + is: "anki-label-container", + }) as LabelContainer; + this.appendChild(this.labelContainer); + + this.editingArea = document.createElement("div", { + is: "anki-editing-area", + }) as EditingArea; + this.appendChild(this.editingArea); + } + + static get observedAttributes(): string[] { + return ["ord"]; + } + + set ord(n: number) { + this.setAttribute("ord", String(n)); + } + + attributeChangedCallback(name: string, _oldValue: string, newValue: string): void { + switch (name) { + case "ord": + this.editingArea.setAttribute("ord", newValue); + this.labelContainer.setAttribute("ord", newValue); + } + } + + initialize(label: string, color: string, content: string): void { + this.labelContainer.initialize(label); + this.editingArea.initialize(color, content); + } + + setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { + this.editingArea.setBaseStyling(fontFamily, fontSize, direction); + } +} diff --git a/ts/editor/focusHandlers.ts b/ts/editor/focusHandlers.ts index 1828d957d..ec676f6db 100644 --- a/ts/editor/focusHandlers.ts +++ b/ts/editor/focusHandlers.ts @@ -1,11 +1,11 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import type { EditingArea } from "."; +import type { EditingArea } from "./editingArea"; +import { saveField } from "./changeTimer"; import { bridgeCommand } from "./lib"; import { enableButtons, disableButtons } from "./toolbar"; -import { saveField } from "./changeTimer"; export function onFocus(evt: FocusEvent): void { const currentField = evt.currentTarget as EditingArea; diff --git a/ts/editor/index.ts b/ts/editor/index.ts index d1d9596de..b826fd075 100644 --- a/ts/editor/index.ts +++ b/ts/editor/index.ts @@ -1,13 +1,15 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import { nodeIsInline, caretToEnd } from "./helpers"; -import { bridgeCommand } from "./lib"; +import { caretToEnd } from "./helpers"; import { saveField } from "./changeTimer"; import { filterHTML } from "./htmlFilter"; import { updateButtonState } from "./toolbar"; -import { onInput, onKey, onKeyUp } from "./inputHandlers"; -import { onFocus, onBlur } from "./focusHandlers"; + +import { EditorField } from "./editorField"; +import { LabelContainer } from "./labelContainer"; +import { EditingArea } from "./editingArea"; +import { Editable } from "./editable"; export { setNoteId, getNoteId } from "./noteId"; export { preventButtonFocus, toggleEditorButton, setFGButton } from "./toolbar"; @@ -23,6 +25,11 @@ declare global { } } +customElements.define("anki-editable", Editable); +customElements.define("anki-editing-area", EditingArea, { extends: "div" }); +customElements.define("anki-label-container", LabelContainer, { extends: "div" }); +customElements.define("anki-editor-field", EditorField, { extends: "div" }); + export function getCurrentField(): EditingArea | null { return document.activeElement instanceof EditingArea ? document.activeElement @@ -63,245 +70,6 @@ export function pasteHTML( } } -function onPaste(evt: ClipboardEvent): void { - bridgeCommand("paste"); - evt.preventDefault(); -} - -function onCutOrCopy(): boolean { - bridgeCommand("cutOrCopy"); - return true; -} - -function containsInlineContent(field: Element): boolean { - if (field.childNodes.length === 0) { - // for now, for all practical purposes, empty fields are in block mode - return false; - } - - for (const child of field.children) { - if (!nodeIsInline(child)) { - return false; - } - } - - return true; -} - -class Editable extends HTMLElement { - set fieldHTML(content: string) { - this.innerHTML = content; - - if (containsInlineContent(this)) { - this.appendChild(document.createElement("br")); - } - } - - get fieldHTML(): string { - return containsInlineContent(this) && this.innerHTML.endsWith("
") - ? this.innerHTML.slice(0, -4) // trim trailing
- : this.innerHTML; - } - - connectedCallback() { - this.setAttribute("contenteditable", ""); - } -} - -customElements.define("anki-editable", Editable); - -export class EditingArea extends HTMLDivElement { - editable: Editable; - baseStyle: HTMLStyleElement; - - constructor() { - super(); - this.attachShadow({ mode: "open" }); - this.className = "field"; - - const rootStyle = document.createElement("link"); - rootStyle.setAttribute("rel", "stylesheet"); - rootStyle.setAttribute("href", "./_anki/css/editable.css"); - this.shadowRoot!.appendChild(rootStyle); - - this.baseStyle = document.createElement("style"); - this.baseStyle.setAttribute("rel", "stylesheet"); - this.shadowRoot!.appendChild(this.baseStyle); - - this.editable = document.createElement("anki-editable") as Editable; - this.shadowRoot!.appendChild(this.editable); - } - - get ord(): number { - return Number(this.getAttribute("ord")); - } - - set fieldHTML(content: string) { - this.editable.fieldHTML = content; - } - - get fieldHTML(): string { - return this.editable.fieldHTML; - } - - connectedCallback(): void { - this.addEventListener("keydown", onKey); - this.addEventListener("keyup", onKeyUp); - this.addEventListener("input", onInput); - this.addEventListener("focus", onFocus); - this.addEventListener("blur", onBlur); - this.addEventListener("paste", onPaste); - this.addEventListener("copy", onCutOrCopy); - this.addEventListener("oncut", onCutOrCopy); - this.addEventListener("mouseup", updateButtonState); - - const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet; - baseStyleSheet.insertRule("anki-editable {}", 0); - } - - disconnectedCallback(): void { - this.removeEventListener("keydown", onKey); - this.removeEventListener("keyup", onKeyUp); - this.removeEventListener("input", onInput); - this.removeEventListener("focus", onFocus); - this.removeEventListener("blur", onBlur); - this.removeEventListener("paste", onPaste); - this.removeEventListener("copy", onCutOrCopy); - this.removeEventListener("oncut", onCutOrCopy); - this.removeEventListener("mouseup", updateButtonState); - } - - initialize(color: string, content: string): void { - this.setBaseColor(color); - this.editable.fieldHTML = content; - } - - setBaseColor(color: string): void { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - firstRule.style.color = color; - } - - setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - firstRule.style.fontFamily = fontFamily; - firstRule.style.fontSize = fontSize; - firstRule.style.direction = direction; - } - - isRightToLeft(): boolean { - const styleSheet = this.baseStyle.sheet as CSSStyleSheet; - const firstRule = styleSheet.cssRules[0] as CSSStyleRule; - return firstRule.style.direction === "rtl"; - } - - getSelection(): Selection { - return this.shadowRoot!.getSelection()!; - } - - focusEditable(): void { - this.editable.focus(); - } - - blurEditable(): void { - this.editable.blur(); - } -} - -customElements.define("anki-editing-area", EditingArea, { extends: "div" }); - -export class LabelContainer extends HTMLDivElement { - sticky: HTMLSpanElement; - label: HTMLSpanElement; - - constructor() { - super(); - this.className = "d-flex"; - - this.sticky = document.createElement("span"); - this.sticky.className = "bi bi-pin-angle-fill me-1 sticky-icon"; - this.sticky.hidden = true; - this.appendChild(this.sticky); - - this.label = document.createElement("span"); - this.label.className = "fieldname"; - this.appendChild(this.label); - - this.toggleSticky = this.toggleSticky.bind(this); - } - - connectedCallback(): void { - this.sticky.addEventListener("click", this.toggleSticky); - } - - disconnectedCallback(): void { - this.sticky.removeEventListener("click", this.toggleSticky); - } - - initialize(labelName: string): void { - this.label.innerText = labelName; - } - - activateSticky(initialState: boolean): void { - this.sticky.classList.toggle("is-active", initialState); - this.sticky.hidden = false; - } - - toggleSticky(): void { - bridgeCommand(`toggleSticky:${this.getAttribute("ord")}`, () => { - this.sticky.classList.toggle("is-active"); - }); - } -} - -customElements.define("anki-label-container", LabelContainer, { extends: "div" }); - -export class EditorField extends HTMLDivElement { - labelContainer: LabelContainer; - editingArea: EditingArea; - - constructor() { - super(); - this.labelContainer = document.createElement("div", { - is: "anki-label-container", - }) as LabelContainer; - this.appendChild(this.labelContainer); - - this.editingArea = document.createElement("div", { - is: "anki-editing-area", - }) as EditingArea; - this.appendChild(this.editingArea); - } - - static get observedAttributes(): string[] { - return ["ord"]; - } - - set ord(n: number) { - this.setAttribute("ord", String(n)); - } - - attributeChangedCallback(name: string, _oldValue: string, newValue: string): void { - switch (name) { - case "ord": - this.editingArea.setAttribute("ord", newValue); - this.labelContainer.setAttribute("ord", newValue); - } - } - - initialize(label: string, color: string, content: string): void { - this.labelContainer.initialize(label); - this.editingArea.initialize(color, content); - } - - setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { - this.editingArea.setBaseStyling(fontFamily, fontSize, direction); - } -} - -customElements.define("anki-editor-field", EditorField, { extends: "div" }); - function adjustFieldAmount(amount: number): void { const fieldsContainer = document.getElementById("fields")!; diff --git a/ts/editor/inputHandlers.ts b/ts/editor/inputHandlers.ts index 052575614..8a4d5b28b 100644 --- a/ts/editor/inputHandlers.ts +++ b/ts/editor/inputHandlers.ts @@ -1,7 +1,7 @@ /* Copyright: Ankitects Pty Ltd and contributors * License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */ -import { EditingArea } from "."; +import { EditingArea } from "./editingArea"; import { caretToEnd, nodeIsElement } from "./helpers"; import { triggerChangeTimer } from "./changeTimer"; import { updateButtonState } from "./toolbar"; diff --git a/ts/editor/labelContainer.ts b/ts/editor/labelContainer.ts new file mode 100644 index 000000000..3d622ed69 --- /dev/null +++ b/ts/editor/labelContainer.ts @@ -0,0 +1,45 @@ +import { bridgeCommand } from "./lib"; + +export class LabelContainer extends HTMLDivElement { + sticky: HTMLSpanElement; + label: HTMLSpanElement; + + constructor() { + super(); + this.className = "d-flex"; + + this.sticky = document.createElement("span"); + this.sticky.className = "bi bi-pin-angle-fill me-1 sticky-icon"; + this.sticky.hidden = true; + this.appendChild(this.sticky); + + this.label = document.createElement("span"); + this.label.className = "fieldname"; + this.appendChild(this.label); + + this.toggleSticky = this.toggleSticky.bind(this); + } + + connectedCallback(): void { + this.sticky.addEventListener("click", this.toggleSticky); + } + + disconnectedCallback(): void { + this.sticky.removeEventListener("click", this.toggleSticky); + } + + initialize(labelName: string): void { + this.label.innerText = labelName; + } + + activateSticky(initialState: boolean): void { + this.sticky.classList.toggle("is-active", initialState); + this.sticky.hidden = false; + } + + toggleSticky(): void { + bridgeCommand(`toggleSticky:${this.getAttribute("ord")}`, () => { + this.sticky.classList.toggle("is-active"); + }); + } +}