anki/ts/editor/OldEditorAdapter.svelte
Henrik Giesel c2768e2188
Translate Editor entirely to Svelte (#1403)
* Translate editor to Svelte

Make editor fields grid rather than flexbox

Refactor ButtonToolbar margins

Remove remaining svelte.d.ts symlinks

Implement saveNow

Fix text surrounding

Remove HTML editor button

Clean up some empty files

Add visual for new field state badges

* Adds new IconConstrain.svelte to generalize the icon handling for
IconButton and Badge

Implement sticky functionality again

Enable Editable and Codable field state badges

Add shortcuts to FieldState badges

Add Shift+F9 shortcut back

Add inline padding back to editor fields, tag editor and toolbar

Make Editable and Codable only "visually hidden"

This way they are still updated in the background
Otherwise reshowing them will always start them up empty

Make empty editing area focusable

Start with moving fieldsKey and currentFieldKey to context.ts

Fix Codable being wrong size when opening for first time

Add back drag'n'drop

Make ButtonItem display: contents again

* This will break the gap between ButtonGroup items, however once we
  have a newer Chromium version we should use CSS gap property anyway

Fix most of typing issues

Use --label-color background color LabelContainer

Add back red color for dupes

Generalize the editor toolbar in the multiroot editor to widgets

Implement Notification.svelte for showing cloze hints

Add colorful icon to notification

Hook up Editable to EditingArea

Move EditingArea into EditorField

Include editorField in editor/context

Fix rebasing issues

Uniformly use SvelteComponentTyped

Take LabelContainer out of EditingArea

Use mirror-dom and node-store to export editable content

Fix editable update mechanism

Prepare passing the editing inputs as slots

Pass in editing inputs as slots

Use codable options again in codemirror

Delete editor/lib.ts

Remove CodableAdapter, Use more generic CodeMirror component

Fix clicking LabelContainer to focus

Use prettier

Rename Editable to ContentEditable

Fix writing Mathjax from Codable to Editable

Correctly adjust output HTML from editable

Refactor EditableStyles out of EditableContainer

Pass Image and Mathjax Handle via slots to Editable

Make Editable add its editingInputApi

Make Editable hideable

Fix font size not being set correctly

Refactor both fieldFocused and focusInCodable to focusInEditable

Fix focusIfField

Bring back $activeInput

Fix ClozeButton

Remove signifyCustomInput

Refactor MathjaxHandle

Refactor out some logic into store-subscribe

Fix Mathjax editor

Use focusTrap instead of focusing div

Delegate focus back to editingInput when refocusing focusTrap

Elegantly move focus between editing inputs when closing/opening

Make Codable tabbable

Automatically move caret to end on editable and codable

+ remove from editingInput api

Fix ButtonDropdown having two rows and missing button margins

Make svelte_check and eslint pass

Satisfy editor svelte_check

Save field updates to db again

Await editable styles before mounting content editable

Remove unused import from OldEditorAdapter

Add copyright header to OldEditorAdapter

Update button active state from contenteditable

* Use activateStickyShortcuts after waiting for noteEditorPromise

* Set fields via stores, make tags correctly set

* Add explaining comment to setFields

* Fix ClozeButton

* Send focus and blur events again

* Fix Codable not correctly updating on blur with invalid HTML

* Remove old code for special Enter behavior in tags

* Do not use logical properties for ButtonToolbar margins

* Remove getCurrentField

Instead use noteEditor->currentField or noteEditor->activeInput

* Remove Extensible type

* Use context-property for NoteEditor, EditorField and EditingArea

* Rename parameter in mirror-dom.allowResubscription

* Fix cutOrCopy

* Refactor context.ts into the individual components

* Move focusing of editingArea up to editorField

* Rename promiseResolve -> promiseWithResolver

* Rename Editable->RichTextInput and Codable->PlainTextInput

* Remove now unnecessary type assertion for `getNoteEditor` and `getEditingArea`

* Refocus field after adding, so subscription to editing area is refreshed
2021-10-18 22:01:15 +10:00

324 lines
11 KiB
Svelte

<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script context="module" lang="ts">
import type { EditorFieldAPI } from "./EditorField.svelte";
import type { RichTextInputAPI } from "./RichTextInput.svelte";
import type { PlainTextInputAPI } from "./PlainTextInput.svelte";
import contextProperty from "../sveltelib/context-property";
export interface NoteEditorAPI {
fields: EditorFieldAPI[];
currentField: Writable<EditorFieldAPI>;
activeInput: Writable<RichTextInputAPI | PlainTextInputAPI | null>;
focusInRichText: Writable<boolean>;
}
const key = Symbol("noteEditor");
const [set, getNoteEditor, hasNoteEditor] = contextProperty<NoteEditorAPI>(key);
export { getNoteEditor, hasNoteEditor };
</script>
<script lang="ts">
import NoteEditor from "./NoteEditor.svelte";
import FieldsEditor from "./FieldsEditor.svelte";
import Fields from "./Fields.svelte";
import EditorField from "./EditorField.svelte";
import type { FieldData } from "./EditorField.svelte";
import TagEditor from "./TagEditor.svelte";
import EditorToolbar from "./EditorToolbar.svelte";
import Notification from "./Notification.svelte";
import Absolute from "../components/Absolute.svelte";
import Badge from "../components/Badge.svelte";
import DecoratedElements from "./DecoratedElements.svelte";
import RichTextInput from "./RichTextInput.svelte";
import { MathjaxHandle } from "./mathjax-overlay";
import { ImageHandle } from "./image-overlay";
import PlainTextInput from "./PlainTextInput.svelte";
import RichTextBadge from "./RichTextBadge.svelte";
import PlainTextBadge from "./PlainTextBadge.svelte";
import StickyBadge from "./StickyBadge.svelte";
import { onMount, onDestroy } from "svelte";
import type { Writable } from "svelte/store";
import { writable, get } from "svelte/store";
import { bridgeCommand } from "../lib/bridgecommand";
import { isApplePlatform } from "../lib/platform";
import { ChangeTimer } from "./change-timer";
import { alertIcon } from "./icons";
function quoteFontFamily(fontFamily: string): string {
// generic families (e.g. sans-serif) must not be quoted
if (!/^[-a-z]+$/.test(fontFamily)) {
fontFamily = `"${fontFamily}"`;
}
return fontFamily;
}
let size = isApplePlatform() ? 1.6 : 2.0;
let wrap = true;
let fieldStores: Writable<string>[] = [];
let fieldNames: string[] = [];
export function setFields(fs: [string, string][]): void {
// this is a bit of a mess -- when moving to Rust calls, we should make
// sure to have two backend endpoints for:
// * the note, which can be set through this view
// * the fieldname, font, etc., which cannot be set
const newFieldNames: string[] = [];
for (const [index, [fieldName]] of fs.entries()) {
newFieldNames[index] = fieldName;
}
for (let i = fieldStores.length; i < newFieldNames.length; i++) {
const newStore = writable("");
fieldStores[i] = newStore;
newStore.subscribe((value) => updateField(i, value));
}
for (let i = fieldStores.length; i > newFieldNames.length; i++) {
fieldStores.pop();
}
for (const [index, [_, fieldContent]] of fs.entries()) {
fieldStores[index].set(fieldContent);
}
fieldNames = newFieldNames;
}
let fonts: [string, number, boolean][] = [];
let richTextsHidden: boolean[] = [];
let plainTextsHidden: boolean[] = [];
export function setFonts(fs: [string, number, boolean][]): void {
fonts = fs;
richTextsHidden = fonts.map((_, index) => richTextsHidden[index] ?? false);
plainTextsHidden = fonts.map((_, index) => plainTextsHidden[index] ?? true);
}
let focusTo: number = 0;
export function focusField(n: number): void {
focusTo = n;
fieldApis[focusTo]?.editingArea?.refocus();
}
let textColor: string = "black";
let highlightColor: string = "black";
export function setColorButtons([textClr, highlightClr]: [string, string]): void {
textColor = textClr;
highlightColor = highlightClr;
}
let tags = writable<string[]>([]);
export function setTags(ts: string[]): void {
$tags = ts;
}
let stickies: boolean[] | null = null;
export function setSticky(sts: boolean[]): void {
stickies = sts;
}
let noteId: number | null = null;
export function setNoteId(ntid: number): void {
noteId = ntid;
}
function getNoteId(): number | null {
return noteId;
}
let cols: ("dupe" | "")[] = [];
export function setBackgrounds(cls: ("dupe" | "")[]): void {
cols = cls;
}
let hint: string = "";
export function setClozeHint(hnt: string): void {
hint = hnt;
}
$: fieldsData = fieldNames.map((name, index) => ({
name,
fontFamily: quoteFontFamily(fonts[index][0]),
fontSize: fonts[index][1],
direction: fonts[index][2] ? "rtl" : "ltr",
})) as FieldData[];
function saveTags({ detail }: CustomEvent): void {
bridgeCommand(`saveTags:${JSON.stringify(detail.tags)}`);
}
const fieldSave = new ChangeTimer();
function updateField(index: number, content: string): void {
fieldSave.schedule(
() => bridgeCommand(`key:${index}:${getNoteId()}:${content}`),
600
);
}
export function saveFieldNow(): void {
/* this will always be a key save */
fieldSave.fireImmediately();
}
export function saveOnPageHide() {
if (document.visibilityState === "hidden") {
// will fire on session close and minimize
saveFieldNow();
}
}
function toggleStickyAll(): void {
bridgeCommand("toggleStickyAll", (values: boolean[]) => (stickies = values));
}
import { registerShortcut } from "../lib/shortcuts";
let deregisterSticky: () => void;
export function activateStickyShortcuts() {
deregisterSticky = registerShortcut(toggleStickyAll, "Shift+F9");
}
export function focusIfField(x: number, y: number): boolean {
const elements = document.elementsFromPoint(x, y);
const first = elements[0];
if (first.shadowRoot) {
const richTextInput = first.shadowRoot.lastElementChild! as HTMLElement;
richTextInput.focus();
return true;
}
return false;
}
let richTextInputs: RichTextInput[] = [];
$: richTextInputs = richTextInputs.filter(Boolean);
let plainTextInputs: PlainTextInput[] = [];
$: plainTextInputs = plainTextInputs.filter(Boolean);
let editorFields: EditorField[] = [];
$: fieldApis = editorFields.filter(Boolean).map((field) => field.api);
const currentField = writable<EditorFieldAPI | null>(null);
const activeInput = writable<RichTextInputAPI | PlainTextInputAPI | null>(null);
const focusInRichText = writable<boolean>(false);
export const api = set(
Object.create(
{
currentField,
activeInput,
focusInRichText,
},
{
fields: { get: () => fieldApis },
}
)
);
onMount(() => {
document.addEventListener("visibilitychange", saveOnPageHide);
return () => document.removeEventListener("visibilitychange", saveOnPageHide);
});
onDestroy(() => deregisterSticky);
</script>
<NoteEditor>
<FieldsEditor>
<EditorToolbar {size} {wrap} {textColor} {highlightColor} />
{#if hint}
<Absolute bottom right --margin="10px">
<Notification>
<Badge --badge-color="tomato" --icon-align="top"
>{@html alertIcon}</Badge
>
<span>{@html hint}</span>
</Notification>
</Absolute>
{/if}
<Fields>
{#each fieldsData as field, index}
<EditorField
{field}
content={fieldStores[index]}
autofocus={index === focusTo}
bind:this={editorFields[index]}
on:focusin={() => {
$currentField = api.fields[index];
bridgeCommand(`focus:${index}`);
}}
on:focusout={() => {
$currentField = null;
bridgeCommand(
`blur:${index}:${getNoteId()}:${get(fieldStores[index])}`
);
}}
--label-color={cols[index] === "dupe"
? "var(--flag1-bg)"
: "transparent"}
>
<svelte:fragment slot="field-state">
<RichTextBadge bind:off={richTextsHidden[index]} />
<PlainTextBadge bind:off={plainTextsHidden[index]} />
{#if stickies}
<StickyBadge active={stickies[index]} {index} />
{/if}
</svelte:fragment>
<svelte:fragment slot="editing-inputs">
<DecoratedElements>
<RichTextInput
hidden={richTextsHidden[index]}
on:focusin={() => {
$focusInRichText = true;
$activeInput = richTextInputs[index].api;
}}
on:focusout={() => {
$focusInRichText = false;
$activeInput = null;
saveFieldNow();
}}
bind:this={richTextInputs[index]}
>
<ImageHandle />
<MathjaxHandle />
</RichTextInput>
<PlainTextInput
hidden={plainTextsHidden[index]}
on:focusin={() => {
$activeInput = plainTextInputs[index].api;
}}
on:focusout={() => {
$activeInput = null;
saveFieldNow();
}}
bind:this={plainTextInputs[index]}
/>
</DecoratedElements>
</svelte:fragment>
</EditorField>
{/each}
</Fields>
</FieldsEditor>
<TagEditor {size} {wrap} {tags} on:tagsupdate={saveTags} />
</NoteEditor>