Add API for adding new buttons, updating existing ones in ButtonGroup

This commit is contained in:
Henrik Giesel 2021-05-05 01:22:51 +02:00
parent 413ac6cf63
commit 26f85a0f9d
12 changed files with 187 additions and 193 deletions

View file

@ -3,32 +3,83 @@ 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 { setContext, getContext } from "svelte"; import type { SvelteComponentTyped } from "svelte";
import { nightModeKey, buttonGroupKey } from "./contextKeys"; 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; export let id: string | undefined = undefined;
let className = ""; let className: string = "";
export { className as class }; export { className as class };
const nightMode = getContext(nightModeKey);
export let api = {}; export let api = {};
export let buttonGroupRef: HTMLDivElement;
let index = 0; $: root = buttonGroupRef?.getRootNode() as Document;
interface ButtonRegistration { interface ButtonRegistration {
order: number; detach: Writable<boolean>;
} }
const items: ButtonRegistration[] = [];
function registerButton(): ButtonRegistration { function registerButton(): ButtonRegistration {
index++; const detach = writable(false);
return { order: index }; 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( setContext(
buttonGroupKey, buttonGroupKey,
Object.assign(api, { Object.assign(api, {
registerButton, registerButton,
insertButton,
appendButton,
showButton,
hideButton,
toggleButton,
}) })
); );
</script> </script>
@ -37,48 +88,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
div { div {
display: flex; display: flex;
justify-items: start; justify-items: start;
flex-wrap: var(--toolbar-wrap); flex-wrap: var(--toolbar-wrap);
padding: calc(var(--toolbar-size) / 10); padding: calc(var(--toolbar-size) / 10);
margin: 0; 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> </style>
<div <div bind:this={buttonGroupRef} {id} class={className} dir="ltr">
{id}
class={className}
class:border-overlap-group={!nightMode}
class:gap-group={nightMode}
div="ltr">
<slot /> <slot />
{#each dynamic as component}
<svelte:component this={component} />
{/each}
</div> </div>

View file

@ -3,11 +3,15 @@ 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 Detachable from "components/Detachable.svelte";
import { getContext } from "svelte"; import { getContext } from "svelte";
import { buttonGroupKey } from "./contextKeys"; import { buttonGroupKey } from "./contextKeys";
const { registerButton } = getContext(buttonGroupKey); const { registerButton } = getContext(buttonGroupKey);
const { order } = registerButton(); const { detach } = registerButton();
</script> </script>
<slot {order} /> <Detachable detach={$detach}>
<slot />
</Detachable>

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
-->
<script lang="typescript">
export let detach = false;
</script>
{#if !detach}
<slot />
{/if}

View file

@ -4,35 +4,32 @@ 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 ButtonGroupItem from "./ButtonGroupItem.svelte";
import { onMount, createEventDispatcher, getContext } from "svelte"; import { onMount, createEventDispatcher, getContext } from "svelte";
import { disabledKey, nightModeKey } from "./contextKeys"; import { disabledKey, nightModeKey } from "./contextKeys";
export let id: string | undefined; export let id: string | undefined = undefined;
export let className = ""; let className: string = "";
export { className as class };
export let tooltip: string | undefined; export let tooltip: string | undefined;
export let disables = true;
export let dropdownToggle = false; export let dropdownToggle = false;
export let disables = true;
export let tabbable = false;
$: extraProps = dropdownToggle $: dropdownProps = dropdownToggle
? { ? {
"data-bs-toggle": "dropdown", "data-bs-toggle": "dropdown",
"aria-expanded": "false", "aria-expanded": "false",
} }
: {}; : {};
let buttonRef: HTMLButtonElement;
function extendClassName(className: string): string {
return `btn ${className}`;
}
const disabled = getContext<Readable<boolean>>(disabledKey); const disabled = getContext<Readable<boolean>>(disabledKey);
$: _disabled = disables && $disabled; $: _disabled = disables && $disabled;
const nightMode = getContext<boolean>(nightModeKey); const nightMode = getContext<boolean>(nightModeKey);
let buttonRef: HTMLButtonElement;
const dispatch = createEventDispatcher(); const dispatch = createEventDispatcher();
onMount(() => dispatch("mount", { button: buttonRef })); onMount(() => dispatch("mount", { button: buttonRef }));
</script> </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); font-size: calc(var(--toolbar-size) / 2.3);
width: auto; width: auto;
height: var(--toolbar-size); 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-day;
@include button.btn-night; @include button.btn-night;
</style> </style>
<ButtonGroupItem let:order> <button
<button
bind:this={buttonRef} bind:this={buttonRef}
{id} {id}
class={extendClassName(className)} class={`btn ${className}`}
class:dropdown-toggle={dropdownToggle} class:dropdown-toggle={dropdownToggle}
class:btn-day={!nightMode} class:btn-day={!nightMode}
class:btn-night={nightMode} class:btn-night={nightMode}
style={`order: ${order};`}
tabindex="-1"
disabled={_disabled}
title={tooltip} title={tooltip}
{...extraProps} {...dropdownProps}
disabled={_disabled}
tabindex={tabbable ? 0 : -1}
on:click on:click
on:mousedown|preventDefault> on:mousedown|preventDefault>
<slot /> <slot />
</button> </button>
</ButtonGroupItem>

View file

@ -3,7 +3,7 @@ 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">
export let id: string | undefined; export let id: string | undefined = undefined;
let className: string | undefined; let className: string | undefined;
export { className as class }; export { className as class };
</script> </script>

View file

@ -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;
}

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
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;
}

View 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);
}
}

View file

@ -25,6 +25,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
export function clearActiveButtons() { export function clearActiveButtons() {
resetAllState(false); resetAllState(false);
} }
/* Export components */
import LabelButton from "components/LabelButton.svelte";
import IconButton from "components/IconButton.svelte";
export const editorToolbar = { LabelButton, IconButton };
</script> </script>
<script lang="typescript"> <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 ColorButtons from "./ColorButtons.svelte";
import TemplateButtons from "./TemplateButtons.svelte"; import TemplateButtons from "./TemplateButtons.svelte";
export const notetypeButtons = {};
export let nightMode: boolean; export let nightMode: boolean;
setContext(nightModeKey, nightMode); setContext(nightModeKey, nightMode);
@ -55,7 +63,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
<WithTheming {style}> <WithTheming {style}>
<StickyBar> <StickyBar>
<NoteTypeButtons /> <NoteTypeButtons api={notetypeButtons} />
<FormatInlineButtons /> <FormatInlineButtons />
<FormatBlockButtons /> <FormatBlockButtons />
<ColorButtons /> <ColorButtons />

View file

@ -19,8 +19,7 @@ import { initToolbar } from "./toolbar";
export { setNoteId, getNoteId } from "./noteId"; export { setNoteId, getNoteId } from "./noteId";
export { saveNow } from "./changeTimer"; export { saveNow } from "./changeTimer";
export { wrap, wrapIntoText } from "./wrap"; export { wrap, wrapIntoText } from "./wrap";
export { editorToolbar } from "./toolbar";
// export * from "./addons";
declare global { declare global {
interface Selection { interface Selection {

View file

@ -34,6 +34,8 @@ export function initToolbar(i18n: Promise<void>): Promise<EditorToolbar> {
/* Exports for editor */ /* Exports for editor */
export { export {
// @ts-expect-error insufficient typing of svelte modules
editorToolbar,
// @ts-expect-error insufficient typing of svelte modules // @ts-expect-error insufficient typing of svelte modules
enableButtons, enableButtons,
// @ts-expect-error insufficient typing of svelte modules // @ts-expect-error insufficient typing of svelte modules

View file

@ -7,6 +7,7 @@ $btn-base-color-day: white;
color: var(--text-fg); color: var(--text-fg);
background-color: $btn-base-color-day; background-color: $btn-base-color-day;
border-color: var(--medium-border) !important; border-color: var(--medium-border) !important;
margin-left: -1px;
} }
@mixin btn-day($with-disabled: true) { @mixin btn-day($with-disabled: true) {
@ -42,6 +43,7 @@ $btn-base-color-night: #666;
color: var(--text-fg); color: var(--text-fg);
background-color: $btn-base-color-night; background-color: $btn-base-color-night;
border-color: $btn-base-color-night; border-color: $btn-base-color-night;
margin-left: 1px;
} }
@mixin btn-night($with-disabled: true) { @mixin btn-night($with-disabled: true) {