anki/ts/editor/index.ts
Damien Elmes 41c4be2f54 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
2021-09-06 21:15:36 +10:00

207 lines
6.4 KiB
TypeScript

// 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",
@typescript-eslint/no-explicit-any: "off",
*/
import "sveltelib/export-runtime";
import "lib/register-package";
import { filterHTML } from "html-filter";
import { updateActiveButtons } from "./toolbar";
import { setupI18n, ModuleName } from "lib/i18n";
import { isApplePlatform } from "lib/platform";
import { registerShortcut } from "lib/shortcuts";
import { bridgeCommand } from "lib/bridgecommand";
import "./fields.css";
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 {
interface Selection {
modify(s: string, t: string, u: string): void;
addRange(r: Range): void;
removeAllRanges(): void;
getRangeAt(n: number): Range;
}
}
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" });
customElements.define("anki-editor-field", EditorField, { extends: "div" });
if (isApplePlatform()) {
registerShortcut(() => bridgeCommand("paste"), "Control+Shift+V");
}
export function focusField(n: number): void {
const field = getEditorField(n);
if (field) {
field.editingArea.focus();
field.editingArea.caretToEnd();
updateActiveButtons(new Event("manualfocus"));
}
}
export function focusIfField(x: number, y: number): boolean {
const elements = document.elementsFromPoint(x, y);
for (let i = 0; i < elements.length; i++) {
const elem = elements[i] as EditingArea;
if (elem instanceof EditingArea) {
elem.focus();
return true;
}
}
return false;
}
export function pasteHTML(
html: string,
internal: boolean,
extendedMode: boolean
): void {
html = filterHTML(html, internal, extendedMode);
if (html !== "") {
setFormat("inserthtml", html);
}
}
function adjustFieldAmount(amount: number): void {
const fieldsContainer = document.getElementById("fields")!;
while (fieldsContainer.childElementCount < amount) {
const newField = document.createElement("div", {
is: "anki-editor-field",
}) as EditorField;
newField.ord = fieldsContainer.childElementCount;
fieldsContainer.appendChild(newField);
}
while (fieldsContainer.childElementCount > amount) {
fieldsContainer.removeChild(fieldsContainer.lastElementChild as Node);
}
}
export function getEditorField(n: number): EditorField | null {
const fields = document.getElementById("fields")!.children;
return (fields[n] as EditorField) ?? null;
}
/// forEachEditorFieldAndProvidedValue:
/// Values should be a list with the same length as the
/// number of fields. Func will be called with each field and
/// value in turn.
export function forEditorField<T>(
values: T[],
func: (field: EditorField, value: T) => void
): void {
const fields = document.getElementById("fields")!.children;
for (let i = 0; i < fields.length; i++) {
const field = fields[i] as EditorField;
func(field, values[i]);
}
}
export function setFields(fields: [string, string][]): void {
// webengine will include the variable after enter+backspace
// if we don't convert it to a literal colour
const color = window
.getComputedStyle(document.documentElement)
.getPropertyValue("--text-fg");
adjustFieldAmount(fields.length);
forEditorField(
fields,
(field: EditorField, [name, fieldContent]: [string, string]): void =>
field.initialize(name, color, fieldContent)
);
if (!getCurrentField()) {
// when initial focus of the window is not on editor (e.g. browser)
fieldFocused.set(false);
}
}
export function setBackgrounds(cols: ("dupe" | "")[]): void {
forEditorField(cols, (field: EditorField, value: "dupe" | "") =>
field.editingArea.classList.toggle("dupe", value === "dupe")
);
document
.getElementById("dupes")!
.classList.toggle("is-inactive", !cols.includes("dupe"));
}
export function setClozeHint(cloze_hint: string): void {
document.getElementById("cloze-hint")!.innerHTML = cloze_hint;
}
export function setFonts(fonts: [string, number, boolean][]): void {
forEditorField(
fonts,
(
field: EditorField,
[fontFamily, fontSize, isRtl]: [string, number, boolean]
) => {
field.setBaseStyling(fontFamily, `${fontSize}px`, isRtl ? "rtl" : "ltr");
}
);
}
export function setColorButtons([textColor, highlightColor]: [string, string]): void {
$editorToolbar.then((editorToolbar) =>
(editorToolbar as any).$set({ textColor, highlightColor })
);
}
export function setSticky(stickies: boolean[]): void {
forEditorField(stickies, (field: EditorField, isSticky: boolean) => {
field.labelContainer.activateSticky(isSticky);
});
}
export function setFormat(cmd: string, arg?: string, nosave = false): void {
document.execCommand(cmd, false, arg);
if (!nosave) {
saveField(getCurrentField() as EditingArea, "key");
updateActiveButtons(new Event(cmd));
}
}
export const i18n = setupI18n({
modules: [
ModuleName.EDITING,
ModuleName.KEYBOARD,
ModuleName.ACTIONS,
ModuleName.BROWSING,
],
});
import type EditorToolbar from "./EditorToolbar.svelte";
export const $editorToolbar: Promise<EditorToolbar> = initToolbar(i18n);