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,
|
saveGeom,
|
||||||
saveSplitter,
|
saveSplitter,
|
||||||
saveState,
|
saveState,
|
||||||
shortcut,
|
|
||||||
showInfo,
|
showInfo,
|
||||||
showWarning,
|
showWarning,
|
||||||
skip_if_selection_is_empty,
|
skip_if_selection_is_empty,
|
||||||
@ -383,23 +382,11 @@ class Browser(QMainWindow):
|
|||||||
|
|
||||||
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
editor._links["preview"] = lambda _editor: self.onTogglePreview()
|
||||||
editor.web.eval(
|
editor.web.eval(
|
||||||
f"""
|
"$editorToolbar.then(({ notetypeButtons }) => notetypeButtons.appendButton({ component: editorToolbar.PreviewButton, id: 'preview' }));"
|
||||||
$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));
|
|
||||||
"""
|
|
||||||
)
|
)
|
||||||
|
|
||||||
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.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)
|
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)
|
gui_hooks.editor_did_init.remove(add_preview_button)
|
||||||
|
|
||||||
@ensure_editor_saved
|
@ensure_editor_saved
|
||||||
|
@ -76,18 +76,9 @@ audio = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
_html = """
|
_html = """
|
||||||
<style>
|
<div id="fields"></div>
|
||||||
:root {
|
<div id="dupes" class="is-inactive">
|
||||||
--bg-color: %s;
|
<a href="#" onclick="pycmd('dupes');return false;">%s</a>
|
||||||
}
|
|
||||||
</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>
|
</div>
|
||||||
"""
|
"""
|
||||||
|
|
||||||
@ -135,10 +126,9 @@ class Editor:
|
|||||||
self.web.set_bridge_command(self.onBridgeCmd, self)
|
self.web.set_bridge_command(self.onBridgeCmd, self)
|
||||||
self.outerLayout.addWidget(self.web, 1)
|
self.outerLayout.addWidget(self.web, 1)
|
||||||
|
|
||||||
bgcol = self.mw.app.palette().window().color().name() # type: ignore
|
|
||||||
# then load page
|
# then load page
|
||||||
self.web.stdHtml(
|
self.web.stdHtml(
|
||||||
_html % (bgcol, tr.editing_show_duplicates()),
|
_html % tr.editing_show_duplicates(),
|
||||||
css=[
|
css=[
|
||||||
"css/editor.css",
|
"css/editor.css",
|
||||||
],
|
],
|
||||||
@ -155,7 +145,7 @@ class Editor:
|
|||||||
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
gui_hooks.editor_did_init_left_buttons(lefttopbtns, self)
|
||||||
|
|
||||||
lefttopbtns_defs = [
|
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
|
for button in lefttopbtns
|
||||||
]
|
]
|
||||||
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
lefttopbtns_js = "\n".join(lefttopbtns_defs)
|
||||||
@ -165,20 +155,16 @@ class Editor:
|
|||||||
# legacy filter
|
# legacy filter
|
||||||
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
righttopbtns = runFilter("setupEditorButtons", righttopbtns, self)
|
||||||
|
|
||||||
righttopbtns_defs = "\n".join(
|
righttopbtns_defs = ", ".join([json.dumps(button) for button in righttopbtns])
|
||||||
[
|
|
||||||
f"editorToolbar.rawButton({{ html: `{button}` }}),"
|
|
||||||
for button in righttopbtns
|
|
||||||
]
|
|
||||||
)
|
|
||||||
righttopbtns_js = (
|
righttopbtns_js = (
|
||||||
f"""
|
f"""
|
||||||
$editorToolbar.then(({{ addButton }}) => addButton(editorToolbar.buttonGroup({{
|
$editorToolbar.then(({{ toolbar }}) => toolbar.appendGroup({{
|
||||||
id: "addons",
|
component: editorToolbar.AddonButtons,
|
||||||
items: [ {righttopbtns_defs} ]
|
id: "addons",
|
||||||
}}), -1));
|
props: {{ buttons: [ {righttopbtns_defs} ] }},
|
||||||
|
}}));
|
||||||
"""
|
"""
|
||||||
if righttopbtns_defs
|
if len(righttopbtns) > 0
|
||||||
else ""
|
else ""
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1278,11 +1264,11 @@ gui_hooks.editor_will_munge_html.append(reverse_url_quoting)
|
|||||||
def set_cloze_button(editor: Editor) -> None:
|
def set_cloze_button(editor: Editor) -> None:
|
||||||
if editor.note.model()["type"] == MODEL_CLOZE:
|
if editor.note.model()["type"] == MODEL_CLOZE:
|
||||||
editor.web.eval(
|
editor.web.eval(
|
||||||
'$editorToolbar.then(({ showButton }) => showButton("template", "cloze")); '
|
'$editorToolbar.then(({ templateButtons }) => templateButtons.showButton("cloze")); '
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
editor.web.eval(
|
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:prettier.bzl", "prettier_test")
|
||||||
load("//ts:eslint.bzl", "eslint_test")
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
load("//ts:esbuild.bzl", "esbuild")
|
load("//ts:esbuild.bzl", "esbuild")
|
||||||
load("//ts:compile_sass.bzl", "compile_sass")
|
|
||||||
|
|
||||||
svelte_files = glob(["*.svelte"])
|
svelte_files = glob(["*.svelte"])
|
||||||
|
|
||||||
@ -25,22 +24,9 @@ compile_svelte(
|
|||||||
visibility = ["//visibility:public"],
|
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(
|
ts_library(
|
||||||
name = "editor-toolbar",
|
name = "components",
|
||||||
module_name = "editor-toolbar",
|
module_name = "components",
|
||||||
srcs = glob(
|
srcs = glob(
|
||||||
["*.ts"],
|
["*.ts"],
|
||||||
exclude = ["*.test.ts"],
|
exclude = ["*.test.ts"],
|
||||||
@ -55,7 +41,7 @@ ts_library(
|
|||||||
"@npm//@types/bootstrap",
|
"@npm//@types/bootstrap",
|
||||||
"@npm//bootstrap",
|
"@npm//bootstrap",
|
||||||
"@npm//svelte",
|
"@npm//svelte",
|
||||||
],
|
] + svelte_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
# Tests
|
# 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">
|
<script lang="typescript">
|
||||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { nightModeKey } from "./contextKeys";
|
import { nightModeKey } from "./contextKeys";
|
||||||
import { mergeTooltipAndShortcut } from "./helpers";
|
|
||||||
|
|
||||||
export let id: string;
|
export let id: string | undefined = undefined;
|
||||||
export let className = "";
|
let className = "";
|
||||||
export let tooltip: string | undefined;
|
export { className as class };
|
||||||
export let shortcutLabel: string | undefined;
|
|
||||||
|
|
||||||
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
export let onChange: (event: Event) => void;
|
|
||||||
|
|
||||||
function extendClassName(className: string): string {
|
|
||||||
return `btn ${className}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const nightMode = getContext(nightModeKey);
|
const nightMode = getContext(nightModeKey);
|
||||||
|
|
||||||
let buttonRef: HTMLButtonElement;
|
|
||||||
let inputRef: HTMLInputElement;
|
|
||||||
|
|
||||||
function delegateToInput() {
|
function delegateToInput() {
|
||||||
inputRef.click();
|
inputRef.click();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let buttonRef: HTMLButtonElement;
|
||||||
|
let inputRef: HTMLInputElement;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
onMount(() => dispatch("mount", { button: buttonRef, input: inputRef }));
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<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";
|
@import "ts/sass/bootstrap/variables";
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0;
|
|
||||||
|
|
||||||
width: calc(var(--toolbar-size) - 0px);
|
width: calc(var(--toolbar-size) - 0px);
|
||||||
height: calc(var(--toolbar-size) - 0px);
|
height: calc(var(--toolbar-size) - 0px);
|
||||||
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
overflow: hidden;
|
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) {
|
@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}
|
bind:this={buttonRef}
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
{id}
|
{id}
|
||||||
class={extendClassName(className)}
|
class={`btn ${className}`}
|
||||||
class:btn-day={!nightMode}
|
class:btn-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
{title}
|
title={tooltip}
|
||||||
on:click={delegateToInput}
|
on:click={delegateToInput}
|
||||||
on:mousedown|preventDefault>
|
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>
|
</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 { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { nightModeKey } from "./contextKeys";
|
import { nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string | undefined = undefined;
|
||||||
export let className = "";
|
let className = "";
|
||||||
export let tooltip: string;
|
export { className as class };
|
||||||
export let label: string;
|
|
||||||
export let shortcutLabel: string | undefined;
|
|
||||||
|
|
||||||
export let onClick: (event: MouseEvent) => void;
|
export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
let buttonRef: HTMLButtonElement;
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
@ -28,6 +26,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
button {
|
button {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
|
||||||
|
font-size: calc(var(--toolbar-size) / 2.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn-day {
|
.btn-day {
|
||||||
@ -52,15 +52,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
span {
|
|
||||||
font-size: calc(var(--toolbar-size) / 2.3);
|
|
||||||
color: inherit;
|
|
||||||
}
|
|
||||||
|
|
||||||
.monospace {
|
|
||||||
font-family: monospace;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button
|
<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-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
on:click={onClick}
|
on:click
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
<span class:me-3={shortcutLabel}>{label}</span>
|
<slot />
|
||||||
{#if shortcutLabel}<span class="monospace">{shortcutLabel}</span>{/if}
|
|
||||||
</button>
|
</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">
|
<script lang="typescript">
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import { getContext, onMount, createEventDispatcher } from "svelte";
|
import { getContext, onMount, createEventDispatcher } from "svelte";
|
||||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
import { disabledKey, nightModeKey, dropdownKey } from "./contextKeys";
|
||||||
import { mergeTooltipAndShortcut } from "./helpers";
|
import type { DropdownProps } from "./dropdown";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string | undefined = undefined;
|
||||||
export let className = "";
|
let className = "";
|
||||||
export let tooltip: string | undefined;
|
export { className as class };
|
||||||
export let shortcutLabel: string | undefined;
|
|
||||||
|
|
||||||
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
export let tooltip: string | undefined = undefined;
|
||||||
|
|
||||||
export let onClick: (event: MouseEvent) => void;
|
|
||||||
export let active = false;
|
export let active = false;
|
||||||
export let disables = true;
|
export let disables = true;
|
||||||
export let dropdownToggle = false;
|
export let tabbable = false;
|
||||||
|
|
||||||
$: extraProps = dropdownToggle
|
|
||||||
? {
|
|
||||||
"data-bs-toggle": "dropdown",
|
|
||||||
"aria-expanded": "false",
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
let buttonRef: HTMLButtonElement;
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
function extendClassName(className: string): string {
|
|
||||||
return `btn ${className}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabled = getContext<Readable<boolean>>(disabledKey);
|
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||||
$: _disabled = disables && $disabled;
|
$: _disabled = disables && $disabled;
|
||||||
|
|
||||||
const nightMode = getContext<boolean>(nightModeKey);
|
const nightMode = getContext<boolean>(nightModeKey);
|
||||||
|
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
@ -47,6 +34,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
@include button.btn-border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include button.btn-day;
|
@include button.btn-day;
|
||||||
@ -77,16 +65,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<button
|
<button
|
||||||
bind:this={buttonRef}
|
bind:this={buttonRef}
|
||||||
{id}
|
{id}
|
||||||
class={extendClassName(className)}
|
class={`btn ${className}`}
|
||||||
class:active
|
class:active
|
||||||
class:dropdown-toggle={dropdownToggle}
|
class:dropdown-toggle={dropdownProps.dropdown}
|
||||||
class:btn-day={!nightMode}
|
class:btn-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
tabindex="-1"
|
title={tooltip}
|
||||||
{title}
|
{...dropdownProps}
|
||||||
disabled={_disabled}
|
disabled={_disabled}
|
||||||
{...extraProps}
|
tabindex={tabbable ? 0 : -1}
|
||||||
on:click={onClick}
|
on:click
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
<span class="p-1"><slot /></span>
|
<span class="p-1"><slot /></span>
|
||||||
</button>
|
</button>
|
@ -5,38 +5,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
import { disabledKey, nightModeKey, dropdownKey } from "./contextKeys";
|
||||||
import { mergeTooltipAndShortcut } from "./helpers";
|
import type { DropdownProps } from "./dropdown";
|
||||||
|
|
||||||
export let id: string;
|
export let id: string | undefined = undefined;
|
||||||
export let className = "";
|
let className: string = "";
|
||||||
export let tooltip: string | undefined;
|
export { className as class };
|
||||||
export let shortcutLabel: string | undefined;
|
|
||||||
export let label: string;
|
|
||||||
|
|
||||||
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
|
export let tooltip: string | undefined = undefined;
|
||||||
|
export let active = false;
|
||||||
export let onClick: (event: MouseEvent) => void;
|
|
||||||
export let disables = true;
|
export let disables = true;
|
||||||
export let dropdownToggle = false;
|
export let tabbable = false;
|
||||||
|
|
||||||
$: extraProps = dropdownToggle
|
|
||||||
? {
|
|
||||||
"data-bs-toggle": "dropdown",
|
|
||||||
"aria-expanded": "false",
|
|
||||||
}
|
|
||||||
: {};
|
|
||||||
|
|
||||||
let buttonRef: HTMLButtonElement;
|
|
||||||
|
|
||||||
function extendClassName(className: string): string {
|
|
||||||
return `btn ${className}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
const disabled = getContext<Readable<boolean>>(disabledKey);
|
const disabled = getContext<Readable<boolean>>(disabledKey);
|
||||||
$: _disabled = disables && $disabled;
|
$: _disabled = disables && $disabled;
|
||||||
|
|
||||||
const nightMode = getContext<boolean>(nightModeKey);
|
const nightMode = getContext<boolean>(nightModeKey);
|
||||||
|
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||||
|
|
||||||
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
const dispatch = createEventDispatcher();
|
||||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
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);
|
font-size: calc(var(--toolbar-size) / 2.3);
|
||||||
width: auto;
|
width: auto;
|
||||||
height: var(--toolbar-size);
|
height: var(--toolbar-size);
|
||||||
|
|
||||||
|
@include button.btn-border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
@include button.btn-day;
|
@include button.btn-day;
|
||||||
@ -59,15 +48,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
<button
|
<button
|
||||||
bind:this={buttonRef}
|
bind:this={buttonRef}
|
||||||
{id}
|
{id}
|
||||||
class={extendClassName(className)}
|
class={`btn ${className}`}
|
||||||
class:dropdown-toggle={dropdownToggle}
|
class:active
|
||||||
|
class:dropdown-toggle={dropdownProps.dropdown}
|
||||||
class:btn-day={!nightMode}
|
class:btn-day={!nightMode}
|
||||||
class:btn-night={nightMode}
|
class:btn-night={nightMode}
|
||||||
tabindex="-1"
|
title={tooltip}
|
||||||
|
{...dropdownProps}
|
||||||
disabled={_disabled}
|
disabled={_disabled}
|
||||||
{title}
|
tabindex={tabbable ? 0 : -1}
|
||||||
{...extraProps}
|
on:click
|
||||||
on:click={onClick}
|
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
{label}
|
<slot />
|
||||||
</button>
|
</button>
|
@ -4,11 +4,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import type { Option } from "./SelectButton";
|
|
||||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
import { disabledKey } from "./contextKeys";
|
import { disabledKey } from "./contextKeys";
|
||||||
import SelectOption from "./SelectOption.svelte";
|
import SelectOption from "./SelectOption.svelte";
|
||||||
|
|
||||||
|
interface Option {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
selected?: false;
|
||||||
|
}
|
||||||
|
|
||||||
export let id: string;
|
export let id: string;
|
||||||
export let className = "";
|
export let className = "";
|
||||||
export let tooltip: string;
|
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
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
|
|
||||||
import type { ToolbarItem } from "./types";
|
|
||||||
import type { Modifier } from "lib/shortcuts";
|
import type { Modifier } from "lib/shortcuts";
|
||||||
|
|
||||||
import { onDestroy } from "svelte";
|
import { onDestroy } from "svelte";
|
||||||
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
import { registerShortcut, getPlatformString } from "lib/shortcuts";
|
||||||
|
|
||||||
export let button: ToolbarItem;
|
|
||||||
export let shortcut: string;
|
export let shortcut: string;
|
||||||
export let optionalModifiers: Modifier[];
|
export let optionalModifiers: Modifier[] | undefined = [];
|
||||||
|
|
||||||
function extend({ ...rest }: DynamicSvelteComponent): DynamicSvelteComponent {
|
const shortcutLabel = getPlatformString(shortcut);
|
||||||
const shortcutLabel = getPlatformString(shortcut);
|
|
||||||
|
|
||||||
return {
|
|
||||||
shortcutLabel,
|
|
||||||
...rest,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
let deregister: () => void;
|
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;
|
const mounted: HTMLButtonElement = detail.button;
|
||||||
deregister = registerShortcut(
|
deregister = registerShortcut(
|
||||||
(event: KeyboardEvent) => {
|
(event: KeyboardEvent) => {
|
||||||
mounted.dispatchEvent(new KeyboardEvent("click", event));
|
mounted.dispatchEvent(new MouseEvent("click", event));
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
},
|
},
|
||||||
shortcut,
|
shortcut,
|
||||||
@ -40,7 +30,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||||||
onDestroy(() => deregister());
|
onDestroy(() => deregister());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:component
|
<slot {createShortcut} {shortcutLabel} />
|
||||||
this={button.component}
|
|
||||||
{...extend(button)}
|
|
||||||
on:mount={createShortcut} />
|
|
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
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export const nightModeKey = Symbol("nightMode");
|
export const nightModeKey = Symbol("nightMode");
|
||||||
export const disabledKey = Symbol("disabled");
|
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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { ToolbarItem } from "./types";
|
export interface DropdownProps {
|
||||||
|
dropdown: boolean;
|
||||||
export interface DropdownMenuProps {
|
"data-bs-toggle"?: string;
|
||||||
id: string;
|
"aria-expanded"?: string;
|
||||||
items: ToolbarItem[];
|
|
||||||
}
|
}
|
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("@npm//@bazel/typescript:index.bzl", "ts_library")
|
||||||
|
load("//ts/svelte:svelte.bzl", "compile_svelte", "svelte_check")
|
||||||
load("//ts:prettier.bzl", "prettier_test")
|
load("//ts:prettier.bzl", "prettier_test")
|
||||||
load("//ts:eslint.bzl", "eslint_test")
|
load("//ts:eslint.bzl", "eslint_test")
|
||||||
load("//ts:esbuild.bzl", "esbuild")
|
load("//ts:esbuild.bzl", "esbuild")
|
||||||
load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons")
|
load("//ts:vendor.bzl", "copy_bootstrap_icons", "copy_mdi_icons")
|
||||||
load("//ts:compile_sass.bzl", "compile_sass")
|
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(
|
compile_sass(
|
||||||
srcs = [
|
srcs = [
|
||||||
"editable.scss",
|
"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(
|
ts_library(
|
||||||
name = "editor_ts",
|
name = "editor_ts",
|
||||||
srcs = glob(["*.ts"]),
|
srcs = glob(["*.ts"]),
|
||||||
tsconfig = "//ts:tsconfig.json",
|
tsconfig = "//ts:tsconfig.json",
|
||||||
deps = [
|
deps = [
|
||||||
"//ts:image_module_support",
|
|
||||||
"//ts/lib",
|
"//ts/lib",
|
||||||
"//ts/sveltelib",
|
"//ts/sveltelib",
|
||||||
|
"//ts/components",
|
||||||
"//ts/html-filter",
|
"//ts/html-filter",
|
||||||
"//ts/editor-toolbar",
|
"//ts:image_module_support",
|
||||||
"@npm//svelte",
|
"@npm//svelte",
|
||||||
],
|
] + svelte_names,
|
||||||
)
|
)
|
||||||
|
|
||||||
copy_bootstrap_icons(
|
copy_bootstrap_icons(
|
||||||
@ -106,8 +139,10 @@ esbuild(
|
|||||||
"bootstrap-icons",
|
"bootstrap-icons",
|
||||||
"mdi-icons",
|
"mdi-icons",
|
||||||
"editor_ts",
|
"editor_ts",
|
||||||
"//ts/editor-toolbar:local_css",
|
"local_css",
|
||||||
"//ts/editor-toolbar:svelte_components",
|
"svelte_components",
|
||||||
|
"//ts/components",
|
||||||
|
"//ts/components:svelte_components",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -121,11 +156,24 @@ prettier_test(
|
|||||||
]),
|
]),
|
||||||
)
|
)
|
||||||
|
|
||||||
# eslint_test(
|
eslint_test(
|
||||||
# name = "eslint",
|
name = "eslint",
|
||||||
# srcs = glob(
|
srcs = glob(
|
||||||
# [
|
[
|
||||||
# "*.ts",
|
"*.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;
|
$btn-disabled-opacity: 0.4;
|
||||||
|
|
||||||
@import "ts/sass/bootstrap/buttons";
|
@import "ts/sass/bootstrap/buttons";
|
||||||
|
@import "ts/sass/bootstrap/button-group";
|
||||||
@import "ts/sass/bootstrap/dropdown";
|
@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;
|
: this.innerHTML;
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback(): void {
|
||||||
this.setAttribute("contenteditable", "");
|
this.setAttribute("contenteditable", "");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,9 +1,13 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
import type { Editable } from "./editable";
|
import type { Editable } from "./editable";
|
||||||
|
|
||||||
import { updateActiveButtons } from "editor-toolbar";
|
import { updateActiveButtons } from "./toolbar";
|
||||||
import { bridgeCommand } from "./lib";
|
import { bridgeCommand } from "./lib";
|
||||||
import { onInput, onKey, onKeyUp } from "./inputHandlers";
|
import { onInput, onKey, onKeyUp } from "./inputHandlers";
|
||||||
import { onFocus, onBlur } from "./focusHandlers";
|
import { onFocus, onBlur } from "./focusHandlers";
|
||||||
|
@ -33,7 +33,7 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
background-color: var(--bg-color);
|
background-color: var(--window-bg);
|
||||||
|
|
||||||
&.is-inactive {
|
&.is-inactive {
|
||||||
display: none;
|
display: none;
|
||||||
@ -59,7 +59,3 @@
|
|||||||
opacity: 0.5;
|
opacity: 0.5;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.flex-basis-100 {
|
|
||||||
flex-basis: 100%;
|
|
||||||
}
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
// Copyright: Ankitects Pty Ltd and contributors
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { enableButtons, disableButtons } from "editor-toolbar";
|
import { enableButtons, disableButtons } from "./toolbar";
|
||||||
import type { EditingArea } from "./editingArea";
|
import type { EditingArea } from "./editingArea";
|
||||||
|
|
||||||
import { saveField } from "./changeTimer";
|
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 getListItem = getAnchorParent(isListItem);
|
||||||
export const getParagraph = getAnchorParent(isParagraph);
|
export const getParagraph = getAnchorParent(isParagraph);
|
||||||
export const getBlockElement = getAnchorParent(isBlockElement);
|
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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
import { filterHTML } from "html-filter";
|
import { filterHTML } from "html-filter";
|
||||||
import { updateActiveButtons, disableButtons } from "editor-toolbar";
|
import { updateActiveButtons, disableButtons } from "./toolbar";
|
||||||
import { setupI18n, ModuleName } from "lib/i18n";
|
import { setupI18n, ModuleName } from "lib/i18n";
|
||||||
|
|
||||||
import "./fields.css";
|
import "./fields.css";
|
||||||
@ -19,8 +23,7 @@ import { initToolbar } from "./toolbar";
|
|||||||
export { setNoteId, getNoteId } from "./noteId";
|
export { setNoteId, getNoteId } from "./noteId";
|
||||||
export { saveNow } from "./changeTimer";
|
export { saveNow } from "./changeTimer";
|
||||||
export { wrap, wrapIntoText } from "./wrap";
|
export { wrap, wrapIntoText } from "./wrap";
|
||||||
|
export { editorToolbar } from "./toolbar";
|
||||||
export * from "./addons";
|
|
||||||
|
|
||||||
declare global {
|
declare global {
|
||||||
interface Selection {
|
interface Selection {
|
||||||
@ -48,14 +51,14 @@ export function focusField(n: number): void {
|
|||||||
if (field) {
|
if (field) {
|
||||||
field.editingArea.focusEditable();
|
field.editingArea.focusEditable();
|
||||||
caretToEnd(field.editingArea);
|
caretToEnd(field.editingArea);
|
||||||
updateActiveButtons();
|
updateActiveButtons(new Event("manualfocus"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function focusIfField(x: number, y: number): boolean {
|
export function focusIfField(x: number, y: number): boolean {
|
||||||
const elements = document.elementsFromPoint(x, y);
|
const elements = document.elementsFromPoint(x, y);
|
||||||
for (let i = 0; i < elements.length; i++) {
|
for (let i = 0; i < elements.length; i++) {
|
||||||
let elem = elements[i] as EditingArea;
|
const elem = elements[i] as EditingArea;
|
||||||
if (elem instanceof EditingArea) {
|
if (elem instanceof EditingArea) {
|
||||||
elem.focusEditable();
|
elem.focusEditable();
|
||||||
return true;
|
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);
|
document.execCommand(cmd, false, arg);
|
||||||
if (!nosave) {
|
if (!nosave) {
|
||||||
saveField(getCurrentField() as EditingArea, "key");
|
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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { updateActiveButtons } from "editor-toolbar";
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { updateActiveButtons } from "./toolbar";
|
||||||
import { EditingArea } from "./editingArea";
|
import { EditingArea } from "./editingArea";
|
||||||
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
|
import { caretToEnd, nodeIsElement, getBlockElement } from "./helpers";
|
||||||
import { triggerChangeTimer } from "./changeTimer";
|
import { triggerChangeTimer } from "./changeTimer";
|
||||||
@ -10,7 +14,7 @@ import { registerShortcut } from "lib/shortcuts";
|
|||||||
export function onInput(event: Event): void {
|
export function onInput(event: Event): void {
|
||||||
// make sure IME changes get saved
|
// make sure IME changes get saved
|
||||||
triggerChangeTimer(event.currentTarget as EditingArea);
|
triggerChangeTimer(event.currentTarget as EditingArea);
|
||||||
updateActiveButtons();
|
updateActiveButtons(event);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function onKey(evt: KeyboardEvent): void {
|
export function onKey(evt: KeyboardEvent): void {
|
||||||
@ -56,7 +60,7 @@ function updateFocus(evt: FocusEvent) {
|
|||||||
const newFocusTarget = evt.target;
|
const newFocusTarget = evt.target;
|
||||||
if (newFocusTarget instanceof EditingArea) {
|
if (newFocusTarget instanceof EditingArea) {
|
||||||
caretToEnd(newFocusTarget);
|
caretToEnd(newFocusTarget);
|
||||||
updateActiveButtons();
|
updateActiveButtons(evt);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,8 @@
|
|||||||
|
@use "ts/sass/button_mixins" as button;
|
||||||
|
|
||||||
.linkb {
|
.linkb {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@include button.btn-border-radius;
|
||||||
}
|
}
|
||||||
|
|
||||||
.topbut {
|
.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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { editorToolbar, EditorToolbar } from "editor-toolbar";
|
|
||||||
|
|
||||||
import { getNotetypeGroup } from "./notetype";
|
/* eslint
|
||||||
import { getFormatInlineGroup } from "./formatInline";
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
import { getFormatBlockGroup, getFormatBlockMenus } from "./formatBlock";
|
@typescript-eslint/no-explicit-any: "off",
|
||||||
import { getColorGroup } from "./color";
|
*/
|
||||||
import { getTemplateGroup, getTemplateMenus } from "./template";
|
|
||||||
|
|
||||||
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;
|
let toolbarResolve: (value: EditorToolbar) => void;
|
||||||
const toolbarPromise = new Promise<EditorToolbar>((resolve) => {
|
const toolbarPromise = new Promise<EditorToolbar>((resolve) => {
|
||||||
toolbarResolve = resolve;
|
toolbarResolve = resolve;
|
||||||
@ -16,21 +22,33 @@ export function initToolbar(i18n: Promise<void>) {
|
|||||||
|
|
||||||
document.addEventListener("DOMContentLoaded", () => {
|
document.addEventListener("DOMContentLoaded", () => {
|
||||||
i18n.then(() => {
|
i18n.then(() => {
|
||||||
const target = document.getElementById("editorToolbar")!;
|
const target = document.body;
|
||||||
|
const anchor = document.getElementById("fields")!;
|
||||||
|
|
||||||
const buttons = [
|
const context = new Map();
|
||||||
getNotetypeGroup(),
|
context.set(disabledKey, disabled);
|
||||||
getFormatInlineGroup(),
|
context.set(
|
||||||
getFormatBlockGroup(),
|
nightModeKey,
|
||||||
getColorGroup(),
|
document.documentElement.classList.contains("night-mode")
|
||||||
getTemplateGroup(),
|
);
|
||||||
];
|
|
||||||
|
|
||||||
const menus = [...getFormatBlockMenus(), ...getTemplateMenus()];
|
toolbarResolve(new EditorToolbar({ target, anchor, context } as any));
|
||||||
|
|
||||||
toolbarResolve(editorToolbar(target, buttons, menus));
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return toolbarPromise;
|
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
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
/* eslint
|
||||||
|
@typescript-eslint/no-non-null-assertion: "off",
|
||||||
|
*/
|
||||||
|
|
||||||
import { getCurrentField, setFormat } from ".";
|
import { getCurrentField, setFormat } from ".";
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
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-reboot";
|
||||||
@import "ts/sass/bootstrap/bootstrap-utilities";
|
@import "ts/sass/bootstrap/bootstrap-utilities";
|
||||||
|
|
||||||
|
/* Bootstrap "extensions" */
|
||||||
|
.flex-basis-100 {
|
||||||
|
flex-basis: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flex-basis-75 {
|
||||||
|
flex-basis: 75%;
|
||||||
|
}
|
||||||
|
|
||||||
body,
|
body,
|
||||||
html {
|
html {
|
||||||
overscroll-behavior: none;
|
overscroll-behavior: none;
|
||||||
|
@ -1,6 +1,14 @@
|
|||||||
@import "ts/sass/bootstrap/functions";
|
@import "ts/sass/bootstrap/functions";
|
||||||
@import "ts/sass/bootstrap/variables";
|
@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;
|
$btn-base-color-day: white;
|
||||||
|
|
||||||
@mixin btn-day-base {
|
@mixin btn-day-base {
|
||||||
@ -9,7 +17,7 @@ $btn-base-color-day: white;
|
|||||||
border-color: var(--medium-border) !important;
|
border-color: var(--medium-border) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin btn-day($with-disabled: true) {
|
@mixin btn-day($with-disabled: true, $with-margin: true) {
|
||||||
.btn-day {
|
.btn-day {
|
||||||
@include btn-day-base;
|
@include btn-day-base;
|
||||||
@content ($btn-base-color-day);
|
@content ($btn-base-color-day);
|
||||||
@ -33,6 +41,10 @@ $btn-base-color-day: white;
|
|||||||
box-shadow: none !important;
|
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;
|
border-color: $btn-base-color-night;
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin btn-night($with-disabled: true) {
|
@mixin btn-night($with-disabled: true, $with-margin: true) {
|
||||||
.btn-night {
|
.btn-night {
|
||||||
@include btn-night-base;
|
@include btn-night-base;
|
||||||
@content ($btn-base-color-night);
|
@content ($btn-base-color-night);
|
||||||
@ -72,6 +84,10 @@ $btn-base-color-night: #666;
|
|||||||
border-color: $btn-base-color-night !important;
|
border-color: $btn-base-color-night !important;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@if ($with-margin) {
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,9 +7,9 @@
|
|||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"lib/*": ["../bazel-bin/ts/lib/*"],
|
"lib/*": ["../bazel-bin/ts/lib/*"],
|
||||||
"sveltelib/*": ["../bazel-bin/ts/sveltelib/*"],
|
"html-filter/*": ["../bazel-bin/ts/html-filter/*"]
|
||||||
"html-filter/*": ["../bazel-bin/ts/html-filter/*"],
|
/* "sveltelib/*": ["../bazel-bin/ts/sveltelib/*"], */
|
||||||
"editor-toolbar/*": ["../bazel-bin/ts/editor-toolbar/*"]
|
/* "components/*": ["../bazel-bin/ts/components/*"], */
|
||||||
},
|
},
|
||||||
"importsNotUsedAsValues": "error",
|
"importsNotUsedAsValues": "error",
|
||||||
"outDir": "dist",
|
"outDir": "dist",
|
||||||
|
Loading…
Reference in New Issue
Block a user