mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 22:42:25 -04:00
Clean up ButtonGroup and factor out extension logic
This commit is contained in:
parent
e80f43e8fc
commit
4a6b3b3786
7 changed files with 190 additions and 114 deletions
|
@ -4,148 +4,95 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import ButtonGroupItem from "./ButtonGroupItem.svelte";
|
import ButtonGroupItem from "./ButtonGroupItem.svelte";
|
||||||
import type { SvelteComponentTyped } from "svelte";
|
|
||||||
import { setContext } from "svelte";
|
import { setContext } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
import { buttonGroupKey } from "./contextKeys";
|
import { buttonGroupKey } from "./contextKeys";
|
||||||
import type { Identifier } from "./identifier";
|
import type { Identifier } from "./identifier";
|
||||||
import { insert, add, find } from "./identifier";
|
import { insert, add } from "./identifier";
|
||||||
import type { ButtonRegistration } from "./buttons";
|
import type { ButtonRegistration } from "./buttons";
|
||||||
import { ButtonPosition } from "./buttons";
|
import { ButtonPosition } from "./buttons";
|
||||||
|
import type { SvelteComponent } from "./registration";
|
||||||
|
import { makeInterface } from "./registration";
|
||||||
|
|
||||||
export let id: string | undefined = undefined;
|
export let id: string | undefined = undefined;
|
||||||
let className: string = "";
|
let className: string = "";
|
||||||
export { className as class };
|
export { className as class };
|
||||||
|
|
||||||
export let api = {};
|
|
||||||
let buttonGroupRef: HTMLDivElement;
|
|
||||||
|
|
||||||
let items: ButtonRegistration[] = [];
|
|
||||||
|
|
||||||
$: {
|
|
||||||
for (const [index, item] of items.entries()) {
|
|
||||||
if (items.length === 1) {
|
|
||||||
item.position.set(ButtonPosition.Standalone);
|
|
||||||
} else if (index === 0) {
|
|
||||||
item.position.set(ButtonPosition.Leftmost);
|
|
||||||
} else if (index === items.length - 1) {
|
|
||||||
item.position.set(ButtonPosition.Rightmost);
|
|
||||||
} else {
|
|
||||||
item.position.set(ButtonPosition.Center);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function makeRegistration(): ButtonRegistration {
|
function makeRegistration(): ButtonRegistration {
|
||||||
const detach = writable(false);
|
const detach = writable(false);
|
||||||
const position = writable(ButtonPosition.Standalone);
|
const position = writable(ButtonPosition.Standalone);
|
||||||
return { detach, position };
|
return { detach, position };
|
||||||
}
|
}
|
||||||
|
|
||||||
function registerButton(
|
const {
|
||||||
index = items.length,
|
registerComponent,
|
||||||
registration = makeRegistration()
|
items,
|
||||||
): ButtonRegistration {
|
dynamicItems,
|
||||||
items.splice(index, 0, registration);
|
getDynamicInterface,
|
||||||
items = items;
|
} = makeInterface(makeRegistration);
|
||||||
return registration;
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SvelteComponent {
|
$: for (const [index, item] of $items.entries()) {
|
||||||
id: string | undefined;
|
if ($items.length === 1) {
|
||||||
component: SvelteComponentTyped;
|
item.position.set(ButtonPosition.Standalone);
|
||||||
props: Record<string, unknown>;
|
} else if (index === 0) {
|
||||||
}
|
item.position.set(ButtonPosition.Leftmost);
|
||||||
|
} else if (index === $items.length - 1) {
|
||||||
const dynamicItems: ButtonRegistration[] = [];
|
item.position.set(ButtonPosition.Rightmost);
|
||||||
let dynamic: SvelteComponent[] = [];
|
} else {
|
||||||
|
item.position.set(ButtonPosition.Center);
|
||||||
function addButton(
|
|
||||||
button: SvelteComponent,
|
|
||||||
add: (added: Element, parent: Element) => number
|
|
||||||
): void {
|
|
||||||
const registration = makeRegistration();
|
|
||||||
|
|
||||||
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);
|
|
||||||
|
|
||||||
if (index >= 0) {
|
|
||||||
registerButton(index, registration);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
observer.disconnect();
|
|
||||||
};
|
|
||||||
|
|
||||||
const observer = new MutationObserver(callback);
|
|
||||||
observer.observe(buttonGroupRef, { childList: true });
|
|
||||||
|
|
||||||
dynamicItems.push(registration);
|
|
||||||
dynamic = [...dynamic, button];
|
|
||||||
}
|
|
||||||
|
|
||||||
const insertButton = (button: SvelteComponent, position: Identifier = 0) =>
|
|
||||||
addButton(button, (added, parent) => insert(added, parent, position));
|
|
||||||
const appendButton = (button: SvelteComponent, position: Identifier = -1) =>
|
|
||||||
addButton(button, (added, parent) => add(added, parent, position));
|
|
||||||
|
|
||||||
function updateRegistration(
|
|
||||||
f: (registration: ButtonRegistration) => void,
|
|
||||||
id: Identifier
|
|
||||||
): void {
|
|
||||||
const match = find(buttonGroupRef.children, id);
|
|
||||||
|
|
||||||
if (match) {
|
|
||||||
const [index] = match;
|
|
||||||
const registration = items[index];
|
|
||||||
f(registration);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const showButton = (id: Identifier) =>
|
setContext(buttonGroupKey, registerComponent);
|
||||||
updateRegistration(({ detach }) => detach.set(false), id);
|
|
||||||
const hideButton = (id: Identifier) =>
|
export let api = {};
|
||||||
updateRegistration(({ detach }) => detach.set(true), id);
|
let buttonGroupRef: HTMLDivElement;
|
||||||
const toggleButton = (id: Identifier) =>
|
|
||||||
updateRegistration(({ detach }) => detach.update((old) => !old), id);
|
$: if (buttonGroupRef) {
|
||||||
|
const { addComponent, updateRegistration } = getDynamicInterface(
|
||||||
|
buttonGroupRef
|
||||||
|
);
|
||||||
|
|
||||||
|
const insertButton = (button: SvelteComponent, position: Identifier = 0) =>
|
||||||
|
addComponent(button, (added, parent) => insert(added, parent, position));
|
||||||
|
const appendButton = (button: SvelteComponent, position: Identifier = -1) =>
|
||||||
|
addComponent(button, (added, parent) => add(added, parent, position));
|
||||||
|
|
||||||
|
const showButton = (id: Identifier) =>
|
||||||
|
updateRegistration(({ detach }) => detach.set(false), id);
|
||||||
|
const hideButton = (id: Identifier) =>
|
||||||
|
updateRegistration(({ detach }) => detach.set(true), id);
|
||||||
|
const toggleButton = (id: Identifier) =>
|
||||||
|
updateRegistration(({ detach }) => detach.update((old) => !old), id);
|
||||||
|
|
||||||
setContext(
|
|
||||||
buttonGroupKey,
|
|
||||||
Object.assign(api, {
|
Object.assign(api, {
|
||||||
registerButton,
|
|
||||||
insertButton,
|
insertButton,
|
||||||
appendButton,
|
appendButton,
|
||||||
showButton,
|
showButton,
|
||||||
hideButton,
|
hideButton,
|
||||||
toggleButton,
|
toggleButton,
|
||||||
})
|
});
|
||||||
);
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
div {
|
div {
|
||||||
display: flex;
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<div bind:this={buttonGroupRef} {id} class={className} dir="ltr">
|
<div
|
||||||
|
bind:this={buttonGroupRef}
|
||||||
|
{id}
|
||||||
|
class={`btn-group ${className}`}
|
||||||
|
dir="ltr"
|
||||||
|
role="group">
|
||||||
<slot />
|
<slot />
|
||||||
{#each dynamic as item, i}
|
{#each $dynamicItems as item}
|
||||||
<ButtonGroupItem id={item.id} registration={dynamicItems[i]}>
|
<ButtonGroupItem id={item[0].id} registration={item[1]}>
|
||||||
<svelte:component this={item.component} {...item.props} />
|
<svelte:component this={item[0].component} {...item[0].props} />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import type { ButtonRegistration } from "./buttons";
|
import type { ButtonRegistration } from "./buttons";
|
||||||
import { ButtonPosition } from "./buttons";
|
import { ButtonPosition } from "./buttons";
|
||||||
|
import type { Register } from "./registration";
|
||||||
|
|
||||||
import { getContext, hasContext } from "svelte";
|
import { getContext, hasContext } from "svelte";
|
||||||
import { buttonGroupKey } from "./contextKeys";
|
import { buttonGroupKey } from "./contextKeys";
|
||||||
|
@ -43,8 +44,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
detach.subscribe((value: boolean) => (detach_ = value));
|
detach.subscribe((value: boolean) => (detach_ = value));
|
||||||
position.subscribe((value: ButtonPosition) => (position_ = value));
|
position.subscribe((value: ButtonPosition) => (position_ = value));
|
||||||
} else if (hasContext(buttonGroupKey)) {
|
} else if (hasContext(buttonGroupKey)) {
|
||||||
const { registerButton } = getContext(buttonGroupKey);
|
const registerComponent = getContext<Register<ButtonRegistration>>(
|
||||||
const { detach, position } = registerButton();
|
buttonGroupKey
|
||||||
|
);
|
||||||
|
const { detach, position } = registerComponent();
|
||||||
detach.subscribe((value: boolean) => (detach_ = value));
|
detach.subscribe((value: boolean) => (detach_ = value));
|
||||||
position.subscribe((value: ButtonPosition) => (position_ = value));
|
position.subscribe((value: ButtonPosition) => (position_ = value));
|
||||||
} else {
|
} else {
|
||||||
|
|
15
ts/components/ButtonToolbar.svelte
Normal file
15
ts/components/ButtonToolbar.svelte
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
<!--
|
||||||
|
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 = undefined;
|
||||||
|
let className: string | undefined;
|
||||||
|
export { className as class };
|
||||||
|
|
||||||
|
export let nowrap = false;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div {id} class={`btn-toolbar ${className}`} class:flex-nowrap={nowrap} role="toolbar">
|
||||||
|
<slot />
|
||||||
|
</div>
|
|
@ -10,9 +10,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
nav {
|
nav {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
@ -24,6 +21,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<nav {id} class="pb-1 {className}">
|
<nav {id} class={`pb-1 ${className}`}>
|
||||||
<slot />
|
<slot />
|
||||||
</nav>
|
</nav>
|
||||||
|
|
110
ts/components/registration.ts
Normal file
110
ts/components/registration.ts
Normal file
|
@ -0,0 +1,110 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
import type { SvelteComponentTyped } from "svelte/internal";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
import type { Identifier } from "./identifier";
|
||||||
|
import { find } from "./identifier";
|
||||||
|
|
||||||
|
export interface SvelteComponent {
|
||||||
|
component: SvelteComponentTyped;
|
||||||
|
id: string | undefined;
|
||||||
|
props: Record<string, unknown> | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Register<T> = (index?: number, registration?: T) => T;
|
||||||
|
|
||||||
|
export interface RegistrationAPI<T> {
|
||||||
|
registerComponent: Register<T>;
|
||||||
|
items: Readable<T[]>;
|
||||||
|
dynamicItems: Readable<[SvelteComponent, T][]>;
|
||||||
|
getDynamicInterface: (elementRef: HTMLElement) => DynamicRegistrationAPI<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DynamicRegistrationAPI<T> {
|
||||||
|
addComponent: (
|
||||||
|
component: SvelteComponent,
|
||||||
|
add: (added: Element, parent: Element) => number
|
||||||
|
) => void;
|
||||||
|
updateRegistration: (
|
||||||
|
update: (registration: T) => void,
|
||||||
|
position: Identifier
|
||||||
|
) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function makeInterface<T>(makeRegistration: () => T): RegistrationAPI<T> {
|
||||||
|
const registrations: T[] = [];
|
||||||
|
const items = writable(registrations);
|
||||||
|
|
||||||
|
function registerComponent(
|
||||||
|
index: number = registrations.length,
|
||||||
|
registration = makeRegistration()
|
||||||
|
): T {
|
||||||
|
registrations.splice(index, 0, registration);
|
||||||
|
items.set(registrations);
|
||||||
|
return registration;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dynamicRegistrations: [SvelteComponent, T][] = [];
|
||||||
|
const dynamicItems = writable(dynamicRegistrations);
|
||||||
|
|
||||||
|
function getDynamicInterface(elementRef: HTMLElement): DynamicRegistrationAPI<T> {
|
||||||
|
function addComponent(
|
||||||
|
component: SvelteComponent,
|
||||||
|
add: (added: Element, parent: Element) => number
|
||||||
|
): void {
|
||||||
|
const registration = makeRegistration();
|
||||||
|
|
||||||
|
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, elementRef);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
registerComponent(index, registration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
observer.disconnect();
|
||||||
|
};
|
||||||
|
|
||||||
|
const observer = new MutationObserver(callback);
|
||||||
|
observer.observe(elementRef, { childList: true });
|
||||||
|
|
||||||
|
dynamicRegistrations.push([component, registration]);
|
||||||
|
dynamicItems.set(dynamicRegistrations);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateRegistration(
|
||||||
|
update: (registration: T) => void,
|
||||||
|
position: Identifier
|
||||||
|
): void {
|
||||||
|
const match = find(elementRef.children, position);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
const [index] = match;
|
||||||
|
const registration = registrations[index];
|
||||||
|
update(registration);
|
||||||
|
items.set(registrations);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
addComponent,
|
||||||
|
updateRegistration,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
registerComponent,
|
||||||
|
items,
|
||||||
|
dynamicItems,
|
||||||
|
getDynamicInterface,
|
||||||
|
};
|
||||||
|
}
|
|
@ -47,6 +47,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import WithTheming from "components/WithTheming.svelte";
|
import WithTheming from "components/WithTheming.svelte";
|
||||||
import StickyBar from "components/StickyBar.svelte";
|
import StickyBar from "components/StickyBar.svelte";
|
||||||
|
import ButtonToolbar from "components/ButtonToolbar.svelte";
|
||||||
|
|
||||||
import NoteTypeButtons from "./NoteTypeButtons.svelte";
|
import NoteTypeButtons from "./NoteTypeButtons.svelte";
|
||||||
import FormatInlineButtons from "./FormatInlineButtons.svelte";
|
import FormatInlineButtons from "./FormatInlineButtons.svelte";
|
||||||
|
@ -75,10 +76,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<WithTheming {style}>
|
<WithTheming {style}>
|
||||||
<StickyBar>
|
<StickyBar>
|
||||||
<NoteTypeButtons api={notetypeButtons} />
|
<ButtonToolbar>
|
||||||
<FormatInlineButtons api={formatInlineButtons} />
|
<NoteTypeButtons api={notetypeButtons} />
|
||||||
<FormatBlockButtons api={formatBlockButtons} />
|
<FormatInlineButtons api={formatInlineButtons} />
|
||||||
<ColorButtons api={colorButtons} />
|
<FormatBlockButtons api={formatBlockButtons} />
|
||||||
<TemplateButtons api={templateButtons} />
|
<ColorButtons api={colorButtons} />
|
||||||
|
<TemplateButtons api={templateButtons} />
|
||||||
|
</ButtonToolbar>
|
||||||
</StickyBar>
|
</StickyBar>
|
||||||
</WithTheming>
|
</WithTheming>
|
||||||
|
|
1
ts/editor/bootstrap.scss
vendored
1
ts/editor/bootstrap.scss
vendored
|
@ -5,4 +5,5 @@
|
||||||
$btn-disabled-opacity: 0.4;
|
$btn-disabled-opacity: 0.4;
|
||||||
|
|
||||||
@import "ts/sass/bootstrap/buttons";
|
@import "ts/sass/bootstrap/buttons";
|
||||||
|
@import "ts/sass/bootstrap/button-group";
|
||||||
@import "ts/sass/bootstrap/dropdown";
|
@import "ts/sass/bootstrap/dropdown";
|
||||||
|
|
Loading…
Reference in a new issue