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//bootstrap",
"@npm//svelte",
],
] + svelte_names,
)
# 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
-->
<script lang="typescript">
import ButtonGroupItem from "./ButtonGroupItem.svelte";
import SquareButton from "./SquareButton.svelte";
import type { Readable } from "svelte/store";
import { getContext, onMount, createEventDispatcher } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys";
export let id: string;
let className = "";
export { className as class };
export let tooltip: string | undefined;
export let active = false;
export let disables = true;
export let dropdownToggle = false;
$: extraProps = dropdownToggle
? {
"data-bs-toggle": "dropdown",
"aria-expanded": "false",
}
: {};
let buttonRef: HTMLButtonElement;
const disabled = getContext<Readable<boolean>>(disabledKey);
$: _disabled = disables && $disabled;
const nightMode = getContext<boolean>(nightModeKey);
const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef }));
</script>
<SquareButton {id} {className} {tooltip} {disables} {dropdownToggle} on:click on:mount>
<slot />
</SquareButton>
<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={`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(
name = "svelte",
srcs = svelte_files,
deps = [],
deps = [
"//ts/components",
],
visibility = ["//visibility:public"],
)
@ -66,13 +68,11 @@ ts_library(
srcs = glob(["*.ts"]),
tsconfig = "//ts:tsconfig.json",
deps = [
"//ts:image_module_support",
"//ts/lib",
"//ts/sveltelib",
"//ts/components",
"//ts/html-filter",
# "svelte_components",
# "//ts/components:svelte_components",
"//ts:image_module_support",
"@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 * as tr from "lib/i18n";
import CommandIconButton from "components/CommandIconButton.svelte";
import IconButton from "components/IconButton.svelte";
import ButtonGroup from "components/ButtonGroup.svelte";
import ButtonDropdown from "components/ButtonDropdown.svelte";
import WithState from "components/WithState.svelte";
import WithDropdownMenu from "components/WithDropdownMenu.svelte";
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">
<ButtonGroup id="justify" {api}>
<CommandIconButton command="justifyLeft" tooltip={tr.editingAlignLeft()}>
<IconButton command="justifyLeft" tooltip={tr.editingAlignLeft()}>
{@html justifyLeftIcon}
</CommandIconButton>
</IconButton>
<CommandIconButton command="justifyCenter" tooltip={tr.editingCenter()}>
<IconButton command="justifyCenter" tooltip={tr.editingCenter()}>
{@html justifyCenterIcon}
</CommandIconButton>
</IconButton>
<CommandIconButton command="justifyCenter" tooltip={tr.editingCenter()}>
<IconButton command="justifyCenter" tooltip={tr.editingCenter()}>
{@html justifyCenterIcon}
</CommandIconButton>
</IconButton>
<CommandIconButton command="justifyRight" tooltip={tr.editingAlignRight()}>
<IconButton command="justifyRight" tooltip={tr.editingAlignRight()}>
{@html justifyRightIcon}
</CommandIconButton>
</IconButton>
<CommandIconButton command="justifyFull" tooltip={tr.editingJustify()}>
<IconButton command="justifyFull" tooltip={tr.editingJustify()}>
{@html justifyFullIcon}
</CommandIconButton>
</IconButton>
</ButtonGroup>
<ButtonGroup id="indentation" {api}>
@ -77,15 +77,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
</ButtonDropdown>
<ButtonGroup id="blockFormatting" {api}>
<CommandIconButton
command="insertUnorderedList"
tooltip={tr.editingUnorderedList()}>
<IconButton command="insertUnorderedList" tooltip={tr.editingUnorderedList()}>
{@html ulIcon}
</CommandIconButton>
</IconButton>
<CommandIconButton command="insertOrderedList" tooltip={tr.editingOrderedList()}>
<IconButton command="insertOrderedList" tooltip={tr.editingOrderedList()}>
{@html olIcon}
</CommandIconButton>
</IconButton>
<WithDropdownMenu menuId="listFormatting">
<IconButton>

View file

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

View file

@ -48,7 +48,7 @@ export function focusField(n: number): void {
if (field) {
field.editingArea.focusEditable();
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);
if (!nosave) {
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 {
// make sure IME changes get saved
triggerChangeTimer(event.currentTarget as EditingArea);
updateActiveButtons();
updateActiveButtons(event);
}
export function onKey(evt: KeyboardEvent): void {
@ -56,7 +56,7 @@ function updateFocus(evt: FocusEvent) {
const newFocusTarget = evt.target;
if (newFocusTarget instanceof EditingArea) {
caretToEnd(newFocusTarget);
updateActiveButtons();
updateActiveButtons(evt);
}
}

View file

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