Merge pull request #1150 from hgiesel/notetypechoose

Editor Toolbar cleanup
This commit is contained in:
Damien Elmes 2021-04-24 11:04:24 +10:00 committed by GitHub
commit c71b684a94
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 374 additions and 330 deletions

View file

@ -386,13 +386,12 @@ class Browser(QMainWindow):
editor._links["preview"] = lambda _editor: self.onTogglePreview() editor._links["preview"] = lambda _editor: self.onTogglePreview()
editor.web.eval( editor.web.eval(
f""" f"""
$editorToolbar.addButton({{ $editorToolbar.addButton(editorToolbar.labelButton({{
component: editorToolbar.LabelButton,
label: `{tr.actions_preview()}`, label: `{tr.actions_preview()}`,
tooltip: `{tr.browsing_preview_selected_card(val=shortcut(preview_shortcut))}`, tooltip: `{tr.browsing_preview_selected_card(val=shortcut(preview_shortcut))}`,
onClick: () => bridgeCommand("preview"), onClick: () => bridgeCommand("preview"),
disables: false, disables: false,
}}, "notetype"); }}), "notetype", -1);
""" """
) )

View file

@ -155,7 +155,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.addButton({{ component: editorToolbar.RawButton, html: `{button}` }}, 'notetype');" f"$editorToolbar.addButton(editorToolbar.rawButton({{ html: `{button}` }}), 'notetype', -1);"
for button in lefttopbtns for button in lefttopbtns
] ]
lefttopbtns_js = "\n".join(lefttopbtns_defs) lefttopbtns_js = "\n".join(lefttopbtns_defs)
@ -167,16 +167,16 @@ class Editor:
righttopbtns_defs = "\n".join( righttopbtns_defs = "\n".join(
[ [
f"{{ component: editorToolbar.RawButton, html: `{button}` }}," f"editorToolbar.rawButton({{ html: `{button}` }}),"
for button in righttopbtns for button in righttopbtns
] ]
) )
righttopbtns_js = ( righttopbtns_js = (
f""" f"""
$editorToolbar.addButtonGroup({{ $editorToolbar.addButton(editorToolbar.buttonGroup({{
id: "addons", id: "addons",
buttons: [ {righttopbtns_defs} ] items: [ {righttopbtns_defs} ]
}}); }}), -1);
""" """
if righttopbtns_defs if righttopbtns_defs
else "" else ""
@ -1277,13 +1277,9 @@ 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.showButton("template", "cloze"); ')
'document.getElementById("editorToolbar").showButton("template", "cloze"); '
)
else: else:
editor.web.eval( editor.web.eval('$editorToolbar.hideButton("template", "cloze"); ')
'document.getElementById("editorToolbar").hideButton("template", "cloze"); '
)
gui_hooks.editor_did_load_note.append(set_cloze_button) gui_hooks.editor_did_load_note.append(set_cloze_button)

View file

@ -5,5 +5,5 @@ import type { ToolbarItem } from "./types";
export interface ButtonDropdownProps { export interface ButtonDropdownProps {
id: string; id: string;
className?: string; className?: string;
buttons: ToolbarItem[]; items: ToolbarItem[];
} }

View file

@ -10,10 +10,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let className = ""; export let className = "";
function extendClassName(className: string): string { function extendClassName(className: string): string {
return `dropdown-menu btn-dropdown-menu py-1 mb-0 ${className}`; return `dropdown-menu btn-dropdown-menu ${className}`;
} }
export let buttons: ToolbarItem[]; export let items: ToolbarItem[];
</script> </script>
<style> <style>
@ -22,6 +22,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
background-color: var(--window-bg); background-color: var(--window-bg);
border-color: var(--medium-border); border-color: var(--medium-border);
} }
:global(ul.btn-dropdown-menu.show) {
display: flex;
}
</style> </style>
<ButtonGroup {id} className={extendClassName(className)} {buttons} /> <ButtonGroup {id} className={extendClassName(className)} {items} />

View file

@ -5,5 +5,6 @@ import type { ToolbarItem } from "./types";
export interface ButtonGroupProps { export interface ButtonGroupProps {
id: string; id: string;
className?: string; className?: string;
buttons: ToolbarItem[]; items: ToolbarItem[];
fullWidth?: boolean;
} }

View file

@ -9,7 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export let id: string | undefined = undefined; export let id: string | undefined = undefined;
export let className = ""; export let className = "";
export let buttons: ToolbarItem[]; export let items: ToolbarItem[];
function filterHidden({ hidden = false, ...props }) { function filterHidden({ hidden = false, ...props }) {
return props; return props;
@ -26,19 +26,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
flex-wrap: var(--toolbar-wrap); flex-wrap: var(--toolbar-wrap);
overflow-y: auto; overflow-y: auto;
padding-inline-start: 0; padding: calc(var(--toolbar-size) / 10);
margin: 0 0 calc(var(--toolbar-size) / 10); margin: 0;
}
.border-overlap-group { &.border-overlap-group {
:global(button), :global(button),
:global(select) { :global(select) {
margin-left: -1px; margin-left: -1px;
}
}
&.gap-group {
:global(button),
:global(select) {
margin-left: 1px;
}
} }
} }
li { li {
display: inline-block; display: contents;
> :global(button), > :global(button),
> :global(select) { > :global(select) {
@ -46,36 +53,31 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
} }
&:nth-child(1) { &:nth-child(1) {
margin-left: calc(var(--toolbar-size) / 7.5);
> :global(button), > :global(button),
> :global(select) { > :global(select) {
/* default 0.25rem */
border-top-left-radius: calc(var(--toolbar-size) / 7.5); border-top-left-radius: calc(var(--toolbar-size) / 7.5);
border-bottom-left-radius: calc(var(--toolbar-size) / 7.5); border-bottom-left-radius: calc(var(--toolbar-size) / 7.5);
} }
} }
&:nth-last-child(1) { &:nth-last-child(1) {
margin-right: calc(var(--toolbar-size) / 7.5);
> :global(button), > :global(button),
> :global(select) { > :global(select) {
border-top-right-radius: calc(var(--toolbar-size) / 7.5); border-top-right-radius: calc(var(--toolbar-size) / 7.5);
border-bottom-right-radius: calc(var(--toolbar-size) / 7.5); border-bottom-right-radius: calc(var(--toolbar-size) / 7.5);
} }
} }
&.gap-item:not(:nth-child(1)) {
margin-left: 1px;
}
} }
</style> </style>
<ul {id} class={className} class:border-overlap-group={!nightMode}> <ul
{#each buttons as button} {id}
class={className}
class:border-overlap-group={!nightMode}
class:gap-group={nightMode}>
{#each items as button}
{#if !button.hidden} {#if !button.hidden}
<li class:gap-item={nightMode}> <li>
<svelte:component this={button.component} {...filterHidden(button)} /> <svelte:component this={button.component} {...filterHidden(button)} />
</li> </li>
{/if} {/if}

View file

@ -4,5 +4,5 @@ import type { ToolbarItem } from "./types";
export interface DropdownMenuProps { export interface DropdownMenuProps {
id: string; id: string;
menuItems: ToolbarItem[]; items: ToolbarItem[];
} }

View file

@ -3,12 +3,12 @@ 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 { getContext } from "svelte"; import { getContext } from "svelte";
import { nightModeKey } from "./contextKeys"; import { nightModeKey } from "./contextKeys";
export let id: string; export let id: string;
export let menuItems: DynamicSvelteComponent[]; export let items: ToolbarItem[];
const nightMode = getContext<boolean>(nightModeKey); const nightMode = getContext<boolean>(nightModeKey);
</script> </script>
@ -27,7 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</style> </style>
<ul {id} class="dropdown-menu" class:night-mode={nightMode}> <ul {id} class="dropdown-menu" class:night-mode={nightMode}>
{#each menuItems as menuItem} {#each items as menuItem}
<li> <li>
<svelte:component this={menuItem.component} {...menuItem} /> <svelte:component this={menuItem.component} {...menuItem} />
</li> </li>

View file

@ -19,16 +19,13 @@ 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 { ToolbarItem } from "./types"; import type { ToolbarItem, IterableToolbarItem } from "./types";
import { setContext } from "svelte"; import { setContext } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys"; import { disabledKey, nightModeKey } from "./contextKeys";
import ButtonGroup from "./ButtonGroup.svelte"; import ButtonGroup from "./ButtonGroup.svelte";
import type { ButtonGroupProps } from "./ButtonGroup";
export let buttons: Readable< export let buttons: Readable<IterableToolbarItem[]>;
(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]
>;
export let menus: Readable<ToolbarItem[]>; export let menus: Readable<ToolbarItem[]>;
$: _buttons = $buttons; $: _buttons = $buttons;
@ -56,17 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
background: var(--bg-color); background: var(--bg-color);
border-bottom: 1px solid var(--border); border-bottom: 1px solid var(--border);
/* Remove outermost marigns */
& > :global(ul) {
& > :global(li:nth-child(1)) {
margin-left: 0;
}
& > :global(li:nth-last-child(1)) {
margin-right: 0;
}
}
} }
</style> </style>
@ -77,5 +63,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</div> </div>
<nav {style}> <nav {style}>
<ButtonGroup buttons={_buttons} className="mt-0" /> <ButtonGroup items={_buttons} className="p-0 mb-1" />
</nav> </nav>

15
ts/editor-toolbar/SelectButton.d.ts vendored Normal file
View file

@ -0,0 +1,15 @@
// 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

@ -4,16 +4,11 @@ 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: boolean;
}
export let id: string; export let id: string;
export let className = ""; export let className = "";
export let tooltip: string; export let tooltip: string;

11
ts/editor-toolbar/WithLabel.d.ts vendored Normal file
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
import type { ToolbarItem } from "./types";
export interface WithLabelProps {
id?: string;
className?: string;
button: ToolbarItem;
label: string;
}

View file

@ -0,0 +1,27 @@
<!--
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";
export let id: string;
export let className = "";
export let label: string;
export let button: ToolbarItem;
</script>
<style lang="scss">
label {
display: flex;
padding: 0 calc(var(--toolbar-size) / 10);
}
</style>
<!-- svelte-ignore a11y-label-has-associated-control -->
<label {id} class={className}>
<span class="me-1">{label}</span>
<svelte:component this={button.component} {...button} />
</label>

View file

@ -1,5 +1,6 @@
// 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 RawButton from "./RawButton.svelte";
import LabelButton from "./LabelButton.svelte"; import LabelButton from "./LabelButton.svelte";
import type { LabelButtonProps } from "./LabelButton"; import type { LabelButtonProps } from "./LabelButton";
import IconButton from "./IconButton.svelte"; import IconButton from "./IconButton.svelte";
@ -8,6 +9,8 @@ import CommandIconButton from "./CommandIconButton.svelte";
import type { CommandIconButtonProps } from "./CommandIconButton"; import type { CommandIconButtonProps } from "./CommandIconButton";
import ColorPicker from "./ColorPicker.svelte"; import ColorPicker from "./ColorPicker.svelte";
import type { ColorPickerProps } from "./ColorPicker"; import type { ColorPickerProps } from "./ColorPicker";
import SelectButton from "./SelectButton.svelte";
import type { SelectButtonProps } from "./SelectButton";
import ButtonGroup from "./ButtonGroup.svelte"; import ButtonGroup from "./ButtonGroup.svelte";
import type { ButtonGroupProps } from "./ButtonGroup"; import type { ButtonGroupProps } from "./ButtonGroup";
@ -23,8 +26,14 @@ import type { WithDropdownMenuProps } from "./WithDropdownMenu";
import WithShortcut from "./WithShortcut.svelte"; import WithShortcut from "./WithShortcut.svelte";
import type { WithShortcutProps } from "./WithShortcut"; import type { WithShortcutProps } from "./WithShortcut";
import WithLabel from "./WithLabel.svelte";
import type { WithLabelProps } from "./WithLabel";
import { dynamicComponent } from "sveltelib/dynamicComponent"; import { dynamicComponent } from "sveltelib/dynamicComponent";
export const rawButton = dynamicComponent<typeof RawButton, { html: string }>(
RawButton
);
export const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>( export const labelButton = dynamicComponent<typeof LabelButton, LabelButtonProps>(
LabelButton LabelButton
); );
@ -38,6 +47,9 @@ export const commandIconButton = dynamicComponent<
export const colorPicker = dynamicComponent<typeof ColorPicker, ColorPickerProps>( export const colorPicker = dynamicComponent<typeof ColorPicker, ColorPickerProps>(
ColorPicker ColorPicker
); );
export const selectButton = dynamicComponent<typeof SelectButton, SelectButtonProps>(
SelectButton
);
export const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>( export const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(
ButtonGroup ButtonGroup
@ -62,3 +74,5 @@ export const withDropdownMenu = dynamicComponent<
export const withShortcut = dynamicComponent<typeof WithShortcut, WithShortcutProps>( export const withShortcut = dynamicComponent<typeof WithShortcut, WithShortcutProps>(
WithShortcut WithShortcut
); );
export const withLabel = dynamicComponent<typeof WithLabel, WithLabelProps>(WithLabel);

View file

@ -0,0 +1,20 @@
// 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

@ -4,43 +4,89 @@ export interface Identifiable {
id?: string; id?: string;
} }
function normalize<T extends Identifiable>( interface IterableIdentifiable<T extends Identifiable> extends Identifiable {
values: T[], items: T[];
idOrIndex: string | number
): number {
if (typeof idOrIndex === "string") {
return values.findIndex((value) => value.id === idOrIndex);
} else {
return idOrIndex >= values.length ? -1 : idOrIndex;
}
} }
export function search<T extends Identifiable>( export type Identifier = string | number;
values: T[],
idOrIndex: string | number function normalize<T extends Identifiable>(
): T | null { iterable: IterableIdentifiable<T>,
const index = normalize(values, idOrIndex); 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; return index >= 0 ? values[index] : null;
} }
export function insert<T extends Identifiable>( export function insert<T extends Identifiable>(
values: T[], iterable: IterableIdentifiable<T> & T,
value: T, value: T,
idOrIndex: string | number idOrIndex: Identifier
): T[] { ): IterableIdentifiable<T> & T {
const index = normalize(values, idOrIndex); const index = normalize(iterable, idOrIndex);
return index >= 0
? [...values.slice(0, index), value, ...values.slice(index)] if (index >= 0) {
: values; iterable.items = iterable.items.slice();
iterable.items.splice(index, 0, value);
}
return iterable;
} }
export function add<T extends Identifiable>( export function add<T extends Identifiable>(
values: T[], iterable: IterableIdentifiable<T> & T,
value: T, value: T,
idOrIndex: string | number idOrIndex: Identifier
): T[] { ): IterableIdentifiable<T> & T {
const index = normalize(values, idOrIndex); const index = normalize(iterable, idOrIndex);
return index >= 0
? [...values.slice(0, index + 1), value, ...values.slice(index + 1)] if (index >= 0) {
: values; 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,54 +1,31 @@
// 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 { SvelteComponentDev } from "svelte/internal"; import type { ToolbarItem, IterableToolbarItem } from "./types";
import type { ToolbarItem } from "./types"; import type { Identifier } from "./identifiable";
import ButtonGroup from "./ButtonGroup.svelte";
import type { ButtonGroupProps } from "./ButtonGroup";
import { dynamicComponent } from "sveltelib/dynamicComponent";
import { Writable, writable } from "svelte/store"; import { Writable, writable } from "svelte/store";
import EditorToolbarSvelte from "./EditorToolbar.svelte"; import EditorToolbarSvelte from "./EditorToolbar.svelte";
import "./bootstrap.css"; import "./bootstrap.css";
import { Identifiable, search, add, insert } from "./identifiable"; import { add, insert, updateRecursive } from "./identifiable";
import { showComponent, hideComponent, toggleComponent } from "./hideable";
interface Hideable { let buttonsResolve: (value: Writable<IterableToolbarItem[]>) => void;
hidden?: boolean;
}
function showComponent(component: Hideable): void {
component.hidden = false;
}
function hideComponent(component: Hideable): void {
component.hidden = true;
}
function toggleComponent(component: Hideable): void {
component.hidden = !component.hidden;
}
const buttonGroup = dynamicComponent<typeof ButtonGroup, ButtonGroupProps>(ButtonGroup);
let buttonsResolve: (
value: Writable<(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]>
) => void;
let menusResolve: (value: Writable<ToolbarItem[]>) => void; let menusResolve: (value: Writable<ToolbarItem[]>) => void;
export class EditorToolbar extends HTMLElement { export class EditorToolbar extends HTMLElement {
component?: SvelteComponentDev; private buttonsPromise: Promise<Writable<IterableToolbarItem[]>> = new Promise(
(resolve) => {
buttonsPromise: Promise< buttonsResolve = resolve;
Writable<(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]> }
> = new Promise((resolve) => { );
buttonsResolve = resolve; private menusPromise: Promise<Writable<ToolbarItem[]>> = new Promise(
}); (resolve): void => {
menusPromise: Promise<Writable<ToolbarItem[]>> = new Promise((resolve): void => { menusResolve = resolve;
menusResolve = resolve; }
}); );
connectedCallback(): void { connectedCallback(): void {
globalThis.$editorToolbar = this; globalThis.$editorToolbar = this;
@ -56,7 +33,7 @@ export class EditorToolbar extends HTMLElement {
const buttons = writable([]); const buttons = writable([]);
const menus = writable([]); const menus = writable([]);
this.component = new EditorToolbarSvelte({ new EditorToolbarSvelte({
target: this, target: this,
props: { props: {
buttons, buttons,
@ -69,117 +46,89 @@ export class EditorToolbar extends HTMLElement {
menusResolve(menus); menusResolve(menus);
} }
updateButtonGroup<T>(
update: (
component: ToolbarItem<typeof ButtonGroup> & ButtonGroupProps & T
) => void,
group: string | number
): void {
this.buttonsPromise.then((buttons) => {
buttons.update((buttonGroups) => {
const foundGroup = search(buttonGroups, group);
if (foundGroup) {
update(
foundGroup as ToolbarItem<typeof ButtonGroup> &
ButtonGroupProps &
T
);
}
return buttonGroups;
});
return buttons;
});
}
showButtonGroup(group: string | number): void {
this.updateButtonGroup<Hideable>(showComponent, group);
}
hideButtonGroup(group: string | number): void {
this.updateButtonGroup<Hideable>(hideComponent, group);
}
toggleButtonGroup(group: string | number): void {
this.updateButtonGroup<Hideable>(toggleComponent, group);
}
insertButtonGroup(newGroup: ButtonGroupProps, group: string | number = 0): void {
this.buttonsPromise.then((buttons) => {
buttons.update((buttonGroups) => {
const newButtonGroup = buttonGroup(newGroup);
return insert(buttonGroups, newButtonGroup, group);
});
return buttons;
});
}
addButtonGroup(newGroup: ButtonGroupProps, group: string | number = -1): void {
this.buttonsPromise.then((buttons) => {
buttons.update((buttonGroups) => {
const newButtonGroup = buttonGroup(newGroup);
return add(buttonGroups, newButtonGroup, group);
});
return buttons;
});
}
updateButton( updateButton(
update: (component: ToolbarItem) => void, update: (component: ToolbarItem) => ToolbarItem,
group: string | number, ...identifiers: Identifier[]
button: string | number
): void { ): void {
this.updateButtonGroup((foundGroup) => { this.buttonsPromise.then(
const foundButton = search(foundGroup.buttons, button); (
buttons: Writable<IterableToolbarItem[]>
): Writable<IterableToolbarItem[]> => {
buttons.update(
(items: IterableToolbarItem[]): IterableToolbarItem[] =>
updateRecursive(
update,
({ items } as unknown) as ToolbarItem,
...identifiers
).items as IterableToolbarItem[]
);
if (foundButton) { return buttons;
update(foundButton);
} }
}, group); );
} }
showButton(group: string | number, button: string | number): void { showButton(...identifiers: Identifier[]): void {
this.updateButton(showComponent, group, button); this.updateButton(showComponent, ...identifiers);
} }
hideButton(group: string | number, button: string | number): void { hideButton(...identifiers: Identifier[]): void {
this.updateButton(hideComponent, group, button); this.updateButton(hideComponent, ...identifiers);
} }
toggleButton(group: string | number, button: string | number): void { toggleButton(...identifiers: Identifier[]): void {
this.updateButton(toggleComponent, group, button); this.updateButton(toggleComponent, ...identifiers);
} }
insertButton( insertButton(newButton: ToolbarItem, ...identifiers: Identifier[]): void {
newButton: ToolbarItem & Identifiable, const initIdentifiers = identifiers.slice(0, -1);
group: string | number, const lastIdentifier = identifiers[identifiers.length - 1];
button: string | number = 0 this.updateButton(
(component: ToolbarItem) =>
insert(component as IterableToolbarItem, newButton, lastIdentifier),
...initIdentifiers
);
}
addButton(newButton: ToolbarItem, ...identifiers: Identifier[]): void {
const initIdentifiers = identifiers.slice(0, -1);
const lastIdentifier = identifiers[identifiers.length - 1];
this.updateButton(
(component: ToolbarItem) =>
add(component as IterableToolbarItem, newButton, lastIdentifier),
...initIdentifiers
);
}
updateMenu(
update: (component: ToolbarItem) => ToolbarItem,
...identifiers: Identifier[]
): void { ): void {
this.updateButtonGroup((component) => { this.menusPromise.then(
component.buttons = insert( (menus: Writable<ToolbarItem[]>): Writable<ToolbarItem[]> => {
component.buttons as (ToolbarItem & Identifiable)[], menus.update(
newButton, (items: ToolbarItem[]): ToolbarItem[] =>
button updateRecursive(
); update,
}, group); ({ items } as unknown) as ToolbarItem,
...identifiers
).items as ToolbarItem[]
);
return menus;
}
);
} }
addButton( addMenu(newMenu: ToolbarItem, ...identifiers: Identifier[]): void {
newButton: ToolbarItem & Identifiable, const initIdentifiers = identifiers.slice(0, -1);
group: string | number, const lastIdentifier = identifiers[identifiers.length - 1];
button: string | number = -1 this.updateMenu(
): void { (component: ToolbarItem) =>
this.updateButtonGroup((component) => { add(component as IterableToolbarItem, newMenu, lastIdentifier),
component.buttons = add( ...initIdentifiers
component.buttons as (ToolbarItem & Identifiable)[], );
newButton,
button
);
}, group);
} }
} }

View file

@ -3,8 +3,15 @@
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
interface ToolbarItem<T extends typeof SvelteComponentDev = typeof SvelteComponentDev> export interface ToolbarItem<
extends DynamicSvelteComponent<T> { T extends typeof SvelteComponentDev = typeof SvelteComponentDev
> extends DynamicSvelteComponent<T> {
id?: string; id?: string;
hidden?: boolean; hidden?: boolean;
} }
export interface IterableToolbarItem<
T extends typeof SvelteComponentDev = typeof SvelteComponentDev
> extends ToolbarItem<T> {
items: ToolbarItem[];
}

View file

@ -1,24 +1,35 @@
// 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 { default as RawButton } from "editor-toolbar/RawButton.svelte"; import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import { default as LabelButton } from "editor-toolbar/LabelButton.svelte";
import { default as IconButton } from "editor-toolbar/IconButton.svelte";
import { default as CommandIconButton } from "editor-toolbar/CommandIconButton.svelte";
import { default as SelectButton } from "editor-toolbar/SelectButton.svelte";
import { default as DropdownMenu } from "editor-toolbar/DropdownMenu.svelte"; import {
import { default as DropdownItem } from "editor-toolbar/DropdownItem.svelte"; buttonGroup,
import { default as ButtonDropdown } from "editor-toolbar/DropdownMenu.svelte"; rawButton,
import { default as WithDropdownMenu } from "editor-toolbar/WithDropdownMenu.svelte"; labelButton,
iconButton,
commandIconButton,
selectButton,
dropdownMenu,
dropdownItem,
buttonDropdown,
withDropdownMenu,
withLabel,
} from "editor-toolbar/dynamicComponents";
export const editorToolbar = { export const editorToolbar: Record<
RawButton, string,
LabelButton, (props: Record<string, unknown>) => DynamicSvelteComponent
IconButton, > = {
CommandIconButton, buttonGroup,
SelectButton, rawButton,
DropdownMenu, labelButton,
DropdownItem, iconButton,
ButtonDropdown, commandIconButton,
WithDropdownMenu, selectButton,
dropdownMenu,
dropdownItem,
buttonDropdown,
withDropdownMenu,
withLabel,
}; };

View file

@ -1,8 +1,6 @@
// 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 WithShortcut from "editor-toolbar/WithShortcut.svelte"; import type { ToolbarItem } from "editor-toolbar/types";
import type { WithShortcutProps } from "editor-toolbar/WithShortcut";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import { iconButton, withShortcut } from "editor-toolbar/dynamicComponents"; import { iconButton, withShortcut } from "editor-toolbar/dynamicComponents";
@ -40,8 +38,7 @@ function onCloze(event: KeyboardEvent | MouseEvent): void {
wrap(`{{c${highestCloze}::`, "}}"); wrap(`{{c${highestCloze}::`, "}}");
} }
export function getClozeButton(): DynamicSvelteComponent<typeof WithShortcut> & export function getClozeButton(): ToolbarItem {
WithShortcutProps {
return withShortcut({ return withShortcut({
id: "cloze", id: "cloze",
shortcut: "Control+Shift+KeyC", shortcut: "Control+Shift+KeyC",

View file

@ -1,8 +1,6 @@
// 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 ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; import type { IterableToolbarItem } from "editor-toolbar/types";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import { import {
iconButton, iconButton,
@ -29,8 +27,7 @@ function wrapWithForecolor(color: string): void {
document.execCommand("forecolor", false, color); document.execCommand("forecolor", false, color);
} }
export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getColorGroup(): IterableToolbarItem {
ButtonGroupProps {
const forecolorButton = withShortcut({ const forecolorButton = withShortcut({
shortcut: "F7", shortcut: "F7",
button: iconButton({ button: iconButton({
@ -52,6 +49,6 @@ export function getColorGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
return buttonGroup({ return buttonGroup({
id: "color", id: "color",
buttons: [forecolorButton, colorpickerButton], items: [forecolorButton, colorpickerButton],
}); });
} }

View file

@ -59,3 +59,7 @@
opacity: 0.5; opacity: 0.5;
} }
} }
.flex-basis-100 {
flex-basis: 100%;
}

View file

@ -1,11 +1,6 @@
// 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 ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; import type { IterableToolbarItem } from "editor-toolbar/types";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import type ButtonDropdown from "editor-toolbar/ButtonDropdown.svelte";
import type { ButtonDropdownProps } from "editor-toolbar/ButtonDropdown";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import type { EditingArea } from "./editingArea"; import type { EditingArea } from "./editingArea";
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
@ -45,8 +40,7 @@ const indentListItem = () => {
} }
}; };
export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDropdown> & export function getFormatBlockMenus(): IterableToolbarItem[] {
ButtonDropdownProps)[] {
const justifyLeftButton = commandIconButton({ const justifyLeftButton = commandIconButton({
icon: justifyLeftIcon, icon: justifyLeftIcon,
command: "justifyLeft", command: "justifyLeft",
@ -73,7 +67,7 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDrop
const justifyGroup = buttonGroup({ const justifyGroup = buttonGroup({
id: "justify", id: "justify",
buttons: [ items: [
justifyLeftButton, justifyLeftButton,
justifyCenterButton, justifyCenterButton,
justifyRightButton, justifyRightButton,
@ -95,19 +89,18 @@ export function getFormatBlockMenus(): (DynamicSvelteComponent<typeof ButtonDrop
const indentationGroup = buttonGroup({ const indentationGroup = buttonGroup({
id: "indentation", id: "indentation",
buttons: [outdentButton, indentButton], items: [outdentButton, indentButton],
}); });
const formattingOptions = buttonDropdown({ const formattingOptions = buttonDropdown({
id: "listFormatting", id: "listFormatting",
buttons: [justifyGroup, indentationGroup], items: [justifyGroup, indentationGroup],
}); });
return [formattingOptions]; return [formattingOptions];
} }
export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getFormatBlockGroup(): IterableToolbarItem {
ButtonGroupProps {
const ulButton = commandIconButton({ const ulButton = commandIconButton({
icon: ulIcon, icon: ulIcon,
command: "insertUnorderedList", command: "insertUnorderedList",
@ -131,6 +124,6 @@ export function getFormatBlockGroup(): DynamicSvelteComponent<typeof ButtonGroup
return buttonGroup({ return buttonGroup({
id: "blockFormatting", id: "blockFormatting",
buttons: [ulButton, olButton, listFormatting], items: [ulButton, olButton, listFormatting],
}); });
} }

View file

@ -1,8 +1,6 @@
// 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 ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; import type { IterableToolbarItem } from "editor-toolbar/types";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import { import {
@ -19,8 +17,7 @@ import superscriptIcon from "./format-superscript.svg";
import subscriptIcon from "./format-subscript.svg"; import subscriptIcon from "./format-subscript.svg";
import eraserIcon from "./eraser.svg"; import eraserIcon from "./eraser.svg";
export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getFormatInlineGroup(): IterableToolbarItem {
ButtonGroupProps {
const boldButton = withShortcut({ const boldButton = withShortcut({
shortcut: "Control+KeyB", shortcut: "Control+KeyB",
button: commandIconButton({ button: commandIconButton({
@ -79,7 +76,7 @@ export function getFormatInlineGroup(): DynamicSvelteComponent<typeof ButtonGrou
return buttonGroup({ return buttonGroup({
id: "inlineFormatting", id: "inlineFormatting",
buttons: [ items: [
boldButton, boldButton,
italicButton, italicButton,
underlineButton, underlineButton,

View file

@ -1,8 +1,6 @@
// 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 ButtonGroup from "editor-toolbar/ButtonGroup.svelte"; import type { IterableToolbarItem } from "editor-toolbar/types";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import { bridgeCommand } from "lib/bridgecommand"; import { bridgeCommand } from "lib/bridgecommand";
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
@ -12,8 +10,7 @@ import {
withShortcut, withShortcut,
} from "editor-toolbar/dynamicComponents"; } from "editor-toolbar/dynamicComponents";
export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getNotetypeGroup(): IterableToolbarItem {
ButtonGroupProps {
const fieldsButton = labelButton({ const fieldsButton = labelButton({
onClick: () => bridgeCommand("fields"), onClick: () => bridgeCommand("fields"),
disables: false, disables: false,
@ -33,6 +30,6 @@ export function getNotetypeGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
return buttonGroup({ return buttonGroup({
id: "notetype", id: "notetype",
buttons: [fieldsButton, cardsButton], items: [fieldsButton, cardsButton],
}); });
} }

View file

@ -1,10 +1,6 @@
// 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 DropdownMenu from "editor-toolbar/DropdownMenu.svelte"; import type { IterableToolbarItem } from "editor-toolbar/types";
import type { DropdownMenuProps } from "editor-toolbar/DropdownMenu";
import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import type { DynamicSvelteComponent } from "sveltelib/dynamicComponent";
import { bridgeCommand } from "lib/bridgecommand"; import { bridgeCommand } from "lib/bridgecommand";
import { import {
@ -40,8 +36,7 @@ function onHtmlEdit(): void {
const mathjaxMenuId = "mathjaxMenu"; const mathjaxMenuId = "mathjaxMenu";
export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> & export function getTemplateGroup(): IterableToolbarItem {
ButtonGroupProps {
const attachmentButton = withShortcut({ const attachmentButton = withShortcut({
shortcut: "F3", shortcut: "F3",
button: iconButton({ button: iconButton({
@ -80,7 +75,7 @@ export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
return buttonGroup({ return buttonGroup({
id: "template", id: "template",
buttons: [ items: [
attachmentButton, attachmentButton,
recordButton, recordButton,
getClozeButton(), getClozeButton(),
@ -90,8 +85,7 @@ export function getTemplateGroup(): DynamicSvelteComponent<typeof ButtonGroup> &
}); });
} }
export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu> & export function getTemplateMenus(): IterableToolbarItem[] {
DropdownMenuProps)[] {
const mathjaxMenuItems = [ const mathjaxMenuItems = [
withShortcut({ withShortcut({
shortcut: "Control+KeyM, KeyM", shortcut: "Control+KeyM, KeyM",
@ -142,7 +136,7 @@ export function getTemplateMenus(): (DynamicSvelteComponent<typeof DropdownMenu>
const mathjaxMenu = dropdownMenu({ const mathjaxMenu = dropdownMenu({
id: mathjaxMenuId, id: mathjaxMenuId,
menuItems: [...mathjaxMenuItems, ...latexMenuItems], items: [...mathjaxMenuItems, ...latexMenuItems],
}); });
return [mathjaxMenu]; return [mathjaxMenu];

View file

@ -1,10 +1,5 @@
// 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 "editor-toolbar/types";
import type ButtonGroup from "editor-toolbar/ButtonGroup.svelte";
import type { ButtonGroupProps } from "editor-toolbar/ButtonGroup";
import type { Writable } from "svelte/store";
import { getNotetypeGroup } from "./notetype"; import { getNotetypeGroup } from "./notetype";
import { getFormatInlineGroup } from "./formatInline"; import { getFormatInlineGroup } from "./formatInline";
import { getFormatBlockGroup, getFormatBlockMenus } from "./formatBlock"; import { getFormatBlockGroup, getFormatBlockMenus } from "./formatBlock";
@ -14,32 +9,18 @@ import { getTemplateGroup, getTemplateMenus } from "./template";
export function initToolbar(i18n: Promise<void>): void { export function initToolbar(i18n: Promise<void>): void {
document.addEventListener("DOMContentLoaded", () => { document.addEventListener("DOMContentLoaded", () => {
i18n.then(() => { i18n.then(() => {
globalThis.$editorToolbar.buttonsPromise.then( const buttons = [
( getNotetypeGroup(),
buttons: Writable< getFormatInlineGroup(),
(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[] getFormatBlockGroup(),
> getColorGroup(),
): Writable<(ToolbarItem<typeof ButtonGroup> & ButtonGroupProps)[]> => { getTemplateGroup(),
buttons.update(() => [ ];
getNotetypeGroup(),
getFormatInlineGroup(),
getFormatBlockGroup(),
getColorGroup(),
getTemplateGroup(),
]);
return buttons;
}
);
globalThis.$editorToolbar.menusPromise.then( const menus = [...getFormatBlockMenus(), ...getTemplateMenus()];
(menus: Writable<ToolbarItem[]>): Writable<ToolbarItem[]> => {
menus.update(() => [ globalThis.$editorToolbar.updateButton(() => ({ items: buttons }));
...getFormatBlockMenus(), globalThis.$editorToolbar.updateMenu(() => ({ items: menus }));
...getTemplateMenus(),
]);
return menus;
}
);
}); });
}); });
} }

View file

@ -9,6 +9,7 @@ $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";
body, html { body,
html {
overscroll-behavior: none; overscroll-behavior: none;
} }