mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 15:32:23 -04:00
Add API for adding new buttons, updating existing ones in ButtonGroup
This commit is contained in:
parent
413ac6cf63
commit
26f85a0f9d
12 changed files with 187 additions and 193 deletions
|
@ -3,32 +3,83 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import { setContext, getContext } from "svelte";
|
||||
import { nightModeKey, buttonGroupKey } from "./contextKeys";
|
||||
import type { SvelteComponentTyped } from "svelte";
|
||||
import { setContext } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
import { buttonGroupKey } from "./contextKeys";
|
||||
import type { Identifier } from "./identifier";
|
||||
import { insert, add, update } from "./identifier";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
const nightMode = getContext(nightModeKey);
|
||||
|
||||
export let api = {};
|
||||
|
||||
let index = 0;
|
||||
export let buttonGroupRef: HTMLDivElement;
|
||||
$: root = buttonGroupRef?.getRootNode() as Document;
|
||||
|
||||
interface ButtonRegistration {
|
||||
order: number;
|
||||
detach: Writable<boolean>;
|
||||
}
|
||||
|
||||
const items: ButtonRegistration[] = [];
|
||||
|
||||
function registerButton(): ButtonRegistration {
|
||||
index++;
|
||||
return { order: index };
|
||||
const detach = writable(false);
|
||||
const registration = { detach };
|
||||
items.push(registration);
|
||||
|
||||
return registration;
|
||||
}
|
||||
|
||||
let dynamic: SvelteComponentTyped[] = [];
|
||||
|
||||
function addButton(
|
||||
button: SvelteComponentTyped,
|
||||
add: (added: Element, parent: Element) => number
|
||||
): void {
|
||||
const callback = (
|
||||
mutations: MutationRecord[],
|
||||
observer: MutationObserver
|
||||
): void => {
|
||||
for (const mutation of mutations) {
|
||||
const addedNode = mutation.addedNodes[0];
|
||||
|
||||
if (addedNode.nodeType === Node.ELEMENT_NODE) {
|
||||
const index = add(addedNode as Element, buttonGroupRef);
|
||||
}
|
||||
}
|
||||
|
||||
observer.disconnect();
|
||||
};
|
||||
|
||||
const observer = new MutationObserver(callback);
|
||||
observer.observe(buttonGroupRef, { childList: true });
|
||||
|
||||
dynamic = [...dynamic, button];
|
||||
}
|
||||
|
||||
const insertButton = (button: SvelteComponentTyped, id: Identifier = 0) =>
|
||||
addButton(button, (added, parent) => insert(added, parent, id));
|
||||
const appendButton = (button: SvelteComponentTyped, id: Identifier = -1) =>
|
||||
addButton(button, (added, parent) => add(added, parent, id));
|
||||
const showButton = (id: Identifier) =>
|
||||
update((element) => element.removeAttribute("hidden"), buttonGroupRef, id);
|
||||
const hideButton = (id: Identifier) =>
|
||||
update((element) => element.setAttribute("hidden", ""), buttonGroupRef, id);
|
||||
const toggleButton = (id: Identifier) =>
|
||||
update((element) => element.toggleAttribute("hidden"), buttonGroupRef, id);
|
||||
|
||||
setContext(
|
||||
buttonGroupKey,
|
||||
Object.assign(api, {
|
||||
registerButton,
|
||||
insertButton,
|
||||
appendButton,
|
||||
showButton,
|
||||
hideButton,
|
||||
toggleButton,
|
||||
})
|
||||
);
|
||||
</script>
|
||||
|
@ -37,48 +88,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
div {
|
||||
display: flex;
|
||||
justify-items: start;
|
||||
|
||||
flex-wrap: var(--toolbar-wrap);
|
||||
|
||||
padding: calc(var(--toolbar-size) / 10);
|
||||
margin: 0;
|
||||
|
||||
> :global(button),
|
||||
> :global(select) {
|
||||
border-radius: calc(var(--toolbar-size) / 7.5);
|
||||
|
||||
&:not(:nth-of-type(1)) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:nth-last-of-type(1)) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.border-overlap-group {
|
||||
:global(button),
|
||||
:global(select) {
|
||||
margin-left: -1px;
|
||||
}
|
||||
}
|
||||
|
||||
&.gap-group {
|
||||
:global(button),
|
||||
:global(select) {
|
||||
margin-left: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<div
|
||||
{id}
|
||||
class={className}
|
||||
class:border-overlap-group={!nightMode}
|
||||
class:gap-group={nightMode}
|
||||
div="ltr">
|
||||
<div bind:this={buttonGroupRef} {id} class={className} dir="ltr">
|
||||
<slot />
|
||||
{#each dynamic as component}
|
||||
<svelte:component this={component} />
|
||||
{/each}
|
||||
</div>
|
||||
|
|
|
@ -3,11 +3,15 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
import Detachable from "components/Detachable.svelte";
|
||||
|
||||
import { getContext } from "svelte";
|
||||
import { buttonGroupKey } from "./contextKeys";
|
||||
|
||||
const { registerButton } = getContext(buttonGroupKey);
|
||||
const { order } = registerButton();
|
||||
const { detach } = registerButton();
|
||||
</script>
|
||||
|
||||
<slot {order} />
|
||||
<Detachable detach={$detach}>
|
||||
<slot />
|
||||
</Detachable>
|
||||
|
|
11
ts/components/Detachable.svelte
Normal file
11
ts/components/Detachable.svelte
Normal file
|
@ -0,0 +1,11 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
export let detach = false;
|
||||
</script>
|
||||
|
||||
{#if !detach}
|
||||
<slot />
|
||||
{/if}
|
|
@ -4,35 +4,32 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="typescript">
|
||||
import type { Readable } from "svelte/store";
|
||||
import ButtonGroupItem from "./ButtonGroupItem.svelte";
|
||||
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||
|
||||
export let id: string | undefined;
|
||||
export let className = "";
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
export let tooltip: string | undefined;
|
||||
export let disables = true;
|
||||
export let dropdownToggle = false;
|
||||
export let disables = true;
|
||||
export let tabbable = false;
|
||||
|
||||
$: extraProps = dropdownToggle
|
||||
$: dropdownProps = 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);
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
</script>
|
||||
|
@ -45,27 +42,36 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
font-size: calc(var(--toolbar-size) / 2.3);
|
||||
width: auto;
|
||||
height: var(--toolbar-size);
|
||||
|
||||
border-radius: calc(var(--toolbar-size) / 7.5);
|
||||
|
||||
&:not(:nth-of-type(1)) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
}
|
||||
|
||||
&:not(:nth-last-of-type(1)) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@include button.btn-day;
|
||||
@include button.btn-night;
|
||||
</style>
|
||||
|
||||
<ButtonGroupItem let:order>
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
class={extendClassName(className)}
|
||||
class:dropdown-toggle={dropdownToggle}
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
style={`order: ${order};`}
|
||||
tabindex="-1"
|
||||
disabled={_disabled}
|
||||
title={tooltip}
|
||||
{...extraProps}
|
||||
on:click
|
||||
on:mousedown|preventDefault>
|
||||
<slot />
|
||||
</button>
|
||||
</ButtonGroupItem>
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
class={`btn ${className}`}
|
||||
class:dropdown-toggle={dropdownToggle}
|
||||
class:btn-day={!nightMode}
|
||||
class:btn-night={nightMode}
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
disabled={_disabled}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
on:click
|
||||
on:mousedown|preventDefault>
|
||||
<slot />
|
||||
</button>
|
||||
|
|
|
@ -3,7 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="typescript">
|
||||
export let id: string | undefined;
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string | undefined;
|
||||
export { className as class };
|
||||
</script>
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
interface Hideable {
|
||||
hidden?: boolean;
|
||||
}
|
||||
|
||||
export function showComponent<T extends Hideable>(component: T): T {
|
||||
component.hidden = false;
|
||||
return component;
|
||||
}
|
||||
|
||||
export function hideComponent<T extends Hideable>(component: T): T {
|
||||
component.hidden = true;
|
||||
return component;
|
||||
}
|
||||
|
||||
export function toggleComponent<T extends Hideable>(component: T): T {
|
||||
component.hidden = !component.hidden;
|
||||
return component;
|
||||
}
|
|
@ -1,92 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface Identifiable {
|
||||
id?: string;
|
||||
}
|
||||
|
||||
interface IterableIdentifiable<T extends Identifiable> extends Identifiable {
|
||||
items: T[];
|
||||
}
|
||||
|
||||
export type Identifier = string | number;
|
||||
|
||||
function normalize<T extends Identifiable>(
|
||||
iterable: IterableIdentifiable<T>,
|
||||
idOrIndex: Identifier
|
||||
): number {
|
||||
let normalizedIndex: number;
|
||||
|
||||
if (typeof idOrIndex === "string") {
|
||||
normalizedIndex = iterable.items.findIndex((value) => value.id === idOrIndex);
|
||||
} else if (idOrIndex < 0) {
|
||||
normalizedIndex = iterable.items.length + idOrIndex;
|
||||
} else {
|
||||
normalizedIndex = idOrIndex;
|
||||
}
|
||||
|
||||
return normalizedIndex >= iterable.items.length ? -1 : normalizedIndex;
|
||||
}
|
||||
|
||||
function search<T extends Identifiable>(values: T[], index: number): T | null {
|
||||
return index >= 0 ? values[index] : null;
|
||||
}
|
||||
|
||||
export function insert<T extends Identifiable>(
|
||||
iterable: IterableIdentifiable<T> & T,
|
||||
value: T,
|
||||
idOrIndex: Identifier
|
||||
): IterableIdentifiable<T> & T {
|
||||
const index = normalize(iterable, idOrIndex);
|
||||
|
||||
if (index >= 0) {
|
||||
iterable.items = iterable.items.slice();
|
||||
iterable.items.splice(index, 0, value);
|
||||
}
|
||||
|
||||
return iterable;
|
||||
}
|
||||
|
||||
export function add<T extends Identifiable>(
|
||||
iterable: IterableIdentifiable<T> & T,
|
||||
value: T,
|
||||
idOrIndex: Identifier
|
||||
): IterableIdentifiable<T> & T {
|
||||
const index = normalize(iterable, idOrIndex);
|
||||
|
||||
if (index >= 0) {
|
||||
iterable.items = iterable.items.slice();
|
||||
iterable.items.splice(index + 1, 0, value);
|
||||
}
|
||||
|
||||
return iterable;
|
||||
}
|
||||
|
||||
function isRecursive<T>(component: Identifiable): component is IterableIdentifiable<T> {
|
||||
return Boolean(Object.prototype.hasOwnProperty.call(component, "items"));
|
||||
}
|
||||
|
||||
export function updateRecursive<T extends Identifiable>(
|
||||
update: (component: T) => T,
|
||||
component: T,
|
||||
...identifiers: Identifier[]
|
||||
): T {
|
||||
if (identifiers.length === 0) {
|
||||
return update(component);
|
||||
} else if (isRecursive<T>(component)) {
|
||||
const [identifier, ...restIdentifiers] = identifiers;
|
||||
const normalizedIndex = normalize(component, identifier);
|
||||
const foundComponent = search(component.items, normalizedIndex);
|
||||
|
||||
if (foundComponent) {
|
||||
component.items[normalizedIndex] = updateRecursive(
|
||||
update,
|
||||
foundComponent as T,
|
||||
...restIdentifiers
|
||||
);
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
||||
|
||||
return component;
|
||||
}
|
55
ts/components/identifier.ts
Normal file
55
ts/components/identifier.ts
Normal file
|
@ -0,0 +1,55 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export type Identifier = string | number;
|
||||
|
||||
function find(collection: HTMLCollection, idOrIndex: Identifier): Element | null {
|
||||
let element: Element | null = null;
|
||||
|
||||
if (typeof idOrIndex === "string") {
|
||||
element = collection.namedItem(idOrIndex);
|
||||
} else if (idOrIndex < 0) {
|
||||
const normalizedIndex = collection.length + idOrIndex;
|
||||
element = collection.item(normalizedIndex);
|
||||
} else {
|
||||
element = collection.item(idOrIndex);
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
export function insert(
|
||||
element: Element,
|
||||
collection: Element,
|
||||
idOrIndex: Identifier
|
||||
): void {
|
||||
const reference = find(collection.children, idOrIndex);
|
||||
|
||||
if (reference) {
|
||||
collection.insertBefore(element, reference);
|
||||
}
|
||||
}
|
||||
|
||||
export function add(
|
||||
element: Element,
|
||||
collection: Element,
|
||||
idOrIndex: Identifier
|
||||
): void {
|
||||
const before = find(collection.children, idOrIndex);
|
||||
|
||||
if (before) {
|
||||
const reference = before.nextElementSibling ?? null;
|
||||
collection.insertBefore(element, reference);
|
||||
}
|
||||
}
|
||||
|
||||
export function update(
|
||||
f: (element: Element) => void,
|
||||
collection: Element,
|
||||
idOrIndex: Identifier
|
||||
): void {
|
||||
const element = find(collection.children, idOrIndex);
|
||||
|
||||
if (element) {
|
||||
f(element);
|
||||
}
|
||||
}
|
|
@ -25,6 +25,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export function clearActiveButtons() {
|
||||
resetAllState(false);
|
||||
}
|
||||
|
||||
/* Export components */
|
||||
import LabelButton from "components/LabelButton.svelte";
|
||||
import IconButton from "components/IconButton.svelte";
|
||||
|
||||
export const editorToolbar = { LabelButton, IconButton };
|
||||
</script>
|
||||
|
||||
<script lang="typescript">
|
||||
|
@ -40,6 +46,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ColorButtons from "./ColorButtons.svelte";
|
||||
import TemplateButtons from "./TemplateButtons.svelte";
|
||||
|
||||
export const notetypeButtons = {};
|
||||
|
||||
export let nightMode: boolean;
|
||||
|
||||
setContext(nightModeKey, nightMode);
|
||||
|
@ -55,7 +63,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<WithTheming {style}>
|
||||
<StickyBar>
|
||||
<NoteTypeButtons />
|
||||
<NoteTypeButtons api={notetypeButtons} />
|
||||
<FormatInlineButtons />
|
||||
<FormatBlockButtons />
|
||||
<ColorButtons />
|
||||
|
|
|
@ -19,8 +19,7 @@ import { initToolbar } from "./toolbar";
|
|||
export { setNoteId, getNoteId } from "./noteId";
|
||||
export { saveNow } from "./changeTimer";
|
||||
export { wrap, wrapIntoText } from "./wrap";
|
||||
|
||||
// export * from "./addons";
|
||||
export { editorToolbar } from "./toolbar";
|
||||
|
||||
declare global {
|
||||
interface Selection {
|
||||
|
|
|
@ -34,6 +34,8 @@ export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
|
|||
|
||||
/* Exports for editor */
|
||||
export {
|
||||
// @ts-expect-error insufficient typing of svelte modules
|
||||
editorToolbar,
|
||||
// @ts-expect-error insufficient typing of svelte modules
|
||||
enableButtons,
|
||||
// @ts-expect-error insufficient typing of svelte modules
|
||||
|
|
|
@ -7,6 +7,7 @@ $btn-base-color-day: white;
|
|||
color: var(--text-fg);
|
||||
background-color: $btn-base-color-day;
|
||||
border-color: var(--medium-border) !important;
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
@mixin btn-day($with-disabled: true) {
|
||||
|
@ -42,6 +43,7 @@ $btn-base-color-night: #666;
|
|||
color: var(--text-fg);
|
||||
background-color: $btn-base-color-night;
|
||||
border-color: $btn-base-color-night;
|
||||
margin-left: 1px;
|
||||
}
|
||||
|
||||
@mixin btn-night($with-disabled: true) {
|
||||
|
|
Loading…
Reference in a new issue