Merge pull request #1159 from hgiesel/newapproach

Prefer idiomatic Svelte features over dynamic components
This commit is contained in:
Damien Elmes 2021-05-08 11:35:31 +10:00 committed by GitHub
commit 010c3da770
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
84 changed files with 1817 additions and 1583 deletions

View File

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

View File

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

View File

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

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

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

View File

@ -0,0 +1,64 @@
<!--
Copyright: Ankitects Pty Ltd and contributors
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
-->
<script lang="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>

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

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

View File

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

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

View File

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

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

View File

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

View File

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

View File

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

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

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

View File

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

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

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export interface DropdownItemProps {
id?: string;
className?: string;
tooltip: string;
onClick: (event: MouseEvent) => void;
label: string;
endLabel: string;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,11 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export interface LabelButtonProps {
id?: string;
className?: string;
label: string;
tooltip: string;
onClick: (event: MouseEvent) => void;
disables?: boolean;
}

View File

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

View File

@ -1,15 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export interface Option {
label: string;
value: string;
selected: boolean;
}
export interface SelectButtonProps {
id: string;
className?: string;
tooltip?: string;
disables: boolean;
options: Option[];
}

View File

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

View File

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

View File

@ -1,11 +0,0 @@
// Copyright: Ankitects Pty Ltd and contributors
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { ToolbarItem } from "./types";
export interface WithLabelProps {
id?: string;
className?: string;
button: ToolbarItem;
label: string;
}

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,7 @@ export class Editable extends HTMLElement {
: this.innerHTML; : this.innerHTML;
} }
connectedCallback() { connectedCallback(): void {
this.setAttribute("contenteditable", ""); this.setAttribute("contenteditable", "");
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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