Refactor {CommandIcon,Icon,Square}Button into IconButton and WithState

This commit is contained in:
Henrik Giesel 2021-04-28 13:09:25 +02:00
parent 2f5074bff6
commit 0baf14dc8b
11 changed files with 221 additions and 226 deletions

View file

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

@ -1,87 +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 command: string;
export let onClick: (event: Event) => void;
function onClickWrapped(event: MouseEvent): void {
onClick(event);
updateButton(command, event);
}
// document.queryCommandState(command);
export let update: (event: Event) => boolean;
updateMap.set(command, update);
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}
{active}
{disables}
{dropdownToggle}
on:click
on:mount>
<slot />
</SquareButton>

View file

@ -3,18 +3,82 @@ 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 ButtonGroupItem from "./ButtonGroupItem.svelte"; import type { Readable } from "svelte/store";
import SquareButton from "./SquareButton.svelte"; import { getContext, onMount, createEventDispatcher } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys";
export let id: string; export let id: string;
let className = ""; let className = "";
export { className as class }; export { className as class };
export let tooltip: string | undefined; export let tooltip: string | undefined;
export let active = false;
export let disables = true; export let disables = true;
export let dropdownToggle = false; export let dropdownToggle = false;
$: extraProps = dropdownToggle
? {
"data-bs-toggle": "dropdown",
"aria-expanded": "false",
}
: {};
let buttonRef: HTMLButtonElement;
const disabled = getContext<Readable<boolean>>(disabledKey);
$: _disabled = disables && $disabled;
const nightMode = getContext<boolean>(nightModeKey);
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef }));
</script> </script>
<SquareButton {id} {className} {tooltip} {disables} {dropdownToggle} on:click on:mount> <style lang="scss">
<slot /> @use "ts/sass/button_mixins" as button;
</SquareButton>
button {
padding: 0;
}
@include button.btn-day;
@include button.btn-night;
span {
display: inline-block;
vertical-align: middle;
/* constrain icon */
width: calc(var(--toolbar-size) - 2px);
height: calc(var(--toolbar-size) - 2px);
& > :global(svg),
& > :global(img) {
fill: currentColor;
vertical-align: unset;
width: 100%;
height: 100%;
}
}
.dropdown-toggle::after {
margin-right: 0.25rem;
}
</style>
<button
bind:this={buttonRef}
{id}
class={`btn ${className}`}
class:active
class:dropdown-toggle={dropdownToggle}
class:btn-day={!nightMode}
class:btn-night={nightMode}
tabindex="-1"
title={tooltip}
disabled={_disabled}
{...extraProps}
on:click
on:mousedown|preventDefault>
<span class="p-1"><slot /></span>
</button>

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
-->
<script lang="typescript">
import type { Readable } from "svelte/store";
import { getContext, onMount, createEventDispatcher } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys";
import { mergeTooltipAndShortcut } from "./helpers";
export let id: string;
export let className = "";
export let tooltip: string | undefined;
export let shortcutLabel: string | undefined;
$: title = mergeTooltipAndShortcut(tooltip, shortcutLabel);
export let onClick: (event: MouseEvent) => void;
export let active = false;
export let disables = true;
export let dropdownToggle = false;
$: extraProps = dropdownToggle
? {
"data-bs-toggle": "dropdown",
"aria-expanded": "false",
}
: {};
let buttonRef: HTMLButtonElement;
function extendClassName(className: string): string {
return `btn ${className}`;
}
const disabled = getContext<Readable<boolean>>(disabledKey);
$: _disabled = disables && $disabled;
const nightMode = getContext<boolean>(nightModeKey);
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef }));
</script>
<style lang="scss">
@use "ts/sass/button_mixins" as button;
button {
padding: 0;
}
@include button.btn-day;
@include button.btn-night;
span {
display: inline-block;
vertical-align: middle;
/* constrain icon */
width: calc(var(--toolbar-size) - 2px);
height: calc(var(--toolbar-size) - 2px);
& > :global(svg),
& > :global(img) {
fill: currentColor;
vertical-align: unset;
width: 100%;
height: 100%;
}
}
.dropdown-toggle::after {
margin-right: 0.25rem;
}
</style>
<button
bind:this={buttonRef}
{id}
class={extendClassName(className)}
class:active
class:dropdown-toggle={dropdownToggle}
class:btn-day={!nightMode}
class:btn-night={nightMode}
tabindex="-1"
{title}
disabled={_disabled}
{...extraProps}
on:click={onClick}
on:mousedown|preventDefault>
<span class="p-1"><slot /></span>
</button>

View file

@ -0,0 +1,68 @@
<!--
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 T = unknown;
type UpdaterMap = Map<string, (event: Event) => T>;
type StateMap = Map<string, T>;
const updaterMap = new Map() as UpdaterMap;
const stateMap = new Map() as StateMap;
const stateStore = writable(stateMap);
function updateAllStateWithCallback(callback: (key: string) => T): 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): T => updaterMap.get(key)(event));
}
export function resetAllState(state: T): void {
updateAllStateWithCallback((): T => state);
}
function updateStateByKey(key: string, event: MouseEvent): 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) => T;
export let state: T;
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

@ -19,7 +19,9 @@ filegroup(
compile_svelte( compile_svelte(
name = "svelte", name = "svelte",
srcs = svelte_files, srcs = svelte_files,
deps = [], deps = [
"//ts/components",
],
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
) )
@ -66,13 +68,11 @@ ts_library(
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/components",
"//ts/html-filter", "//ts/html-filter",
# "svelte_components", "//ts:image_module_support",
# "//ts/components:svelte_components",
"@npm//svelte", "@npm//svelte",
], ],
) )

View file

@ -6,10 +6,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
import type { EditingArea } from "./editingArea"; import type { EditingArea } from "./editingArea";
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import CommandIconButton from "components/CommandIconButton.svelte";
import IconButton from "components/IconButton.svelte"; import IconButton from "components/IconButton.svelte";
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
import ButtonDropdown from "components/ButtonDropdown.svelte"; import ButtonDropdown from "components/ButtonDropdown.svelte";
import WithState from "components/WithState.svelte";
import WithDropdownMenu from "components/WithDropdownMenu.svelte"; import WithDropdownMenu from "components/WithDropdownMenu.svelte";
import { getListItem } from "./helpers"; import { getListItem } from "./helpers";
@ -44,25 +44,25 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonDropdown id="listFormatting"> <ButtonDropdown id="listFormatting">
<ButtonGroup id="justify" {api}> <ButtonGroup id="justify" {api}>
<CommandIconButton command="justifyLeft" tooltip={tr.editingAlignLeft()}> <IconButton command="justifyLeft" tooltip={tr.editingAlignLeft()}>
{@html justifyLeftIcon} {@html justifyLeftIcon}
</CommandIconButton> </IconButton>
<CommandIconButton command="justifyCenter" tooltip={tr.editingCenter()}> <IconButton command="justifyCenter" tooltip={tr.editingCenter()}>
{@html justifyCenterIcon} {@html justifyCenterIcon}
</CommandIconButton> </IconButton>
<CommandIconButton command="justifyCenter" tooltip={tr.editingCenter()}> <IconButton command="justifyCenter" tooltip={tr.editingCenter()}>
{@html justifyCenterIcon} {@html justifyCenterIcon}
</CommandIconButton> </IconButton>
<CommandIconButton command="justifyRight" tooltip={tr.editingAlignRight()}> <IconButton command="justifyRight" tooltip={tr.editingAlignRight()}>
{@html justifyRightIcon} {@html justifyRightIcon}
</CommandIconButton> </IconButton>
<CommandIconButton command="justifyFull" tooltip={tr.editingJustify()}> <IconButton command="justifyFull" tooltip={tr.editingJustify()}>
{@html justifyFullIcon} {@html justifyFullIcon}
</CommandIconButton> </IconButton>
</ButtonGroup> </ButtonGroup>
<ButtonGroup id="indentation" {api}> <ButtonGroup id="indentation" {api}>
@ -77,15 +77,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</ButtonDropdown> </ButtonDropdown>
<ButtonGroup id="blockFormatting" {api}> <ButtonGroup id="blockFormatting" {api}>
<CommandIconButton <IconButton command="insertUnorderedList" tooltip={tr.editingUnorderedList()}>
command="insertUnorderedList"
tooltip={tr.editingUnorderedList()}>
{@html ulIcon} {@html ulIcon}
</CommandIconButton> </IconButton>
<CommandIconButton command="insertOrderedList" tooltip={tr.editingOrderedList()}> <IconButton command="insertOrderedList" tooltip={tr.editingOrderedList()}>
{@html olIcon} {@html olIcon}
</CommandIconButton> </IconButton>
<WithDropdownMenu menuId="listFormatting"> <WithDropdownMenu menuId="listFormatting">
<IconButton> <IconButton>

View file

@ -5,9 +5,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<script lang="typescript"> <script lang="typescript">
import * as tr from "lib/i18n"; import * as tr from "lib/i18n";
import CommandIconButton from "components/CommandIconButton.svelte";
import IconButton from "components/IconButton.svelte"; import IconButton from "components/IconButton.svelte";
import ButtonGroup from "components/ButtonGroup.svelte"; import ButtonGroup from "components/ButtonGroup.svelte";
import WithState from "components/WithState.svelte";
import WithShortcut from "components/WithShortcut.svelte"; import WithShortcut from "components/WithShortcut.svelte";
import { import {
@ -24,22 +24,63 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<ButtonGroup id="notetype" {api}> <ButtonGroup id="notetype" {api}>
<WithShortcut shortcut="Control+KeyB" let:createShortcut let:shortcutLabel> <WithShortcut shortcut="Control+KeyB" let:createShortcut let:shortcutLabel>
<CommandIconButton <WithState
tooltip={`${tr.editingBoldText()} (${shortcutLabel})`} key="bold"
command="bold" update={() => document.queryCommandState('bold')}
onClick={() => document.execCommand('bold')} let:state={active}
on:mount={createShortcut}> let:updateState>
{@html boldIcon} <IconButton
</CommandIconButton> tooltip={`${tr.editingBoldText()} (${shortcutLabel})`}
{active}
on:click={(event) => {
document.execCommand('bold');
updateState(event);
}}
on:mount={createShortcut}>
{@html boldIcon}
</IconButton>
</WithState>
</WithShortcut> </WithShortcut>
<WithShortcut shortcut="Control+KeyI"> <WithShortcut shortcut="Control+KeyI" let:createShortcut let:shortcutLabel>
<CommandIconButton tooltip={tr.editingItalicText()} command="italic"> <WithState
{@html italicIcon} key="italic"
</CommandIconButton> update={() => document.queryCommandState('italic')}
let:state={active}
let:updateState>
<IconButton
tooltip={`${tr.editingItalicText()} (${shortcutLabel})`}
{active}
on:click={(event) => {
document.execCommand('italic');
updateState(event);
}}
on:mount={createShortcut}>
{@html italicIcon}
</IconButton>
</WithState>
</WithShortcut> </WithShortcut>
<WithShortcut shortcut="Control+KeyU"> <WithShortcut shortcut="Control+KeyU" let:createShortcut let:shortcutLabel>
<WithState
key="underline"
update={() => document.queryCommandState('underline')}
let:state={active}
let:updateState>
<IconButton
tooltip={`${tr.editingUnderlineText()} (${shortcutLabel})`}
{active}
on:click={(event) => {
document.execCommand('underline');
updateState(event);
}}
on:mount={createShortcut}>
{@html underlineIcon}
</IconButton>
</WithState>
</WithShortcut>
<!--<WithShortcut shortcut="Control+KeyU">
<CommandIconButton tooltip={tr.editingUnderlineText()} command="underline"> <CommandIconButton tooltip={tr.editingUnderlineText()} command="underline">
{@html underlineIcon} {@html underlineIcon}
</CommandIconButton> </CommandIconButton>
@ -65,5 +106,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
}}> }}>
{@html eraserIcon} {@html eraserIcon}
</IconButton> </IconButton>
</WithShortcut> </WithShortcut> -->
</ButtonGroup> </ButtonGroup>

View file

@ -48,7 +48,7 @@ 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"));
} }
} }
@ -163,7 +163,7 @@ export function setFormat(cmd: string, arg?: any, nosave: boolean = 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));
} }
} }

View file

@ -10,7 +10,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 +56,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

@ -33,7 +33,10 @@ export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
} }
/* Exports for editor */ /* Exports for editor */
export {
updateAllState as updateActiveButtons,
resetAllState as clearActiveButtons,
} from "components/WithState.svelte";
// @ts-expect-error insufficient typing of svelte modules // @ts-expect-error insufficient typing of svelte modules
export { enableButtons, disableButtons } from "./EditorToolbar.svelte"; export { enableButtons, disableButtons } from "./EditorToolbar.svelte";
// @ts-expect-error insufficient typing of svelte modules
export { updateActiveButtons, clearActiveButtons } from "components/CommandIconButton.svelte";