mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 06:22:22 -04:00
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:
parent
6909a095d5
commit
41c4be2f54
10 changed files with 112 additions and 58 deletions
|
@ -3,7 +3,6 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { EditingArea } from "./editing-area";
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
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 CommandIconButton from "./CommandIconButton.svelte";
|
||||
|
||||
import { getListItem } from "./helpers";
|
||||
import { getCurrentField, getListItem } from "./helpers";
|
||||
import {
|
||||
ulIcon,
|
||||
olIcon,
|
||||
|
@ -31,8 +30,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let api = {};
|
||||
|
||||
function outdentListItem() {
|
||||
const currentField = document.activeElement as EditingArea;
|
||||
if (getListItem(currentField.shadowRoot!)) {
|
||||
const currentField = getCurrentField();
|
||||
if (getListItem(currentField.editableContainer.shadowRoot!)) {
|
||||
document.execCommand("outdent");
|
||||
} else {
|
||||
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() {
|
||||
const currentField = document.activeElement as EditingArea;
|
||||
if (getListItem(currentField.shadowRoot!)) {
|
||||
const currentField = getCurrentField();
|
||||
if (getListItem(currentField.editableContainer.shadowRoot!)) {
|
||||
document.execCommand("indent");
|
||||
} else {
|
||||
alert("Indent/unindent currently only works with lists.");
|
||||
|
|
|
@ -18,8 +18,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
import ClozeButton from "./ClozeButton.svelte";
|
||||
|
||||
import { getCurrentField } from ".";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
import { getCurrentField, appendInParentheses } from "./helpers";
|
||||
import { wrapCurrent } from "./wrap";
|
||||
import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons";
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { EditingArea } from "./editing-area";
|
||||
|
||||
import { getCurrentField } from ".";
|
||||
import { getCurrentField } from "./helpers";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { getNoteId } from "./note-id";
|
||||
|
||||
|
@ -23,9 +23,8 @@ function clearChangeTimer(): void {
|
|||
|
||||
export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
|
||||
clearChangeTimer();
|
||||
bridgeCommand(
|
||||
`${type}:${currentField.ord}:${getNoteId()}:${currentField.fieldHTML}`
|
||||
);
|
||||
const command = `${type}:${currentField.ord}:${getNoteId()}:${currentField.fieldHTML}`
|
||||
bridgeCommand(command);
|
||||
}
|
||||
|
||||
export function saveNow(keepFocus: boolean): void {
|
||||
|
|
57
ts/editor/editable-container.ts
Normal file
57
ts/editor/editable-container.ts
Normal 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";
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import type { EditableContainer } from "./editable-container";
|
||||
import type { Editable } from "./editable";
|
||||
import type { Codable } from "./codable";
|
||||
|
||||
|
@ -18,35 +19,25 @@ function onCutOrCopy(): void {
|
|||
}
|
||||
|
||||
export class EditingArea extends HTMLDivElement {
|
||||
editableContainer: EditableContainer;
|
||||
editable: Editable;
|
||||
codable: Codable;
|
||||
baseStyle: HTMLStyleElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.attachShadow({ mode: "open" });
|
||||
this.className = "field";
|
||||
|
||||
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");
|
||||
this.shadowRoot!.appendChild(rootStyle);
|
||||
|
||||
this.baseStyle = document.createElement("style");
|
||||
this.baseStyle.setAttribute("rel", "stylesheet");
|
||||
this.shadowRoot!.appendChild(this.baseStyle);
|
||||
|
||||
this.editableContainer = document.createElement("div", {
|
||||
is: "anki-editable-container",
|
||||
}) as EditableContainer;
|
||||
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", {
|
||||
is: "anki-codable",
|
||||
}) as Codable;
|
||||
this.shadowRoot!.appendChild(this.codable);
|
||||
this.appendChild(this.codable);
|
||||
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
}
|
||||
|
@ -71,23 +62,20 @@ export class EditingArea extends HTMLDivElement {
|
|||
this.addEventListener("keydown", onKey);
|
||||
this.addEventListener("keyup", onKeyUp);
|
||||
this.addEventListener("input", onInput);
|
||||
this.addEventListener("focus", onFocus);
|
||||
this.addEventListener("blur", onBlur);
|
||||
this.addEventListener("focusin", onFocus);
|
||||
this.addEventListener("focusout", onBlur);
|
||||
this.addEventListener("paste", this.onPaste);
|
||||
this.addEventListener("copy", onCutOrCopy);
|
||||
this.addEventListener("oncut", onCutOrCopy);
|
||||
this.addEventListener("mouseup", updateActiveButtons);
|
||||
|
||||
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("focusin", onFocus);
|
||||
this.removeEventListener("focusout", onBlur);
|
||||
this.removeEventListener("paste", this.onPaste);
|
||||
this.removeEventListener("copy", onCutOrCopy);
|
||||
this.removeEventListener("oncut", onCutOrCopy);
|
||||
|
@ -100,9 +88,7 @@ export class EditingArea extends HTMLDivElement {
|
|||
}
|
||||
|
||||
setBaseColor(color: string): void {
|
||||
const styleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||
const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
|
||||
firstRule.style.color = color;
|
||||
this.editableContainer.setBaseColor(color);
|
||||
}
|
||||
|
||||
quoteFontFamily(fontFamily: string): string {
|
||||
|
@ -114,17 +100,15 @@ export class EditingArea extends HTMLDivElement {
|
|||
}
|
||||
|
||||
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 = this.quoteFontFamily(fontFamily);
|
||||
firstRule.style.fontSize = fontSize;
|
||||
firstRule.style.direction = direction;
|
||||
this.editableContainer.setBaseStyling(
|
||||
this.quoteFontFamily(fontFamily),
|
||||
fontSize,
|
||||
direction,
|
||||
);
|
||||
}
|
||||
|
||||
isRightToLeft(): boolean {
|
||||
const styleSheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||
const firstRule = styleSheet.cssRules[0] as CSSStyleRule;
|
||||
return firstRule.style.direction === "rtl";
|
||||
return this.editableContainer.isRightToLeft();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
|
@ -140,11 +124,12 @@ export class EditingArea extends HTMLDivElement {
|
|||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return document.activeElement === this;
|
||||
return document.activeElement?.closest(".field") === this;
|
||||
}
|
||||
|
||||
getSelection(): Selection {
|
||||
return this.shadowRoot!.getSelection()!;
|
||||
const root = this.activeInput.getRootNode() as Document | ShadowRoot;
|
||||
return root.getSelection()!;
|
||||
}
|
||||
|
||||
surroundSelection(before: string, after: string): void {
|
||||
|
|
|
@ -62,3 +62,12 @@
|
|||
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;
|
||||
}
|
||||
|
|
|
@ -10,12 +10,13 @@ import type { EditingArea } from "./editing-area";
|
|||
|
||||
import { saveField } from "./change-timer";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { getCurrentField } from "./helpers";
|
||||
|
||||
export function onFocus(evt: FocusEvent): void {
|
||||
const currentField = evt.currentTarget as EditingArea;
|
||||
currentField.focus();
|
||||
|
||||
if (currentField.shadowRoot!.getSelection()!.anchorNode === null) {
|
||||
if (currentField.getSelection().anchorNode === null) {
|
||||
// selection is not inside editable after focusing
|
||||
currentField.caretToEnd();
|
||||
}
|
||||
|
@ -26,7 +27,7 @@ export function onFocus(evt: FocusEvent): void {
|
|||
|
||||
export function onBlur(evt: FocusEvent): void {
|
||||
const previousFocus = evt.currentTarget as EditingArea;
|
||||
const currentFieldUnchanged = previousFocus === document.activeElement;
|
||||
const currentFieldUnchanged = previousFocus === getCurrentField();
|
||||
|
||||
saveField(previousFocus, currentFieldUnchanged ? "key" : "blur");
|
||||
fieldFocused.set(false);
|
||||
|
|
|
@ -5,6 +5,12 @@
|
|||
@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 {
|
||||
return node.nodeType === Node.ELEMENT_NODE;
|
||||
}
|
||||
|
|
|
@ -23,15 +23,18 @@ import { saveField } from "./change-timer";
|
|||
import { EditorField } from "./editor-field";
|
||||
import { LabelContainer } from "./label-container";
|
||||
import { EditingArea } from "./editing-area";
|
||||
import { EditableContainer } from "./editable-container";
|
||||
import { Editable } from "./editable";
|
||||
import { Codable } from "./codable";
|
||||
import { initToolbar, fieldFocused } from "./toolbar";
|
||||
import { getCurrentField } from "./helpers";
|
||||
|
||||
export { setNoteId, getNoteId } from "./note-id";
|
||||
export { saveNow } from "./change-timer";
|
||||
export { wrap, wrapIntoText } from "./wrap";
|
||||
export { editorToolbar } from "./toolbar";
|
||||
export { activateStickyShortcuts } from "./label-container";
|
||||
export { getCurrentField } from "./helpers";
|
||||
export { components } from "./Components.svelte";
|
||||
|
||||
declare global {
|
||||
|
@ -44,6 +47,7 @@ declare global {
|
|||
}
|
||||
|
||||
customElements.define("anki-editable", Editable);
|
||||
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
|
||||
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
||||
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
||||
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
||||
|
@ -53,12 +57,6 @@ if (isApplePlatform()) {
|
|||
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 {
|
||||
const field = getEditorField(n);
|
||||
|
||||
|
|
|
@ -5,7 +5,8 @@
|
|||
@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 {
|
||||
const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
|
||||
|
|
Loading…
Reference in a new issue