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
This commit is contained in:
parent
cbc358ff0b
commit
c2768e2188
|
@ -58,8 +58,8 @@
|
|||
"@mdi/svg": "^6.1.95",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
"@types/lodash-es": "^4.17.4",
|
||||
"bootstrap": "=5.0.2",
|
||||
"@types/marked": "^3.0.1",
|
||||
"bootstrap": "=5.0.2",
|
||||
"bootstrap-icons": "^1.4.0",
|
||||
"codemirror": "^5.63.1",
|
||||
"css-browser-selector": "^0.6.5",
|
||||
|
|
|
@ -60,7 +60,7 @@ class AddCards(QDialog):
|
|||
|
||||
def setupEditor(self) -> None:
|
||||
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self, True)
|
||||
self.editor.web.eval("activateStickyShortcuts();")
|
||||
self.editor.web.eval("noteEditorPromise.then(() => activateStickyShortcuts());")
|
||||
|
||||
def setup_choosers(self) -> None:
|
||||
defaults = self.col.defaults_for_adding(
|
||||
|
@ -264,16 +264,6 @@ class AddCards(QDialog):
|
|||
|
||||
return True
|
||||
|
||||
def keyPressEvent(self, evt: QKeyEvent) -> None:
|
||||
"Show answer on RET or register answer."
|
||||
if (
|
||||
evt.key() in (Qt.Key.Key_Enter, Qt.Key.Key_Return)
|
||||
and self.editor.tags.hasFocus()
|
||||
):
|
||||
evt.accept()
|
||||
return
|
||||
return QDialog.keyPressEvent(self, evt)
|
||||
|
||||
def reject(self) -> None:
|
||||
self.ifCanClose(self._reject)
|
||||
|
||||
|
|
|
@ -78,15 +78,6 @@ audio = (
|
|||
"webm",
|
||||
)
|
||||
|
||||
_html = """
|
||||
<div id="fields"></div>
|
||||
<div id="dupes" class="d-none">
|
||||
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
||||
</div>
|
||||
<div id="cloze-hint" class="d-none"></div>
|
||||
<div id="tag-editor-anchor" class="d-none"></div>
|
||||
"""
|
||||
|
||||
|
||||
class Editor:
|
||||
"""The screen that embeds an editing widget should listen for changes via
|
||||
|
@ -135,14 +126,9 @@ class Editor:
|
|||
|
||||
# then load page
|
||||
self.web.stdHtml(
|
||||
_html % tr.editing_show_duplicates(),
|
||||
css=[
|
||||
"css/editor.css",
|
||||
],
|
||||
js=[
|
||||
"js/vendor/jquery.min.js",
|
||||
"js/editor.js",
|
||||
],
|
||||
"", # % tr.editing_show_duplicates(),
|
||||
css=["css/editor.css"],
|
||||
js=["js/editor.js"],
|
||||
context=self,
|
||||
default_css=True,
|
||||
)
|
||||
|
@ -506,7 +492,9 @@ $editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
|||
js += " setSticky(%s);" % json.dumps(sticky)
|
||||
|
||||
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
||||
self.web.evalWithCallback(js, oncallback)
|
||||
self.web.evalWithCallback(
|
||||
f"noteEditorPromise.then(() => {{ {js} }})", oncallback
|
||||
)
|
||||
|
||||
def _save_current_note(self) -> None:
|
||||
"Call after note is updated with data from webview."
|
||||
|
|
|
@ -31,8 +31,7 @@ $utilities: (
|
|||
flex-basis: 75%;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
* {
|
||||
overscroll-behavior: none;
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let top: boolean = false;
|
||||
export let bottom: boolean = false;
|
||||
export let left: boolean = false;
|
||||
export let right: boolean = false;
|
||||
</script>
|
||||
|
||||
<div class="absolute" class:top class:bottom class:left class:right>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.absolute {
|
||||
position: absolute;
|
||||
margin: var(--margin, 0);
|
||||
z-index: 20;
|
||||
}
|
||||
|
||||
.top {
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.bottom {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.left {
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.right {
|
||||
right: 0;
|
||||
}
|
||||
</style>
|
|
@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import IconConstrain from "./IconConstrain.svelte";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
import { dropdownKey } from "./context-keys";
|
||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||
|
@ -11,6 +12,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export { className as class };
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
export let iconSize = 100;
|
||||
export let widthMultiplier = 1;
|
||||
export let flipX = false;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let spanRef: HTMLSpanElement;
|
||||
|
@ -25,27 +30,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<span
|
||||
bind:this={spanRef}
|
||||
title={tooltip}
|
||||
class={`badge ${className}`}
|
||||
class="badge {className}"
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
{...dropdownProps}
|
||||
on:click
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
<IconConstrain {iconSize} {widthMultiplier} {flipX}>
|
||||
<slot />
|
||||
</IconConstrain>
|
||||
</span>
|
||||
|
||||
<style>
|
||||
.badge {
|
||||
color: inherit;
|
||||
color: var(--badge-color, inherit);
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
display: none;
|
||||
}
|
||||
|
||||
span :global(svg) {
|
||||
border-radius: inherit;
|
||||
vertical-align: var(--badge-align, -0.125rem);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,7 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
wrap={false}
|
||||
{api}
|
||||
>
|
||||
<div on:mousedown|preventDefault|stopPropagation>
|
||||
<div on:mousedown|preventDefault|stopPropagation on:click>
|
||||
<slot />
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
|
|
|
@ -111,9 +111,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: var(--buttons-wrap);
|
||||
padding: calc(var(--buttons-size) / 10);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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 WithTheming from "./WithTheming.svelte";
|
||||
import Detachable from "./Detachable.svelte";
|
||||
|
||||
import type { ButtonRegistration } from "./buttons";
|
||||
|
@ -58,9 +57,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
</script>
|
||||
|
||||
<!-- div in WithTheming is necessary to preserve item position -->
|
||||
<WithTheming {id} {style}>
|
||||
<!-- div is necessary to preserve item position -->
|
||||
<div {id} {style}>
|
||||
<Detachable {detached}>
|
||||
<slot />
|
||||
</Detachable>
|
||||
</WithTheming>
|
||||
</div>
|
||||
|
|
|
@ -80,7 +80,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<div
|
||||
bind:this={buttonToolbarRef}
|
||||
{id}
|
||||
class="btn-toolbar container wrap-variable {className}"
|
||||
class="button-toolbar btn-toolbar {className}"
|
||||
class:nightMode
|
||||
{style}
|
||||
role="toolbar"
|
||||
|
@ -101,7 +101,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
@include scrollbar.night-mode;
|
||||
}
|
||||
|
||||
.wrap-variable {
|
||||
.button-toolbar {
|
||||
flex-wrap: var(--buttons-wrap);
|
||||
|
||||
> :global(*) {
|
||||
/* TODO replace with gap once available */
|
||||
margin-right: 0.15rem;
|
||||
margin-bottom: 0.15rem;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,6 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import IconConstrain from "./IconConstrain.svelte";
|
||||
import { getContext, onMount, createEventDispatcher } from "svelte";
|
||||
import { nightModeKey, dropdownKey } from "./context-keys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
|
@ -16,9 +17,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let disabled = false;
|
||||
export let tabbable = false;
|
||||
|
||||
export let iconSize: number = 75;
|
||||
export let widthMultiplier: number = 1;
|
||||
export let flipX: boolean = false;
|
||||
export let iconSize = 75;
|
||||
export let widthMultiplier = 1;
|
||||
export let flipX = false;
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
|
@ -32,12 +33,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
class={`btn ${className}`}
|
||||
class="btn {className}"
|
||||
class:active
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
style={`--icon-size: ${iconSize}%`}
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
{disabled}
|
||||
|
@ -45,9 +45,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
on:click
|
||||
on:mousedown|preventDefault
|
||||
>
|
||||
<span class:flip-x={flipX} style={`--width-multiplier: ${widthMultiplier};`}>
|
||||
<IconConstrain {flipX} {widthMultiplier} {iconSize}>
|
||||
<slot />
|
||||
</span>
|
||||
</IconConstrain>
|
||||
</button>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -61,35 +61,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
@include button.btn-day;
|
||||
@include button.btn-night;
|
||||
|
||||
span {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
|
||||
/* constrain icon */
|
||||
width: calc((var(--buttons-size) - 2px) * var(--width-multiplier));
|
||||
height: calc(var(--buttons-size) - 2px);
|
||||
|
||||
& > :global(svg),
|
||||
& > :global(img) {
|
||||
position: absolute;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
top: calc((100% - var(--icon-size)) / 2);
|
||||
bottom: calc((100% - var(--icon-size)) / 2);
|
||||
left: calc((100% - var(--icon-size)) / 2);
|
||||
right: calc((100% - var(--icon-size)) / 2);
|
||||
|
||||
fill: currentColor;
|
||||
vertical-align: unset;
|
||||
}
|
||||
|
||||
&.flip-x > :global(svg),
|
||||
&.flip-x > :global(img) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
|
||||
.dropdown-toggle::after {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,47 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let iconSize: number = 100;
|
||||
export let widthMultiplier: number = 1;
|
||||
export let flipX: boolean = false;
|
||||
</script>
|
||||
|
||||
<span
|
||||
class:flip-x={flipX}
|
||||
style="--width-multiplier: {widthMultiplier}; --icon-size: {iconSize}%;"
|
||||
>
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
vertical-align: var(--icon-align, middle);
|
||||
|
||||
/* constrain icon */
|
||||
width: calc((var(--buttons-size, 22px) - 2px) * var(--width-multiplier));
|
||||
height: calc(var(--buttons-size, 22px) - 2px);
|
||||
|
||||
& > :global(svg),
|
||||
& > :global(img) {
|
||||
position: absolute;
|
||||
width: var(--icon-size);
|
||||
height: var(--icon-size);
|
||||
top: calc((100% - var(--icon-size)) / 2);
|
||||
bottom: calc((100% - var(--icon-size)) / 2);
|
||||
left: calc((100% - var(--icon-size)) / 2);
|
||||
right: calc((100% - var(--icon-size)) / 2);
|
||||
|
||||
fill: currentColor;
|
||||
vertical-align: unset;
|
||||
}
|
||||
|
||||
&.flip-x > :global(svg),
|
||||
&.flip-x > :global(img) {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -28,14 +28,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<!-- div is necessary to preserve item position -->
|
||||
<div {id}>
|
||||
<div class="item" {id}>
|
||||
<Detachable {detached}>
|
||||
<slot />
|
||||
</Detachable>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
/* TODO reactivate this once we can use CSS gap */
|
||||
/* .item { */
|
||||
/* display: contents; */
|
||||
/* } */
|
||||
</style>
|
||||
|
|
|
@ -0,0 +1,21 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import { registerShortcut } from "../lib/shortcuts";
|
||||
|
||||
export let keyCombination: string;
|
||||
export let target: EventTarget | Document = document;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() =>
|
||||
registerShortcut(
|
||||
(event: KeyboardEvent) => dispatch("action", { originalEvent: event }),
|
||||
keyCombination,
|
||||
target as any
|
||||
)
|
||||
);
|
||||
</script>
|
|
@ -17,12 +17,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<style lang="scss">
|
||||
footer {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: var(--window-bg);
|
||||
border-top: 1px solid var(--medium-border);
|
||||
border-style: solid;
|
||||
border-color: var(--medium-border);
|
||||
border-width: 0;
|
||||
padding: 0 3px;
|
||||
|
||||
bottom: 0;
|
||||
border-top-width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -15,12 +15,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<style lang="scss">
|
||||
header {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: var(--window-bg);
|
||||
border-bottom: 1px solid var(--medium-border);
|
||||
border-style: solid;
|
||||
border-color: var(--medium-border);
|
||||
border-width: 0;
|
||||
padding: 0 3px;
|
||||
|
||||
top: 0;
|
||||
border-bottom-width: 1px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
export let style: string;
|
||||
</script>
|
||||
|
||||
<div {id} class={className} {style}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
|
@ -17,18 +17,22 @@ compile_sass(
|
|||
],
|
||||
)
|
||||
|
||||
compile_svelte()
|
||||
|
||||
typescript(
|
||||
name = "editable",
|
||||
deps = [
|
||||
":svelte",
|
||||
_ts_deps = [
|
||||
"//ts/components",
|
||||
"//ts/lib",
|
||||
"//ts/sveltelib",
|
||||
"@npm//mathjax",
|
||||
"@npm//mathjax-full",
|
||||
"@npm//svelte",
|
||||
]
|
||||
|
||||
compile_svelte(deps = _ts_deps)
|
||||
|
||||
typescript(
|
||||
name = "editable",
|
||||
deps = _ts_deps + [
|
||||
":svelte",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,40 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import { updateAllState } from "../components/WithState.svelte";
|
||||
|
||||
export let nodes: Writable<DocumentFragment>;
|
||||
export let resolve: (editable: HTMLElement) => void;
|
||||
export let mirror: (
|
||||
editable: HTMLElement,
|
||||
params: { store: Writable<DocumentFragment> }
|
||||
) => void;
|
||||
</script>
|
||||
|
||||
<anki-editable
|
||||
contenteditable="true"
|
||||
use:resolve
|
||||
use:mirror={{ store: nodes }}
|
||||
on:focus
|
||||
on:blur
|
||||
on:click={updateAllState}
|
||||
on:keyup={updateAllState}
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
anki-editable {
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* editable-base.scss contains styling targeting user HTML */
|
||||
</style>
|
|
@ -34,8 +34,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
onMount(() => {
|
||||
observer.observe(image);
|
||||
|
||||
if (autofocus) {
|
||||
image.click();
|
||||
// This should trigger a focusing of the Mathjax Handle
|
||||
const focusEvent = new CustomEvent("focusmathjax", {
|
||||
detail: image,
|
||||
bubbles: true,
|
||||
composed: true,
|
||||
});
|
||||
|
||||
image.dispatchEvent(focusEvent);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -58,6 +66,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
/>
|
||||
|
||||
<style lang="scss">
|
||||
:global(anki-mathjax) {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
img {
|
||||
vertical-align: var(--vertical-center);
|
||||
}
|
||||
|
|
|
@ -20,9 +20,14 @@ export interface DecoratedElement extends HTMLElement {
|
|||
undecorate(): void;
|
||||
}
|
||||
|
||||
export interface DecoratedElementConstructor extends CustomElementConstructor {
|
||||
prototype: DecoratedElement;
|
||||
interface WithTagName {
|
||||
tagName: string;
|
||||
}
|
||||
|
||||
export interface DecoratedElementConstructor
|
||||
extends CustomElementConstructor,
|
||||
WithTagName {
|
||||
prototype: DecoratedElement;
|
||||
/**
|
||||
* Transforms elements in input HTML from undecorated to stored state.
|
||||
*/
|
||||
|
@ -33,13 +38,13 @@ export interface DecoratedElementConstructor extends CustomElementConstructor {
|
|||
toUndecorated(stored: string): string;
|
||||
}
|
||||
|
||||
class DefineArray extends Array {
|
||||
push(...elements: DecoratedElementConstructor[]) {
|
||||
export class CustomElementArray<
|
||||
T extends CustomElementConstructor & WithTagName
|
||||
> extends Array<T> {
|
||||
push(...elements: T[]): number {
|
||||
for (const element of elements) {
|
||||
customElements.define(element.tagName, element);
|
||||
}
|
||||
return super.push(...elements);
|
||||
}
|
||||
}
|
||||
|
||||
export const decoratedComponents: DecoratedElementConstructor[] = new DefineArray();
|
||||
|
|
|
@ -1,22 +1,11 @@
|
|||
@use "sass/scrollbar";
|
||||
|
||||
anki-editable {
|
||||
display: block;
|
||||
overflow-wrap: break-word;
|
||||
overflow: auto;
|
||||
padding: 6px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
* {
|
||||
* {
|
||||
max-width: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
anki-mathjax {
|
||||
white-space: pre;
|
||||
:host(.nightMode) & {
|
||||
@include scrollbar.night-mode;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
|
@ -29,27 +18,6 @@ p {
|
|||
}
|
||||
}
|
||||
|
||||
:host(.nightMode) * {
|
||||
@include scrollbar.night-mode;
|
||||
}
|
||||
|
||||
img.drawing {
|
||||
zoom: 50%;
|
||||
|
||||
.nightMode & {
|
||||
filter: unquote("invert() hue-rotate(180deg)");
|
||||
}
|
||||
}
|
||||
|
||||
[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@import "codemirror/lib/codemirror";
|
||||
@import "codemirror/theme/monokai";
|
||||
@import "codemirror/addon/fold/foldgutter";
|
||||
|
||||
.CodeMirror {
|
||||
height: auto;
|
||||
padding: 6px 0;
|
||||
}
|
||||
|
|
|
@ -1,96 +0,0 @@
|
|||
// 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",
|
||||
*/
|
||||
|
||||
function loadStyleLink(container: Node, href: string): Promise<void> {
|
||||
const rootStyle = document.createElement("link");
|
||||
rootStyle.setAttribute("rel", "stylesheet");
|
||||
rootStyle.setAttribute("href", href);
|
||||
|
||||
let styleResolve: () => void;
|
||||
const stylePromise = new Promise<void>((resolve) => (styleResolve = resolve));
|
||||
|
||||
rootStyle.addEventListener("load", () => styleResolve());
|
||||
container.appendChild(rootStyle);
|
||||
|
||||
return stylePromise;
|
||||
}
|
||||
|
||||
function loadStyleTag(container: Node): [HTMLStyleElement, Promise<void>] {
|
||||
const style = document.createElement("style");
|
||||
style.setAttribute("rel", "stylesheet");
|
||||
|
||||
let styleResolve: () => void;
|
||||
const stylePromise = new Promise<void>((resolve) => (styleResolve = resolve));
|
||||
|
||||
style.addEventListener("load", () => styleResolve());
|
||||
container.appendChild(style);
|
||||
|
||||
return [style, stylePromise];
|
||||
}
|
||||
|
||||
export class EditableContainer extends HTMLDivElement {
|
||||
baseStyle: HTMLStyleElement;
|
||||
imageStyle: HTMLStyleElement;
|
||||
|
||||
imagePromise: Promise<void>;
|
||||
stylePromise: Promise<void>;
|
||||
|
||||
baseRule?: CSSStyleRule;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
const shadow = this.attachShadow({ mode: "open" });
|
||||
|
||||
if (document.documentElement.classList.contains("night-mode")) {
|
||||
this.classList.add("night-mode");
|
||||
}
|
||||
|
||||
const rootPromise = loadStyleLink(shadow, "./_anki/css/editable-build.css");
|
||||
const [baseStyle, basePromise] = loadStyleTag(shadow);
|
||||
const [imageStyle, imagePromise] = loadStyleTag(shadow);
|
||||
|
||||
this.baseStyle = baseStyle;
|
||||
this.imageStyle = imageStyle;
|
||||
|
||||
this.imagePromise = imagePromise;
|
||||
this.stylePromise = Promise.all([
|
||||
rootPromise,
|
||||
basePromise,
|
||||
imagePromise,
|
||||
]) as unknown as Promise<void>;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
const sheet = this.baseStyle.sheet as CSSStyleSheet;
|
||||
const baseIndex = sheet.insertRule("anki-editable {}");
|
||||
this.baseRule = sheet.cssRules[baseIndex] as CSSStyleRule;
|
||||
}
|
||||
|
||||
initialize(color: string): void {
|
||||
this.setBaseColor(color);
|
||||
}
|
||||
|
||||
setBaseColor(color: string): void {
|
||||
if (this.baseRule) {
|
||||
this.baseRule.style.color = color;
|
||||
}
|
||||
}
|
||||
|
||||
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
|
||||
if (this.baseRule) {
|
||||
this.baseRule.style.fontFamily = fontFamily;
|
||||
this.baseRule.style.fontSize = fontSize;
|
||||
this.baseRule.style.direction = direction;
|
||||
}
|
||||
}
|
||||
|
||||
isRightToLeft(): boolean {
|
||||
return this.baseRule!.style.direction === "rtl";
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("anki-editable-container", EditableContainer, { extends: "div" });
|
|
@ -1,89 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// <reference types="../lib/shadow-dom" />
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import type { DecoratedElement } from "./decorated";
|
||||
import { decoratedComponents } from "./decorated";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { elementIsBlock, getBlockElement } from "../lib/dom";
|
||||
import { wrapInternal } from "../lib/wrap";
|
||||
|
||||
export function caretToEnd(node: Node): void {
|
||||
const range = document.createRange();
|
||||
range.selectNodeContents(node);
|
||||
range.collapse(false);
|
||||
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
}
|
||||
|
||||
function containsInlineContent(element: Element): boolean {
|
||||
for (const child of element.children) {
|
||||
if (elementIsBlock(child) || !containsInlineContent(child)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
export class Editable extends HTMLElement {
|
||||
set fieldHTML(content: string) {
|
||||
this.innerHTML = content;
|
||||
|
||||
if (content.length > 0 && containsInlineContent(this)) {
|
||||
this.appendChild(document.createElement("br"));
|
||||
}
|
||||
}
|
||||
|
||||
get fieldHTML(): string {
|
||||
const clone = this.cloneNode(true) as Element;
|
||||
|
||||
for (const component of decoratedComponents) {
|
||||
for (const element of clone.getElementsByTagName(component.tagName)) {
|
||||
(element as DecoratedElement).undecorate();
|
||||
}
|
||||
}
|
||||
|
||||
const result =
|
||||
containsInlineContent(clone) && clone.innerHTML.endsWith("<br>")
|
||||
? clone.innerHTML.slice(0, -4) // trim trailing <br>
|
||||
: clone.innerHTML;
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("contenteditable", "");
|
||||
}
|
||||
|
||||
caretToEnd(): void {
|
||||
caretToEnd(this);
|
||||
}
|
||||
|
||||
surroundSelection(before: string, after: string): void {
|
||||
wrapInternal(this.getRootNode() as ShadowRoot, before, after, false);
|
||||
}
|
||||
|
||||
onEnter(event: KeyboardEvent): void {
|
||||
if (
|
||||
!getBlockElement(this.getRootNode() as Document | ShadowRoot) !==
|
||||
event.shiftKey
|
||||
) {
|
||||
event.preventDefault();
|
||||
document.execCommand("insertLineBreak");
|
||||
}
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent): void {
|
||||
bridgeCommand("paste");
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("anki-editable", Editable);
|
|
@ -2,6 +2,7 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import "./editable-base.css";
|
||||
import "./editable-container";
|
||||
import "./editable";
|
||||
import "./mathjax-component";
|
||||
|
||||
/* only imported for the CSS */
|
||||
import "./ContentEditable.svelte";
|
||||
import "./Mathjax.svelte";
|
||||
|
|
|
@ -9,7 +9,6 @@
|
|||
import "mathjax/es5/tex-svg-full";
|
||||
|
||||
import type { DecoratedElement, DecoratedElementConstructor } from "./decorated";
|
||||
import { decoratedComponents } from "./decorated";
|
||||
import { nodeIsElement } from "../lib/dom";
|
||||
import { nightModeKey } from "../components/context-keys";
|
||||
|
||||
|
@ -38,7 +37,7 @@ function moveNodeOutOfElement(
|
|||
}
|
||||
|
||||
function placeCaretAfter(node: Node): void {
|
||||
const range = document.createRange();
|
||||
const range = new Range();
|
||||
range.setStartAfter(node);
|
||||
range.collapse(false);
|
||||
|
||||
|
@ -192,5 +191,3 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
decoratedComponents.push(Mathjax);
|
|
@ -8,7 +8,7 @@ load("//ts:typescript.bzl", "typescript")
|
|||
|
||||
compile_sass(
|
||||
srcs = [
|
||||
"fields.scss",
|
||||
"editor-base.scss",
|
||||
],
|
||||
group = "base_css",
|
||||
visibility = ["//visibility:public"],
|
||||
|
@ -33,12 +33,7 @@ compile_sass(
|
|||
],
|
||||
)
|
||||
|
||||
compile_svelte()
|
||||
|
||||
typescript(
|
||||
name = "editor_ts",
|
||||
deps = [
|
||||
":svelte",
|
||||
_ts_deps = [
|
||||
"//ts/components",
|
||||
"//ts/editable",
|
||||
"//ts/html-filter",
|
||||
|
@ -48,6 +43,14 @@ typescript(
|
|||
"@npm//@types/codemirror",
|
||||
"@npm//codemirror",
|
||||
"@npm//svelte",
|
||||
]
|
||||
|
||||
compile_svelte(deps = _ts_deps)
|
||||
|
||||
typescript(
|
||||
name = "editor_ts",
|
||||
deps = _ts_deps + [
|
||||
":svelte",
|
||||
],
|
||||
)
|
||||
|
||||
|
@ -79,13 +82,14 @@ eslint_test()
|
|||
svelte_check(
|
||||
name = "svelte_check",
|
||||
srcs = glob([
|
||||
"*.ts",
|
||||
"*.svelte",
|
||||
"**/*.ts",
|
||||
"**/*.svelte",
|
||||
]) + [
|
||||
"//sass:button_mixins_lib",
|
||||
"//sass/bootstrap",
|
||||
"@npm//@types/bootstrap",
|
||||
"//ts/components",
|
||||
"//ts/editable",
|
||||
"@npm//@types/bootstrap",
|
||||
"@npm//@types/codemirror",
|
||||
"@npm//codemirror",
|
||||
],
|
||||
|
|
|
@ -5,20 +5,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="typescript">
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
import WithShortcut from "../components/WithShortcut.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
|
||||
import * as tr from "../lib/ftl";
|
||||
import { withButton } from "../components/helpers";
|
||||
import { ellipseIcon } from "./icons";
|
||||
import { forEditorField } from ".";
|
||||
import { wrapCurrent } from "./wrap";
|
||||
import { get } from "svelte/store";
|
||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||
|
||||
const noteEditor = getNoteEditor();
|
||||
const { focusInRichText, activeInput } = noteEditor;
|
||||
|
||||
const clozePattern = /\{\{c(\d+)::/gu;
|
||||
function getCurrentHighestCloze(increment: boolean): number {
|
||||
let highest = 0;
|
||||
|
||||
forEditorField([], (field) => {
|
||||
const fieldHTML = field.editingArea.fieldHTML;
|
||||
for (const field of noteEditor.fields) {
|
||||
const content = field.editingArea?.content;
|
||||
const fieldHTML = content ? get(content) : "";
|
||||
|
||||
const matches: number[] = [];
|
||||
let match: RegExpMatchArray | null = null;
|
||||
|
||||
|
@ -27,7 +31,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
highest = Math.max(highest, ...matches);
|
||||
});
|
||||
}
|
||||
|
||||
if (increment) {
|
||||
highest++;
|
||||
|
@ -38,19 +42,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
function onCloze(event: KeyboardEvent | MouseEvent): void {
|
||||
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||
wrapCurrent(`{{c${highestCloze}::`, "}}");
|
||||
$activeInput?.surround(`{{c${highestCloze}::`, "}}");
|
||||
}
|
||||
|
||||
$: disabled = !$focusInRichText;
|
||||
</script>
|
||||
|
||||
<WithShortcut shortcut={"Control+Alt?+Shift+C"} let:createShortcut let:shortcutLabel>
|
||||
<OnlyEditable let:disabled>
|
||||
<WithShortcut shortcut="Control+Alt?+Shift+C" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
|
||||
tooltip="{tr.editingClozeDeletion()} {shortcutLabel}"
|
||||
{disabled}
|
||||
on:click={onCloze}
|
||||
on:mount={withButton(createShortcut)}
|
||||
>
|
||||
{@html ellipseIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</WithShortcut>
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="typescript">
|
||||
import { CodeMirror as CodeMirrorLib } from "./code-mirror";
|
||||
|
||||
export interface CodeMirrorAPI {
|
||||
readonly editor: CodeMirrorLib.EditorFromTextArea;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import storeSubscribe from "../sveltelib/store-subscribe";
|
||||
|
||||
export let configuration: CodeMirror.EditorConfiguration;
|
||||
export let code: Writable<string>;
|
||||
|
||||
let codeMirror: CodeMirror.EditorFromTextArea;
|
||||
|
||||
function setValue(content: string): void {
|
||||
codeMirror.setValue(content);
|
||||
}
|
||||
|
||||
const { subscribe, unsubscribe } = storeSubscribe(code, setValue, false);
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
function openCodeMirror(textarea: HTMLTextAreaElement): void {
|
||||
codeMirror = CodeMirrorLib.fromTextArea(textarea, configuration);
|
||||
|
||||
// TODO passing in the tabindex option does not do anything: bug?
|
||||
codeMirror.getInputField().tabIndex = 0;
|
||||
|
||||
codeMirror.on("change", () => dispatch("change", codeMirror.getValue()));
|
||||
codeMirror.on("focus", () => {
|
||||
unsubscribe();
|
||||
dispatch("focus");
|
||||
});
|
||||
codeMirror.on("blur", () => {
|
||||
subscribe();
|
||||
dispatch("blur");
|
||||
});
|
||||
|
||||
subscribe();
|
||||
}
|
||||
|
||||
export const api = Object.create(
|
||||
{},
|
||||
{
|
||||
editor: { get: () => codeMirror },
|
||||
}
|
||||
) as CodeMirrorAPI;
|
||||
</script>
|
||||
|
||||
<div class="code-mirror">
|
||||
<textarea tabindex="-1" hidden use:openCodeMirror />
|
||||
</div>
|
|
@ -9,13 +9,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ColorPicker from "../components/ColorPicker.svelte";
|
||||
import WithShortcut from "../components/WithShortcut.svelte";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
|
||||
import * as tr from "../lib/ftl";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { withButton } from "../components/helpers";
|
||||
import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons";
|
||||
import { appendInParentheses, execCommand } from "./helpers";
|
||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||
|
||||
export let api = {};
|
||||
export let textColor: string;
|
||||
|
@ -31,11 +31,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const wrapWithBackcolor = (color: string) => () => {
|
||||
execCommand("backcolor", false, color);
|
||||
};
|
||||
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
$: disabled = !$focusInRichText;
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<WithColorHelper color={textColor} let:colorHelperIcon let:setColor>
|
||||
<OnlyEditable let:disabled>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F7"} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
|
@ -76,11 +78,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
</OnlyEditable>
|
||||
</WithColorHelper>
|
||||
|
||||
<WithColorHelper color={highlightColor} let:colorHelperIcon let:setColor>
|
||||
<OnlyEditable let:disabled>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingSetTextHighlightColor()}
|
||||
|
@ -109,6 +109,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
/>
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
</OnlyEditable>
|
||||
</WithColorHelper>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -6,10 +6,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import IconButton from "../components/IconButton.svelte";
|
||||
import WithShortcut from "../components/WithShortcut.svelte";
|
||||
import WithState from "../components/WithState.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
|
||||
import { withButton } from "../components/helpers";
|
||||
import { appendInParentheses, execCommand, queryCommandState } from "./helpers";
|
||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||
|
||||
export let key: string;
|
||||
export let tooltip: string;
|
||||
|
@ -17,14 +17,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
export let withoutShortcut = false;
|
||||
export let withoutState = false;
|
||||
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
$: disabled = !$focusInRichText;
|
||||
</script>
|
||||
|
||||
<OnlyEditable let:disabled>
|
||||
{#if withoutShortcut && withoutState}
|
||||
{#if withoutShortcut && withoutState}
|
||||
<IconButton {tooltip} {disabled} on:click={() => execCommand(key)}>
|
||||
<slot />
|
||||
</IconButton>
|
||||
{:else if withoutShortcut}
|
||||
{:else if withoutShortcut}
|
||||
<WithState
|
||||
{key}
|
||||
update={() => queryCommandState(key)}
|
||||
|
@ -43,7 +45,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<slot />
|
||||
</IconButton>
|
||||
</WithState>
|
||||
{:else if withoutState}
|
||||
{:else if withoutState}
|
||||
<WithShortcut {shortcut} let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tooltip, shortcutLabel)}
|
||||
|
@ -54,7 +56,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<slot />
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
{:else}
|
||||
{:else}
|
||||
<WithShortcut {shortcut} let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
{key}
|
||||
|
@ -76,5 +78,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
{/if}
|
||||
</OnlyEditable>
|
||||
{/if}
|
||||
|
|
|
@ -10,7 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import WithState from "../components/WithState.svelte";
|
||||
|
||||
import * as contextKeys from "../components/context-keys";
|
||||
import * as editorContextKeys from "./context-keys";
|
||||
import * as editorContextKeys from "./NoteEditor.svelte";
|
||||
|
||||
export const components = {
|
||||
IconButton,
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
<!--
|
||||
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">
|
||||
interface Identifiable {
|
||||
id: string;
|
||||
}
|
||||
|
||||
export interface StyleLinkType extends Identifiable {
|
||||
type: "link";
|
||||
href: string;
|
||||
}
|
||||
|
||||
export interface StyleTagType extends Identifiable {
|
||||
type: "style";
|
||||
}
|
||||
|
||||
export type StyleType = StyleLinkType | StyleTagType;
|
||||
|
||||
type StyleHTMLTag = HTMLStyleElement | HTMLLinkElement;
|
||||
|
||||
export interface StyleObject {
|
||||
element: StyleHTMLTag;
|
||||
}
|
||||
|
||||
export const customStylesKey = Symbol("customStyles");
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { setContext } from "svelte";
|
||||
|
||||
import StyleLink from "./StyleLink.svelte";
|
||||
import StyleTag from "./StyleTag.svelte";
|
||||
|
||||
export let styles: StyleType[];
|
||||
export const styleMap = new Map<string, StyleObject>();
|
||||
|
||||
const resolvers = new Map<string, (object: StyleObject) => void>();
|
||||
|
||||
function register(id: string, object: StyleObject): void {
|
||||
styleMap.set(id, object);
|
||||
|
||||
if (resolvers.has(id)) {
|
||||
resolvers.get(id)!(object);
|
||||
resolvers.delete(id);
|
||||
}
|
||||
}
|
||||
|
||||
function deregister(id: string): void {
|
||||
styleMap.delete(id);
|
||||
}
|
||||
|
||||
setContext(customStylesKey, { register, deregister });
|
||||
|
||||
function waitForRegistration(id: string): Promise<StyleObject> {
|
||||
let styleResolve: (element: StyleObject) => void;
|
||||
const promise = new Promise<StyleObject>((resolve) => (styleResolve = resolve));
|
||||
|
||||
resolvers.set(id, styleResolve!);
|
||||
return promise;
|
||||
}
|
||||
|
||||
export function addStyleLink(id: string, href: string): Promise<StyleObject> {
|
||||
styles.push({ id, type: "link", href });
|
||||
styles = styles;
|
||||
|
||||
return waitForRegistration(id);
|
||||
}
|
||||
|
||||
export function addStyleTag(id: string): Promise<StyleObject> {
|
||||
styles.push({ id, type: "style" });
|
||||
styles = styles;
|
||||
|
||||
return waitForRegistration(id);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#each styles as style (style.id)}
|
||||
{#if style.type === "link"}
|
||||
<StyleLink id={style.id} href={style.href} />
|
||||
{:else}
|
||||
<StyleTag id={style.id} />
|
||||
{/if}
|
||||
{/each}
|
|
@ -0,0 +1,27 @@
|
|||
<!--
|
||||
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 {
|
||||
CustomElementArray,
|
||||
DecoratedElementConstructor,
|
||||
} from "../editable/decorated";
|
||||
import { Mathjax } from "../editable/mathjax-element";
|
||||
import contextProperty from "../sveltelib/context-property";
|
||||
|
||||
const decoratedElements = new CustomElementArray<DecoratedElementConstructor>();
|
||||
decoratedElements.push(Mathjax);
|
||||
|
||||
const key = Symbol("decoratedElements");
|
||||
const [set, getDecoratedElements, hasDecoratedElements] =
|
||||
contextProperty<CustomElementArray<DecoratedElementConstructor>>(key);
|
||||
|
||||
export { getDecoratedElements, hasDecoratedElements };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
set(decoratedElements);
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,164 @@
|
|||
<!--
|
||||
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 { Writable } from "svelte/store";
|
||||
import contextProperty from "../sveltelib/context-property";
|
||||
|
||||
export interface EditingInputAPI {
|
||||
readonly name: string;
|
||||
focusable: boolean;
|
||||
focus(): void;
|
||||
refocus(): void;
|
||||
}
|
||||
|
||||
export interface EditingAreaAPI {
|
||||
content: Writable<string>;
|
||||
editingInputs: Writable<EditingInputAPI[]>;
|
||||
focus(): void;
|
||||
refocus(): void;
|
||||
}
|
||||
|
||||
const key = Symbol("editingArea");
|
||||
const [set, getEditingArea, hasEditingArea] = contextProperty<EditingAreaAPI>(key);
|
||||
|
||||
export { getEditingArea, hasEditingArea };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
import { onMount, setContext as svelteSetContext } from "svelte";
|
||||
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
||||
|
||||
export let fontFamily: string;
|
||||
const fontFamilyStore = writable(fontFamily);
|
||||
$: $fontFamilyStore = fontFamily;
|
||||
svelteSetContext(fontFamilyKey, fontFamilyStore);
|
||||
|
||||
export let fontSize: number;
|
||||
const fontSizeStore = writable(fontSize);
|
||||
$: $fontSizeStore = fontSize;
|
||||
svelteSetContext(fontSizeKey, fontSizeStore);
|
||||
|
||||
export let content: Writable<string>;
|
||||
export let autofocus = false;
|
||||
|
||||
let editingArea: HTMLElement;
|
||||
let focusTrap: HTMLInputElement;
|
||||
|
||||
const inputsStore = writable<EditingInputAPI[]>([]);
|
||||
|
||||
$: editingInputs = $inputsStore;
|
||||
|
||||
function getAvailableInput(): EditingInputAPI | undefined {
|
||||
return editingInputs.find((input) => input.focusable);
|
||||
}
|
||||
|
||||
function focusEditingInputIfAvailable(): boolean {
|
||||
const availableInput = getAvailableInput();
|
||||
|
||||
if (availableInput) {
|
||||
availableInput.focus();
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
function focusEditingInputIfFocusTrapFocused(): void {
|
||||
if (document.activeElement === focusTrap) {
|
||||
focusEditingInputIfAvailable();
|
||||
}
|
||||
}
|
||||
|
||||
$: {
|
||||
$inputsStore;
|
||||
focusEditingInputIfFocusTrapFocused();
|
||||
}
|
||||
|
||||
function focus(): void {
|
||||
if (editingArea.contains(document.activeElement)) {
|
||||
// do nothing
|
||||
} else if (!focusEditingInputIfAvailable()) {
|
||||
focusTrap.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function refocus(): void {
|
||||
const availableInput = getAvailableInput();
|
||||
|
||||
if (availableInput) {
|
||||
availableInput.refocus();
|
||||
} else {
|
||||
focusTrap.blur();
|
||||
focusTrap.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function focusEditingInputInsteadIfAvailable(event: FocusEvent): void {
|
||||
if (focusEditingInputIfAvailable()) {
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
// prevents editor field being entirely deselected when
|
||||
// closing active field
|
||||
function trapFocusOnBlurOut(event: FocusEvent): void {
|
||||
if (!event.relatedTarget && editingInputs.every((input) => !input.focusable)) {
|
||||
focusTrap.focus();
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
export const api = set({
|
||||
content,
|
||||
editingInputs: inputsStore,
|
||||
focus,
|
||||
refocus,
|
||||
});
|
||||
|
||||
onMount(() => {
|
||||
if (autofocus) {
|
||||
focus();
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<input
|
||||
bind:this={focusTrap}
|
||||
readonly
|
||||
tabindex="-1"
|
||||
class="focus-trap"
|
||||
on:focus={focusEditingInputInsteadIfAvailable}
|
||||
/>
|
||||
|
||||
<div bind:this={editingArea} class="editing-area" on:focusout={trapFocusOnBlurOut}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editing-area {
|
||||
position: relative;
|
||||
background: var(--frame-bg);
|
||||
border-radius: 0 0 5px 5px;
|
||||
|
||||
&:focus {
|
||||
outline: none;
|
||||
}
|
||||
}
|
||||
|
||||
.focus-trap {
|
||||
display: block;
|
||||
width: 0px;
|
||||
height: 0;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
-webkit-appearance: none;
|
||||
background: none;
|
||||
resize: none;
|
||||
appearance: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,104 @@
|
|||
<!--
|
||||
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 { EditingAreaAPI } from "./EditingArea.svelte";
|
||||
import contextProperty from "../sveltelib/context-property";
|
||||
|
||||
export interface FieldData {
|
||||
name: string;
|
||||
fontFamily: string;
|
||||
fontSize: number;
|
||||
direction: "ltr" | "rtl";
|
||||
}
|
||||
|
||||
export interface EditorFieldAPI {
|
||||
element: HTMLElement;
|
||||
index: number;
|
||||
direction: "ltr" | "rtl";
|
||||
editingArea?: EditingAreaAPI;
|
||||
}
|
||||
|
||||
const key = Symbol("editorField");
|
||||
const [set, getEditorField, hasEditorField] = contextProperty<EditorFieldAPI>(key);
|
||||
|
||||
export { getEditorField, hasEditorField };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import EditingArea from "./EditingArea.svelte";
|
||||
import LabelContainer from "./LabelContainer.svelte";
|
||||
import LabelName from "./LabelName.svelte";
|
||||
import FieldState from "./FieldState.svelte";
|
||||
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { directionKey } from "../lib/context-keys";
|
||||
|
||||
export let content: Writable<string>;
|
||||
export let field: FieldData;
|
||||
export let autofocus = false;
|
||||
|
||||
const directionStore = writable();
|
||||
setContext(directionKey, directionStore);
|
||||
|
||||
$: $directionStore = field.direction;
|
||||
|
||||
let editorField: HTMLElement;
|
||||
|
||||
export const api = set(
|
||||
Object.create(
|
||||
{},
|
||||
{
|
||||
element: {
|
||||
get: () => editorField,
|
||||
},
|
||||
direction: {
|
||||
get: () => $directionStore,
|
||||
},
|
||||
}
|
||||
) as EditorFieldAPI
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={editorField}
|
||||
class="editor-field"
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:click={() => api.editingArea?.focus()}
|
||||
>
|
||||
<LabelContainer>
|
||||
<LabelName>{field.name}</LabelName>
|
||||
<FieldState><slot name="field-state" /></FieldState>
|
||||
</LabelContainer>
|
||||
<EditingArea
|
||||
{content}
|
||||
{autofocus}
|
||||
fontFamily={field.fontFamily}
|
||||
fontSize={field.fontSize}
|
||||
bind:api={api.editingArea}
|
||||
>
|
||||
<slot name="editing-inputs" />
|
||||
</EditingArea>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editor-field {
|
||||
--border-color: var(--border);
|
||||
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
min-width: 0;
|
||||
|
||||
&:focus-within {
|
||||
--border-color: var(--focus-border);
|
||||
|
||||
outline: none;
|
||||
box-shadow: 0 0 0 3px var(--focus-shadow);
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -25,7 +25,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
import { isApplePlatform } from "../lib/platform";
|
||||
import StickyHeader from "../components/StickyHeader.svelte";
|
||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
@ -36,8 +35,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ColorButtons from "./ColorButtons.svelte";
|
||||
import TemplateButtons from "./TemplateButtons.svelte";
|
||||
|
||||
export let size = isApplePlatform() ? 1.6 : 2.0;
|
||||
export let wrap = true;
|
||||
export let size: number;
|
||||
export let wrap: boolean;
|
||||
|
||||
export let textColor: string;
|
||||
export let highlightColor: string;
|
||||
|
|
|
@ -0,0 +1,17 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<span class="field-state justify-content-between">
|
||||
<slot />
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
.field-state {
|
||||
display: flex;
|
||||
|
||||
& > :global(*) {
|
||||
margin-left: 2px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,27 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<div class="fields">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.fields {
|
||||
display: grid;
|
||||
grid-auto-rows: min-content;
|
||||
grid-gap: 4px;
|
||||
|
||||
padding: 5px 3px 0;
|
||||
/* set height to 100% for rich text widgets */
|
||||
height: 100%;
|
||||
|
||||
/* moves the scrollbar inside the editor */
|
||||
overflow-x: hidden;
|
||||
|
||||
> :global(:last-child) {
|
||||
/* bottom padding is eaten by overflow-x */
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,17 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<div class="multi-root-editor">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.multi-root-editor {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
</style>
|
|
@ -9,12 +9,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ButtonDropdown from "../components/ButtonDropdown.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import WithDropdown from "../components/WithDropdown.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
import CommandIconButton from "./CommandIconButton.svelte";
|
||||
|
||||
import * as tr from "../lib/ftl";
|
||||
import { getListItem } from "../lib/dom";
|
||||
import { getCurrentField, execCommand } from "./helpers";
|
||||
import { execCommand } from "./helpers";
|
||||
import {
|
||||
ulIcon,
|
||||
olIcon,
|
||||
|
@ -26,12 +25,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
indentIcon,
|
||||
outdentIcon,
|
||||
} from "./icons";
|
||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||
|
||||
export let api = {};
|
||||
|
||||
function outdentListItem() {
|
||||
const currentField = getCurrentField();
|
||||
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
|
||||
if (getListItem(document.activeElement!.shadowRoot!)) {
|
||||
execCommand("outdent");
|
||||
} else {
|
||||
alert("Indent/unindent currently only works with lists.");
|
||||
|
@ -39,13 +38,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function indentListItem() {
|
||||
const currentField = getCurrentField();
|
||||
if (getListItem(currentField!.editableContainer.shadowRoot!)) {
|
||||
if (getListItem(document.activeElement!.shadowRoot!)) {
|
||||
execCommand("indent");
|
||||
} else {
|
||||
alert("Indent/unindent currently only works with lists.");
|
||||
}
|
||||
}
|
||||
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
$: disabled = !$focusInRichText;
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
|
@ -67,14 +68,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdown let:createDropdown>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
{disabled}
|
||||
on:mount={(event) => createDropdown(event.detail.button)}
|
||||
>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
|
||||
<ButtonDropdown>
|
||||
<Item id="justify">
|
||||
|
@ -120,7 +119,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<Item id="indentation">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
on:click={outdentListItem}
|
||||
tooltip={tr.editingOutdent()}
|
||||
|
@ -128,11 +126,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton
|
||||
on:click={indentListItem}
|
||||
tooltip={tr.editingIndent()}
|
||||
|
@ -140,7 +136,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
|
|
|
@ -3,18 +3,22 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext } from "svelte";
|
||||
import { directionKey } from "../lib/context-keys";
|
||||
import { afterUpdate, createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
export let isRtl: boolean;
|
||||
|
||||
let dimensions: HTMLDivElement;
|
||||
let overflowFix = 0;
|
||||
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
|
||||
function updateOverflow(dimensions: HTMLDivElement): void {
|
||||
const boundingClientRect = dimensions.getBoundingClientRect();
|
||||
const overflow = isRtl
|
||||
? window.innerWidth - boundingClientRect.x - boundingClientRect.width
|
||||
: boundingClientRect.x;
|
||||
const overflow =
|
||||
$direction === "ltr"
|
||||
? boundingClientRect.x
|
||||
: window.innerWidth - boundingClientRect.x - boundingClientRect.width;
|
||||
|
||||
overflowFix = Math.min(0, overflowFix + overflow, overflow);
|
||||
}
|
||||
|
@ -33,7 +37,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<div
|
||||
bind:this={dimensions}
|
||||
class="image-handle-dimensions"
|
||||
class:is-rtl={isRtl}
|
||||
class:is-rtl={$direction === "rtl"}
|
||||
style="--overflow-fix: {overflowFix}px"
|
||||
use:updateOverflowAsync
|
||||
>
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "../lib/ftl";
|
||||
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
|
||||
import { sizeActual, sizeMinimized } from "./icons";
|
||||
|
||||
export let active: boolean;
|
||||
export let isRtl: boolean;
|
||||
|
||||
$: icon = active ? sizeActual : sizeMinimized;
|
||||
</script>
|
||||
|
||||
<ButtonGroup size={1.6}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton {active} flipX={isRtl} tooltip={tr.editingActualSize()} on:click
|
||||
>{@html icon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext } from "svelte";
|
||||
import { directionKey } from "../lib/context-keys";
|
||||
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
</script>
|
||||
|
||||
<div
|
||||
class="label-container"
|
||||
class:rtl={$direction === "rtl"}
|
||||
on:mousedown|preventDefault
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.label-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
background-color: var(--label-color, transparent);
|
||||
|
||||
border-width: 0 0 1px;
|
||||
border-style: dashed;
|
||||
border-color: var(--border-color);
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
padding: 0px 6px;
|
||||
}
|
||||
|
||||
.rtl {
|
||||
direction: rtl;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,11 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<span class="label-name"><slot /></span>
|
||||
|
||||
<style lang="scss">
|
||||
.label-name {
|
||||
user-select: none;
|
||||
}
|
||||
</style>
|
|
@ -1,109 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import WithDropdown from "../components/WithDropdown.svelte";
|
||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||
import DropdownMenu from "../components/DropdownMenu.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
import HandleSelection from "./HandleSelection.svelte";
|
||||
import HandleBackground from "./HandleBackground.svelte";
|
||||
import HandleControl from "./HandleControl.svelte";
|
||||
import MathjaxHandleInlineBlock from "./MathjaxHandleInlineBlock.svelte";
|
||||
import MathjaxHandleEditor from "./MathjaxHandleEditor.svelte";
|
||||
|
||||
export let activeImage: HTMLImageElement | null = null;
|
||||
export let container: HTMLElement;
|
||||
export let isRtl: boolean;
|
||||
|
||||
let dropdownApi: any;
|
||||
|
||||
let removeEventListener: () => void = () => {
|
||||
/* noop */
|
||||
};
|
||||
|
||||
function onImageResize(): void {
|
||||
if (activeImage) {
|
||||
errorMessage = activeImage.title;
|
||||
updateSelection().then(() => dropdownApi.update());
|
||||
}
|
||||
}
|
||||
|
||||
$: if (activeImage) {
|
||||
activeImage.addEventListener("resize", onImageResize);
|
||||
|
||||
const lastImage = activeImage;
|
||||
removeEventListener = () =>
|
||||
lastImage.removeEventListener("resize", onImageResize);
|
||||
} else {
|
||||
removeEventListener();
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(async () => {
|
||||
if (activeImage) {
|
||||
await updateSelection();
|
||||
dropdownApi.update();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(container);
|
||||
|
||||
let updateSelection: () => Promise<void>;
|
||||
let errorMessage: string;
|
||||
|
||||
function getComponent(image: HTMLImageElement): HTMLElement {
|
||||
return image.closest("anki-mathjax")! as HTMLElement;
|
||||
}
|
||||
|
||||
function onEditorUpdate(event: CustomEvent): void {
|
||||
/* this updates the image in Mathjax.svelte */
|
||||
getComponent(activeImage!).dataset.mathjax = event.detail.mathjax;
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithDropdown
|
||||
drop="down"
|
||||
autoOpen={true}
|
||||
autoClose={false}
|
||||
distance={4}
|
||||
let:createDropdown
|
||||
>
|
||||
{#if activeImage}
|
||||
<HandleSelection
|
||||
image={activeImage}
|
||||
{container}
|
||||
bind:updateSelection
|
||||
on:mount={(event) => (dropdownApi = createDropdown(event.detail.selection))}
|
||||
>
|
||||
<HandleBackground tooltip={errorMessage} />
|
||||
|
||||
<HandleControl offsetX={1} offsetY={1} />
|
||||
</HandleSelection>
|
||||
|
||||
<DropdownMenu>
|
||||
<MathjaxHandleEditor
|
||||
initialValue={getComponent(activeImage).dataset.mathjax ?? ""}
|
||||
on:update={onEditorUpdate}
|
||||
/>
|
||||
<div class="margin-x">
|
||||
<ButtonToolbar>
|
||||
<Item>
|
||||
<MathjaxHandleInlineBlock
|
||||
{activeImage}
|
||||
{isRtl}
|
||||
on:click={updateSelection}
|
||||
/>
|
||||
</Item>
|
||||
</ButtonToolbar>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
{/if}
|
||||
</WithDropdown>
|
||||
|
||||
<style lang="scss">
|
||||
.margin-x {
|
||||
margin: 0 0.125rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,16 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<div class="note-editor">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.note-editor {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { fly } from "svelte/transition";
|
||||
</script>
|
||||
|
||||
<div class="notification" transition:fly={{ x: 200 }}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.notification {
|
||||
background-color: var(--notification-bg, var(--window-bg));
|
||||
user-select: none;
|
||||
|
||||
border: 1px solid var(--medium-border);
|
||||
border-radius: 5px;
|
||||
padding: 0.9rem 1.2rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,323 @@
|
|||
<!--
|
||||
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>
|
|
@ -1,14 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import WithContext from "../components/WithContext.svelte";
|
||||
import { fieldFocusedKey, inCodableKey } from "./context-keys";
|
||||
</script>
|
||||
|
||||
<WithContext key={fieldFocusedKey} let:context={fieldFocused}>
|
||||
<WithContext key={inCodableKey} let:context={inCodable}>
|
||||
<slot disabled={!fieldFocused || inCodable} />
|
||||
</WithContext>
|
||||
</WithContext>
|
|
@ -0,0 +1,45 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Badge from "../components/Badge.svelte";
|
||||
|
||||
import * as tr from "../lib/ftl";
|
||||
import { onMount } from "svelte";
|
||||
import { htmlOn, htmlOff } from "./icons";
|
||||
import { getEditorField } from "./EditorField.svelte";
|
||||
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
||||
|
||||
const editorField = getEditorField();
|
||||
const keyCombination = "Control+Shift+X";
|
||||
|
||||
export let off = false;
|
||||
|
||||
$: icon = off ? htmlOff : htmlOn;
|
||||
|
||||
function toggle() {
|
||||
off = !off;
|
||||
}
|
||||
|
||||
onMount(() =>
|
||||
registerShortcut(toggle, keyCombination, editorField.element as HTMLElement)
|
||||
);
|
||||
</script>
|
||||
|
||||
<span on:click|stopPropagation={toggle}>
|
||||
<Badge
|
||||
tooltip="{tr.editingHtmlEditor()} ({getPlatformString(keyCombination)})"
|
||||
iconSize={80}>{@html icon}</Badge
|
||||
>
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,160 @@
|
|||
<!--
|
||||
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 { EditingInputAPI } from "./EditingArea.svelte";
|
||||
|
||||
export interface PlainTextInputAPI extends EditingInputAPI {
|
||||
name: "plain-text";
|
||||
moveCaretToEnd(): void;
|
||||
toggle(): boolean;
|
||||
surround(before: string, after: string): void;
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import CodeMirror from "./CodeMirror.svelte";
|
||||
import type { CodeMirrorAPI } from "./CodeMirror.svelte";
|
||||
|
||||
import { tick, onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import { getDecoratedElements } from "./DecoratedElements.svelte";
|
||||
import { getEditingArea } from "./EditingArea.svelte";
|
||||
import { htmlanki, baseOptions, gutterOptions } from "./code-mirror";
|
||||
|
||||
export let hidden = false;
|
||||
|
||||
const configuration = {
|
||||
mode: htmlanki,
|
||||
...baseOptions,
|
||||
...gutterOptions,
|
||||
};
|
||||
|
||||
const { editingInputs, content } = getEditingArea();
|
||||
const decoratedElements = getDecoratedElements();
|
||||
const code = writable($content);
|
||||
|
||||
function adjustInputHTML(html: string): string {
|
||||
for (const component of decoratedElements) {
|
||||
html = component.toUndecorated(html);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
const parser = new DOMParser();
|
||||
// TODO Expose this somehow
|
||||
const parseStyle = "<style>anki-mathjax { white-space: pre; }</style>";
|
||||
|
||||
function parseAsHTML(html: string): string {
|
||||
const doc = parser.parseFromString(parseStyle + html, "text/html");
|
||||
const body = doc.body;
|
||||
|
||||
for (const script of body.getElementsByTagName("script")) {
|
||||
script.remove();
|
||||
}
|
||||
|
||||
for (const script of body.getElementsByTagName("link")) {
|
||||
script.remove();
|
||||
}
|
||||
|
||||
for (const style of body.getElementsByTagName("style")) {
|
||||
style.remove();
|
||||
}
|
||||
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
function adjustOutputHTML(html: string): string {
|
||||
for (const component of decoratedElements) {
|
||||
html = component.toStored(html);
|
||||
}
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
let codeMirror: CodeMirrorAPI;
|
||||
|
||||
function focus(): void {
|
||||
codeMirror?.editor.focus();
|
||||
}
|
||||
|
||||
function refocus(): void {
|
||||
(codeMirror?.editor as any).display.input.blur();
|
||||
focus();
|
||||
}
|
||||
|
||||
function moveCaretToEnd(): void {
|
||||
codeMirror?.editor.setCursor(codeMirror.editor.lineCount(), 0);
|
||||
}
|
||||
|
||||
function surround(before: string, after: string): void {
|
||||
const selection = codeMirror?.editor.getSelection();
|
||||
codeMirror?.editor.replaceSelection(before + selection + after);
|
||||
}
|
||||
|
||||
export const api = {
|
||||
name: "plain-text",
|
||||
focus,
|
||||
focusable: !hidden,
|
||||
moveCaretToEnd,
|
||||
refocus,
|
||||
toggle(): boolean {
|
||||
hidden = !hidden;
|
||||
return hidden;
|
||||
},
|
||||
surround,
|
||||
} as PlainTextInputAPI;
|
||||
|
||||
function pushUpdate(): void {
|
||||
api.focusable = !hidden;
|
||||
$editingInputs = $editingInputs;
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
codeMirror.editor.refresh();
|
||||
}
|
||||
|
||||
$: {
|
||||
hidden;
|
||||
tick().then(refresh);
|
||||
pushUpdate();
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
$editingInputs.push(api);
|
||||
$editingInputs = $editingInputs;
|
||||
|
||||
const unsubscribeFromEditingArea = content.subscribe((value: string): void => {
|
||||
const adjusted = adjustInputHTML(value);
|
||||
code.set(adjusted);
|
||||
});
|
||||
|
||||
const unsubscribeToEditingArea = code.subscribe((value: string): void => {
|
||||
const parsed = parseAsHTML(value);
|
||||
content.set(adjustOutputHTML(parsed));
|
||||
});
|
||||
|
||||
return () => {
|
||||
unsubscribeFromEditingArea();
|
||||
unsubscribeToEditingArea();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class:hidden on:focusin on:focusout>
|
||||
<CodeMirror
|
||||
{configuration}
|
||||
{code}
|
||||
bind:api={codeMirror}
|
||||
on:focus={moveCaretToEnd}
|
||||
on:change={({ detail: html }) => code.set(parseAsHTML(html))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,30 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Badge from "../components/Badge.svelte";
|
||||
import { richTextOn, richTextOff } from "./icons";
|
||||
|
||||
export let off: boolean;
|
||||
|
||||
function toggle(): void {
|
||||
off = !off;
|
||||
}
|
||||
|
||||
$: icon = off ? richTextOff : richTextOn;
|
||||
</script>
|
||||
|
||||
<span on:click|stopPropagation={toggle}>
|
||||
<Badge iconSize={80}>{@html icon}</Badge>
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,255 @@
|
|||
<!--
|
||||
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>
|
|
@ -0,0 +1,64 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import CustomStyles from "./CustomStyles.svelte";
|
||||
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
import type { StyleLinkType, StyleObject } from "./CustomStyles.svelte";
|
||||
import { getContext } from "svelte";
|
||||
import type { Readable } from "svelte/store";
|
||||
import { fontFamilyKey, fontSizeKey, directionKey } from "../lib/context-keys";
|
||||
|
||||
const [promise, customStylesResolve] = promiseWithResolver<CustomStyles>();
|
||||
const [userBaseStyle, userBaseResolve] = promiseWithResolver<StyleObject>();
|
||||
const [userBaseRule, userBaseRuleResolve] = promiseWithResolver<CSSStyleRule>();
|
||||
|
||||
const stylesDidLoad: Promise<unknown> = Promise.all([
|
||||
promise,
|
||||
userBaseStyle,
|
||||
userBaseRule,
|
||||
]);
|
||||
|
||||
userBaseStyle.then((baseStyle: StyleObject) => {
|
||||
const sheet = baseStyle.element.sheet as CSSStyleSheet;
|
||||
const baseIndex = sheet.insertRule("anki-editable {}");
|
||||
userBaseRuleResolve(sheet.cssRules[baseIndex] as CSSStyleRule);
|
||||
});
|
||||
|
||||
export let color: string;
|
||||
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
||||
const fontSize = getContext<Readable<number>>(fontSizeKey);
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
|
||||
async function setStyling(property: string, value: unknown): Promise<void> {
|
||||
const rule = await userBaseRule;
|
||||
rule.style[property] = value;
|
||||
}
|
||||
|
||||
$: setStyling("color", color);
|
||||
$: setStyling("fontFamily", $fontFamily);
|
||||
$: setStyling("fontSize", $fontSize + "px");
|
||||
$: setStyling("direction", $direction);
|
||||
|
||||
const styles = [
|
||||
{
|
||||
id: "rootStyle",
|
||||
type: "link" as "link",
|
||||
href: "./_anki/css/editable-build.css",
|
||||
} as StyleLinkType,
|
||||
];
|
||||
|
||||
function attachToShadow(element: Element) {
|
||||
const customStyles = new CustomStyles({
|
||||
target: element.shadowRoot as any,
|
||||
props: { styles },
|
||||
});
|
||||
|
||||
customStyles.addStyleTag("userBase").then(userBaseResolve);
|
||||
customStylesResolve(customStyles);
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {attachToShadow} {promise} {stylesDidLoad} />
|
|
@ -0,0 +1,12 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
export let setter: (value: any) => void;
|
||||
export let value: any;
|
||||
|
||||
setter(value);
|
||||
</script>
|
||||
|
||||
<slot />
|
|
@ -0,0 +1,47 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Badge from "../components/Badge.svelte";
|
||||
import { onMount } from "svelte";
|
||||
import { stickyOn, stickyOff } from "./icons";
|
||||
import { getEditorField } from "./EditorField.svelte";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
||||
|
||||
export let active: boolean;
|
||||
|
||||
$: icon = active ? stickyOn : stickyOff;
|
||||
|
||||
const editorField = getEditorField();
|
||||
const keyCombination = "F9";
|
||||
|
||||
export let index: number;
|
||||
|
||||
function toggleSticky() {
|
||||
bridgeCommand(`toggleSticky:${index}`, (value: boolean) => {
|
||||
active = value;
|
||||
});
|
||||
}
|
||||
|
||||
onMount(() => registerShortcut(toggleSticky, keyCombination, editorField.element));
|
||||
</script>
|
||||
|
||||
<span on:click|stopPropagation={toggleSticky}>
|
||||
<Badge
|
||||
tooltip="{tr.editingToggleSticky()} ({getPlatformString(keyCombination)})"
|
||||
widthMultiplier={0.7}>{@html icon}</Badge
|
||||
>
|
||||
</span>
|
||||
|
||||
<style lang="scss">
|
||||
span {
|
||||
opacity: 0.6;
|
||||
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,22 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from "svelte";
|
||||
import { customStylesKey } from "./CustomStyles.svelte";
|
||||
|
||||
export let id: string;
|
||||
export let href: string;
|
||||
|
||||
const { register, deregister } = getContext(customStylesKey);
|
||||
|
||||
function onLoad(event: Event): void {
|
||||
const link = event.target! as HTMLLinkElement;
|
||||
register(id, { element: link });
|
||||
}
|
||||
|
||||
onDestroy(() => deregister(id));
|
||||
</script>
|
||||
|
||||
<link {id} rel="stylesheet" {href} on:load={onLoad} />
|
|
@ -0,0 +1,24 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext, onDestroy } from "svelte";
|
||||
import { customStylesKey } from "./CustomStyles.svelte";
|
||||
|
||||
export let id: string;
|
||||
|
||||
const { register, deregister } = getContext(customStylesKey);
|
||||
|
||||
function onLoad(event: Event): void {
|
||||
const style = event.target! as HTMLStyleElement;
|
||||
register(id, { element: style });
|
||||
}
|
||||
|
||||
onDestroy(() => deregister(id));
|
||||
</script>
|
||||
|
||||
<!-- otherwise Svelte thinks it's a scoped style tag -->
|
||||
{#if true}
|
||||
<style {id} on:load={onLoad}></style>
|
||||
{/if}
|
|
@ -3,9 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { tick } from "svelte";
|
||||
import { isApplePlatform } from "../lib/platform";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import StickyFooter from "../components/StickyFooter.svelte";
|
||||
import TagOptionsBadge from "./TagOptionsBadge.svelte";
|
||||
import TagEditMode from "./TagEditMode.svelte";
|
||||
|
@ -24,24 +23,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import { postRequest } from "../lib/postrequest";
|
||||
import { execCommand } from "./helpers";
|
||||
|
||||
export let tags: TagType[] = [];
|
||||
export let size: number;
|
||||
export let wrap: boolean;
|
||||
export let tags: Writable<string[]>;
|
||||
|
||||
export let size = isApplePlatform() ? 1.6 : 2.0;
|
||||
export let wrap = true;
|
||||
|
||||
export function resetTags(names: string[]): void {
|
||||
tags = names.map(replaceWithUnicodeSeparator).map(attachId);
|
||||
let tagTypes: TagType[];
|
||||
function tagsToTagTypes(tags: string[]): void {
|
||||
tagTypes = tags.map(
|
||||
(tag: string): TagType => attachId(replaceWithUnicodeSeparator(tag))
|
||||
);
|
||||
}
|
||||
|
||||
$: tagsToTagTypes($tags);
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
const noSuggestions = Promise.resolve([]);
|
||||
let suggestionsPromise: Promise<string[]> = noSuggestions;
|
||||
|
||||
function saveTags(): void {
|
||||
bridgeCommand(
|
||||
`saveTags:${JSON.stringify(
|
||||
tags.map((tag) => tag.name).map(replaceWithColons)
|
||||
)}`
|
||||
);
|
||||
const tags = tagTypes.map((tag: TagType) => tag.name).map(replaceWithColons);
|
||||
dispatch("tagsupdate", { tags });
|
||||
|
||||
suggestionsPromise = noSuggestions;
|
||||
}
|
||||
|
@ -68,7 +69,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const withoutSingleColonAtStartOrEnd = /^:?([^:].*?[^:]):?$/;
|
||||
|
||||
function updateSuggestions(): void {
|
||||
const activeTag = tags[active!];
|
||||
const activeTag = tagTypes[active!];
|
||||
const activeName = activeTag.name;
|
||||
|
||||
autocompleteDisabled = activeName.length === 0;
|
||||
|
@ -91,7 +92,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function onAutocomplete(selected: string): void {
|
||||
const activeTag = tags[active!];
|
||||
const activeTag = tagTypes[active!];
|
||||
|
||||
activeName = selected ?? activeTag.name;
|
||||
activeInput.setSelectionRange(Infinity, Infinity);
|
||||
|
@ -99,7 +100,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
async function updateTagName(tag: TagType): Promise<void> {
|
||||
tag.name = activeName;
|
||||
tags = tags;
|
||||
tagTypes = tagTypes;
|
||||
|
||||
await tick();
|
||||
if (activeInput) {
|
||||
|
@ -115,10 +116,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
function appendEmptyTag(): void {
|
||||
// used by tag badge and tag spacer
|
||||
const lastTag = tags[tags.length - 1];
|
||||
const lastTag = tagTypes[tagTypes.length - 1];
|
||||
|
||||
if (!lastTag || lastTag.name.length > 0) {
|
||||
appendTagAndFocusAt(tags.length - 1, "");
|
||||
appendTagAndFocusAt(tagTypes.length - 1, "");
|
||||
}
|
||||
|
||||
const tagsHadFocus = active === null;
|
||||
|
@ -130,18 +131,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function appendTagAndFocusAt(index: number, name: string): void {
|
||||
tags.splice(index + 1, 0, attachId(name));
|
||||
tags = tags;
|
||||
tagTypes.splice(index + 1, 0, attachId(name));
|
||||
tagTypes = tagTypes;
|
||||
setActiveAfterBlur(index + 1);
|
||||
}
|
||||
|
||||
function isActiveNameUniqueAt(index: number): boolean {
|
||||
const names = tags.map(getName);
|
||||
const names = tagTypes.map(getName);
|
||||
names.splice(index, 1);
|
||||
|
||||
const contained = names.indexOf(activeName);
|
||||
if (contained >= 0) {
|
||||
tags[contained >= index ? contained + 1 : contained].flash();
|
||||
tagTypes[contained >= index ? contained + 1 : contained].flash();
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -182,15 +183,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
function insertTagKeepFocus(index: number): void {
|
||||
if (isActiveNameUniqueAt(index)) {
|
||||
tags.splice(index, 0, attachId(activeName));
|
||||
tagTypes.splice(index, 0, attachId(activeName));
|
||||
active!++;
|
||||
tags = tags;
|
||||
tagTypes = tagTypes;
|
||||
}
|
||||
}
|
||||
|
||||
function deleteTagAt(index: number): TagType {
|
||||
const deleted = tags.splice(index, 1)[0];
|
||||
tags = tags;
|
||||
const deleted = tagTypes.splice(index, 1)[0];
|
||||
tagTypes = tagTypes;
|
||||
|
||||
if (activeAfterBlur !== null && activeAfterBlur > index) {
|
||||
activeAfterBlur--;
|
||||
|
@ -204,7 +205,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function isLast(index: number): boolean {
|
||||
return index === tags.length - 1;
|
||||
return index === tagTypes.length - 1;
|
||||
}
|
||||
|
||||
function joinWithPreviousTag(index: number): void {
|
||||
|
@ -215,7 +216,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const deleted = deleteTagAt(index - 1);
|
||||
activeName = deleted.name + activeName;
|
||||
active!--;
|
||||
updateTagName(tags[active!]);
|
||||
updateTagName(tagTypes[active!]);
|
||||
}
|
||||
|
||||
function joinWithNextTag(index: number): void {
|
||||
|
@ -225,7 +226,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
const deleted = deleteTagAt(index + 1);
|
||||
activeName = activeName + deleted.name;
|
||||
updateTagName(tags[active!]);
|
||||
updateTagName(tagTypes[active!]);
|
||||
}
|
||||
|
||||
function moveToPreviousTag(index: number): void {
|
||||
|
@ -254,7 +255,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function deleteTagIfNotUnique(tag: TagType, index: number): void {
|
||||
if (!tags.includes(tag)) {
|
||||
if (!tagTypes.includes(tag)) {
|
||||
// already deleted
|
||||
return;
|
||||
}
|
||||
|
@ -307,8 +308,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let selectionFocus: number | null = null;
|
||||
|
||||
function select(index: number) {
|
||||
tags[index].selected = !tags[index].selected;
|
||||
tags = tags;
|
||||
tagTypes[index].selected = !tagTypes[index].selected;
|
||||
tagTypes = tagTypes;
|
||||
|
||||
selectionAnchor = index;
|
||||
}
|
||||
|
@ -325,14 +326,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const to = Math.max(selectionAnchor, selectionFocus);
|
||||
|
||||
for (let index = from; index <= to; index++) {
|
||||
tags[index].selected = true;
|
||||
tagTypes[index].selected = true;
|
||||
}
|
||||
|
||||
tags = tags;
|
||||
tagTypes = tagTypes;
|
||||
}
|
||||
|
||||
function deselect() {
|
||||
tags = tags.map((tag: TagType): TagType => ({ ...tag, selected: false }));
|
||||
tagTypes = tagTypes.map(
|
||||
(tag: TagType): TagType => ({ ...tag, selected: false })
|
||||
);
|
||||
selectionAnchor = null;
|
||||
selectionFocus = null;
|
||||
}
|
||||
|
@ -361,12 +364,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function selectAllTags() {
|
||||
tags.forEach((tag) => (tag.selected = true));
|
||||
tags = tags;
|
||||
tagTypes.forEach((tag) => (tag.selected = true));
|
||||
tagTypes = tagTypes;
|
||||
}
|
||||
|
||||
function copySelectedTags() {
|
||||
const content = tags
|
||||
const content = tagTypes
|
||||
.filter((tag) => tag.selected)
|
||||
.map((tag) => replaceWithColons(tag.name))
|
||||
.join("\n");
|
||||
|
@ -375,7 +378,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function deleteSelectedTags() {
|
||||
tags.map((tag, index) => [tag.selected, index])
|
||||
tagTypes
|
||||
.map((tag, index) => [tag.selected, index])
|
||||
.filter(([selected]) => selected)
|
||||
.reverse()
|
||||
.forEach(([, index]) => deleteTagAt(index as number));
|
||||
|
@ -394,8 +398,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<StickyFooter bind:height class="d-flex">
|
||||
{#if !wrap}
|
||||
<TagOptionsBadge
|
||||
--buttons-size="{size}rem"
|
||||
showSelectionsOptions={tags.some((tag) => tag.selected)}
|
||||
showSelectionsOptions={tagTypes.some((tag) => tag.selected)}
|
||||
bind:badgeHeight
|
||||
on:tagselectall={selectAllTags}
|
||||
on:tagcopy={copySelectedTags}
|
||||
|
@ -412,7 +415,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
{#if wrap}
|
||||
<TagOptionsBadge
|
||||
showSelectionsOptions={tags.some((tag) => tag.selected)}
|
||||
showSelectionsOptions={tagTypes.some((tag) => tag.selected)}
|
||||
bind:badgeHeight
|
||||
on:tagselectall={selectAllTags}
|
||||
on:tagcopy={copySelectedTags}
|
||||
|
@ -421,13 +424,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
/>
|
||||
{/if}
|
||||
|
||||
{#each tags as tag, index (tag.id)}
|
||||
<div
|
||||
class="position-relative tag-margins"
|
||||
class:hide-tag={index === active}
|
||||
>
|
||||
{#each tagTypes as tag, index (tag.id)}
|
||||
<div class="position-relative" class:hide-tag={index === active}>
|
||||
<TagEditMode
|
||||
class="ms-0 tag-margins-inner"
|
||||
class="ms-0"
|
||||
name={index === active ? activeName : tag.name}
|
||||
tooltip={tag.name}
|
||||
active={index === active}
|
||||
|
@ -499,7 +499,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
on:click={appendEmptyTag}
|
||||
/>
|
||||
|
||||
<div class="position-relative tag-margins hide-tag zero-width-tag">
|
||||
<div class="position-relative hide-tag zero-width-tag">
|
||||
<!-- makes sure footer does not resize when adding first tag -->
|
||||
<Tag>SPACER</Tag>
|
||||
</div>
|
||||
|
@ -522,14 +522,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
padding-right: 0 !important;
|
||||
}
|
||||
|
||||
.tag-margins {
|
||||
margin-bottom: 0.15rem;
|
||||
|
||||
:global(.tag-margins-inner) {
|
||||
margin-right: 2px;
|
||||
}
|
||||
}
|
||||
|
||||
.adjust-position {
|
||||
:global(.tag-input) {
|
||||
/* recreates positioning of Tag component */
|
||||
|
|
|
@ -3,10 +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 * as tr from "../lib/ftl";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { fieldFocusedKey, inCodableKey } from "./context-keys";
|
||||
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
|
@ -14,16 +10,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import DropdownItem from "../components/DropdownItem.svelte";
|
||||
import WithDropdown from "../components/WithDropdown.svelte";
|
||||
import WithShortcut from "../components/WithShortcut.svelte";
|
||||
import WithContext from "../components/WithContext.svelte";
|
||||
import OnlyEditable from "./OnlyEditable.svelte";
|
||||
import ClozeButton from "./ClozeButton.svelte";
|
||||
|
||||
import { getCurrentField, appendInParentheses } from "./helpers";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
import { withButton } from "../components/helpers";
|
||||
import { wrapCurrent } from "./wrap";
|
||||
import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons";
|
||||
import { paperclipIcon, micIcon, functionIcon } from "./icons";
|
||||
|
||||
export let api = {};
|
||||
const { focusInRichText, activeInput } = getNoteEditor();
|
||||
|
||||
function onAttachment(): void {
|
||||
bridgeCommand("attach");
|
||||
|
@ -33,18 +30,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
bridgeCommand("record");
|
||||
}
|
||||
|
||||
function onHtmlEdit() {
|
||||
const currentField = getCurrentField();
|
||||
if (currentField) {
|
||||
currentField.toggleHtmlEdit();
|
||||
}
|
||||
}
|
||||
$: disabled = !$focusInRichText;
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F3"} let:createShortcut let:shortcutLabel>
|
||||
<OnlyEditable let:disabled>
|
||||
<WithShortcut shortcut="F3" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingAttachPicturesaudiovideo(),
|
||||
|
@ -57,18 +48,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
{@html paperclipIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut={"F5"} let:createShortcut let:shortcutLabel>
|
||||
<OnlyEditable let:disabled>
|
||||
<WithShortcut shortcut="F5" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingRecordAudio(),
|
||||
shortcutLabel
|
||||
)}
|
||||
tooltip={appendInParentheses(tr.editingRecordAudio(), shortcutLabel)}
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onRecord}
|
||||
|
@ -76,33 +62,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
{@html micIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem id="cloze">
|
||||
<OnlyEditable>
|
||||
<ClozeButton />
|
||||
</OnlyEditable>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdown let:createDropdown>
|
||||
<OnlyEditable let:disabled>
|
||||
<IconButton {disabled} on:mount={withButton(createDropdown)}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
</OnlyEditable>
|
||||
|
||||
<DropdownMenu>
|
||||
<WithShortcut
|
||||
shortcut={"Control+M, M"}
|
||||
shortcut="Control+M, M"
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() =>
|
||||
wrapCurrent(
|
||||
$activeInput?.surround(
|
||||
"<anki-mathjax focusonmount>",
|
||||
"</anki-mathjax>"
|
||||
)}
|
||||
|
@ -114,13 +95,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut={"Control+M, E"}
|
||||
shortcut="Control+M, E"
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() =>
|
||||
wrapCurrent(
|
||||
$activeInput?.surround(
|
||||
'<anki-mathjax block="true" focusonmount>',
|
||||
"</anki-matjax>"
|
||||
)}
|
||||
|
@ -132,13 +113,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut={"Control+M, C"}
|
||||
shortcut="Control+M, C"
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() =>
|
||||
wrapCurrent(
|
||||
$activeInput?.surround(
|
||||
"<anki-mathjax focusonmount>\\ce{",
|
||||
"}</anki-mathjax>"
|
||||
)}
|
||||
|
@ -150,12 +131,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut={"Control+T, T"}
|
||||
shortcut="Control+T, T"
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrapCurrent("[latex]", "[/latex]")}
|
||||
on:click={() => $activeInput?.surround("[latex]", "[/latex]")}
|
||||
on:mount={withButton(createShortcut)}
|
||||
>
|
||||
{tr.editingLatex()}
|
||||
|
@ -164,12 +145,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut={"Control+T, E"}
|
||||
shortcut="Control+T, E"
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrapCurrent("[$]", "[/$]")}
|
||||
on:click={() => $activeInput?.surround("[$]", "[/$]")}
|
||||
on:mount={withButton(createShortcut)}
|
||||
>
|
||||
{tr.editingLatexEquation()}
|
||||
|
@ -178,12 +159,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut={"Control+T, M"}
|
||||
shortcut="Control+T, M"
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<DropdownItem
|
||||
on:click={() => wrapCurrent("[$$]", "[/$$]")}
|
||||
on:click={() => $activeInput?.surround("[$$]", "[/$$]")}
|
||||
on:mount={withButton(createShortcut)}
|
||||
>
|
||||
{tr.editingLatexMathEnv()}
|
||||
|
@ -193,30 +174,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithContext key={fieldFocusedKey} let:context={fieldFocused}>
|
||||
<WithContext key={inCodableKey} let:context={inCodable}>
|
||||
<WithShortcut
|
||||
shortcut={"Control+Shift+X"}
|
||||
let:createShortcut
|
||||
let:shortcutLabel
|
||||
>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(
|
||||
tr.editingHtmlEditor(),
|
||||
shortcutLabel
|
||||
)}
|
||||
iconSize={70}
|
||||
active={inCodable}
|
||||
disabled={!fieldFocused}
|
||||
on:click={onHtmlEdit}
|
||||
on:mount={withButton(createShortcut)}
|
||||
>
|
||||
{@html xmlIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</WithContext>
|
||||
</WithContext>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -1,83 +0,0 @@
|
|||
// 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",
|
||||
*/
|
||||
|
||||
import { CodeMirror, htmlanki, baseOptions, gutterOptions } from "./codeMirror";
|
||||
import { inCodable } from "./toolbar";
|
||||
|
||||
const codeMirrorOptions = {
|
||||
mode: htmlanki,
|
||||
...baseOptions,
|
||||
...gutterOptions,
|
||||
};
|
||||
|
||||
const parser = new DOMParser();
|
||||
const parseStyle = "<style>anki-mathjax { white-space: pre; }</style>";
|
||||
|
||||
function parseHTML(html: string): string {
|
||||
const doc = parser.parseFromString(`${parseStyle}${html}`, "text/html");
|
||||
return doc.body.innerHTML;
|
||||
}
|
||||
|
||||
export class Codable extends HTMLTextAreaElement {
|
||||
codeMirror: CodeMirror.EditorFromTextArea | undefined;
|
||||
|
||||
get active(): boolean {
|
||||
return Boolean(this.codeMirror);
|
||||
}
|
||||
|
||||
set fieldHTML(content: string) {
|
||||
if (this.active) {
|
||||
this.codeMirror?.setValue(content);
|
||||
} else {
|
||||
this.value = content;
|
||||
}
|
||||
}
|
||||
|
||||
get fieldHTML(): string {
|
||||
return parseHTML(this.active ? this.codeMirror!.getValue() : this.value);
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("hidden", "");
|
||||
}
|
||||
|
||||
setup(html: string): void {
|
||||
this.fieldHTML = html;
|
||||
this.codeMirror = CodeMirror.fromTextArea(this, codeMirrorOptions);
|
||||
this.codeMirror.on("blur", () => inCodable.set(false));
|
||||
}
|
||||
|
||||
teardown(): string {
|
||||
this.codeMirror!.toTextArea();
|
||||
this.codeMirror = undefined;
|
||||
return this.fieldHTML;
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.codeMirror!.focus();
|
||||
inCodable.set(true);
|
||||
}
|
||||
|
||||
caretToEnd(): void {
|
||||
this.codeMirror!.setCursor(this.codeMirror!.lineCount(), 0);
|
||||
}
|
||||
|
||||
surroundSelection(before: string, after: string): void {
|
||||
const selection = this.codeMirror!.getSelection();
|
||||
this.codeMirror!.replaceSelection(before + selection + after);
|
||||
}
|
||||
|
||||
onEnter(): void {
|
||||
/* default */
|
||||
}
|
||||
|
||||
onPaste(): void {
|
||||
/* default */
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("anki-codable", Codable, { extends: "textarea" });
|
|
@ -24,21 +24,18 @@ export const htmlanki = {
|
|||
},
|
||||
};
|
||||
|
||||
const noop = (): void => {
|
||||
/* noop */
|
||||
};
|
||||
|
||||
export const baseOptions = {
|
||||
export const baseOptions: CodeMirror.EditorConfiguration = {
|
||||
theme: "monokai",
|
||||
lineWrapping: true,
|
||||
matchTags: { bothTags: true },
|
||||
autoCloseTags: true,
|
||||
extraKeys: { Tab: noop, "Shift-Tab": noop },
|
||||
extraKeys: { Tab: false, "Shift-Tab": false },
|
||||
tabindex: 0,
|
||||
viewportMargin: Infinity,
|
||||
lineWiseCopyCut: false,
|
||||
};
|
||||
|
||||
export const gutterOptions = {
|
||||
export const gutterOptions: CodeMirror.EditorConfiguration = {
|
||||
gutters: ["CodeMirror-linenumbers", "CodeMirror-foldgutter"],
|
||||
lineNumbers: true,
|
||||
foldGutter: true,
|
|
@ -1,296 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/// <reference types="../lib/shadow-dom" />
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import ImageHandle from "./ImageHandle.svelte";
|
||||
import MathjaxHandle from "./MathjaxHandle.svelte";
|
||||
|
||||
import type { EditableContainer } from "../editable/editable-container";
|
||||
import type { Editable } from "../editable/editable";
|
||||
import type { Codable } from "./codable";
|
||||
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { onInput, onKey, onKeyUp } from "./input-handlers";
|
||||
import { deferFocusDown, saveFieldIfFieldChanged } from "./focus-handlers";
|
||||
import { nightModeKey } from "../components/context-keys";
|
||||
import { decoratedComponents } from "../editable/decorated";
|
||||
|
||||
function onCutOrCopy(): void {
|
||||
bridgeCommand("cutOrCopy");
|
||||
}
|
||||
|
||||
export class EditingArea extends HTMLDivElement {
|
||||
imageHandle: Promise<ImageHandle>;
|
||||
mathjaxHandle: MathjaxHandle;
|
||||
editableContainer: EditableContainer;
|
||||
editable: Editable;
|
||||
codable: Codable;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.className = "field";
|
||||
|
||||
this.editableContainer = document.createElement("div", {
|
||||
is: "anki-editable-container",
|
||||
}) as EditableContainer;
|
||||
|
||||
this.editable = document.createElement("anki-editable") as Editable;
|
||||
this.editableContainer.shadowRoot!.appendChild(this.editable);
|
||||
this.appendChild(this.editableContainer);
|
||||
|
||||
const context = new Map();
|
||||
context.set(
|
||||
nightModeKey,
|
||||
document.documentElement.classList.contains("night-mode")
|
||||
);
|
||||
|
||||
let imageHandleResolve: (value: ImageHandle) => void;
|
||||
this.imageHandle = new Promise<ImageHandle>(
|
||||
(resolve) => (imageHandleResolve = resolve)
|
||||
);
|
||||
|
||||
this.editableContainer.imagePromise.then(() =>
|
||||
imageHandleResolve(
|
||||
new ImageHandle({
|
||||
target: this,
|
||||
anchor: this.editableContainer,
|
||||
props: {
|
||||
container: this.editable,
|
||||
sheet: this.editableContainer.imageStyle.sheet,
|
||||
},
|
||||
context,
|
||||
} as any)
|
||||
)
|
||||
);
|
||||
|
||||
this.mathjaxHandle = new MathjaxHandle({
|
||||
target: this,
|
||||
anchor: this.editableContainer,
|
||||
props: {
|
||||
container: this.editable,
|
||||
},
|
||||
context,
|
||||
} as any);
|
||||
|
||||
this.codable = document.createElement("textarea", {
|
||||
is: "anki-codable",
|
||||
}) as Codable;
|
||||
this.appendChild(this.codable);
|
||||
|
||||
this.onFocus = this.onFocus.bind(this);
|
||||
this.onBlur = this.onBlur.bind(this);
|
||||
this.onKey = this.onKey.bind(this);
|
||||
this.onPaste = this.onPaste.bind(this);
|
||||
this.showHandles = this.showHandles.bind(this);
|
||||
}
|
||||
|
||||
get activeInput(): Editable | Codable {
|
||||
return this.codable.active ? this.codable : this.editable;
|
||||
}
|
||||
|
||||
get ord(): number {
|
||||
return Number(this.getAttribute("ord"));
|
||||
}
|
||||
|
||||
set fieldHTML(content: string) {
|
||||
this.imageHandle.then(() => {
|
||||
let result = content;
|
||||
|
||||
for (const component of decoratedComponents) {
|
||||
result = component.toUndecorated(result);
|
||||
}
|
||||
|
||||
this.activeInput.fieldHTML = result;
|
||||
});
|
||||
}
|
||||
|
||||
get fieldHTML(): string {
|
||||
let result = this.activeInput.fieldHTML;
|
||||
for (const component of decoratedComponents) {
|
||||
result = component.toStored(result);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.addEventListener("keydown", this.onKey);
|
||||
this.addEventListener("keyup", onKeyUp);
|
||||
this.addEventListener("input", onInput);
|
||||
this.addEventListener("focusin", this.onFocus);
|
||||
this.addEventListener("focusout", this.onBlur);
|
||||
this.addEventListener("paste", this.onPaste);
|
||||
this.addEventListener("copy", onCutOrCopy);
|
||||
this.addEventListener("oncut", onCutOrCopy);
|
||||
this.addEventListener("mouseup", updateActiveButtons);
|
||||
this.editable.addEventListener("click", this.showHandles);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("keydown", this.onKey);
|
||||
this.removeEventListener("keyup", onKeyUp);
|
||||
this.removeEventListener("input", onInput);
|
||||
this.removeEventListener("focusin", this.onFocus);
|
||||
this.removeEventListener("focusout", this.onBlur);
|
||||
this.removeEventListener("paste", this.onPaste);
|
||||
this.removeEventListener("copy", onCutOrCopy);
|
||||
this.removeEventListener("oncut", onCutOrCopy);
|
||||
this.removeEventListener("mouseup", updateActiveButtons);
|
||||
this.editable.removeEventListener("click", this.showHandles);
|
||||
}
|
||||
|
||||
initialize(color: string, content: string): void {
|
||||
this.editableContainer.stylePromise.then(() => {
|
||||
this.setBaseColor(color);
|
||||
this.fieldHTML = content;
|
||||
});
|
||||
}
|
||||
|
||||
setBaseColor(color: string): void {
|
||||
this.editableContainer.setBaseColor(color);
|
||||
}
|
||||
|
||||
quoteFontFamily(fontFamily: string): string {
|
||||
// generic families (e.g. sans-serif) must not be quoted
|
||||
if (!/^[-a-z]+$/.test(fontFamily)) {
|
||||
fontFamily = `"${fontFamily}"`;
|
||||
}
|
||||
return fontFamily;
|
||||
}
|
||||
|
||||
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
|
||||
this.editableContainer.setBaseStyling(
|
||||
this.quoteFontFamily(fontFamily),
|
||||
fontSize,
|
||||
direction
|
||||
);
|
||||
}
|
||||
|
||||
isRightToLeft(): boolean {
|
||||
return this.editableContainer.isRightToLeft();
|
||||
}
|
||||
|
||||
focus(): void {
|
||||
this.activeInput.focus();
|
||||
}
|
||||
|
||||
blur(): void {
|
||||
this.activeInput.blur();
|
||||
}
|
||||
|
||||
caretToEnd(): void {
|
||||
this.activeInput.caretToEnd();
|
||||
}
|
||||
|
||||
hasFocus(): boolean {
|
||||
return document.activeElement?.closest(".field") === this;
|
||||
}
|
||||
|
||||
getSelection(): Selection {
|
||||
const root = this.activeInput.getRootNode() as Document | ShadowRoot;
|
||||
return root.getSelection()!;
|
||||
}
|
||||
|
||||
surroundSelection(before: string, after: string): void {
|
||||
this.activeInput.surroundSelection(before, after);
|
||||
}
|
||||
|
||||
onFocus(): void {
|
||||
deferFocusDown(this);
|
||||
}
|
||||
|
||||
onBlur(event: FocusEvent): void {
|
||||
saveFieldIfFieldChanged(this, event.relatedTarget as Element);
|
||||
}
|
||||
|
||||
onEnter(event: KeyboardEvent): void {
|
||||
this.activeInput.onEnter(event);
|
||||
}
|
||||
|
||||
onKey(event: KeyboardEvent): void {
|
||||
this.resetHandles();
|
||||
onKey(event);
|
||||
}
|
||||
|
||||
onPaste(event: ClipboardEvent): void {
|
||||
this.resetHandles();
|
||||
this.activeInput.onPaste(event);
|
||||
}
|
||||
|
||||
resetHandles(): Promise<void> {
|
||||
const promise = this.imageHandle.then((imageHandle) =>
|
||||
(imageHandle as any).$set({
|
||||
activeImage: null,
|
||||
})
|
||||
);
|
||||
|
||||
(this.mathjaxHandle as any).$set({
|
||||
activeImage: null,
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
async showHandles(event: MouseEvent): Promise<void> {
|
||||
if (event.target instanceof HTMLImageElement) {
|
||||
const image = event.target as HTMLImageElement;
|
||||
await this.resetHandles();
|
||||
|
||||
if (!image.dataset.anki) {
|
||||
await this.imageHandle.then((imageHandle) =>
|
||||
(imageHandle as any).$set({
|
||||
activeImage: image,
|
||||
isRtl: this.isRightToLeft(),
|
||||
})
|
||||
);
|
||||
} else if (image.dataset.anki === "mathjax") {
|
||||
(this.mathjaxHandle as any).$set({
|
||||
activeImage: image,
|
||||
isRtl: this.isRightToLeft(),
|
||||
});
|
||||
}
|
||||
} else {
|
||||
await this.resetHandles();
|
||||
}
|
||||
}
|
||||
|
||||
toggleHtmlEdit(): void {
|
||||
const hadFocus = this.hasFocus();
|
||||
|
||||
if (this.codable.active) {
|
||||
this.fieldHTML = this.codable.teardown();
|
||||
this.editable.hidden = false;
|
||||
} else {
|
||||
this.resetHandles();
|
||||
this.editable.hidden = true;
|
||||
this.codable.setup(this.editable.fieldHTML);
|
||||
}
|
||||
|
||||
if (hadFocus) {
|
||||
this.focus();
|
||||
this.caretToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Use focus instead
|
||||
*/
|
||||
focusEditable(): void {
|
||||
focus();
|
||||
}
|
||||
/**
|
||||
* @deprecated Use blur instead
|
||||
*/
|
||||
blurEditable(): void {
|
||||
blur();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("anki-editing-area", EditingArea, { extends: "div" });
|
|
@ -0,0 +1,55 @@
|
|||
/* Copyright: Ankitects Pty Ltd and contributors
|
||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||
|
||||
@use "base";
|
||||
@use "scrollbar";
|
||||
@use "button-mixins";
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.nightMode {
|
||||
@include scrollbar.night-mode;
|
||||
}
|
||||
|
||||
#dupes,
|
||||
#cloze-hint {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
text-align: center;
|
||||
background-color: var(--window-bg);
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
.icon > svg {
|
||||
fill: var(--text-fg);
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
cursor: pointer;
|
||||
|
||||
&.is-inactive {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.icon--hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
/* CodeMirror */
|
||||
@import "codemirror/lib/codemirror";
|
||||
@import "codemirror/theme/monokai";
|
||||
@import "codemirror/addon/fold/foldgutter";
|
||||
|
||||
.CodeMirror {
|
||||
height: auto;
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding: 6px 0;
|
||||
}
|
|
@ -1,69 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { EditingArea } from "./editing-area";
|
||||
import type { LabelContainer } from "./label-container";
|
||||
|
||||
export class EditorField extends HTMLDivElement {
|
||||
labelContainer: LabelContainer;
|
||||
editingArea: EditingArea;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.className = "editorfield";
|
||||
|
||||
this.labelContainer = document.createElement("div", {
|
||||
is: "anki-label-container",
|
||||
}) as LabelContainer;
|
||||
this.appendChild(this.labelContainer);
|
||||
|
||||
this.editingArea = document.createElement("div", {
|
||||
is: "anki-editing-area",
|
||||
}) as EditingArea;
|
||||
this.appendChild(this.editingArea);
|
||||
|
||||
this.focusIfNotFocused = this.focusIfNotFocused.bind(this);
|
||||
}
|
||||
|
||||
static get observedAttributes(): string[] {
|
||||
return ["ord"];
|
||||
}
|
||||
|
||||
set ord(n: number) {
|
||||
this.setAttribute("ord", String(n));
|
||||
}
|
||||
|
||||
focusIfNotFocused(): void {
|
||||
if (!this.editingArea.hasFocus()) {
|
||||
this.editingArea.focus();
|
||||
this.editingArea.caretToEnd();
|
||||
}
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.addEventListener("mousedown", this.focusIfNotFocused);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("mousedown", this.focusIfNotFocused);
|
||||
}
|
||||
|
||||
attributeChangedCallback(name: string, _oldValue: string, newValue: string): void {
|
||||
switch (name) {
|
||||
case "ord":
|
||||
this.editingArea.setAttribute("ord", newValue);
|
||||
this.labelContainer.setAttribute("ord", newValue);
|
||||
}
|
||||
}
|
||||
|
||||
initialize(label: string, color: string, content: string): void {
|
||||
this.labelContainer.initialize(label);
|
||||
this.editingArea.initialize(color, content);
|
||||
}
|
||||
|
||||
setBaseStyling(fontFamily: string, fontSize: string, direction: string): void {
|
||||
this.editingArea.setBaseStyling(fontFamily, fontSize, direction);
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("anki-editor-field", EditorField, { extends: "div" });
|
|
@ -1,109 +0,0 @@
|
|||
/* Copyright: Ankitects Pty Ltd and contributors
|
||||
* License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html */
|
||||
|
||||
@use "sass/base";
|
||||
@use "sass/scrollbar";
|
||||
@use "sass/button-mixins";
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nightMode {
|
||||
@include scrollbar.night-mode;
|
||||
}
|
||||
|
||||
#fields {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
margin: 3px 0;
|
||||
}
|
||||
|
||||
.editorfield {
|
||||
margin: 3px;
|
||||
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border-color);
|
||||
|
||||
--border-color: var(--border);
|
||||
|
||||
&:focus-within {
|
||||
box-shadow: 0 0 0 3px var(--focus-shadow);
|
||||
|
||||
--border-color: var(--focus-border);
|
||||
}
|
||||
}
|
||||
|
||||
/* editing-area */
|
||||
.field {
|
||||
position: relative;
|
||||
|
||||
background: var(--frame-bg);
|
||||
border-radius: 0 0 5px 5px;
|
||||
|
||||
&.dupe {
|
||||
// this works around the background colour persisting in copy+paste
|
||||
// (https://github.com/ankitects/anki/pull/1278)
|
||||
background-image: linear-gradient(var(--flag1-bg), var(--flag1-bg));
|
||||
}
|
||||
}
|
||||
|
||||
.fname {
|
||||
border-width: 0 0 1px;
|
||||
border-style: dashed;
|
||||
border-color: var(--border-color);
|
||||
border-radius: 5px 5px 0 0;
|
||||
|
||||
padding: 0px 6px;
|
||||
}
|
||||
|
||||
.fieldname {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
#dupes,
|
||||
#cloze-hint {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
|
||||
text-align: center;
|
||||
background-color: var(--window-bg);
|
||||
|
||||
a {
|
||||
color: var(--link);
|
||||
}
|
||||
}
|
||||
|
||||
.icon > svg {
|
||||
fill: var(--text-fg);
|
||||
}
|
||||
|
||||
.pin-icon {
|
||||
cursor: pointer;
|
||||
|
||||
&.is-inactive {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
&.icon--hover {
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
@import "codemirror/lib/codemirror";
|
||||
@import "codemirror/theme/monokai";
|
||||
@import "codemirror/addon/fold/foldgutter";
|
||||
|
||||
.CodeMirror {
|
||||
height: auto;
|
||||
border-radius: 0 0 5px 5px;
|
||||
padding: 6px 0;
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
// 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",
|
||||
*/
|
||||
|
||||
import { fieldFocused } from "./toolbar";
|
||||
import type { EditingArea } from "./editing-area";
|
||||
|
||||
import { saveField } from "./saving";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { getCurrentField } from "./helpers";
|
||||
|
||||
export function deferFocusDown(editingArea: EditingArea): void {
|
||||
editingArea.focus();
|
||||
editingArea.caretToEnd();
|
||||
|
||||
if (editingArea.getSelection().anchorNode === null) {
|
||||
// selection is not inside editable after focusing
|
||||
editingArea.caretToEnd();
|
||||
}
|
||||
|
||||
bridgeCommand(`focus:${editingArea.ord}`);
|
||||
fieldFocused.set(true);
|
||||
}
|
||||
|
||||
export function saveFieldIfFieldChanged(
|
||||
editingArea: EditingArea,
|
||||
focusTo: Element | null
|
||||
): void {
|
||||
const fieldChanged =
|
||||
editingArea !== getCurrentField() && !editingArea.contains(focusTo);
|
||||
|
||||
saveField(editingArea, fieldChanged ? "blur" : "key");
|
||||
fieldFocused.set(false);
|
||||
|
||||
if (fieldChanged) {
|
||||
editingArea.resetHandles();
|
||||
}
|
||||
}
|
|
@ -1,12 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { EditingArea } from "./editing-area";
|
||||
|
||||
export function getCurrentField(): EditingArea | null {
|
||||
return document.activeElement?.closest(".field") ?? null;
|
||||
}
|
||||
|
||||
export function appendInParentheses(text: string, appendix: string): string {
|
||||
return `${text} (${appendix})`;
|
||||
}
|
||||
|
|
|
@ -31,7 +31,6 @@ export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg";
|
|||
export { default as micIcon } from "bootstrap-icons/icons/mic.svg";
|
||||
export { default as ellipseIcon } from "@mdi/svg/svg/contain.svg";
|
||||
export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg";
|
||||
export { default as xmlIcon } from "@mdi/svg/svg/xml.svg";
|
||||
|
||||
export { default as tagIcon } from "@mdi/svg/svg/tag.svg";
|
||||
export { default as addTagIcon } from "@mdi/svg/svg/tag-plus.svg";
|
||||
|
@ -41,13 +40,13 @@ export { default as deleteIcon } from "bootstrap-icons/icons/x.svg";
|
|||
export const arrowIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="transparent" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2 5l6 6 6-6"/></svg>';
|
||||
|
||||
// image handle
|
||||
export { default as floatNoneIcon } from "@mdi/svg/svg/format-float-none.svg";
|
||||
export { default as floatLeftIcon } from "@mdi/svg/svg/format-float-left.svg";
|
||||
export { default as floatRightIcon } from "@mdi/svg/svg/format-float-right.svg";
|
||||
export { default as alertIcon } from "@mdi/svg/svg/alert.svg";
|
||||
|
||||
export { default as sizeActual } from "@mdi/svg/svg/image-size-select-actual.svg";
|
||||
export { default as sizeMinimized } from "@mdi/svg/svg/image-size-select-large.svg";
|
||||
export { default as richTextOn } from "@mdi/svg/svg/eye-outline.svg";
|
||||
export { default as richTextOff } from "@mdi/svg/svg/eye-off-outline.svg";
|
||||
|
||||
export { default as inlineIcon } from "@mdi/svg/svg/format-wrap-square.svg";
|
||||
export { default as blockIcon } from "@mdi/svg/svg/format-wrap-top-bottom.svg";
|
||||
export { default as htmlOn } from "@mdi/svg/svg/code-tags.svg";
|
||||
export { default as htmlOff } from "@mdi/svg/svg/xml.svg";
|
||||
|
||||
export { default as stickyOn } from "@mdi/svg/svg/pin-outline.svg";
|
||||
export { default as stickyOff } from "@mdi/svg/svg/pin-off-outline.svg";
|
||||
|
|
|
@ -3,21 +3,25 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "../lib/ftl";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext } from "svelte";
|
||||
import { directionKey } from "../../lib/context-keys";
|
||||
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { floatNoneIcon, floatLeftIcon, floatRightIcon } from "./icons";
|
||||
|
||||
export let image: HTMLImageElement;
|
||||
export let isRtl: boolean;
|
||||
|
||||
const [inlineStartIcon, inlineEndIcon] = isRtl
|
||||
? [floatRightIcon, floatLeftIcon]
|
||||
: [floatLeftIcon, floatRightIcon];
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
const [inlineStartIcon, inlineEndIcon] =
|
||||
$direction === "ltr"
|
||||
? [floatLeftIcon, floatRightIcon]
|
||||
: [floatRightIcon, floatLeftIcon];
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
@ -27,7 +31,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<IconButton
|
||||
tooltip={tr.editingFloatLeft()}
|
||||
active={image.style.float === "left"}
|
||||
flipX={isRtl}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "left";
|
||||
setTimeout(() => dispatch("update"));
|
||||
|
@ -39,7 +43,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<IconButton
|
||||
tooltip={tr.editingFloatNone()}
|
||||
active={image.style.float === "" || image.style.float === "none"}
|
||||
flipX={isRtl}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.removeProperty("float");
|
||||
|
||||
|
@ -56,7 +60,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<IconButton
|
||||
tooltip={tr.editingFloatRight()}
|
||||
active={image.style.float === "right"}
|
||||
flipX={isRtl}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "right";
|
||||
setTimeout(() => dispatch("update"));
|
|
@ -3,24 +3,52 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import WithDropdown from "../components/WithDropdown.svelte";
|
||||
import ButtonDropdown from "../components/ButtonDropdown.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleSelection from "../HandleSelection.svelte";
|
||||
import HandleControl from "../HandleControl.svelte";
|
||||
import HandleLabel from "../HandleLabel.svelte";
|
||||
|
||||
import WithImageConstrained from "./WithImageConstrained.svelte";
|
||||
import HandleBackground from "./HandleBackground.svelte";
|
||||
import HandleSelection from "./HandleSelection.svelte";
|
||||
import HandleControl from "./HandleControl.svelte";
|
||||
import HandleLabel from "./HandleLabel.svelte";
|
||||
import ImageHandleFloatButtons from "./ImageHandleFloatButtons.svelte";
|
||||
import ImageHandleSizeSelect from "./ImageHandleSizeSelect.svelte";
|
||||
import FloatButtons from "./FloatButtons.svelte";
|
||||
import SizeSelect from "./SizeSelect.svelte";
|
||||
|
||||
import { onDestroy } from "svelte";
|
||||
import { tick, onDestroy } from "svelte";
|
||||
import type { StyleObject } from "../CustomStyles.svelte";
|
||||
import { getRichTextInput } from "../RichTextInput.svelte";
|
||||
|
||||
export let activeImage: HTMLImageElement | null = null;
|
||||
export let container: HTMLElement;
|
||||
export let sheet: CSSStyleSheet;
|
||||
export let isRtl: boolean = false;
|
||||
const { container, styles } = getRichTextInput();
|
||||
|
||||
const sheetPromise = styles
|
||||
.addStyleTag("imageOverlay")
|
||||
.then((styleObject: StyleObject) => styleObject.element.sheet!);
|
||||
|
||||
let activeImage: HTMLImageElement | null = null;
|
||||
|
||||
async function resetHandle(): Promise<void> {
|
||||
activeImage = null;
|
||||
await tick();
|
||||
}
|
||||
|
||||
async function maybeShowHandle(event: Event): Promise<void> {
|
||||
await resetHandle();
|
||||
|
||||
if (event.target instanceof HTMLImageElement) {
|
||||
const image = event.target;
|
||||
|
||||
if (!image.dataset.anki) {
|
||||
activeImage = image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
container.addEventListener("click", maybeShowHandle);
|
||||
container.addEventListener("blur", resetHandle);
|
||||
container.addEventListener("key", resetHandle);
|
||||
container.addEventListener("paste", resetHandle);
|
||||
|
||||
$: naturalWidth = activeImage?.naturalWidth;
|
||||
$: naturalHeight = activeImage?.naturalHeight;
|
||||
|
@ -56,7 +84,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let updateSelection: () => Promise<void>;
|
||||
|
||||
async function updateSizesWithDimensions() {
|
||||
await updateSelection();
|
||||
await updateSelection?.();
|
||||
updateDimensions();
|
||||
}
|
||||
|
||||
|
@ -130,22 +158,30 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
activeImage!.width = width;
|
||||
}
|
||||
|
||||
onDestroy(() => resizeObserver.disconnect());
|
||||
onDestroy(() => {
|
||||
resizeObserver.disconnect();
|
||||
container.removeEventListener("click", maybeShowHandle);
|
||||
container.removeEventListener("blur", resetHandle);
|
||||
container.removeEventListener("key", resetHandle);
|
||||
container.removeEventListener("paste", resetHandle);
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if sheet}
|
||||
<WithDropdown
|
||||
<WithDropdown
|
||||
drop="down"
|
||||
autoOpen={true}
|
||||
autoClose={false}
|
||||
distance={3}
|
||||
let:createDropdown
|
||||
let:dropdownObject
|
||||
>
|
||||
>
|
||||
{#await sheetPromise then sheet}
|
||||
<WithImageConstrained
|
||||
{sheet}
|
||||
{container}
|
||||
{activeImage}
|
||||
maxWidth={250}
|
||||
maxHeight={125}
|
||||
on:update={() => {
|
||||
updateSizesWithDimensions();
|
||||
dropdownObject.update();
|
||||
|
@ -162,7 +198,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
<HandleBackground on:dblclick={toggleActualSize} />
|
||||
|
||||
<HandleLabel {isRtl} on:mount={updateDimensions}>
|
||||
<HandleLabel on:mount={updateDimensions}>
|
||||
<span>{actualWidth}×{actualHeight}</span>
|
||||
{#if customDimensions}
|
||||
<span>(Original: {naturalWidth}×{naturalHeight})</span
|
||||
|
@ -187,25 +223,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}}
|
||||
/>
|
||||
</HandleSelection>
|
||||
<ButtonDropdown>
|
||||
<div on:click={updateSizesWithDimensions}>
|
||||
<ButtonDropdown on:click={updateSizesWithDimensions}>
|
||||
<Item>
|
||||
<ImageHandleFloatButtons
|
||||
<FloatButtons
|
||||
image={activeImage}
|
||||
{isRtl}
|
||||
on:update={dropdownObject.update}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
<ImageHandleSizeSelect
|
||||
{active}
|
||||
{isRtl}
|
||||
on:click={toggleActualSize}
|
||||
/>
|
||||
<SizeSelect {active} on:click={toggleActualSize} />
|
||||
</Item>
|
||||
</div>
|
||||
</ButtonDropdown>
|
||||
{/if}
|
||||
</WithImageConstrained>
|
||||
</WithDropdown>
|
||||
{/if}
|
||||
{/await}
|
||||
</WithDropdown>
|
|
@ -0,0 +1,33 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "../../lib/ftl";
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext } from "svelte";
|
||||
import { directionKey } from "../../lib/context-keys";
|
||||
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
|
||||
import { sizeActual, sizeMinimized } from "./icons";
|
||||
|
||||
export let active: boolean;
|
||||
|
||||
$: icon = active ? sizeActual : sizeMinimized;
|
||||
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
</script>
|
||||
|
||||
<ButtonGroup size={1.6}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
{active}
|
||||
flipX={$direction === "rtl"}
|
||||
tooltip={tr.editingActualSize()}
|
||||
on:click>{@html icon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="typescript">
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import { nodeIsElement } from "../lib/dom";
|
||||
import { nodeIsElement } from "../../lib/dom";
|
||||
|
||||
export let activeImage: HTMLImageElement | null;
|
||||
export let container: HTMLElement;
|
||||
|
@ -23,8 +23,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
export let maxWidth = 250;
|
||||
export let maxHeight = 125;
|
||||
export let maxWidth: number;
|
||||
export let maxHeight: number;
|
||||
|
||||
$: restrictionAspectRatio = maxWidth / maxHeight;
|
||||
|
||||
|
@ -55,7 +55,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
return createPathRecursive([], element).join(" > ");
|
||||
}
|
||||
|
||||
export const images: HTMLImageElement[] = [];
|
||||
const images: HTMLImageElement[] = [];
|
||||
|
||||
$: for (const [index, image] of images.entries()) {
|
||||
const rule = sheet.cssRules[index] as CSSStyleRule;
|
||||
|
@ -132,12 +132,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function addImageOnLoad(image: HTMLImageElement): void {
|
||||
if (image.complete && image.naturalWidth !== 0 && image.naturalHeight !== 0) {
|
||||
if (image.complete && image.naturalWidth > 0 && image.naturalHeight > 0) {
|
||||
addImage(image);
|
||||
} else {
|
||||
image.addEventListener("load", () => {
|
||||
addImage(image);
|
||||
});
|
||||
image.addEventListener("load", () => addImage(image));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -169,10 +167,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
});
|
||||
|
||||
$: if (container) {
|
||||
mutationObserver.observe(container, { childList: true, subtree: true });
|
||||
mutationObserver.observe(container, {
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
|
||||
for (const image of filterImages([...container.childNodes])) {
|
||||
addImageOnLoad(image);
|
||||
}
|
||||
|
||||
onDestroy(() => mutationObserver.disconnect());
|
||||
|
||||
export function toggleActualSize() {
|
||||
const index = images.indexOf(activeImage!);
|
||||
if (index === -1) {
|
||||
|
@ -190,8 +195,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
dispatch("update", active);
|
||||
}
|
||||
|
||||
onDestroy(() => mutationObserver.disconnect());
|
||||
</script>
|
||||
|
||||
{#if activeImage}
|
|
@ -0,0 +1,9 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as floatNoneIcon } from "@mdi/svg/svg/format-float-none.svg";
|
||||
export { default as floatLeftIcon } from "@mdi/svg/svg/format-float-left.svg";
|
||||
export { default as floatRightIcon } from "@mdi/svg/svg/format-float-right.svg";
|
||||
|
||||
export { default as sizeActual } from "@mdi/svg/svg/image-size-select-actual.svg";
|
||||
export { default as sizeMinimized } from "@mdi/svg/svg/image-size-select-large.svg";
|
|
@ -1,5 +1,4 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export const fieldFocusedKey = Symbol("fieldFocused");
|
||||
export const inCodableKey = Symbol("inCodable");
|
||||
export { default as ImageHandle } from "./ImageHandle.svelte";
|
|
@ -6,42 +6,36 @@
|
|||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
import { filterHTML } from "../html-filter";
|
||||
import { updateAllState } from "../components/WithState.svelte";
|
||||
import { noop } from "../lib/functional";
|
||||
|
||||
export const $editorToolbar = new Promise(noop);
|
||||
|
||||
export function pasteHTML(
|
||||
html: string,
|
||||
internal: boolean,
|
||||
extendedMode: boolean
|
||||
): void {
|
||||
html = filterHTML(html, internal, extendedMode);
|
||||
|
||||
if (html !== "") {
|
||||
setFormat("inserthtml", html);
|
||||
}
|
||||
}
|
||||
|
||||
export function setFormat(cmd: string, arg?: string, _nosave = false): void {
|
||||
document.execCommand(cmd, false, arg);
|
||||
updateAllState(new Event(cmd));
|
||||
}
|
||||
|
||||
import "../sveltelib/export-runtime";
|
||||
import "../lib/register-package";
|
||||
|
||||
import type EditorToolbar from "./EditorToolbar.svelte";
|
||||
import type TagEditor from "./TagEditor.svelte";
|
||||
|
||||
import { filterHTML } from "../html-filter";
|
||||
import { setupI18n, ModuleName } from "../lib/i18n";
|
||||
import { isApplePlatform } from "../lib/platform";
|
||||
import { registerShortcut } from "../lib/shortcuts";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { saveField } from "./saving";
|
||||
|
||||
import "./fields.css";
|
||||
|
||||
import "./label-container";
|
||||
import "./codable";
|
||||
import "./editor-field";
|
||||
import type { EditorField } from "./editor-field";
|
||||
import { EditingArea } from "./editing-area";
|
||||
import "../editable/editable-container";
|
||||
import "../editable/editable";
|
||||
import "../editable/mathjax-component";
|
||||
|
||||
import { initToolbar, fieldFocused } from "./toolbar";
|
||||
import { initTagEditor } from "./tag-editor";
|
||||
import { getCurrentField } from "./helpers";
|
||||
|
||||
export { setNoteId, getNoteId } from "./note-id";
|
||||
export { saveNow } from "./saving";
|
||||
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 {
|
||||
|
@ -56,148 +50,6 @@ 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("d-none", !cols.includes("dupe"));
|
||||
}
|
||||
|
||||
export function setClozeHint(hint: string): void {
|
||||
const clozeHint = document.getElementById("cloze-hint")!;
|
||||
|
||||
clozeHint.innerHTML = hint;
|
||||
clozeHint.classList.toggle("d-none", hint.length === 0);
|
||||
}
|
||||
|
||||
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 function setTags(tags: string[]): void {
|
||||
$tagEditor.then((tagEditor: TagEditor): void => tagEditor.resetTags(tags));
|
||||
}
|
||||
|
||||
export const i18n = setupI18n({
|
||||
modules: [
|
||||
ModuleName.EDITING,
|
||||
|
@ -207,5 +59,54 @@ export const i18n = setupI18n({
|
|||
],
|
||||
});
|
||||
|
||||
export const $editorToolbar: Promise<EditorToolbar> = initToolbar(i18n);
|
||||
export const $tagEditor: Promise<TagEditor> = initTagEditor(i18n);
|
||||
import OldEditorAdapter from "./OldEditorAdapter.svelte";
|
||||
import { nightModeKey } from "../components/context-keys";
|
||||
|
||||
import "./editor-base.css";
|
||||
import "./bootstrap.css";
|
||||
import "./legacy.css";
|
||||
|
||||
function setupNoteEditor(i18n: Promise<void>): Promise<OldEditorAdapter> {
|
||||
let editorResolve: (value: OldEditorAdapter) => void;
|
||||
const editorPromise = new Promise<OldEditorAdapter>((resolve) => {
|
||||
editorResolve = resolve;
|
||||
});
|
||||
|
||||
const context = new Map<symbol, unknown>();
|
||||
|
||||
context.set(
|
||||
nightModeKey,
|
||||
document.documentElement.classList.contains("night-mode")
|
||||
);
|
||||
|
||||
i18n.then(() => {
|
||||
const noteEditor = new OldEditorAdapter({
|
||||
target: document.body,
|
||||
props: {
|
||||
class: "h-100",
|
||||
},
|
||||
context,
|
||||
} as any);
|
||||
|
||||
Object.assign(globalThis, {
|
||||
setFields: noteEditor.setFields,
|
||||
setFonts: noteEditor.setFonts,
|
||||
focusField: noteEditor.focusField,
|
||||
setColorButtons: noteEditor.setColorButtons,
|
||||
setTags: noteEditor.setTags,
|
||||
setSticky: noteEditor.setSticky,
|
||||
setBackgrounds: noteEditor.setBackgrounds,
|
||||
setClozeHint: noteEditor.setClozeHint,
|
||||
saveNow: noteEditor.saveFieldNow,
|
||||
activateStickyShortcuts: noteEditor.activateStickyShortcuts,
|
||||
focusIfField: noteEditor.focusIfField,
|
||||
setNoteId: noteEditor.setNoteId,
|
||||
});
|
||||
|
||||
editorResolve(noteEditor);
|
||||
});
|
||||
|
||||
return editorPromise;
|
||||
}
|
||||
|
||||
export const noteEditorPromise = setupNoteEditor(i18n);
|
||||
|
|
|
@ -1,84 +0,0 @@
|
|||
// 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",
|
||||
*/
|
||||
|
||||
import { nodeIsElement } from "../lib/dom";
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { EditingArea } from "./editing-area";
|
||||
import { triggerChangeTimer } from "./saving";
|
||||
import { registerShortcut } from "../lib/shortcuts";
|
||||
|
||||
export function onInput(event: Event): void {
|
||||
// make sure IME changes get saved
|
||||
triggerChangeTimer(event.currentTarget as EditingArea);
|
||||
updateActiveButtons(event);
|
||||
}
|
||||
|
||||
export function onKey(evt: KeyboardEvent): void {
|
||||
const currentField = evt.currentTarget as EditingArea;
|
||||
|
||||
// esc clears focus, allowing dialog to close
|
||||
if (evt.code === "Escape") {
|
||||
return currentField.blur();
|
||||
}
|
||||
|
||||
// prefer <br> instead of <div></div>
|
||||
if (evt.code === "Enter") {
|
||||
return currentField.onEnter(evt);
|
||||
}
|
||||
|
||||
// // fix Ctrl+right/left handling in RTL fields
|
||||
if (currentField.isRightToLeft()) {
|
||||
const selection = currentField.getSelection();
|
||||
const granularity = evt.ctrlKey ? "word" : "character";
|
||||
const alter = evt.shiftKey ? "extend" : "move";
|
||||
|
||||
switch (evt.code) {
|
||||
case "ArrowRight":
|
||||
selection.modify(alter, "right", granularity);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
case "ArrowLeft":
|
||||
selection.modify(alter, "left", granularity);
|
||||
evt.preventDefault();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
triggerChangeTimer(currentField);
|
||||
}
|
||||
|
||||
function updateFocus(evt: FocusEvent) {
|
||||
const newFocusTarget = evt.target;
|
||||
if (newFocusTarget instanceof EditingArea) {
|
||||
newFocusTarget.caretToEnd();
|
||||
updateActiveButtons(evt);
|
||||
}
|
||||
}
|
||||
|
||||
registerShortcut(
|
||||
() => document.addEventListener("focusin", updateFocus, { once: true }),
|
||||
"Shift?+Tab"
|
||||
);
|
||||
|
||||
export function onKeyUp(evt: KeyboardEvent): void {
|
||||
const currentField = evt.currentTarget as EditingArea;
|
||||
|
||||
// Avoid div element on remove
|
||||
if (evt.code === "Enter" || evt.code === "Backspace") {
|
||||
const anchor = currentField.getSelection().anchorNode as Node;
|
||||
|
||||
if (
|
||||
nodeIsElement(anchor) &&
|
||||
anchor.tagName === "DIV" &&
|
||||
!(anchor instanceof EditingArea) &&
|
||||
anchor.childElementCount === 1 &&
|
||||
anchor.children[0].tagName === "BR"
|
||||
) {
|
||||
anchor.replaceWith(anchor.children[0]);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,130 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { EditorField } from "./editor-field";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { registerShortcut } from "../lib/shortcuts";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
import { saveField } from "./saving";
|
||||
import { getCurrentField, forEditorField, i18n } from ".";
|
||||
import pinIcon from "bootstrap-icons/icons/pin-angle.svg";
|
||||
|
||||
function toggleStickyCurrentField(): void {
|
||||
const currentField = getCurrentField();
|
||||
|
||||
if (currentField) {
|
||||
const labelContainer = (currentField.parentElement as EditorField)
|
||||
.labelContainer;
|
||||
labelContainer.toggleSticky();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleStickyAll(): void {
|
||||
bridgeCommand("toggleStickyAll", (values: boolean[]) =>
|
||||
forEditorField(values, (field, value) => field.labelContainer.setSticky(value))
|
||||
);
|
||||
}
|
||||
|
||||
export function activateStickyShortcuts(): void {
|
||||
registerShortcut(toggleStickyCurrentField, "F9");
|
||||
registerShortcut(toggleStickyAll, "Shift+F9");
|
||||
}
|
||||
|
||||
export class LabelContainer extends HTMLDivElement {
|
||||
label: HTMLSpanElement;
|
||||
fieldState: HTMLSpanElement;
|
||||
|
||||
sticky: HTMLSpanElement;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.className = "fname d-flex justify-content-between";
|
||||
|
||||
this.label = document.createElement("span");
|
||||
this.label.className = "fieldname";
|
||||
this.appendChild(this.label);
|
||||
|
||||
this.fieldState = document.createElement("span");
|
||||
this.fieldState.className = "field-state d-flex justify-content-between";
|
||||
this.appendChild(this.fieldState);
|
||||
|
||||
this.sticky = document.createElement("span");
|
||||
this.sticky.className = "icon pin-icon";
|
||||
this.sticky.innerHTML = pinIcon;
|
||||
this.sticky.hidden = true;
|
||||
|
||||
i18n.then(() => {
|
||||
this.sticky.title = appendInParentheses(tr.editingToggleSticky(), "F9");
|
||||
});
|
||||
|
||||
this.fieldState.appendChild(this.sticky);
|
||||
|
||||
this.setSticky = this.setSticky.bind(this);
|
||||
this.hoverIcon = this.hoverIcon.bind(this);
|
||||
this.removeHoverIcon = this.removeHoverIcon.bind(this);
|
||||
this.toggleSticky = this.toggleSticky.bind(this);
|
||||
this.toggleStickyEvent = this.toggleStickyEvent.bind(this);
|
||||
this.keepFocus = this.keepFocus.bind(this);
|
||||
this.stopPropagation = this.stopPropagation.bind(this);
|
||||
}
|
||||
|
||||
keepFocus(event: Event): void {
|
||||
event.preventDefault();
|
||||
}
|
||||
|
||||
stopPropagation(event: Event): void {
|
||||
this.keepFocus(event);
|
||||
event.stopPropagation();
|
||||
}
|
||||
|
||||
connectedCallback(): void {
|
||||
this.addEventListener("mousedown", this.keepFocus);
|
||||
this.sticky.addEventListener("mousedown", this.stopPropagation);
|
||||
this.sticky.addEventListener("click", this.toggleStickyEvent);
|
||||
this.sticky.addEventListener("mouseenter", this.hoverIcon);
|
||||
this.sticky.addEventListener("mouseleave", this.removeHoverIcon);
|
||||
}
|
||||
|
||||
disconnectedCallback(): void {
|
||||
this.removeEventListener("mousedown", this.keepFocus);
|
||||
this.sticky.removeEventListener("mousedown", this.stopPropagation);
|
||||
this.sticky.removeEventListener("click", this.toggleStickyEvent);
|
||||
this.sticky.removeEventListener("mouseenter", this.hoverIcon);
|
||||
this.sticky.removeEventListener("mouseleave", this.removeHoverIcon);
|
||||
}
|
||||
|
||||
initialize(labelName: string): void {
|
||||
this.label.innerText = labelName;
|
||||
}
|
||||
|
||||
activateSticky(initialState: boolean): void {
|
||||
this.setSticky(initialState);
|
||||
this.sticky.hidden = false;
|
||||
}
|
||||
|
||||
setSticky(state: boolean): void {
|
||||
this.sticky.classList.toggle("is-inactive", !state);
|
||||
}
|
||||
|
||||
hoverIcon(): void {
|
||||
this.sticky.classList.add("icon--hover");
|
||||
}
|
||||
|
||||
removeHoverIcon(): void {
|
||||
this.sticky.classList.remove("icon--hover");
|
||||
}
|
||||
|
||||
toggleSticky(): void {
|
||||
saveField((this.parentElement as EditorField).editingArea, "key");
|
||||
bridgeCommand(`toggleSticky:${this.getAttribute("ord")}`, this.setSticky);
|
||||
this.removeHoverIcon();
|
||||
}
|
||||
|
||||
toggleStickyEvent(event: Event): void {
|
||||
event.stopPropagation();
|
||||
this.toggleSticky();
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("anki-label-container", LabelContainer, { extends: "div" });
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
bridgeCommand<T>(command: string, callback?: (value: T) => void): void;
|
||||
}
|
||||
}
|
||||
|
||||
export function bridgeCommand<T>(command: string, callback?: (value: T) => void): void {
|
||||
window.bridgeCommand<T>(command, callback);
|
||||
}
|
|
@ -4,8 +4,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="typescript">
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
import { ChangeTimer } from "./change-timer";
|
||||
import { CodeMirror, latex, baseOptions } from "./codeMirror";
|
||||
import { ChangeTimer } from "../change-timer";
|
||||
import { CodeMirror, latex, baseOptions } from "../code-mirror";
|
||||
|
||||
export let initialValue: string;
|
||||
|
||||
|
@ -19,14 +19,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const dispatch = createEventDispatcher();
|
||||
|
||||
function onInput() {
|
||||
changeTimer.schedule(
|
||||
() => dispatch("update", { mathjax: codeMirror.getValue() }),
|
||||
400
|
||||
);
|
||||
dispatch("update", { mathjax: codeMirror.getValue() });
|
||||
|
||||
/* changeTimer.schedule( */
|
||||
/* () => dispatch("update", { mathjax: codeMirror.getValue() }), */
|
||||
/* 400 */
|
||||
/* ); */
|
||||
}
|
||||
|
||||
function onBlur() {
|
||||
changeTimer.fireImmediately();
|
||||
dispatch("codemirrorblur");
|
||||
}
|
||||
|
||||
function openCodemirror(textarea: HTMLTextAreaElement): void {
|
|
@ -3,23 +3,19 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
|
||||
import * as tr from "../lib/ftl";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { inlineIcon, blockIcon } from "./icons";
|
||||
|
||||
export let activeImage: HTMLImageElement;
|
||||
export let isRtl: boolean;
|
||||
|
||||
$: mathjaxElement = activeImage.parentElement!;
|
||||
</script>
|
||||
|
||||
<ButtonGroup size={1.6} wrap={false}>
|
||||
{#if isRtl}
|
||||
<!-- fixme -->
|
||||
{/if}
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingMathjaxInline()}
|
|
@ -0,0 +1,155 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
|
||||
import HandleSelection from "../HandleSelection.svelte";
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleControl from "../HandleControl.svelte";
|
||||
|
||||
import InlineBlock from "./InlineBlock.svelte";
|
||||
import Editor from "./Editor.svelte";
|
||||
|
||||
import { onMount, tick } from "svelte";
|
||||
import { getRichTextInput } from "../RichTextInput.svelte";
|
||||
import { noop } from "../../lib/functional";
|
||||
|
||||
const { container, api } = getRichTextInput();
|
||||
|
||||
let activeImage: HTMLImageElement | null = null;
|
||||
let allow: () => void;
|
||||
|
||||
function showHandle(image: HTMLImageElement): void {
|
||||
allow = api.preventResubscription();
|
||||
activeImage = image;
|
||||
}
|
||||
|
||||
async function maybeShowHandle(event: Event): Promise<void> {
|
||||
await resetHandle();
|
||||
const target = event.target;
|
||||
|
||||
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
||||
showHandle(target);
|
||||
}
|
||||
}
|
||||
|
||||
async function showAutofocusHandle({
|
||||
detail,
|
||||
}: CustomEvent<HTMLImageElement>): Promise<void> {
|
||||
await resetHandle();
|
||||
showHandle(detail);
|
||||
}
|
||||
|
||||
async function resetHandle(): Promise<void> {
|
||||
if (activeImage) {
|
||||
allow();
|
||||
activeImage = null;
|
||||
await tick();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
container.addEventListener("click", maybeShowHandle);
|
||||
container.addEventListener("focusmathjax" as any, showAutofocusHandle);
|
||||
container.addEventListener("key", resetHandle);
|
||||
container.addEventListener("paste", resetHandle);
|
||||
|
||||
return () => {
|
||||
container.removeEventListener("click", maybeShowHandle);
|
||||
container.removeEventListener("focusmathjax" as any, showAutofocusHandle);
|
||||
container.removeEventListener("key", resetHandle);
|
||||
container.removeEventListener("paste", resetHandle);
|
||||
};
|
||||
});
|
||||
|
||||
let dropdownApi: any;
|
||||
async function onImageResize(): Promise<void> {
|
||||
if (activeImage) {
|
||||
errorMessage = activeImage.title;
|
||||
await updateSelection();
|
||||
dropdownApi.update();
|
||||
}
|
||||
}
|
||||
|
||||
let clearResize = noop;
|
||||
function handleImageResizing(activeImage: HTMLImageElement | null) {
|
||||
if (activeImage) {
|
||||
activeImage.addEventListener("resize", onImageResize);
|
||||
|
||||
const lastImage = activeImage;
|
||||
clearResize = () => lastImage.removeEventListener("resize", onImageResize);
|
||||
} else {
|
||||
clearResize();
|
||||
}
|
||||
}
|
||||
|
||||
$: handleImageResizing(activeImage);
|
||||
|
||||
const resizeObserver = new ResizeObserver(async () => {
|
||||
if (activeImage) {
|
||||
await updateSelection();
|
||||
dropdownApi.update();
|
||||
}
|
||||
});
|
||||
|
||||
resizeObserver.observe(container);
|
||||
|
||||
let updateSelection: () => Promise<void>;
|
||||
let errorMessage: string;
|
||||
|
||||
function getComponent(image: HTMLImageElement): HTMLElement {
|
||||
return image.closest("anki-mathjax")! as HTMLElement;
|
||||
}
|
||||
|
||||
function onEditorUpdate(event: CustomEvent): void {
|
||||
/* this updates the image in Mathjax.svelte */
|
||||
getComponent(activeImage!).dataset.mathjax = event.detail.mathjax;
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithDropdown
|
||||
drop="down"
|
||||
autoOpen={true}
|
||||
autoClose={false}
|
||||
distance={4}
|
||||
let:createDropdown
|
||||
>
|
||||
{#if activeImage}
|
||||
<HandleSelection
|
||||
image={activeImage}
|
||||
{container}
|
||||
bind:updateSelection
|
||||
on:mount={(event) => (dropdownApi = createDropdown(event.detail.selection))}
|
||||
>
|
||||
<HandleBackground tooltip={errorMessage} />
|
||||
|
||||
<HandleControl offsetX={1} offsetY={1} />
|
||||
</HandleSelection>
|
||||
|
||||
<DropdownMenu>
|
||||
<Editor
|
||||
initialValue={getComponent(activeImage).dataset.mathjax ?? ""}
|
||||
on:update={onEditorUpdate}
|
||||
on:codemirrorblur={resetHandle}
|
||||
/>
|
||||
<div class="margin-x">
|
||||
<ButtonToolbar>
|
||||
<Item>
|
||||
<InlineBlock {activeImage} on:click={updateSelection} />
|
||||
</Item>
|
||||
</ButtonToolbar>
|
||||
</div>
|
||||
</DropdownMenu>
|
||||
{/if}
|
||||
</WithDropdown>
|
||||
|
||||
<style lang="scss">
|
||||
.margin-x {
|
||||
margin: 0 0.125rem;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as inlineIcon } from "@mdi/svg/svg/format-wrap-square.svg";
|
||||
export { default as blockIcon } from "@mdi/svg/svg/format-wrap-top-bottom.svg";
|
|
@ -0,0 +1,4 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as MathjaxHandle } from "./MathjaxHandle.svelte";
|
|
@ -1,12 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
let currentNoteId: number | null = null;
|
||||
|
||||
export function setNoteId(id: number): void {
|
||||
currentNoteId = id;
|
||||
}
|
||||
|
||||
export function getNoteId(): number | null {
|
||||
return currentNoteId;
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { EditingArea } from "./editing-area";
|
||||
|
||||
import { ChangeTimer } from "./change-timer";
|
||||
import { getCurrentField } from "./helpers";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { getNoteId } from "./note-id";
|
||||
|
||||
const saveFieldTimer = new ChangeTimer();
|
||||
|
||||
export function triggerChangeTimer(currentField: EditingArea): void {
|
||||
saveFieldTimer.schedule(() => saveField(currentField, "key"), 600);
|
||||
}
|
||||
|
||||
export function saveField(currentField: EditingArea, type: "blur" | "key"): void {
|
||||
saveFieldTimer.clear();
|
||||
const command = `${type}:${currentField.ord}:${getNoteId()}:${
|
||||
currentField.fieldHTML
|
||||
}`;
|
||||
bridgeCommand(command);
|
||||
}
|
||||
|
||||
export function saveNow(keepFocus: boolean): void {
|
||||
const currentField = getCurrentField();
|
||||
|
||||
if (!currentField) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (keepFocus) {
|
||||
saveFieldTimer.fireImmediately();
|
||||
} else {
|
||||
// triggers onBlur, which saves
|
||||
saveFieldTimer.clear();
|
||||
currentField.blur();
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
// 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 { nightModeKey } from "../components/context-keys";
|
||||
|
||||
import TagEditor from "./TagEditor.svelte";
|
||||
import "./bootstrap.css";
|
||||
|
||||
export function initTagEditor(i18n: Promise<void>): Promise<TagEditor> {
|
||||
let tagEditorResolve: (value: TagEditor) => void;
|
||||
const tagEditorPromise = new Promise<TagEditor>((resolve) => {
|
||||
tagEditorResolve = resolve;
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
i18n.then(() => {
|
||||
const target = document.body;
|
||||
const anchor = document.getElementById("tag-editor-anchor")!;
|
||||
|
||||
const context = new Map();
|
||||
context.set(
|
||||
nightModeKey,
|
||||
document.documentElement.classList.contains("night-mode")
|
||||
);
|
||||
|
||||
tagEditorResolve(new TagEditor({ target, anchor, context } as any));
|
||||
})
|
||||
);
|
||||
|
||||
return tagEditorPromise;
|
||||
}
|
||||
|
||||
export {} from "./TagEditor.svelte";
|
|
@ -1,49 +0,0 @@
|
|||
// 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 { nightModeKey } from "../components/context-keys";
|
||||
import { fieldFocusedKey, inCodableKey } from "./context-keys";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import EditorToolbar from "./EditorToolbar.svelte";
|
||||
import "./bootstrap.css";
|
||||
|
||||
export const fieldFocused = writable(false);
|
||||
export const inCodable = writable(false);
|
||||
|
||||
export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
|
||||
let toolbarResolve: (value: EditorToolbar) => void;
|
||||
const toolbarPromise = new Promise<EditorToolbar>((resolve) => {
|
||||
toolbarResolve = resolve;
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () =>
|
||||
i18n.then(() => {
|
||||
const target = document.body;
|
||||
const anchor = document.getElementById("fields")!;
|
||||
|
||||
const context = new Map();
|
||||
context.set(fieldFocusedKey, fieldFocused);
|
||||
context.set(inCodableKey, inCodable);
|
||||
context.set(
|
||||
nightModeKey,
|
||||
document.documentElement.classList.contains("night-mode")
|
||||
);
|
||||
|
||||
toolbarResolve(new EditorToolbar({ target, anchor, context } as any));
|
||||
})
|
||||
);
|
||||
|
||||
return toolbarPromise;
|
||||
}
|
||||
|
||||
export {
|
||||
updateActiveButtons,
|
||||
clearActiveButtons,
|
||||
editorToolbar,
|
||||
} from "./EditorToolbar.svelte";
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["*"],
|
||||
"include": ["*", "image-overlay/*", "mathjax-overlay/*"],
|
||||
"references": [
|
||||
{ "path": "../components" },
|
||||
{ "path": "../lib" },
|
||||
|
|
|
@ -1,31 +0,0 @@
|
|||
// 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",
|
||||
*/
|
||||
|
||||
import { wrapInternal } from "../lib/wrap";
|
||||
import { getCurrentField } from ".";
|
||||
|
||||
export function wrap(front: string, back: string): void {
|
||||
const editingArea = getCurrentField();
|
||||
|
||||
if (editingArea) {
|
||||
wrapInternal(editingArea.editableContainer.shadowRoot!, front, back, false);
|
||||
}
|
||||
}
|
||||
|
||||
export function wrapCurrent(front: string, back: string): void {
|
||||
const currentField = getCurrentField()!;
|
||||
currentField.surroundSelection(front, back);
|
||||
}
|
||||
|
||||
/* currently unused */
|
||||
export function wrapIntoText(front: string, back: string): void {
|
||||
const editingArea = getCurrentField();
|
||||
|
||||
if (editingArea) {
|
||||
wrapInternal(editingArea.editableContainer.shadowRoot!, front, back, false);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export const fontFamilyKey = Symbol("fontFamily");
|
||||
export const fontSizeKey = Symbol("fontSize");
|
||||
export const directionKey = Symbol("direction");
|
|
@ -50,8 +50,30 @@ export function elementIsBlock(element: Element): boolean {
|
|||
return BLOCK_TAGS.includes(element.tagName);
|
||||
}
|
||||
|
||||
export function nodeContainsInlineContent(node: Node): boolean {
|
||||
for (const child of node.childNodes) {
|
||||
if (
|
||||
(nodeIsElement(child) && elementIsBlock(child)) ||
|
||||
!nodeContainsInlineContent(child)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// empty node is trivially inline
|
||||
return true;
|
||||
}
|
||||
|
||||
export function fragmentToString(fragment: DocumentFragment): string {
|
||||
const fragmentDiv = document.createElement("div");
|
||||
fragmentDiv.appendChild(fragment);
|
||||
const html = fragmentDiv.innerHTML;
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
export function caretToEnd(node: Node): void {
|
||||
const range = document.createRange();
|
||||
const range = new Range();
|
||||
range.selectNodeContents(node);
|
||||
range.collapse(false);
|
||||
const selection = (node.getRootNode() as Document | ShadowRoot).getSelection()!;
|
||||
|
@ -61,8 +83,8 @@ export function caretToEnd(node: Node): void {
|
|||
|
||||
const getAnchorParent =
|
||||
<T extends Element>(predicate: (element: Element) => element is T) =>
|
||||
(currentField: DocumentOrShadowRoot): T | null => {
|
||||
const anchor = currentField.getSelection()?.anchorNode;
|
||||
(root: DocumentOrShadowRoot): T | null => {
|
||||
const anchor = root.getSelection()?.anchorNode;
|
||||
|
||||
if (!anchor) {
|
||||
return null;
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export const noop: () => void = () => {
|
||||
/* noop */
|
||||
};
|
|
@ -0,0 +1,13 @@
|
|||
// 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 function promiseWithResolver<T>(): [Promise<T>, (value: T) => void] {
|
||||
let resolve: (object: T) => void;
|
||||
const promise = new Promise<T>((res) => (resolve = res));
|
||||
|
||||
return [promise, resolve!];
|
||||
}
|
|
@ -4,4 +4,8 @@ declare global {
|
|||
interface DocumentOrShadowRoot {
|
||||
getSelection(): Selection | null;
|
||||
}
|
||||
|
||||
interface Node {
|
||||
getRootNode(options?: GetRootNodeOptions): DocumentOrShadowRoot;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -116,6 +116,7 @@ const GENERAL_KEY = 0;
|
|||
const NUMPAD_KEY = 3;
|
||||
|
||||
function innerShortcut(
|
||||
target: EventTarget | Document,
|
||||
lastEvent: KeyboardEvent,
|
||||
callback: (event: KeyboardEvent) => void,
|
||||
...checks: ((event: KeyboardEvent) => boolean)[]
|
||||
|
@ -128,7 +129,7 @@ function innerShortcut(
|
|||
const [nextCheck, ...restChecks] = checks;
|
||||
const handler = (event: KeyboardEvent): void => {
|
||||
if (nextCheck(event)) {
|
||||
innerShortcut(event, callback, ...restChecks);
|
||||
innerShortcut(target, event, callback, ...restChecks);
|
||||
clearTimeout(interval);
|
||||
} else if (
|
||||
event.location === GENERAL_KEY ||
|
||||
|
@ -145,19 +146,20 @@ function innerShortcut(
|
|||
|
||||
export function registerShortcut(
|
||||
callback: (event: KeyboardEvent) => void,
|
||||
keyCombinationString: string
|
||||
keyCombinationString: string,
|
||||
target: EventTarget | Document = document
|
||||
): () => void {
|
||||
const [check, ...restChecks] =
|
||||
splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
|
||||
|
||||
const handler = (event: KeyboardEvent): void => {
|
||||
if (check(event)) {
|
||||
innerShortcut(event, callback, ...restChecks);
|
||||
innerShortcut(target, event, callback, ...restChecks);
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("keydown", handler);
|
||||
return (): void => document.removeEventListener("keydown", handler);
|
||||
target.addEventListener("keydown", handler as EventListener);
|
||||
return (): void => target.removeEventListener("keydown", handler as EventListener);
|
||||
}
|
||||
|
||||
registerPackage("anki/shortcuts", {
|
||||
|
|
|
@ -19,7 +19,7 @@ function moveCursorPastPostfix(selection: Selection, postfix: string): void {
|
|||
}
|
||||
|
||||
export function wrapInternal(
|
||||
root: Document | ShadowRoot,
|
||||
root: DocumentOrShadowRoot,
|
||||
front: string,
|
||||
back: string,
|
||||
plainText: boolean
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
"path": "node_modules/@fluent/bundle",
|
||||
"licenseFile": "node_modules/@fluent/bundle/README.md"
|
||||
},
|
||||
"@mdi/svg@6.1.95": {
|
||||
"@mdi/svg@6.2.95": {
|
||||
"licenses": "Apache-2.0",
|
||||
"repository": "https://github.com/Templarian/MaterialDesign-SVG",
|
||||
"publisher": "Austin Andrews",
|
||||
|
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { setContext, getContext, hasContext } from "svelte";
|
||||
|
||||
type ContextProperty<T> = [
|
||||
(value: T) => T,
|
||||
// this typing is a lie insofar that calling get
|
||||
// outside of the component's context will return undefined
|
||||
() => T,
|
||||
() => boolean
|
||||
];
|
||||
|
||||
function contextProperty<T>(key: symbol): ContextProperty<T> {
|
||||
function set(context: T): T {
|
||||
setContext(key, context);
|
||||
return context;
|
||||
}
|
||||
|
||||
function get(): T {
|
||||
return getContext(key);
|
||||
}
|
||||
|
||||
function has(): boolean {
|
||||
return hasContext(key);
|
||||
}
|
||||
|
||||
return [set, get, has];
|
||||
}
|
||||
|
||||
export default contextProperty;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue