mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02: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
|
||||
node_modules
|
||||
bazel-*
|
||||
.bazel
|
||||
ftl/usage
|
||||
.mypy_cache
|
||||
.mypy_cache
|
||||
|
|
|
@ -413,9 +413,6 @@ class Browser(QMainWindow):
|
|||
|
||||
def add_preview_button(editor: Editor) -> None:
|
||||
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)
|
||||
self.editor = aqt.editor.Editor(
|
||||
|
@ -633,9 +630,7 @@ class Browser(QMainWindow):
|
|||
|
||||
def toggle_preview_button_state(self, active: bool) -> None:
|
||||
if self.editor.web:
|
||||
self.editor.web.eval(
|
||||
f"editorToolbar.togglePreviewButtonState({json.dumps(active)});"
|
||||
)
|
||||
self.editor.web.eval(f"togglePreviewButtonState({json.dumps(active)});")
|
||||
|
||||
def _cleanup_preview(self) -> None:
|
||||
if self._previewer:
|
||||
|
|
|
@ -174,7 +174,7 @@ class Editor:
|
|||
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
|
||||
righttopbtns_js = (
|
||||
f"""
|
||||
uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
|
||||
require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].toolbar.toolbar.append({{
|
||||
component: editorToolbar.AddonButtons,
|
||||
id: "addons",
|
||||
props: {{ buttons: [ {righttopbtns_defs} ] }},
|
||||
|
@ -525,7 +525,9 @@ uiPromise.then(noteEditor => noteEditor.toolbar.toolbar.appendGroup({{
|
|||
js += " setSticky(%s);" % json.dumps(sticky)
|
||||
|
||||
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:
|
||||
"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:
|
||||
cloze_hint = tr.adding_cloze_outside_cloze_field()
|
||||
|
||||
self.web.eval(f"uiPromise.then(() => setBackgrounds({json.dumps(cols)}));")
|
||||
self.web.eval(f"uiPromise.then(() => setClozeHint({json.dumps(cloze_hint)}));")
|
||||
self.web.eval(
|
||||
'require("anki/ui").loaded.then(() => {'
|
||||
f"setBackgrounds({json.dumps(cols)});\n"
|
||||
f"setClozeHint({json.dumps(cloze_hint)});\n"
|
||||
"}); "
|
||||
)
|
||||
|
||||
def showDupes(self) -> None:
|
||||
aqt.dialogs.open(
|
||||
|
@ -1353,14 +1359,12 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
|||
|
||||
|
||||
def set_cloze_button(editor: Editor) -> None:
|
||||
if editor.note.note_type()["type"] == MODEL_CLOZE:
|
||||
editor.web.eval(
|
||||
'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.showButton("cloze")); '
|
||||
)
|
||||
else:
|
||||
editor.web.eval(
|
||||
'uiPromise.then((noteEditor) => noteEditor.toolbar.templateButtons.hideButton("cloze")); '
|
||||
)
|
||||
action = "show" if editor.note.note_type()["type"] == MODEL_CLOZE else "hide"
|
||||
editor.web.eval(
|
||||
'require("anki/ui").loaded.then(() =>'
|
||||
f'require("anki/NoteEditor").instances[0].toolbar.templateButtons.{action}("cloze")'
|
||||
"); "
|
||||
)
|
||||
|
||||
|
||||
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 StickyContainer from "../components/StickyContainer.svelte";
|
||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import LabelButton from "../components/LabelButton.svelte";
|
||||
import Badge from "../components/Badge.svelte";
|
||||
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"
|
||||
>
|
||||
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
||||
<Item>
|
||||
<ButtonGroupItem>
|
||||
<LabelButton disabled={true}>
|
||||
{$info.oldNotetypeName}
|
||||
</LabelButton>
|
||||
</ButtonGroupItem>
|
||||
</Item>
|
||||
<Item>
|
||||
<Badge iconSize={70}>
|
||||
{#if window.getComputedStyle(document.body).direction == "rtl"}
|
||||
{@html arrowLeftIcon}
|
||||
{:else}
|
||||
{@html arrowRightIcon}
|
||||
{/if}
|
||||
</Badge>
|
||||
</Item>
|
||||
<Item>
|
||||
<ButtonGroup class="flex-grow-1">
|
||||
<ButtonGroupItem>
|
||||
<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>
|
||||
<LabelButton disabled={true}>
|
||||
{$info.oldNotetypeName}
|
||||
</LabelButton>
|
||||
<Badge iconSize={70}>
|
||||
{#if window.getComputedStyle(document.body).direction == "rtl"}
|
||||
{@html arrowLeftIcon}
|
||||
{:else}
|
||||
{@html arrowRightIcon}
|
||||
{/if}
|
||||
</Badge>
|
||||
<ButtonGroup class="flex-grow-1">
|
||||
<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>
|
||||
</ButtonGroup>
|
||||
|
||||
<Item>
|
||||
<SaveButton {state} />
|
||||
</Item>
|
||||
<SaveButton {state} />
|
||||
</ButtonToolbar>
|
||||
</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 ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import LabelButton from "../components/LabelButton.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>
|
||||
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<LabelButton
|
||||
theme="primary"
|
||||
tooltip={getPlatformString(keyCombination)}
|
||||
on:click={save}>{tr.actionsSave()}</LabelButton
|
||||
>
|
||||
<Shortcut {keyCombination} on:action={save} />
|
||||
</ButtonGroupItem>
|
||||
<LabelButton
|
||||
theme="primary"
|
||||
tooltip={getPlatformString(keyCombination)}
|
||||
on:click={save}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="5px">{tr.actionsSave()}</LabelButton
|
||||
>
|
||||
<Shortcut {keyCombination} on:action={save} />
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -42,6 +42,7 @@ svelte_check(
|
|||
"//sass:breakpoints_lib",
|
||||
"//sass/bootstrap",
|
||||
"@npm//@types/bootstrap",
|
||||
"//ts/lib:lib_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 = "";
|
||||
export { className as class };
|
||||
|
||||
export let api: Record<string, unknown> | undefined = undefined;
|
||||
|
||||
setContext(dropdownKey, null);
|
||||
</script>
|
||||
|
||||
<ButtonToolbar
|
||||
{id}
|
||||
class="dropdown-menu btn-dropdown-menu {className}"
|
||||
wrap={false}
|
||||
{api}
|
||||
>
|
||||
<ButtonToolbar {id} class="dropdown-menu btn-dropdown-menu {className}" wrap={false}>
|
||||
<div on:mousedown|preventDefault|stopPropagation on:click>
|
||||
<slot />
|
||||
</div>
|
||||
|
|
|
@ -2,35 +2,12 @@
|
|||
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 { 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">
|
||||
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;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
export let size: number | undefined = undefined;
|
||||
|
||||
export let wrap: boolean | undefined = undefined;
|
||||
|
||||
$: 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;
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
bind:this={buttonGroupRef}
|
||||
{id}
|
||||
class="button-group btn-group {className}"
|
||||
{style}
|
||||
dir="ltr"
|
||||
role="group"
|
||||
>
|
||||
<div {id} class="button-group btn-group {className}" {style} dir="ltr" role="group">
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -1,67 +1,105 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
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 { 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">
|
||||
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 registration: ButtonRegistration | undefined = undefined;
|
||||
export let hostProps: ButtonSlotHostProps | undefined = undefined;
|
||||
|
||||
let detached: boolean;
|
||||
let position_: ButtonPosition;
|
||||
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 leftStyle = `--border-left-radius: ${radius}; --border-right-radius: 0; `;
|
||||
const rightStyle = `--border-left-radius: 0; --border-right-radius: ${radius}; `;
|
||||
|
||||
$: {
|
||||
switch (position_) {
|
||||
function updateButtonStyle(position: ButtonPosition) {
|
||||
switch (position) {
|
||||
case ButtonPosition.Standalone:
|
||||
style = `--border-left-radius: ${radius}; --border-right-radius: ${radius}; `;
|
||||
break;
|
||||
case ButtonPosition.InlineStart:
|
||||
style = leftStyle;
|
||||
style = `--border-left-radius: ${radius}; --border-right-radius: 0; `;
|
||||
break;
|
||||
case ButtonPosition.Center:
|
||||
style = "--border-left-radius: 0; --border-right-radius: 0; ";
|
||||
break;
|
||||
case ButtonPosition.InlineEnd:
|
||||
style = rightStyle;
|
||||
style = `--border-left-radius: 0; --border-right-radius: ${radius}; `;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (registration) {
|
||||
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;
|
||||
}
|
||||
$: updateButtonStyle($position);
|
||||
</script>
|
||||
|
||||
<!-- div is necessary to preserve item position -->
|
||||
<div {id} class="button-group-item" {style}>
|
||||
<Detachable {detached}>
|
||||
<div class="button-group-item" {id} {style}>
|
||||
{#if !$detach}
|
||||
<slot />
|
||||
</Detachable>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
|
|
@ -2,27 +2,7 @@
|
|||
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 { 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">
|
||||
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";
|
||||
|
||||
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;
|
||||
|
||||
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>
|
||||
|
||||
<div
|
||||
bind:this={buttonToolbarRef}
|
||||
{id}
|
||||
class="button-toolbar btn-toolbar {className}"
|
||||
class:nightMode={$pageTheme.isDark}
|
||||
|
@ -102,11 +32,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
on:focusout
|
||||
>
|
||||
<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">
|
||||
|
@ -114,7 +39,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
flex-wrap: var(--buttons-wrap);
|
||||
padding-left: 0.15rem;
|
||||
|
||||
> :global(*) > :global(*) {
|
||||
:global(.button-group) {
|
||||
/* TODO replace with gap once available */
|
||||
margin-right: 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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Section from "./Section.svelte";
|
||||
import type { Breakpoint } from "./types";
|
||||
|
||||
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 */
|
||||
export let breakpoint: Breakpoint | "fluid" = "fluid";
|
||||
export let api: Record<string, never> | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<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-fluid={breakpoint === "fluid"}
|
||||
>
|
||||
<Section {api}>
|
||||
<slot />
|
||||
</Section>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Detachable from "./Detachable.svelte";
|
||||
|
||||
import type { Register, Registration } from "./registration";
|
||||
|
||||
import { getContext, hasContext } from "svelte";
|
||||
import { sectionKey } from "./context-keys";
|
||||
import type { SlotHostProps } from "../sveltelib/dynamic-slotting";
|
||||
import { defaultSlotHostContext } from "../sveltelib/dynamic-slotting";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
export let registration: Registration | undefined = undefined;
|
||||
export let hostProps: SlotHostProps | undefined = undefined;
|
||||
|
||||
let detached: boolean;
|
||||
|
||||
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;
|
||||
if (!defaultSlotHostContext.available()) {
|
||||
console.log("Item: should always have a slotHostContext");
|
||||
}
|
||||
|
||||
const { detach } = hostProps ?? defaultSlotHostContext.get().getProps();
|
||||
</script>
|
||||
|
||||
<!-- div is necessary to preserve item position -->
|
||||
<div class="item" {id}>
|
||||
<Detachable {detached}>
|
||||
{#if !$detach}
|
||||
<slot />
|
||||
</Detachable>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<Item {id}>
|
||||
<div class="row {className}">
|
||||
<slot />
|
||||
</div>
|
||||
</Item>
|
||||
<div class="row {className}">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.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 breakpoint: Breakpoint | "fluid" = "fluid";
|
||||
export let api: Record<string, never> | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<div {id} bind:offsetHeight={height} class="sticky-container {className}">
|
||||
<Container {breakpoint} {api}>
|
||||
<Container {breakpoint}>
|
||||
<slot />
|
||||
</Container>
|
||||
</div>
|
||||
|
|
|
@ -85,9 +85,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
api = {
|
||||
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 />
|
||||
toggle: () => {}, // toggle: dropdown.toggle.bind(dropdown),
|
||||
toggle: () => {},
|
||||
/* toggle: dropdown.toggle.bind(dropdown), */
|
||||
hide: dropdown.hide.bind(dropdown),
|
||||
update: dropdown.update.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());
|
||||
</script>
|
||||
|
||||
<div class={dropClass}>
|
||||
<div class="with-dropdown {dropClass}">
|
||||
<slot {createDropdown} dropdownObject={api} />
|
||||
</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";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
export let api: Record<string, never>;
|
||||
|
||||
let components = state.addonComponents;
|
||||
const auxData = state.currentAuxData;
|
||||
</script>
|
||||
|
||||
{#if $components.length || state.haveAddons}
|
||||
<TitledContainer title="Add-ons" {api}>
|
||||
<TitledContainer title="Add-ons">
|
||||
<p>
|
||||
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
|
||||
|
|
|
@ -9,6 +9,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import SpinBoxFloatRow from "./SpinBoxFloatRow.svelte";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import CardStateCustomizer from "./CardStateCustomizer.svelte";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
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;
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigAdvancedTitle()} {api}>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.maximumReviewInterval}
|
||||
defaultValue={defaults.maximumReviewInterval}
|
||||
min={1}
|
||||
max={365 * 100}
|
||||
markdownTooltip={tr.deckConfigMaximumIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingMaximumInterval()}
|
||||
</SpinBoxRow>
|
||||
<TitledContainer title={tr.deckConfigAdvancedTitle()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.maximumReviewInterval}
|
||||
defaultValue={defaults.maximumReviewInterval}
|
||||
min={1}
|
||||
max={365 * 100}
|
||||
markdownTooltip={tr.deckConfigMaximumIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingMaximumInterval()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.initialEase}
|
||||
defaultValue={defaults.initialEase}
|
||||
min={1.31}
|
||||
max={5}
|
||||
markdownTooltip={tr.deckConfigStartingEaseTooltip()}
|
||||
>
|
||||
{tr.schedulingStartingEase()}
|
||||
</SpinBoxFloatRow>
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.initialEase}
|
||||
defaultValue={defaults.initialEase}
|
||||
min={1.31}
|
||||
max={5}
|
||||
markdownTooltip={tr.deckConfigStartingEaseTooltip()}
|
||||
>
|
||||
{tr.schedulingStartingEase()}
|
||||
</SpinBoxFloatRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.easyMultiplier}
|
||||
defaultValue={defaults.easyMultiplier}
|
||||
min={1}
|
||||
max={3}
|
||||
markdownTooltip={tr.deckConfigEasyBonusTooltip()}
|
||||
>
|
||||
{tr.schedulingEasyBonus()}
|
||||
</SpinBoxFloatRow>
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.easyMultiplier}
|
||||
defaultValue={defaults.easyMultiplier}
|
||||
min={1}
|
||||
max={3}
|
||||
markdownTooltip={tr.deckConfigEasyBonusTooltip()}
|
||||
>
|
||||
{tr.schedulingEasyBonus()}
|
||||
</SpinBoxFloatRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.intervalMultiplier}
|
||||
defaultValue={defaults.intervalMultiplier}
|
||||
min={0.5}
|
||||
max={2}
|
||||
markdownTooltip={tr.deckConfigIntervalModifierTooltip()}
|
||||
>
|
||||
{tr.schedulingIntervalModifier()}
|
||||
</SpinBoxFloatRow>
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.intervalMultiplier}
|
||||
defaultValue={defaults.intervalMultiplier}
|
||||
min={0.5}
|
||||
max={2}
|
||||
markdownTooltip={tr.deckConfigIntervalModifierTooltip()}
|
||||
>
|
||||
{tr.schedulingIntervalModifier()}
|
||||
</SpinBoxFloatRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.hardMultiplier}
|
||||
defaultValue={defaults.hardMultiplier}
|
||||
min={0.5}
|
||||
max={1.3}
|
||||
markdownTooltip={tr.deckConfigHardIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingHardInterval()}
|
||||
</SpinBoxFloatRow>
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.hardMultiplier}
|
||||
defaultValue={defaults.hardMultiplier}
|
||||
min={0.5}
|
||||
max={1.3}
|
||||
markdownTooltip={tr.deckConfigHardIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingHardInterval()}
|
||||
</SpinBoxFloatRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.lapseMultiplier}
|
||||
defaultValue={defaults.lapseMultiplier}
|
||||
max={1}
|
||||
markdownTooltip={tr.deckConfigNewIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingNewInterval()}
|
||||
</SpinBoxFloatRow>
|
||||
<Item>
|
||||
<SpinBoxFloatRow
|
||||
bind:value={$config.lapseMultiplier}
|
||||
defaultValue={defaults.lapseMultiplier}
|
||||
max={1}
|
||||
markdownTooltip={tr.deckConfigNewIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingNewInterval()}
|
||||
</SpinBoxFloatRow>
|
||||
</Item>
|
||||
|
||||
{#if state.v3Scheduler}
|
||||
<CardStateCustomizer bind:value={$cardStateCustomizer} />
|
||||
{/if}
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<CardStateCustomizer bind:value={$cardStateCustomizer} />
|
||||
</Item>
|
||||
{/if}
|
||||
</DynamicallySlottable>
|
||||
</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 SwitchRow from "./SwitchRow.svelte";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
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;
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigAudioTitle()} {api}>
|
||||
<SwitchRow
|
||||
bind:value={$config.disableAutoplay}
|
||||
defaultValue={defaults.disableAutoplay}
|
||||
>
|
||||
{tr.deckConfigDisableAutoplay()}
|
||||
</SwitchRow>
|
||||
<TitledContainer title={tr.deckConfigAudioTitle()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.disableAutoplay}
|
||||
defaultValue={defaults.disableAutoplay}
|
||||
>
|
||||
{tr.deckConfigDisableAutoplay()}
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
|
||||
<SwitchRow
|
||||
bind:value={$config.skipQuestionWhenReplayingAnswer}
|
||||
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
|
||||
markdownTooltip={tr.deckConfigAlwaysIncludeQuestionAudioTooltip()}
|
||||
>
|
||||
{tr.deckConfigSkipQuestionWhenReplaying()}
|
||||
</SwitchRow>
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.skipQuestionWhenReplayingAnswer}
|
||||
defaultValue={defaults.skipQuestionWhenReplayingAnswer}
|
||||
markdownTooltip={tr.deckConfigAlwaysIncludeQuestionAudioTooltip()}
|
||||
>
|
||||
{tr.deckConfigSkipQuestionWhenReplaying()}
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</TitledContainer>
|
||||
|
|
|
@ -17,6 +17,7 @@ compile_sass(
|
|||
"//sass:base_lib",
|
||||
"//sass:breakpoints_lib",
|
||||
"//sass:scrollbar_lib",
|
||||
"//sass:night_mode_lib",
|
||||
"//sass/bootstrap",
|
||||
],
|
||||
)
|
||||
|
@ -37,6 +38,10 @@ _ts_deps = [
|
|||
|
||||
compile_svelte(
|
||||
deps = _ts_deps + [
|
||||
"//sass:base_lib",
|
||||
"//sass:breakpoints_lib",
|
||||
"//sass:scrollbar_lib",
|
||||
"//sass:night_mode_lib",
|
||||
"//sass/bootstrap",
|
||||
],
|
||||
)
|
||||
|
@ -77,9 +82,11 @@ svelte_check(
|
|||
"*.ts",
|
||||
"*.svelte",
|
||||
]) + [
|
||||
"//sass:base_lib",
|
||||
"//sass:button_mixins_lib",
|
||||
"//sass:night_mode_lib",
|
||||
"//sass:scrollbar_lib",
|
||||
"//sass:breakpoints_lib",
|
||||
"//sass:night_mode_lib",
|
||||
"//sass/bootstrap",
|
||||
"//ts/components",
|
||||
"//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 * as tr from "../lib/ftl";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
export let state: DeckOptionsState;
|
||||
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;
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigBuryTitle()} {api}>
|
||||
<SwitchRow
|
||||
bind:value={$config.buryNew}
|
||||
defaultValue={defaults.buryNew}
|
||||
markdownTooltip={tr.deckConfigBuryTooltip()}
|
||||
>
|
||||
{tr.deckConfigBuryNewSiblings()}
|
||||
</SwitchRow>
|
||||
<TitledContainer title={tr.deckConfigBuryTitle()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.buryNew}
|
||||
defaultValue={defaults.buryNew}
|
||||
markdownTooltip={tr.deckConfigBuryTooltip()}
|
||||
>
|
||||
{tr.deckConfigBuryNewSiblings()}
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
|
||||
<SwitchRow
|
||||
bind:value={$config.buryReviews}
|
||||
defaultValue={defaults.buryReviews}
|
||||
markdownTooltip={tr.deckConfigBuryTooltip()}
|
||||
>
|
||||
{tr.deckConfigBuryReviewSiblings()}
|
||||
</SwitchRow>
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.buryReviews}
|
||||
defaultValue={defaults.buryReviews}
|
||||
markdownTooltip={tr.deckConfigBuryTooltip()}
|
||||
>
|
||||
{tr.deckConfigBuryReviewSiblings()}
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</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 StickyContainer from "../components/StickyContainer.svelte";
|
||||
import ButtonToolbar from "../components/ButtonToolbar.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
|
||||
import SelectButton from "../components/SelectButton.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">
|
||||
<ButtonToolbar class="justify-content-between" size={2.3} wrap={false}>
|
||||
<Item>
|
||||
<ButtonGroup class="flex-grow-1">
|
||||
<ButtonGroupItem>
|
||||
<SelectButton class="flex-grow-1" on:change={blur}>
|
||||
{#each $configList as entry}
|
||||
<SelectOption
|
||||
value={String(entry.idx)}
|
||||
selected={entry.current}
|
||||
>
|
||||
{configLabel(entry)}
|
||||
</SelectOption>
|
||||
{/each}
|
||||
</SelectButton>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
<ButtonGroup class="flex-grow-1">
|
||||
<SelectButton
|
||||
class="flex-grow-1"
|
||||
on:change={blur}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="5px"
|
||||
>
|
||||
{#each $configList as entry}
|
||||
<SelectOption value={String(entry.idx)} selected={entry.current}>
|
||||
{configLabel(entry)}
|
||||
</SelectOption>
|
||||
{/each}
|
||||
</SelectButton>
|
||||
</ButtonGroup>
|
||||
|
||||
<Item>
|
||||
<SaveButton
|
||||
{state}
|
||||
on:add={promptToAdd}
|
||||
on:clone={promptToClone}
|
||||
on:rename={promptToRename}
|
||||
/>
|
||||
</Item>
|
||||
<SaveButton
|
||||
{state}
|
||||
on:add={promptToAdd}
|
||||
on:clone={promptToClone}
|
||||
on:rename={promptToRename}
|
||||
/>
|
||||
</ButtonToolbar>
|
||||
</StickyContainer>
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
<script lang="ts">
|
||||
import * as tr from "../lib/ftl";
|
||||
import TitledContainer from "./TitledContainer.svelte";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import SpinBoxRow from "./SpinBoxRow.svelte";
|
||||
import Warning from "./Warning.svelte";
|
||||
|
@ -40,28 +41,34 @@
|
|||
: "";
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigDailyLimits()} {api}>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.newPerDay}
|
||||
defaultValue={defaults.newPerDay}
|
||||
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
||||
>
|
||||
{tr.schedulingNewCardsday()}
|
||||
</SpinBoxRow>
|
||||
<TitledContainer title={tr.deckConfigDailyLimits()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.newPerDay}
|
||||
defaultValue={defaults.newPerDay}
|
||||
markdownTooltip={tr.deckConfigNewLimitTooltip() + v3Extra}
|
||||
>
|
||||
{tr.schedulingNewCardsday()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Warning warning={newCardsGreaterThanParent} />
|
||||
</Item>
|
||||
<Item>
|
||||
<Warning warning={newCardsGreaterThanParent} />
|
||||
</Item>
|
||||
|
||||
<SpinBoxRow
|
||||
bind:value={$config.reviewsPerDay}
|
||||
defaultValue={defaults.reviewsPerDay}
|
||||
markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
||||
>
|
||||
{tr.schedulingMaximumReviewsday()}
|
||||
</SpinBoxRow>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.reviewsPerDay}
|
||||
defaultValue={defaults.reviewsPerDay}
|
||||
markdownTooltip={tr.deckConfigReviewLimitTooltip() + v3Extra}
|
||||
>
|
||||
{tr.schedulingMaximumReviewsday()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Warning warning={reviewsTooLow} />
|
||||
</Item>
|
||||
<Item>
|
||||
<Warning warning={reviewsTooLow} />
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</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 AudioOptions from "./AudioOptions.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 { 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 timerOptions = {};
|
||||
export const audioOptions = {};
|
||||
export const addonOptions = {};
|
||||
export const advancedOptions = {};
|
||||
</script>
|
||||
|
||||
|
@ -63,45 +64,64 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
--gutter-inline="0.25rem"
|
||||
--gutter-block="0.5rem"
|
||||
class="container-columns"
|
||||
api={options}
|
||||
>
|
||||
<Row class="row-columns">
|
||||
<DailyLimits {state} api={dailyLimits} />
|
||||
</Row>
|
||||
<DynamicallySlottable slotHost={Item} api={options}>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<DailyLimits {state} api={dailyLimits} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Row class="row-columns">
|
||||
<NewOptions {state} api={newOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<NewOptions {state} api={newOptions} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Row class="row-columns">
|
||||
<LapseOptions {state} api={lapseOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<LapseOptions {state} api={lapseOptions} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
{#if state.v3Scheduler}
|
||||
<Row class="row-columns">
|
||||
<DisplayOrder {state} api={displayOrder} />
|
||||
</Row>
|
||||
{/if}
|
||||
{#if state.v3Scheduler}
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<DisplayOrder {state} api={displayOrder} />
|
||||
</Row>
|
||||
</Item>
|
||||
{/if}
|
||||
|
||||
<Row class="row-columns">
|
||||
<TimerOptions {state} api={timerOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<TimerOptions {state} api={timerOptions} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Row class="row-columns">
|
||||
<BuryOptions {state} api={buryOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<BuryOptions {state} api={buryOptions} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Row class="row-columns">
|
||||
<AudioOptions {state} api={audioOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<AudioOptions {state} api={audioOptions} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Row class="row-columns">
|
||||
<Addons {state} api={addonOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<Addons {state} />
|
||||
</Row>
|
||||
</Item>
|
||||
|
||||
<Row class="row-columns">
|
||||
<AdvancedOptions {state} api={advancedOptions} />
|
||||
</Row>
|
||||
<Item>
|
||||
<Row class="row-columns">
|
||||
<AdvancedOptions {state} api={advancedOptions} />
|
||||
</Row>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</Container>
|
||||
</div>
|
||||
|
||||
|
|
|
@ -5,8 +5,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="ts">
|
||||
import * as tr from "../lib/ftl";
|
||||
import TitledContainer from "./TitledContainer.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import EnumSelectorRow from "./EnumSelectorRow.svelte";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
import { reviewMixChoices } from "./strings";
|
||||
|
@ -45,59 +46,62 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
];
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigOrderingTitle()} {api}>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newCardGatherPriority}
|
||||
defaultValue={defaults.newCardGatherPriority}
|
||||
choices={newGatherPriorityChoices}
|
||||
markdownTooltip={tr.deckConfigNewGatherPriorityTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigNewGatherPriority()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
<TitledContainer title={tr.deckConfigOrderingTitle()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newCardGatherPriority}
|
||||
defaultValue={defaults.newCardGatherPriority}
|
||||
choices={newGatherPriorityChoices}
|
||||
markdownTooltip={tr.deckConfigNewGatherPriorityTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigNewGatherPriority()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newCardSortOrder}
|
||||
defaultValue={defaults.newCardSortOrder}
|
||||
choices={newSortOrderChoices}
|
||||
markdownTooltip={tr.deckConfigNewCardSortOrderTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigNewCardSortOrder()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newCardSortOrder}
|
||||
defaultValue={defaults.newCardSortOrder}
|
||||
choices={newSortOrderChoices}
|
||||
markdownTooltip={tr.deckConfigNewCardSortOrderTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigNewCardSortOrder()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newMix}
|
||||
defaultValue={defaults.newMix}
|
||||
choices={reviewMixChoices()}
|
||||
markdownTooltip={tr.deckConfigNewReviewPriorityTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigNewReviewPriority()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newMix}
|
||||
defaultValue={defaults.newMix}
|
||||
choices={reviewMixChoices()}
|
||||
markdownTooltip={tr.deckConfigNewReviewPriorityTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigNewReviewPriority()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.interdayLearningMix}
|
||||
defaultValue={defaults.interdayLearningMix}
|
||||
choices={reviewMixChoices()}
|
||||
markdownTooltip={tr.deckConfigInterdayStepPriorityTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigInterdayStepPriority()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.interdayLearningMix}
|
||||
defaultValue={defaults.interdayLearningMix}
|
||||
choices={reviewMixChoices()}
|
||||
markdownTooltip={tr.deckConfigInterdayStepPriorityTooltip() +
|
||||
currentDeck}
|
||||
>
|
||||
{tr.deckConfigInterdayStepPriority()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.reviewOrder}
|
||||
defaultValue={defaults.reviewOrder}
|
||||
choices={reviewOrderChoices}
|
||||
markdownTooltip={tr.deckConfigReviewSortOrderTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigReviewSortOrder()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.reviewOrder}
|
||||
defaultValue={defaults.reviewOrder}
|
||||
choices={reviewOrderChoices}
|
||||
markdownTooltip={tr.deckConfigReviewSortOrderTooltip() + currentDeck}
|
||||
>
|
||||
{tr.deckConfigReviewSortOrder()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</TitledContainer>
|
||||
|
|
|
@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="ts">
|
||||
import * as tr from "../lib/ftl";
|
||||
import TitledContainer from "./TitledContainer.svelte";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import StepsInputRow from "./StepsInputRow.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()];
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.schedulingLapses()} {api}>
|
||||
<StepsInputRow
|
||||
bind:value={$config.relearnSteps}
|
||||
defaultValue={defaults.relearnSteps}
|
||||
markdownTooltip={tr.deckConfigRelearningStepsTooltip()}
|
||||
>
|
||||
{tr.deckConfigRelearningSteps()}
|
||||
</StepsInputRow>
|
||||
<TitledContainer title={tr.schedulingLapses()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<StepsInputRow
|
||||
bind:value={$config.relearnSteps}
|
||||
defaultValue={defaults.relearnSteps}
|
||||
markdownTooltip={tr.deckConfigRelearningStepsTooltip()}
|
||||
>
|
||||
{tr.deckConfigRelearningSteps()}
|
||||
</StepsInputRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxRow
|
||||
bind:value={$config.minimumLapseInterval}
|
||||
defaultValue={defaults.minimumLapseInterval}
|
||||
min={1}
|
||||
markdownTooltip={tr.deckConfigMinimumIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingMinimumInterval()}
|
||||
</SpinBoxRow>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.minimumLapseInterval}
|
||||
defaultValue={defaults.minimumLapseInterval}
|
||||
min={1}
|
||||
markdownTooltip={tr.deckConfigMinimumIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingMinimumInterval()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Warning warning={stepsExceedMinimumInterval} />
|
||||
</Item>
|
||||
<Item>
|
||||
<Warning warning={stepsExceedMinimumInterval} />
|
||||
</Item>
|
||||
|
||||
<SpinBoxRow
|
||||
bind:value={$config.leechThreshold}
|
||||
defaultValue={defaults.leechThreshold}
|
||||
min={1}
|
||||
markdownTooltip={tr.deckConfigLeechThresholdTooltip()}
|
||||
>
|
||||
{tr.schedulingLeechThreshold()}
|
||||
</SpinBoxRow>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.leechThreshold}
|
||||
defaultValue={defaults.leechThreshold}
|
||||
min={1}
|
||||
markdownTooltip={tr.deckConfigLeechThresholdTooltip()}
|
||||
>
|
||||
{tr.schedulingLeechThreshold()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.leechAction}
|
||||
defaultValue={defaults.leechAction}
|
||||
choices={leechChoices}
|
||||
breakpoint="sm"
|
||||
markdownTooltip={tr.deckConfigLeechActionTooltip()}
|
||||
>
|
||||
{tr.schedulingLeechAction()}
|
||||
</EnumSelectorRow>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.leechAction}
|
||||
defaultValue={defaults.leechAction}
|
||||
choices={leechChoices}
|
||||
breakpoint="sm"
|
||||
markdownTooltip={tr.deckConfigLeechActionTooltip()}
|
||||
>
|
||||
{tr.schedulingLeechAction()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</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 SpinBoxRow from "./SpinBoxRow.svelte";
|
||||
import EnumSelectorRow from "./EnumSelectorRow.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import Warning from "./Warning.svelte";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
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>
|
||||
|
||||
<TitledContainer title={tr.schedulingNewCards()} {api}>
|
||||
<StepsInputRow
|
||||
bind:value={$config.learnSteps}
|
||||
defaultValue={defaults.learnSteps}
|
||||
markdownTooltip={tr.deckConfigLearningStepsTooltip()}
|
||||
>
|
||||
{tr.deckConfigLearningSteps()}
|
||||
</StepsInputRow>
|
||||
<TitledContainer title={tr.schedulingNewCards()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<StepsInputRow
|
||||
bind:value={$config.learnSteps}
|
||||
defaultValue={defaults.learnSteps}
|
||||
markdownTooltip={tr.deckConfigLearningStepsTooltip()}
|
||||
>
|
||||
{tr.deckConfigLearningSteps()}
|
||||
</StepsInputRow>
|
||||
</Item>
|
||||
|
||||
<SpinBoxRow
|
||||
bind:value={$config.graduatingIntervalGood}
|
||||
defaultValue={defaults.graduatingIntervalGood}
|
||||
markdownTooltip={tr.deckConfigGraduatingIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingGraduatingInterval()}
|
||||
</SpinBoxRow>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.graduatingIntervalGood}
|
||||
defaultValue={defaults.graduatingIntervalGood}
|
||||
markdownTooltip={tr.deckConfigGraduatingIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingGraduatingInterval()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Warning warning={stepsExceedGraduatingInterval} />
|
||||
</Item>
|
||||
<Item>
|
||||
<Warning warning={stepsExceedGraduatingInterval} />
|
||||
</Item>
|
||||
|
||||
<SpinBoxRow
|
||||
bind:value={$config.graduatingIntervalEasy}
|
||||
defaultValue={defaults.graduatingIntervalEasy}
|
||||
markdownTooltip={tr.deckConfigEasyIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingEasyInterval()}
|
||||
</SpinBoxRow>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.graduatingIntervalEasy}
|
||||
defaultValue={defaults.graduatingIntervalEasy}
|
||||
markdownTooltip={tr.deckConfigEasyIntervalTooltip()}
|
||||
>
|
||||
{tr.schedulingEasyInterval()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<Warning warning={goodExceedsEasy} />
|
||||
</Item>
|
||||
<Item>
|
||||
<Warning warning={goodExceedsEasy} />
|
||||
</Item>
|
||||
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newCardInsertOrder}
|
||||
defaultValue={defaults.newCardInsertOrder}
|
||||
choices={newInsertOrderChoices}
|
||||
breakpoint={"md"}
|
||||
markdownTooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
||||
>
|
||||
{tr.deckConfigNewInsertionOrder()}
|
||||
</EnumSelectorRow>
|
||||
<Item>
|
||||
<EnumSelectorRow
|
||||
bind:value={$config.newCardInsertOrder}
|
||||
defaultValue={defaults.newCardInsertOrder}
|
||||
choices={newInsertOrderChoices}
|
||||
breakpoint={"md"}
|
||||
markdownTooltip={tr.deckConfigNewInsertionOrderTooltip()}
|
||||
>
|
||||
{tr.deckConfigNewInsertionOrder()}
|
||||
</EnumSelectorRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</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 ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
|
||||
import LabelButton from "../components/LabelButton.svelte";
|
||||
import DropdownMenu from "../components/DropdownMenu.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>
|
||||
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<LabelButton
|
||||
theme="primary"
|
||||
on:click={() => save(false)}
|
||||
tooltip={getPlatformString(saveKeyCombination)}
|
||||
>{tr.deckConfigSaveButton()}</LabelButton
|
||||
>
|
||||
<Shortcut keyCombination={saveKeyCombination} on:click={() => save(false)} />
|
||||
</ButtonGroupItem>
|
||||
<LabelButton
|
||||
theme="primary"
|
||||
on:click={() => save(false)}
|
||||
tooltip={getPlatformString(saveKeyCombination)}
|
||||
--border-left-radius="5px">{tr.deckConfigSaveButton()}</LabelButton
|
||||
>
|
||||
<Shortcut keyCombination={saveKeyCombination} on:click={() => save(false)} />
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdown let:createDropdown>
|
||||
<LabelButton
|
||||
on:mount={(event) => (dropdown = createDropdown(event.detail.button))}
|
||||
on:click={() => dropdown.toggle()}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownItem on:click={() => dispatch("add")}
|
||||
>{tr.deckConfigAddGroup()}</DropdownItem
|
||||
>
|
||||
<DropdownItem on:click={() => dispatch("clone")}
|
||||
>{tr.deckConfigCloneGroup()}</DropdownItem
|
||||
>
|
||||
<DropdownItem on:click={() => dispatch("rename")}>
|
||||
{tr.deckConfigRenameGroup()}
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={removeConfig}
|
||||
>{tr.deckConfigRemoveGroup()}</DropdownItem
|
||||
>
|
||||
<DropdownDivider />
|
||||
<DropdownItem on:click={() => save(true)}>
|
||||
{tr.deckConfigSaveToAllSubdecks()}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
</ButtonGroupItem>
|
||||
<WithDropdown let:createDropdown --border-right-radius="5px">
|
||||
<LabelButton
|
||||
on:click={() => dropdown.toggle()}
|
||||
on:mount={(event) => (dropdown = createDropdown(event.detail.button))}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<DropdownItem on:click={() => dispatch("add")}
|
||||
>{tr.deckConfigAddGroup()}</DropdownItem
|
||||
>
|
||||
<DropdownItem on:click={() => dispatch("clone")}
|
||||
>{tr.deckConfigCloneGroup()}</DropdownItem
|
||||
>
|
||||
<DropdownItem on:click={() => dispatch("rename")}>
|
||||
{tr.deckConfigRenameGroup()}
|
||||
</DropdownItem>
|
||||
<DropdownItem on:click={removeConfig}
|
||||
>{tr.deckConfigRemoveGroup()}</DropdownItem
|
||||
>
|
||||
<DropdownDivider />
|
||||
<DropdownItem on:click={() => save(true)}>
|
||||
{tr.deckConfigSaveToAllSubdecks()}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -4,6 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import TitledContainer from "./TitledContainer.svelte";
|
||||
import DynamicallySlottable from "../components/DynamicallySlottable.svelte";
|
||||
import Item from "../components/Item.svelte";
|
||||
import SpinBoxRow from "./SpinBoxRow.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;
|
||||
</script>
|
||||
|
||||
<TitledContainer title={tr.deckConfigTimerTitle()} {api}>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.capAnswerTimeToSecs}
|
||||
defaultValue={defaults.capAnswerTimeToSecs}
|
||||
min={30}
|
||||
max={600}
|
||||
markdownTooltip={tr.deckConfigMaximumAnswerSecsTooltip()}
|
||||
>
|
||||
{tr.deckConfigMaximumAnswerSecs()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
<TitledContainer title={tr.deckConfigTimerTitle()}>
|
||||
<DynamicallySlottable slotHost={Item} {api}>
|
||||
<Item>
|
||||
<SpinBoxRow
|
||||
bind:value={$config.capAnswerTimeToSecs}
|
||||
defaultValue={defaults.capAnswerTimeToSecs}
|
||||
min={30}
|
||||
max={600}
|
||||
markdownTooltip={tr.deckConfigMaximumAnswerSecsTooltip()}
|
||||
>
|
||||
{tr.deckConfigMaximumAnswerSecs()}
|
||||
</SpinBoxRow>
|
||||
</Item>
|
||||
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.showTimer}
|
||||
defaultValue={defaults.showTimer}
|
||||
markdownTooltip={tr.deckConfigShowAnswerTimerTooltip()}
|
||||
>
|
||||
{tr.schedulingShowAnswerTimer()}
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
<Item>
|
||||
<SwitchRow
|
||||
bind:value={$config.showTimer}
|
||||
defaultValue={defaults.showTimer}
|
||||
markdownTooltip={tr.deckConfigShowAnswerTimerTooltip()}
|
||||
>
|
||||
{tr.schedulingShowAnswerTimer()}
|
||||
</SwitchRow>
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</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";
|
||||
|
||||
export let title: string;
|
||||
export let api: Record<string, never> | undefined = undefined;
|
||||
</script>
|
||||
|
||||
<Container --gutter-block="2px" --container-margin="0" {api}>
|
||||
<Container --gutter-block="2px" --container-margin="0">
|
||||
<h1>{title}</h1>
|
||||
|
||||
<slot />
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 { Position } from "./location";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 { 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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
|
||||
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
|
||||
import NoteEditor from "./NoteEditor.svelte";
|
||||
import ButtonGroupItem from "../components/ButtonGroupItem.svelte";
|
||||
import PreviewButton from "./PreviewButton.svelte";
|
||||
import type { NoteEditorAPI } from "./NoteEditor.svelte";
|
||||
|
||||
const api: Partial<NoteEditorAPI> = {};
|
||||
let noteEditor: OldEditorAdapter;
|
||||
let noteEditor: NoteEditor;
|
||||
|
||||
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>
|
||||
|
||||
<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 key = Symbol("decoratedElements");
|
||||
const [set, getDecoratedElements, hasDecoratedElements] =
|
||||
const [context, setContextProperty] =
|
||||
contextProperty<CustomElementArray<DecoratedElementConstructor>>(key);
|
||||
|
||||
export { getDecoratedElements, hasDecoratedElements };
|
||||
export { context };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
set(decoratedElements);
|
||||
setContextProperty(decoratedElements);
|
||||
</script>
|
||||
|
||||
<slot />
|
||||
|
|
|
@ -22,9 +22,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
const key = Symbol("editingArea");
|
||||
const [set, getEditingArea, hasEditingArea] = contextProperty<EditingAreaAPI>(key);
|
||||
const [context, setContextProperty] = contextProperty<EditingAreaAPI>(key);
|
||||
|
||||
export { getEditingArea, hasEditingArea };
|
||||
export { context };
|
||||
</script>
|
||||
|
||||
<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(
|
||||
api,
|
||||
set({
|
||||
content,
|
||||
editingInputs: inputsStore,
|
||||
focus,
|
||||
refocus,
|
||||
}),
|
||||
);
|
||||
const api = Object.assign(apiPartial, {
|
||||
content,
|
||||
editingInputs: inputsStore,
|
||||
focus,
|
||||
refocus,
|
||||
});
|
||||
|
||||
setContextProperty(api);
|
||||
|
||||
onMount(() => {
|
||||
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 [set, getEditorField, hasEditorField] = contextProperty<EditorFieldAPI>(key);
|
||||
const [context, setContextProperty] = contextProperty<EditorFieldAPI>(key);
|
||||
|
||||
export { getEditorField, hasEditorField };
|
||||
export { context };
|
||||
</script>
|
||||
|
||||
<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 autofocus = false;
|
||||
|
||||
export let api: (Partial<EditorFieldAPI> & Destroyable) | undefined = undefined;
|
||||
|
||||
const directionStore = writable<"ltr" | "rtl">();
|
||||
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 [element, elementResolve] = promiseWithResolver<HTMLElement>();
|
||||
|
||||
const editorFieldApi = set({
|
||||
let apiPartial: Partial<EditorFieldAPI> & Destroyable;
|
||||
export { apiPartial as api };
|
||||
|
||||
const api: EditorFieldAPI & Destroyable = Object.assign(apiPartial, {
|
||||
element,
|
||||
direction: directionStore,
|
||||
editingArea: editingArea as EditingAreaAPI,
|
||||
});
|
||||
|
||||
if (api) {
|
||||
Object.assign(api, editorFieldApi);
|
||||
}
|
||||
setContextProperty(api);
|
||||
|
||||
onDestroy(() => api?.destroy());
|
||||
</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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getDecoratedElements } from "./DecoratedElements.svelte";
|
||||
import { context } from "./DecoratedElements.svelte";
|
||||
import { Mathjax } from "../editable/mathjax-element";
|
||||
|
||||
const decoratedElements = getDecoratedElements();
|
||||
const decoratedElements = context.get();
|
||||
decoratedElements.push(Mathjax);
|
||||
|
||||
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 { registerShortcut } from "../lib/shortcuts";
|
||||
import StickyBadge from "./StickyBadge.svelte";
|
||||
import OldEditorAdapter from "./OldEditorAdapter.svelte";
|
||||
import type { NoteEditorAPI } from "./OldEditorAdapter.svelte";
|
||||
import NoteEditor from "./NoteEditor.svelte";
|
||||
import type { NoteEditorAPI } from "./NoteEditor.svelte";
|
||||
|
||||
const api: Partial<NoteEditorAPI> = {};
|
||||
let noteEditor: OldEditorAdapter;
|
||||
let noteEditor: NoteEditor;
|
||||
|
||||
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);
|
||||
</script>
|
||||
|
||||
<OldEditorAdapter bind:this={noteEditor} {api}>
|
||||
<NoteEditor bind:this={noteEditor} {api}>
|
||||
<svelte:fragment slot="field-state" let:index>
|
||||
<StickyBadge active={stickies[index]} {index} />
|
||||
</svelte:fragment>
|
||||
</OldEditorAdapter>
|
||||
</NoteEditor>
|
||||
|
|
|
@ -2,14 +2,371 @@
|
|||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="ts">
|
||||
import type { Writable } from "svelte/store";
|
||||
import 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">
|
||||
<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>
|
||||
|
||||
<style lang="scss">
|
||||
.note-editor {
|
||||
height: 100%;
|
||||
|
||||
display: flex;
|
||||
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 { onMount } from "svelte";
|
||||
import { htmlOn, htmlOff } from "./icons";
|
||||
import { getEditorField } from "./EditorField.svelte";
|
||||
import { context as editorFieldContext } from "./EditorField.svelte";
|
||||
import { registerShortcut, getPlatformString } from "../lib/shortcuts";
|
||||
|
||||
const editorField = getEditorField();
|
||||
const editorField = editorFieldContext.get();
|
||||
const keyCombination = "Control+Shift+X";
|
||||
|
||||
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";
|
||||
|
||||
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 lang="ts">
|
||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { getPlatformString } from "../lib/shortcuts";
|
||||
|
||||
import LabelButton from "../../components/LabelButton.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import LabelButton from "../components/LabelButton.svelte";
|
||||
import Shortcut from "../components/Shortcut.svelte";
|
||||
|
||||
const keyCombination = "Control+Shift+P";
|
||||
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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import OldEditorAdapter from "../editor/OldEditorAdapter.svelte";
|
||||
import type { NoteEditorAPI } from "../editor/OldEditorAdapter.svelte";
|
||||
import NoteEditor from "../editor/NoteEditor.svelte";
|
||||
import type { NoteEditorAPI } from "../editor/NoteEditor.svelte";
|
||||
|
||||
const api: Partial<NoteEditorAPI> = {};
|
||||
let noteEditor: OldEditorAdapter;
|
||||
let noteEditor: NoteEditor;
|
||||
|
||||
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>
|
||||
|
||||
<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 { onMount } from "svelte";
|
||||
import { stickyOn, stickyOff } from "./icons";
|
||||
import { getEditorField } from "./EditorField.svelte";
|
||||
import { context as editorFieldContext } from "./EditorField.svelte";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
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;
|
||||
|
||||
const editorField = getEditorField();
|
||||
const editorField = editorFieldContext.get();
|
||||
const keyCombination = "F9";
|
||||
|
||||
export let index: number;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import "./legacy.css";
|
||||
import "./editor-base.css";
|
||||
|
||||
import "../lib/register-package";
|
||||
import "../lib/runtime-require";
|
||||
import "../sveltelib/export-runtime";
|
||||
|
||||
declare global {
|
||||
|
|
|
@ -4,15 +4,33 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
|
||||
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>
|
||||
|
||||
<ButtonGroup>
|
||||
{#each buttons as button}
|
||||
<ButtonGroupItem>
|
||||
{#each buttons as button, index}
|
||||
<div style={getBorderRadius(index, buttons.length)}>
|
||||
{@html button}
|
||||
</ButtonGroupItem>
|
||||
</div>
|
||||
{/each}
|
||||
</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 { getPlatformString } from "../../lib/shortcuts";
|
||||
import { getSurrounder } from "../surround";
|
||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import type { RichTextInputAPI } from "../rich-text-input";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
import { boldIcon } from "./icons";
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
const { focusInRichText, activeInput } = getNoteEditor();
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
|
||||
$: input = $activeInput;
|
||||
$: disabled = !$focusInRichText;
|
||||
$: surrounder = disabled ? null : getSurrounder(input as RichTextInputAPI);
|
||||
$: input = $focusedInput as RichTextInputAPI;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: surrounder = disabled ? null : getSurrounder(input);
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
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 { getPlatformString } from "../../lib/shortcuts";
|
||||
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 { editingInputIsRichText } from "../rich-text-input";
|
||||
import { ellipseIcon } from "./icons";
|
||||
|
||||
const noteEditor = getNoteEditor();
|
||||
const { focusInRichText, activeInput } = noteEditor;
|
||||
const { focusedInput, fields } = noteEditorContext.get();
|
||||
|
||||
const clozePattern = /\{\{c(\d+)::/gu;
|
||||
function getCurrentHighestCloze(increment: boolean): number {
|
||||
let highest = 0;
|
||||
|
||||
for (const field of noteEditor.fields) {
|
||||
for (const field of fields) {
|
||||
const content = field.editingArea?.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);
|
||||
}
|
||||
|
||||
$: richTextAPI = $activeInput as RichTextInputAPI;
|
||||
$: richTextAPI = $focusedInput as RichTextInputAPI;
|
||||
|
||||
async function onCloze(event: KeyboardEvent | MouseEvent): Promise<void> {
|
||||
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);
|
||||
}
|
||||
|
||||
$: disabled = !$focusInRichText;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
|
||||
const keyCombination = "Control+Alt?+Shift+C";
|
||||
</script>
|
||||
|
|
|
@ -4,17 +4,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import ColorPicker from "../../components/ColorPicker.svelte";
|
||||
import Shortcut from "../../components/Shortcut.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 { bridgeCommand } from "../../lib/bridgecommand";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
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";
|
||||
|
||||
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);
|
||||
};
|
||||
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
$: disabled = !$focusInRichText;
|
||||
const { focusedInput } = context.get();
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<WithColorHelper color={textColor} let:colorHelperIcon let:setColor>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingSetTextColor()} ({getPlatformString(
|
||||
forecolorKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={forecolorWrap}
|
||||
>
|
||||
{@html textColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
<Shortcut
|
||||
keyCombination={forecolorKeyCombination}
|
||||
on:action={forecolorWrap}
|
||||
/>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroup>
|
||||
<DynamicallySlottable
|
||||
slotHost={ButtonGroupItem}
|
||||
{createProps}
|
||||
{updatePropsList}
|
||||
{setSlotHostContext}
|
||||
{api}
|
||||
>
|
||||
<WithColorHelper color={textColor} let:colorHelperIcon let:setColor>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingSetTextColor()} ({getPlatformString(
|
||||
forecolorKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={forecolorWrap}
|
||||
>
|
||||
{@html textColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
<Shortcut
|
||||
keyCombination={forecolorKeyCombination}
|
||||
on:action={forecolorWrap}
|
||||
/>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingChangeColor()} ({getPlatformString(
|
||||
backcolorKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
widthMultiplier={0.5}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker
|
||||
on:change={(event) => {
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingChangeColor()} ({getPlatformString(
|
||||
backcolorKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
widthMultiplier={0.5}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker
|
||||
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);
|
||||
bridgeCommand(`lastTextColor:${textColor}`);
|
||||
forecolorWrap = wrapWithForecolor(setColor(event));
|
||||
forecolorWrap();
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
<Shortcut
|
||||
keyCombination={backcolorKeyCombination}
|
||||
on:action={(event) => {
|
||||
const textColor = setColor(event);
|
||||
bridgeCommand(`lastTextColor:${textColor}`);
|
||||
forecolorWrap = wrapWithForecolor(setColor(event));
|
||||
forecolorWrap();
|
||||
}}
|
||||
/>
|
||||
</ButtonGroupItem>
|
||||
</WithColorHelper>
|
||||
</ButtonGroupItem>
|
||||
</WithColorHelper>
|
||||
|
||||
<WithColorHelper color={highlightColor} let:colorHelperIcon let:setColor>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingSetTextHighlightColor()}
|
||||
{disabled}
|
||||
on:click={backcolorWrap}
|
||||
>
|
||||
{@html highlightColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
<WithColorHelper color={highlightColor} let:colorHelperIcon let:setColor>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingSetTextHighlightColor()}
|
||||
{disabled}
|
||||
on:click={backcolorWrap}
|
||||
>
|
||||
{@html highlightColorIcon}
|
||||
{@html colorHelperIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingChangeColor()}
|
||||
widthMultiplier={0.5}
|
||||
{disabled}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker
|
||||
on:change={(event) => {
|
||||
const highlightColor = setColor(event);
|
||||
bridgeCommand(`lastHighlightColor:${highlightColor}`);
|
||||
backcolorWrap = wrapWithBackcolor(highlightColor);
|
||||
backcolorWrap();
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
</WithColorHelper>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingChangeColor()}
|
||||
widthMultiplier={0.5}
|
||||
{disabled}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
<ColorPicker
|
||||
on:change={(event) => {
|
||||
const highlightColor = setColor(event);
|
||||
bridgeCommand(`lastHighlightColor:${highlightColor}`);
|
||||
backcolorWrap = wrapWithBackcolor(highlightColor);
|
||||
backcolorWrap();
|
||||
}}
|
||||
/>
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
</WithColorHelper>
|
||||
</DynamicallySlottable>
|
||||
</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 { 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 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 withoutState = false;
|
||||
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
|
||||
function action() {
|
||||
execCommand(key);
|
||||
}
|
||||
|
||||
$: disabled = !$focusInRichText;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
</script>
|
||||
|
||||
{#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">
|
||||
import { updateAllState, resetAllState } from "../../components/WithState.svelte";
|
||||
import type { ButtonGroupAPI } from "../../components/ButtonGroup.svelte";
|
||||
import type { ButtonToolbarAPI } from "../../components/ButtonToolbar.svelte";
|
||||
import type { DefaultSlotInterface } from "../../sveltelib/dynamic-slotting";
|
||||
|
||||
export function updateActiveButtons(event: Event) {
|
||||
updateAllState(event);
|
||||
|
@ -16,31 +15,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
export interface EditorToolbarAPI {
|
||||
toolbar: ButtonToolbarAPI;
|
||||
notetypeButtons: ButtonGroupAPI;
|
||||
formatInlineButtons: ButtonGroupAPI;
|
||||
formatBlockButtons: ButtonGroupAPI;
|
||||
colorButtons: ButtonGroupAPI;
|
||||
templateButtons: ButtonGroupAPI;
|
||||
toolbar: DefaultSlotInterface;
|
||||
notetypeButtons: DefaultSlotInterface;
|
||||
formatInlineButtons: DefaultSlotInterface;
|
||||
formatBlockButtons: DefaultSlotInterface;
|
||||
colorButtons: DefaultSlotInterface;
|
||||
templateButtons: DefaultSlotInterface;
|
||||
}
|
||||
|
||||
/* Our dynamic components */
|
||||
import AddonButtons from "./AddonButtons.svelte";
|
||||
import PreviewButton, { togglePreviewButtonState } from "./PreviewButton.svelte";
|
||||
|
||||
export const editorToolbar = {
|
||||
AddonButtons,
|
||||
PreviewButton,
|
||||
togglePreviewButtonState,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import StickyContainer from "../../components/StickyContainer.svelte";
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
|
||||
import NoteTypeButtons from "./NoteTypeButtons.svelte";
|
||||
import NotetypeButtons from "./NotetypeButtons.svelte";
|
||||
import FormatInlineButtons from "./FormatInlineButtons.svelte";
|
||||
import FormatBlockButtons from "./FormatBlockButtons.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>
|
||||
|
||||
<StickyContainer --gutter-block="0.1rem" --sticky-borders="0 0 1px">
|
||||
<ButtonToolbar {size} {wrap} api={toolbar}>
|
||||
<Item id="notetype">
|
||||
<NoteTypeButtons api={notetypeButtons} />
|
||||
</Item>
|
||||
<ButtonToolbar {size} {wrap}>
|
||||
<DynamicallySlottable slotHost={Item} api={toolbar}>
|
||||
<Item id="notetype">
|
||||
<NotetypeButtons api={notetypeButtons}>
|
||||
<slot name="notetypeButtons" />
|
||||
</NotetypeButtons>
|
||||
</Item>
|
||||
|
||||
<Item id="inlineFormatting">
|
||||
<FormatInlineButtons api={formatInlineButtons} />
|
||||
</Item>
|
||||
<Item id="inlineFormatting">
|
||||
<FormatInlineButtons api={formatInlineButtons} />
|
||||
</Item>
|
||||
|
||||
<Item id="blockFormatting">
|
||||
<FormatBlockButtons api={formatBlockButtons} />
|
||||
</Item>
|
||||
<Item id="blockFormatting">
|
||||
<FormatBlockButtons api={formatBlockButtons} />
|
||||
</Item>
|
||||
|
||||
<Item id="color">
|
||||
<ColorButtons {textColor} {highlightColor} api={colorButtons} />
|
||||
</Item>
|
||||
<Item id="color">
|
||||
<ColorButtons {textColor} {highlightColor} api={colorButtons} />
|
||||
</Item>
|
||||
|
||||
<Item id="template">
|
||||
<TemplateButtons api={templateButtons} />
|
||||
</Item>
|
||||
<Item id="template">
|
||||
<TemplateButtons api={templateButtons} />
|
||||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</ButtonToolbar>
|
||||
</StickyContainer>
|
||||
|
|
|
@ -4,13 +4,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.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 { getListItem } from "../../lib/dom";
|
||||
|
@ -27,7 +32,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
indentIcon,
|
||||
outdentIcon,
|
||||
} from "./icons";
|
||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
||||
import { context } from "../NoteEditor.svelte";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
|
||||
export let api = {};
|
||||
|
||||
|
@ -49,86 +55,87 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
$: disabled = !$focusInRichText;
|
||||
const { focusedInput } = context.get();
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="insertUnorderedList"
|
||||
tooltip={tr.editingUnorderedList()}
|
||||
shortcut="Control+,">{@html ulIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="insertOrderedList"
|
||||
tooltip={tr.editingOrderedList()}
|
||||
shortcut="Control+.">{@html olIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdown let:createDropdown>
|
||||
<IconButton
|
||||
{disabled}
|
||||
on:mount={(event) => createDropdown(event.detail.button)}
|
||||
<ButtonGroup>
|
||||
<DynamicallySlottable
|
||||
slotHost={ButtonGroupItem}
|
||||
{createProps}
|
||||
{updatePropsList}
|
||||
{setSlotHostContext}
|
||||
{api}
|
||||
>
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="insertUnorderedList"
|
||||
tooltip={tr.editingUnorderedList()}
|
||||
shortcut="Control+,">{@html ulIcon}</CommandIconButton
|
||||
>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonDropdown>
|
||||
<Item id="justify">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="insertOrderedList"
|
||||
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
|
||||
key="justifyLeft"
|
||||
tooltip={tr.editingAlignLeft()}
|
||||
withoutShortcut
|
||||
--border-left-radius="5px"
|
||||
>{@html justifyLeftIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="justifyCenter"
|
||||
tooltip={tr.editingCenter()}
|
||||
withoutShortcut
|
||||
>{@html justifyCenterIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="justifyRight"
|
||||
tooltip={tr.editingAlignRight()}
|
||||
withoutShortcut
|
||||
>{@html justifyRightIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="justifyFull"
|
||||
tooltip={tr.editingJustify()}
|
||||
withoutShortcut
|
||||
--border-right-radius="5px"
|
||||
>{@html justifyFullIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
|
||||
<Item id="indentation">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<Item id="indentation">
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
tooltip="{tr.editingOutdent()} ({getPlatformString(
|
||||
outdentKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={outdentListItem}
|
||||
--border-left-radius="5px"
|
||||
>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
|
@ -137,15 +144,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
keyCombination={outdentKeyCombination}
|
||||
on:action={outdentListItem}
|
||||
/>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingIndent()} ({getPlatformString(
|
||||
indentKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={indentListItem}
|
||||
--border-right-radius="5px"
|
||||
>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
|
@ -154,10 +160,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
keyCombination={indentKeyCombination}
|
||||
on:action={indentListItem}
|
||||
/>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
</ButtonDropdown>
|
||||
</WithDropdown>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
</ButtonDropdown>
|
||||
</WithDropdown>
|
||||
</ButtonGroupItem>
|
||||
</DynamicallySlottable>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -4,11 +4,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import CommandIconButton from "./CommandIconButton.svelte";
|
||||
import BoldButton from "./BoldButton.svelte";
|
||||
import ItalicButton from "./ItalicButton.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 { 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 = {};
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<BoldButton />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroup>
|
||||
<DynamicallySlottable
|
||||
slotHost={ButtonGroupItem}
|
||||
{createProps}
|
||||
{updatePropsList}
|
||||
{setSlotHostContext}
|
||||
{api}
|
||||
>
|
||||
<ButtonGroupItem>
|
||||
<BoldButton />
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<ItalicButton />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<ItalicButton />
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<UnderlineButton />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<UnderlineButton />
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="superscript"
|
||||
shortcut="Control+="
|
||||
tooltip={tr.editingSuperscript()}>{@html superscriptIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="superscript"
|
||||
shortcut="Control+="
|
||||
tooltip={tr.editingSuperscript()}
|
||||
>{@html superscriptIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="subscript"
|
||||
shortcut="Control+Shift+="
|
||||
tooltip={tr.editingSubscript()}>{@html subscriptIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="subscript"
|
||||
shortcut="Control+Shift+="
|
||||
tooltip={tr.editingSubscript()}>{@html subscriptIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="removeFormat"
|
||||
shortcut="Control+R"
|
||||
tooltip={tr.editingRemoveFormatting()}
|
||||
withoutState>{@html eraserIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<CommandIconButton
|
||||
key="removeFormat"
|
||||
shortcut="Control+R"
|
||||
tooltip={tr.editingRemoveFormatting()}
|
||||
withoutState>{@html eraserIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</DynamicallySlottable>
|
||||
</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 { getSurrounder } from "../surround";
|
||||
import { italicIcon } from "./icons";
|
||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import type { RichTextInputAPI } from "../rich-text-input";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
|
||||
function matchItalic(element: Element): Exclude<MatchResult, MatchResult.ALONG> {
|
||||
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;
|
||||
}
|
||||
|
||||
const { focusInRichText, activeInput } = getNoteEditor();
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
|
||||
$: input = $activeInput;
|
||||
$: disabled = !$focusInRichText;
|
||||
$: surrounder = disabled ? null : getSurrounder(input as RichTextInputAPI);
|
||||
$: input = $focusedInput as RichTextInputAPI;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: surrounder = disabled ? null : getSurrounder(input);
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return !input || input.name === "plain-text"
|
||||
return disabled
|
||||
? Promise.resolve(false)
|
||||
: 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 { wrapInternal } from "../../lib/wrap";
|
||||
import { functionIcon } from "./icons";
|
||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||
import type { RichTextInputAPI } from "../rich-text-input";
|
||||
import { editingInputIsRichText } from "../rich-text-input";
|
||||
|
||||
const { activeInput, focusInRichText } = getNoteEditor();
|
||||
$: richTextAPI = $activeInput as RichTextInputAPI;
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
$: richTextAPI = $focusedInput as RichTextInputAPI;
|
||||
|
||||
async function surround(front: string, back: string): Promise<void> {
|
||||
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()],
|
||||
];
|
||||
|
||||
$: disabled = !$focusInRichText;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
</script>
|
||||
|
||||
<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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import * as tr from "../../lib/ftl";
|
||||
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 Shortcut from "../../components/Shortcut.svelte";
|
||||
import ClozeButton from "./ClozeButton.svelte";
|
||||
import LatexButton from "./LatexButton.svelte";
|
||||
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||
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";
|
||||
|
||||
export let api = {};
|
||||
const { focusInRichText } = getNoteEditor();
|
||||
const { focusedInput } = context.get();
|
||||
|
||||
const attachmentKeyCombination = "F3";
|
||||
const attachmentKeyCombination = "F7";
|
||||
function onAttachment(): void {
|
||||
bridgeCommand("attach");
|
||||
}
|
||||
|
||||
const recordKeyCombination = "F5";
|
||||
const recordKeyCombination = "F8";
|
||||
function onRecord(): void {
|
||||
bridgeCommand("record");
|
||||
}
|
||||
|
||||
$: disabled = !$focusInRichText;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingAttachPicturesaudiovideo()} ({getPlatformString(
|
||||
attachmentKeyCombination,
|
||||
)})"
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onAttachment}
|
||||
>
|
||||
{@html paperclipIcon}
|
||||
</IconButton>
|
||||
<Shortcut keyCombination={attachmentKeyCombination} on:action={onAttachment} />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroup>
|
||||
<DynamicallySlottable
|
||||
slotHost={ButtonGroupItem}
|
||||
{createProps}
|
||||
{updatePropsList}
|
||||
{setSlotHostContext}
|
||||
{api}
|
||||
>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingAttachPicturesaudiovideo()} ({getPlatformString(
|
||||
attachmentKeyCombination,
|
||||
)})"
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onAttachment}
|
||||
>
|
||||
{@html paperclipIcon}
|
||||
</IconButton>
|
||||
<Shortcut
|
||||
keyCombination={attachmentKeyCombination}
|
||||
on:action={onAttachment}
|
||||
/>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingRecordAudio()} ({getPlatformString(
|
||||
recordKeyCombination,
|
||||
)})"
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onRecord}
|
||||
>
|
||||
{@html micIcon}
|
||||
</IconButton>
|
||||
<Shortcut keyCombination={recordKeyCombination} on:action={onRecord} />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip="{tr.editingRecordAudio()} ({getPlatformString(
|
||||
recordKeyCombination,
|
||||
)})"
|
||||
iconSize={70}
|
||||
{disabled}
|
||||
on:click={onRecord}
|
||||
>
|
||||
{@html micIcon}
|
||||
</IconButton>
|
||||
<Shortcut keyCombination={recordKeyCombination} on:action={onRecord} />
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem id="cloze">
|
||||
<ClozeButton />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem id="cloze">
|
||||
<ClozeButton />
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<LatexButton />
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroupItem>
|
||||
<LatexButton />
|
||||
</ButtonGroupItem>
|
||||
</DynamicallySlottable>
|
||||
</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 { getPlatformString } from "../../lib/shortcuts";
|
||||
import { getSurrounder } from "../surround";
|
||||
import { getNoteEditor } from "../OldEditorAdapter.svelte";
|
||||
import type { RichTextInputAPI } from "../rich-text-input";
|
||||
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> {
|
||||
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;
|
||||
}
|
||||
|
||||
const { focusInRichText, activeInput } = getNoteEditor();
|
||||
const { focusedInput } = context.get();
|
||||
|
||||
$: input = $activeInput;
|
||||
$: disabled = !$focusInRichText;
|
||||
$: surrounder = disabled ? null : getSurrounder(input as RichTextInputAPI);
|
||||
$: input = $focusedInput as RichTextInputAPI;
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: surrounder = disabled ? null : getSurrounder(input);
|
||||
|
||||
function updateStateFromActiveInput(): Promise<boolean> {
|
||||
return !input || input.name === "plain-text"
|
||||
return disabled
|
||||
? Promise.resolve(false)
|
||||
: 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 ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
|
||||
import { createEventDispatcher } from "svelte";
|
||||
|
@ -27,44 +26,40 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<ButtonGroup size={1.6} wrap={false}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatLeft()}
|
||||
active={image.style.float === "left"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "left";
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}>{@html inlineStartIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatLeft()}
|
||||
active={image.style.float === "left"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "left";
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}
|
||||
--border-left-radius="5px">{@html inlineStartIcon}</IconButton
|
||||
>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatNone()}
|
||||
active={image.style.float === "" || image.style.float === "none"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.removeProperty("float");
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatNone()}
|
||||
active={image.style.float === "" || image.style.float === "none"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.removeProperty("float");
|
||||
|
||||
if (image.getAttribute("style")?.length === 0) {
|
||||
image.removeAttribute("style");
|
||||
}
|
||||
if (image.getAttribute("style")?.length === 0) {
|
||||
image.removeAttribute("style");
|
||||
}
|
||||
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}>{@html floatNoneIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}>{@html floatNoneIcon}</IconButton
|
||||
>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatRight()}
|
||||
active={image.style.float === "right"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "right";
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}>{@html inlineEndIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatRight()}
|
||||
active={image.style.float === "right"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "right";
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}
|
||||
--border-right-radius="5px">{@html inlineEndIcon}</IconButton
|
||||
>
|
||||
</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 WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleSelection from "../HandleSelection.svelte";
|
||||
import HandleControl from "../HandleControl.svelte";
|
||||
import HandleLabel from "../HandleLabel.svelte";
|
||||
import { getRichTextInput } from "../rich-text-input";
|
||||
import { context } from "../rich-text-input";
|
||||
|
||||
import WithImageConstrained from "./WithImageConstrained.svelte";
|
||||
import FloatButtons from "./FloatButtons.svelte";
|
||||
import SizeSelect from "./SizeSelect.svelte";
|
||||
|
||||
const { container, styles } = getRichTextInput();
|
||||
const { container, styles } = context.get();
|
||||
|
||||
const sheetPromise = styles
|
||||
.addStyleTag("imageOverlay")
|
||||
|
@ -233,15 +232,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
/>
|
||||
</HandleSelection>
|
||||
<ButtonDropdown on:click={updateSizesWithDimensions}>
|
||||
<Item>
|
||||
<FloatButtons
|
||||
image={activeImage}
|
||||
on:update={dropdownObject.update}
|
||||
/>
|
||||
</Item>
|
||||
<Item>
|
||||
<SizeSelect {active} on:click={toggleActualSize} />
|
||||
</Item>
|
||||
<FloatButtons
|
||||
image={activeImage}
|
||||
on:update={dropdownObject.update}
|
||||
/>
|
||||
<SizeSelect {active} on:click={toggleActualSize} />
|
||||
</ButtonDropdown>
|
||||
{/if}
|
||||
</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 ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
|
||||
import { sizeActual, sizeMinimized } from "./icons";
|
||||
|
@ -22,12 +21,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<ButtonGroup size={1.6}>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
{active}
|
||||
flipX={$direction === "rtl"}
|
||||
tooltip={tr.editingActualSize()}
|
||||
on:click>{@html icon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<IconButton
|
||||
{active}
|
||||
flipX={$direction === "rtl"}
|
||||
tooltip={tr.editingActualSize()}
|
||||
on:click
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="5px">{@html icon}</IconButton
|
||||
>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import BrowserEditor from "./BrowserEditor.svelte";
|
||||
import { editorModules } from "./base";
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
import { globalExport } from "../lib/globals";
|
||||
import { setupI18n } from "../lib/i18n";
|
||||
|
||||
const [uiPromise, uiResolve] = promiseWithResolver();
|
||||
import { uiResolve } from "../lib/ui";
|
||||
|
||||
async function setupBrowserEditor(): Promise<void> {
|
||||
await setupI18n({ modules: editorModules });
|
||||
|
@ -20,9 +18,4 @@ async function setupBrowserEditor(): Promise<void> {
|
|||
setupBrowserEditor();
|
||||
|
||||
import * as base from "./base";
|
||||
|
||||
globalExport({
|
||||
...base,
|
||||
uiPromise,
|
||||
noteEditorPromise: uiPromise,
|
||||
});
|
||||
globalExport(base);
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import NoteCreator from "./NoteCreator.svelte";
|
||||
import { editorModules } from "./base";
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
import { globalExport } from "../lib/globals";
|
||||
import { setupI18n } from "../lib/i18n";
|
||||
|
||||
const [uiPromise, uiResolve] = promiseWithResolver();
|
||||
import { uiResolve } from "../lib/ui";
|
||||
|
||||
async function setupNoteCreator(): Promise<void> {
|
||||
await setupI18n({ modules: editorModules });
|
||||
|
@ -20,9 +18,4 @@ async function setupNoteCreator(): Promise<void> {
|
|||
setupNoteCreator();
|
||||
|
||||
import * as base from "./base";
|
||||
|
||||
globalExport({
|
||||
...base,
|
||||
uiPromise,
|
||||
noteEditorPromise: uiPromise,
|
||||
});
|
||||
globalExport(base);
|
||||
|
|
|
@ -2,11 +2,9 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import ReviewerEditor from "./ReviewerEditor.svelte";
|
||||
import { editorModules } from "./base";
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
import { globalExport } from "../lib/globals";
|
||||
import { setupI18n } from "../lib/i18n";
|
||||
|
||||
const [uiPromise, uiResolve] = promiseWithResolver();
|
||||
import { uiResolve } from "../lib/ui";
|
||||
|
||||
async function setupReviewerEditor(): Promise<void> {
|
||||
await setupI18n({ modules: editorModules });
|
||||
|
@ -20,9 +18,4 @@ async function setupReviewerEditor(): Promise<void> {
|
|||
setupReviewerEditor();
|
||||
|
||||
import * as base from "./base";
|
||||
|
||||
globalExport({
|
||||
...base,
|
||||
uiPromise,
|
||||
noteEditorPromise: uiPromise,
|
||||
});
|
||||
globalExport(base);
|
||||
|
|
|
@ -4,9 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "../../components/ButtonGroupItem.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { inlineIcon, blockIcon, deleteIcon } from "./icons";
|
||||
|
@ -25,42 +23,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<ButtonToolbar size={1.6} wrap={false}>
|
||||
<Item>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingMathjaxInline()}
|
||||
active={!isBlock}
|
||||
on:click={() => {
|
||||
isBlock = false;
|
||||
updateBlock();
|
||||
}}
|
||||
on:click>{@html inlineIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
tooltip={tr.editingMathjaxInline()}
|
||||
active={!isBlock}
|
||||
on:click={() => {
|
||||
isBlock = false;
|
||||
updateBlock();
|
||||
}}
|
||||
on:click
|
||||
--border-left-radius="5px">{@html inlineIcon}</IconButton
|
||||
>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.editingMathjaxBlock()}
|
||||
active={isBlock}
|
||||
on:click={() => {
|
||||
isBlock = true;
|
||||
updateBlock();
|
||||
}}
|
||||
on:click>{@html blockIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
<IconButton
|
||||
tooltip={tr.editingMathjaxBlock()}
|
||||
active={isBlock}
|
||||
on:click={() => {
|
||||
isBlock = true;
|
||||
updateBlock();
|
||||
}}
|
||||
on:click
|
||||
--border-right-radius="5px">{@html blockIcon}</IconButton
|
||||
>
|
||||
</ButtonGroup>
|
||||
|
||||
<Item>
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
tooltip={tr.actionsDelete()}
|
||||
on:click={() => dispatch("delete")}>{@html deleteIcon}</IconButton
|
||||
>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</Item>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
tooltip={tr.actionsDelete()}
|
||||
on:click={() => dispatch("delete")}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="5px">{@html deleteIcon}</IconButton
|
||||
>
|
||||
</ButtonGroup>
|
||||
</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 HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleControl from "../HandleControl.svelte";
|
||||
import { getRichTextInput } from "../rich-text-input";
|
||||
import { context } from "../rich-text-input";
|
||||
import MathjaxMenu from "./MathjaxMenu.svelte";
|
||||
|
||||
const { container, api } = getRichTextInput();
|
||||
const { container, api } = context.get();
|
||||
const { flushCaret, preventResubscription } = api;
|
||||
|
||||
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 { writable } from "svelte/store";
|
||||
import { pageTheme } from "../../sveltelib/theme";
|
||||
import { getDecoratedElements } from "../DecoratedElements.svelte";
|
||||
import { getEditingArea } from "../EditingArea.svelte";
|
||||
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||
import CodeMirror from "../CodeMirror.svelte";
|
||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
||||
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,
|
||||
};
|
||||
|
||||
const { editingInputs, content } = getEditingArea();
|
||||
const decoratedElements = getDecoratedElements();
|
||||
const { editingInputs, content } = editingAreaContext.get();
|
||||
const decoratedElements = decoratedElementsContext.get();
|
||||
const code = writable($content);
|
||||
|
||||
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>;
|
||||
}
|
||||
|
||||
export function editingInputIsRichText(
|
||||
editingInput: EditingInputAPI | null,
|
||||
): editingInput is RichTextInputAPI {
|
||||
return editingInput?.name === "rich-text";
|
||||
}
|
||||
|
||||
export interface RichTextInputContextAPI {
|
||||
styles: CustomStyles;
|
||||
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 [set, getRichTextInput, hasRichTextInput] =
|
||||
contextProperty<RichTextInputContextAPI>(key);
|
||||
|
||||
export { getRichTextInput, hasRichTextInput };
|
||||
const [context, setContextProperty] = contextProperty<RichTextInputContextAPI>(key);
|
||||
|
||||
import getDOMMirror from "../../sveltelib/mirror-dom";
|
||||
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,
|
||||
} = getInputManager();
|
||||
|
||||
export { getTriggerAfterInput, getTriggerOnInput, getTriggerOnNextInsert };
|
||||
export { context, getTriggerAfterInput, getTriggerOnInput, getTriggerOnNextInsert };
|
||||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
|
@ -61,8 +64,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
} from "../../lib/dom";
|
||||
import ContentEditable from "../../editable/ContentEditable.svelte";
|
||||
import { placeCaretAfterContent } from "../../domlib/place-caret";
|
||||
import { getDecoratedElements } from "../DecoratedElements.svelte";
|
||||
import { getEditingArea } from "../EditingArea.svelte";
|
||||
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||
import { promiseWithResolver } from "../../lib/promise";
|
||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||
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;
|
||||
|
||||
const { content, editingInputs } = getEditingArea();
|
||||
const decoratedElements = getDecoratedElements();
|
||||
const { content, editingInputs } = editingAreaContext.get();
|
||||
const decoratedElements = decoratedElementsContext.get();
|
||||
|
||||
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">
|
||||
{#await Promise.all( [richTextPromise, stylesPromise], ) then [container, styles]}
|
||||
<SetContext setter={set} value={{ container, styles, api }}>
|
||||
<SetContext
|
||||
setter={setContextProperty}
|
||||
value={{ container, styles, api }}
|
||||
>
|
||||
<slot />
|
||||
</SetContext>
|
||||
{/await}
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as RichTextInput, getRichTextInput } from "./RichTextInput.svelte";
|
||||
export {
|
||||
default as RichTextInput,
|
||||
context,
|
||||
editingInputIsRichText,
|
||||
} from "./RichTextInput.svelte";
|
||||
export type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 {
|
||||
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
|
||||
// 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.
|
||||
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.
|
||||
(window as any).require = function (name: string): unknown {
|
||||
const lib = runtimeLibraries[name];
|
||||
if (lib === undefined) {
|
||||
throw new Error(`Cannot require(${name}) at runtime.`);
|
||||
}
|
||||
return lib;
|
||||
};
|
||||
Object.assign(window, { require });
|
||||
|
||||
registerPackage("anki/packages", {
|
||||
// We also register require here, so add-ons can have a type-save variant of require (TODO, see AnkiPackages above)
|
||||
require,
|
||||
listPackages,
|
||||
hasPackages,
|
||||
});
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
import type { Modifier } from "./keys";
|
||||
|
||||
import { registerPackage } from "./register-package";
|
||||
import { registerPackage } from "./runtime-require";
|
||||
import {
|
||||
modifiersToPlatformString,
|
||||
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 createdFiles = {};
|
||||
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
|
||||
if (fileName.startsWith(cwd)) {
|
||||
fileName = fileName.substring(cwd.length + 1);
|
||||
|
@ -109,32 +109,28 @@ async function emitTypings(svelte: SvelteTsxFile[], deps: InputFile[]): Promise<
|
|||
// }
|
||||
|
||||
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> {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.writeFile(file, data, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
async function writeFile(file: string, data: string): Promise<void> {
|
||||
await fs.promises.writeFile(file, data);
|
||||
}
|
||||
|
||||
function readFile(file) {
|
||||
return new Promise((resolve, reject) => {
|
||||
fs.readFile(file, "utf8", (err, data) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(data);
|
||||
});
|
||||
});
|
||||
function readFile(file: string): Promise<string> {
|
||||
return fs.promises.readFile(file, "utf-8");
|
||||
}
|
||||
|
||||
async function compileSingleSvelte(
|
||||
|
|
|
@ -3,29 +3,44 @@
|
|||
|
||||
import { setContext, getContext, hasContext } from "svelte";
|
||||
|
||||
type ContextProperty<T> = [
|
||||
(value: T) => T,
|
||||
// this typing is a lie insofar that calling get
|
||||
// outside of the component's context will return undefined
|
||||
() => T,
|
||||
() => boolean,
|
||||
];
|
||||
type SetContextPropertyAction<T> = (value: T) => void;
|
||||
|
||||
function contextProperty<T>(key: symbol): ContextProperty<T> {
|
||||
function set(context: T): T {
|
||||
export interface ContextProperty<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);
|
||||
return context;
|
||||
}
|
||||
|
||||
function get(): T {
|
||||
return getContext(key);
|
||||
}
|
||||
const context = {
|
||||
get(): T {
|
||||
return getContext(key);
|
||||
},
|
||||
available(): boolean {
|
||||
return hasContext(key);
|
||||
},
|
||||
};
|
||||
|
||||
function has(): boolean {
|
||||
return hasContext(key);
|
||||
}
|
||||
|
||||
return [set, get, has];
|
||||
return [context, set];
|
||||
}
|
||||
|
||||
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
|
||||
// would not work.
|
||||
|
||||
import { runtimeLibraries } from "../lib/runtime-require";
|
||||
import { registerPackageRaw } from "../lib/runtime-require";
|
||||
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
|
||||
|
||||
import { readable, get } from "svelte/store";
|
||||
import { registerPackage } from "../lib/register-package";
|
||||
import { registerPackage } from "../lib/runtime-require";
|
||||
|
||||
interface ThemeInfo {
|
||||
isDark: boolean;
|
||||
|
|
0
ts/sveltelib/types.ts
Normal file
0
ts/sveltelib/types.ts
Normal file
Loading…
Reference in a new issue