mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 22:12:21 -04:00
Improved add-on extension API (#1626)
* Add componentHook functionality * Register package NoteEditor * Rename OldEditorAdapter to NoteEditor * Expose instances in component-hook as well * Rename NoteTypeButtons to NotetypeButtons * Move PreviewButton initialization to BrowserEditor.svelte * Remove focusInRichText - Same thing can be done by inspecting activeInput * Satisfy formatter * Fix remaining rebase issues * Add .bazel to .prettierignore * Rename currentField and activeInput to focused{Field,Input} * Move identifier to lib and registration to sveltelib * Fix Dynamic component insertion * Simplify editingInputIsRichText * Give extra warning in svelte/svelte.ts - This was caused by doing a rename of a files, that only differed in case: NoteTypeButtons.svelte to NotetypeButtons.svelte - It was quite tough to figure out, and this console.log might make it easier if it ever happens again * Change signature of contextProperty * Add ts/typings for add-on definition files * Add Anki types in typings/common/index.d.ts * Export without .svelte suffix It conflicts with how Svelte types its packages * Fix left over .svelte import from editor.py * Rename NoteTypeButtons to unrelated to ensure case-only rename * Rename back to NotetypeButtons.svelte * Remove unused component-hook.ts, Fix typing in lifecycle-hooks * Merge runtime-require and register-package into one file + Give some preliminary types to require * Rename uiDidLoad to loaded * Fix eslint / svelte-check * Rename context imports to noteEditorContext * Fix import name mismatch - I wonder why these issues are not caught by svelte-check? * Rename two missed usages of uiDidLoad * Fix ButtonDropdown from having wrong border-radius * Uniformly rename libraries to packages - I don't have a strong opinion on whether to name them libraries or packages, I just think we should have a uniform name. - JS/TS only uses the terms "module" and "namespace", however `package` is a reserved keyword for future use, whereas `library` is not. * Refactor registration.ts into dynamic-slotting - This is part of an effort to refactor the dynamic slotting (extending buttons) functionality out of components like ButtonGroup. * Remove dynamically-slottable logic from ButtonToolbar * Use DynamicallySlottable in editor-toolbar * Fix no border radius on indentation button dropdown * Fix AddonButtons * Remove Item/ButtonGroupItem in deck-options, where it's not necessary * Remove unnecessary uses of Item and ButtonGroupItem * Fix remaining tests * Fix relative imports * Revert change return value of remapBinToSrcDir to ./bazel/out... * Remove typings directory * Adjust comments for dynamic-slottings
This commit is contained in:
parent
8afe36b8e9
commit
a981e56008
89 changed files with 2227 additions and 1957 deletions
|
@ -2,5 +2,6 @@ licenses.json
|
||||||
vendor
|
vendor
|
||||||
node_modules
|
node_modules
|
||||||
bazel-*
|
bazel-*
|
||||||
|
.bazel
|
||||||
ftl/usage
|
ftl/usage
|
||||||
.mypy_cache
|
.mypy_cache
|
|
@ -413,9 +413,6 @@ class Browser(QMainWindow):
|
||||||
|
|
||||||
def add_preview_button(editor: Editor) -> None:
|
def add_preview_button(editor: Editor) -> None:
|
||||||
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
||||||
editor.web.eval(
|
|
||||||
"noteEditorPromise.then(noteEditor => noteEditor.toolbar.notetypeButtons.appendButton({ component: editorToolbar.PreviewButton, id: 'preview' }));",
|
|
||||||
)
|
|
||||||
|
|
||||||
gui_hooks.editor_did_init.append(add_preview_button)
|
gui_hooks.editor_did_init.append(add_preview_button)
|
||||||
self.editor = aqt.editor.Editor(
|
self.editor = aqt.editor.Editor(
|
||||||
|
@ -633,9 +630,7 @@ class Browser(QMainWindow):
|
||||||
|
|
||||||
def toggle_preview_button_state(self, active: bool) -> None:
|
def toggle_preview_button_state(self, active: bool) -> None:
|
||||||
if self.editor.web:
|
if self.editor.web:
|
||||||
self.editor.web.eval(
|
self.editor.web.eval(f"togglePreviewButtonState({json.dumps(active)});")
|
||||||
f"editorToolbar.togglePreviewButtonState({json.dumps(active)});"
|
|
||||||
)
|
|
||||||
|
|
||||||
def _cleanup_preview(self) -> None:
|
def _cleanup_preview(self) -> None:
|
||||||
if self._previewer:
|
if self._previewer:
|
||||||
|
|
|
@ -174,7 +174,7 @@ class Editor:
|
||||||
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
|
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
|
||||||
righttopbtns_js = (
|
righttopbtns_js = (
|
||||||
f"""
|
f"""
|
||||||
uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
|
require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].toolbar.toolbar.append({{
|
||||||
component: editorToolbar.AddonButtons,
|
component: editorToolbar.AddonButtons,
|
||||||
id: "addons",
|
id: "addons",
|
||||||
props: {{ buttons: [ {righttopbtns_defs} ] }},
|
props: {{ buttons: [ {righttopbtns_defs} ] }},
|
||||||
|
@ -525,7 +525,9 @@ uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
|
||||||
js += " setSticky(%s);" % json.dumps(sticky)
|
js += " setSticky(%s);" % json.dumps(sticky)
|
||||||
|
|
||||||
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
||||||
self.web.evalWithCallback(f"uiPromise.then(() => {{ {js} }})", oncallback)
|
self.web.evalWithCallback(
|
||||||
|
f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback
|
||||||
|
)
|
||||||
|
|
||||||
def _save_current_note(self) -> None:
|
def _save_current_note(self) -> None:
|
||||||
"Call after note is updated with data from webview."
|
"Call after note is updated with data from webview."
|
||||||
|
@ -579,8 +581,12 @@ uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
|
||||||
elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE:
|
elif result == NoteFieldsCheckResult.FIELD_NOT_CLOZE:
|
||||||
cloze_hint = tr.adding_cloze_outside_cloze_field()
|
cloze_hint = tr.adding_cloze_outside_cloze_field()
|
||||||
|
|
||||||
self.web.eval(f"uiPromise.then(() => setBackgrounds({json.dumps(cols)}));")
|
self.web.eval(
|
||||||
self.web.eval(f"uiPromise.then(() => setClozeHint({json.dumps(cloze_hint)}));")
|
'require("anki/ui").loaded.then(() => {'
|
||||||
|
f"setBackgrounds({json.dumps(cols)});\n"
|
||||||
|
f"setClozeHint({json.dumps(cloze_hint)});\n"
|
||||||
|
"}); "
|
||||||
|
)
|
||||||
|
|
||||||
def showDupes(self) -> None:
|
def showDupes(self) -> None:
|
||||||
aqt.dialogs.open(
|
aqt.dialogs.open(
|
||||||
|
@ -1353,14 +1359,12 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
||||||
|
|
||||||
|
|
||||||
def set_cloze_button(editor: Editor) -> None:
|
def set_cloze_button(editor: Editor) -> None:
|
||||||
if editor.note.note_type()["type"] == MODEL_CLOZE:
|
action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide"
|
||||||
editor.web.eval(
|
editor.web.eval(
|
||||||
'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.showButton("cloze")); '
|
'require("anki/ui").loaded.then(() =>'
|
||||||
)
|
f'require("anki/NoteEditor").instances[0].toolbar.templateButtons.{action}("cloze")'
|
||||||
else:
|
"); "
|
||||||
editor.web.eval(
|
)
|
||||||
'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.hideButton("cloze")); '
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
gui_hooks.editor_did_load_note.append(set_cloze_button)
|
gui_hooks.editor_did_load_note.append(set_cloze_button)
|
||||||
|
|
|
@ -6,9 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { ChangeNotetypeState } from "./lib";
|
import type { ChangeNotetypeState } from "./lib";
|
||||||
import StickyContainer from "../components/StickyContainer.svelte";
|
import StickyContainer from "../components/StickyContainer.svelte";
|
||||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
|
||||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
|
||||||
import LabelButton from "../components/LabelButton.svelte";
|
import LabelButton from "../components/LabelButton.svelte";
|
||||||
import Badge from "../components/Badge.svelte";
|
import Badge from "../components/Badge.svelte";
|
||||||
import { arrowRightIcon, arrowLeftIcon } from "./icons";
|
import { arrowRightIcon, arrowLeftIcon } from "./icons";
|
||||||
|
@ -33,41 +31,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
--sticky-borders="0 0 1px"
|
--sticky-borders="0 0 1px"
|
||||||
>
|
>
|
||||||
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
||||||
<Item>
|
<LabelButton disabled={true}>
|
||||||
<ButtonGroupItem>
|
{$info.oldNotetypeName}
|
||||||
<LabelButton disabled={true}>
|
</LabelButton>
|
||||||
{$info.oldNotetypeName}
|
<Badge iconSize={70}>
|
||||||
</LabelButton>
|
{#if window.getComputedStyle(document.body).direction == "rtl"}
|
||||||
</ButtonGroupItem>
|
{@html arrowLeftIcon}
|
||||||
</Item>
|
{:else}
|
||||||
<Item>
|
{@html arrowRightIcon}
|
||||||
<Badge iconSize={70}>
|
{/if}
|
||||||
{#if window.getComputedStyle(document.body).direction == "rtl"}
|
</Badge>
|
||||||
{@html arrowLeftIcon}
|
<ButtonGroup class="flex-grow-1">
|
||||||
{:else}
|
<SelectButton class="flex-grow-1" on:change={blur}>
|
||||||
{@html arrowRightIcon}
|
{#each $notetypes as entry}
|
||||||
{/if}
|
<SelectOption value={String(entry.idx)} selected={entry.current}>
|
||||||
</Badge>
|
{entry.name}
|
||||||
</Item>
|
</SelectOption>
|
||||||
<Item>
|
{/each}
|
||||||
<ButtonGroup class="flex-grow-1">
|
</SelectButton>
|
||||||
<ButtonGroupItem>
|
</ButtonGroup>
|
||||||
<SelectButton class="flex-grow-1" on:change={blur}>
|
|
||||||
{#each $notetypes as entry}
|
|
||||||
<SelectOption
|
|
||||||
value={String(entry.idx)}
|
|
||||||
selected={entry.current}
|
|
||||||
>
|
|
||||||
{entry.name}
|
|
||||||
</SelectOption>
|
|
||||||
{/each}
|
|
||||||
</SelectButton>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Item>
|
|
||||||
|
|
||||||
<Item>
|
<SaveButton {state} />
|
||||||
<SaveButton {state} />
|
|
||||||
</Item>
|
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyContainer>
|
</StickyContainer>
|
||||||
|
|
|
@ -8,7 +8,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { getPlatformString } from "../lib/shortcuts";
|
import { getPlatformString } from "../lib/shortcuts";
|
||||||
|
|
||||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
|
||||||
import LabelButton from "../components/LabelButton.svelte";
|
import LabelButton from "../components/LabelButton.svelte";
|
||||||
import Shortcut from "../components/Shortcut.svelte";
|
import Shortcut from "../components/Shortcut.svelte";
|
||||||
|
|
||||||
|
@ -25,12 +24,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<ButtonGroupItem>
|
<LabelButton
|
||||||
<LabelButton
|
theme="primary"
|
||||||
theme="primary"
|
tooltip={getPlatformString(keyCombination)}
|
||||||
tooltip={getPlatformString(keyCombination)}
|
on:click={save}
|
||||||
on:click={save}>{tr.actionsSave()}</LabelButton
|
--border-left-radius="5px"
|
||||||
>
|
--border-right-radius="5px">{tr.actionsSave()}</LabelButton
|
||||||
<Shortcut {keyCombination} on:action={save} />
|
>
|
||||||
</ButtonGroupItem>
|
<Shortcut {keyCombination} on:action={save} />
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -42,6 +42,7 @@ svelte_check(
|
||||||
"//sass:breakpoints_lib",
|
"//sass:breakpoints_lib",
|
||||||
"//sass/bootstrap",
|
"//sass/bootstrap",
|
||||||
"@npm//@types/bootstrap",
|
"@npm//@types/bootstrap",
|
||||||
|
"//ts/lib:lib_pkg",
|
||||||
"//ts/sveltelib:sveltelib_pkg",
|
"//ts/sveltelib:sveltelib_pkg",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
|
@ -12,17 +12,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let className = "";
|
let className = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
export let api: Record<string, unknown> | undefined = undefined;
|
|
||||||
|
|
||||||
setContext(dropdownKey, null);
|
setContext(dropdownKey, null);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonToolbar
|
<ButtonToolbar {id} class="dropdown-menu btn-dropdown-menu {className}" wrap={false}>
|
||||||
{id}
|
|
||||||
class="dropdown-menu btn-dropdown-menu {className}"
|
|
||||||
wrap={false}
|
|
||||||
{api}
|
|
||||||
>
|
|
||||||
<div on:mousedown|preventDefault|stopPropagation on:click>
|
<div on:mousedown|preventDefault|stopPropagation on:click>
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -2,35 +2,12 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
|
||||||
import type { SvelteComponent } from "./registration";
|
|
||||||
import type { Identifier } from "./identifier";
|
|
||||||
|
|
||||||
export interface ButtonGroupAPI {
|
|
||||||
insertButton(button: SvelteComponent, position: Identifier): void;
|
|
||||||
appendButton(button: SvelteComponent, position: Identifier): void;
|
|
||||||
showButton(position: Identifier): void;
|
|
||||||
hideButton(position: Identifier): void;
|
|
||||||
toggleButton(position: Identifier): void;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonGroupItem from "./ButtonGroupItem.svelte";
|
|
||||||
import { setContext } from "svelte";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import { buttonGroupKey } from "./context-keys";
|
|
||||||
import { insertElement, appendElement } from "./identifier";
|
|
||||||
import type { ButtonRegistration } from "./buttons";
|
|
||||||
import { ButtonPosition } from "./buttons";
|
|
||||||
import { makeInterface } from "./registration";
|
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
let className: string = "";
|
let className: string = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
export let size: number | undefined = undefined;
|
export let size: number | undefined = undefined;
|
||||||
|
|
||||||
export let wrap: boolean | undefined = undefined;
|
export let wrap: boolean | undefined = undefined;
|
||||||
|
|
||||||
$: buttonSize = size ? `--buttons-size: ${size}rem; ` : "";
|
$: buttonSize = size ? `--buttons-size: ${size}rem; ` : "";
|
||||||
|
@ -42,86 +19,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
$: style = buttonSize + buttonWrap;
|
$: style = buttonSize + buttonWrap;
|
||||||
|
|
||||||
function makeRegistration(): ButtonRegistration {
|
|
||||||
const detach = writable(false);
|
|
||||||
const position = writable(ButtonPosition.Standalone);
|
|
||||||
return { detach, position };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { registerComponent, items, dynamicItems, getDynamicInterface } =
|
|
||||||
makeInterface(makeRegistration);
|
|
||||||
|
|
||||||
$: for (const [index, item] of $items.entries()) {
|
|
||||||
item.position.update(() => {
|
|
||||||
if ($items.length === 1) {
|
|
||||||
return ButtonPosition.Standalone;
|
|
||||||
} else if (index === 0) {
|
|
||||||
return ButtonPosition.InlineStart;
|
|
||||||
} else if (index === $items.length - 1) {
|
|
||||||
return ButtonPosition.InlineEnd;
|
|
||||||
} else {
|
|
||||||
return ButtonPosition.Center;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
setContext(buttonGroupKey, registerComponent);
|
|
||||||
|
|
||||||
export let api: Partial<ButtonGroupAPI> | undefined = undefined;
|
|
||||||
let buttonGroupRef: HTMLDivElement;
|
|
||||||
|
|
||||||
function createApi(): void {
|
|
||||||
const { addComponent, updateRegistration } =
|
|
||||||
getDynamicInterface(buttonGroupRef);
|
|
||||||
|
|
||||||
const insertButton = (button: SvelteComponent, position: Identifier = 0) =>
|
|
||||||
addComponent(button, (added, parent) =>
|
|
||||||
insertElement(added, parent, position),
|
|
||||||
);
|
|
||||||
const appendButton = (button: SvelteComponent, position: Identifier = -1) =>
|
|
||||||
addComponent(button, (added, parent) =>
|
|
||||||
appendElement(added, parent, position),
|
|
||||||
);
|
|
||||||
|
|
||||||
const showButton = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.set(false), id);
|
|
||||||
const hideButton = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.set(true), id);
|
|
||||||
const toggleButton = (id: Identifier) =>
|
|
||||||
updateRegistration(
|
|
||||||
({ detach }) => detach.update((old: boolean): boolean => !old),
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.assign(api, {
|
|
||||||
insertButton,
|
|
||||||
appendButton,
|
|
||||||
showButton,
|
|
||||||
hideButton,
|
|
||||||
toggleButton,
|
|
||||||
} as ButtonGroupAPI);
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (api && buttonGroupRef) {
|
|
||||||
createApi();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div {id} class="button-group btn-group {className}" {style} dir="ltr" role="group">
|
||||||
bind:this={buttonGroupRef}
|
|
||||||
{id}
|
|
||||||
class="button-group btn-group {className}"
|
|
||||||
{style}
|
|
||||||
dir="ltr"
|
|
||||||
role="group"
|
|
||||||
>
|
|
||||||
<slot />
|
<slot />
|
||||||
{#each $dynamicItems as item (item[0].id)}
|
|
||||||
<ButtonGroupItem id={item[0].id} registration={item[1]}>
|
|
||||||
<svelte:component this={item[0].component} {...item[0].props} />
|
|
||||||
</ButtonGroupItem>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -1,67 +1,105 @@
|
||||||
<!--
|
<!--
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
|
<script context="module" lang="ts">
|
||||||
|
import { writable, get } from "svelte/store";
|
||||||
|
import contextProperty from "../sveltelib/context-property";
|
||||||
|
import type { Writable } from "svelte/store";
|
||||||
|
import type {
|
||||||
|
SlotHostProps,
|
||||||
|
GetSlotHostProps,
|
||||||
|
} from "../sveltelib/dynamic-slotting";
|
||||||
|
|
||||||
|
enum ButtonPosition {
|
||||||
|
Standalone,
|
||||||
|
InlineStart,
|
||||||
|
Center,
|
||||||
|
InlineEnd,
|
||||||
|
}
|
||||||
|
|
||||||
|
interface ButtonSlotHostProps extends SlotHostProps {
|
||||||
|
position: Writable<ButtonPosition>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const key = Symbol("buttonGroup");
|
||||||
|
const [context, setSlotHostContext] =
|
||||||
|
contextProperty<GetSlotHostProps<ButtonSlotHostProps>>(key);
|
||||||
|
|
||||||
|
export { setSlotHostContext };
|
||||||
|
|
||||||
|
export function createProps(): ButtonSlotHostProps {
|
||||||
|
return {
|
||||||
|
detach: writable(false),
|
||||||
|
position: writable(ButtonPosition.Standalone),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function nonDetached(props: ButtonSlotHostProps): boolean {
|
||||||
|
return !get(props.detach);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updatePropsList(
|
||||||
|
propsList: ButtonSlotHostProps[],
|
||||||
|
): ButtonSlotHostProps[] {
|
||||||
|
const list = Array.from(propsList.filter(nonDetached).entries());
|
||||||
|
|
||||||
|
for (const [index, props] of list) {
|
||||||
|
const position = props.position;
|
||||||
|
|
||||||
|
if (list.length === 1) {
|
||||||
|
position.set(ButtonPosition.Standalone);
|
||||||
|
} else if (index === 0) {
|
||||||
|
position.set(ButtonPosition.InlineStart);
|
||||||
|
} else if (index === list.length - 1) {
|
||||||
|
position.set(ButtonPosition.InlineEnd);
|
||||||
|
} else {
|
||||||
|
position.set(ButtonPosition.Center);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return propsList;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Detachable from "./Detachable.svelte";
|
|
||||||
|
|
||||||
import type { ButtonRegistration } from "./buttons";
|
|
||||||
import { ButtonPosition } from "./buttons";
|
|
||||||
import type { Register } from "./registration";
|
|
||||||
|
|
||||||
import { getContext, hasContext } from "svelte";
|
|
||||||
import { buttonGroupKey } from "./context-keys";
|
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
export let registration: ButtonRegistration | undefined = undefined;
|
export let hostProps: ButtonSlotHostProps | undefined = undefined;
|
||||||
|
|
||||||
let detached: boolean;
|
|
||||||
let position_: ButtonPosition;
|
|
||||||
let style: string;
|
let style: string;
|
||||||
|
|
||||||
|
if (!context.available()) {
|
||||||
|
console.log("ButtonGroupItem: should always have a slotHostContext");
|
||||||
|
}
|
||||||
|
|
||||||
|
const { detach, position } = hostProps ?? context.get().getProps();
|
||||||
const radius = "5px";
|
const radius = "5px";
|
||||||
|
|
||||||
const leftStyle = `--border-left-radius: ${radius}; --border-right-radius: 0; `;
|
function updateButtonStyle(position: ButtonPosition) {
|
||||||
const rightStyle = `--border-left-radius: 0; --border-right-radius: ${radius}; `;
|
switch (position) {
|
||||||
|
|
||||||
$: {
|
|
||||||
switch (position_) {
|
|
||||||
case ButtonPosition.Standalone:
|
case ButtonPosition.Standalone:
|
||||||
style = `--border-left-radius: ${radius}; --border-right-radius: ${radius}; `;
|
style = `--border-left-radius: ${radius}; --border-right-radius: ${radius}; `;
|
||||||
break;
|
break;
|
||||||
case ButtonPosition.InlineStart:
|
case ButtonPosition.InlineStart:
|
||||||
style = leftStyle;
|
style = `--border-left-radius: ${radius}; --border-right-radius: 0; `;
|
||||||
break;
|
break;
|
||||||
case ButtonPosition.Center:
|
case ButtonPosition.Center:
|
||||||
style = "--border-left-radius: 0; --border-right-radius: 0; ";
|
style = "--border-left-radius: 0; --border-right-radius: 0; ";
|
||||||
break;
|
break;
|
||||||
case ButtonPosition.InlineEnd:
|
case ButtonPosition.InlineEnd:
|
||||||
style = rightStyle;
|
style = `--border-left-radius: 0; --border-right-radius: ${radius}; `;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (registration) {
|
$: updateButtonStyle($position);
|
||||||
const { detach, position } = registration;
|
|
||||||
detach.subscribe((value: boolean) => (detached = value));
|
|
||||||
position.subscribe((value: ButtonPosition) => (position_ = value));
|
|
||||||
} else if (hasContext(buttonGroupKey)) {
|
|
||||||
const registerComponent =
|
|
||||||
getContext<Register<ButtonRegistration>>(buttonGroupKey);
|
|
||||||
const { detach, position } = registerComponent();
|
|
||||||
detach.subscribe((value: boolean) => (detached = value));
|
|
||||||
position.subscribe((value: ButtonPosition) => (position_ = value));
|
|
||||||
} else {
|
|
||||||
detached = false;
|
|
||||||
position_ = ButtonPosition.Standalone;
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- div is necessary to preserve item position -->
|
<!-- div is necessary to preserve item position -->
|
||||||
<div {id} class="button-group-item" {style}>
|
<div class="button-group-item" {id} {style}>
|
||||||
<Detachable {detached}>
|
{#if !$detach}
|
||||||
<slot />
|
<slot />
|
||||||
</Detachable>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -2,27 +2,7 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
|
||||||
import type { Identifier } from "./identifier";
|
|
||||||
import type { SvelteComponent } from "./registration";
|
|
||||||
|
|
||||||
export interface ButtonToolbarAPI {
|
|
||||||
insertGroup(button: SvelteComponent, position: Identifier): void;
|
|
||||||
appendGroup(button: SvelteComponent, position: Identifier): void;
|
|
||||||
showGroup(position: Identifier): void;
|
|
||||||
hideGroup(position: Identifier): void;
|
|
||||||
toggleGroup(position: Identifier): void;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { setContext } from "svelte";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import Item from "./Item.svelte";
|
|
||||||
import type { Registration } from "./registration";
|
|
||||||
import { sectionKey } from "./context-keys";
|
|
||||||
import { insertElement, appendElement } from "./identifier";
|
|
||||||
import { makeInterface } from "./registration";
|
|
||||||
import { pageTheme } from "../sveltelib/theme";
|
import { pageTheme } from "../sveltelib/theme";
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
|
@ -41,59 +21,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
$: style = buttonSize + buttonWrap;
|
$: style = buttonSize + buttonWrap;
|
||||||
|
|
||||||
function makeRegistration(): Registration {
|
|
||||||
const detach = writable(false);
|
|
||||||
return { detach };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { registerComponent, dynamicItems, getDynamicInterface } =
|
|
||||||
makeInterface(makeRegistration);
|
|
||||||
|
|
||||||
setContext(sectionKey, registerComponent);
|
|
||||||
|
|
||||||
export let api: Partial<ButtonToolbarAPI> | undefined = undefined;
|
|
||||||
let buttonToolbarRef: HTMLDivElement;
|
|
||||||
|
|
||||||
function createApi(): void {
|
|
||||||
const { addComponent, updateRegistration } =
|
|
||||||
getDynamicInterface(buttonToolbarRef);
|
|
||||||
|
|
||||||
const insertGroup = (group: SvelteComponent, position: Identifier = 0) =>
|
|
||||||
addComponent(group, (added, parent) =>
|
|
||||||
insertElement(added, parent, position),
|
|
||||||
);
|
|
||||||
const appendGroup = (group: SvelteComponent, position: Identifier = -1) =>
|
|
||||||
addComponent(group, (added, parent) =>
|
|
||||||
appendElement(added, parent, position),
|
|
||||||
);
|
|
||||||
|
|
||||||
const showGroup = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.set(false), id);
|
|
||||||
const hideGroup = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.set(true), id);
|
|
||||||
const toggleGroup = (id: Identifier) =>
|
|
||||||
updateRegistration(
|
|
||||||
({ detach }) => detach.update((old: boolean): boolean => !old),
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.assign(api, {
|
|
||||||
insertGroup,
|
|
||||||
appendGroup,
|
|
||||||
showGroup,
|
|
||||||
hideGroup,
|
|
||||||
toggleGroup,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
$: if (buttonToolbarRef && api) {
|
|
||||||
createApi();
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
bind:this={buttonToolbarRef}
|
|
||||||
{id}
|
{id}
|
||||||
class="button-toolbar btn-toolbar {className}"
|
class="button-toolbar btn-toolbar {className}"
|
||||||
class:nightMode={$pageTheme.isDark}
|
class:nightMode={$pageTheme.isDark}
|
||||||
|
@ -102,11 +32,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
on:focusout
|
on:focusout
|
||||||
>
|
>
|
||||||
<slot />
|
<slot />
|
||||||
{#each $dynamicItems as item}
|
|
||||||
<Item id={item[0].id} registration={item[1]}>
|
|
||||||
<svelte:component this={item[0].component} {...item[0].props} />
|
|
||||||
</Item>
|
|
||||||
{/each}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -114,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
flex-wrap: var(--buttons-wrap);
|
flex-wrap: var(--buttons-wrap);
|
||||||
padding-left: 0.15rem;
|
padding-left: 0.15rem;
|
||||||
|
|
||||||
> :global(*) > :global(*) {
|
:global(.button-group) {
|
||||||
/* TODO replace with gap once available */
|
/* TODO replace with gap once available */
|
||||||
margin-right: 0.15rem;
|
margin-right: 0.15rem;
|
||||||
margin-bottom: 0.15rem;
|
margin-bottom: 0.15rem;
|
||||||
|
|
|
@ -3,7 +3,6 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Section from "./Section.svelte";
|
|
||||||
import type { Breakpoint } from "./types";
|
import type { Breakpoint } from "./types";
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
|
@ -12,7 +11,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
/* width: 100% if viewport < breakpoint otherwise with gutters */
|
/* width: 100% if viewport < breakpoint otherwise with gutters */
|
||||||
export let breakpoint: Breakpoint | "fluid" = "fluid";
|
export let breakpoint: Breakpoint | "fluid" = "fluid";
|
||||||
export let api: Record<string, never> | undefined = undefined;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -26,9 +24,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
class:container-xxl={breakpoint === "xxl"}
|
class:container-xxl={breakpoint === "xxl"}
|
||||||
class:container-fluid={breakpoint === "fluid"}
|
class:container-fluid={breakpoint === "fluid"}
|
||||||
>
|
>
|
||||||
<Section {api}>
|
<slot />
|
||||||
<slot />
|
|
||||||
</Section>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -1,11 +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">
|
|
||||||
export let detached = false;
|
|
||||||
</script>
|
|
||||||
|
|
||||||
{#if !detached}
|
|
||||||
<slot />
|
|
||||||
{/if}
|
|
64
ts/components/DynamicallySlottable.svelte
Normal file
64
ts/components/DynamicallySlottable.svelte
Normal file
|
@ -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 type Item from "./Item.svelte";
|
||||||
|
import type ButtonGroupItem from "./ButtonGroupItem.svelte";
|
||||||
|
/* import type { SlotHostProps } from "../sveltelib/dynamic-slotting"; */
|
||||||
|
|
||||||
|
import dynamicSlotting, {
|
||||||
|
defaultProps,
|
||||||
|
defaultInterface,
|
||||||
|
setSlotHostContext as defaultContext,
|
||||||
|
} from "../sveltelib/dynamic-slotting";
|
||||||
|
|
||||||
|
function id<T>(value: T): T {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This should be a Svelte component that accepts `id` and `hostProps`
|
||||||
|
* as their props, only mounts a div with display:contents, and retrieves
|
||||||
|
* its props via .getProps().
|
||||||
|
* For a minimal example, have a look at `Item.svelte`.
|
||||||
|
*/
|
||||||
|
export let slotHost: typeof Item | typeof ButtonGroupItem;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We cannot properly type these right now.
|
||||||
|
*/
|
||||||
|
export let createProps: any /* <T extends SlotHostProps>() => T */ =
|
||||||
|
defaultProps as any;
|
||||||
|
export let updatePropsList: any /* <T extends SlotHostProps>(list: T[]) => T[] */ =
|
||||||
|
id;
|
||||||
|
export let setSlotHostContext = defaultContext;
|
||||||
|
export let createInterface = defaultInterface;
|
||||||
|
|
||||||
|
const { slotsInterface, resolveSlotContainer, dynamicSlotted } = dynamicSlotting(
|
||||||
|
createProps,
|
||||||
|
updatePropsList,
|
||||||
|
setSlotHostContext,
|
||||||
|
createInterface,
|
||||||
|
);
|
||||||
|
|
||||||
|
export let api: Partial<Record<string, unknown>>;
|
||||||
|
|
||||||
|
Object.assign(api, slotsInterface);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="dynamically-slottable" use:resolveSlotContainer>
|
||||||
|
<slot />
|
||||||
|
|
||||||
|
{#each $dynamicSlotted as { component, hostProps } (component.id)}
|
||||||
|
<svelte:component this={slotHost} id={component.id} {hostProps}>
|
||||||
|
<svelte:component this={component.component} {...component.props} />
|
||||||
|
</svelte:component>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.dynamically-slottable {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -3,35 +3,24 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Detachable from "./Detachable.svelte";
|
import type { SlotHostProps } from "../sveltelib/dynamic-slotting";
|
||||||
|
import { defaultSlotHostContext } from "../sveltelib/dynamic-slotting";
|
||||||
import type { Register, Registration } from "./registration";
|
|
||||||
|
|
||||||
import { getContext, hasContext } from "svelte";
|
|
||||||
import { sectionKey } from "./context-keys";
|
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
export let registration: Registration | undefined = undefined;
|
export let hostProps: SlotHostProps | undefined = undefined;
|
||||||
|
|
||||||
let detached: boolean;
|
if (!defaultSlotHostContext.available()) {
|
||||||
|
console.log("Item: should always have a slotHostContext");
|
||||||
if (registration) {
|
|
||||||
const { detach } = registration;
|
|
||||||
detach.subscribe((value: boolean) => (detached = value));
|
|
||||||
} else if (hasContext(sectionKey)) {
|
|
||||||
const registerComponent = getContext<Register<Registration>>(sectionKey);
|
|
||||||
const { detach } = registerComponent();
|
|
||||||
detach.subscribe((value: boolean) => (detached = value));
|
|
||||||
} else {
|
|
||||||
detached = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const { detach } = hostProps ?? defaultSlotHostContext.get().getProps();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<!-- div is necessary to preserve item position -->
|
<!-- div is necessary to preserve item position -->
|
||||||
<div class="item" {id}>
|
<div class="item" {id}>
|
||||||
<Detachable {detached}>
|
{#if !$detach}
|
||||||
<slot />
|
<slot />
|
||||||
</Detachable>
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
|
|
@ -3,18 +3,13 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Item from "../components/Item.svelte";
|
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
|
||||||
let className: string = "";
|
let className: string = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Item {id}>
|
<div class="row {className}">
|
||||||
<div class="row {className}">
|
<slot />
|
||||||
<slot />
|
</div>
|
||||||
</div>
|
|
||||||
</Item>
|
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.row {
|
.row {
|
||||||
|
|
|
@ -1,69 +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 { setContext } from "svelte";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import Item from "./Item.svelte";
|
|
||||||
import { sectionKey } from "./context-keys";
|
|
||||||
import type { Identifier } from "./identifier";
|
|
||||||
import { insertElement, appendElement } from "./identifier";
|
|
||||||
import type { SvelteComponent, Registration } from "./registration";
|
|
||||||
import { makeInterface } from "./registration";
|
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
|
||||||
|
|
||||||
function makeRegistration(): Registration {
|
|
||||||
const detach = writable(false);
|
|
||||||
return { detach };
|
|
||||||
}
|
|
||||||
|
|
||||||
const { registerComponent, dynamicItems, getDynamicInterface } =
|
|
||||||
makeInterface(makeRegistration);
|
|
||||||
|
|
||||||
setContext(sectionKey, registerComponent);
|
|
||||||
|
|
||||||
export let api: Record<string, never> | undefined = undefined;
|
|
||||||
let sectionRef: HTMLDivElement;
|
|
||||||
|
|
||||||
$: if (sectionRef && api) {
|
|
||||||
const { addComponent, updateRegistration } = getDynamicInterface(sectionRef);
|
|
||||||
|
|
||||||
const insert = (group: SvelteComponent, position: Identifier = 0) =>
|
|
||||||
addComponent(group, (added, parent) =>
|
|
||||||
insertElement(added, parent, position),
|
|
||||||
);
|
|
||||||
const append = (group: SvelteComponent, position: Identifier = -1) =>
|
|
||||||
addComponent(group, (added, parent) =>
|
|
||||||
appendElement(added, parent, position),
|
|
||||||
);
|
|
||||||
|
|
||||||
const show = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.set(false), id);
|
|
||||||
const hide = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.set(true), id);
|
|
||||||
const toggle = (id: Identifier) =>
|
|
||||||
updateRegistration(
|
|
||||||
({ detach }) => detach.update((old: boolean): boolean => !old),
|
|
||||||
id,
|
|
||||||
);
|
|
||||||
|
|
||||||
Object.assign(api, { insert, append, show, hide, toggle });
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div bind:this={sectionRef} {id}>
|
|
||||||
<slot />
|
|
||||||
{#each $dynamicItems as item}
|
|
||||||
<Item id={item[0].id} registration={item[1]}>
|
|
||||||
<svelte:component this={item[0].component} {...item[0].props} />
|
|
||||||
</Item>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
div {
|
|
||||||
display: contents;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -12,11 +12,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
export let height: number = 0;
|
export let height: number = 0;
|
||||||
export let breakpoint: Breakpoint | "fluid" = "fluid";
|
export let breakpoint: Breakpoint | "fluid" = "fluid";
|
||||||
export let api: Record<string, never> | undefined = undefined;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div {id} bind:offsetHeight={height} class="sticky-container {className}">
|
<div {id} bind:offsetHeight={height} class="sticky-container {className}">
|
||||||
<Container {breakpoint} {api}>
|
<Container {breakpoint}>
|
||||||
<slot />
|
<slot />
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -85,9 +85,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
api = {
|
api = {
|
||||||
show: dropdown.show.bind(dropdown),
|
show: dropdown.show.bind(dropdown),
|
||||||
// TODO this is quite confusing, but commenting this fixes Bootstrap
|
// TODO this is quite confusing, but having a noop function fixes Bootstrap
|
||||||
// in the deck-options when not including Bootstrap via <script />
|
// in the deck-options when not including Bootstrap via <script />
|
||||||
toggle: () => {}, // toggle: dropdown.toggle.bind(dropdown),
|
toggle: () => {},
|
||||||
|
/* toggle: dropdown.toggle.bind(dropdown), */
|
||||||
hide: dropdown.hide.bind(dropdown),
|
hide: dropdown.hide.bind(dropdown),
|
||||||
update: dropdown.update.bind(dropdown),
|
update: dropdown.update.bind(dropdown),
|
||||||
dispose: dropdown.dispose.bind(dropdown),
|
dispose: dropdown.dispose.bind(dropdown),
|
||||||
|
@ -100,7 +101,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
onDestroy(() => dropdown?.dispose());
|
onDestroy(() => dropdown?.dispose());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class={dropClass}>
|
<div class="with-dropdown {dropClass}">
|
||||||
<slot {createDropdown} dropdownObject={api} />
|
<slot {createDropdown} dropdownObject={api} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,15 +0,0 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import type { Registration } from "./registration";
|
|
||||||
|
|
||||||
export enum ButtonPosition {
|
|
||||||
Standalone,
|
|
||||||
InlineStart,
|
|
||||||
Center,
|
|
||||||
InlineEnd,
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ButtonRegistration extends Registration {
|
|
||||||
position: Writable<ButtonPosition>;
|
|
||||||
}
|
|
|
@ -1,87 +0,0 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
export type Identifier = string | number;
|
|
||||||
|
|
||||||
export function findElement(
|
|
||||||
collection: HTMLCollection,
|
|
||||||
idOrIndex: Identifier,
|
|
||||||
): [number, Element] | null {
|
|
||||||
let result: [number, Element] | null = null;
|
|
||||||
|
|
||||||
if (typeof idOrIndex === "string") {
|
|
||||||
const element = collection.namedItem(idOrIndex);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
const index = Array.prototype.indexOf.call(collection, element);
|
|
||||||
result = [index, element];
|
|
||||||
}
|
|
||||||
} else if (idOrIndex < 0) {
|
|
||||||
const index = collection.length + idOrIndex;
|
|
||||||
const element = collection.item(index);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
result = [index, element];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const index = idOrIndex;
|
|
||||||
const element = collection.item(index);
|
|
||||||
|
|
||||||
if (element) {
|
|
||||||
result = [index, element];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function insertElement(
|
|
||||||
element: Element,
|
|
||||||
collection: Element,
|
|
||||||
idOrIndex: Identifier,
|
|
||||||
): number {
|
|
||||||
const match = findElement(collection.children, idOrIndex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [index, reference] = match;
|
|
||||||
collection.insertBefore(element, reference[0]);
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function appendElement(
|
|
||||||
element: Element,
|
|
||||||
collection: Element,
|
|
||||||
idOrIndex: Identifier,
|
|
||||||
): number {
|
|
||||||
const match = findElement(collection.children, idOrIndex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [index, before] = match;
|
|
||||||
const reference = before.nextElementSibling ?? null;
|
|
||||||
collection.insertBefore(element, reference);
|
|
||||||
|
|
||||||
return index + 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateElement(
|
|
||||||
f: (element: Element) => void,
|
|
||||||
collection: Element,
|
|
||||||
idOrIndex: Identifier,
|
|
||||||
): number {
|
|
||||||
const match = findElement(collection.children, idOrIndex);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [index, element] = match;
|
|
||||||
f(element[0]);
|
|
||||||
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
return -1;
|
|
||||||
}
|
|
|
@ -1,126 +0,0 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
import type { SvelteComponentTyped } from "svelte/internal";
|
|
||||||
import type { Writable, Readable } from "svelte/store";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
import type { Identifier } from "./identifier";
|
|
||||||
import { findElement } from "./identifier";
|
|
||||||
|
|
||||||
export interface SvelteComponent {
|
|
||||||
component: SvelteComponentTyped;
|
|
||||||
id: string;
|
|
||||||
props: Record<string, unknown> | undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Registration {
|
|
||||||
detach: Writable<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Register<T extends Registration> = (index?: number, registration?: T) => T;
|
|
||||||
|
|
||||||
export interface RegistrationAPI<T extends Registration> {
|
|
||||||
registerComponent: Register<T>;
|
|
||||||
items: Readable<T[]>;
|
|
||||||
dynamicItems: Readable<[SvelteComponent, T][]>;
|
|
||||||
getDynamicInterface: (elementRef: HTMLElement) => DynamicRegistrationAPI<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DynamicRegistrationAPI<T> {
|
|
||||||
addComponent: (
|
|
||||||
component: SvelteComponent,
|
|
||||||
add: (added: Element, parent: Element) => number,
|
|
||||||
) => void;
|
|
||||||
updateRegistration: (
|
|
||||||
update: (registration: T) => void,
|
|
||||||
position: Identifier,
|
|
||||||
) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function nodeIsElement(node: Node): node is Element {
|
|
||||||
return node.nodeType === Node.ELEMENT_NODE;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function makeInterface<T extends Registration>(
|
|
||||||
makeRegistration: () => T,
|
|
||||||
): RegistrationAPI<T> {
|
|
||||||
const registrations: T[] = [];
|
|
||||||
const items = writable(registrations);
|
|
||||||
|
|
||||||
function registerComponent(
|
|
||||||
index: number = registrations.length,
|
|
||||||
registration = makeRegistration(),
|
|
||||||
): T {
|
|
||||||
items.update((registrations) => {
|
|
||||||
registrations.splice(index, 0, registration);
|
|
||||||
return registrations;
|
|
||||||
});
|
|
||||||
|
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
const dynamicRegistrations: [SvelteComponent, T][] = [];
|
|
||||||
const dynamicItems = writable(dynamicRegistrations);
|
|
||||||
|
|
||||||
function getDynamicInterface(elementRef: HTMLElement): DynamicRegistrationAPI<T> {
|
|
||||||
function addComponent(
|
|
||||||
component: SvelteComponent,
|
|
||||||
add: (added: Element, parent: Element) => number,
|
|
||||||
): void {
|
|
||||||
const registration = makeRegistration();
|
|
||||||
|
|
||||||
const callback = (
|
|
||||||
mutations: MutationRecord[],
|
|
||||||
observer: MutationObserver,
|
|
||||||
): void => {
|
|
||||||
for (const mutation of mutations) {
|
|
||||||
for (const addedNode of mutation.addedNodes) {
|
|
||||||
if (
|
|
||||||
nodeIsElement(addedNode) &&
|
|
||||||
(!component.id || addedNode.id === component.id)
|
|
||||||
) {
|
|
||||||
const index = add(addedNode, elementRef);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
registerComponent(index, registration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return observer.disconnect();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new MutationObserver(callback);
|
|
||||||
observer.observe(elementRef, { childList: true });
|
|
||||||
|
|
||||||
dynamicRegistrations.push([component, registration]);
|
|
||||||
dynamicItems.set(dynamicRegistrations);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateRegistration(
|
|
||||||
update: (registration: T) => void,
|
|
||||||
position: Identifier,
|
|
||||||
): void {
|
|
||||||
const match = findElement(elementRef.children, position);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [index] = match;
|
|
||||||
const registration = registrations[index];
|
|
||||||
update(registration);
|
|
||||||
items.set(registrations);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
addComponent,
|
|
||||||
updateRegistration,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
registerComponent,
|
|
||||||
items,
|
|
||||||
dynamicItems,
|
|
||||||
getDynamicInterface,
|
|
||||||
};
|
|
||||||
}
|
|
|
@ -7,14 +7,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let api: Record<string, never>;
|
|
||||||
|
|
||||||
let components = state.addonComponents;
|
let components = state.addonComponents;
|
||||||
const auxData = state.currentAuxData;
|
const auxData = state.currentAuxData;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if $components.length || state.haveAddons}
|
{#if $components.length || state.haveAddons}
|
||||||
<TitledContainer title="Add-ons" {api}>
|
<TitledContainer title="Add-ons">
|
||||||
<p>
|
<p>
|
||||||
If you're using an add-on that hasn't been updated to use this new screen
|
If you're using an add-on that hasn't been updated to use this new screen
|
||||||
yet, you can access the old deck options screen by holding down the shift
|
yet, you can access the old deck options screen by holding down the shift
|
||||||
|
|
|
@ -9,6 +9,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import CardStateCustomizer from "./CardStateCustomizer.svelte";
|
import CardStateCustomizer from "./CardStateCustomizer.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
|
import Item from "../components/Item.svelte";
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let api: Record<string, never>;
|
export let api: Record<string, never>;
|
||||||
|
@ -18,67 +20,83 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let cardStateCustomizer = state.cardStateCustomizer;
|
let cardStateCustomizer = state.cardStateCustomizer;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigAdvancedTitle()} {api}>
|
<TitledContainer title={tr.deckConfigAdvancedTitle()}>
|
||||||
<SpinBoxRow
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
bind:value={$config.maximumReviewInterval}
|
<Item>
|
||||||
defaultValue={defaults.maximumReviewInterval}
|
<SpinBoxRow
|
||||||
min={1}
|
bind:value={$config.maximumReviewInterval}
|
||||||
max={365 * 100}
|
defaultValue={defaults.maximumReviewInterval}
|
||||||
markdownTooltip={tr.deckConfigMaximumIntervalTooltip()}
|
min={1}
|
||||||
>
|
max={365 * 100}
|
||||||
{tr.schedulingMaximumInterval()}
|
markdownTooltip={tr.deckConfigMaximumIntervalTooltip()}
|
||||||
</SpinBoxRow>
|
>
|
||||||
|
{tr.schedulingMaximumInterval()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxFloatRow
|
<Item>
|
||||||
bind:value={$config.initialEase}
|
<SpinBoxFloatRow
|
||||||
defaultValue={defaults.initialEase}
|
bind:value={$config.initialEase}
|
||||||
min={1.31}
|
defaultValue={defaults.initialEase}
|
||||||
max={5}
|
min={1.31}
|
||||||
markdownTooltip={tr.deckConfigStartingEaseTooltip()}
|
max={5}
|
||||||
>
|
markdownTooltip={tr.deckConfigStartingEaseTooltip()}
|
||||||
{tr.schedulingStartingEase()}
|
>
|
||||||
</SpinBoxFloatRow>
|
{tr.schedulingStartingEase()}
|
||||||
|
</SpinBoxFloatRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxFloatRow
|
<Item>
|
||||||
bind:value={$config.easyMultiplier}
|
<SpinBoxFloatRow
|
||||||
defaultValue={defaults.easyMultiplier}
|
bind:value={$config.easyMultiplier}
|
||||||
min={1}
|
defaultValue={defaults.easyMultiplier}
|
||||||
max={3}
|
min={1}
|
||||||
markdownTooltip={tr.deckConfigEasyBonusTooltip()}
|
max={3}
|
||||||
>
|
markdownTooltip={tr.deckConfigEasyBonusTooltip()}
|
||||||
{tr.schedulingEasyBonus()}
|
>
|
||||||
</SpinBoxFloatRow>
|
{tr.schedulingEasyBonus()}
|
||||||
|
</SpinBoxFloatRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxFloatRow
|
<Item>
|
||||||
bind:value={$config.intervalMultiplier}
|
<SpinBoxFloatRow
|
||||||
defaultValue={defaults.intervalMultiplier}
|
bind:value={$config.intervalMultiplier}
|
||||||
min={0.5}
|
defaultValue={defaults.intervalMultiplier}
|
||||||
max={2}
|
min={0.5}
|
||||||
markdownTooltip={tr.deckConfigIntervalModifierTooltip()}
|
max={2}
|
||||||
>
|
markdownTooltip={tr.deckConfigIntervalModifierTooltip()}
|
||||||
{tr.schedulingIntervalModifier()}
|
>
|
||||||
</SpinBoxFloatRow>
|
{tr.schedulingIntervalModifier()}
|
||||||
|
</SpinBoxFloatRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxFloatRow
|
<Item>
|
||||||
bind:value={$config.hardMultiplier}
|
<SpinBoxFloatRow
|
||||||
defaultValue={defaults.hardMultiplier}
|
bind:value={$config.hardMultiplier}
|
||||||
min={0.5}
|
defaultValue={defaults.hardMultiplier}
|
||||||
max={1.3}
|
min={0.5}
|
||||||
markdownTooltip={tr.deckConfigHardIntervalTooltip()}
|
max={1.3}
|
||||||
>
|
markdownTooltip={tr.deckConfigHardIntervalTooltip()}
|
||||||
{tr.schedulingHardInterval()}
|
>
|
||||||
</SpinBoxFloatRow>
|
{tr.schedulingHardInterval()}
|
||||||
|
</SpinBoxFloatRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxFloatRow
|
<Item>
|
||||||
bind:value={$config.lapseMultiplier}
|
<SpinBoxFloatRow
|
||||||
defaultValue={defaults.lapseMultiplier}
|
bind:value={$config.lapseMultiplier}
|
||||||
max={1}
|
defaultValue={defaults.lapseMultiplier}
|
||||||
markdownTooltip={tr.deckConfigNewIntervalTooltip()}
|
max={1}
|
||||||
>
|
markdownTooltip={tr.deckConfigNewIntervalTooltip()}
|
||||||
{tr.schedulingNewInterval()}
|
>
|
||||||
</SpinBoxFloatRow>
|
{tr.schedulingNewInterval()}
|
||||||
|
</SpinBoxFloatRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
{#if state.v3Scheduler}
|
{#if state.v3Scheduler}
|
||||||
<CardStateCustomizer bind:value={$cardStateCustomizer} />
|
<Item>
|
||||||
{/if}
|
<CardStateCustomizer bind:value={$cardStateCustomizer} />
|
||||||
|
</Item>
|
||||||
|
{/if}
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -7,6 +7,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import TitledContainer from "./TitledContainer.svelte";
|
import TitledContainer from "./TitledContainer.svelte";
|
||||||
import SwitchRow from "./SwitchRow.svelte";
|
import SwitchRow from "./SwitchRow.svelte";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
|
import Item from "../components/Item.svelte";
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let api: Record<string, never>;
|
export let api: Record<string, never>;
|
||||||
|
@ -15,19 +17,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let defaults = state.defaults;
|
let defaults = state.defaults;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigAudioTitle()} {api}>
|
<TitledContainer title={tr.deckConfigAudioTitle()}>
|
||||||
<SwitchRow
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
bind:value={$config.disableAutoplay}
|
<Item>
|
||||||
defaultValue={defaults.disableAutoplay}
|
<SwitchRow
|
||||||
>
|
bind:value={$config.disableAutoplay}
|
||||||
{tr.deckConfigDisableAutoplay()}
|
defaultValue={defaults.disableAutoplay}
|
||||||
</SwitchRow>
|
>
|
||||||
|
{tr.deckConfigDisableAutoplay()}
|
||||||
|
</SwitchRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SwitchRow
|
<Item>
|
||||||
bind:value={$config.skipQuestionWhenReplayingAnswer}
|
<SwitchRow
|
||||||
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
|
bind:value={$config.skipQuestionWhenReplayingAnswer}
|
||||||
markdownTooltip={tr.deckConfigAlwaysIncludeQuestionAudioTooltip()}
|
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
|
||||||
>
|
markdownTooltip={tr.deckConfigAlwaysIncludeQuestionAudioTooltip()}
|
||||||
{tr.deckConfigSkipQuestionWhenReplaying()}
|
>
|
||||||
</SwitchRow>
|
{tr.deckConfigSkipQuestionWhenReplaying()}
|
||||||
|
</SwitchRow>
|
||||||
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -17,6 +17,7 @@ compile_sass(
|
||||||
"//sass:base_lib",
|
"//sass:base_lib",
|
||||||
"//sass:breakpoints_lib",
|
"//sass:breakpoints_lib",
|
||||||
"//sass:scrollbar_lib",
|
"//sass:scrollbar_lib",
|
||||||
|
"//sass:night_mode_lib",
|
||||||
"//sass/bootstrap",
|
"//sass/bootstrap",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -37,6 +38,10 @@ _ts_deps = [
|
||||||
|
|
||||||
compile_svelte(
|
compile_svelte(
|
||||||
deps = _ts_deps + [
|
deps = _ts_deps + [
|
||||||
|
"//sass:base_lib",
|
||||||
|
"//sass:breakpoints_lib",
|
||||||
|
"//sass:scrollbar_lib",
|
||||||
|
"//sass:night_mode_lib",
|
||||||
"//sass/bootstrap",
|
"//sass/bootstrap",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
@ -77,9 +82,11 @@ svelte_check(
|
||||||
"*.ts",
|
"*.ts",
|
||||||
"*.svelte",
|
"*.svelte",
|
||||||
]) + [
|
]) + [
|
||||||
|
"//sass:base_lib",
|
||||||
"//sass:button_mixins_lib",
|
"//sass:button_mixins_lib",
|
||||||
"//sass:night_mode_lib",
|
"//sass:scrollbar_lib",
|
||||||
"//sass:breakpoints_lib",
|
"//sass:breakpoints_lib",
|
||||||
|
"//sass:night_mode_lib",
|
||||||
"//sass/bootstrap",
|
"//sass/bootstrap",
|
||||||
"//ts/components",
|
"//ts/components",
|
||||||
"//ts/sveltelib:sveltelib_pkg",
|
"//ts/sveltelib:sveltelib_pkg",
|
||||||
|
|
|
@ -7,6 +7,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import SwitchRow from "./SwitchRow.svelte";
|
import SwitchRow from "./SwitchRow.svelte";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
|
import Item from "../components/Item.svelte";
|
||||||
|
|
||||||
export let state: DeckOptionsState;
|
export let state: DeckOptionsState;
|
||||||
export let api: Record<string, never>;
|
export let api: Record<string, never>;
|
||||||
|
@ -15,20 +17,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let defaults = state.defaults;
|
let defaults = state.defaults;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigBuryTitle()} {api}>
|
<TitledContainer title={tr.deckConfigBuryTitle()}>
|
||||||
<SwitchRow
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
bind:value={$config.buryNew}
|
<Item>
|
||||||
defaultValue={defaults.buryNew}
|
<SwitchRow
|
||||||
markdownTooltip={tr.deckConfigBuryTooltip()}
|
bind:value={$config.buryNew}
|
||||||
>
|
defaultValue={defaults.buryNew}
|
||||||
{tr.deckConfigBuryNewSiblings()}
|
markdownTooltip={tr.deckConfigBuryTooltip()}
|
||||||
</SwitchRow>
|
>
|
||||||
|
{tr.deckConfigBuryNewSiblings()}
|
||||||
|
</SwitchRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SwitchRow
|
<Item>
|
||||||
bind:value={$config.buryReviews}
|
<SwitchRow
|
||||||
defaultValue={defaults.buryReviews}
|
bind:value={$config.buryReviews}
|
||||||
markdownTooltip={tr.deckConfigBuryTooltip()}
|
defaultValue={defaults.buryReviews}
|
||||||
>
|
markdownTooltip={tr.deckConfigBuryTooltip()}
|
||||||
{tr.deckConfigBuryReviewSiblings()}
|
>
|
||||||
</SwitchRow>
|
{tr.deckConfigBuryReviewSiblings()}
|
||||||
|
</SwitchRow>
|
||||||
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -12,9 +12,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import TextInputModal from "./TextInputModal.svelte";
|
import TextInputModal from "./TextInputModal.svelte";
|
||||||
import StickyContainer from "../components/StickyContainer.svelte";
|
import StickyContainer from "../components/StickyContainer.svelte";
|
||||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
|
||||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
|
||||||
|
|
||||||
import SelectButton from "../components/SelectButton.svelte";
|
import SelectButton from "../components/SelectButton.svelte";
|
||||||
import SelectOption from "../components/SelectOption.svelte";
|
import SelectOption from "../components/SelectOption.svelte";
|
||||||
|
@ -89,30 +87,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<StickyContainer --gutter-block="0.5rem" --sticky-borders="0 0 1px" breakpoint="sm">
|
<StickyContainer --gutter-block="0.5rem" --sticky-borders="0 0 1px" breakpoint="sm">
|
||||||
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
||||||
<Item>
|
<ButtonGroup class="flex-grow-1">
|
||||||
<ButtonGroup class="flex-grow-1">
|
<SelectButton
|
||||||
<ButtonGroupItem>
|
class="flex-grow-1"
|
||||||
<SelectButton class="flex-grow-1" on:change={blur}>
|
on:change={blur}
|
||||||
{#each $configList as entry}
|
--border-left-radius="5px"
|
||||||
<SelectOption
|
--border-right-radius="5px"
|
||||||
value={String(entry.idx)}
|
>
|
||||||
selected={entry.current}
|
{#each $configList as entry}
|
||||||
>
|
<SelectOption value={String(entry.idx)} selected={entry.current}>
|
||||||
{configLabel(entry)}
|
{configLabel(entry)}
|
||||||
</SelectOption>
|
</SelectOption>
|
||||||
{/each}
|
{/each}
|
||||||
</SelectButton>
|
</SelectButton>
|
||||||
</ButtonGroupItem>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
|
||||||
</Item>
|
|
||||||
|
|
||||||
<Item>
|
<SaveButton
|
||||||
<SaveButton
|
{state}
|
||||||
{state}
|
on:add={promptToAdd}
|
||||||
on:add={promptToAdd}
|
on:clone={promptToClone}
|
||||||
on:clone={promptToClone}
|
on:rename={promptToRename}
|
||||||
on:rename={promptToRename}
|
/>
|
||||||
/>
|
|
||||||
</Item>
|
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyContainer>
|
</StickyContainer>
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import TitledContainer from "./TitledContainer.svelte";
|
import TitledContainer from "./TitledContainer.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
import Item from "../components/Item.svelte";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
import Warning from "./Warning.svelte";
|
import Warning from "./Warning.svelte";
|
||||||
|
@ -40,28 +41,34 @@
|
||||||
: "";
|
: "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigDailyLimits()} {api}>
|
<TitledContainer title={tr.deckConfigDailyLimits()}>
|
||||||
<SpinBoxRow
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
bind:value={$config.newPerDay}
|
<Item>
|
||||||
defaultValue={defaults.newPerDay}
|
<SpinBoxRow
|
||||||
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
bind:value={$config.newPerDay}
|
||||||
>
|
defaultValue={defaults.newPerDay}
|
||||||
{tr.schedulingNewCardsday()}
|
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
||||||
</SpinBoxRow>
|
>
|
||||||
|
{tr.schedulingNewCardsday()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<Warning warning={newCardsGreaterThanParent} />
|
<Warning warning={newCardsGreaterThanParent} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<SpinBoxRow
|
<Item>
|
||||||
bind:value={$config.reviewsPerDay}
|
<SpinBoxRow
|
||||||
defaultValue={defaults.reviewsPerDay}
|
bind:value={$config.reviewsPerDay}
|
||||||
markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
defaultValue={defaults.reviewsPerDay}
|
||||||
>
|
markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
||||||
{tr.schedulingMaximumReviewsday()}
|
>
|
||||||
</SpinBoxRow>
|
{tr.schedulingMaximumReviewsday()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<Warning warning={reviewsTooLow} />
|
<Warning warning={reviewsTooLow} />
|
||||||
</Item>
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -15,6 +15,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import TimerOptions from "./TimerOptions.svelte";
|
import TimerOptions from "./TimerOptions.svelte";
|
||||||
import AudioOptions from "./AudioOptions.svelte";
|
import AudioOptions from "./AudioOptions.svelte";
|
||||||
import Addons from "./Addons.svelte";
|
import Addons from "./Addons.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
|
import Item from "../components/Item.svelte";
|
||||||
|
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import type { Writable } from "svelte/store";
|
import type { Writable } from "svelte/store";
|
||||||
|
@ -51,7 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export const displayOrder = {};
|
export const displayOrder = {};
|
||||||
export const timerOptions = {};
|
export const timerOptions = {};
|
||||||
export const audioOptions = {};
|
export const audioOptions = {};
|
||||||
export const addonOptions = {};
|
|
||||||
export const advancedOptions = {};
|
export const advancedOptions = {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -63,45 +64,64 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
--gutter-inline="0.25rem"
|
--gutter-inline="0.25rem"
|
||||||
--gutter-block="0.5rem"
|
--gutter-block="0.5rem"
|
||||||
class="container-columns"
|
class="container-columns"
|
||||||
api={options}
|
|
||||||
>
|
>
|
||||||
<Row class="row-columns">
|
<DynamicallySlottable slotHost={Item} api={options}>
|
||||||
<DailyLimits {state} api={dailyLimits} />
|
<Item>
|
||||||
</Row>
|
<Row class="row-columns">
|
||||||
|
<DailyLimits {state} api={dailyLimits} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<NewOptions {state} api={newOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<NewOptions {state} api={newOptions} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<LapseOptions {state} api={lapseOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<LapseOptions {state} api={lapseOptions} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
{#if state.v3Scheduler}
|
{#if state.v3Scheduler}
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<DisplayOrder {state} api={displayOrder} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<DisplayOrder {state} api={displayOrder} />
|
||||||
{/if}
|
</Row>
|
||||||
|
</Item>
|
||||||
|
{/if}
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<TimerOptions {state} api={timerOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<TimerOptions {state} api={timerOptions} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<BuryOptions {state} api={buryOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<BuryOptions {state} api={buryOptions} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<AudioOptions {state} api={audioOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<AudioOptions {state} api={audioOptions} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<Addons {state} api={addonOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<Addons {state} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Row class="row-columns">
|
<Item>
|
||||||
<AdvancedOptions {state} api={advancedOptions} />
|
<Row class="row-columns">
|
||||||
</Row>
|
<AdvancedOptions {state} api={advancedOptions} />
|
||||||
|
</Row>
|
||||||
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</Container>
|
</Container>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -5,8 +5,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import TitledContainer from "./TitledContainer.svelte";
|
import TitledContainer from "./TitledContainer.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
|
||||||
import EnumSelectorRow from "./EnumSelectorRow.svelte";
|
import EnumSelectorRow from "./EnumSelectorRow.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
|
import Item from "../components/Item.svelte";
|
||||||
|
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import { reviewMixChoices } from "./strings";
|
import { reviewMixChoices } from "./strings";
|
||||||
|
@ -45,59 +46,62 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
];
|
];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigOrderingTitle()} {api}>
|
<TitledContainer title={tr.deckConfigOrderingTitle()}>
|
||||||
<Item>
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
<EnumSelectorRow
|
<Item>
|
||||||
bind:value={$config.newCardGatherPriority}
|
<EnumSelectorRow
|
||||||
defaultValue={defaults.newCardGatherPriority}
|
bind:value={$config.newCardGatherPriority}
|
||||||
choices={newGatherPriorityChoices}
|
defaultValue={defaults.newCardGatherPriority}
|
||||||
markdownTooltip={tr.deckConfigNewGatherPriorityTooltip() + currentDeck}
|
choices={newGatherPriorityChoices}
|
||||||
>
|
markdownTooltip={tr.deckConfigNewGatherPriorityTooltip() + currentDeck}
|
||||||
{tr.deckConfigNewGatherPriority()}
|
>
|
||||||
</EnumSelectorRow>
|
{tr.deckConfigNewGatherPriority()}
|
||||||
</Item>
|
</EnumSelectorRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.newCardSortOrder}
|
bind:value={$config.newCardSortOrder}
|
||||||
defaultValue={defaults.newCardSortOrder}
|
defaultValue={defaults.newCardSortOrder}
|
||||||
choices={newSortOrderChoices}
|
choices={newSortOrderChoices}
|
||||||
markdownTooltip={tr.deckConfigNewCardSortOrderTooltip() + currentDeck}
|
markdownTooltip={tr.deckConfigNewCardSortOrderTooltip() + currentDeck}
|
||||||
>
|
>
|
||||||
{tr.deckConfigNewCardSortOrder()}
|
{tr.deckConfigNewCardSortOrder()}
|
||||||
</EnumSelectorRow>
|
</EnumSelectorRow>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.newMix}
|
bind:value={$config.newMix}
|
||||||
defaultValue={defaults.newMix}
|
defaultValue={defaults.newMix}
|
||||||
choices={reviewMixChoices()}
|
choices={reviewMixChoices()}
|
||||||
markdownTooltip={tr.deckConfigNewReviewPriorityTooltip() + currentDeck}
|
markdownTooltip={tr.deckConfigNewReviewPriorityTooltip() + currentDeck}
|
||||||
>
|
>
|
||||||
{tr.deckConfigNewReviewPriority()}
|
{tr.deckConfigNewReviewPriority()}
|
||||||
</EnumSelectorRow>
|
</EnumSelectorRow>
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.interdayLearningMix}
|
bind:value={$config.interdayLearningMix}
|
||||||
defaultValue={defaults.interdayLearningMix}
|
defaultValue={defaults.interdayLearningMix}
|
||||||
choices={reviewMixChoices()}
|
choices={reviewMixChoices()}
|
||||||
markdownTooltip={tr.deckConfigInterdayStepPriorityTooltip() + currentDeck}
|
markdownTooltip={tr.deckConfigInterdayStepPriorityTooltip() +
|
||||||
>
|
currentDeck}
|
||||||
{tr.deckConfigInterdayStepPriority()}
|
>
|
||||||
</EnumSelectorRow>
|
{tr.deckConfigInterdayStepPriority()}
|
||||||
</Item>
|
</EnumSelectorRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<EnumSelectorRow
|
<EnumSelectorRow
|
||||||
bind:value={$config.reviewOrder}
|
bind:value={$config.reviewOrder}
|
||||||
defaultValue={defaults.reviewOrder}
|
defaultValue={defaults.reviewOrder}
|
||||||
choices={reviewOrderChoices}
|
choices={reviewOrderChoices}
|
||||||
markdownTooltip={tr.deckConfigReviewSortOrderTooltip() + currentDeck}
|
markdownTooltip={tr.deckConfigReviewSortOrderTooltip() + currentDeck}
|
||||||
>
|
>
|
||||||
{tr.deckConfigReviewSortOrder()}
|
{tr.deckConfigReviewSortOrder()}
|
||||||
</EnumSelectorRow>
|
</EnumSelectorRow>
|
||||||
</Item>
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import TitledContainer from "./TitledContainer.svelte";
|
import TitledContainer from "./TitledContainer.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
import Item from "../components/Item.svelte";
|
||||||
import StepsInputRow from "./StepsInputRow.svelte";
|
import StepsInputRow from "./StepsInputRow.svelte";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
|
@ -32,44 +33,54 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
|
const leechChoices = [tr.actionsSuspendCard(), tr.schedulingTagOnly()];
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.schedulingLapses()} {api}>
|
<TitledContainer title={tr.schedulingLapses()}>
|
||||||
<StepsInputRow
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
bind:value={$config.relearnSteps}
|
<Item>
|
||||||
defaultValue={defaults.relearnSteps}
|
<StepsInputRow
|
||||||
markdownTooltip={tr.deckConfigRelearningStepsTooltip()}
|
bind:value={$config.relearnSteps}
|
||||||
>
|
defaultValue={defaults.relearnSteps}
|
||||||
{tr.deckConfigRelearningSteps()}
|
markdownTooltip={tr.deckConfigRelearningStepsTooltip()}
|
||||||
</StepsInputRow>
|
>
|
||||||
|
{tr.deckConfigRelearningSteps()}
|
||||||
|
</StepsInputRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxRow
|
<Item>
|
||||||
bind:value={$config.minimumLapseInterval}
|
<SpinBoxRow
|
||||||
defaultValue={defaults.minimumLapseInterval}
|
bind:value={$config.minimumLapseInterval}
|
||||||
min={1}
|
defaultValue={defaults.minimumLapseInterval}
|
||||||
markdownTooltip={tr.deckConfigMinimumIntervalTooltip()}
|
min={1}
|
||||||
>
|
markdownTooltip={tr.deckConfigMinimumIntervalTooltip()}
|
||||||
{tr.schedulingMinimumInterval()}
|
>
|
||||||
</SpinBoxRow>
|
{tr.schedulingMinimumInterval()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<Warning warning={stepsExceedMinimumInterval} />
|
<Warning warning={stepsExceedMinimumInterval} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<SpinBoxRow
|
<Item>
|
||||||
bind:value={$config.leechThreshold}
|
<SpinBoxRow
|
||||||
defaultValue={defaults.leechThreshold}
|
bind:value={$config.leechThreshold}
|
||||||
min={1}
|
defaultValue={defaults.leechThreshold}
|
||||||
markdownTooltip={tr.deckConfigLeechThresholdTooltip()}
|
min={1}
|
||||||
>
|
markdownTooltip={tr.deckConfigLeechThresholdTooltip()}
|
||||||
{tr.schedulingLeechThreshold()}
|
>
|
||||||
</SpinBoxRow>
|
{tr.schedulingLeechThreshold()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<EnumSelectorRow
|
<Item>
|
||||||
bind:value={$config.leechAction}
|
<EnumSelectorRow
|
||||||
defaultValue={defaults.leechAction}
|
bind:value={$config.leechAction}
|
||||||
choices={leechChoices}
|
defaultValue={defaults.leechAction}
|
||||||
breakpoint="sm"
|
choices={leechChoices}
|
||||||
markdownTooltip={tr.deckConfigLeechActionTooltip()}
|
breakpoint="sm"
|
||||||
>
|
markdownTooltip={tr.deckConfigLeechActionTooltip()}
|
||||||
{tr.schedulingLeechAction()}
|
>
|
||||||
</EnumSelectorRow>
|
{tr.schedulingLeechAction()}
|
||||||
|
</EnumSelectorRow>
|
||||||
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -7,8 +7,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import StepsInputRow from "./StepsInputRow.svelte";
|
import StepsInputRow from "./StepsInputRow.svelte";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
import EnumSelectorRow from "./EnumSelectorRow.svelte";
|
import EnumSelectorRow from "./EnumSelectorRow.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
|
||||||
import Warning from "./Warning.svelte";
|
import Warning from "./Warning.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
|
import Item from "../components/Item.svelte";
|
||||||
|
|
||||||
import type { DeckOptionsState } from "./lib";
|
import type { DeckOptionsState } from "./lib";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
|
@ -41,46 +42,56 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
: "";
|
: "";
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.schedulingNewCards()} {api}>
|
<TitledContainer title={tr.schedulingNewCards()}>
|
||||||
<StepsInputRow
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
bind:value={$config.learnSteps}
|
<Item>
|
||||||
defaultValue={defaults.learnSteps}
|
<StepsInputRow
|
||||||
markdownTooltip={tr.deckConfigLearningStepsTooltip()}
|
bind:value={$config.learnSteps}
|
||||||
>
|
defaultValue={defaults.learnSteps}
|
||||||
{tr.deckConfigLearningSteps()}
|
markdownTooltip={tr.deckConfigLearningStepsTooltip()}
|
||||||
</StepsInputRow>
|
>
|
||||||
|
{tr.deckConfigLearningSteps()}
|
||||||
|
</StepsInputRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<SpinBoxRow
|
<Item>
|
||||||
bind:value={$config.graduatingIntervalGood}
|
<SpinBoxRow
|
||||||
defaultValue={defaults.graduatingIntervalGood}
|
bind:value={$config.graduatingIntervalGood}
|
||||||
markdownTooltip={tr.deckConfigGraduatingIntervalTooltip()}
|
defaultValue={defaults.graduatingIntervalGood}
|
||||||
>
|
markdownTooltip={tr.deckConfigGraduatingIntervalTooltip()}
|
||||||
{tr.schedulingGraduatingInterval()}
|
>
|
||||||
</SpinBoxRow>
|
{tr.schedulingGraduatingInterval()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<Warning warning={stepsExceedGraduatingInterval} />
|
<Warning warning={stepsExceedGraduatingInterval} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<SpinBoxRow
|
<Item>
|
||||||
bind:value={$config.graduatingIntervalEasy}
|
<SpinBoxRow
|
||||||
defaultValue={defaults.graduatingIntervalEasy}
|
bind:value={$config.graduatingIntervalEasy}
|
||||||
markdownTooltip={tr.deckConfigEasyIntervalTooltip()}
|
defaultValue={defaults.graduatingIntervalEasy}
|
||||||
>
|
markdownTooltip={tr.deckConfigEasyIntervalTooltip()}
|
||||||
{tr.schedulingEasyInterval()}
|
>
|
||||||
</SpinBoxRow>
|
{tr.schedulingEasyInterval()}
|
||||||
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<Warning warning={goodExceedsEasy} />
|
<Warning warning={goodExceedsEasy} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<EnumSelectorRow
|
<Item>
|
||||||
bind:value={$config.newCardInsertOrder}
|
<EnumSelectorRow
|
||||||
defaultValue={defaults.newCardInsertOrder}
|
bind:value={$config.newCardInsertOrder}
|
||||||
choices={newInsertOrderChoices}
|
defaultValue={defaults.newCardInsertOrder}
|
||||||
breakpoint={"md"}
|
choices={newInsertOrderChoices}
|
||||||
markdownTooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
breakpoint={"md"}
|
||||||
>
|
markdownTooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
||||||
{tr.deckConfigNewInsertionOrder()}
|
>
|
||||||
</EnumSelectorRow>
|
{tr.deckConfigNewInsertionOrder()}
|
||||||
|
</EnumSelectorRow>
|
||||||
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -11,8 +11,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { withCollapsedWhitespace } from "../lib/i18n";
|
import { withCollapsedWhitespace } from "../lib/i18n";
|
||||||
|
|
||||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
|
||||||
|
|
||||||
import LabelButton from "../components/LabelButton.svelte";
|
import LabelButton from "../components/LabelButton.svelte";
|
||||||
import DropdownMenu from "../components/DropdownMenu.svelte";
|
import DropdownMenu from "../components/DropdownMenu.svelte";
|
||||||
import DropdownItem from "../components/DropdownItem.svelte";
|
import DropdownItem from "../components/DropdownItem.svelte";
|
||||||
|
@ -62,40 +60,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<ButtonGroupItem>
|
<LabelButton
|
||||||
<LabelButton
|
theme="primary"
|
||||||
theme="primary"
|
on:click={() => save(false)}
|
||||||
on:click={() => save(false)}
|
tooltip={getPlatformString(saveKeyCombination)}
|
||||||
tooltip={getPlatformString(saveKeyCombination)}
|
--border-left-radius="5px">{tr.deckConfigSaveButton()}</LabelButton
|
||||||
>{tr.deckConfigSaveButton()}</LabelButton
|
>
|
||||||
>
|
<Shortcut keyCombination={saveKeyCombination} on:click={() => save(false)} />
|
||||||
<Shortcut keyCombination={saveKeyCombination} on:click={() => save(false)} />
|
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<WithDropdown let:createDropdown --border-right-radius="5px">
|
||||||
<WithDropdown let:createDropdown>
|
<LabelButton
|
||||||
<LabelButton
|
on:click={() => dropdown.toggle()}
|
||||||
on:mount={(event) => (dropdown = createDropdown(event.detail.button))}
|
on:mount={(event) => (dropdown = createDropdown(event.detail.button))}
|
||||||
on:click={() => dropdown.toggle()}
|
/>
|
||||||
/>
|
<DropdownMenu>
|
||||||
<DropdownMenu>
|
<DropdownItem on:click={() => dispatch("add")}
|
||||||
<DropdownItem on:click={() => dispatch("add")}
|
>{tr.deckConfigAddGroup()}</DropdownItem
|
||||||
>{tr.deckConfigAddGroup()}</DropdownItem
|
>
|
||||||
>
|
<DropdownItem on:click={() => dispatch("clone")}
|
||||||
<DropdownItem on:click={() => dispatch("clone")}
|
>{tr.deckConfigCloneGroup()}</DropdownItem
|
||||||
>{tr.deckConfigCloneGroup()}</DropdownItem
|
>
|
||||||
>
|
<DropdownItem on:click={() => dispatch("rename")}>
|
||||||
<DropdownItem on:click={() => dispatch("rename")}>
|
{tr.deckConfigRenameGroup()}
|
||||||
{tr.deckConfigRenameGroup()}
|
</DropdownItem>
|
||||||
</DropdownItem>
|
<DropdownItem on:click={removeConfig}
|
||||||
<DropdownItem on:click={removeConfig}
|
>{tr.deckConfigRemoveGroup()}</DropdownItem
|
||||||
>{tr.deckConfigRemoveGroup()}</DropdownItem
|
>
|
||||||
>
|
<DropdownDivider />
|
||||||
<DropdownDivider />
|
<DropdownItem on:click={() => save(true)}>
|
||||||
<DropdownItem on:click={() => save(true)}>
|
{tr.deckConfigSaveToAllSubdecks()}
|
||||||
{tr.deckConfigSaveToAllSubdecks()}
|
</DropdownItem>
|
||||||
</DropdownItem>
|
</DropdownMenu>
|
||||||
</DropdownMenu>
|
</WithDropdown>
|
||||||
</WithDropdown>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import TitledContainer from "./TitledContainer.svelte";
|
import TitledContainer from "./TitledContainer.svelte";
|
||||||
|
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||||
import Item from "../components/Item.svelte";
|
import Item from "../components/Item.svelte";
|
||||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||||
import SwitchRow from "./SwitchRow.svelte";
|
import SwitchRow from "./SwitchRow.svelte";
|
||||||
|
@ -17,26 +18,28 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let defaults = state.defaults;
|
let defaults = state.defaults;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<TitledContainer title={tr.deckConfigTimerTitle()} {api}>
|
<TitledContainer title={tr.deckConfigTimerTitle()}>
|
||||||
<Item>
|
<DynamicallySlottable slotHost={Item} {api}>
|
||||||
<SpinBoxRow
|
<Item>
|
||||||
bind:value={$config.capAnswerTimeToSecs}
|
<SpinBoxRow
|
||||||
defaultValue={defaults.capAnswerTimeToSecs}
|
bind:value={$config.capAnswerTimeToSecs}
|
||||||
min={30}
|
defaultValue={defaults.capAnswerTimeToSecs}
|
||||||
max={600}
|
min={30}
|
||||||
markdownTooltip={tr.deckConfigMaximumAnswerSecsTooltip()}
|
max={600}
|
||||||
>
|
markdownTooltip={tr.deckConfigMaximumAnswerSecsTooltip()}
|
||||||
{tr.deckConfigMaximumAnswerSecs()}
|
>
|
||||||
</SpinBoxRow>
|
{tr.deckConfigMaximumAnswerSecs()}
|
||||||
</Item>
|
</SpinBoxRow>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item>
|
<Item>
|
||||||
<SwitchRow
|
<SwitchRow
|
||||||
bind:value={$config.showTimer}
|
bind:value={$config.showTimer}
|
||||||
defaultValue={defaults.showTimer}
|
defaultValue={defaults.showTimer}
|
||||||
markdownTooltip={tr.deckConfigShowAnswerTimerTooltip()}
|
markdownTooltip={tr.deckConfigShowAnswerTimerTooltip()}
|
||||||
>
|
>
|
||||||
{tr.schedulingShowAnswerTimer()}
|
{tr.schedulingShowAnswerTimer()}
|
||||||
</SwitchRow>
|
</SwitchRow>
|
||||||
</Item>
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</TitledContainer>
|
</TitledContainer>
|
||||||
|
|
|
@ -6,10 +6,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import Container from "../components/Container.svelte";
|
import Container from "../components/Container.svelte";
|
||||||
|
|
||||||
export let title: string;
|
export let title: string;
|
||||||
export let api: Record<string, never> | undefined = undefined;
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<Container --gutter-block="2px" --container-margin="0" {api}>
|
<Container --gutter-block="2px" --container-margin="0">
|
||||||
<h1>{title}</h1>
|
<h1>{title}</h1>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { registerPackage } from "../../lib/register-package";
|
import { registerPackage } from "../../lib/runtime-require";
|
||||||
|
|
||||||
import { saveSelection, restoreSelection } from "./document";
|
import { saveSelection, restoreSelection } from "./document";
|
||||||
import { Position } from "./location";
|
import { Position } from "./location";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { registerPackage } from "../../lib/register-package";
|
import { registerPackage } from "../../lib/runtime-require";
|
||||||
|
|
||||||
import { surroundNoSplitting } from "./no-splitting";
|
import { surroundNoSplitting } from "./no-splitting";
|
||||||
import { unsurround } from "./unsurround";
|
import { unsurround } from "./unsurround";
|
||||||
|
|
|
@ -3,11 +3,13 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
|
import NoteEditor from "./NoteEditor.svelte";
|
||||||
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
|
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||||
|
import PreviewButton from "./PreviewButton.svelte";
|
||||||
|
import type { NoteEditorAPI } from "./NoteEditor.svelte";
|
||||||
|
|
||||||
const api: Partial<NoteEditorAPI> = {};
|
const api: Partial<NoteEditorAPI> = {};
|
||||||
let noteEditor: OldEditorAdapter;
|
let noteEditor: NoteEditor;
|
||||||
|
|
||||||
export let uiResolve: (api: NoteEditorAPI) => void;
|
export let uiResolve: (api: NoteEditorAPI) => void;
|
||||||
|
|
||||||
|
@ -16,4 +18,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OldEditorAdapter bind:this={noteEditor} {api} />
|
<NoteEditor bind:this={noteEditor} {api}>
|
||||||
|
<svelte:fragment slot="notetypeButtons">
|
||||||
|
<ButtonGroupItem>
|
||||||
|
<PreviewButton />
|
||||||
|
</ButtonGroupItem>
|
||||||
|
</svelte:fragment>
|
||||||
|
</NoteEditor>
|
||||||
|
|
|
@ -12,14 +12,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const decoratedElements = new CustomElementArray<DecoratedElementConstructor>();
|
const decoratedElements = new CustomElementArray<DecoratedElementConstructor>();
|
||||||
|
|
||||||
const key = Symbol("decoratedElements");
|
const key = Symbol("decoratedElements");
|
||||||
const [set, getDecoratedElements, hasDecoratedElements] =
|
const [context, setContextProperty] =
|
||||||
contextProperty<CustomElementArray<DecoratedElementConstructor>>(key);
|
contextProperty<CustomElementArray<DecoratedElementConstructor>>(key);
|
||||||
|
|
||||||
export { getDecoratedElements, hasDecoratedElements };
|
export { context };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
set(decoratedElements);
|
setContextProperty(decoratedElements);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<slot />
|
<slot />
|
||||||
|
|
|
@ -22,9 +22,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = Symbol("editingArea");
|
const key = Symbol("editingArea");
|
||||||
const [set, getEditingArea, hasEditingArea] = contextProperty<EditingAreaAPI>(key);
|
const [context, setContextProperty] = contextProperty<EditingAreaAPI>(key);
|
||||||
|
|
||||||
export { getEditingArea, hasEditingArea };
|
export { context };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -117,17 +117,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export let api: Partial<EditingAreaAPI>;
|
let apiPartial: Partial<EditingAreaAPI>;
|
||||||
|
export { apiPartial as api };
|
||||||
|
|
||||||
Object.assign(
|
const api = Object.assign(apiPartial, {
|
||||||
api,
|
content,
|
||||||
set({
|
editingInputs: inputsStore,
|
||||||
content,
|
focus,
|
||||||
editingInputs: inputsStore,
|
refocus,
|
||||||
focus,
|
});
|
||||||
refocus,
|
|
||||||
}),
|
setContextProperty(api);
|
||||||
);
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
if (autofocus) {
|
if (autofocus) {
|
||||||
|
|
|
@ -22,9 +22,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = Symbol("editorField");
|
const key = Symbol("editorField");
|
||||||
const [set, getEditorField, hasEditorField] = contextProperty<EditorFieldAPI>(key);
|
const [context, setContextProperty] = contextProperty<EditorFieldAPI>(key);
|
||||||
|
|
||||||
export { getEditorField, hasEditorField };
|
export { context };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -45,8 +45,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let field: FieldData;
|
export let field: FieldData;
|
||||||
export let autofocus = false;
|
export let autofocus = false;
|
||||||
|
|
||||||
export let api: (Partial<EditorFieldAPI> & Destroyable) | undefined = undefined;
|
|
||||||
|
|
||||||
const directionStore = writable<"ltr" | "rtl">();
|
const directionStore = writable<"ltr" | "rtl">();
|
||||||
setContext(directionKey, directionStore);
|
setContext(directionKey, directionStore);
|
||||||
|
|
||||||
|
@ -55,15 +53,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const editingArea: Partial<EditingAreaAPI> = {};
|
const editingArea: Partial<EditingAreaAPI> = {};
|
||||||
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
const [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
||||||
|
|
||||||
const editorFieldApi = set({
|
let apiPartial: Partial<EditorFieldAPI> & Destroyable;
|
||||||
|
export { apiPartial as api };
|
||||||
|
|
||||||
|
const api: EditorFieldAPI & Destroyable = Object.assign(apiPartial, {
|
||||||
element,
|
element,
|
||||||
direction: directionStore,
|
direction: directionStore,
|
||||||
editingArea: editingArea as EditingAreaAPI,
|
editingArea: editingArea as EditingAreaAPI,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (api) {
|
setContextProperty(api);
|
||||||
Object.assign(api, editorFieldApi);
|
|
||||||
}
|
|
||||||
|
|
||||||
onDestroy(() => api?.destroy());
|
onDestroy(() => api?.destroy());
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -3,10 +3,10 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { getDecoratedElements } from "./DecoratedElements.svelte";
|
import { context } from "./DecoratedElements.svelte";
|
||||||
import { Mathjax } from "../editable/mathjax-element";
|
import { Mathjax } from "../editable/mathjax-element";
|
||||||
|
|
||||||
const decoratedElements = getDecoratedElements();
|
const decoratedElements = context.get();
|
||||||
decoratedElements.push(Mathjax);
|
decoratedElements.push(Mathjax);
|
||||||
|
|
||||||
import { parsingInstructions } from "./plain-text-input";
|
import { parsingInstructions } from "./plain-text-input";
|
||||||
|
|
|
@ -7,11 +7,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { bridgeCommand } from "../lib/bridgecommand";
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
import { registerShortcut } from "../lib/shortcuts";
|
import { registerShortcut } from "../lib/shortcuts";
|
||||||
import StickyBadge from "./StickyBadge.svelte";
|
import StickyBadge from "./StickyBadge.svelte";
|
||||||
import OldEditorAdapter from "./OldEditorAdapter.svelte";
|
import NoteEditor from "./NoteEditor.svelte";
|
||||||
import type { NoteEditorAPI } from "./OldEditorAdapter.svelte";
|
import type { NoteEditorAPI } from "./NoteEditor.svelte";
|
||||||
|
|
||||||
const api: Partial<NoteEditorAPI> = {};
|
const api: Partial<NoteEditorAPI> = {};
|
||||||
let noteEditor: OldEditorAdapter;
|
let noteEditor: NoteEditor;
|
||||||
|
|
||||||
export let uiResolve: (api: NoteEditorAPI) => void;
|
export let uiResolve: (api: NoteEditorAPI) => void;
|
||||||
|
|
||||||
|
@ -43,8 +43,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
onDestroy(() => deregisterSticky);
|
onDestroy(() => deregisterSticky);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OldEditorAdapter bind:this={noteEditor} {api}>
|
<NoteEditor bind:this={noteEditor} {api}>
|
||||||
<svelte:fragment slot="field-state" let:index>
|
<svelte:fragment slot="field-state" let:index>
|
||||||
<StickyBadge active={stickies[index]} {index} />
|
<StickyBadge active={stickies[index]} {index} />
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</OldEditorAdapter>
|
</NoteEditor>
|
||||||
|
|
|
@ -2,14 +2,371 @@
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
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 type { EditorFieldAPI } from "./EditorField.svelte";
|
||||||
|
import type { EditingInputAPI } from "./EditingArea.svelte";
|
||||||
|
import type { EditorToolbarAPI } from "./editor-toolbar";
|
||||||
|
|
||||||
|
export interface NoteEditorAPI {
|
||||||
|
fields: EditorFieldAPI[];
|
||||||
|
focusedField: Writable<EditorFieldAPI | null>;
|
||||||
|
focusedInput: Writable<EditingInputAPI | null>;
|
||||||
|
toolbar: EditorToolbarAPI;
|
||||||
|
}
|
||||||
|
|
||||||
|
import contextProperty from "../sveltelib/context-property";
|
||||||
|
import lifecycleHooks from "../sveltelib/lifecycle-hooks";
|
||||||
|
import { registerPackage } from "../lib/runtime-require";
|
||||||
|
|
||||||
|
const key = Symbol("noteEditor");
|
||||||
|
const [context, setContextProperty] = contextProperty<NoteEditorAPI>(key);
|
||||||
|
const [lifecycle, instances, setupLifecycleHooks] = lifecycleHooks<NoteEditorAPI>();
|
||||||
|
|
||||||
|
export { context };
|
||||||
|
|
||||||
|
registerPackage("anki/NoteEditor", {
|
||||||
|
context,
|
||||||
|
lifecycle,
|
||||||
|
instances,
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
import { writable, get } from "svelte/store";
|
||||||
|
import Absolute from "../components/Absolute.svelte";
|
||||||
|
import Badge from "../components/Badge.svelte";
|
||||||
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
|
import { isApplePlatform } from "../lib/platform";
|
||||||
|
|
||||||
|
import FieldsEditor from "./FieldsEditor.svelte";
|
||||||
|
import Fields from "./Fields.svelte";
|
||||||
|
import EditorField from "./EditorField.svelte";
|
||||||
|
import type { FieldData } from "./EditorField.svelte";
|
||||||
|
import { TagEditor } from "./tag-editor";
|
||||||
|
|
||||||
|
import { EditorToolbar } from "./editor-toolbar";
|
||||||
|
import Notification from "./Notification.svelte";
|
||||||
|
import DuplicateLink from "./DuplicateLink.svelte";
|
||||||
|
|
||||||
|
import DecoratedElements from "./DecoratedElements.svelte";
|
||||||
|
import { RichTextInput, editingInputIsRichText } from "./rich-text-input";
|
||||||
|
import { PlainTextInput } from "./plain-text-input";
|
||||||
|
import { MathjaxHandle } from "./mathjax-overlay";
|
||||||
|
import { ImageHandle } from "./image-overlay";
|
||||||
|
import MathjaxElement from "./MathjaxElement.svelte";
|
||||||
|
import FrameElement from "./FrameElement.svelte";
|
||||||
|
|
||||||
|
import RichTextBadge from "./RichTextBadge.svelte";
|
||||||
|
import PlainTextBadge from "./PlainTextBadge.svelte";
|
||||||
|
|
||||||
|
import { ChangeTimer } from "./change-timer";
|
||||||
|
import { clearableArray } from "./destroyable";
|
||||||
|
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 : 1.8;
|
||||||
|
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.length
|
||||||
|
) {
|
||||||
|
fieldStores.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [index, [_, fieldContent]] of fs.entries()) {
|
||||||
|
fieldStores[index].set(fieldContent);
|
||||||
|
}
|
||||||
|
|
||||||
|
fieldNames = newFieldNames;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fieldDescriptions: string[] = [];
|
||||||
|
export function setDescriptions(fs: string[]): void {
|
||||||
|
fieldDescriptions = fs;
|
||||||
|
}
|
||||||
|
|
||||||
|
let fonts: [string, number, boolean][] = [];
|
||||||
|
let richTextsHidden: boolean[] = [];
|
||||||
|
let plainTextsHidden: boolean[] = [];
|
||||||
|
let fields = clearableArray<EditorFieldAPI>();
|
||||||
|
|
||||||
|
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 {
|
||||||
|
if (typeof n === "number") {
|
||||||
|
focusTo = n;
|
||||||
|
fields[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 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,
|
||||||
|
description: fieldDescriptions[index],
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 toolbar: Partial<EditorToolbarAPI> = {};
|
||||||
|
|
||||||
|
import { wrapInternal } from "../lib/wrap";
|
||||||
|
import * as oldEditorAdapter from "./old-editor-adapter";
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
function wrap(before: string, after: string): void {
|
||||||
|
if (!$focusedInput || !editingInputIsRichText($focusedInput)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$focusedInput.element.then((element) => {
|
||||||
|
wrapInternal(element, before, after, false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(globalThis, {
|
||||||
|
setFields,
|
||||||
|
setDescriptions,
|
||||||
|
setFonts,
|
||||||
|
focusField,
|
||||||
|
setColorButtons,
|
||||||
|
setTags,
|
||||||
|
setBackgrounds,
|
||||||
|
setClozeHint,
|
||||||
|
saveNow: saveFieldNow,
|
||||||
|
focusIfField,
|
||||||
|
setNoteId,
|
||||||
|
wrap,
|
||||||
|
...oldEditorAdapter,
|
||||||
|
});
|
||||||
|
|
||||||
|
document.addEventListener("visibilitychange", saveOnPageHide);
|
||||||
|
return () => document.removeEventListener("visibilitychange", saveOnPageHide);
|
||||||
|
});
|
||||||
|
|
||||||
|
let apiPartial: Partial<NoteEditorAPI> = {};
|
||||||
|
export { apiPartial as api };
|
||||||
|
|
||||||
|
const focusedField: NoteEditorAPI["focusedField"] = writable(null);
|
||||||
|
const focusedInput: NoteEditorAPI["focusedInput"] = writable(null);
|
||||||
|
|
||||||
|
const api: NoteEditorAPI = {
|
||||||
|
...apiPartial,
|
||||||
|
focusedField,
|
||||||
|
focusedInput,
|
||||||
|
toolbar: toolbar as EditorToolbarAPI,
|
||||||
|
fields,
|
||||||
|
};
|
||||||
|
|
||||||
|
setContextProperty(api);
|
||||||
|
setupLifecycleHooks(api);
|
||||||
|
</script>
|
||||||
|
|
||||||
<div class="note-editor">
|
<div class="note-editor">
|
||||||
<slot />
|
<FieldsEditor>
|
||||||
|
<EditorToolbar {size} {wrap} {textColor} {highlightColor} api={toolbar}>
|
||||||
|
<slot slot="notetypeButtons" name="notetypeButtons" />
|
||||||
|
</EditorToolbar>
|
||||||
|
|
||||||
|
{#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>
|
||||||
|
<DecoratedElements>
|
||||||
|
{#each fieldsData as field, index}
|
||||||
|
<EditorField
|
||||||
|
{field}
|
||||||
|
content={fieldStores[index]}
|
||||||
|
autofocus={index === focusTo}
|
||||||
|
api={fields[index]}
|
||||||
|
on:focusin={() => {
|
||||||
|
$focusedField = fields[index];
|
||||||
|
bridgeCommand(`focus:${index}`);
|
||||||
|
}}
|
||||||
|
on:focusout={() => {
|
||||||
|
$focusedField = null;
|
||||||
|
bridgeCommand(
|
||||||
|
`blur:${index}:${getNoteId()}:${get(
|
||||||
|
fieldStores[index],
|
||||||
|
)}`,
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
--label-color={cols[index] === "dupe"
|
||||||
|
? "var(--flag1-bg)"
|
||||||
|
: "transparent"}
|
||||||
|
>
|
||||||
|
<svelte:fragment slot="field-state">
|
||||||
|
{#if cols[index] === "dupe"}
|
||||||
|
<DuplicateLink />
|
||||||
|
{/if}
|
||||||
|
<RichTextBadge bind:off={richTextsHidden[index]} />
|
||||||
|
<PlainTextBadge bind:off={plainTextsHidden[index]} />
|
||||||
|
|
||||||
|
<slot name="field-state" {field} {index} />
|
||||||
|
</svelte:fragment>
|
||||||
|
|
||||||
|
<svelte:fragment slot="editing-inputs">
|
||||||
|
<RichTextInput
|
||||||
|
hidden={richTextsHidden[index]}
|
||||||
|
on:focusin={() => {
|
||||||
|
$focusedInput = richTextInputs[index].api;
|
||||||
|
}}
|
||||||
|
on:focusout={() => {
|
||||||
|
$focusedInput = null;
|
||||||
|
saveFieldNow();
|
||||||
|
}}
|
||||||
|
bind:this={richTextInputs[index]}
|
||||||
|
>
|
||||||
|
<ImageHandle />
|
||||||
|
<MathjaxHandle />
|
||||||
|
</RichTextInput>
|
||||||
|
|
||||||
|
<PlainTextInput
|
||||||
|
hidden={plainTextsHidden[index]}
|
||||||
|
on:focusin={() => {
|
||||||
|
$focusedInput = plainTextInputs[index].api;
|
||||||
|
}}
|
||||||
|
on:focusout={() => {
|
||||||
|
$focusedInput = null;
|
||||||
|
saveFieldNow();
|
||||||
|
}}
|
||||||
|
bind:this={plainTextInputs[index]}
|
||||||
|
/>
|
||||||
|
</svelte:fragment>
|
||||||
|
</EditorField>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<MathjaxElement />
|
||||||
|
<FrameElement />
|
||||||
|
</DecoratedElements>
|
||||||
|
</Fields>
|
||||||
|
</FieldsEditor>
|
||||||
|
|
||||||
|
<TagEditor {size} {wrap} {tags} on:tagsupdate={saveTags} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.note-editor {
|
.note-editor {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,361 +0,0 @@
|
||||||
<!--
|
|
||||||
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 "./rich-text-input";
|
|
||||||
import type { PlainTextInputAPI } from "./plain-text-input";
|
|
||||||
import type { EditorToolbarAPI } from "./editor-toolbar";
|
|
||||||
|
|
||||||
import contextProperty from "../sveltelib/context-property";
|
|
||||||
import { writable, get } from "svelte/store";
|
|
||||||
|
|
||||||
export interface NoteEditorAPI {
|
|
||||||
fields: EditorFieldAPI[];
|
|
||||||
currentField: Writable<EditorFieldAPI | null>;
|
|
||||||
activeInput: Writable<RichTextInputAPI | PlainTextInputAPI | null>;
|
|
||||||
focusInRichText: Writable<boolean>;
|
|
||||||
toolbar: EditorToolbarAPI;
|
|
||||||
}
|
|
||||||
|
|
||||||
const key = Symbol("noteEditor");
|
|
||||||
const [set, getNoteEditor, hasNoteEditor] = contextProperty<NoteEditorAPI>(key);
|
|
||||||
|
|
||||||
export { getNoteEditor, hasNoteEditor };
|
|
||||||
|
|
||||||
const activeInput = writable<RichTextInputAPI | PlainTextInputAPI | null>(null);
|
|
||||||
const currentField = writable<EditorFieldAPI | null>(null);
|
|
||||||
</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 "./tag-editor";
|
|
||||||
|
|
||||||
import { EditorToolbar } from "./editor-toolbar";
|
|
||||||
import Notification from "./Notification.svelte";
|
|
||||||
import Absolute from "../components/Absolute.svelte";
|
|
||||||
import Badge from "../components/Badge.svelte";
|
|
||||||
import DuplicateLink from "./DuplicateLink.svelte";
|
|
||||||
|
|
||||||
import DecoratedElements from "./DecoratedElements.svelte";
|
|
||||||
import { RichTextInput } from "./rich-text-input";
|
|
||||||
import { MathjaxHandle } from "./mathjax-overlay";
|
|
||||||
import { ImageHandle } from "./image-overlay";
|
|
||||||
import { PlainTextInput } from "./plain-text-input";
|
|
||||||
import MathjaxElement from "./MathjaxElement.svelte";
|
|
||||||
import FrameElement from "./FrameElement.svelte";
|
|
||||||
|
|
||||||
import RichTextBadge from "./RichTextBadge.svelte";
|
|
||||||
import PlainTextBadge from "./PlainTextBadge.svelte";
|
|
||||||
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import { bridgeCommand } from "../lib/bridgecommand";
|
|
||||||
import { isApplePlatform } from "../lib/platform";
|
|
||||||
import { ChangeTimer } from "./change-timer";
|
|
||||||
import { alertIcon } from "./icons";
|
|
||||||
import { clearableArray } from "./destroyable";
|
|
||||||
|
|
||||||
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 : 1.8;
|
|
||||||
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.length
|
|
||||||
) {
|
|
||||||
fieldStores.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const [index, [_, fieldContent]] of fs.entries()) {
|
|
||||||
fieldStores[index].set(fieldContent);
|
|
||||||
}
|
|
||||||
|
|
||||||
fieldNames = newFieldNames;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fieldDescriptions: string[] = [];
|
|
||||||
export function setDescriptions(fs: string[]): void {
|
|
||||||
fieldDescriptions = fs;
|
|
||||||
}
|
|
||||||
|
|
||||||
let fonts: [string, number, boolean][] = [];
|
|
||||||
let richTextsHidden: boolean[] = [];
|
|
||||||
let plainTextsHidden: boolean[] = [];
|
|
||||||
let fields = clearableArray<EditorFieldAPI>();
|
|
||||||
|
|
||||||
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 {
|
|
||||||
if (typeof n === "number") {
|
|
||||||
focusTo = n;
|
|
||||||
fields[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 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,
|
|
||||||
description: fieldDescriptions[index],
|
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
const focusInRichText = writable<boolean>(false);
|
|
||||||
|
|
||||||
let toolbar: Partial<EditorToolbarAPI> = {};
|
|
||||||
|
|
||||||
export let api = {};
|
|
||||||
|
|
||||||
Object.assign(
|
|
||||||
api,
|
|
||||||
set({
|
|
||||||
currentField,
|
|
||||||
activeInput,
|
|
||||||
focusInRichText,
|
|
||||||
toolbar: toolbar as EditorToolbarAPI,
|
|
||||||
fields,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
import { wrapInternal } from "../lib/wrap";
|
|
||||||
import * as oldEditorAdapter from "./old-editor-adapter";
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
function wrap(before: string, after: string): void {
|
|
||||||
if (!get(focusInRichText)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const input = get(activeInput!) as RichTextInputAPI;
|
|
||||||
|
|
||||||
input.element.then((element) => {
|
|
||||||
wrapInternal(element, before, after, false);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
Object.assign(globalThis, {
|
|
||||||
setFields,
|
|
||||||
setDescriptions,
|
|
||||||
setFonts,
|
|
||||||
focusField,
|
|
||||||
setColorButtons,
|
|
||||||
setTags,
|
|
||||||
setBackgrounds,
|
|
||||||
setClozeHint,
|
|
||||||
saveNow: saveFieldNow,
|
|
||||||
focusIfField,
|
|
||||||
setNoteId,
|
|
||||||
wrap,
|
|
||||||
...oldEditorAdapter,
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener("visibilitychange", saveOnPageHide);
|
|
||||||
return () => document.removeEventListener("visibilitychange", saveOnPageHide);
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<NoteEditor>
|
|
||||||
<FieldsEditor>
|
|
||||||
<EditorToolbar {size} {wrap} {textColor} {highlightColor} api={toolbar} />
|
|
||||||
|
|
||||||
{#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>
|
|
||||||
<DecoratedElements>
|
|
||||||
{#each fieldsData as field, index}
|
|
||||||
<EditorField
|
|
||||||
{field}
|
|
||||||
content={fieldStores[index]}
|
|
||||||
autofocus={index === focusTo}
|
|
||||||
api={fields[index]}
|
|
||||||
on:focusin={() => {
|
|
||||||
$currentField = 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">
|
|
||||||
{#if cols[index] === "dupe"}
|
|
||||||
<DuplicateLink />
|
|
||||||
{/if}
|
|
||||||
<RichTextBadge bind:off={richTextsHidden[index]} />
|
|
||||||
<PlainTextBadge bind:off={plainTextsHidden[index]} />
|
|
||||||
|
|
||||||
<slot name="field-state" {field} {index} />
|
|
||||||
</svelte:fragment>
|
|
||||||
|
|
||||||
<svelte:fragment slot="editing-inputs">
|
|
||||||
<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]}
|
|
||||||
/>
|
|
||||||
</svelte:fragment>
|
|
||||||
</EditorField>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<MathjaxElement />
|
|
||||||
<FrameElement />
|
|
||||||
</DecoratedElements>
|
|
||||||
</Fields>
|
|
||||||
</FieldsEditor>
|
|
||||||
|
|
||||||
<TagEditor {size} {wrap} {tags} on:tagsupdate={saveTags} />
|
|
||||||
</NoteEditor>
|
|
|
@ -7,10 +7,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { htmlOn, htmlOff } from "./icons";
|
import { htmlOn, htmlOff } from "./icons";
|
||||||
import { getEditorField } from "./EditorField.svelte";
|
import { context as editorFieldContext } from "./EditorField.svelte";
|
||||||
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
||||||
|
|
||||||
const editorField = getEditorField();
|
const editorField = editorFieldContext.get();
|
||||||
const keyCombination = "Control+Shift+X";
|
const keyCombination = "Control+Shift+X";
|
||||||
|
|
||||||
export let off = false;
|
export let off = false;
|
||||||
|
|
|
@ -6,16 +6,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
const active = writable(false);
|
const active = writable(false);
|
||||||
export const togglePreviewButtonState = (state: boolean) => active.set(state);
|
|
||||||
|
export function togglePreviewButtonState(state: boolean): void {
|
||||||
|
active.set(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.assign(globalThis, { togglePreviewButtonState });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import * as tr from "../lib/ftl";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
import * as tr from "../../lib/ftl";
|
import { getPlatformString } from "../lib/shortcuts";
|
||||||
|
|
||||||
import LabelButton from "../../components/LabelButton.svelte";
|
import LabelButton from "../components/LabelButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../components/Shortcut.svelte";
|
||||||
|
|
||||||
const keyCombination = "Control+Shift+P";
|
const keyCombination = "Control+Shift+P";
|
||||||
function preview(): void {
|
function preview(): void {
|
|
@ -3,11 +3,11 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
|
import NoteEditor from "../editor/NoteEditor.svelte";
|
||||||
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
|
import type { NoteEditorAPI } from "../editor/NoteEditor.svelte";
|
||||||
|
|
||||||
const api: Partial<NoteEditorAPI> = {};
|
const api: Partial<NoteEditorAPI> = {};
|
||||||
let noteEditor: OldEditorAdapter;
|
let noteEditor: NoteEditor;
|
||||||
|
|
||||||
export let uiResolve: (api: NoteEditorAPI) => void;
|
export let uiResolve: (api: NoteEditorAPI) => void;
|
||||||
|
|
||||||
|
@ -16,4 +16,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OldEditorAdapter bind:this={noteEditor} {api} />
|
<NoteEditor bind:this={noteEditor} {api} />
|
||||||
|
|
|
@ -6,7 +6,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import Badge from "../components/Badge.svelte";
|
import Badge from "../components/Badge.svelte";
|
||||||
import { onMount } from "svelte";
|
import { onMount } from "svelte";
|
||||||
import { stickyOn, stickyOff } from "./icons";
|
import { stickyOn, stickyOff } from "./icons";
|
||||||
import { getEditorField } from "./EditorField.svelte";
|
import { context as editorFieldContext } from "./EditorField.svelte";
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { bridgeCommand } from "../lib/bridgecommand";
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
||||||
|
@ -15,7 +15,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
$: icon = active ? stickyOn : stickyOff;
|
$: icon = active ? stickyOn : stickyOff;
|
||||||
|
|
||||||
const editorField = getEditorField();
|
const editorField = editorFieldContext.get();
|
||||||
const keyCombination = "F9";
|
const keyCombination = "F9";
|
||||||
|
|
||||||
export let index: number;
|
export let index: number;
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
import "./legacy.css";
|
import "./legacy.css";
|
||||||
import "./editor-base.css";
|
import "./editor-base.css";
|
||||||
|
|
||||||
import "../lib/register-package";
|
import "../lib/runtime-require";
|
||||||
import "../sveltelib/export-runtime";
|
import "../sveltelib/export-runtime";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
|
|
|
@ -4,15 +4,33 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
|
|
||||||
export let buttons: string[];
|
export let buttons: string[];
|
||||||
|
|
||||||
|
const radius = "5px";
|
||||||
|
function getBorderRadius(index: number, length: number): string {
|
||||||
|
if (index === 0 && length === 1) {
|
||||||
|
return `--border-left-radius: ${radius}; --border-right-radius: ${radius}; `;
|
||||||
|
} else if (index === 0) {
|
||||||
|
return `--border-left-radius: ${radius}; --border-right-radius: 0; `;
|
||||||
|
} else if (index === length - 1) {
|
||||||
|
return `--border-left-radius: 0; --border-right-radius: ${radius}; `;
|
||||||
|
} else {
|
||||||
|
return "--border-left-radius: 0; --border-right-radius: 0; ";
|
||||||
|
}
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
{#each buttons as button}
|
{#each buttons as button, index}
|
||||||
<ButtonGroupItem>
|
<div style={getBorderRadius(index, buttons.length)}>
|
||||||
{@html button}
|
{@html button}
|
||||||
</ButtonGroupItem>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
div {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -10,8 +10,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { MatchResult } from "../../domlib/surround";
|
import { MatchResult } from "../../domlib/surround";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { getSurrounder } from "../surround";
|
import { getSurrounder } from "../surround";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import type { RichTextInputAPI } from "../rich-text-input";
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import { boldIcon } from "./icons";
|
import { boldIcon } from "./icons";
|
||||||
|
|
||||||
function matchBold(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
function matchBold(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
||||||
|
@ -42,11 +43,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return !htmlElement.hasAttribute("style") && element.className.length === 0;
|
return !htmlElement.hasAttribute("style") && element.className.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { focusInRichText, activeInput } = getNoteEditor();
|
const { focusedInput } = noteEditorContext.get();
|
||||||
|
|
||||||
$: input = $activeInput;
|
$: input = $focusedInput as RichTextInputAPI;
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
$: surrounder = disabled ? null : getSurrounder(input as RichTextInputAPI);
|
$: surrounder = disabled ? null : getSurrounder(input);
|
||||||
|
|
||||||
function updateStateFromActiveInput(): Promise<boolean> {
|
function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
return disabled ? Promise.resolve(false) : surrounder!.isSurrounded(matchBold);
|
return disabled ? Promise.resolve(false) : surrounder!.isSurrounded(matchBold);
|
||||||
|
|
|
@ -10,18 +10,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { wrapInternal } from "../../lib/wrap";
|
import { wrapInternal } from "../../lib/wrap";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import type { RichTextInputAPI } from "../rich-text-input";
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import { ellipseIcon } from "./icons";
|
import { ellipseIcon } from "./icons";
|
||||||
|
|
||||||
const noteEditor = getNoteEditor();
|
const { focusedInput, fields } = noteEditorContext.get();
|
||||||
const { focusInRichText, activeInput } = noteEditor;
|
|
||||||
|
|
||||||
const clozePattern = /\{\{c(\d+)::/gu;
|
const clozePattern = /\{\{c(\d+)::/gu;
|
||||||
function getCurrentHighestCloze(increment: boolean): number {
|
function getCurrentHighestCloze(increment: boolean): number {
|
||||||
let highest = 0;
|
let highest = 0;
|
||||||
|
|
||||||
for (const field of noteEditor.fields) {
|
for (const field of fields) {
|
||||||
const content = field.editingArea?.content;
|
const content = field.editingArea?.content;
|
||||||
const fieldHTML = content ? get(content) : "";
|
const fieldHTML = content ? get(content) : "";
|
||||||
|
|
||||||
|
@ -42,7 +42,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return Math.max(1, highest);
|
return Math.max(1, highest);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: richTextAPI = $activeInput as RichTextInputAPI;
|
$: richTextAPI = $focusedInput as RichTextInputAPI;
|
||||||
|
|
||||||
async function onCloze(event: KeyboardEvent | MouseEvent): Promise<void> {
|
async function onCloze(event: KeyboardEvent | MouseEvent): Promise<void> {
|
||||||
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||||
|
@ -50,7 +50,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
wrapInternal(richText, `{{c${highestCloze}::`, "}}", false);
|
wrapInternal(richText, `{{c${highestCloze}::`, "}}", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
|
|
||||||
const keyCombination = "Control+Alt?+Shift+C";
|
const keyCombination = "Control+Alt?+Shift+C";
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,17 +4,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import ColorPicker from "../../components/ColorPicker.svelte";
|
import ColorPicker from "../../components/ColorPicker.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithColorHelper from "./WithColorHelper.svelte";
|
import WithColorHelper from "./WithColorHelper.svelte";
|
||||||
|
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||||
|
import ButtonGroupItem, {
|
||||||
|
createProps,
|
||||||
|
updatePropsList,
|
||||||
|
setSlotHostContext,
|
||||||
|
} from "../../components/ButtonGroupItem.svelte";
|
||||||
|
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { execCommand } from "../helpers";
|
import { execCommand } from "../helpers";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons";
|
import { textColorIcon, highlightColorIcon, arrowIcon } from "./icons";
|
||||||
|
|
||||||
export let api = {};
|
export let api = {};
|
||||||
|
@ -35,87 +41,95 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
execCommand("backcolor", false, color);
|
execCommand("backcolor", false, color);
|
||||||
};
|
};
|
||||||
|
|
||||||
const { focusInRichText } = getNoteEditor();
|
const { focusedInput } = context.get();
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup>
|
||||||
<WithColorHelper color={textColor} let:colorHelperIcon let:setColor>
|
<DynamicallySlottable
|
||||||
<ButtonGroupItem>
|
slotHost={ButtonGroupItem}
|
||||||
<IconButton
|
{createProps}
|
||||||
tooltip="{tr.editingSetTextColor()} ({getPlatformString(
|
{updatePropsList}
|
||||||
forecolorKeyCombination,
|
{setSlotHostContext}
|
||||||
)})"
|
{api}
|
||||||
{disabled}
|
>
|
||||||
on:click={forecolorWrap}
|
<WithColorHelper color={textColor} let:colorHelperIcon let:setColor>
|
||||||
>
|
<ButtonGroupItem>
|
||||||
{@html textColorIcon}
|
<IconButton
|
||||||
{@html colorHelperIcon}
|
tooltip="{tr.editingSetTextColor()} ({getPlatformString(
|
||||||
</IconButton>
|
forecolorKeyCombination,
|
||||||
<Shortcut
|
)})"
|
||||||
keyCombination={forecolorKeyCombination}
|
{disabled}
|
||||||
on:action={forecolorWrap}
|
on:click={forecolorWrap}
|
||||||
/>
|
>
|
||||||
</ButtonGroupItem>
|
{@html textColorIcon}
|
||||||
|
{@html colorHelperIcon}
|
||||||
|
</IconButton>
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={forecolorKeyCombination}
|
||||||
|
on:action={forecolorWrap}
|
||||||
|
/>
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingChangeColor()} ({getPlatformString(
|
tooltip="{tr.editingChangeColor()} ({getPlatformString(
|
||||||
backcolorKeyCombination,
|
backcolorKeyCombination,
|
||||||
)})"
|
)})"
|
||||||
{disabled}
|
{disabled}
|
||||||
widthMultiplier={0.5}
|
widthMultiplier={0.5}
|
||||||
>
|
>
|
||||||
{@html arrowIcon}
|
{@html arrowIcon}
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
on:change={(event) => {
|
on:change={(event) => {
|
||||||
|
const textColor = setColor(event);
|
||||||
|
bridgeCommand(`lastTextColor:${textColor}`);
|
||||||
|
forecolorWrap = wrapWithForecolor(setColor(event));
|
||||||
|
forecolorWrap();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</IconButton>
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={backcolorKeyCombination}
|
||||||
|
on:action={(event) => {
|
||||||
const textColor = setColor(event);
|
const textColor = setColor(event);
|
||||||
bridgeCommand(`lastTextColor:${textColor}`);
|
bridgeCommand(`lastTextColor:${textColor}`);
|
||||||
forecolorWrap = wrapWithForecolor(setColor(event));
|
forecolorWrap = wrapWithForecolor(setColor(event));
|
||||||
forecolorWrap();
|
forecolorWrap();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</ButtonGroupItem>
|
||||||
<Shortcut
|
</WithColorHelper>
|
||||||
keyCombination={backcolorKeyCombination}
|
|
||||||
on:action={(event) => {
|
|
||||||
const textColor = setColor(event);
|
|
||||||
bridgeCommand(`lastTextColor:${textColor}`);
|
|
||||||
forecolorWrap = wrapWithForecolor(setColor(event));
|
|
||||||
forecolorWrap();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
</WithColorHelper>
|
|
||||||
|
|
||||||
<WithColorHelper color={highlightColor} let:colorHelperIcon let:setColor>
|
<WithColorHelper color={highlightColor} let:colorHelperIcon let:setColor>
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={tr.editingSetTextHighlightColor()}
|
tooltip={tr.editingSetTextHighlightColor()}
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={backcolorWrap}
|
on:click={backcolorWrap}
|
||||||
>
|
>
|
||||||
{@html highlightColorIcon}
|
{@html highlightColorIcon}
|
||||||
{@html colorHelperIcon}
|
{@html colorHelperIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip={tr.editingChangeColor()}
|
tooltip={tr.editingChangeColor()}
|
||||||
widthMultiplier={0.5}
|
widthMultiplier={0.5}
|
||||||
{disabled}
|
{disabled}
|
||||||
>
|
>
|
||||||
{@html arrowIcon}
|
{@html arrowIcon}
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
on:change={(event) => {
|
on:change={(event) => {
|
||||||
const highlightColor = setColor(event);
|
const highlightColor = setColor(event);
|
||||||
bridgeCommand(`lastHighlightColor:${highlightColor}`);
|
bridgeCommand(`lastHighlightColor:${highlightColor}`);
|
||||||
backcolorWrap = wrapWithBackcolor(highlightColor);
|
backcolorWrap = wrapWithBackcolor(highlightColor);
|
||||||
backcolorWrap();
|
backcolorWrap();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
</WithColorHelper>
|
</WithColorHelper>
|
||||||
|
</DynamicallySlottable>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -8,7 +8,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import WithState from "../../components/WithState.svelte";
|
import WithState from "../../components/WithState.svelte";
|
||||||
|
|
||||||
import { execCommand, queryCommandState } from "../helpers";
|
import { execCommand, queryCommandState } from "../helpers";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
|
|
||||||
export let key: string;
|
export let key: string;
|
||||||
export let tooltip: string;
|
export let tooltip: string;
|
||||||
|
@ -17,13 +18,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let withoutShortcut = false;
|
export let withoutShortcut = false;
|
||||||
export let withoutState = false;
|
export let withoutState = false;
|
||||||
|
|
||||||
const { focusInRichText } = getNoteEditor();
|
const { focusedInput } = noteEditorContext.get();
|
||||||
|
|
||||||
function action() {
|
function action() {
|
||||||
execCommand(key);
|
execCommand(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if withoutState}
|
{#if withoutState}
|
||||||
|
|
|
@ -4,8 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import { updateAllState, resetAllState } from "../../components/WithState.svelte";
|
import { updateAllState, resetAllState } from "../../components/WithState.svelte";
|
||||||
import type { ButtonGroupAPI } from "../../components/ButtonGroup.svelte";
|
import type { DefaultSlotInterface } from "../../sveltelib/dynamic-slotting";
|
||||||
import type { ButtonToolbarAPI } from "../../components/ButtonToolbar.svelte";
|
|
||||||
|
|
||||||
export function updateActiveButtons(event: Event) {
|
export function updateActiveButtons(event: Event) {
|
||||||
updateAllState(event);
|
updateAllState(event);
|
||||||
|
@ -16,31 +15,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditorToolbarAPI {
|
export interface EditorToolbarAPI {
|
||||||
toolbar: ButtonToolbarAPI;
|
toolbar: DefaultSlotInterface;
|
||||||
notetypeButtons: ButtonGroupAPI;
|
notetypeButtons: DefaultSlotInterface;
|
||||||
formatInlineButtons: ButtonGroupAPI;
|
formatInlineButtons: DefaultSlotInterface;
|
||||||
formatBlockButtons: ButtonGroupAPI;
|
formatBlockButtons: DefaultSlotInterface;
|
||||||
colorButtons: ButtonGroupAPI;
|
colorButtons: DefaultSlotInterface;
|
||||||
templateButtons: ButtonGroupAPI;
|
templateButtons: DefaultSlotInterface;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Our dynamic components */
|
/* Our dynamic components */
|
||||||
import AddonButtons from "./AddonButtons.svelte";
|
import AddonButtons from "./AddonButtons.svelte";
|
||||||
import PreviewButton, { togglePreviewButtonState } from "./PreviewButton.svelte";
|
|
||||||
|
|
||||||
export const editorToolbar = {
|
export const editorToolbar = {
|
||||||
AddonButtons,
|
AddonButtons,
|
||||||
PreviewButton,
|
|
||||||
togglePreviewButtonState,
|
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import StickyContainer from "../../components/StickyContainer.svelte";
|
import StickyContainer from "../../components/StickyContainer.svelte";
|
||||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||||
|
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||||
import Item from "../../components/Item.svelte";
|
import Item from "../../components/Item.svelte";
|
||||||
|
|
||||||
import NoteTypeButtons from "./NoteTypeButtons.svelte";
|
import NotetypeButtons from "./NotetypeButtons.svelte";
|
||||||
import FormatInlineButtons from "./FormatInlineButtons.svelte";
|
import FormatInlineButtons from "./FormatInlineButtons.svelte";
|
||||||
import FormatBlockButtons from "./FormatBlockButtons.svelte";
|
import FormatBlockButtons from "./FormatBlockButtons.svelte";
|
||||||
import ColorButtons from "./ColorButtons.svelte";
|
import ColorButtons from "./ColorButtons.svelte";
|
||||||
|
@ -72,25 +69,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<StickyContainer --gutter-block="0.1rem" --sticky-borders="0 0 1px">
|
<StickyContainer --gutter-block="0.1rem" --sticky-borders="0 0 1px">
|
||||||
<ButtonToolbar {size} {wrap} api={toolbar}>
|
<ButtonToolbar {size} {wrap}>
|
||||||
<Item id="notetype">
|
<DynamicallySlottable slotHost={Item} api={toolbar}>
|
||||||
<NoteTypeButtons api={notetypeButtons} />
|
<Item id="notetype">
|
||||||
</Item>
|
<NotetypeButtons api={notetypeButtons}>
|
||||||
|
<slot name="notetypeButtons" />
|
||||||
|
</NotetypeButtons>
|
||||||
|
</Item>
|
||||||
|
|
||||||
<Item id="inlineFormatting">
|
<Item id="inlineFormatting">
|
||||||
<FormatInlineButtons api={formatInlineButtons} />
|
<FormatInlineButtons api={formatInlineButtons} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item id="blockFormatting">
|
<Item id="blockFormatting">
|
||||||
<FormatBlockButtons api={formatBlockButtons} />
|
<FormatBlockButtons api={formatBlockButtons} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item id="color">
|
<Item id="color">
|
||||||
<ColorButtons {textColor} {highlightColor} api={colorButtons} />
|
<ColorButtons {textColor} {highlightColor} api={colorButtons} />
|
||||||
</Item>
|
</Item>
|
||||||
|
|
||||||
<Item id="template">
|
<Item id="template">
|
||||||
<TemplateButtons api={templateButtons} />
|
<TemplateButtons api={templateButtons} />
|
||||||
</Item>
|
</Item>
|
||||||
|
</DynamicallySlottable>
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
</StickyContainer>
|
</StickyContainer>
|
||||||
|
|
|
@ -4,13 +4,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||||
import Item from "../../components/Item.svelte";
|
import Item from "../../components/Item.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||||
import CommandIconButton from "./CommandIconButton.svelte";
|
import CommandIconButton from "./CommandIconButton.svelte";
|
||||||
|
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||||
|
import ButtonGroupItem, {
|
||||||
|
createProps,
|
||||||
|
updatePropsList,
|
||||||
|
setSlotHostContext,
|
||||||
|
} from "../../components/ButtonGroupItem.svelte";
|
||||||
|
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { getListItem } from "../../lib/dom";
|
import { getListItem } from "../../lib/dom";
|
||||||
|
@ -27,7 +32,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
indentIcon,
|
indentIcon,
|
||||||
outdentIcon,
|
outdentIcon,
|
||||||
} from "./icons";
|
} from "./icons";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
|
|
||||||
export let api = {};
|
export let api = {};
|
||||||
|
|
||||||
|
@ -49,86 +55,87 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const { focusInRichText } = getNoteEditor();
|
const { focusedInput } = context.get();
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup>
|
||||||
<ButtonGroupItem>
|
<DynamicallySlottable
|
||||||
<CommandIconButton
|
slotHost={ButtonGroupItem}
|
||||||
key="insertUnorderedList"
|
{createProps}
|
||||||
tooltip={tr.editingUnorderedList()}
|
{updatePropsList}
|
||||||
shortcut="Control+,">{@html ulIcon}</CommandIconButton
|
{setSlotHostContext}
|
||||||
>
|
{api}
|
||||||
</ButtonGroupItem>
|
>
|
||||||
|
<ButtonGroupItem>
|
||||||
<ButtonGroupItem>
|
<CommandIconButton
|
||||||
<CommandIconButton
|
key="insertUnorderedList"
|
||||||
key="insertOrderedList"
|
tooltip={tr.editingUnorderedList()}
|
||||||
tooltip={tr.editingOrderedList()}
|
shortcut="Control+,">{@html ulIcon}</CommandIconButton
|
||||||
shortcut="Control+.">{@html olIcon}</CommandIconButton
|
|
||||||
>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<WithDropdown let:createDropdown>
|
|
||||||
<IconButton
|
|
||||||
{disabled}
|
|
||||||
on:mount={(event) => createDropdown(event.detail.button)}
|
|
||||||
>
|
>
|
||||||
{@html listOptionsIcon}
|
</ButtonGroupItem>
|
||||||
</IconButton>
|
|
||||||
|
|
||||||
<ButtonDropdown>
|
<ButtonGroupItem>
|
||||||
<Item id="justify">
|
<CommandIconButton
|
||||||
<ButtonGroup>
|
key="insertOrderedList"
|
||||||
<ButtonGroupItem>
|
tooltip={tr.editingOrderedList()}
|
||||||
|
shortcut="Control+.">{@html olIcon}</CommandIconButton
|
||||||
|
>
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
|
<ButtonGroupItem>
|
||||||
|
<WithDropdown let:createDropdown>
|
||||||
|
<IconButton
|
||||||
|
{disabled}
|
||||||
|
on:mount={(event) => createDropdown(event.detail.button)}
|
||||||
|
>
|
||||||
|
{@html listOptionsIcon}
|
||||||
|
</IconButton>
|
||||||
|
|
||||||
|
<ButtonDropdown>
|
||||||
|
<Item id="justify">
|
||||||
|
<ButtonGroup>
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="justifyLeft"
|
key="justifyLeft"
|
||||||
tooltip={tr.editingAlignLeft()}
|
tooltip={tr.editingAlignLeft()}
|
||||||
withoutShortcut
|
withoutShortcut
|
||||||
|
--border-left-radius="5px"
|
||||||
>{@html justifyLeftIcon}</CommandIconButton
|
>{@html justifyLeftIcon}</CommandIconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="justifyCenter"
|
key="justifyCenter"
|
||||||
tooltip={tr.editingCenter()}
|
tooltip={tr.editingCenter()}
|
||||||
withoutShortcut
|
withoutShortcut
|
||||||
>{@html justifyCenterIcon}</CommandIconButton
|
>{@html justifyCenterIcon}</CommandIconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="justifyRight"
|
key="justifyRight"
|
||||||
tooltip={tr.editingAlignRight()}
|
tooltip={tr.editingAlignRight()}
|
||||||
withoutShortcut
|
withoutShortcut
|
||||||
>{@html justifyRightIcon}</CommandIconButton
|
>{@html justifyRightIcon}</CommandIconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="justifyFull"
|
key="justifyFull"
|
||||||
tooltip={tr.editingJustify()}
|
tooltip={tr.editingJustify()}
|
||||||
withoutShortcut
|
withoutShortcut
|
||||||
|
--border-right-radius="5px"
|
||||||
>{@html justifyFullIcon}</CommandIconButton
|
>{@html justifyFullIcon}</CommandIconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
</Item>
|
||||||
</Item>
|
|
||||||
|
|
||||||
<Item id="indentation">
|
<Item id="indentation">
|
||||||
<ButtonGroup>
|
<ButtonGroup>
|
||||||
<ButtonGroupItem>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingOutdent()} ({getPlatformString(
|
tooltip="{tr.editingOutdent()} ({getPlatformString(
|
||||||
outdentKeyCombination,
|
outdentKeyCombination,
|
||||||
)})"
|
)})"
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={outdentListItem}
|
on:click={outdentListItem}
|
||||||
|
--border-left-radius="5px"
|
||||||
>
|
>
|
||||||
{@html outdentIcon}
|
{@html outdentIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -137,15 +144,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
keyCombination={outdentKeyCombination}
|
keyCombination={outdentKeyCombination}
|
||||||
on:action={outdentListItem}
|
on:action={outdentListItem}
|
||||||
/>
|
/>
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingIndent()} ({getPlatformString(
|
tooltip="{tr.editingIndent()} ({getPlatformString(
|
||||||
indentKeyCombination,
|
indentKeyCombination,
|
||||||
)})"
|
)})"
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={indentListItem}
|
on:click={indentListItem}
|
||||||
|
--border-right-radius="5px"
|
||||||
>
|
>
|
||||||
{@html indentIcon}
|
{@html indentIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
@ -154,10 +160,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
keyCombination={indentKeyCombination}
|
keyCombination={indentKeyCombination}
|
||||||
on:action={indentListItem}
|
on:action={indentListItem}
|
||||||
/>
|
/>
|
||||||
</ButtonGroupItem>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
</Item>
|
||||||
</Item>
|
</ButtonDropdown>
|
||||||
</ButtonDropdown>
|
</WithDropdown>
|
||||||
</WithDropdown>
|
</ButtonGroupItem>
|
||||||
</ButtonGroupItem>
|
</DynamicallySlottable>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -4,11 +4,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import CommandIconButton from "./CommandIconButton.svelte";
|
import CommandIconButton from "./CommandIconButton.svelte";
|
||||||
import BoldButton from "./BoldButton.svelte";
|
import BoldButton from "./BoldButton.svelte";
|
||||||
import ItalicButton from "./ItalicButton.svelte";
|
import ItalicButton from "./ItalicButton.svelte";
|
||||||
import UnderlineButton from "./UnderlineButton.svelte";
|
import UnderlineButton from "./UnderlineButton.svelte";
|
||||||
|
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||||
|
import ButtonGroupItem, {
|
||||||
|
createProps,
|
||||||
|
updatePropsList,
|
||||||
|
setSlotHostContext,
|
||||||
|
} from "../../components/ButtonGroupItem.svelte";
|
||||||
|
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { superscriptIcon, subscriptIcon, eraserIcon } from "./icons";
|
import { superscriptIcon, subscriptIcon, eraserIcon } from "./icons";
|
||||||
|
@ -16,41 +21,50 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let api = {};
|
export let api = {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup>
|
||||||
<ButtonGroupItem>
|
<DynamicallySlottable
|
||||||
<BoldButton />
|
slotHost={ButtonGroupItem}
|
||||||
</ButtonGroupItem>
|
{createProps}
|
||||||
|
{updatePropsList}
|
||||||
|
{setSlotHostContext}
|
||||||
|
{api}
|
||||||
|
>
|
||||||
|
<ButtonGroupItem>
|
||||||
|
<BoldButton />
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<ItalicButton />
|
<ItalicButton />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<UnderlineButton />
|
<UnderlineButton />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="superscript"
|
key="superscript"
|
||||||
shortcut="Control+="
|
shortcut="Control+="
|
||||||
tooltip={tr.editingSuperscript()}>{@html superscriptIcon}</CommandIconButton
|
tooltip={tr.editingSuperscript()}
|
||||||
>
|
>{@html superscriptIcon}</CommandIconButton
|
||||||
</ButtonGroupItem>
|
>
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="subscript"
|
key="subscript"
|
||||||
shortcut="Control+Shift+="
|
shortcut="Control+Shift+="
|
||||||
tooltip={tr.editingSubscript()}>{@html subscriptIcon}</CommandIconButton
|
tooltip={tr.editingSubscript()}>{@html subscriptIcon}</CommandIconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<CommandIconButton
|
<CommandIconButton
|
||||||
key="removeFormat"
|
key="removeFormat"
|
||||||
shortcut="Control+R"
|
shortcut="Control+R"
|
||||||
tooltip={tr.editingRemoveFormatting()}
|
tooltip={tr.editingRemoveFormatting()}
|
||||||
withoutState>{@html eraserIcon}</CommandIconButton
|
withoutState>{@html eraserIcon}</CommandIconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
</DynamicallySlottable>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -11,8 +11,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { getSurrounder } from "../surround";
|
import { getSurrounder } from "../surround";
|
||||||
import { italicIcon } from "./icons";
|
import { italicIcon } from "./icons";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import type { RichTextInputAPI } from "../rich-text-input";
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
|
|
||||||
function matchItalic(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
function matchItalic(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
||||||
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
|
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
|
||||||
|
@ -41,14 +42,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return !htmlElement.hasAttribute("style") && element.className.length === 0;
|
return !htmlElement.hasAttribute("style") && element.className.length === 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { focusInRichText, activeInput } = getNoteEditor();
|
const { focusedInput } = noteEditorContext.get();
|
||||||
|
|
||||||
$: input = $activeInput;
|
$: input = $focusedInput as RichTextInputAPI;
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
$: surrounder = disabled ? null : getSurrounder(input as RichTextInputAPI);
|
$: surrounder = disabled ? null : getSurrounder(input);
|
||||||
|
|
||||||
function updateStateFromActiveInput(): Promise<boolean> {
|
function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
return !input || input.name === "plain-text"
|
return disabled
|
||||||
? Promise.resolve(false)
|
? Promise.resolve(false)
|
||||||
: surrounder!.isSurrounded(matchItalic);
|
: surrounder!.isSurrounded(matchItalic);
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,11 +13,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { wrapInternal } from "../../lib/wrap";
|
import { wrapInternal } from "../../lib/wrap";
|
||||||
import { functionIcon } from "./icons";
|
import { functionIcon } from "./icons";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import type { RichTextInputAPI } from "../rich-text-input";
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
|
|
||||||
const { activeInput, focusInRichText } = getNoteEditor();
|
const { focusedInput } = noteEditorContext.get();
|
||||||
$: richTextAPI = $activeInput as RichTextInputAPI;
|
$: richTextAPI = $focusedInput as RichTextInputAPI;
|
||||||
|
|
||||||
async function surround(front: string, back: string): Promise<void> {
|
async function surround(front: string, back: string): Promise<void> {
|
||||||
const element = await richTextAPI.element;
|
const element = await richTextAPI.element;
|
||||||
|
@ -59,7 +60,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
[onLatexMathEnv, "Control+T, M", tr.editingLatexMathEnv()],
|
[onLatexMathEnv, "Control+T, M", tr.editingLatexMathEnv()],
|
||||||
];
|
];
|
||||||
|
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<WithDropdown let:createDropdown>
|
<WithDropdown let:createDropdown>
|
||||||
|
|
|
@ -1,41 +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 * as tr from "../../lib/ftl";
|
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
|
||||||
|
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import LabelButton from "../../components/LabelButton.svelte";
|
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
|
||||||
|
|
||||||
export let api = {};
|
|
||||||
|
|
||||||
const keyCombination = "Control+L";
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<LabelButton
|
|
||||||
tooltip={tr.editingCustomizeFields()}
|
|
||||||
on:click={() => bridgeCommand("fields")}
|
|
||||||
>
|
|
||||||
{tr.editingFields()}...
|
|
||||||
</LabelButton>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
|
||||||
<LabelButton
|
|
||||||
tooltip="{tr.editingCustomizeCardTemplates()} ({getPlatformString(
|
|
||||||
keyCombination,
|
|
||||||
)})"
|
|
||||||
on:click={() => bridgeCommand("cards")}
|
|
||||||
>
|
|
||||||
{tr.editingCards()}...
|
|
||||||
</LabelButton>
|
|
||||||
<Shortcut {keyCombination} on:action={() => bridgeCommand("cards")} />
|
|
||||||
</ButtonGroupItem>
|
|
||||||
</ButtonGroup>
|
|
56
ts/editor/editor-toolbar/NotetypeButtons.svelte
Normal file
56
ts/editor/editor-toolbar/NotetypeButtons.svelte
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
<!--
|
||||||
|
Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
-->
|
||||||
|
<script lang="ts">
|
||||||
|
import * as tr from "../../lib/ftl";
|
||||||
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
|
|
||||||
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
|
import LabelButton from "../../components/LabelButton.svelte";
|
||||||
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
|
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||||
|
import ButtonGroupItem, {
|
||||||
|
createProps,
|
||||||
|
updatePropsList,
|
||||||
|
setSlotHostContext,
|
||||||
|
} from "../../components/ButtonGroupItem.svelte";
|
||||||
|
|
||||||
|
export let api = {};
|
||||||
|
|
||||||
|
const keyCombination = "Control+L";
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<ButtonGroup>
|
||||||
|
<DynamicallySlottable
|
||||||
|
slotHost={ButtonGroupItem}
|
||||||
|
{createProps}
|
||||||
|
{updatePropsList}
|
||||||
|
{setSlotHostContext}
|
||||||
|
{api}
|
||||||
|
>
|
||||||
|
<ButtonGroupItem>
|
||||||
|
<LabelButton
|
||||||
|
tooltip={tr.editingCustomizeFields()}
|
||||||
|
on:click={() => bridgeCommand("fields")}
|
||||||
|
>
|
||||||
|
{tr.editingFields()}...
|
||||||
|
</LabelButton>
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
|
<ButtonGroupItem>
|
||||||
|
<LabelButton
|
||||||
|
tooltip="{tr.editingCustomizeCardTemplates()} ({getPlatformString(
|
||||||
|
keyCombination,
|
||||||
|
)})"
|
||||||
|
on:click={() => bridgeCommand("cards")}
|
||||||
|
>
|
||||||
|
{tr.editingCards()}...
|
||||||
|
</LabelButton>
|
||||||
|
<Shortcut {keyCombination} on:action={() => bridgeCommand("cards")} />
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
|
<slot />
|
||||||
|
</DynamicallySlottable>
|
||||||
|
</ButtonGroup>
|
|
@ -3,68 +3,87 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import * as tr from "../../lib/ftl";
|
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||||
|
import ButtonGroupItem, {
|
||||||
|
createProps,
|
||||||
|
updatePropsList,
|
||||||
|
setSlotHostContext,
|
||||||
|
} from "../../components/ButtonGroupItem.svelte";
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import ClozeButton from "./ClozeButton.svelte";
|
import ClozeButton from "./ClozeButton.svelte";
|
||||||
import LatexButton from "./LatexButton.svelte";
|
import LatexButton from "./LatexButton.svelte";
|
||||||
|
|
||||||
|
import * as tr from "../../lib/ftl";
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import { paperclipIcon, micIcon } from "./icons";
|
import { paperclipIcon, micIcon } from "./icons";
|
||||||
|
|
||||||
export let api = {};
|
const { focusedInput } = context.get();
|
||||||
const { focusInRichText } = getNoteEditor();
|
|
||||||
|
|
||||||
const attachmentKeyCombination = "F3";
|
const attachmentKeyCombination = "F7";
|
||||||
function onAttachment(): void {
|
function onAttachment(): void {
|
||||||
bridgeCommand("attach");
|
bridgeCommand("attach");
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordKeyCombination = "F5";
|
const recordKeyCombination = "F8";
|
||||||
function onRecord(): void {
|
function onRecord(): void {
|
||||||
bridgeCommand("record");
|
bridgeCommand("record");
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
|
|
||||||
|
export let api = {};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup>
|
||||||
<ButtonGroupItem>
|
<DynamicallySlottable
|
||||||
<IconButton
|
slotHost={ButtonGroupItem}
|
||||||
tooltip="{tr.editingAttachPicturesaudiovideo()} ({getPlatformString(
|
{createProps}
|
||||||
attachmentKeyCombination,
|
{updatePropsList}
|
||||||
)})"
|
{setSlotHostContext}
|
||||||
iconSize={70}
|
{api}
|
||||||
{disabled}
|
>
|
||||||
on:click={onAttachment}
|
<ButtonGroupItem>
|
||||||
>
|
<IconButton
|
||||||
{@html paperclipIcon}
|
tooltip="{tr.editingAttachPicturesaudiovideo()} ({getPlatformString(
|
||||||
</IconButton>
|
attachmentKeyCombination,
|
||||||
<Shortcut keyCombination={attachmentKeyCombination} on:action={onAttachment} />
|
)})"
|
||||||
</ButtonGroupItem>
|
iconSize={70}
|
||||||
|
{disabled}
|
||||||
|
on:click={onAttachment}
|
||||||
|
>
|
||||||
|
{@html paperclipIcon}
|
||||||
|
</IconButton>
|
||||||
|
<Shortcut
|
||||||
|
keyCombination={attachmentKeyCombination}
|
||||||
|
on:action={onAttachment}
|
||||||
|
/>
|
||||||
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingRecordAudio()} ({getPlatformString(
|
tooltip="{tr.editingRecordAudio()} ({getPlatformString(
|
||||||
recordKeyCombination,
|
recordKeyCombination,
|
||||||
)})"
|
)})"
|
||||||
iconSize={70}
|
iconSize={70}
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={onRecord}
|
on:click={onRecord}
|
||||||
>
|
>
|
||||||
{@html micIcon}
|
{@html micIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Shortcut keyCombination={recordKeyCombination} on:action={onRecord} />
|
<Shortcut keyCombination={recordKeyCombination} on:action={onRecord} />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem id="cloze">
|
<ButtonGroupItem id="cloze">
|
||||||
<ClozeButton />
|
<ClozeButton />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<LatexButton />
|
<LatexButton />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
</DynamicallySlottable>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -10,9 +10,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { MatchResult } from "../../domlib/surround";
|
import { MatchResult } from "../../domlib/surround";
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { getSurrounder } from "../surround";
|
import { getSurrounder } from "../surround";
|
||||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
|
||||||
import type { RichTextInputAPI } from "../rich-text-input";
|
|
||||||
import { underlineIcon } from "./icons";
|
import { underlineIcon } from "./icons";
|
||||||
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
|
|
||||||
function matchUnderline(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
function matchUnderline(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
||||||
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
|
if (!(element instanceof HTMLElement) && !(element instanceof SVGElement)) {
|
||||||
|
@ -26,14 +27,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return MatchResult.NO_MATCH;
|
return MatchResult.NO_MATCH;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { focusInRichText, activeInput } = getNoteEditor();
|
const { focusedInput } = context.get();
|
||||||
|
|
||||||
$: input = $activeInput;
|
$: input = $focusedInput as RichTextInputAPI;
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !editingInputIsRichText($focusedInput);
|
||||||
$: surrounder = disabled ? null : getSurrounder(input as RichTextInputAPI);
|
$: surrounder = disabled ? null : getSurrounder(input);
|
||||||
|
|
||||||
function updateStateFromActiveInput(): Promise<boolean> {
|
function updateStateFromActiveInput(): Promise<boolean> {
|
||||||
return !input || input.name === "plain-text"
|
return disabled
|
||||||
? Promise.resolve(false)
|
? Promise.resolve(false)
|
||||||
: surrounder!.isSurrounded(matchUnderline);
|
: surrounder!.isSurrounded(matchUnderline);
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,7 +9,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { directionKey } from "../../lib/context-keys";
|
import { directionKey } from "../../lib/context-keys";
|
||||||
|
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
|
|
||||||
import { createEventDispatcher } from "svelte";
|
import { createEventDispatcher } from "svelte";
|
||||||
|
@ -27,44 +26,40 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup size={1.6} wrap={false}>
|
<ButtonGroup size={1.6} wrap={false}>
|
||||||
<ButtonGroupItem>
|
<IconButton
|
||||||
<IconButton
|
tooltip={tr.editingFloatLeft()}
|
||||||
tooltip={tr.editingFloatLeft()}
|
active={image.style.float === "left"}
|
||||||
active={image.style.float === "left"}
|
flipX={$direction === "rtl"}
|
||||||
flipX={$direction === "rtl"}
|
on:click={() => {
|
||||||
on:click={() => {
|
image.style.float = "left";
|
||||||
image.style.float = "left";
|
setTimeout(() => dispatch("update"));
|
||||||
setTimeout(() => dispatch("update"));
|
}}
|
||||||
}}>{@html inlineStartIcon}</IconButton
|
--border-left-radius="5px">{@html inlineStartIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<IconButton
|
||||||
<IconButton
|
tooltip={tr.editingFloatNone()}
|
||||||
tooltip={tr.editingFloatNone()}
|
active={image.style.float === "" || image.style.float === "none"}
|
||||||
active={image.style.float === "" || image.style.float === "none"}
|
flipX={$direction === "rtl"}
|
||||||
flipX={$direction === "rtl"}
|
on:click={() => {
|
||||||
on:click={() => {
|
image.style.removeProperty("float");
|
||||||
image.style.removeProperty("float");
|
|
||||||
|
|
||||||
if (image.getAttribute("style")?.length === 0) {
|
if (image.getAttribute("style")?.length === 0) {
|
||||||
image.removeAttribute("style");
|
image.removeAttribute("style");
|
||||||
}
|
}
|
||||||
|
|
||||||
setTimeout(() => dispatch("update"));
|
setTimeout(() => dispatch("update"));
|
||||||
}}>{@html floatNoneIcon}</IconButton
|
}}>{@html floatNoneIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<IconButton
|
||||||
<IconButton
|
tooltip={tr.editingFloatRight()}
|
||||||
tooltip={tr.editingFloatRight()}
|
active={image.style.float === "right"}
|
||||||
active={image.style.float === "right"}
|
flipX={$direction === "rtl"}
|
||||||
flipX={$direction === "rtl"}
|
on:click={() => {
|
||||||
on:click={() => {
|
image.style.float = "right";
|
||||||
image.style.float = "right";
|
setTimeout(() => dispatch("update"));
|
||||||
setTimeout(() => dispatch("update"));
|
}}
|
||||||
}}>{@html inlineEndIcon}</IconButton
|
--border-right-radius="5px">{@html inlineEndIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -6,19 +6,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { tick, onDestroy } from "svelte";
|
import { tick, onDestroy } from "svelte";
|
||||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||||
import Item from "../../components/Item.svelte";
|
|
||||||
|
|
||||||
import HandleBackground from "../HandleBackground.svelte";
|
import HandleBackground from "../HandleBackground.svelte";
|
||||||
import HandleSelection from "../HandleSelection.svelte";
|
import HandleSelection from "../HandleSelection.svelte";
|
||||||
import HandleControl from "../HandleControl.svelte";
|
import HandleControl from "../HandleControl.svelte";
|
||||||
import HandleLabel from "../HandleLabel.svelte";
|
import HandleLabel from "../HandleLabel.svelte";
|
||||||
import { getRichTextInput } from "../rich-text-input";
|
import { context } from "../rich-text-input";
|
||||||
|
|
||||||
import WithImageConstrained from "./WithImageConstrained.svelte";
|
import WithImageConstrained from "./WithImageConstrained.svelte";
|
||||||
import FloatButtons from "./FloatButtons.svelte";
|
import FloatButtons from "./FloatButtons.svelte";
|
||||||
import SizeSelect from "./SizeSelect.svelte";
|
import SizeSelect from "./SizeSelect.svelte";
|
||||||
|
|
||||||
const { container, styles } = getRichTextInput();
|
const { container, styles } = context.get();
|
||||||
|
|
||||||
const sheetPromise = styles
|
const sheetPromise = styles
|
||||||
.addStyleTag("imageOverlay")
|
.addStyleTag("imageOverlay")
|
||||||
|
@ -233,15 +232,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
/>
|
/>
|
||||||
</HandleSelection>
|
</HandleSelection>
|
||||||
<ButtonDropdown on:click={updateSizesWithDimensions}>
|
<ButtonDropdown on:click={updateSizesWithDimensions}>
|
||||||
<Item>
|
<FloatButtons
|
||||||
<FloatButtons
|
image={activeImage}
|
||||||
image={activeImage}
|
on:update={dropdownObject.update}
|
||||||
on:update={dropdownObject.update}
|
/>
|
||||||
/>
|
<SizeSelect {active} on:click={toggleActualSize} />
|
||||||
</Item>
|
|
||||||
<Item>
|
|
||||||
<SizeSelect {active} on:click={toggleActualSize} />
|
|
||||||
</Item>
|
|
||||||
</ButtonDropdown>
|
</ButtonDropdown>
|
||||||
{/if}
|
{/if}
|
||||||
</WithImageConstrained>
|
</WithImageConstrained>
|
||||||
|
|
|
@ -9,7 +9,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { directionKey } from "../../lib/context-keys";
|
import { directionKey } from "../../lib/context-keys";
|
||||||
|
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
|
|
||||||
import { sizeActual, sizeMinimized } from "./icons";
|
import { sizeActual, sizeMinimized } from "./icons";
|
||||||
|
@ -22,12 +21,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup size={1.6}>
|
<ButtonGroup size={1.6}>
|
||||||
<ButtonGroupItem>
|
<IconButton
|
||||||
<IconButton
|
{active}
|
||||||
{active}
|
flipX={$direction === "rtl"}
|
||||||
flipX={$direction === "rtl"}
|
tooltip={tr.editingActualSize()}
|
||||||
tooltip={tr.editingActualSize()}
|
on:click
|
||||||
on:click>{@html icon}</IconButton
|
--border-left-radius="5px"
|
||||||
>
|
--border-right-radius="5px">{@html icon}</IconButton
|
||||||
</ButtonGroupItem>
|
>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import BrowserEditor from "./BrowserEditor.svelte";
|
import BrowserEditor from "./BrowserEditor.svelte";
|
||||||
import { editorModules } from "./base";
|
import { editorModules } from "./base";
|
||||||
import { promiseWithResolver } from "../lib/promise";
|
|
||||||
import { globalExport } from "../lib/globals";
|
import { globalExport } from "../lib/globals";
|
||||||
import { setupI18n } from "../lib/i18n";
|
import { setupI18n } from "../lib/i18n";
|
||||||
|
import { uiResolve } from "../lib/ui";
|
||||||
const [uiPromise, uiResolve] = promiseWithResolver();
|
|
||||||
|
|
||||||
async function setupBrowserEditor(): Promise<void> {
|
async function setupBrowserEditor(): Promise<void> {
|
||||||
await setupI18n({ modules: editorModules });
|
await setupI18n({ modules: editorModules });
|
||||||
|
@ -20,9 +18,4 @@ async function setupBrowserEditor(): Promise<void> {
|
||||||
setupBrowserEditor();
|
setupBrowserEditor();
|
||||||
|
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
|
globalExport(base);
|
||||||
globalExport({
|
|
||||||
...base,
|
|
||||||
uiPromise,
|
|
||||||
noteEditorPromise: uiPromise,
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import NoteCreator from "./NoteCreator.svelte";
|
import NoteCreator from "./NoteCreator.svelte";
|
||||||
import { editorModules } from "./base";
|
import { editorModules } from "./base";
|
||||||
import { promiseWithResolver } from "../lib/promise";
|
|
||||||
import { globalExport } from "../lib/globals";
|
import { globalExport } from "../lib/globals";
|
||||||
import { setupI18n } from "../lib/i18n";
|
import { setupI18n } from "../lib/i18n";
|
||||||
|
import { uiResolve } from "../lib/ui";
|
||||||
const [uiPromise, uiResolve] = promiseWithResolver();
|
|
||||||
|
|
||||||
async function setupNoteCreator(): Promise<void> {
|
async function setupNoteCreator(): Promise<void> {
|
||||||
await setupI18n({ modules: editorModules });
|
await setupI18n({ modules: editorModules });
|
||||||
|
@ -20,9 +18,4 @@ async function setupNoteCreator(): Promise<void> {
|
||||||
setupNoteCreator();
|
setupNoteCreator();
|
||||||
|
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
|
globalExport(base);
|
||||||
globalExport({
|
|
||||||
...base,
|
|
||||||
uiPromise,
|
|
||||||
noteEditorPromise: uiPromise,
|
|
||||||
});
|
|
||||||
|
|
|
@ -2,11 +2,9 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import ReviewerEditor from "./ReviewerEditor.svelte";
|
import ReviewerEditor from "./ReviewerEditor.svelte";
|
||||||
import { editorModules } from "./base";
|
import { editorModules } from "./base";
|
||||||
import { promiseWithResolver } from "../lib/promise";
|
|
||||||
import { globalExport } from "../lib/globals";
|
import { globalExport } from "../lib/globals";
|
||||||
import { setupI18n } from "../lib/i18n";
|
import { setupI18n } from "../lib/i18n";
|
||||||
|
import { uiResolve } from "../lib/ui";
|
||||||
const [uiPromise, uiResolve] = promiseWithResolver();
|
|
||||||
|
|
||||||
async function setupReviewerEditor(): Promise<void> {
|
async function setupReviewerEditor(): Promise<void> {
|
||||||
await setupI18n({ modules: editorModules });
|
await setupI18n({ modules: editorModules });
|
||||||
|
@ -20,9 +18,4 @@ async function setupReviewerEditor(): Promise<void> {
|
||||||
setupReviewerEditor();
|
setupReviewerEditor();
|
||||||
|
|
||||||
import * as base from "./base";
|
import * as base from "./base";
|
||||||
|
globalExport(base);
|
||||||
globalExport({
|
|
||||||
...base,
|
|
||||||
uiPromise,
|
|
||||||
noteEditorPromise: uiPromise,
|
|
||||||
});
|
|
||||||
|
|
|
@ -4,9 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||||
import Item from "../../components/Item.svelte";
|
|
||||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { inlineIcon, blockIcon, deleteIcon } from "./icons";
|
import { inlineIcon, blockIcon, deleteIcon } from "./icons";
|
||||||
|
@ -25,42 +23,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonToolbar size={1.6} wrap={false}>
|
<ButtonToolbar size={1.6} wrap={false}>
|
||||||
<Item>
|
<ButtonGroup>
|
||||||
<ButtonGroup>
|
<IconButton
|
||||||
<ButtonGroupItem>
|
tooltip={tr.editingMathjaxInline()}
|
||||||
<IconButton
|
active={!isBlock}
|
||||||
tooltip={tr.editingMathjaxInline()}
|
on:click={() => {
|
||||||
active={!isBlock}
|
isBlock = false;
|
||||||
on:click={() => {
|
updateBlock();
|
||||||
isBlock = false;
|
}}
|
||||||
updateBlock();
|
on:click
|
||||||
}}
|
--border-left-radius="5px">{@html inlineIcon}</IconButton
|
||||||
on:click>{@html inlineIcon}</IconButton
|
>
|
||||||
>
|
|
||||||
</ButtonGroupItem>
|
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<IconButton
|
||||||
<IconButton
|
tooltip={tr.editingMathjaxBlock()}
|
||||||
tooltip={tr.editingMathjaxBlock()}
|
active={isBlock}
|
||||||
active={isBlock}
|
on:click={() => {
|
||||||
on:click={() => {
|
isBlock = true;
|
||||||
isBlock = true;
|
updateBlock();
|
||||||
updateBlock();
|
}}
|
||||||
}}
|
on:click
|
||||||
on:click>{@html blockIcon}</IconButton
|
--border-right-radius="5px">{@html blockIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
|
||||||
</Item>
|
|
||||||
|
|
||||||
<Item>
|
<ButtonGroup>
|
||||||
<ButtonGroup>
|
<IconButton
|
||||||
<ButtonGroupItem>
|
tooltip={tr.actionsDelete()}
|
||||||
<IconButton
|
on:click={() => dispatch("delete")}
|
||||||
tooltip={tr.actionsDelete()}
|
--border-left-radius="5px"
|
||||||
on:click={() => dispatch("delete")}>{@html deleteIcon}</IconButton
|
--border-right-radius="5px">{@html deleteIcon}</IconButton
|
||||||
>
|
>
|
||||||
</ButtonGroupItem>
|
</ButtonGroup>
|
||||||
</ButtonGroup>
|
|
||||||
</Item>
|
|
||||||
</ButtonToolbar>
|
</ButtonToolbar>
|
||||||
|
|
|
@ -12,10 +12,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import HandleSelection from "../HandleSelection.svelte";
|
import HandleSelection from "../HandleSelection.svelte";
|
||||||
import HandleBackground from "../HandleBackground.svelte";
|
import HandleBackground from "../HandleBackground.svelte";
|
||||||
import HandleControl from "../HandleControl.svelte";
|
import HandleControl from "../HandleControl.svelte";
|
||||||
import { getRichTextInput } from "../rich-text-input";
|
import { context } from "../rich-text-input";
|
||||||
import MathjaxMenu from "./MathjaxMenu.svelte";
|
import MathjaxMenu from "./MathjaxMenu.svelte";
|
||||||
|
|
||||||
const { container, api } = getRichTextInput();
|
const { container, api } = context.get();
|
||||||
const { flushCaret, preventResubscription } = api;
|
const { flushCaret, preventResubscription } = api;
|
||||||
|
|
||||||
const code = writable("");
|
const code = writable("");
|
||||||
|
|
|
@ -20,8 +20,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { tick, onMount } from "svelte";
|
import { tick, onMount } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { pageTheme } from "../../sveltelib/theme";
|
import { pageTheme } from "../../sveltelib/theme";
|
||||||
import { getDecoratedElements } from "../DecoratedElements.svelte";
|
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||||
import { getEditingArea } from "../EditingArea.svelte";
|
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||||
import CodeMirror from "../CodeMirror.svelte";
|
import CodeMirror from "../CodeMirror.svelte";
|
||||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
||||||
import { htmlanki, baseOptions, gutterOptions } from "../code-mirror";
|
import { htmlanki, baseOptions, gutterOptions } from "../code-mirror";
|
||||||
|
@ -34,8 +34,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
...gutterOptions,
|
...gutterOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
const { editingInputs, content } = getEditingArea();
|
const { editingInputs, content } = editingAreaContext.get();
|
||||||
const decoratedElements = getDecoratedElements();
|
const decoratedElements = decoratedElementsContext.get();
|
||||||
const code = writable($content);
|
const code = writable($content);
|
||||||
|
|
||||||
function adjustInputHTML(html: string): string {
|
function adjustInputHTML(html: string): string {
|
||||||
|
|
|
@ -27,6 +27,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
getTriggerAfterInput(): Trigger<OnInputCallback>;
|
getTriggerAfterInput(): Trigger<OnInputCallback>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function editingInputIsRichText(
|
||||||
|
editingInput: EditingInputAPI | null,
|
||||||
|
): editingInput is RichTextInputAPI {
|
||||||
|
return editingInput?.name === "rich-text";
|
||||||
|
}
|
||||||
|
|
||||||
export interface RichTextInputContextAPI {
|
export interface RichTextInputContextAPI {
|
||||||
styles: CustomStyles;
|
styles: CustomStyles;
|
||||||
container: HTMLElement;
|
container: HTMLElement;
|
||||||
|
@ -34,10 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = Symbol("richText");
|
const key = Symbol("richText");
|
||||||
const [set, getRichTextInput, hasRichTextInput] =
|
const [context, setContextProperty] = contextProperty<RichTextInputContextAPI>(key);
|
||||||
contextProperty<RichTextInputContextAPI>(key);
|
|
||||||
|
|
||||||
export { getRichTextInput, hasRichTextInput };
|
|
||||||
|
|
||||||
import getDOMMirror from "../../sveltelib/mirror-dom";
|
import getDOMMirror from "../../sveltelib/mirror-dom";
|
||||||
import getInputManager from "../../sveltelib/input-manager";
|
import getInputManager from "../../sveltelib/input-manager";
|
||||||
|
@ -49,7 +52,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
getTriggerOnNextInsert,
|
getTriggerOnNextInsert,
|
||||||
} = getInputManager();
|
} = getInputManager();
|
||||||
|
|
||||||
export { getTriggerAfterInput, getTriggerOnInput, getTriggerOnNextInsert };
|
export { context, getTriggerAfterInput, getTriggerOnInput, getTriggerOnNextInsert };
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -61,8 +64,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
} from "../../lib/dom";
|
} from "../../lib/dom";
|
||||||
import ContentEditable from "../../editable/ContentEditable.svelte";
|
import ContentEditable from "../../editable/ContentEditable.svelte";
|
||||||
import { placeCaretAfterContent } from "../../domlib/place-caret";
|
import { placeCaretAfterContent } from "../../domlib/place-caret";
|
||||||
import { getDecoratedElements } from "../DecoratedElements.svelte";
|
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||||
import { getEditingArea } from "../EditingArea.svelte";
|
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||||
import { promiseWithResolver } from "../../lib/promise";
|
import { promiseWithResolver } from "../../lib/promise";
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import { on } from "../../lib/events";
|
import { on } from "../../lib/events";
|
||||||
|
@ -73,8 +76,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
export let hidden: boolean;
|
export let hidden: boolean;
|
||||||
|
|
||||||
const { content, editingInputs } = getEditingArea();
|
const { content, editingInputs } = editingAreaContext.get();
|
||||||
const decoratedElements = getDecoratedElements();
|
const decoratedElements = decoratedElementsContext.get();
|
||||||
|
|
||||||
const range = document.createRange();
|
const range = document.createRange();
|
||||||
|
|
||||||
|
@ -269,7 +272,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<div class="rich-text-widgets">
|
<div class="rich-text-widgets">
|
||||||
{#await Promise.all( [richTextPromise, stylesPromise], ) then [container, styles]}
|
{#await Promise.all( [richTextPromise, stylesPromise], ) then [container, styles]}
|
||||||
<SetContext setter={set} value={{ container, styles, api }}>
|
<SetContext
|
||||||
|
setter={setContextProperty}
|
||||||
|
value={{ container, styles, api }}
|
||||||
|
>
|
||||||
<slot />
|
<slot />
|
||||||
</SetContext>
|
</SetContext>
|
||||||
{/await}
|
{/await}
|
||||||
|
|
|
@ -1,5 +1,9 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
export { default as RichTextInput, getRichTextInput } from "./RichTextInput.svelte";
|
export {
|
||||||
|
default as RichTextInput,
|
||||||
|
context,
|
||||||
|
editingInputIsRichText,
|
||||||
|
} from "./RichTextInput.svelte";
|
||||||
export type { RichTextInputAPI } from "./RichTextInput.svelte";
|
export type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { registerPackage } from "./register-package";
|
import { registerPackage } from "./runtime-require";
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Window {
|
interface Window {
|
||||||
|
|
109
ts/lib/children-access.ts
Normal file
109
ts/lib/children-access.ts
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
export type Identifier = Element | string | number;
|
||||||
|
|
||||||
|
function findElement<T extends Element>(
|
||||||
|
collection: HTMLCollection,
|
||||||
|
identifier: Identifier,
|
||||||
|
): [T, number] | null {
|
||||||
|
let element: T;
|
||||||
|
let index: number;
|
||||||
|
|
||||||
|
if (identifier instanceof Element) {
|
||||||
|
element = identifier as T;
|
||||||
|
index = Array.prototype.indexOf.call(collection, element);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (typeof identifier === "string") {
|
||||||
|
const item = collection.namedItem(identifier);
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
element = item as T;
|
||||||
|
index = Array.prototype.indexOf.call(collection, element);
|
||||||
|
if (index < 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else if (identifier < 0) {
|
||||||
|
index = collection.length + identifier;
|
||||||
|
const item = collection.item(index);
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
element = item as T;
|
||||||
|
} else {
|
||||||
|
index = identifier;
|
||||||
|
const item = collection.item(index);
|
||||||
|
if (!item) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
element = item as T;
|
||||||
|
}
|
||||||
|
|
||||||
|
return [element, index];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a convenient access API for the children
|
||||||
|
* of an element via identifiers. Identifiers can be:
|
||||||
|
* - integers: signify the position
|
||||||
|
* - negative integers: signify the offset from the end (-1 being the last element)
|
||||||
|
* - strings: signify the id of an element
|
||||||
|
* - the child directly
|
||||||
|
*/
|
||||||
|
class ChildrenAccess<T extends Element> {
|
||||||
|
parent: T;
|
||||||
|
|
||||||
|
constructor(parent: T) {
|
||||||
|
this.parent = parent;
|
||||||
|
}
|
||||||
|
|
||||||
|
insertElement(element: Element, identifier: Identifier): number {
|
||||||
|
const match = findElement(this.parent.children, identifier);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [reference, index] = match;
|
||||||
|
this.parent.insertBefore(element, reference[0]);
|
||||||
|
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
appendElement(element: Element, identifier: Identifier): number {
|
||||||
|
const match = findElement(this.parent.children, identifier);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [before, index] = match;
|
||||||
|
const reference = before.nextElementSibling ?? null;
|
||||||
|
this.parent.insertBefore(element, reference);
|
||||||
|
|
||||||
|
return index + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateElement(
|
||||||
|
f: (element: T, index: number) => void,
|
||||||
|
identifier: Identifier,
|
||||||
|
): boolean {
|
||||||
|
const match = findElement<T>(this.parent.children, identifier);
|
||||||
|
|
||||||
|
if (!match) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
f(...match);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function childrenAccess<T extends Element>(parent: T): ChildrenAccess<T> {
|
||||||
|
return new ChildrenAccess<T>(parent);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default childrenAccess;
|
||||||
|
export type { ChildrenAccess };
|
12
ts/lib/helpers.ts
Normal file
12
ts/lib/helpers.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
export type Callback = () => void;
|
||||||
|
|
||||||
|
export function removeItem<T>(items: T[], item: T): void {
|
||||||
|
const index = items.findIndex((i: T): boolean => i === item);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
items.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,57 +0,0 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
import { runtimeLibraries } from "./runtime-require";
|
|
||||||
|
|
||||||
const prohibit = () => false;
|
|
||||||
|
|
||||||
export function registerPackage(
|
|
||||||
name: string,
|
|
||||||
entries: Record<string, unknown>,
|
|
||||||
deprecation?: Record<string, string>,
|
|
||||||
): void {
|
|
||||||
const pack = deprecation
|
|
||||||
? new Proxy(entries, {
|
|
||||||
set: prohibit,
|
|
||||||
defineProperty: prohibit,
|
|
||||||
deleteProperty: prohibit,
|
|
||||||
get: (target, name: string) => {
|
|
||||||
if (name in deprecation) {
|
|
||||||
console.log(`anki: ${name} is deprecated: ${deprecation[name]}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
return target[name];
|
|
||||||
},
|
|
||||||
})
|
|
||||||
: entries;
|
|
||||||
|
|
||||||
runtimeLibraries[name] = pack;
|
|
||||||
}
|
|
||||||
|
|
||||||
function listPackages(): string[] {
|
|
||||||
return Object.keys(runtimeLibraries);
|
|
||||||
}
|
|
||||||
|
|
||||||
function hasPackages(...names: string[]): boolean {
|
|
||||||
const libraries = listPackages();
|
|
||||||
return names.reduce(
|
|
||||||
(accu: boolean, name: string) => accu && libraries.includes(name),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function immediatelyDeprecated() {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
registerPackage(
|
|
||||||
"anki/packages",
|
|
||||||
{
|
|
||||||
listPackages,
|
|
||||||
hasPackages,
|
|
||||||
immediatelyDeprecated,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
[immediatelyDeprecated.name]: "Do not use this function",
|
|
||||||
},
|
|
||||||
);
|
|
|
@ -1,19 +1,97 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
/* eslint
|
/**
|
||||||
@typescript-eslint/no-explicit-any: "off",
|
* Names of anki packages
|
||||||
|
*
|
||||||
|
* @privateRemarks
|
||||||
|
* Originally this was more strictly typed as a record:
|
||||||
|
* ```ts
|
||||||
|
* type AnkiPackages = {
|
||||||
|
* "anki/NoteEditor": NoteEditorPackage,
|
||||||
|
* }
|
||||||
|
* ```
|
||||||
|
* This would be very useful for `require`: the result could be strictly typed.
|
||||||
|
* However cross-module type imports currently don't work.
|
||||||
*/
|
*/
|
||||||
|
type AnkiPackages =
|
||||||
|
| "anki/NoteEditor"
|
||||||
|
| "anki/packages"
|
||||||
|
| "anki/bridgecommand"
|
||||||
|
| "anki/shortcuts"
|
||||||
|
| "anki/theme"
|
||||||
|
| "anki/location"
|
||||||
|
| "anki/surround"
|
||||||
|
| "anki/ui";
|
||||||
|
type PackageDeprecation<T extends Record<string, unknown>> = {
|
||||||
|
[key in keyof T]?: string;
|
||||||
|
};
|
||||||
|
|
||||||
/// This can be extended to allow require() calls at runtime, for libraries
|
/// This can be extended to allow require() calls at runtime, for packages
|
||||||
/// that are not included at bundling time.
|
/// that are not included at bundling time.
|
||||||
export const runtimeLibraries = {};
|
const runtimePackages: Partial<Record<AnkiPackages, Record<string, unknown>>> = {};
|
||||||
|
const prohibit = () => false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Packages registered with this function escape the typing provided by `AnkiPackages`
|
||||||
|
*/
|
||||||
|
export function registerPackageRaw(
|
||||||
|
name: string,
|
||||||
|
entries: Record<string, unknown>,
|
||||||
|
): void {
|
||||||
|
runtimePackages[name] = entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function registerPackage<
|
||||||
|
T extends AnkiPackages,
|
||||||
|
U extends Record<string, unknown>,
|
||||||
|
>(name: T, entries: U, deprecation?: PackageDeprecation<U>): void {
|
||||||
|
const pack = deprecation
|
||||||
|
? new Proxy(entries, {
|
||||||
|
set: prohibit,
|
||||||
|
defineProperty: prohibit,
|
||||||
|
deleteProperty: prohibit,
|
||||||
|
get: (target, name: string) => {
|
||||||
|
if (name in deprecation) {
|
||||||
|
console.log(`anki: ${name} is deprecated: ${deprecation[name]}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return target[name];
|
||||||
|
},
|
||||||
|
})
|
||||||
|
: entries;
|
||||||
|
|
||||||
|
registerPackageRaw(name, pack);
|
||||||
|
}
|
||||||
|
|
||||||
|
function require<T extends AnkiPackages>(name: T): Record<string, unknown> | undefined {
|
||||||
|
if (!(name in runtimePackages)) {
|
||||||
|
throw new Error(`Cannot require "${name}" at runtime.`);
|
||||||
|
} else {
|
||||||
|
return runtimePackages[name];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function listPackages(): string[] {
|
||||||
|
return Object.keys(runtimePackages);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasPackages(...names: string[]): boolean {
|
||||||
|
for (const name of names) {
|
||||||
|
if (!(name in runtimePackages)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
// Export require() as a global.
|
// Export require() as a global.
|
||||||
(window as any).require = function (name: string): unknown {
|
Object.assign(window, { require });
|
||||||
const lib = runtimeLibraries[name];
|
|
||||||
if (lib === undefined) {
|
registerPackage("anki/packages", {
|
||||||
throw new Error(`Cannot require(${name}) at runtime.`);
|
// We also register require here, so add-ons can have a type-save variant of require (TODO, see AnkiPackages above)
|
||||||
}
|
require,
|
||||||
return lib;
|
listPackages,
|
||||||
};
|
hasPackages,
|
||||||
|
});
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
|
|
||||||
import type { Modifier } from "./keys";
|
import type { Modifier } from "./keys";
|
||||||
|
|
||||||
import { registerPackage } from "./register-package";
|
import { registerPackage } from "./runtime-require";
|
||||||
import {
|
import {
|
||||||
modifiersToPlatformString,
|
modifiersToPlatformString,
|
||||||
keyToPlatformString,
|
keyToPlatformString,
|
||||||
|
|
13
ts/lib/ui.ts
Normal file
13
ts/lib/ui.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import { promiseWithResolver } from "./promise";
|
||||||
|
import { registerPackage } from "./runtime-require";
|
||||||
|
|
||||||
|
const [loaded, uiResolve] = promiseWithResolver();
|
||||||
|
|
||||||
|
registerPackage("anki/ui", {
|
||||||
|
loaded,
|
||||||
|
});
|
||||||
|
|
||||||
|
export { uiResolve };
|
|
@ -96,7 +96,7 @@ async function emitTypings(svelte: SvelteTsxFile[], deps: InputFile[]): Promise<
|
||||||
const tsHost = ts.createCompilerHost(parsedCommandLine.options);
|
const tsHost = ts.createCompilerHost(parsedCommandLine.options);
|
||||||
const createdFiles = {};
|
const createdFiles = {};
|
||||||
const cwd = ts.sys.getCurrentDirectory().replace(/\\/g, "/");
|
const cwd = ts.sys.getCurrentDirectory().replace(/\\/g, "/");
|
||||||
tsHost.writeFile = (fileName, contents) => {
|
tsHost.writeFile = (fileName: string, contents: string): void => {
|
||||||
// tsc makes some paths absolute for some reason
|
// tsc makes some paths absolute for some reason
|
||||||
if (fileName.startsWith(cwd)) {
|
if (fileName.startsWith(cwd)) {
|
||||||
fileName = fileName.substring(cwd.length + 1);
|
fileName = fileName.substring(cwd.length + 1);
|
||||||
|
@ -109,32 +109,28 @@ async function emitTypings(svelte: SvelteTsxFile[], deps: InputFile[]): Promise<
|
||||||
// }
|
// }
|
||||||
|
|
||||||
for (const file of svelte) {
|
for (const file of svelte) {
|
||||||
await writeFile(file.realDtsPath, createdFiles[file.virtualDtsPath]);
|
if (!(file.virtualDtsPath in createdFiles)) {
|
||||||
|
/**
|
||||||
|
* This can happen if you do a case-only rename
|
||||||
|
* e.g. NoteTypeButtons.svelte -> NotetypeButtons.svelte
|
||||||
|
*/
|
||||||
|
console.log(
|
||||||
|
"file not among created files: ",
|
||||||
|
file.virtualDtsPath,
|
||||||
|
Object.keys(createdFiles),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await writeFile(file.realDtsPath, createdFiles[file.virtualDtsPath]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function writeFile(file, data): Promise<void> {
|
async function writeFile(file: string, data: string): Promise<void> {
|
||||||
return new Promise((resolve, reject) => {
|
await fs.promises.writeFile(file, data);
|
||||||
fs.writeFile(file, data, (err) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function readFile(file) {
|
function readFile(file: string): Promise<string> {
|
||||||
return new Promise((resolve, reject) => {
|
return fs.promises.readFile(file, "utf-8");
|
||||||
fs.readFile(file, "utf8", (err, data) => {
|
|
||||||
if (err) {
|
|
||||||
reject(err);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resolve(data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function compileSingleSvelte(
|
async function compileSingleSvelte(
|
||||||
|
|
|
@ -3,29 +3,44 @@
|
||||||
|
|
||||||
import { setContext, getContext, hasContext } from "svelte";
|
import { setContext, getContext, hasContext } from "svelte";
|
||||||
|
|
||||||
type ContextProperty<T> = [
|
type SetContextPropertyAction<T> = (value: T) => void;
|
||||||
(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> {
|
export interface ContextProperty<T> {
|
||||||
function set(context: T): T {
|
/**
|
||||||
|
* Retrieves the component's context
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* The typing of the return value is a lie insofar as calling `get` outside
|
||||||
|
* of the component's context will return `undefined`.
|
||||||
|
* If you are uncertain if your component is actually within the context
|
||||||
|
* of this component, you should check with `available` first.
|
||||||
|
*
|
||||||
|
* @returns The component's context
|
||||||
|
*/
|
||||||
|
get(): T;
|
||||||
|
/**
|
||||||
|
* Checks whether the component's context is available
|
||||||
|
*/
|
||||||
|
available(): boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
function contextProperty<T>(
|
||||||
|
key: symbol,
|
||||||
|
): [ContextProperty<T>, SetContextPropertyAction<T>] {
|
||||||
|
function set(context: T): void {
|
||||||
setContext(key, context);
|
setContext(key, context);
|
||||||
return context;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function get(): T {
|
const context = {
|
||||||
return getContext(key);
|
get(): T {
|
||||||
}
|
return getContext(key);
|
||||||
|
},
|
||||||
|
available(): boolean {
|
||||||
|
return hasContext(key);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
function has(): boolean {
|
return [context, set];
|
||||||
return hasContext(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
return [set, get, has];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default contextProperty;
|
export default contextProperty;
|
||||||
|
|
299
ts/sveltelib/dynamic-slotting.ts
Normal file
299
ts/sveltelib/dynamic-slotting.ts
Normal file
|
@ -0,0 +1,299 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { SvelteComponent } from "svelte";
|
||||||
|
import type { Readable, Writable } from "svelte/store";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { Identifier } from "../lib/children-access";
|
||||||
|
import { promiseWithResolver } from "../lib/promise";
|
||||||
|
import type { ChildrenAccess } from "../lib/children-access";
|
||||||
|
import type { Callback } from "../lib/helpers";
|
||||||
|
import { removeItem } from "../lib/helpers";
|
||||||
|
import childrenAccess from "../lib/children-access";
|
||||||
|
import { nodeIsElement } from "../lib/dom";
|
||||||
|
|
||||||
|
export interface DynamicSvelteComponent {
|
||||||
|
component: typeof SvelteComponent;
|
||||||
|
/**
|
||||||
|
* Props that are passed to the component
|
||||||
|
*/
|
||||||
|
props?: Record<string, unknown>;
|
||||||
|
/**
|
||||||
|
* ID that will be assigned to the component that hosts
|
||||||
|
* the dynamic component (slot host)
|
||||||
|
*/
|
||||||
|
id?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Props that will be passed to the slot host, e.g. ButtonGroupItem.
|
||||||
|
*/
|
||||||
|
export interface SlotHostProps {
|
||||||
|
detach: Writable<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateInterfaceAPI<T extends SlotHostProps, U extends Element> {
|
||||||
|
addComponent(
|
||||||
|
component: DynamicSvelteComponent,
|
||||||
|
reinsert: (newElement: U, access: ChildrenAccess<U>) => number,
|
||||||
|
): Promise<{ destroy: Callback }>;
|
||||||
|
updateProps(update: (hostProps: T) => T, identifier: Identifier): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GetSlotHostProps<T> {
|
||||||
|
getProps(): T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DynamicSlotted<T extends SlotHostProps = SlotHostProps> {
|
||||||
|
component: DynamicSvelteComponent;
|
||||||
|
hostProps: T;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DynamicSlottingAPI<
|
||||||
|
T extends SlotHostProps,
|
||||||
|
U extends Element,
|
||||||
|
X extends Record<string, unknown>,
|
||||||
|
> {
|
||||||
|
/**
|
||||||
|
* This should be used as an action on the element that hosts the slot hosts.
|
||||||
|
*/
|
||||||
|
resolveSlotContainer: (element: U) => void;
|
||||||
|
/**
|
||||||
|
* Contains the props for the DynamicSlot component
|
||||||
|
*/
|
||||||
|
dynamicSlotted: Readable<DynamicSlotted<T>[]>;
|
||||||
|
slotsInterface: X;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allow add-on developers to dynamically extend/modify components our components
|
||||||
|
*
|
||||||
|
* @remarks
|
||||||
|
* It allows to insert elements inbetween the components, or modify their props.
|
||||||
|
* Practically speaking, we let Svelte do the initial insertion of an element,
|
||||||
|
* but then immediately move it to its destination, and save a reference to it.
|
||||||
|
*
|
||||||
|
* @experimental
|
||||||
|
*/
|
||||||
|
function dynamicSlotting<
|
||||||
|
T extends SlotHostProps,
|
||||||
|
U extends Element,
|
||||||
|
X extends Record<string, unknown>,
|
||||||
|
>(
|
||||||
|
/**
|
||||||
|
* A function which will create props which are passed to the dynamically
|
||||||
|
* slotted component's host component, the slot host, e.g. `ButtonGroupItem`
|
||||||
|
*/
|
||||||
|
makeProps: () => T,
|
||||||
|
/**
|
||||||
|
* This is called on *all* items whenever any item updates
|
||||||
|
*/
|
||||||
|
updatePropsList: (propsList: T[]) => T[],
|
||||||
|
/**
|
||||||
|
* A function to create an interface to interact with slotted components
|
||||||
|
*/
|
||||||
|
setSlotHostContext: (callback: GetSlotHostProps<T>) => void,
|
||||||
|
createInterface: (api: CreateInterfaceAPI<T, U>) => X,
|
||||||
|
): DynamicSlottingAPI<T, U, X> {
|
||||||
|
const slotted = writable<T[]>([]);
|
||||||
|
slotted.subscribe(updatePropsList);
|
||||||
|
|
||||||
|
function addDynamicallySlotted(index: number, props: T): void {
|
||||||
|
slotted.update((slotted: T[]): T[] => {
|
||||||
|
slotted.splice(index, 0, props);
|
||||||
|
return slotted;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const [elementPromise, resolveSlotContainer] = promiseWithResolver<U>();
|
||||||
|
const accessPromise = elementPromise.then(childrenAccess);
|
||||||
|
|
||||||
|
const dynamicSlotted = writable<DynamicSlotted<T>[]>([]);
|
||||||
|
|
||||||
|
async function addComponent(
|
||||||
|
component: DynamicSvelteComponent,
|
||||||
|
reinsert: (newElement: U, access: ChildrenAccess<U>) => number,
|
||||||
|
): Promise<{ destroy: Callback }> {
|
||||||
|
const [dynamicallySlottedMounted, resolveDynamicallySlotted] =
|
||||||
|
promiseWithResolver();
|
||||||
|
const access = await accessPromise;
|
||||||
|
const hostProps = makeProps();
|
||||||
|
|
||||||
|
function elementIsDynamicComponent(element: Element): boolean {
|
||||||
|
return !component.id || element.id === component.id;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callback(
|
||||||
|
mutations: MutationRecord[],
|
||||||
|
observer: MutationObserver,
|
||||||
|
): Promise<void> {
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
for (const addedNode of mutation.addedNodes) {
|
||||||
|
if (
|
||||||
|
!nodeIsElement(addedNode) ||
|
||||||
|
!elementIsDynamicComponent(addedNode)
|
||||||
|
) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const theElement = addedNode as U;
|
||||||
|
const index = reinsert(theElement, access);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
addDynamicallySlotted(index, hostProps);
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveDynamicallySlotted(undefined);
|
||||||
|
return observer.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
observer.observe(access.parent, { childList: true });
|
||||||
|
|
||||||
|
const dynamicSlot = {
|
||||||
|
component,
|
||||||
|
hostProps,
|
||||||
|
};
|
||||||
|
|
||||||
|
dynamicSlotted.update(
|
||||||
|
(dynamicSlotted: DynamicSlotted<T>[]): DynamicSlotted<T>[] => {
|
||||||
|
dynamicSlotted.push(dynamicSlot);
|
||||||
|
return dynamicSlotted;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await dynamicallySlottedMounted;
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
dynamicSlotted.update(
|
||||||
|
(dynamicSlotted: DynamicSlotted<T>[]): DynamicSlotted<T>[] => {
|
||||||
|
// TODO needs testing, if Svelte actually correctly removes the element
|
||||||
|
removeItem(dynamicSlotted, dynamicSlot);
|
||||||
|
return dynamicSlotted;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateProps(
|
||||||
|
update: (props: T) => T,
|
||||||
|
identifier: Identifier,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const access = await accessPromise;
|
||||||
|
|
||||||
|
return access.updateElement((_element: U, index: number): void => {
|
||||||
|
slotted.update((slottedProps: T[]) => {
|
||||||
|
slottedProps[index] = update(slottedProps[index]);
|
||||||
|
return slottedProps;
|
||||||
|
});
|
||||||
|
}, identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
const slotsInterface = createInterface({ addComponent, updateProps });
|
||||||
|
|
||||||
|
function getSlotHostProps(): T {
|
||||||
|
const props = makeProps();
|
||||||
|
|
||||||
|
slotted.update((slotted: T[]): T[] => {
|
||||||
|
slotted.push(props);
|
||||||
|
return slotted;
|
||||||
|
});
|
||||||
|
|
||||||
|
return props;
|
||||||
|
}
|
||||||
|
|
||||||
|
setSlotHostContext({ getProps: getSlotHostProps });
|
||||||
|
|
||||||
|
return {
|
||||||
|
dynamicSlotted,
|
||||||
|
resolveSlotContainer,
|
||||||
|
slotsInterface,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default dynamicSlotting;
|
||||||
|
|
||||||
|
/** Convenient default functions for dynamic slotting */
|
||||||
|
|
||||||
|
export function defaultProps(): SlotHostProps {
|
||||||
|
return {
|
||||||
|
detach: writable(false),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DefaultSlotInterface extends Record<string, unknown> {
|
||||||
|
insert(
|
||||||
|
button: DynamicSvelteComponent,
|
||||||
|
position?: Identifier,
|
||||||
|
): Promise<{ destroy: Callback }>;
|
||||||
|
append(
|
||||||
|
button: DynamicSvelteComponent,
|
||||||
|
position?: Identifier,
|
||||||
|
): Promise<{ destroy: Callback }>;
|
||||||
|
show(position: Identifier): Promise<boolean>;
|
||||||
|
hide(position: Identifier): Promise<boolean>;
|
||||||
|
toggle(position: Identifier): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultInterface<T extends SlotHostProps, U extends Element>({
|
||||||
|
addComponent,
|
||||||
|
updateProps,
|
||||||
|
}: CreateInterfaceAPI<T, U>): DefaultSlotInterface {
|
||||||
|
function insert(
|
||||||
|
component: DynamicSvelteComponent,
|
||||||
|
id: Identifier = 0,
|
||||||
|
): Promise<{ destroy: Callback }> {
|
||||||
|
return addComponent(component, (element: Element, access: ChildrenAccess<U>) =>
|
||||||
|
access.insertElement(element, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function append(
|
||||||
|
component: DynamicSvelteComponent,
|
||||||
|
id: Identifier = -1,
|
||||||
|
): Promise<{ destroy: Callback }> {
|
||||||
|
return addComponent(component, (element: Element, access: ChildrenAccess<U>) =>
|
||||||
|
access.appendElement(element, id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function show(id: Identifier): Promise<boolean> {
|
||||||
|
return updateProps((props: T): T => {
|
||||||
|
props.detach.set(false);
|
||||||
|
return props;
|
||||||
|
}, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hide(id: Identifier): Promise<boolean> {
|
||||||
|
return updateProps((props: T): T => {
|
||||||
|
props.detach.set(true);
|
||||||
|
return props;
|
||||||
|
}, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggle(id: Identifier): Promise<boolean> {
|
||||||
|
return updateProps((props: T): T => {
|
||||||
|
props.detach.update((detached: boolean) => !detached);
|
||||||
|
return props;
|
||||||
|
}, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
insert,
|
||||||
|
append,
|
||||||
|
show,
|
||||||
|
hide,
|
||||||
|
toggle,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
import contextProperty from "./context-property";
|
||||||
|
|
||||||
|
const key = Symbol("dynamicSlotting");
|
||||||
|
const [defaultSlotHostContext, setSlotHostContext] =
|
||||||
|
contextProperty<GetSlotHostProps<SlotHostProps>>(key);
|
||||||
|
|
||||||
|
export { defaultSlotHostContext, setSlotHostContext };
|
|
@ -5,7 +5,7 @@
|
||||||
// If they were to bundle their own runtime, things like bindings and contexts
|
// If they were to bundle their own runtime, things like bindings and contexts
|
||||||
// would not work.
|
// would not work.
|
||||||
|
|
||||||
import { runtimeLibraries } from "../lib/runtime-require";
|
import { registerPackageRaw } from "../lib/runtime-require";
|
||||||
import * as svelteRuntime from "svelte/internal";
|
import * as svelteRuntime from "svelte/internal";
|
||||||
|
|
||||||
runtimeLibraries["svelte/internal"] = svelteRuntime;
|
registerPackageRaw("svelte/internal", svelteRuntime);
|
||||||
|
|
76
ts/sveltelib/lifecycle-hooks.ts
Normal file
76
ts/sveltelib/lifecycle-hooks.ts
Normal file
|
@ -0,0 +1,76 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import { onMount as svelteOnMount, onDestroy as svelteOnDestroy } from "svelte";
|
||||||
|
import type { Callback } from "../lib/helpers";
|
||||||
|
import { removeItem } from "../lib/helpers";
|
||||||
|
|
||||||
|
type ComponentAPIMount<T> = (api: T) => Callback | void;
|
||||||
|
type ComponentAPIDestroy<T> = (api: T) => void;
|
||||||
|
|
||||||
|
type SetLifecycleHooksAction<T> = (api: T) => void;
|
||||||
|
|
||||||
|
export interface LifecycleHooks<T> {
|
||||||
|
onMount(callback: ComponentAPIMount<T>): Callback;
|
||||||
|
onDestroy(callback: ComponentAPIDestroy<T>): Callback;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Makes the Svelte lifecycle hooks accessible to add-ons.
|
||||||
|
* Currently we expose onMount and onDestroy in here, but it is fully
|
||||||
|
* thinkable to expose the others as well, given a good use case.
|
||||||
|
*/
|
||||||
|
function lifecycleHooks<T>(): [LifecycleHooks<T>, T[], SetLifecycleHooksAction<T>] {
|
||||||
|
const instances: T[] = [];
|
||||||
|
const mountCallbacks: ComponentAPIMount<T>[] = [];
|
||||||
|
const destroyCallbacks: ComponentAPIDestroy<T>[] = [];
|
||||||
|
|
||||||
|
function setup(api: T): void {
|
||||||
|
svelteOnMount(() => {
|
||||||
|
const cleanups: Callback[] = [];
|
||||||
|
|
||||||
|
for (const callback of mountCallbacks) {
|
||||||
|
const cleanup = callback(api);
|
||||||
|
|
||||||
|
if (cleanup) {
|
||||||
|
cleanups.push(cleanup);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
instances.push(api);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const cleanup of cleanups) {
|
||||||
|
cleanup();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
svelteOnDestroy(() => {
|
||||||
|
removeItem(instances, api);
|
||||||
|
|
||||||
|
for (const callback of destroyCallbacks) {
|
||||||
|
callback(api);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onMount(callback: ComponentAPIMount<T>): Callback {
|
||||||
|
mountCallbacks.push(callback);
|
||||||
|
return () => removeItem(mountCallbacks, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDestroy(callback: ComponentAPIDestroy<T>): Callback {
|
||||||
|
destroyCallbacks.push(callback);
|
||||||
|
return () => removeItem(mountCallbacks, callback);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lifecycle = {
|
||||||
|
onMount,
|
||||||
|
onDestroy,
|
||||||
|
};
|
||||||
|
|
||||||
|
return [lifecycle, instances, setup];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default lifecycleHooks;
|
|
@ -2,7 +2,7 @@
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { readable, get } from "svelte/store";
|
import { readable, get } from "svelte/store";
|
||||||
import { registerPackage } from "../lib/register-package";
|
import { registerPackage } from "../lib/runtime-require";
|
||||||
|
|
||||||
interface ThemeInfo {
|
interface ThemeInfo {
|
||||||
isDark: boolean;
|
isDark: boolean;
|
||||||
|
|
0
ts/sveltelib/types.ts
Normal file
0
ts/sveltelib/types.ts
Normal file
Loading…
Reference in a new issue