Introduce editable-container

Contains the shadow root, and references to the styles.
Is ignorant of Editable.
Is necessary, so our we editable.scss does not need to contain
information about Codable, ImageHandle or all those other things which
have nothing to do with Editable
This commit is contained in:
Damien Elmes 2021-07-19 23:27:11 +10:00
parent 6909a095d5
commit 41c4be2f54
10 changed files with 112 additions and 58 deletions

View file

@ -3,7 +3,6 @@ Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
--> -->
<script lang="typescript"> <script lang="typescript">
import type { EditingArea } from "./editing-area";
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
@ -15,7 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import OnlyEditable from "./OnlyEditable.svelte"; import OnlyEditable from "./OnlyEditable.svelte";
import CommandIconButton from "./CommandIconButton.svelte"; import CommandIconButton from "./CommandIconButton.svelte";
import { getListItem } from "./helpers"; import { getCurrentField, getListItem } from "./helpers";
import { import {
ulIcon, ulIcon,
olIcon, olIcon,
@ -31,8 +30,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let api = {}; export let api = {};
function outdentListItem() { function outdentListItem() {
const currentField = document.activeElement as EditingArea; const currentField = getCurrentField();
if (getListItem(currentField.shadowRoot!)) { if (getListItem(currentField.editableContainer.shadowRoot!)) {
document.execCommand("outdent"); document.execCommand("outdent");
} else { } else {
alert("Indent/unindent currently only works with lists."); alert("Indent/unindent currently only works with lists.");
@ -40,8 +39,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
function indentListItem() { function indentListItem() {
const currentField = document.activeElement as EditingArea; const currentField = getCurrentField();
if (getListItem(currentField.shadowRoot!)) { if (getListItem(currentField.editableContainer.shadowRoot!)) {
document.execCommand("indent"); document.execCommand("indent");
} else { } else {
alert("Indent/unindent currently only works with lists."); alert("Indent/unindent currently only works with lists.");

View file

@ -18,8 +18,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import OnlyEditable from "./OnlyEditable.svelte"; import OnlyEditable from "./OnlyEditable.svelte";
import ClozeButton from "./ClozeButton.svelte"; import ClozeButton from "./ClozeButton.svelte";
import { getCurrentField } from "."; import { getCurrentField, appendInParentheses } from "./helpers";
import { appendInParentheses } from "./helpers";
import { wrapCurrent } from "./wrap"; import { wrapCurrent } from "./wrap";
import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons"; import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons";

View file

@ -3,7 +3,7 @@
import type { EditingArea } from "./editing-area"; import type { EditingArea } from "./editing-area";
import { getCurrentField } from "."; import { getCurrentField } from "./helpers";
import { bridgeCommand } from "./lib"; import { bridgeCommand } from "./lib";
import { getNoteId } from "./note-id"; import { getNoteId } from "./note-id";
@ -23,9 +23,8 @@ function clearChangeTimer(): void {
export function saveField(currentField: EditingArea, type: "blur" | "key"): void { export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
clearChangeTimer(); clearChangeTimer();
bridgeCommand( const command = `${type}:${currentField.ord}:${getNoteId()}:${currentField.fieldHTML}`
`${type}:${currentField.ord}:${getNoteId()}:${currentField.fieldHTML}` bridgeCommand(command);
);
} }
export function saveNow(keepFocus: boolean): void { export function saveNow(keepFocus: boolean): void {

View file

@ -0,0 +1,57 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
/* eslint
@typescript-eslint/no-non-null-assertion: "off",
*/
export class EditableContainer extends HTMLDivElement {
baseStyle: HTMLStyleElement;
constructor() {
super();
const shadow = this.attachShadow({ mode: "open" });
if (document.documentElement.classList.contains("night-mode")) {
this.classList.add("night-mode");
}
const rootStyle = document.createElement("link");
rootStyle.setAttribute("rel", "stylesheet");
rootStyle.setAttribute("href", "./_anki/css/editable.css");
shadow.appendChild(rootStyle);
this.baseStyle = document.createElement("style");
this.baseStyle.setAttribute("rel", "stylesheet");
shadow.appendChild(this.baseStyle);
}
connectedCallback(): void {
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
baseStyleSheet.insertRule("anki-editable {}", 0);
}
initialize(color: string): void {
this.setBaseColor(color);
}
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";
}
}

View file

@ -5,6 +5,7 @@
@typescript-eslint/no-non-null-assertion: "off", @typescript-eslint/no-non-null-assertion: "off",
*/ */
import type { EditableContainer } from "./editable-container";
import type { Editable } from "./editable"; import type { Editable } from "./editable";
import type { Codable } from "./codable"; import type { Codable } from "./codable";
@ -18,35 +19,25 @@ function onCutOrCopy(): void {
} }
export class EditingArea extends HTMLDivElement { export class EditingArea extends HTMLDivElement {
editableContainer: EditableContainer;
editable: Editable; editable: Editable;
codable: Codable; codable: Codable;
baseStyle: HTMLStyleElement;
constructor() { constructor() {
super(); super();
this.attachShadow({ mode: "open" });
this.className = "field"; this.className = "field";
if (document.documentElement.classList.contains("night-mode")) { this.editableContainer = document.createElement("div", {
this.classList.add("night-mode"); is: "anki-editable-container",
} }) as EditableContainer;
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.editable = document.createElement("anki-editable") as Editable;
this.shadowRoot!.appendChild(this.editable); this.editableContainer.shadowRoot!.appendChild(this.editable);
this.appendChild(this.editableContainer);
this.codable = document.createElement("textarea", { this.codable = document.createElement("textarea", {
is: "anki-codable", is: "anki-codable",
}) as Codable; }) as Codable;
this.shadowRoot!.appendChild(this.codable); this.appendChild(this.codable);
this.onPaste = this.onPaste.bind(this); this.onPaste = this.onPaste.bind(this);
} }
@ -71,23 +62,20 @@ export class EditingArea extends HTMLDivElement {
this.addEventListener("keydown", onKey); this.addEventListener("keydown", onKey);
this.addEventListener("keyup", onKeyUp); this.addEventListener("keyup", onKeyUp);
this.addEventListener("input", onInput); this.addEventListener("input", onInput);
this.addEventListener("focus", onFocus); this.addEventListener("focusin", onFocus);
this.addEventListener("blur", onBlur); this.addEventListener("focusout", onBlur);
this.addEventListener("paste", this.onPaste); this.addEventListener("paste", this.onPaste);
this.addEventListener("copy", onCutOrCopy); this.addEventListener("copy", onCutOrCopy);
this.addEventListener("oncut", onCutOrCopy); this.addEventListener("oncut", onCutOrCopy);
this.addEventListener("mouseup", updateActiveButtons); this.addEventListener("mouseup", updateActiveButtons);
const baseStyleSheet = this.baseStyle.sheet as CSSStyleSheet;
baseStyleSheet.insertRule("anki-editable {}", 0);
} }
disconnectedCallback(): void { disconnectedCallback(): void {
this.removeEventListener("keydown", onKey); this.removeEventListener("keydown", onKey);
this.removeEventListener("keyup", onKeyUp); this.removeEventListener("keyup", onKeyUp);
this.removeEventListener("input", onInput); this.removeEventListener("input", onInput);
this.removeEventListener("focus", onFocus); this.removeEventListener("focusin", onFocus);
this.removeEventListener("blur", onBlur); this.removeEventListener("focusout", onBlur);
this.removeEventListener("paste", this.onPaste); this.removeEventListener("paste", this.onPaste);
this.removeEventListener("copy", onCutOrCopy); this.removeEventListener("copy", onCutOrCopy);
this.removeEventListener("oncut", onCutOrCopy); this.removeEventListener("oncut", onCutOrCopy);
@ -100,9 +88,7 @@ export class EditingArea extends HTMLDivElement {
} }
setBaseColor(color: string): void { setBaseColor(color: string): void {
const styleSheet = this.baseStyle.sheet as CSSStyleSheet; this.editableContainer.setBaseColor(color);
const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
firstRule.style.color = color;
} }
quoteFontFamily(fontFamily: string): string { quoteFontFamily(fontFamily: string): string {
@ -114,17 +100,15 @@ export class EditingArea extends HTMLDivElement {
} }
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void { setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
const styleSheet = this.baseStyle.sheet as CSSStyleSheet; this.editableContainer.setBaseStyling(
const firstRule = styleSheet.cssRules[0] as CSSStyleRule; this.quoteFontFamily(fontFamily),
firstRule.style.fontFamily = this.quoteFontFamily(fontFamily); fontSize,
firstRule.style.fontSize = fontSize; direction,
firstRule.style.direction = direction; );
} }
isRightToLeft(): boolean { isRightToLeft(): boolean {
const styleSheet = this.baseStyle.sheet as CSSStyleSheet; return this.editableContainer.isRightToLeft();
const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
return firstRule.style.direction === "rtl";
} }
focus(): void { focus(): void {
@ -140,11 +124,12 @@ export class EditingArea extends HTMLDivElement {
} }
hasFocus(): boolean { hasFocus(): boolean {
return document.activeElement === this; return document.activeElement?.closest(".field") === this;
} }
getSelection(): Selection { getSelection(): Selection {
return this.shadowRoot!.getSelection()!; const root = this.activeInput.getRootNode() as Document | ShadowRoot;
return root.getSelection()!;
} }
surroundSelection(before: string, after: string): void { surroundSelection(before: string, after: string): void {

View file

@ -62,3 +62,12 @@
opacity: 0.5; opacity: 0.5;
} }
} }
@import "ts/sass/codemirror/lib/codemirror";
@import "ts/sass/codemirror/theme/monokai";
@import "ts/sass/codemirror/addon/fold/foldgutter";
.CodeMirror {
height: auto;
padding: 6px 0;
}

View file

@ -10,12 +10,13 @@ import type { EditingArea } from "./editing-area";
import { saveField } from "./change-timer"; import { saveField } from "./change-timer";
import { bridgeCommand } from "./lib"; import { bridgeCommand } from "./lib";
import { getCurrentField } from "./helpers";
export function onFocus(evt: FocusEvent): void { export function onFocus(evt: FocusEvent): void {
const currentField = evt.currentTarget as EditingArea; const currentField = evt.currentTarget as EditingArea;
currentField.focus(); currentField.focus();
if (currentField.shadowRoot!.getSelection()!.anchorNode === null) { if (currentField.getSelection().anchorNode === null) {
// selection is not inside editable after focusing // selection is not inside editable after focusing
currentField.caretToEnd(); currentField.caretToEnd();
} }
@ -26,7 +27,7 @@ export function onFocus(evt: FocusEvent): void {
export function onBlur(evt: FocusEvent): void { export function onBlur(evt: FocusEvent): void {
const previousFocus = evt.currentTarget as EditingArea; const previousFocus = evt.currentTarget as EditingArea;
const currentFieldUnchanged = previousFocus === document.activeElement; const currentFieldUnchanged = previousFocus === getCurrentField();
saveField(previousFocus, currentFieldUnchanged ? "key" : "blur"); saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
fieldFocused.set(false); fieldFocused.set(false);

View file

@ -5,6 +5,12 @@
@typescript-eslint/no-non-null-assertion: "off", @typescript-eslint/no-non-null-assertion: "off",
*/ */
import type { EditingArea } from "./editing-area";
export function getCurrentField(): EditingArea | null {
return document.activeElement?.closest(".field") ?? null;
}
export function nodeIsElement(node: Node): node is Element { export function nodeIsElement(node: Node): node is Element {
return node.nodeType === Node.ELEMENT_NODE; return node.nodeType === Node.ELEMENT_NODE;
} }

View file

@ -23,15 +23,18 @@ import { saveField } from "./change-timer";
import { EditorField } from "./editor-field"; import { EditorField } from "./editor-field";
import { LabelContainer } from "./label-container"; import { LabelContainer } from "./label-container";
import { EditingArea } from "./editing-area"; import { EditingArea } from "./editing-area";
import { EditableContainer } from "./editable-container";
import { Editable } from "./editable"; import { Editable } from "./editable";
import { Codable } from "./codable"; import { Codable } from "./codable";
import { initToolbar, fieldFocused } from "./toolbar"; import { initToolbar, fieldFocused } from "./toolbar";
import { getCurrentField } from "./helpers";
export { setNoteId, getNoteId } from "./note-id"; export { setNoteId, getNoteId } from "./note-id";
export { saveNow } from "./change-timer"; export { saveNow } from "./change-timer";
export { wrap, wrapIntoText } from "./wrap"; export { wrap, wrapIntoText } from "./wrap";
export { editorToolbar } from "./toolbar"; export { editorToolbar } from "./toolbar";
export { activateStickyShortcuts } from "./label-container"; export { activateStickyShortcuts } from "./label-container";
export { getCurrentField } from "./helpers";
export { components } from "./Components.svelte"; export { components } from "./Components.svelte";
declare global { declare global {
@ -44,6 +47,7 @@ declare global {
} }
customElements.define("anki-editable", Editable); customElements.define("anki-editable", Editable);
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
customElements.define("anki-codable", Codable, { extends: "textarea" }); customElements.define("anki-codable", Codable, { extends: "textarea" });
customElements.define("anki-editing-area", EditingArea, { extends: "div" }); customElements.define("anki-editing-area", EditingArea, { extends: "div" });
customElements.define("anki-label-container", LabelContainer, { extends: "div" }); customElements.define("anki-label-container", LabelContainer, { extends: "div" });
@ -53,12 +57,6 @@ if (isApplePlatform()) {
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V"); registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
} }
export function getCurrentField(): EditingArea | null {
return document.activeElement instanceof EditingArea
? document.activeElement
: null;
}
export function focusField(n: number): void { export function focusField(n: number): void {
const field = getEditorField(n); const field = getEditorField(n);

View file

@ -5,7 +5,8 @@
@typescript-eslint/no-non-null-assertion: "off", @typescript-eslint/no-non-null-assertion: "off",
*/ */
import { getCurrentField, setFormat } from "."; import { getCurrentField } from "./helpers";
import { setFormat } from ".";
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*)$/)!;