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

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

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">
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>

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
-->
<script lang="typescript">
export let id: string | undefined;
export let id: string | undefined = undefined;
let className: string | undefined;
export { className as class };
</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() {
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 />

View file

@ -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 {

View file

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

View file

@ -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) {