mirror of
https://github.com/ankitects/anki.git
synced 2025-12-10 13:26:56 -05:00
Refactor {CommandIcon,Icon,Square}Button into IconButton and WithState
This commit is contained in:
parent
2f5074bff6
commit
0baf14dc8b
11 changed files with 221 additions and 226 deletions
|
|
@ -41,7 +41,7 @@ ts_library(
|
|||
"@npm//@types/bootstrap",
|
||||
"@npm//bootstrap",
|
||||
"@npm//svelte",
|
||||
],
|
||||
] + svelte_names,
|
||||
)
|
||||
|
||||
# Tests
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
68
ts/components/WithState.svelte
Normal file
68
ts/components/WithState.svelte
Normal 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} />
|
||||
|
|
@ -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",
|
||||
],
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue