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:
Henrik Giesel 2022-02-03 05:52:11 +01:00 committed by GitHub
parent 8afe36b8e9
commit a981e56008
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
89 changed files with 2227 additions and 1957 deletions

View file

@ -2,5 +2,6 @@ licenses.json
vendor
node_modules
bazel-*
.bazel
ftl/usage
.mypy_cache
.mypy_cache

View file

@ -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:

View file

@ -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)

View file

@ -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>

View file

@ -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>

View file

@ -42,6 +42,7 @@ svelte_check(
"//sass:breakpoints_lib",
"//sass/bootstrap",
"@npm//@types/bootstrap",
"//ts/lib:lib_pkg",
"//ts/sveltelib:sveltelib_pkg",
],
)

View file

@ -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>

View file

@ -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">

View file

@ -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">

View file

@ -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;

View file

@ -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">

View file

@ -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}

View 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>

View file

@ -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">

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>;
}

View file

@ -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;
}

View file

@ -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,
};
}

View file

@ -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

View file

@ -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>

View file

@ -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>

View file

@ -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",

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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 />

View file

@ -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";

View file

@ -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";

View file

@ -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>

View file

@ -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 />

View file

@ -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) {

View file

@ -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>

View file

@ -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";

View file

@ -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>

View file

@ -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;
}

View file

@ -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>

View file

@ -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;

View file

@ -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 {

View file

@ -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} />

View file

@ -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;

View file

@ -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 {

View file

@ -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>

View file

@ -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);

View file

@ -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>

View file

@ -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>

View file

@ -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}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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);
}

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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);
}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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);

View file

@ -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);

View file

@ -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);

View file

@ -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>

View file

@ -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("");

View file

@ -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 {

View file

@ -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}

View file

@ -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";

View file

@ -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
View 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
View 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);
}
}

View file

@ -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",
},
);

View file

@ -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,
});

View file

@ -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
View 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 };

View file

@ -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(

View file

@ -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;

View 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 };

View file

@ -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);

View 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;

View file

@ -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
View file