Merge pull request #1159 from hgiesel/newapproach
Prefer idiomatic Svelte features over dynamic components
This commit is contained in:
commit
010c3da770
@ -68,7 +68,6 @@ from aqt.utils import (
|
||||
saveGeom,
|
||||
saveSplitter,
|
||||
saveState,
|
||||
shortcut,
|
||||
showInfo,
|
||||
showWarning,
|
||||
skip_if_selection_is_empty,
|
||||
@ -383,23 +382,11 @@ class Browser(QMainWindow):
|
||||
|
||||
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
||||
editor.web.eval(
|
||||
f"""
|
||||
$editorToolbar.then(({{ addButton }}) => addButton(editorToolbar.labelButton({{
|
||||
label: `{tr.actions_preview()}`,
|
||||
tooltip: `{tr.browsing_preview_selected_card(val=shortcut(preview_shortcut))}`,
|
||||
onClick: () => bridgeCommand("preview"),
|
||||
disables: false,
|
||||
}}), "notetype", -1));
|
||||
"""
|
||||
"$editorToolbar.then(({ notetypeButtons }) => notetypeButtons.appendButton({ component: editorToolbar.PreviewButton, id: 'preview' }));"
|
||||
)
|
||||
|
||||
def add_preview_shortcut(cuts: List[Tuple], editor: Editor) -> None:
|
||||
cuts.append(("Ctrl+Shift+P", self.onTogglePreview, True))
|
||||
|
||||
gui_hooks.editor_did_init.append(add_preview_button)
|
||||
gui_hooks.editor_did_init_shortcuts.append(add_preview_shortcut)
|
||||
self.editor = aqt.editor.Editor(self.mw, self.form.fieldsArea, self)
|
||||
gui_hooks.editor_did_init_shortcuts.remove(add_preview_shortcut)
|
||||
gui_hooks.editor_did_init.remove(add_preview_button)
|
||||
|
||||
@ensure_editor_saved
|
||||
|
@ -76,18 +76,9 @@ audio = (
|
||||
)
|
||||
|
||||
_html = """
|
||||
<style>
|
||||
:root {
|
||||
--bg-color: %s;
|
||||
}
|
||||
</style>
|
||||
<div>
|
||||
<div id="editorToolbar"></div>
|
||||
<div id="fields">
|
||||
</div>
|
||||
<div id="dupes" class="is-inactive">
|
||||
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
||||
</div>
|
||||
<div id="fields"></div>
|
||||
<div id="dupes" class="is-inactive">
|
||||
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
||||
</div>
|
||||
"""
|
||||
|
||||
@ -135,10 +126,9 @@ class Editor:
|
||||
self.web.set_bridge_command(self.onBridgeCmd, self)
|
||||
self.outerLayout.addWidget(self.web, 1)
|
||||
|
||||
bgcol = self.mw.app.palette().window().color().name() # type: ignore
|
||||
# then load page
|
||||
self.web.stdHtml(
|
||||
_html % (bgcol, tr.editing_show_duplicates()),
|
||||
_html % tr.editing_show_duplicates(),
|
||||
css=[
|
||||
"css/editor.css",
|
||||
],
|
||||
@ -155,7 +145,7 @@ class Editor:
|
||||
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
||||
|
||||
lefttopbtns_defs = [
|
||||
f"$editorToolbar.then(({{ addButton }}) => addButton(editorToolbar.rawButton({{ html: `{button}` }}), 'notetype', -1));"
|
||||
f"$editorToolbar.then(({{ notetypeButtons }}) => notetypeButtons.appendButton({{ component: editorToolbar.Raw, props: {{ html: {json.dumps(button)} }} }}, -1));"
|
||||
for button in lefttopbtns
|
||||
]
|
||||
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
||||
@ -165,20 +155,16 @@ class Editor:
|
||||
# legacy filter
|
||||
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
||||
|
||||
righttopbtns_defs = "\n".join(
|
||||
[
|
||||
f"editorToolbar.rawButton({{ html: `{button}` }}),"
|
||||
for button in righttopbtns
|
||||
]
|
||||
)
|
||||
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
|
||||
righttopbtns_js = (
|
||||
f"""
|
||||
$editorToolbar.then(({{ addButton }}) => addButton(editorToolbar.buttonGroup({{
|
||||
id: "addons",
|
||||
items: [ {righttopbtns_defs} ]
|
||||
}}), -1));
|
||||
$editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||
component: editorToolbar.AddonButtons,
|
||||
id: "addons",
|
||||
props: {{ buttons: [ {righttopbtns_defs} ] }},
|
||||
}}));
|
||||
"""
|
||||
if righttopbtns_defs
|
||||
if len(righttopbtns) > 0
|
||||
else ""
|
||||
)
|
||||
|
||||
@ -1278,11 +1264,11 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
||||
def set_cloze_button(editor: Editor) -> None:
|
||||
if editor.note.model()["type"] == MODEL_CLOZE:
|
||||
editor.web.eval(
|
||||
'$editorToolbar.then(({ showButton }) => showButton("template", "cloze")); '
|
||||
'$editorToolbar.then(({ templateButtons }) => templateButtons.showButton("cloze")); '
|
||||
)
|
||||
else:
|
||||
editor.web.eval(
|
||||
'$editorToolbar.then(({ hideButton }) => hideButton("template", "cloze")); '
|
||||
'$editorToolbar.then(({ templateButtons }) => templateButtons.hideButton("cloze")); '
|
||||
)
|
||||
|
||||
|
||||
|
@ -3,7 +3,6 @@ load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||
load("//ts:prettier.bzl", "prettier_test")
|
||||
load("//ts:eslint.bzl", "eslint_test")
|
||||
load("//ts:esbuild.bzl", "esbuild")
|
||||
load("//ts:compile_sass.bzl", "compile_sass")
|
||||
|
||||
svelte_files = glob(["*.svelte"])
|
||||
|
||||
@ -25,22 +24,9 @@ compile_svelte(
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
compile_sass(
|
||||
srcs = [
|
||||
"bootstrap.scss",
|
||||
"legacy.scss",
|
||||
],
|
||||
group = "local_css",
|
||||
deps = [
|
||||
"//ts/sass:button_mixins_lib",
|
||||
"//ts/sass/bootstrap",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "editor-toolbar",
|
||||
module_name = "editor-toolbar",
|
||||
name = "components",
|
||||
module_name = "components",
|
||||
srcs = glob(
|
||||
["*.ts"],
|
||||
exclude = ["*.test.ts"],
|
||||
@ -55,7 +41,7 @@ ts_library(
|
||||
"@npm//@types/bootstrap",
|
||||
"@npm//bootstrap",
|
||||
"@npm//svelte",
|
||||
],
|
||||
] + svelte_names,
|
||||
)
|
||||
|
||||
# Tests
|
48
ts/components/ButtonDropdown.svelte
Normal file
48
ts/components/ButtonDropdown.svelte
Normal file
@ -0,0 +1,48 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { setContext } from "svelte";
|
||||
import { dropdownKey } from "./contextKeys";
|
||||
|
||||
import WithTheming from "./WithTheming.svelte";
|
||||
import ButtonToolbar from "./ButtonToolbar.svelte";
|
||||
|
||||
export let id: string | undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
export let api: Record<string, unknown> | undefined = undefined;
|
||||
|
||||
setContext(dropdownKey, null);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.dropdown-menu.btn-dropdown-menu) {
|
||||
display: none;
|
||||
min-width: 0;
|
||||
padding: calc(var(--toolbar-size) / 10) 0;
|
||||
|
||||
background-color: var(--window-bg);
|
||||
border-color: var(--medium-border);
|
||||
|
||||
:global(.btn-group) {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dropdown-menu.btn-dropdown-menu.show) {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<WithTheming style="--toolbar-wrap: nowrap; ">
|
||||
<ButtonToolbar
|
||||
{id}
|
||||
class={`dropdown-menu btn-dropdown-menu ${className}`}
|
||||
nowrap={true}
|
||||
{api}>
|
||||
<slot />
|
||||
</ButtonToolbar>
|
||||
</WithTheming>
|
101
ts/components/ButtonGroup.svelte
Normal file
101
ts/components/ButtonGroup.svelte
Normal file
@ -0,0 +1,101 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import ButtonGroupItem from "./ButtonGroupItem.svelte";
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import { buttonGroupKey } from "./contextKeys";
|
||||
import type { Identifier } from "./identifier";
|
||||
import { insert, add } from "./identifier";
|
||||
import type { ButtonRegistration } from "./buttons";
|
||||
import { ButtonPosition } from "./buttons";
|
||||
import type { SvelteComponent } from "./registration";
|
||||
import { makeInterface } from "./registration";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
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()) {
|
||||
if ($items.length === 1) {
|
||||
item.position.set(ButtonPosition.Standalone);
|
||||
} else if (index === 0) {
|
||||
item.position.set(ButtonPosition.Leftmost);
|
||||
} else if (index === $items.length - 1) {
|
||||
item.position.set(ButtonPosition.Rightmost);
|
||||
} else {
|
||||
item.position.set(ButtonPosition.Center);
|
||||
}
|
||||
}
|
||||
|
||||
setContext(buttonGroupKey, registerComponent);
|
||||
|
||||
export let api: Record<string, unknown> | undefined = undefined;
|
||||
let buttonGroupRef: HTMLDivElement;
|
||||
|
||||
$: if (api && buttonGroupRef) {
|
||||
const { addComponent, updateRegistration } = getDynamicInterface(
|
||||
buttonGroupRef
|
||||
);
|
||||
|
||||
const insertButton = (button: SvelteComponent, position: Identifier = 0) =>
|
||||
addComponent(button, (added, parent) => insert(added, parent, position));
|
||||
const appendButton = (button: SvelteComponent, position: Identifier = -1) =>
|
||||
addComponent(button, (added, parent) => add(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,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
flex-wrap: var(--toolbar-wrap);
|
||||
padding: calc(var(--toolbar-size) / 10);
|
||||
margin: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
bind:this={buttonGroupRef}
|
||||
{id}
|
||||
class={`btn-group ${className}`}
|
||||
dir="ltr"
|
||||
role="group">
|
||||
<slot />
|
||||
{#each $dynamicItems as item}
|
||||
<ButtonGroupItem id={item[0].id} registration={item[1]}>
|
||||
<svelte:component this={item[0].component} {...item[0].props} />
|
||||
</ButtonGroupItem>
|
||||
{/each}
|
||||
</div>
|
64
ts/components/ButtonGroupItem.svelte
Normal file
64
ts/components/ButtonGroupItem.svelte
Normal file
@ -0,0 +1,64 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import WithTheming from "components/WithTheming.svelte";
|
||||
import Detachable from "components/Detachable.svelte";
|
||||
|
||||
import type { ButtonRegistration } from "./buttons";
|
||||
import { ButtonPosition } from "./buttons";
|
||||
import type { Register } from "./registration";
|
||||
|
||||
import { getContext, hasContext } from "svelte";
|
||||
import { buttonGroupKey } from "./contextKeys";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
export let registration: ButtonRegistration | undefined = undefined;
|
||||
|
||||
let detached: boolean;
|
||||
let position_: ButtonPosition;
|
||||
let style: string;
|
||||
|
||||
const radius = "calc(var(--toolbar-size) / 7.5)";
|
||||
|
||||
$: {
|
||||
switch (position_) {
|
||||
case ButtonPosition.Standalone:
|
||||
style = `--border-left-radius: ${radius}; --border-right-radius: ${radius}; `;
|
||||
break;
|
||||
case ButtonPosition.Leftmost:
|
||||
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.Rightmost:
|
||||
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;
|
||||
}
|
||||
</script>
|
||||
|
||||
<!-- div in WithTheming is necessary to preserve item position -->
|
||||
<WithTheming {id} {style}>
|
||||
<Detachable {detached}>
|
||||
<slot />
|
||||
</Detachable>
|
||||
</WithTheming>
|
78
ts/components/ButtonToolbar.svelte
Normal file
78
ts/components/ButtonToolbar.svelte
Normal file
@ -0,0 +1,78 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { setContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
import ButtonToolbarItem from "./ButtonToolbarItem.svelte";
|
||||
import type { ButtonGroupRegistration } from "./buttons";
|
||||
import { buttonToolbarKey } from "./contextKeys";
|
||||
import type { Identifier } from "./identifier";
|
||||
import { insert, add } from "./identifier";
|
||||
import type { SvelteComponent } from "./registration";
|
||||
import { makeInterface } from "./registration";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
export let nowrap = false;
|
||||
|
||||
function makeRegistration(): ButtonGroupRegistration {
|
||||
const detach = writable(false);
|
||||
return { detach };
|
||||
}
|
||||
|
||||
const { registerComponent, dynamicItems, getDynamicInterface } = makeInterface(
|
||||
makeRegistration
|
||||
);
|
||||
|
||||
setContext(buttonToolbarKey, registerComponent);
|
||||
|
||||
export let api: Record<string, unknown> | undefined = undefined;
|
||||
let buttonToolbarRef: HTMLDivElement;
|
||||
|
||||
$: if (buttonToolbarRef && api) {
|
||||
const { addComponent, updateRegistration } = getDynamicInterface(
|
||||
buttonToolbarRef
|
||||
);
|
||||
|
||||
const insertGroup = (group: SvelteComponent, position: Identifier = 0) =>
|
||||
addComponent(group, (added, parent) => insert(added, parent, position));
|
||||
const appendGroup = (group: SvelteComponent, position: Identifier = -1) =>
|
||||
addComponent(group, (added, parent) => add(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,
|
||||
});
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={buttonToolbarRef}
|
||||
{id}
|
||||
class={`btn-toolbar ${className}`}
|
||||
class:flex-nowrap={nowrap}
|
||||
role="toolbar">
|
||||
<slot />
|
||||
{#each $dynamicItems as item}
|
||||
<ButtonToolbarItem id={item[0].id} registration={item[1]}>
|
||||
<svelte:component this={item[0].component} {...item[0].props} />
|
||||
</ButtonToolbarItem>
|
||||
{/each}
|
||||
</div>
|
44
ts/components/ButtonToolbarItem.svelte
Normal file
44
ts/components/ButtonToolbarItem.svelte
Normal file
@ -0,0 +1,44 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import Detachable from "components/Detachable.svelte";
|
||||
|
||||
import type { ButtonGroupRegistration } from "./buttons";
|
||||
import type { Register } from "./registration";
|
||||
|
||||
import { getContext, hasContext } from "svelte";
|
||||
import { buttonToolbarKey } from "./contextKeys";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
export let registration: ButtonGroupRegistration | undefined = undefined;
|
||||
|
||||
let detached: boolean;
|
||||
|
||||
if (registration) {
|
||||
const { detach } = registration;
|
||||
detach.subscribe((value: boolean) => (detached = value));
|
||||
} else if (hasContext(buttonToolbarKey)) {
|
||||
const registerComponent = getContext<Register<ButtonGroupRegistration>>(
|
||||
buttonToolbarKey
|
||||
);
|
||||
const { detach } = registerComponent();
|
||||
detach.subscribe((value: boolean) => (detached = value));
|
||||
} else {
|
||||
detached = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
|
||||
<!-- div is necessary to preserve item position -->
|
||||
<div {id}>
|
||||
<Detachable {detached}>
|
||||
<slot />
|
||||
</Detachable>
|
||||
</div>
|
@ -5,32 +5,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<script lang="typescript">
|
||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||
import { nightModeKey } from "./contextKeys";
|
||||
import { mergeTooltipAndShortcut } from "./helpers";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string | undefined;
|
||||
export let shortcutLabel: string | undefined;
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
||||
|
||||
export let onChange: (event: Event) => void;
|
||||
|
||||
function extendClassName(className: string): string {
|
||||
return `btn ${className}`;
|
||||
}
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
const nightMode = getContext(nightModeKey);
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
let inputRef: HTMLInputElement;
|
||||
|
||||
function delegateToInput() {
|
||||
inputRef.click();
|
||||
}
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
let inputRef: HTMLInputElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
onMount(() => dispatch("mount", { button: buttonRef, input: inputRef }));
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@ -40,13 +32,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
@import "ts/sass/bootstrap/variables";
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
|
||||
width: calc(var(--toolbar-size) - 0px);
|
||||
height: calc(var(--toolbar-size) - 0px);
|
||||
|
||||
padding: 4px;
|
||||
overflow: hidden;
|
||||
|
||||
border-top-left-radius: var(--border-left-radius);
|
||||
border-bottom-left-radius: var(--border-left-radius);
|
||||
|
||||
border-top-right-radius: var(--border-right-radius);
|
||||
border-bottom-right-radius: var(--border-right-radius);
|
||||
}
|
||||
|
||||
@include button.btn-day($with-disabled: false) using ($base) {
|
||||
@ -68,11 +64,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
bind:this={buttonRef}
|
||||
tabindex="-1"
|
||||
{id}
|
||||
class={extendClassName(className)}
|
||||
class={`btn ${className}`}
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
{title}
|
||||
title={tooltip}
|
||||
on:click={delegateToInput}
|
||||
on:mousedown|preventDefault>
|
||||
<input tabindex="-1" bind:this={inputRef} type="color" on:change={onChange} />
|
||||
<input tabindex="-1" bind:this={inputRef} type="color" on:change />
|
||||
</button>
|
11
ts/components/Detachable.svelte
Normal file
11
ts/components/Detachable.svelte
Normal file
@ -0,0 +1,11 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
export let detached = false;
|
||||
</script>
|
||||
|
||||
{#if !detached}
|
||||
<slot />
|
||||
{/if}
|
@ -6,13 +6,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||
import { nightModeKey } from "./contextKeys";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string;
|
||||
export let label: string;
|
||||
export let shortcutLabel: string | undefined;
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
export let onClick: (event: MouseEvent) => void;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
@ -28,6 +26,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
button {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
||||
font-size: calc(var(--toolbar-size) / 2.3);
|
||||
}
|
||||
|
||||
.btn-day {
|
||||
@ -52,15 +52,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
span {
|
||||
font-size: calc(var(--toolbar-size) / 2.3);
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.monospace {
|
||||
font-family: monospace;
|
||||
}
|
||||
</style>
|
||||
|
||||
<button
|
||||
@ -70,8 +61,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
title={tooltip}
|
||||
on:click={onClick}
|
||||
on:click
|
||||
on:mousedown|preventDefault>
|
||||
<span class:me-3={shortcutLabel}>{label}</span>
|
||||
{#if shortcutLabel}<span class="monospace">{shortcutLabel}</span>{/if}
|
||||
<slot />
|
||||
</button>
|
23
ts/components/DropdownMenu.svelte
Normal file
23
ts/components/DropdownMenu.svelte
Normal file
@ -0,0 +1,23 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { setContext } from "svelte";
|
||||
import { dropdownKey } from "./contextKeys";
|
||||
|
||||
export let id: string | undefined;
|
||||
|
||||
setContext(dropdownKey, null);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
background-color: var(--frame-bg);
|
||||
border-color: var(--medium-border);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div {id} class="dropdown-menu">
|
||||
<slot />
|
||||
</div>
|
@ -5,38 +5,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<script lang="typescript">
|
||||
import type { Readable } from "svelte/store";
|
||||
import { getContext, onMount, createEventDispatcher } from "svelte";
|
||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||
import { mergeTooltipAndShortcut } from "./helpers";
|
||||
import { disabledKey, nightModeKey, dropdownKey } from "./contextKeys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string | undefined;
|
||||
export let shortcutLabel: string | undefined;
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
||||
|
||||
export let onClick: (event: MouseEvent) => void;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let active = false;
|
||||
export let disables = true;
|
||||
export let dropdownToggle = false;
|
||||
|
||||
$: extraProps = dropdownToggle
|
||||
? {
|
||||
"data-bs-toggle": "dropdown",
|
||||
"aria-expanded": "false",
|
||||
}
|
||||
: {};
|
||||
export let tabbable = false;
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
function extendClassName(className: string): string {
|
||||
return `btn ${className}`;
|
||||
}
|
||||
|
||||
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||
$: _disabled = disables && $disabled;
|
||||
|
||||
const nightMode = getContext<boolean>(nightModeKey);
|
||||
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
@ -47,6 +34,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
button {
|
||||
padding: 0;
|
||||
@include button.btn-border-radius;
|
||||
}
|
||||
|
||||
@include button.btn-day;
|
||||
@ -77,16 +65,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
class={extendClassName(className)}
|
||||
class={`btn ${className}`}
|
||||
class:active
|
||||
class:dropdown-toggle={dropdownToggle}
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
tabindex="-1"
|
||||
{title}
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
disabled={_disabled}
|
||||
{...extraProps}
|
||||
on:click={onClick}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
on:click
|
||||
on:mousedown|preventDefault>
|
||||
<span class="p-1"><slot /></span>
|
||||
</button>
|
@ -5,38 +5,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<script lang="typescript">
|
||||
import type { Readable } from "svelte/store";
|
||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||
import { mergeTooltipAndShortcut } from "./helpers";
|
||||
import { disabledKey, nightModeKey, dropdownKey } from "./contextKeys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string | undefined;
|
||||
export let shortcutLabel: string | undefined;
|
||||
export let label: string;
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
||||
|
||||
export let onClick: (event: MouseEvent) => void;
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let active = false;
|
||||
export let disables = true;
|
||||
export let dropdownToggle = false;
|
||||
|
||||
$: extraProps = dropdownToggle
|
||||
? {
|
||||
"data-bs-toggle": "dropdown",
|
||||
"aria-expanded": "false",
|
||||
}
|
||||
: {};
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
function extendClassName(className: string): string {
|
||||
return `btn ${className}`;
|
||||
}
|
||||
export let tabbable = false;
|
||||
|
||||
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||
$: _disabled = disables && $disabled;
|
||||
|
||||
const nightMode = getContext<boolean>(nightModeKey);
|
||||
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
@ -50,6 +37,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
font-size: calc(var(--toolbar-size) / 2.3);
|
||||
width: auto;
|
||||
height: var(--toolbar-size);
|
||||
|
||||
@include button.btn-border-radius;
|
||||
}
|
||||
|
||||
@include button.btn-day;
|
||||
@ -59,15 +48,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
class={extendClassName(className)}
|
||||
class:dropdown-toggle={dropdownToggle}
|
||||
class={`btn ${className}`}
|
||||
class:active
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
tabindex="-1"
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
disabled={_disabled}
|
||||
{title}
|
||||
{...extraProps}
|
||||
on:click={onClick}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
on:click
|
||||
on:mousedown|preventDefault>
|
||||
{label}
|
||||
<slot />
|
||||
</button>
|
@ -4,11 +4,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { Readable } from "svelte/store";
|
||||
import type { Option } from "./SelectButton";
|
||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||
import { disabledKey } from "./contextKeys";
|
||||
import SelectOption from "./SelectOption.svelte";
|
||||
|
||||
interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
selected?: false;
|
||||
}
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string;
|
26
ts/components/StickyBar.svelte
Normal file
26
ts/components/StickyBar.svelte
Normal file
@ -0,0 +1,26 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string | undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: var(--window-bg);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
</style>
|
||||
|
||||
<nav {id} class={`pb-1 ${className}`}>
|
||||
<slot />
|
||||
</nav>
|
42
ts/components/WithDropdownMenu.svelte
Normal file
42
ts/components/WithDropdownMenu.svelte
Normal file
@ -0,0 +1,42 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||
|
||||
import { setContext } from "svelte";
|
||||
import { dropdownKey } from "./contextKeys";
|
||||
|
||||
setContext(dropdownKey, {
|
||||
dropdown: true,
|
||||
"data-bs-toggle": "dropdown",
|
||||
"aria-expanded": "false",
|
||||
});
|
||||
|
||||
const menuId = Math.random().toString(36).substring(2);
|
||||
|
||||
/* Normally dropdown and trigger are associated with a
|
||||
/* common ancestor with .dropdown class */
|
||||
function createDropdown(event: CustomEvent): void {
|
||||
const button: HTMLButtonElement = event.detail.button;
|
||||
|
||||
/* Prevent focus on menu activation */
|
||||
const noop = () => {};
|
||||
Object.defineProperty(button, "focus", { value: noop });
|
||||
|
||||
const menu = (button.getRootNode() as Document) /* or shadow root */
|
||||
.getElementById(menuId);
|
||||
|
||||
if (!menu) {
|
||||
console.log(`Could not find menu "${menuId}" for dropdown menu.`);
|
||||
} else {
|
||||
const dropdown = new Dropdown(button);
|
||||
|
||||
/* Set custom menu without using common element with .dropdown */
|
||||
(dropdown as any)._menu = menu;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {createDropdown} {menuId} />
|
@ -3,25 +3,15 @@ Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||
import type { ToolbarItem } from "./types";
|
||||
import type { Modifier } from "lib/shortcuts";
|
||||
|
||||
import { onDestroy } from "svelte";
|
||||
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
||||
|
||||
export let button: ToolbarItem;
|
||||
export let shortcut: string;
|
||||
export let optionalModifiers: Modifier[];
|
||||
export let optionalModifiers: Modifier[] | undefined = [];
|
||||
|
||||
function extend({ ...rest }: DynamicSvelteComponent): DynamicSvelteComponent {
|
||||
const shortcutLabel = getPlatformString(shortcut);
|
||||
|
||||
return {
|
||||
shortcutLabel,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
const shortcutLabel = getPlatformString(shortcut);
|
||||
|
||||
let deregister: () => void;
|
||||
|
||||
@ -29,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
const mounted: HTMLButtonElement = detail.button;
|
||||
deregister = registerShortcut(
|
||||
(event: KeyboardEvent) => {
|
||||
mounted.dispatchEvent(new KeyboardEvent("click", event));
|
||||
mounted.dispatchEvent(new MouseEvent("click", event));
|
||||
event.preventDefault();
|
||||
},
|
||||
shortcut,
|
||||
@ -40,7 +30,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
onDestroy(() => deregister());
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
this={button.component}
|
||||
{...extend(button)}
|
||||
on:mount={createShortcut} />
|
||||
<slot {createShortcut} {shortcutLabel} />
|
69
ts/components/WithState.svelte
Normal file
69
ts/components/WithState.svelte
Normal file
@ -0,0 +1,69 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript" context="module">
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type UpdaterMap = Map<string, (event: Event) => boolean>;
|
||||
type StateMap = Map<string, boolean>;
|
||||
|
||||
const updaterMap = new Map() as UpdaterMap;
|
||||
const stateMap = new Map() as StateMap;
|
||||
const stateStore = writable(stateMap);
|
||||
|
||||
function updateAllStateWithCallback(callback: (key: string) => boolean): void {
|
||||
stateStore.update(
|
||||
(map: StateMap): StateMap => {
|
||||
const newMap = new Map() as StateMap;
|
||||
|
||||
for (const key of map.keys()) {
|
||||
newMap.set(key, callback(key));
|
||||
}
|
||||
|
||||
return newMap;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function updateAllState(event: Event): void {
|
||||
updateAllStateWithCallback((key: string): boolean =>
|
||||
updaterMap.get(key)!(event)
|
||||
);
|
||||
}
|
||||
|
||||
export function resetAllState(state: boolean): void {
|
||||
updateAllStateWithCallback((): boolean => state);
|
||||
}
|
||||
|
||||
function updateStateByKey(key: string, event: Event): void {
|
||||
stateStore.update(
|
||||
(map: StateMap): StateMap => {
|
||||
map.set(key, updaterMap.get(key)!(event));
|
||||
return map;
|
||||
}
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
export let key: string;
|
||||
export let update: (event: Event) => boolean;
|
||||
|
||||
let state: boolean = false;
|
||||
|
||||
updaterMap.set(key, update);
|
||||
|
||||
stateStore.subscribe((map: StateMap): (() => void) => {
|
||||
state = Boolean(map.get(key));
|
||||
return () => map.delete(key);
|
||||
});
|
||||
|
||||
stateMap.set(key, state);
|
||||
|
||||
function updateState(event: Event): void {
|
||||
updateStateByKey(key, event);
|
||||
}
|
||||
</script>
|
||||
|
||||
<slot {state} {updateState} />
|
18
ts/components/WithTheming.svelte
Normal file
18
ts/components/WithTheming.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
export let id: string | undefined = undefined;
|
||||
export let style: string;
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div {id} {style}>
|
||||
<slot />
|
||||
</div>
|
19
ts/components/buttons.ts
Normal file
19
ts/components/buttons.ts
Normal file
@ -0,0 +1,19 @@
|
||||
// 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";
|
||||
|
||||
export enum ButtonPosition {
|
||||
Standalone,
|
||||
Leftmost,
|
||||
Center,
|
||||
Rightmost,
|
||||
}
|
||||
|
||||
export interface ButtonRegistration {
|
||||
detach: Writable<boolean>;
|
||||
position: Writable<ButtonPosition>;
|
||||
}
|
||||
|
||||
export interface ButtonGroupRegistration {
|
||||
detach: Writable<boolean>;
|
||||
}
|
@ -2,3 +2,7 @@
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export const nightModeKey = Symbol("nightMode");
|
||||
export const disabledKey = Symbol("disabled");
|
||||
|
||||
export const buttonToolbarKey = Symbol("buttonToolbar");
|
||||
export const buttonGroupKey = Symbol("buttonGroup");
|
||||
export const dropdownKey = Symbol("dropdown");
|
@ -1,8 +1,7 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
export interface DropdownMenuProps {
|
||||
id: string;
|
||||
items: ToolbarItem[];
|
||||
export interface DropdownProps {
|
||||
dropdown: boolean;
|
||||
"data-bs-toggle"?: string;
|
||||
"aria-expanded"?: string;
|
||||
}
|
87
ts/components/identifier.ts
Normal file
87
ts/components/identifier.ts
Normal file
@ -0,0 +1,87 @@
|
||||
// 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 find(
|
||||
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 insert(
|
||||
element: Element,
|
||||
collection: Element,
|
||||
idOrIndex: Identifier
|
||||
): number {
|
||||
const match = find(collection.children, idOrIndex);
|
||||
|
||||
if (match) {
|
||||
const [index, reference] = match;
|
||||
collection.insertBefore(element, reference[0]);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
export function add(
|
||||
element: Element,
|
||||
collection: Element,
|
||||
idOrIndex: Identifier
|
||||
): number {
|
||||
const match = find(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 update(
|
||||
f: (element: Element) => void,
|
||||
collection: Element,
|
||||
idOrIndex: Identifier
|
||||
): number {
|
||||
const match = find(collection.children, idOrIndex);
|
||||
|
||||
if (match) {
|
||||
const [index, element] = match;
|
||||
f(element[0]);
|
||||
|
||||
return index;
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
117
ts/components/registration.ts
Normal file
117
ts/components/registration.ts
Normal file
@ -0,0 +1,117 @@
|
||||
// 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 { Readable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
import type { Identifier } from "./identifier";
|
||||
import { find } from "./identifier";
|
||||
|
||||
export interface SvelteComponent {
|
||||
component: SvelteComponentTyped;
|
||||
id: string;
|
||||
props: Record<string, unknown> | undefined;
|
||||
}
|
||||
|
||||
export type Register<T> = (index?: number, registration?: T) => T;
|
||||
|
||||
export interface RegistrationAPI<T> {
|
||||
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>(makeRegistration: () => T): RegistrationAPI<T> {
|
||||
const registrations: T[] = [];
|
||||
const items = writable(registrations);
|
||||
|
||||
function registerComponent(
|
||||
index: number = registrations.length,
|
||||
registration = makeRegistration()
|
||||
): T {
|
||||
registrations.splice(index, 0, registration);
|
||||
items.set(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 = find(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,
|
||||
};
|
||||
}
|
9
ts/editor-toolbar/ButtonDropdown.d.ts
vendored
9
ts/editor-toolbar/ButtonDropdown.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
export interface ButtonDropdownProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
items: ToolbarItem[];
|
||||
}
|
@ -1,31 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { ToolbarItem } from "./types";
|
||||
import ButtonGroup from "./ButtonGroup.svelte";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
|
||||
function extendClassName(className: string): string {
|
||||
return `dropdown-menu btn-dropdown-menu ${className}`;
|
||||
}
|
||||
|
||||
export let items: ToolbarItem[];
|
||||
</script>
|
||||
|
||||
<style>
|
||||
:global(ul.btn-dropdown-menu) {
|
||||
display: none;
|
||||
background-color: var(--window-bg);
|
||||
border-color: var(--medium-border);
|
||||
}
|
||||
|
||||
:global(ul.btn-dropdown-menu.show) {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
||||
|
||||
<ButtonGroup {id} className={extendClassName(className)} {items} />
|
10
ts/editor-toolbar/ButtonGroup.d.ts
vendored
10
ts/editor-toolbar/ButtonGroup.d.ts
vendored
@ -1,10 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
export interface ButtonGroupProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
items: ToolbarItem[];
|
||||
fullWidth?: boolean;
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { ToolbarItem } from "./types";
|
||||
import { getContext } from "svelte";
|
||||
import { nightModeKey } from "./contextKeys";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
export let className = "";
|
||||
export let items: ToolbarItem[];
|
||||
|
||||
function filterHidden({ hidden = false, ...props }) {
|
||||
return props;
|
||||
}
|
||||
|
||||
const nightMode = getContext(nightModeKey);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
ul {
|
||||
display: flex;
|
||||
justify-items: start;
|
||||
|
||||
flex-wrap: var(--toolbar-wrap);
|
||||
overflow-y: auto;
|
||||
|
||||
padding: calc(var(--toolbar-size) / 10);
|
||||
margin: 0;
|
||||
|
||||
&.border-overlap-group {
|
||||
:global(button),
|
||||
:global(select) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.gap-group {
|
||||
:global(button),
|
||||
:global(select) {
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
li {
|
||||
display: contents;
|
||||
|
||||
> :global(button),
|
||||
> :global(select) {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
&:nth-child(1) {
|
||||
> :global(button),
|
||||
> :global(select) {
|
||||
border-top-left-radius: calc(var(--toolbar-size) / 7.5);
|
||||
border-bottom-left-radius: calc(var(--toolbar-size) / 7.5);
|
||||
}
|
||||
}
|
||||
|
||||
&:nth-last-child(1) {
|
||||
> :global(button),
|
||||
> :global(select) {
|
||||
border-top-right-radius: calc(var(--toolbar-size) / 7.5);
|
||||
border-bottom-right-radius: calc(var(--toolbar-size) / 7.5);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<ul
|
||||
{id}
|
||||
class={className}
|
||||
class:border-overlap-group={!nightMode}
|
||||
class:gap-group={nightMode}>
|
||||
{#each items as button}
|
||||
{#if !button.hidden}
|
||||
<li>
|
||||
<svelte:component this={button.component} {...filterHidden(button)} />
|
||||
</li>
|
||||
{/if}
|
||||
{/each}
|
||||
</ul>
|
8
ts/editor-toolbar/ColorPicker.d.ts
vendored
8
ts/editor-toolbar/ColorPicker.d.ts
vendored
@ -1,8 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface ColorPickerProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
tooltip: string;
|
||||
onChange: (event: Event) => void;
|
||||
}
|
16
ts/editor-toolbar/CommandIconButton.d.ts
vendored
16
ts/editor-toolbar/CommandIconButton.d.ts
vendored
@ -1,16 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface CommandIconButtonProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
tooltip: string;
|
||||
icon: string;
|
||||
|
||||
command: string;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
|
||||
onUpdate: (event: Event) => boolean;
|
||||
|
||||
disables?: boolean;
|
||||
dropdownToggle?: boolean;
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript" context="module">
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type UpdateMap = Map<string, (event: Event) => boolean>;
|
||||
type ActiveMap = Map<string, boolean>;
|
||||
|
||||
const updateMap = new Map() as UpdateMap;
|
||||
const activeMap = new Map() as ActiveMap;
|
||||
const activeStore = writable(activeMap);
|
||||
|
||||
function updateButton(key: string, event: MouseEvent): void {
|
||||
activeStore.update(
|
||||
(map: ActiveMap): ActiveMap =>
|
||||
new Map([...map, [key, updateMap.get(key)(event)]])
|
||||
);
|
||||
}
|
||||
|
||||
function updateButtons(callback: (key: string) => boolean): void {
|
||||
activeStore.update(
|
||||
(map: ActiveMap): ActiveMap => {
|
||||
const newMap = new Map() as ActiveMap;
|
||||
|
||||
for (const key of map.keys()) {
|
||||
newMap.set(key, callback(key));
|
||||
}
|
||||
|
||||
return newMap;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function updateActiveButtons(event: Event) {
|
||||
updateButtons((key: string): boolean => updateMap.get(key)(event));
|
||||
}
|
||||
|
||||
export function clearActiveButtons() {
|
||||
updateButtons((): boolean => false);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
import SquareButton from "./SquareButton.svelte";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string;
|
||||
export let shortcutLabel: string | undefined;
|
||||
export let icon: string;
|
||||
|
||||
export let command: string;
|
||||
export let onClick = (_event: MouseEvent) => {
|
||||
document.execCommand(command);
|
||||
};
|
||||
|
||||
function onClickWrapped(event: MouseEvent): void {
|
||||
onClick(event);
|
||||
updateButton(command, event);
|
||||
}
|
||||
|
||||
export let onUpdate = (_event: Event) => document.queryCommandState(command);
|
||||
|
||||
updateMap.set(command, onUpdate);
|
||||
|
||||
let active = false;
|
||||
|
||||
activeStore.subscribe((map: ActiveMap): (() => void) => {
|
||||
active = Boolean(map.get(command));
|
||||
return () => map.delete(command);
|
||||
});
|
||||
activeMap.set(command, active);
|
||||
|
||||
export let disables = true;
|
||||
export let dropdownToggle = false;
|
||||
</script>
|
||||
|
||||
<SquareButton
|
||||
{id}
|
||||
{className}
|
||||
{tooltip}
|
||||
{shortcutLabel}
|
||||
{active}
|
||||
{disables}
|
||||
{dropdownToggle}
|
||||
onClick={onClickWrapped}
|
||||
on:mount>
|
||||
{@html icon}
|
||||
</SquareButton>
|
11
ts/editor-toolbar/DropdownItem.d.ts
vendored
11
ts/editor-toolbar/DropdownItem.d.ts
vendored
@ -1,11 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface DropdownItemProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
tooltip: string;
|
||||
|
||||
onClick: (event: MouseEvent) => void;
|
||||
label: string;
|
||||
endLabel: string;
|
||||
}
|
@ -1,35 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { ToolbarItem } from "./types";
|
||||
import { getContext } from "svelte";
|
||||
import { nightModeKey } from "./contextKeys";
|
||||
|
||||
export let id: string;
|
||||
export let items: ToolbarItem[];
|
||||
|
||||
const nightMode = getContext<boolean>(nightModeKey);
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
@use 'ts/sass/button_mixins' as button;
|
||||
|
||||
ul {
|
||||
background-color: white;
|
||||
border-color: var(--medium-border);
|
||||
}
|
||||
|
||||
.night-mode {
|
||||
background-color: var(--bg-color);
|
||||
}
|
||||
</style>
|
||||
|
||||
<ul {id} class="dropdown-menu" class:night-mode={nightMode}>
|
||||
{#each items as menuItem}
|
||||
<li>
|
||||
<svelte:component this={menuItem.component} {...menuItem} />
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
@ -1,137 +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="typescript">
|
||||
import "./legacy.css";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
const disabled = writable(false);
|
||||
|
||||
export function enableButtons(): void {
|
||||
disabled.set(false);
|
||||
}
|
||||
|
||||
export function disableButtons(): void {
|
||||
disabled.set(true);
|
||||
}
|
||||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
import type { Identifier } from "./identifiable";
|
||||
import type { ToolbarItem, IterableToolbarItem } from "./types";
|
||||
|
||||
import { setContext } from "svelte";
|
||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||
import { add, insert, updateRecursive } from "./identifiable";
|
||||
import { showComponent, hideComponent, toggleComponent } from "./hideable";
|
||||
|
||||
import ButtonGroup from "./ButtonGroup.svelte";
|
||||
|
||||
export let buttons: IterableToolbarItem[];
|
||||
export let menus: ToolbarItem[];
|
||||
export let nightMode: boolean;
|
||||
|
||||
setContext(nightModeKey, nightMode);
|
||||
setContext(disabledKey, disabled);
|
||||
|
||||
export let size: number = 30;
|
||||
export let wraps: boolean = true;
|
||||
|
||||
$: style = `--toolbar-size: ${size}px; --toolbar-wrap: ${
|
||||
wraps ? "wrap" : "nowrap"
|
||||
}`;
|
||||
|
||||
export function updateButton(
|
||||
update: (component: ToolbarItem) => ToolbarItem,
|
||||
...identifiers: Identifier[]
|
||||
): void {
|
||||
buttons = updateRecursive(
|
||||
update,
|
||||
({ items: buttons } as unknown) as ToolbarItem,
|
||||
...identifiers
|
||||
).items as IterableToolbarItem[];
|
||||
}
|
||||
|
||||
export function showButton(...identifiers: Identifier[]): void {
|
||||
updateButton(showComponent, ...identifiers);
|
||||
}
|
||||
|
||||
export function hideButton(...identifiers: Identifier[]): void {
|
||||
updateButton(hideComponent, ...identifiers);
|
||||
}
|
||||
|
||||
export function toggleButton(...identifiers: Identifier[]): void {
|
||||
updateButton(toggleComponent, ...identifiers);
|
||||
}
|
||||
|
||||
export function insertButton(
|
||||
newButton: ToolbarItem,
|
||||
...identifiers: Identifier[]
|
||||
): void {
|
||||
const initIdentifiers = identifiers.slice(0, -1);
|
||||
const lastIdentifier = identifiers[identifiers.length - 1];
|
||||
updateButton(
|
||||
(component: ToolbarItem) =>
|
||||
insert(component as IterableToolbarItem, newButton, lastIdentifier),
|
||||
|
||||
...initIdentifiers
|
||||
);
|
||||
}
|
||||
|
||||
export function addButton(
|
||||
newButton: ToolbarItem,
|
||||
...identifiers: Identifier[]
|
||||
): void {
|
||||
const initIdentifiers = identifiers.slice(0, -1);
|
||||
const lastIdentifier = identifiers[identifiers.length - 1];
|
||||
updateButton(
|
||||
(component: ToolbarItem) =>
|
||||
add(component as IterableToolbarItem, newButton, lastIdentifier),
|
||||
...initIdentifiers
|
||||
);
|
||||
}
|
||||
|
||||
export function updateMenu(
|
||||
update: (component: ToolbarItem) => ToolbarItem,
|
||||
...identifiers: Identifier[]
|
||||
): void {
|
||||
menus = updateRecursive(
|
||||
update,
|
||||
({ items: menus } as unknown) as ToolbarItem,
|
||||
...identifiers
|
||||
).items as ToolbarItem[];
|
||||
}
|
||||
|
||||
export function addMenu(newMenu: ToolbarItem, ...identifiers: Identifier[]): void {
|
||||
const initIdentifiers = identifiers.slice(0, -1);
|
||||
const lastIdentifier = identifiers[identifiers.length - 1];
|
||||
updateMenu(
|
||||
(component: ToolbarItem) =>
|
||||
add(component as IterableToolbarItem, newMenu, lastIdentifier),
|
||||
...initIdentifiers
|
||||
);
|
||||
}
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
nav {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: var(--bg-color);
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
</style>
|
||||
|
||||
<div {style}>
|
||||
{#each menus as menu}
|
||||
<svelte:component this={menu.component} {...menu} />
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
<nav {style}>
|
||||
<ButtonGroup items={buttons} className="p-0 mb-1" />
|
||||
</nav>
|
9
ts/editor-toolbar/IconButton.d.ts
vendored
9
ts/editor-toolbar/IconButton.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface IconButtonProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
tooltip: string;
|
||||
icon: string;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
}
|
@ -1,30 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import SquareButton from "./SquareButton.svelte";
|
||||
|
||||
export let id: string;
|
||||
export let className = "";
|
||||
export let tooltip: string | undefined;
|
||||
export let shortcutLabel: string | undefined;
|
||||
export let icon: string;
|
||||
|
||||
export let onClick: (event: MouseEvent) => void;
|
||||
|
||||
export let disables = true;
|
||||
export let dropdownToggle = false;
|
||||
</script>
|
||||
|
||||
<SquareButton
|
||||
{id}
|
||||
{className}
|
||||
{tooltip}
|
||||
{shortcutLabel}
|
||||
{onClick}
|
||||
{disables}
|
||||
{dropdownToggle}
|
||||
on:mount>
|
||||
{@html icon}
|
||||
</SquareButton>
|
11
ts/editor-toolbar/LabelButton.d.ts
vendored
11
ts/editor-toolbar/LabelButton.d.ts
vendored
@ -1,11 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface LabelButtonProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
|
||||
label: string;
|
||||
tooltip: string;
|
||||
onClick: (event: MouseEvent) => void;
|
||||
disables?: boolean;
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { onMount, createEventDispatcher } from "svelte";
|
||||
|
||||
export let html: string;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: null }));
|
||||
</script>
|
||||
|
||||
{@html html}
|
15
ts/editor-toolbar/SelectButton.d.ts
vendored
15
ts/editor-toolbar/SelectButton.d.ts
vendored
@ -1,15 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface Option {
|
||||
label: string;
|
||||
value: string;
|
||||
selected: boolean;
|
||||
}
|
||||
|
||||
export interface SelectButtonProps {
|
||||
id: string;
|
||||
className?: string;
|
||||
tooltip?: string;
|
||||
disables: boolean;
|
||||
options: Option[];
|
||||
}
|
8
ts/editor-toolbar/WithDropdownMenu.d.ts
vendored
8
ts/editor-toolbar/WithDropdownMenu.d.ts
vendored
@ -1,8 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
export interface WithDropdownMenuProps {
|
||||
button: ToolbarItem;
|
||||
menuId: string;
|
||||
}
|
@ -1,53 +0,0 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||
|
||||
/* Bootstrap dropdown are normally declared alongside the associated button
|
||||
* However we cannot do that, as the menus cannot be declared in sticky-positioned elements
|
||||
*/
|
||||
export let button: ToolbarItem;
|
||||
export let menuId: string;
|
||||
|
||||
function extend({
|
||||
className,
|
||||
...rest
|
||||
}: DynamicSvelteComponent): DynamicSvelteComponent {
|
||||
return {
|
||||
dropdownToggle: true,
|
||||
...rest,
|
||||
};
|
||||
}
|
||||
|
||||
function createDropdown({ detail }: CustomEvent): void {
|
||||
const button: HTMLButtonElement = detail.button;
|
||||
|
||||
/* Prevent focus on menu activation */
|
||||
const noop = () => {};
|
||||
Object.defineProperty(button, "focus", { value: noop });
|
||||
|
||||
/* Set custom menu without using .dropdown
|
||||
* Rendering the menu here would cause the menu to
|
||||
* be displayed outside of the visible area
|
||||
*/
|
||||
const dropdown = new Dropdown(button);
|
||||
const menu = (button.getRootNode() as Document) /* or shadow root */
|
||||
.getElementById(menuId);
|
||||
|
||||
if (!menu) {
|
||||
console.log(`Could not find menu "${menuId}" for dropdown menu.`);
|
||||
}
|
||||
|
||||
(dropdown as any)._menu = menu;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:component
|
||||
this={button.component}
|
||||
{...extend(button)}
|
||||
on:mount={createDropdown} />
|
11
ts/editor-toolbar/WithLabel.d.ts
vendored
11
ts/editor-toolbar/WithLabel.d.ts
vendored
@ -1,11 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
export interface WithLabelProps {
|
||||
id?: string;
|
||||
className?: string;
|
||||
|
||||
button: ToolbarItem;
|
||||
label: string;
|
||||
}
|
9
ts/editor-toolbar/WithShortcut.d.ts
vendored
9
ts/editor-toolbar/WithShortcut.d.ts
vendored
@ -1,9 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "./types";
|
||||
|
||||
export interface WithShortcutProps {
|
||||
button: ToolbarItem;
|
||||
shortcut: string;
|
||||
optionalModifiers: string[];
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import RawButton from "./RawButton.svelte";
|
||||
import LabelButton from "./LabelButton.svelte";
|
||||
import type { LabelButtonProps } from "./LabelButton";
|
||||
import IconButton from "./IconButton.svelte";
|
||||
import type { IconButtonProps } from "./IconButton";
|
||||
import CommandIconButton from "./CommandIconButton.svelte";
|
||||
import type { CommandIconButtonProps } from "./CommandIconButton";
|
||||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import type { ColorPickerProps } from "./ColorPicker";
|
||||
import SelectButton from "./SelectButton.svelte";
|
||||
import type { SelectButtonProps } from "./SelectButton";
|
||||
import ButtonGroup from "./ButtonGroup.svelte";
|
||||
import type { ButtonGroupProps } from "./ButtonGroup";
|
||||
|
||||
import ButtonDropdown from "./ButtonDropdown.svelte";
|
||||
import type { ButtonDropdownProps } from "./ButtonDropdown";
|
||||
import DropdownMenu from "./DropdownMenu.svelte";
|
||||
import type { DropdownMenuProps } from "./DropdownMenu";
|
||||
import DropdownItem from "./DropdownItem.svelte";
|
||||
import type { DropdownItemProps } from "./DropdownItem";
|
||||
import WithDropdownMenu from "./WithDropdownMenu.svelte";
|
||||
import type { WithDropdownMenuProps } from "./WithDropdownMenu";
|
||||
|
||||
import WithShortcut from "./WithShortcut.svelte";
|
||||
import type { WithShortcutProps } from "./WithShortcut";
|
||||
|
||||
import WithLabel from "./WithLabel.svelte";
|
||||
import type { WithLabelProps } from "./WithLabel";
|
||||
|
||||
import { dynamicComponent } from "sveltelib/dynamicComponent";
|
||||
|
||||
export const rawButton = dynamicComponent<typeof RawButton, { html: string }>(
|
||||
RawButton
|
||||
);
|
||||
export const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(
|
||||
LabelButton
|
||||
);
|
||||
export const iconButton = dynamicComponent<typeof IconButton, IconButtonProps>(
|
||||
IconButton
|
||||
);
|
||||
export const commandIconButton = dynamicComponent<
|
||||
typeof CommandIconButton,
|
||||
CommandIconButtonProps
|
||||
>(CommandIconButton);
|
||||
export const colorPicker = dynamicComponent<typeof ColorPicker, ColorPickerProps>(
|
||||
ColorPicker
|
||||
);
|
||||
export const selectButton = dynamicComponent<typeof SelectButton, SelectButtonProps>(
|
||||
SelectButton
|
||||
);
|
||||
|
||||
export const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(
|
||||
ButtonGroup
|
||||
);
|
||||
export const buttonDropdown = dynamicComponent<
|
||||
typeof ButtonDropdown,
|
||||
ButtonDropdownProps
|
||||
>(ButtonDropdown);
|
||||
|
||||
export const dropdownMenu = dynamicComponent<typeof DropdownMenu, DropdownMenuProps>(
|
||||
DropdownMenu
|
||||
);
|
||||
export const dropdownItem = dynamicComponent<typeof DropdownItem, DropdownItemProps>(
|
||||
DropdownItem
|
||||
);
|
||||
|
||||
export const withDropdownMenu = dynamicComponent<
|
||||
typeof WithDropdownMenu,
|
||||
WithDropdownMenuProps
|
||||
>(WithDropdownMenu);
|
||||
|
||||
export const withShortcut = dynamicComponent<typeof WithShortcut, WithShortcutProps>(
|
||||
WithShortcut
|
||||
);
|
||||
|
||||
export const withLabel = dynamicComponent<typeof WithLabel, WithLabelProps>(WithLabel);
|
@ -1,20 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
interface Hideable {
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export function showComponent<T extends Hideable>(component: T): T {
|
||||
component.hidden = false;
|
||||
return component;
|
||||
}
|
||||
|
||||
export function hideComponent<T extends Hideable>(component: T): T {
|
||||
component.hidden = true;
|
||||
return component;
|
||||
}
|
||||
|
||||
export function toggleComponent<T extends Hideable>(component: T): T {
|
||||
component.hidden = !component.hidden;
|
||||
return component;
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface Identifiable {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface IterableIdentifiable<T extends Identifiable> extends Identifiable {
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export type Identifier = string | number;
|
||||
|
||||
function normalize<T extends Identifiable>(
|
||||
iterable: IterableIdentifiable<T>,
|
||||
idOrIndex: Identifier
|
||||
): number {
|
||||
let normalizedIndex: number;
|
||||
|
||||
if (typeof idOrIndex === "string") {
|
||||
normalizedIndex = iterable.items.findIndex((value) => value.id === idOrIndex);
|
||||
} else if (idOrIndex < 0) {
|
||||
normalizedIndex = iterable.items.length + idOrIndex;
|
||||
} else {
|
||||
normalizedIndex = idOrIndex;
|
||||
}
|
||||
|
||||
return normalizedIndex >= iterable.items.length ? -1 : normalizedIndex;
|
||||
}
|
||||
|
||||
function search<T extends Identifiable>(values: T[], index: number): T | null {
|
||||
return index >= 0 ? values[index] : null;
|
||||
}
|
||||
|
||||
export function insert<T extends Identifiable>(
|
||||
iterable: IterableIdentifiable<T> & T,
|
||||
value: T,
|
||||
idOrIndex: Identifier
|
||||
): IterableIdentifiable<T> & T {
|
||||
const index = normalize(iterable, idOrIndex);
|
||||
|
||||
if (index >= 0) {
|
||||
iterable.items = iterable.items.slice();
|
||||
iterable.items.splice(index, 0, value);
|
||||
}
|
||||
|
||||
return iterable;
|
||||
}
|
||||
|
||||
export function add<T extends Identifiable>(
|
||||
iterable: IterableIdentifiable<T> & T,
|
||||
value: T,
|
||||
idOrIndex: Identifier
|
||||
): IterableIdentifiable<T> & T {
|
||||
const index = normalize(iterable, idOrIndex);
|
||||
|
||||
if (index >= 0) {
|
||||
iterable.items = iterable.items.slice();
|
||||
iterable.items.splice(index + 1, 0, value);
|
||||
}
|
||||
|
||||
return iterable;
|
||||
}
|
||||
|
||||
function isRecursive<T>(component: Identifiable): component is IterableIdentifiable<T> {
|
||||
return Boolean(Object.prototype.hasOwnProperty.call(component, "items"));
|
||||
}
|
||||
|
||||
export function updateRecursive<T extends Identifiable>(
|
||||
update: (component: T) => T,
|
||||
component: T,
|
||||
...identifiers: Identifier[]
|
||||
): T {
|
||||
if (identifiers.length === 0) {
|
||||
return update(component);
|
||||
} else if (isRecursive<T>(component)) {
|
||||
const [identifier, ...restIdentifiers] = identifiers;
|
||||
const normalizedIndex = normalize(component, identifier);
|
||||
const foundComponent = search(component.items, normalizedIndex);
|
||||
|
||||
if (foundComponent) {
|
||||
component.items[normalizedIndex] = updateRecursive(
|
||||
update,
|
||||
foundComponent as T,
|
||||
...restIdentifiers
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
@ -1,29 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem, IterableToolbarItem } from "./types";
|
||||
|
||||
import EditorToolbar from "./EditorToolbar.svelte";
|
||||
export { default as EditorToolbar } from "./EditorToolbar.svelte";
|
||||
|
||||
import "./bootstrap.css";
|
||||
|
||||
export function editorToolbar(
|
||||
target: HTMLElement,
|
||||
buttons: IterableToolbarItem[] = [],
|
||||
menus: ToolbarItem[] = []
|
||||
): EditorToolbar {
|
||||
return new EditorToolbar({
|
||||
target,
|
||||
props: {
|
||||
buttons,
|
||||
menus,
|
||||
nightMode: document.documentElement.classList.contains("night-mode"),
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/* Exports for editor */
|
||||
// @ts-expect-error insufficient typing of svelte modules
|
||||
export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte";
|
||||
// @ts-expect-error insufficient typing of svelte modules
|
||||
export { enableButtons, disableButtons } from "./EditorToolbar.svelte";
|
18
ts/editor/AddonButtons.svelte
Normal file
18
ts/editor/AddonButtons.svelte
Normal file
@ -0,0 +1,18 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
|
||||
export let buttons: string[];
|
||||
</script>
|
||||
|
||||
<ButtonGroup>
|
||||
{#each buttons as button}
|
||||
<ButtonGroupItem>
|
||||
{@html button}
|
||||
</ButtonGroupItem>
|
||||
{/each}
|
||||
</ButtonGroup>
|
@ -1,10 +1,30 @@
|
||||
load("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||
load("//ts:prettier.bzl", "prettier_test")
|
||||
load("//ts:eslint.bzl", "eslint_test")
|
||||
load("//ts:esbuild.bzl", "esbuild")
|
||||
load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons")
|
||||
load("//ts:compile_sass.bzl", "compile_sass")
|
||||
|
||||
svelte_files = glob(["*.svelte"])
|
||||
|
||||
svelte_names = [f.replace(".svelte", "") for f in svelte_files]
|
||||
|
||||
filegroup(
|
||||
name = "svelte_components",
|
||||
srcs = svelte_names,
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
compile_svelte(
|
||||
name = "svelte",
|
||||
srcs = svelte_files,
|
||||
deps = [
|
||||
"//ts/components",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
compile_sass(
|
||||
srcs = [
|
||||
"editable.scss",
|
||||
@ -30,18 +50,31 @@ compile_sass(
|
||||
],
|
||||
)
|
||||
|
||||
compile_sass(
|
||||
srcs = [
|
||||
"bootstrap.scss",
|
||||
"legacy.scss",
|
||||
],
|
||||
group = "local_css",
|
||||
deps = [
|
||||
"//ts/sass:button_mixins_lib",
|
||||
"//ts/sass/bootstrap",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
ts_library(
|
||||
name = "editor_ts",
|
||||
srcs = glob(["*.ts"]),
|
||||
tsconfig = "//ts:tsconfig.json",
|
||||
deps = [
|
||||
"//ts:image_module_support",
|
||||
"//ts/lib",
|
||||
"//ts/sveltelib",
|
||||
"//ts/components",
|
||||
"//ts/html-filter",
|
||||
"//ts/editor-toolbar",
|
||||
"//ts:image_module_support",
|
||||
"@npm//svelte",
|
||||
],
|
||||
] + svelte_names,
|
||||
)
|
||||
|
||||
copy_bootstrap_icons(
|
||||
@ -106,8 +139,10 @@ esbuild(
|
||||
"bootstrap-icons",
|
||||
"mdi-icons",
|
||||
"editor_ts",
|
||||
"//ts/editor-toolbar:local_css",
|
||||
"//ts/editor-toolbar:svelte_components",
|
||||
"local_css",
|
||||
"svelte_components",
|
||||
"//ts/components",
|
||||
"//ts/components:svelte_components",
|
||||
],
|
||||
)
|
||||
|
||||
@ -121,11 +156,24 @@ prettier_test(
|
||||
]),
|
||||
)
|
||||
|
||||
# eslint_test(
|
||||
# name = "eslint",
|
||||
# srcs = glob(
|
||||
# [
|
||||
# "*.ts",
|
||||
# ],
|
||||
# ),
|
||||
# )
|
||||
eslint_test(
|
||||
name = "eslint",
|
||||
srcs = glob(
|
||||
[
|
||||
"*.ts",
|
||||
],
|
||||
),
|
||||
)
|
||||
|
||||
svelte_check(
|
||||
name = "svelte_check",
|
||||
srcs = glob([
|
||||
"*.ts",
|
||||
"*.svelte",
|
||||
]) + [
|
||||
"//ts/sass:button_mixins_lib",
|
||||
"//ts/sass/bootstrap",
|
||||
"//ts/components:svelte_components",
|
||||
"@npm//@types/bootstrap",
|
||||
],
|
||||
)
|
||||
|
55
ts/editor/ClozeButton.svelte
Normal file
55
ts/editor/ClozeButton.svelte
Normal file
@ -0,0 +1,55 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
|
||||
import { bracketsIcon } from "./icons";
|
||||
import { forEditorField } from ".";
|
||||
import { wrap } from "./wrap";
|
||||
|
||||
const clozePattern = /\{\{c(\d+)::/gu;
|
||||
function getCurrentHighestCloze(increment: boolean): number {
|
||||
let highest = 0;
|
||||
|
||||
forEditorField([], (field) => {
|
||||
const fieldHTML = field.editingArea.editable.fieldHTML;
|
||||
const matches: number[] = [];
|
||||
let match: RegExpMatchArray | null = null;
|
||||
|
||||
while ((match = clozePattern.exec(fieldHTML))) {
|
||||
matches.push(Number(match[1]));
|
||||
}
|
||||
|
||||
highest = Math.max(highest, ...matches);
|
||||
});
|
||||
|
||||
if (increment) {
|
||||
highest++;
|
||||
}
|
||||
|
||||
return Math.max(1, highest);
|
||||
}
|
||||
|
||||
function onCloze(event: KeyboardEvent | MouseEvent): void {
|
||||
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||
wrap(`{{c${highestCloze}::`, "}}");
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithShortcut
|
||||
shortcut="Control+Shift+KeyC"
|
||||
optionalModifiers={['Alt']}
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={`${tr.editingClozeDeletion()} (${shortcutLabel})`}
|
||||
on:click={onCloze}
|
||||
on:mount={createShortcut}>
|
||||
{@html bracketsIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
58
ts/editor/ColorButtons.svelte
Normal file
58
ts/editor/ColorButtons.svelte
Normal file
@ -0,0 +1,58 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
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 WithShortcut from "components/WithShortcut.svelte";
|
||||
|
||||
import { squareFillIcon } from "./icons";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
|
||||
import "./color.css";
|
||||
|
||||
export let api = {};
|
||||
|
||||
const foregroundColorKeyword = "--foreground-color";
|
||||
let color = "black";
|
||||
|
||||
$: {
|
||||
document.documentElement.style.setProperty(foregroundColorKeyword, color);
|
||||
}
|
||||
|
||||
function wrapWithForecolor(): void {
|
||||
document.execCommand("forecolor", false, color);
|
||||
}
|
||||
|
||||
function setWithCurrentColor({ currentTarget }: Event): void {
|
||||
color = (currentTarget as HTMLInputElement).value;
|
||||
}
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="F7" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
class="forecolor"
|
||||
tooltip={appendInParentheses(tr.editingSetForegroundColor(), shortcutLabel)}
|
||||
on:click={wrapWithForecolor}
|
||||
on:mount={createShortcut}>
|
||||
{@html squareFillIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="F8" let:createShortcut let:shortcutLabel>
|
||||
<ColorPicker
|
||||
tooltip={appendInParentheses(tr.editingChangeColor(), shortcutLabel)}
|
||||
on:change={setWithCurrentColor}
|
||||
on:mount={createShortcut} />
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
83
ts/editor/EditorToolbar.svelte
Normal file
83
ts/editor/EditorToolbar.svelte
Normal file
@ -0,0 +1,83 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script context="module" lang="typescript">
|
||||
import "./legacy.css";
|
||||
// @ts-expect-error Insufficient typing
|
||||
import { updateAllState, resetAllState } from "components/WithState.svelte";
|
||||
|
||||
export function updateActiveButtons(event: Event) {
|
||||
updateAllState(event);
|
||||
}
|
||||
|
||||
export function clearActiveButtons() {
|
||||
resetAllState(false);
|
||||
}
|
||||
|
||||
/* Export components */
|
||||
import AddonButtons from "./AddonButtons.svelte";
|
||||
import PreviewButton from "./PreviewButton.svelte";
|
||||
import LabelButton from "components/LabelButton.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
|
||||
export const editorToolbar = {
|
||||
AddonButtons,
|
||||
PreviewButton,
|
||||
LabelButton,
|
||||
IconButton,
|
||||
};
|
||||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
import WithTheming from "components/WithTheming.svelte";
|
||||
import StickyBar from "components/StickyBar.svelte";
|
||||
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
||||
import ButtonToolbarItem from "components/ButtonToolbarItem.svelte";
|
||||
|
||||
import NoteTypeButtons from "./NoteTypeButtons.svelte";
|
||||
import FormatInlineButtons from "./FormatInlineButtons.svelte";
|
||||
import FormatBlockButtons from "./FormatBlockButtons.svelte";
|
||||
import ColorButtons from "./ColorButtons.svelte";
|
||||
import TemplateButtons from "./TemplateButtons.svelte";
|
||||
|
||||
export const toolbar = {};
|
||||
export const notetypeButtons = {};
|
||||
export const formatInlineButtons = {};
|
||||
export const formatBlockButtons = {};
|
||||
export const colorButtons = {};
|
||||
export const templateButtons = {};
|
||||
|
||||
export let size: number = 30;
|
||||
export let wraps: boolean = true;
|
||||
|
||||
$: style = `--toolbar-size: ${size}px; --toolbar-wrap: ${
|
||||
wraps ? "wrap" : "nowrap"
|
||||
}`;
|
||||
</script>
|
||||
|
||||
<WithTheming {style}>
|
||||
<StickyBar>
|
||||
<ButtonToolbar api={toolbar}>
|
||||
<ButtonToolbarItem id="notetype">
|
||||
<NoteTypeButtons api={notetypeButtons} />
|
||||
</ButtonToolbarItem>
|
||||
|
||||
<ButtonToolbarItem id="inlineFormatting">
|
||||
<FormatInlineButtons api={formatInlineButtons} />
|
||||
</ButtonToolbarItem>
|
||||
|
||||
<ButtonToolbarItem id="blockFormatting">
|
||||
<FormatBlockButtons api={formatBlockButtons} />
|
||||
</ButtonToolbarItem>
|
||||
|
||||
<ButtonToolbarItem id="color">
|
||||
<ColorButtons api={colorButtons} />
|
||||
</ButtonToolbarItem>
|
||||
|
||||
<ButtonToolbarItem id="template">
|
||||
<TemplateButtons api={templateButtons} />
|
||||
</ButtonToolbarItem>
|
||||
</ButtonToolbar>
|
||||
</StickyBar>
|
||||
</WithTheming>
|
189
ts/editor/FormatBlockButtons.svelte
Normal file
189
ts/editor/FormatBlockButtons.svelte
Normal file
@ -0,0 +1,189 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import type { EditingArea } from "./editingArea";
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
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 ButtonToolbarItem from "components/ButtonToolbarItem.svelte";
|
||||
import WithState from "components/WithState.svelte";
|
||||
import WithDropdownMenu from "components/WithDropdownMenu.svelte";
|
||||
|
||||
import { getListItem } from "./helpers";
|
||||
import {
|
||||
ulIcon,
|
||||
olIcon,
|
||||
listOptionsIcon,
|
||||
justifyFullIcon,
|
||||
justifyLeftIcon,
|
||||
justifyRightIcon,
|
||||
justifyCenterIcon,
|
||||
indentIcon,
|
||||
outdentIcon,
|
||||
} from "./icons";
|
||||
|
||||
export let api = {};
|
||||
|
||||
function outdentListItem() {
|
||||
const currentField = document.activeElement as EditingArea;
|
||||
if (getListItem(currentField.shadowRoot!)) {
|
||||
document.execCommand("outdent");
|
||||
}
|
||||
}
|
||||
|
||||
function indentListItem() {
|
||||
const currentField = document.activeElement as EditingArea;
|
||||
if (getListItem(currentField.shadowRoot!)) {
|
||||
document.execCommand("indent");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
key="insertUnorderedList"
|
||||
update={() => document.queryCommandState('insertUnorderedList')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={tr.editingUnorderedList()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('insertUnorderedList');
|
||||
updateState(event);
|
||||
}}>
|
||||
{@html ulIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
key="insertOrderedList"
|
||||
update={() => document.queryCommandState('insertOrderedList')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={tr.editingOrderedList()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('insertOrderedList');
|
||||
updateState(event);
|
||||
}}>
|
||||
{@html olIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdownMenu let:createDropdown let:menuId>
|
||||
<IconButton on:mount={createDropdown}>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
|
||||
<ButtonDropdown id={menuId}>
|
||||
<ButtonToolbarItem id="justify">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
key="justifyLeft"
|
||||
update={() => document.queryCommandState('justifyLeft')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={tr.editingAlignLeft()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('justifyLeft');
|
||||
updateState(event);
|
||||
}}>
|
||||
{@html justifyLeftIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
key="justifyCenter"
|
||||
update={() => document.queryCommandState('justifyCenter')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={tr.editingCenter()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('justifyCenter');
|
||||
updateState(event);
|
||||
}}>
|
||||
{@html justifyCenterIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
key="justifyRight"
|
||||
update={() => document.queryCommandState('justifyRight')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={tr.editingAlignRight()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('justifyRight');
|
||||
updateState(event);
|
||||
}}>
|
||||
{@html justifyRightIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithState
|
||||
key="justifyFull"
|
||||
update={() => document.queryCommandState('justifyFull')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={tr.editingJustify()}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('justifyFull');
|
||||
updateState(event);
|
||||
}}>
|
||||
{@html justifyFullIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbarItem>
|
||||
|
||||
<ButtonToolbarItem id="indentation">
|
||||
<ButtonGroup>
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
on:click={outdentListItem}
|
||||
tooltip={tr.editingOutdent()}>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<IconButton
|
||||
on:click={indentListItem}
|
||||
tooltip={tr.editingIndent()}>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbarItem>
|
||||
</ButtonDropdown>
|
||||
</WithDropdownMenu>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
148
ts/editor/FormatInlineButtons.svelte
Normal file
148
ts/editor/FormatInlineButtons.svelte
Normal file
@ -0,0 +1,148 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import WithState from "components/WithState.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
|
||||
import {
|
||||
boldIcon,
|
||||
italicIcon,
|
||||
underlineIcon,
|
||||
superscriptIcon,
|
||||
subscriptIcon,
|
||||
eraserIcon,
|
||||
} from "./icons";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="Control+KeyB" let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="bold"
|
||||
update={() => document.queryCommandState('bold')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingBoldText(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('bold');
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}>
|
||||
{@html boldIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="Control+KeyI" let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="italic"
|
||||
update={() => document.queryCommandState('italic')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingItalicText(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('italic');
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}>
|
||||
{@html italicIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="Control+KeyU" let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="underline"
|
||||
update={() => document.queryCommandState('underline')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingUnderlineText(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('underline');
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}>
|
||||
{@html underlineIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut
|
||||
shortcut="Control+Shift+Equal"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<WithState
|
||||
key="superscript"
|
||||
update={() => document.queryCommandState('superscript')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingSuperscript(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('superscript');
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}>
|
||||
{@html superscriptIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="Control+Equal" let:createShortcut let:shortcutLabel>
|
||||
<WithState
|
||||
key="subscript"
|
||||
update={() => document.queryCommandState('subscript')}
|
||||
let:state={active}
|
||||
let:updateState>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingSubscript(), shortcutLabel)}
|
||||
{active}
|
||||
on:click={(event) => {
|
||||
document.execCommand('subscript');
|
||||
updateState(event);
|
||||
}}
|
||||
on:mount={createShortcut}>
|
||||
{@html subscriptIcon}
|
||||
</IconButton>
|
||||
</WithState>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="Control+KeyR" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingRemoveFormatting(), shortcutLabel)}
|
||||
on:click={() => {
|
||||
document.execCommand('removeFormat');
|
||||
}}
|
||||
on:mount={createShortcut}>
|
||||
{@html eraserIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
38
ts/editor/NoteTypeButtons.svelte
Normal file
38
ts/editor/NoteTypeButtons.svelte
Normal file
@ -0,0 +1,38 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { bridgeCommand } from "lib/bridgecommand";
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
import LabelButton from "components/LabelButton.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<LabelButton
|
||||
disables={false}
|
||||
tooltip={tr.editingCustomizeFields()}
|
||||
on:click={() => bridgeCommand('fields')}>
|
||||
{tr.editingFields()}...
|
||||
</LabelButton>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="Control+KeyL" let:createShortcut let:shortcutLabel>
|
||||
<LabelButton
|
||||
disables={false}
|
||||
tooltip={`${tr.editingCustomizeCardTemplates()} (${shortcutLabel})`}
|
||||
on:click={() => bridgeCommand('cards')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingCards()}...
|
||||
</LabelButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
21
ts/editor/PreviewButton.svelte
Normal file
21
ts/editor/PreviewButton.svelte
Normal file
@ -0,0 +1,21 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { bridgeCommand } from "lib/bridgecommand";
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import LabelButton from "components/LabelButton.svelte";
|
||||
</script>
|
||||
|
||||
<WithShortcut shortcut="Control+Shift+KeyP" let:createShortcut let:shortcutLabel>
|
||||
<LabelButton
|
||||
tooltip={tr.browsingPreviewSelectedCard({ val: shortcutLabel })}
|
||||
disables={false}
|
||||
on:click={() => bridgeCommand('preview')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.actionsPreview()}
|
||||
</LabelButton>
|
||||
</WithShortcut>
|
159
ts/editor/TemplateButtons.svelte
Normal file
159
ts/editor/TemplateButtons.svelte
Normal file
@ -0,0 +1,159 @@
|
||||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import * as tr from "lib/i18n";
|
||||
import { bridgeCommand } from "lib/bridgecommand";
|
||||
|
||||
import ButtonGroup from "components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem from "components/ButtonGroupItem.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
import DropdownMenu from "components/DropdownMenu.svelte";
|
||||
import DropdownItem from "components/DropdownItem.svelte";
|
||||
import WithDropdownMenu from "components/WithDropdownMenu.svelte";
|
||||
import WithShortcut from "components/WithShortcut.svelte";
|
||||
import ClozeButton from "./ClozeButton.svelte";
|
||||
|
||||
import { wrap } from "./wrap";
|
||||
import { appendInParentheses } from "./helpers";
|
||||
import { paperclipIcon, micIcon, functionIcon, xmlIcon } from "./icons";
|
||||
|
||||
export let api = {};
|
||||
|
||||
function onAttachment(): void {
|
||||
bridgeCommand("attach");
|
||||
}
|
||||
|
||||
function onRecord(): void {
|
||||
bridgeCommand("record");
|
||||
}
|
||||
|
||||
function onHtmlEdit(): void {
|
||||
bridgeCommand("htmlEdit");
|
||||
}
|
||||
</script>
|
||||
|
||||
<ButtonGroup {api}>
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="F3" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingAttachPicturesaudiovideo(), shortcutLabel)}
|
||||
on:click={onAttachment}
|
||||
on:mount={createShortcut}>
|
||||
{@html paperclipIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut shortcut="F5" let:createShortcut let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingRecordAudio(), shortcutLabel)}
|
||||
on:click={onRecord}
|
||||
on:mount={createShortcut}>
|
||||
{@html micIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem id="cloze">
|
||||
<ClozeButton />
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdownMenu let:menuId let:createDropdown>
|
||||
<IconButton on:mount={createDropdown}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
|
||||
<DropdownMenu id={menuId}>
|
||||
<WithShortcut
|
||||
shortcut="Control+KeyM, KeyM"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<DropdownItem
|
||||
on:click={() => wrap('\\(', '\\)')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingMathjaxInline()}
|
||||
<span class="ps-1 float-end">{shortcutLabel}</span>
|
||||
</DropdownItem>
|
||||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut="Control+KeyM, KeyE"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<DropdownItem
|
||||
on:click={() => wrap('\\[', '\\]')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingMathjaxBlock()}
|
||||
<span class="ps-1 float-end">{shortcutLabel}</span>
|
||||
</DropdownItem>
|
||||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut="Control+KeyM, KeyC"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<DropdownItem
|
||||
on:click={() => wrap('\\(\\ce{', '}\\)')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingMathjaxChemistry()}
|
||||
<span class="ps-1 float-end">{shortcutLabel}</span>
|
||||
</DropdownItem>
|
||||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut="Control+KeyT, KeyT"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<DropdownItem
|
||||
on:click={() => wrap('[latex]', '[/latex]')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingLatex()}
|
||||
<span class="ps-1 float-end">{shortcutLabel}</span>
|
||||
</DropdownItem>
|
||||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut="Control+KeyT, KeyE"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<DropdownItem
|
||||
on:click={() => wrap('[$]', '[/$]')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingLatexEquation()}
|
||||
<span class="ps-1 float-end">{shortcutLabel}</span>
|
||||
</DropdownItem>
|
||||
</WithShortcut>
|
||||
|
||||
<WithShortcut
|
||||
shortcut="Control+KeyT, KeyM"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<DropdownItem
|
||||
on:click={() => wrap('[$$]', '[/$$]')}
|
||||
on:mount={createShortcut}>
|
||||
{tr.editingLatexMathEnv()}
|
||||
<span class="ps-1 float-end">{shortcutLabel}</span>
|
||||
</DropdownItem>
|
||||
</WithShortcut>
|
||||
</DropdownMenu>
|
||||
</WithDropdownMenu>
|
||||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithShortcut
|
||||
shortcut="Control+Shift+KeyX"
|
||||
let:createShortcut
|
||||
let:shortcutLabel>
|
||||
<IconButton
|
||||
tooltip={appendInParentheses(tr.editingHtmlEditor(), shortcutLabel)}
|
||||
on:click={onHtmlEdit}
|
||||
on:mount={createShortcut}>
|
||||
{@html xmlIcon}
|
||||
</IconButton>
|
||||
</WithShortcut>
|
||||
</ButtonGroupItem>
|
||||
</ButtonGroup>
|
@ -1,35 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
||||
|
||||
import {
|
||||
buttonGroup,
|
||||
rawButton,
|
||||
labelButton,
|
||||
iconButton,
|
||||
commandIconButton,
|
||||
selectButton,
|
||||
dropdownMenu,
|
||||
dropdownItem,
|
||||
buttonDropdown,
|
||||
withDropdownMenu,
|
||||
withLabel,
|
||||
} from "editor-toolbar/dynamicComponents";
|
||||
|
||||
export const editorToolbar: Record<
|
||||
string,
|
||||
(props: Record<string, unknown>) => DynamicSvelteComponent
|
||||
> = {
|
||||
buttonGroup,
|
||||
rawButton,
|
||||
labelButton,
|
||||
iconButton,
|
||||
commandIconButton,
|
||||
selectButton,
|
||||
|
||||
dropdownMenu,
|
||||
dropdownItem,
|
||||
buttonDropdown,
|
||||
withDropdownMenu,
|
||||
withLabel,
|
||||
};
|
@ -5,4 +5,5 @@
|
||||
$btn-disabled-opacity: 0.4;
|
||||
|
||||
@import "ts/sass/bootstrap/buttons";
|
||||
@import "ts/sass/bootstrap/button-group";
|
||||
@import "ts/sass/bootstrap/dropdown";
|
@ -1,52 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { ToolbarItem } from "editor-toolbar/types";
|
||||
|
||||
import * as tr from "lib/i18n";
|
||||
import { iconButton, withShortcut } from "editor-toolbar/dynamicComponents";
|
||||
|
||||
import bracketsIcon from "./code-brackets.svg";
|
||||
|
||||
import { forEditorField } from ".";
|
||||
import { wrap } from "./wrap";
|
||||
|
||||
const clozePattern = /\{\{c(\d+)::/gu;
|
||||
function getCurrentHighestCloze(increment: boolean): number {
|
||||
let highest = 0;
|
||||
|
||||
forEditorField([], (field) => {
|
||||
const fieldHTML = field.editingArea.editable.fieldHTML;
|
||||
const matches: number[] = [];
|
||||
let match: RegExpMatchArray | null = null;
|
||||
|
||||
while ((match = clozePattern.exec(fieldHTML))) {
|
||||
matches.push(Number(match[1]));
|
||||
}
|
||||
|
||||
highest = Math.max(highest, ...matches);
|
||||
});
|
||||
|
||||
if (increment) {
|
||||
highest++;
|
||||
}
|
||||
|
||||
return Math.max(1, highest);
|
||||
}
|
||||
|
||||
function onCloze(event: KeyboardEvent | MouseEvent): void {
|
||||
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||
wrap(`{{c${highestCloze}::`, "}}");
|
||||
}
|
||||
|
||||
export function getClozeButton(): ToolbarItem {
|
||||
return withShortcut({
|
||||
id: "cloze",
|
||||
shortcut: "Control+Shift+KeyC",
|
||||
optionalModifiers: ["Alt"],
|
||||
button: iconButton({
|
||||
icon: bracketsIcon,
|
||||
onClick: onCloze,
|
||||
tooltip: tr.editingClozeDeletion(),
|
||||
}),
|
||||
});
|
||||
}
|
@ -1,54 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { IterableToolbarItem } from "editor-toolbar/types";
|
||||
|
||||
import {
|
||||
iconButton,
|
||||
colorPicker,
|
||||
buttonGroup,
|
||||
withShortcut,
|
||||
} from "editor-toolbar/dynamicComponents";
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
import squareFillIcon from "./square-fill.svg";
|
||||
import "./color.css";
|
||||
|
||||
const foregroundColorKeyword = "--foreground-color";
|
||||
|
||||
function setForegroundColor(color: string): void {
|
||||
document.documentElement.style.setProperty(foregroundColorKeyword, color);
|
||||
}
|
||||
|
||||
function getForecolor(): string {
|
||||
return document.documentElement.style.getPropertyValue(foregroundColorKeyword);
|
||||
}
|
||||
|
||||
function wrapWithForecolor(color: string): void {
|
||||
document.execCommand("forecolor", false, color);
|
||||
}
|
||||
|
||||
export function getColorGroup(): IterableToolbarItem {
|
||||
const forecolorButton = withShortcut({
|
||||
shortcut: "F7",
|
||||
button: iconButton({
|
||||
icon: squareFillIcon,
|
||||
className: "forecolor",
|
||||
onClick: () => wrapWithForecolor(getForecolor()),
|
||||
tooltip: tr.editingSetForegroundColor(),
|
||||
}),
|
||||
});
|
||||
|
||||
const colorpickerButton = withShortcut({
|
||||
shortcut: "F8",
|
||||
button: colorPicker({
|
||||
onChange: ({ currentTarget }) =>
|
||||
setForegroundColor((currentTarget as HTMLInputElement).value),
|
||||
tooltip: tr.editingChangeColor(),
|
||||
}),
|
||||
});
|
||||
|
||||
return buttonGroup({
|
||||
id: "color",
|
||||
items: [forecolorButton, colorpickerButton],
|
||||
});
|
||||
}
|
@ -33,7 +33,7 @@ export class Editable extends HTMLElement {
|
||||
: this.innerHTML;
|
||||
}
|
||||
|
||||
connectedCallback() {
|
||||
connectedCallback(): void {
|
||||
this.setAttribute("contenteditable", "");
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,13 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import type { Editable } from "./editable";
|
||||
|
||||
import { updateActiveButtons } from "editor-toolbar";
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { bridgeCommand } from "./lib";
|
||||
import { onInput, onKey, onKeyUp } from "./inputHandlers";
|
||||
import { onFocus, onBlur } from "./focusHandlers";
|
||||
|
@ -33,7 +33,7 @@
|
||||
bottom: 0;
|
||||
|
||||
text-align: center;
|
||||
background-color: var(--bg-color);
|
||||
background-color: var(--window-bg);
|
||||
|
||||
&.is-inactive {
|
||||
display: none;
|
||||
@ -59,7 +59,3 @@
|
||||
opacity: 0.5;
|
||||
}
|
||||
}
|
||||
|
||||
.flex-basis-100 {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
@ -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 { enableButtons, disableButtons } from "editor-toolbar";
|
||||
import { enableButtons, disableButtons } from "./toolbar";
|
||||
import type { EditingArea } from "./editingArea";
|
||||
|
||||
import { saveField } from "./changeTimer";
|
||||
|
@ -1,129 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { IterableToolbarItem } from "editor-toolbar/types";
|
||||
import type { EditingArea } from "./editingArea";
|
||||
|
||||
import * as tr from "lib/i18n";
|
||||
import {
|
||||
commandIconButton,
|
||||
iconButton,
|
||||
buttonGroup,
|
||||
buttonDropdown,
|
||||
withDropdownMenu,
|
||||
} from "editor-toolbar/dynamicComponents";
|
||||
|
||||
import { getListItem } from "./helpers";
|
||||
|
||||
import ulIcon from "./list-ul.svg";
|
||||
import olIcon from "./list-ol.svg";
|
||||
import listOptionsIcon from "./text-paragraph.svg";
|
||||
|
||||
import justifyFullIcon from "./justify.svg";
|
||||
import justifyLeftIcon from "./text-left.svg";
|
||||
import justifyRightIcon from "./text-right.svg";
|
||||
import justifyCenterIcon from "./text-center.svg";
|
||||
|
||||
import indentIcon from "./text-indent-left.svg";
|
||||
import outdentIcon from "./text-indent-right.svg";
|
||||
|
||||
const outdentListItem = () => {
|
||||
const currentField = document.activeElement as EditingArea;
|
||||
if (getListItem(currentField.shadowRoot!)) {
|
||||
document.execCommand("outdent");
|
||||
}
|
||||
};
|
||||
|
||||
const indentListItem = () => {
|
||||
const currentField = document.activeElement as EditingArea;
|
||||
if (getListItem(currentField.shadowRoot!)) {
|
||||
document.execCommand("indent");
|
||||
}
|
||||
};
|
||||
|
||||
export function getFormatBlockMenus(): IterableToolbarItem[] {
|
||||
const justifyLeftButton = commandIconButton({
|
||||
icon: justifyLeftIcon,
|
||||
command: "justifyLeft",
|
||||
tooltip: tr.editingAlignLeft(),
|
||||
});
|
||||
|
||||
const justifyCenterButton = commandIconButton({
|
||||
icon: justifyCenterIcon,
|
||||
command: "justifyCenter",
|
||||
tooltip: tr.editingCenter(),
|
||||
});
|
||||
|
||||
const justifyRightButton = commandIconButton({
|
||||
icon: justifyRightIcon,
|
||||
command: "justifyRight",
|
||||
tooltip: tr.editingAlignRight(),
|
||||
});
|
||||
|
||||
const justifyFullButton = commandIconButton({
|
||||
icon: justifyFullIcon,
|
||||
command: "justifyFull",
|
||||
tooltip: tr.editingJustify(),
|
||||
});
|
||||
|
||||
const justifyGroup = buttonGroup({
|
||||
id: "justify",
|
||||
items: [
|
||||
justifyLeftButton,
|
||||
justifyCenterButton,
|
||||
justifyRightButton,
|
||||
justifyFullButton,
|
||||
],
|
||||
});
|
||||
|
||||
const outdentButton = iconButton({
|
||||
icon: outdentIcon,
|
||||
onClick: outdentListItem,
|
||||
tooltip: tr.editingOutdent(),
|
||||
});
|
||||
|
||||
const indentButton = iconButton({
|
||||
icon: indentIcon,
|
||||
onClick: indentListItem,
|
||||
tooltip: tr.editingIndent(),
|
||||
});
|
||||
|
||||
const indentationGroup = buttonGroup({
|
||||
id: "indentation",
|
||||
items: [outdentButton, indentButton],
|
||||
});
|
||||
|
||||
const formattingOptions = buttonDropdown({
|
||||
id: "listFormatting",
|
||||
items: [justifyGroup, indentationGroup],
|
||||
});
|
||||
|
||||
return [formattingOptions];
|
||||
}
|
||||
|
||||
export function getFormatBlockGroup(): IterableToolbarItem {
|
||||
const ulButton = commandIconButton({
|
||||
icon: ulIcon,
|
||||
command: "insertUnorderedList",
|
||||
tooltip: tr.editingUnorderedList(),
|
||||
});
|
||||
|
||||
const olButton = commandIconButton({
|
||||
icon: olIcon,
|
||||
command: "insertOrderedList",
|
||||
tooltip: tr.editingOrderedList(),
|
||||
});
|
||||
|
||||
const listFormattingButton = iconButton({
|
||||
icon: listOptionsIcon,
|
||||
});
|
||||
|
||||
const listFormatting = withDropdownMenu({
|
||||
button: listFormattingButton,
|
||||
menuId: "listFormatting",
|
||||
});
|
||||
|
||||
return buttonGroup({
|
||||
id: "blockFormatting",
|
||||
items: [ulButton, olButton, listFormatting],
|
||||
});
|
||||
}
|
@ -1,88 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { IterableToolbarItem } from "editor-toolbar/types";
|
||||
|
||||
import * as tr from "lib/i18n";
|
||||
import {
|
||||
commandIconButton,
|
||||
iconButton,
|
||||
buttonGroup,
|
||||
withShortcut,
|
||||
} from "editor-toolbar/dynamicComponents";
|
||||
|
||||
import boldIcon from "./type-bold.svg";
|
||||
import italicIcon from "./type-italic.svg";
|
||||
import underlineIcon from "./type-underline.svg";
|
||||
import superscriptIcon from "./format-superscript.svg";
|
||||
import subscriptIcon from "./format-subscript.svg";
|
||||
import eraserIcon from "./eraser.svg";
|
||||
|
||||
export function getFormatInlineGroup(): IterableToolbarItem {
|
||||
const boldButton = withShortcut({
|
||||
shortcut: "Control+KeyB",
|
||||
button: commandIconButton({
|
||||
icon: boldIcon,
|
||||
tooltip: tr.editingBoldText(),
|
||||
command: "bold",
|
||||
}),
|
||||
});
|
||||
|
||||
const italicButton = withShortcut({
|
||||
shortcut: "Control+KeyI",
|
||||
button: commandIconButton({
|
||||
icon: italicIcon,
|
||||
tooltip: tr.editingItalicText(),
|
||||
command: "italic",
|
||||
}),
|
||||
});
|
||||
|
||||
const underlineButton = withShortcut({
|
||||
shortcut: "Control+KeyU",
|
||||
button: commandIconButton({
|
||||
icon: underlineIcon,
|
||||
tooltip: tr.editingUnderlineText(),
|
||||
command: "underline",
|
||||
}),
|
||||
});
|
||||
|
||||
const superscriptButton = withShortcut({
|
||||
shortcut: "Control+Shift+Equal",
|
||||
button: commandIconButton({
|
||||
icon: superscriptIcon,
|
||||
tooltip: tr.editingSuperscript(),
|
||||
command: "superscript",
|
||||
}),
|
||||
});
|
||||
|
||||
const subscriptButton = withShortcut({
|
||||
shortcut: "Control+Equal",
|
||||
button: commandIconButton({
|
||||
icon: subscriptIcon,
|
||||
tooltip: tr.editingSubscript(),
|
||||
command: "subscript",
|
||||
}),
|
||||
});
|
||||
|
||||
const removeFormatButton = withShortcut({
|
||||
shortcut: "Control+KeyR",
|
||||
button: iconButton({
|
||||
icon: eraserIcon,
|
||||
tooltip: tr.editingRemoveFormatting(),
|
||||
onClick: () => {
|
||||
document.execCommand("removeFormat");
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return buttonGroup({
|
||||
id: "inlineFormatting",
|
||||
items: [
|
||||
boldButton,
|
||||
italicButton,
|
||||
underlineButton,
|
||||
superscriptButton,
|
||||
subscriptButton,
|
||||
removeFormatButton,
|
||||
],
|
||||
});
|
||||
}
|
@ -110,3 +110,7 @@ const isBlockElement = (
|
||||
export const getListItem = getAnchorParent(isListItem);
|
||||
export const getParagraph = getAnchorParent(isParagraph);
|
||||
export const getBlockElement = getAnchorParent(isBlockElement);
|
||||
|
||||
export function appendInParentheses(text: string, appendix: string): string {
|
||||
return `${text} (${appendix})`;
|
||||
}
|
||||
|
29
ts/editor/icons.ts
Normal file
29
ts/editor/icons.ts
Normal file
@ -0,0 +1,29 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
export { default as boldIcon } from "./type-bold.svg";
|
||||
export { default as italicIcon } from "./type-italic.svg";
|
||||
export { default as underlineIcon } from "./type-underline.svg";
|
||||
export { default as superscriptIcon } from "./format-superscript.svg";
|
||||
export { default as subscriptIcon } from "./format-subscript.svg";
|
||||
export { default as eraserIcon } from "./eraser.svg";
|
||||
|
||||
export { default as ulIcon } from "./list-ul.svg";
|
||||
export { default as olIcon } from "./list-ol.svg";
|
||||
export { default as listOptionsIcon } from "./text-paragraph.svg";
|
||||
|
||||
export { default as justifyFullIcon } from "./justify.svg";
|
||||
export { default as justifyLeftIcon } from "./text-left.svg";
|
||||
export { default as justifyRightIcon } from "./text-right.svg";
|
||||
export { default as justifyCenterIcon } from "./text-center.svg";
|
||||
|
||||
export { default as indentIcon } from "./text-indent-left.svg";
|
||||
export { default as outdentIcon } from "./text-indent-right.svg";
|
||||
|
||||
export { default as squareFillIcon } from "./square-fill.svg";
|
||||
|
||||
export { default as paperclipIcon } from "./paperclip.svg";
|
||||
export { default as micIcon } from "./mic.svg";
|
||||
export { default as bracketsIcon } from "./code-brackets.svg";
|
||||
export { default as functionIcon } from "./function-variant.svg";
|
||||
export { default as xmlIcon } from "./xml.svg";
|
@ -1,8 +1,12 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import { filterHTML } from "html-filter";
|
||||
import { updateActiveButtons, disableButtons } from "editor-toolbar";
|
||||
import { updateActiveButtons, disableButtons } from "./toolbar";
|
||||
import { setupI18n, ModuleName } from "lib/i18n";
|
||||
|
||||
import "./fields.css";
|
||||
@ -19,8 +23,7 @@ import { initToolbar } from "./toolbar";
|
||||
export { setNoteId, getNoteId } from "./noteId";
|
||||
export { saveNow } from "./changeTimer";
|
||||
export { wrap, wrapIntoText } from "./wrap";
|
||||
|
||||
export * from "./addons";
|
||||
export { editorToolbar } from "./toolbar";
|
||||
|
||||
declare global {
|
||||
interface Selection {
|
||||
@ -48,14 +51,14 @@ export function focusField(n: number): void {
|
||||
if (field) {
|
||||
field.editingArea.focusEditable();
|
||||
caretToEnd(field.editingArea);
|
||||
updateActiveButtons();
|
||||
updateActiveButtons(new Event("manualfocus"));
|
||||
}
|
||||
}
|
||||
|
||||
export function focusIfField(x: number, y: number): boolean {
|
||||
const elements = document.elementsFromPoint(x, y);
|
||||
for (let i = 0; i < elements.length; i++) {
|
||||
let elem = elements[i] as EditingArea;
|
||||
const elem = elements[i] as EditingArea;
|
||||
if (elem instanceof EditingArea) {
|
||||
elem.focusEditable();
|
||||
return true;
|
||||
@ -159,14 +162,23 @@ export function setSticky(stickies: boolean[]): void {
|
||||
});
|
||||
}
|
||||
|
||||
export function setFormat(cmd: string, arg?: any, nosave: boolean = false): void {
|
||||
export function setFormat(cmd: string, arg?: string, nosave = false): void {
|
||||
document.execCommand(cmd, false, arg);
|
||||
if (!nosave) {
|
||||
saveField(getCurrentField() as EditingArea, "key");
|
||||
updateActiveButtons();
|
||||
updateActiveButtons(new Event(cmd));
|
||||
}
|
||||
}
|
||||
|
||||
const i18n = setupI18n({ modules: [ModuleName.EDITING, ModuleName.KEYBOARD] });
|
||||
const i18n = setupI18n({
|
||||
modules: [
|
||||
ModuleName.EDITING,
|
||||
ModuleName.KEYBOARD,
|
||||
ModuleName.ACTIONS,
|
||||
ModuleName.BROWSING,
|
||||
],
|
||||
});
|
||||
|
||||
export const $editorToolbar = initToolbar(i18n);
|
||||
import type EditorToolbar from "./EditorToolbar.svelte";
|
||||
|
||||
export const $editorToolbar: Promise<EditorToolbar> = initToolbar(i18n);
|
||||
|
@ -1,7 +1,11 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { updateActiveButtons } from "editor-toolbar";
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import { updateActiveButtons } from "./toolbar";
|
||||
import { EditingArea } from "./editingArea";
|
||||
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
|
||||
import { triggerChangeTimer } from "./changeTimer";
|
||||
@ -10,7 +14,7 @@ import { registerShortcut } from "lib/shortcuts";
|
||||
export function onInput(event: Event): void {
|
||||
// make sure IME changes get saved
|
||||
triggerChangeTimer(event.currentTarget as EditingArea);
|
||||
updateActiveButtons();
|
||||
updateActiveButtons(event);
|
||||
}
|
||||
|
||||
export function onKey(evt: KeyboardEvent): void {
|
||||
@ -56,7 +60,7 @@ function updateFocus(evt: FocusEvent) {
|
||||
const newFocusTarget = evt.target;
|
||||
if (newFocusTarget instanceof EditingArea) {
|
||||
caretToEnd(newFocusTarget);
|
||||
updateActiveButtons();
|
||||
updateActiveButtons(evt);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,5 +1,8 @@
|
||||
@use "ts/sass/button_mixins" as button;
|
||||
|
||||
.linkb {
|
||||
display: inline-block;
|
||||
@include button.btn-border-radius;
|
||||
}
|
||||
|
||||
.topbut {
|
@ -1,35 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { IterableToolbarItem } from "editor-toolbar/types";
|
||||
|
||||
import { bridgeCommand } from "lib/bridgecommand";
|
||||
import * as tr from "lib/i18n";
|
||||
import {
|
||||
labelButton,
|
||||
buttonGroup,
|
||||
withShortcut,
|
||||
} from "editor-toolbar/dynamicComponents";
|
||||
|
||||
export function getNotetypeGroup(): IterableToolbarItem {
|
||||
const fieldsButton = labelButton({
|
||||
onClick: () => bridgeCommand("fields"),
|
||||
disables: false,
|
||||
label: `${tr.editingFields()}...`,
|
||||
tooltip: tr.editingCustomizeFields(),
|
||||
});
|
||||
|
||||
const cardsButton = withShortcut({
|
||||
shortcut: "Control+KeyL",
|
||||
button: labelButton({
|
||||
onClick: () => bridgeCommand("cards"),
|
||||
disables: false,
|
||||
label: `${tr.editingCards()}...`,
|
||||
tooltip: tr.editingCustomizeCardTemplates(),
|
||||
}),
|
||||
});
|
||||
|
||||
return buttonGroup({
|
||||
id: "notetype",
|
||||
items: [fieldsButton, cardsButton],
|
||||
});
|
||||
}
|
@ -1,143 +0,0 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import type { IterableToolbarItem } from "editor-toolbar/types";
|
||||
|
||||
import { bridgeCommand } from "lib/bridgecommand";
|
||||
import {
|
||||
iconButton,
|
||||
withDropdownMenu,
|
||||
dropdownMenu,
|
||||
dropdownItem,
|
||||
buttonGroup,
|
||||
withShortcut,
|
||||
} from "editor-toolbar/dynamicComponents";
|
||||
import * as tr from "lib/i18n";
|
||||
|
||||
import { wrap } from "./wrap";
|
||||
|
||||
import paperclipIcon from "./paperclip.svg";
|
||||
import micIcon from "./mic.svg";
|
||||
import functionIcon from "./function-variant.svg";
|
||||
import xmlIcon from "./xml.svg";
|
||||
|
||||
import { getClozeButton } from "./cloze";
|
||||
|
||||
function onAttachment(): void {
|
||||
bridgeCommand("attach");
|
||||
}
|
||||
|
||||
function onRecord(): void {
|
||||
bridgeCommand("record");
|
||||
}
|
||||
|
||||
function onHtmlEdit(): void {
|
||||
bridgeCommand("htmlEdit");
|
||||
}
|
||||
|
||||
const mathjaxMenuId = "mathjaxMenu";
|
||||
|
||||
export function getTemplateGroup(): IterableToolbarItem {
|
||||
const attachmentButton = withShortcut({
|
||||
shortcut: "F3",
|
||||
button: iconButton({
|
||||
icon: paperclipIcon,
|
||||
onClick: onAttachment,
|
||||
tooltip: tr.editingAttachPicturesaudiovideo(),
|
||||
}),
|
||||
});
|
||||
|
||||
const recordButton = withShortcut({
|
||||
shortcut: "F5",
|
||||
button: iconButton({
|
||||
icon: micIcon,
|
||||
onClick: onRecord,
|
||||
tooltip: tr.editingRecordAudio(),
|
||||
}),
|
||||
});
|
||||
|
||||
const mathjaxButton = iconButton({
|
||||
icon: functionIcon,
|
||||
});
|
||||
|
||||
const mathjaxButtonWithMenu = withDropdownMenu({
|
||||
button: mathjaxButton,
|
||||
menuId: mathjaxMenuId,
|
||||
});
|
||||
|
||||
const htmlButton = withShortcut({
|
||||
shortcut: "Control+Shift+KeyX",
|
||||
button: iconButton({
|
||||
icon: xmlIcon,
|
||||
onClick: onHtmlEdit,
|
||||
tooltip: tr.editingHtmlEditor(),
|
||||
}),
|
||||
});
|
||||
|
||||
return buttonGroup({
|
||||
id: "template",
|
||||
items: [
|
||||
attachmentButton,
|
||||
recordButton,
|
||||
getClozeButton(),
|
||||
mathjaxButtonWithMenu,
|
||||
htmlButton,
|
||||
],
|
||||
});
|
||||
}
|
||||
|
||||
export function getTemplateMenus(): IterableToolbarItem[] {
|
||||
const mathjaxMenuItems = [
|
||||
withShortcut({
|
||||
shortcut: "Control+KeyM, KeyM",
|
||||
button: dropdownItem({
|
||||
onClick: () => wrap("\\(", "\\)"),
|
||||
label: tr.editingMathjaxInline(),
|
||||
}),
|
||||
}),
|
||||
withShortcut({
|
||||
shortcut: "Control+KeyM, KeyE",
|
||||
button: dropdownItem({
|
||||
onClick: () => wrap("\\[", "\\]"),
|
||||
label: tr.editingMathjaxBlock(),
|
||||
}),
|
||||
}),
|
||||
withShortcut({
|
||||
shortcut: "Control+KeyM, KeyC",
|
||||
button: dropdownItem({
|
||||
onClick: () => wrap("\\(\\ce{", "}\\)"),
|
||||
label: tr.editingMathjaxChemistry(),
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
const latexMenuItems = [
|
||||
withShortcut({
|
||||
shortcut: "Control+KeyT, KeyT",
|
||||
button: dropdownItem({
|
||||
onClick: () => wrap("[latex]", "[/latex]"),
|
||||
label: tr.editingLatex(),
|
||||
}),
|
||||
}),
|
||||
withShortcut({
|
||||
shortcut: "Control+KeyT, KeyE",
|
||||
button: dropdownItem({
|
||||
onClick: () => wrap("[$]", "[/$]"),
|
||||
label: tr.editingLatexEquation(),
|
||||
}),
|
||||
}),
|
||||
withShortcut({
|
||||
shortcut: "Control+KeyT, KeyM",
|
||||
button: dropdownItem({
|
||||
onClick: () => wrap("[$$]", "[/$$]"),
|
||||
label: tr.editingLatexMathEnv(),
|
||||
}),
|
||||
}),
|
||||
];
|
||||
|
||||
const mathjaxMenu = dropdownMenu({
|
||||
id: mathjaxMenuId,
|
||||
items: [...mathjaxMenuItems, ...latexMenuItems],
|
||||
});
|
||||
|
||||
return [mathjaxMenu];
|
||||
}
|
@ -1,14 +1,20 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
import { editorToolbar, EditorToolbar } from "editor-toolbar";
|
||||
|
||||
import { getNotetypeGroup } from "./notetype";
|
||||
import { getFormatInlineGroup } from "./formatInline";
|
||||
import { getFormatBlockGroup, getFormatBlockMenus } from "./formatBlock";
|
||||
import { getColorGroup } from "./color";
|
||||
import { getTemplateGroup, getTemplateMenus } from "./template";
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
@typescript-eslint/no-explicit-any: "off",
|
||||
*/
|
||||
|
||||
export function initToolbar(i18n: Promise<void>) {
|
||||
import { disabledKey, nightModeKey } from "components/contextKeys";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import EditorToolbar from "./EditorToolbar.svelte";
|
||||
import "./bootstrap.css";
|
||||
|
||||
const disabled = writable(false);
|
||||
|
||||
export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
|
||||
let toolbarResolve: (value: EditorToolbar) => void;
|
||||
const toolbarPromise = new Promise<EditorToolbar>((resolve) => {
|
||||
toolbarResolve = resolve;
|
||||
@ -16,21 +22,33 @@ export function initToolbar(i18n: Promise<void>) {
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
i18n.then(() => {
|
||||
const target = document.getElementById("editorToolbar")!;
|
||||
const target = document.body;
|
||||
const anchor = document.getElementById("fields")!;
|
||||
|
||||
const buttons = [
|
||||
getNotetypeGroup(),
|
||||
getFormatInlineGroup(),
|
||||
getFormatBlockGroup(),
|
||||
getColorGroup(),
|
||||
getTemplateGroup(),
|
||||
];
|
||||
const context = new Map();
|
||||
context.set(disabledKey, disabled);
|
||||
context.set(
|
||||
nightModeKey,
|
||||
document.documentElement.classList.contains("night-mode")
|
||||
);
|
||||
|
||||
const menus = [...getFormatBlockMenus(), ...getTemplateMenus()];
|
||||
|
||||
toolbarResolve(editorToolbar(target, buttons, menus));
|
||||
toolbarResolve(new EditorToolbar({ target, anchor, context } as any));
|
||||
});
|
||||
});
|
||||
|
||||
return toolbarPromise;
|
||||
}
|
||||
|
||||
export function enableButtons(): void {
|
||||
disabled.set(false);
|
||||
}
|
||||
|
||||
export function disableButtons(): void {
|
||||
disabled.set(true);
|
||||
}
|
||||
|
||||
export {
|
||||
updateActiveButtons,
|
||||
clearActiveButtons,
|
||||
editorToolbar,
|
||||
} from "./EditorToolbar.svelte";
|
||||
|
@ -1,6 +1,10 @@
|
||||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/* eslint
|
||||
@typescript-eslint/no-non-null-assertion: "off",
|
||||
*/
|
||||
|
||||
import { getCurrentField, setFormat } from ".";
|
||||
|
||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
||||
|
@ -9,6 +9,15 @@ $link-hover-decoration: none;
|
||||
@import "ts/sass/bootstrap/bootstrap-reboot";
|
||||
@import "ts/sass/bootstrap/bootstrap-utilities";
|
||||
|
||||
/* Bootstrap "extensions" */
|
||||
.flex-basis-100 {
|
||||
flex-basis: 100%;
|
||||
}
|
||||
|
||||
.flex-basis-75 {
|
||||
flex-basis: 75%;
|
||||
}
|
||||
|
||||
body,
|
||||
html {
|
||||
overscroll-behavior: none;
|
||||
|
@ -1,6 +1,14 @@
|
||||
@import "ts/sass/bootstrap/functions";
|
||||
@import "ts/sass/bootstrap/variables";
|
||||
|
||||
@mixin btn-border-radius {
|
||||
border-top-left-radius: var(--border-left-radius);
|
||||
border-bottom-left-radius: var(--border-left-radius);
|
||||
|
||||
border-top-right-radius: var(--border-right-radius);
|
||||
border-bottom-right-radius: var(--border-right-radius);
|
||||
}
|
||||
|
||||
$btn-base-color-day: white;
|
||||
|
||||
@mixin btn-day-base {
|
||||
@ -9,7 +17,7 @@ $btn-base-color-day: white;
|
||||
border-color: var(--medium-border) !important;
|
||||
}
|
||||
|
||||
@mixin btn-day($with-disabled: true) {
|
||||
@mixin btn-day($with-disabled: true, $with-margin: true) {
|
||||
.btn-day {
|
||||
@include btn-day-base;
|
||||
@content ($btn-base-color-day);
|
||||
@ -33,6 +41,10 @@ $btn-base-color-day: white;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($with-margin) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -44,7 +56,7 @@ $btn-base-color-night: #666;
|
||||
border-color: $btn-base-color-night;
|
||||
}
|
||||
|
||||
@mixin btn-night($with-disabled: true) {
|
||||
@mixin btn-night($with-disabled: true, $with-margin: true) {
|
||||
.btn-night {
|
||||
@include btn-night-base;
|
||||
@content ($btn-base-color-night);
|
||||
@ -72,6 +84,10 @@ $btn-base-color-night: #666;
|
||||
border-color: $btn-base-color-night !important;
|
||||
}
|
||||
}
|
||||
|
||||
@if ($with-margin) {
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7,9 +7,9 @@
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"lib/*": ["../bazel-bin/ts/lib/*"],
|
||||
"sveltelib/*": ["../bazel-bin/ts/sveltelib/*"],
|
||||
"html-filter/*": ["../bazel-bin/ts/html-filter/*"],
|
||||
"editor-toolbar/*": ["../bazel-bin/ts/editor-toolbar/*"]
|
||||
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
|
||||
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
|
||||
/* "components/*": ["../bazel-bin/ts/components/*"], */
|
||||
},
|
||||
"importsNotUsedAsValues": "error",
|
||||
"outDir": "dist",
|
||||
|
Loading…
Reference in New Issue
Block a user