c2768e2188
* 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
256 lines
7.4 KiB
Svelte
256 lines
7.4 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 CustomStyles from "./CustomStyles.svelte";
|
|
import type { EditingInputAPI } from "./EditingArea.svelte";
|
|
import contextProperty from "../sveltelib/context-property";
|
|
|
|
export interface RichTextInputAPI extends EditingInputAPI {
|
|
name: "rich-text";
|
|
moveCaretToEnd(): void;
|
|
refocus(): void;
|
|
toggle(): boolean;
|
|
surround(before: string, after: string): void;
|
|
preventResubscription(): () => void;
|
|
}
|
|
|
|
export interface RichTextInputContextAPI {
|
|
styles: CustomStyles;
|
|
container: HTMLElement;
|
|
api: RichTextInputAPI;
|
|
}
|
|
|
|
const key = Symbol("richText");
|
|
const [set, getRichTextInput, hasRichTextInput] =
|
|
contextProperty<RichTextInputContextAPI>(key);
|
|
|
|
export { getRichTextInput, hasRichTextInput };
|
|
</script>
|
|
|
|
<script lang="ts">
|
|
import RichTextStyles from "./RichTextStyles.svelte";
|
|
import SetContext from "./SetContext.svelte";
|
|
import ContentEditable from "../editable/ContentEditable.svelte";
|
|
|
|
import { onMount, getAllContexts } from "svelte";
|
|
import {
|
|
nodeIsElement,
|
|
nodeContainsInlineContent,
|
|
fragmentToString,
|
|
caretToEnd,
|
|
} from "../lib/dom";
|
|
import { getDecoratedElements } from "./DecoratedElements.svelte";
|
|
import { getEditingArea } from "./EditingArea.svelte";
|
|
import { promiseWithResolver } from "../lib/promise";
|
|
import { bridgeCommand } from "../lib/bridgecommand";
|
|
import { wrapInternal } from "../lib/wrap";
|
|
import { nodeStore } from "../sveltelib/node-store";
|
|
import type { DecoratedElement } from "../editable/decorated";
|
|
|
|
export let hidden: boolean;
|
|
|
|
const { content, editingInputs } = getEditingArea();
|
|
const decoratedElements = getDecoratedElements();
|
|
|
|
const range = document.createRange();
|
|
|
|
function normalizeFragment(fragment: DocumentFragment): void {
|
|
fragment.normalize();
|
|
|
|
for (const decorated of decoratedElements) {
|
|
for (const element of fragment.querySelectorAll(
|
|
decorated.tagName
|
|
) as NodeListOf<DecoratedElement>) {
|
|
element.undecorate();
|
|
}
|
|
}
|
|
}
|
|
|
|
const nodes = nodeStore<DocumentFragment>(undefined, normalizeFragment);
|
|
|
|
function adjustInputHTML(html: string): string {
|
|
for (const component of decoratedElements) {
|
|
html = component.toUndecorated(html);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function adjustInputFragment(fragment: DocumentFragment): void {
|
|
if (nodeContainsInlineContent(fragment)) {
|
|
fragment.appendChild(document.createElement("br"));
|
|
}
|
|
}
|
|
|
|
function writeFromEditingArea(html: string): void {
|
|
/* we need createContextualFragment so that customElements are initialized */
|
|
const fragment = range.createContextualFragment(adjustInputHTML(html));
|
|
adjustInputFragment(fragment);
|
|
|
|
nodes.setUnprocessed(fragment);
|
|
}
|
|
|
|
function adjustOutputFragment(fragment: DocumentFragment): void {
|
|
if (
|
|
fragment.hasChildNodes() &&
|
|
nodeIsElement(fragment.lastChild!) &&
|
|
nodeContainsInlineContent(fragment) &&
|
|
fragment.lastChild!.tagName === "BR"
|
|
) {
|
|
fragment.lastChild!.remove();
|
|
}
|
|
}
|
|
|
|
function adjustOutputHTML(html: string): string {
|
|
for (const component of decoratedElements) {
|
|
html = component.toStored(html);
|
|
}
|
|
|
|
return html;
|
|
}
|
|
|
|
function writeToEditingArea(fragment: DocumentFragment): void {
|
|
const clone = document.importNode(fragment, true);
|
|
adjustOutputFragment(clone);
|
|
|
|
const output = adjustOutputHTML(fragmentToString(clone));
|
|
content.set(output);
|
|
}
|
|
|
|
function attachShadow(element: Element): void {
|
|
element.attachShadow({ mode: "open" });
|
|
}
|
|
|
|
const [richTextPromise, richTextResolve] = promiseWithResolver<HTMLElement>();
|
|
|
|
function resolve(richTextInput: HTMLElement): { destroy: () => void } {
|
|
function onPaste(event: Event): void {
|
|
event.preventDefault();
|
|
bridgeCommand("paste");
|
|
}
|
|
|
|
function onCutOrCopy(): void {
|
|
bridgeCommand("cutOrCopy");
|
|
}
|
|
|
|
richTextInput.addEventListener("paste", onPaste);
|
|
richTextInput.addEventListener("copy", onCutOrCopy);
|
|
richTextInput.addEventListener("cut", onCutOrCopy);
|
|
richTextResolve(richTextInput);
|
|
|
|
return {
|
|
destroy() {
|
|
richTextInput.removeEventListener("paste", onPaste);
|
|
richTextInput.removeEventListener("copy", onCutOrCopy);
|
|
richTextInput.removeEventListener("cut", onCutOrCopy);
|
|
},
|
|
};
|
|
}
|
|
|
|
import getDOMMirror from "../sveltelib/mirror-dom";
|
|
|
|
const { mirror, preventResubscription } = getDOMMirror();
|
|
|
|
function moveCaretToEnd() {
|
|
richTextPromise.then(caretToEnd);
|
|
}
|
|
|
|
const allContexts = getAllContexts();
|
|
|
|
function attachContentEditable(element: Element, { stylesDidLoad }): void {
|
|
stylesDidLoad.then(() => {
|
|
const contentEditable = new ContentEditable({
|
|
target: element.shadowRoot!,
|
|
props: {
|
|
nodes,
|
|
resolve,
|
|
mirror,
|
|
},
|
|
context: allContexts,
|
|
});
|
|
|
|
contentEditable.$on("focus", moveCaretToEnd);
|
|
});
|
|
}
|
|
|
|
export const api: RichTextInputAPI = {
|
|
name: "rich-text",
|
|
focus() {
|
|
richTextPromise.then((richText) => richText.focus());
|
|
},
|
|
refocus() {
|
|
richTextPromise.then((richText) => {
|
|
richText.blur();
|
|
richText.focus();
|
|
});
|
|
},
|
|
moveCaretToEnd,
|
|
focusable: !hidden,
|
|
toggle(): boolean {
|
|
hidden = !hidden;
|
|
return hidden;
|
|
},
|
|
surround(before: string, after: string) {
|
|
richTextPromise.then((richText) =>
|
|
wrapInternal(richText.getRootNode() as any, before, after, false)
|
|
);
|
|
},
|
|
preventResubscription,
|
|
};
|
|
|
|
function pushUpdate(): void {
|
|
api.focusable = !hidden;
|
|
$editingInputs = $editingInputs;
|
|
}
|
|
|
|
$: {
|
|
hidden;
|
|
pushUpdate();
|
|
}
|
|
|
|
onMount(() => {
|
|
$editingInputs.push(api);
|
|
$editingInputs = $editingInputs;
|
|
|
|
const unsubscribeFromEditingArea = content.subscribe(writeFromEditingArea);
|
|
const unsubscribeToEditingArea = nodes.subscribe(writeToEditingArea);
|
|
|
|
return () => {
|
|
unsubscribeFromEditingArea();
|
|
unsubscribeToEditingArea();
|
|
};
|
|
});
|
|
</script>
|
|
|
|
<RichTextStyles
|
|
color="white"
|
|
let:attachToShadow={attachStyles}
|
|
let:promise={stylesPromise}
|
|
let:stylesDidLoad
|
|
>
|
|
<div
|
|
class:hidden
|
|
use:attachShadow
|
|
use:attachStyles
|
|
use:attachContentEditable={{ stylesDidLoad }}
|
|
on:focusin
|
|
on:focusout
|
|
/>
|
|
|
|
<div class="editable-widgets">
|
|
{#await Promise.all([richTextPromise, stylesPromise]) then [container, styles]}
|
|
<SetContext setter={set} value={{ container, styles, api }}>
|
|
<slot />
|
|
</SetContext>
|
|
{/await}
|
|
</div>
|
|
</RichTextStyles>
|
|
|
|
<style lang="scss">
|
|
.hidden {
|
|
display: none;
|
|
}
|
|
</style>
|