Anki/ts/editor/plain-text-input/PlainTextInput.svelte
Damien Elmes 9f55cf26fc
Switch to SvelteKit (#3077)
* Update to latest Node LTS

* Add sveltekit

* Split tslib into separate @generated and @tslib components

SvelteKit's path aliases don't support multiple locations, so our old
approach of using @tslib to refer to both ts/lib and out/ts/lib will no
longer work. Instead, all generated sources and their includes are
placed in a separate out/ts/generated folder, and imported via @generated
instead. This also allows us to generate .ts files, instead of needing
to output separate .d.ts and .js files.

* Switch package.json to module type

* Avoid usage of baseUrl

Incompatible with SvelteKit

* Move sass into ts; use relative links

SvelteKit's default sass support doesn't allow overriding loadPaths

* jest->vitest, graphs example working with yarn dev

* most pages working in dev mode

* Some fixes after rebasing

* Fix/silence some svelte-check errors

* Get image-occlusion working with Fabric types

* Post-rebase lock changes

* Editor is now checked

* SvelteKit build integrated into ninja

* Use the new SvelteKit entrypoint for pages like congrats/deck options/etc

* Run eslint once for ts/**; fix some tests

* Fix a bunch of issues introduced when rebasing over latest main

* Run eslint fix

* Fix remaining eslint+pylint issues; tests now all pass

* Fix some issues with a clean build

* Latest bufbuild no longer requires @__PURE__ hack

* Add a few missed dependencies

* Add yarn.bat to fix Windows build

* Fix pages failing to show when ANKI_API_PORT not defined

* Fix svelte-check and vitest on Windows

* Set node path in ./yarn

* Move svelte-kit output to ts/.svelte-kit

Sadly, I couldn't figure out a way to store it in out/ if out/ is
a symlink, as it breaks module resolution when SvelteKit is run.

* Allow HMR inside Anki

* Skip SvelteKit build when HMR is defined

* Fix some post-rebase issues

I should have done a normal merge instead.
2024-03-31 09:16:31 +01:00

189 lines
5.1 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 { registerPackage } from "@tslib/runtime-require";
import lifecycleHooks from "$lib/sveltelib/lifecycle-hooks";
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
import type { EditingInputAPI, FocusableInputAPI } from "../EditingArea.svelte";
export interface PlainTextInputAPI extends EditingInputAPI {
name: "plain-text";
moveCaretToEnd(): void;
toggle(): boolean;
codeMirror: CodeMirrorAPI;
}
export const parsingInstructions: string[] = [];
export const closeHTMLTags = writable(true);
const [lifecycle, instances, setupLifecycleHooks] =
lifecycleHooks<PlainTextInputAPI>();
registerPackage("anki/PlainTextInput", {
lifecycle,
instances,
});
</script>
<script lang="ts">
import { singleCallback } from "@tslib/typing";
import { onMount, tick } from "svelte";
import { writable } from "svelte/store";
import { pageTheme } from "$lib/sveltelib/theme";
import { baseOptions, gutterOptions, htmlanki } from "../code-mirror";
import CodeMirror from "../CodeMirror.svelte";
import { context as editingAreaContext } from "../EditingArea.svelte";
import { Flag } from "../helpers";
import { context as noteEditorContext } from "../NoteEditor.svelte";
import removeProhibitedTags from "./remove-prohibited";
import { storedToUndecorated, undecoratedToStored } from "./transform";
export let hidden = false;
export let fieldCollapsed = false;
export const focusFlag = new Flag();
$: configuration = {
mode: htmlanki,
...baseOptions,
...gutterOptions,
...{ autoCloseTags: $closeHTMLTags },
};
const { focusedInput } = noteEditorContext.get();
const { editingInputs, content } = editingAreaContext.get();
const code = writable($content);
let codeMirror = {} as CodeMirrorAPI;
async function focus(): Promise<void> {
const editor = await codeMirror.editor;
editor.focus();
}
async function moveCaretToEnd(): Promise<void> {
const editor = await codeMirror.editor;
editor.setCursor(editor.lineCount(), 0);
}
async function refocus(): Promise<void> {
const editor = (await codeMirror.editor) as any;
editor.display.input.blur();
focus();
moveCaretToEnd();
}
function toggle(): boolean {
hidden = !hidden;
return hidden;
}
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
const editor = (await codeMirror.editor) as any;
if (target === editor.display.input.textarea) {
return api;
}
return null;
}
export const api: PlainTextInputAPI = {
name: "plain-text",
focus,
focusable: !hidden,
moveCaretToEnd,
refocus,
toggle,
getInputAPI,
codeMirror,
};
/**
* Communicate to editing area that input is not focusable
*/
function pushUpdate(isFocusable: boolean): void {
api.focusable = isFocusable;
$editingInputs = $editingInputs;
}
async function refresh(): Promise<void> {
const editor = await codeMirror.editor;
editor.refresh();
}
$: {
pushUpdate(!(hidden || fieldCollapsed));
tick().then(() => {
refresh();
if (focusFlag.checkAndReset()) {
refocus();
}
});
}
function onChange({ detail: html }: CustomEvent<string>): void {
code.set(removeProhibitedTags(html));
}
onMount(() => {
$editingInputs.push(api);
$editingInputs = $editingInputs;
return singleCallback(
content.subscribe((html: string): void =>
/* We call `removeProhibitedTags` here, because content might
* have been changed outside the editor, and we need to parse
* it to get the "neutral" value. Otherwise, there might be
* conflicts with other editing inputs */
code.set(removeProhibitedTags(storedToUndecorated(html))),
),
code.subscribe((html: string): void =>
content.set(undecoratedToStored(html)),
),
);
});
setupLifecycleHooks(api);
</script>
<div
class="plain-text-input"
class:light-theme={!$pageTheme.isDark}
on:focusin={() => ($focusedInput = api)}
{hidden}
>
<CodeMirror
{configuration}
{code}
{hidden}
bind:api={codeMirror}
on:change={onChange}
/>
</div>
<style lang="scss">
.plain-text-input {
height: 100%;
:global(.CodeMirror) {
height: 100%;
background: var(--canvas-code);
padding-inline: 4px;
}
:global(.CodeMirror-lines) {
padding: 8px 0;
}
:global(.CodeMirror-gutters) {
background: var(--canvas-code);
}
}
</style>