mirror of
https://github.com/ankitects/anki.git
synced 2025-09-23 16:26:40 -04:00
Make it so you can include DropdownMenus on all kinds of buttons
This commit is contained in:
parent
5eb07d3fc7
commit
391f64f648
13 changed files with 178 additions and 88 deletions
|
@ -1,10 +1,6 @@
|
||||||
<script lang="typescript" context="module">
|
|
||||||
export type Buttons =
|
|
||||||
| { component: SvelteComponent; [...arg: string]: unknown }
|
|
||||||
| Buttons[];
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import type { Buttons } from "./types";
|
||||||
|
|
||||||
export let buttons: Buttons;
|
export let buttons: Buttons;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -1,15 +1,14 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
export let className: string;
|
export let id = "";
|
||||||
|
export let className = "";
|
||||||
|
export let props: Record<string, string> = {};
|
||||||
|
|
||||||
export let onChange: (event: ChangeEvent) => void;
|
export let onChange: (event: ChangeEvent) => void;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
button {
|
button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
|
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
|
@ -24,19 +23,23 @@
|
||||||
|
|
||||||
span {
|
span {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
input {
|
input {
|
||||||
|
display: inline-block;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
width: 100%;
|
width: 28px;
|
||||||
height: 100%;
|
height: calc(28px - 4px);
|
||||||
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button tabindex="-1" on:mousedown|preventDefault>
|
<button tabindex="-1" {id} class={className} {...props} on:mousedown|preventDefault>
|
||||||
<span class={className}> <input type="color" on:change={onChange} /> </span>
|
<span> <input type="color" on:change={onChange} /> </span>
|
||||||
</button>
|
</button>
|
||||||
|
|
|
@ -34,9 +34,12 @@
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import InnerButton from "./InnerButton.svelte";
|
import SquareButton from "./SquareButton.svelte";
|
||||||
|
|
||||||
|
export let id = "";
|
||||||
export let className = "";
|
export let className = "";
|
||||||
|
export let props: Record<string, string> = {};
|
||||||
|
|
||||||
export let icon = "";
|
export let icon = "";
|
||||||
export let command: string;
|
export let command: string;
|
||||||
export let activatable = true;
|
export let activatable = true;
|
||||||
|
@ -57,6 +60,6 @@
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InnerButton {className} {active} {onClick}>
|
<SquareButton {id} {className} {props} {active} {onClick}>
|
||||||
{@html icon}
|
{@html icon}
|
||||||
</InnerButton>
|
</SquareButton>
|
||||||
|
|
|
@ -1,25 +0,0 @@
|
||||||
<script lang="typescript">
|
|
||||||
import { onMount } from "svelte";
|
|
||||||
|
|
||||||
export let buttonId = `dropdownMenuButton123`;
|
|
||||||
|
|
||||||
// ${Math.random().toString(36).substring(2)}`
|
|
||||||
let dropdownButtonRef: HTMLButtonElement;
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
/* Prevent focus on menu activation */
|
|
||||||
const noop = () => {};
|
|
||||||
Object.defineProperty(dropdownButtonRef, 'focus', { value: noop });
|
|
||||||
|
|
||||||
/* Set custom menu without using .dropdown
|
|
||||||
* Rendering the menu here would cause the menu to
|
|
||||||
* be displayed outside of the visible area
|
|
||||||
*/
|
|
||||||
const dropdown = new bootstrap.Dropdown(dropdownButtonRef);
|
|
||||||
dropdown._menu = document.getElementById(buttonId);
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<button bind:this={dropdownButtonRef} class="dropdown-toggle" data-bs-toggle="dropdown" aria-expanded="false" on:mousedown|preventDefault>
|
|
||||||
Dropdown button
|
|
||||||
</button>
|
|
40
ts/editor-toolbar/DropdownMenu.svelte
Normal file
40
ts/editor-toolbar/DropdownMenu.svelte
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<script lang="typescript">
|
||||||
|
import { onMount } from "svelte";
|
||||||
|
|
||||||
|
import type { ButtonDefinition } from "./types";
|
||||||
|
|
||||||
|
export let button: ButtonDefinition;
|
||||||
|
export let menuId: string;
|
||||||
|
|
||||||
|
function extend({ className, props, ...rest }: ButtonDefinition): ButtonDefinition {
|
||||||
|
return {
|
||||||
|
className: `${className} dropdown-toggle`,
|
||||||
|
props: {
|
||||||
|
"data-bs-toggle": "dropdown",
|
||||||
|
"aria-expanded": "false",
|
||||||
|
...props,
|
||||||
|
},
|
||||||
|
...rest,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDropdown({ detail }: CustomEvent): void {
|
||||||
|
const button: HTMLButtonElement = detail.button;
|
||||||
|
|
||||||
|
/* Prevent focus on menu activation */
|
||||||
|
const noop = () => {};
|
||||||
|
Object.defineProperty(button, "focus", { value: noop });
|
||||||
|
|
||||||
|
/* Set custom menu without using .dropdown
|
||||||
|
* Rendering the menu here would cause the menu to
|
||||||
|
* be displayed outside of the visible area
|
||||||
|
*/
|
||||||
|
const dropdown = new bootstrap.Dropdown(button);
|
||||||
|
dropdown._menu = button.getRootNode().getElementById(menuId);
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<svelte:component
|
||||||
|
this={button.component}
|
||||||
|
{...extend(button)}
|
||||||
|
on:mount={createDropdown} />
|
|
@ -5,7 +5,7 @@
|
||||||
import { disabledKey, nightModeKey } from "./contextKeys";
|
import { disabledKey, nightModeKey } from "./contextKeys";
|
||||||
|
|
||||||
import ButtonGroup from "./ButtonGroup.svelte";
|
import ButtonGroup from "./ButtonGroup.svelte";
|
||||||
import type { Buttons } from "./ButtonGroup.svelte";
|
import type { Buttons } from "./types";
|
||||||
|
|
||||||
export let buttons: Buttons = [];
|
export let buttons: Buttons = [];
|
||||||
export let nightMode: boolean;
|
export let nightMode: boolean;
|
||||||
|
@ -25,7 +25,7 @@
|
||||||
background: var(--bg-color);
|
background: var(--bg-color);
|
||||||
border-bottom: 1px solid var(--border);
|
border-bottom: 1px solid var(--border);
|
||||||
|
|
||||||
/* Remove most outer marigns */
|
/* Remove outermost marigns */
|
||||||
& > :global(ul) {
|
& > :global(ul) {
|
||||||
& > :global(li:nth-child(1)) {
|
& > :global(li:nth-child(1)) {
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
import InnerButton from "./InnerButton.svelte";
|
import SquareButton from "./SquareButton.svelte";
|
||||||
|
|
||||||
|
export let id = "";
|
||||||
export let className = "";
|
export let className = "";
|
||||||
|
export let props: Record<string, string> = {};
|
||||||
|
|
||||||
export let icon = "";
|
export let icon = "";
|
||||||
export let onClick: (event: ClickEvent) => void;
|
export let onClick: (event: ClickEvent) => void;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<InnerButton {className} {onClick}>
|
<SquareButton {id} {className} {props} {onClick}>
|
||||||
{@html icon}
|
{@html icon}
|
||||||
</InnerButton>
|
</SquareButton>
|
||||||
|
|
|
@ -1,5 +1,27 @@
|
||||||
<script lang="typescript">
|
<script lang="typescript">
|
||||||
|
import { onMount, createEventDispatcher, getContext } from "svelte";
|
||||||
|
import type { Readable } from "svelte/store";
|
||||||
|
import { disabledKey } from "./contextKeys";
|
||||||
|
|
||||||
|
export let id = "";
|
||||||
|
export let className = "";
|
||||||
|
export let props: Record<string, string> = {};
|
||||||
|
|
||||||
export let label: string;
|
export let label: string;
|
||||||
|
export let onClick: (event: ClickEvent) => void;
|
||||||
|
|
||||||
|
let buttonRef: HTMLButtonElement;
|
||||||
|
|
||||||
|
function extendClassName(className: string): string {
|
||||||
|
return `${className} btn btn-secondary`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const dispatch = createEventDispatcher();
|
||||||
|
|
||||||
|
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||||
|
|
||||||
|
const disabledStore = getContext(disabledKey);
|
||||||
|
$: disabled = $disabledStore;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@ -7,15 +29,34 @@
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
width: auto;
|
width: auto;
|
||||||
height: 26px;
|
height: calc(28px + 2px);
|
||||||
|
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
margin: 0 3px;
|
|
||||||
|
border-radius: 0;
|
||||||
|
border-color: var(--faint-border);
|
||||||
|
|
||||||
&:focus {
|
&:focus {
|
||||||
box-shadow: 0 0 12px 4px rgb(255 255 255 / 0.5);
|
box-shadow: 0 0 12px 4px rgb(255 255 255 / 0.5);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&[disabled] {
|
||||||
|
opacity: 0.4;
|
||||||
|
cursor: not-allowed;
|
||||||
|
|
||||||
|
border-color: var(--faint-border);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button class="btn btn-secondary" tabindex="-1" on:click> {label} </button>
|
<button
|
||||||
|
bind:this={buttonRef}
|
||||||
|
{disabled}
|
||||||
|
{id}
|
||||||
|
class={extendClassName(className)}
|
||||||
|
{...props}
|
||||||
|
tabindex="-1"
|
||||||
|
on:click={onClick}
|
||||||
|
on:mousedown|preventDefault>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
|
|
@ -3,31 +3,24 @@
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
import { disabledKey } from "./contextKeys";
|
import { disabledKey } from "./contextKeys";
|
||||||
|
|
||||||
export let className: string = "";
|
export let id = "";
|
||||||
|
export let className = "";
|
||||||
|
export let props: Record<string, string> = {};
|
||||||
|
|
||||||
export let onClick: (event: ClickEvent) => void;
|
export let onClick: (event: ClickEvent) => void;
|
||||||
export let active = false;
|
export let active = false;
|
||||||
|
|
||||||
const disabledStore = getContext(disabledKey)
|
const disabledStore = getContext(disabledKey);
|
||||||
$: disabled = $disabledStore;
|
$: disabled = $disabledStore;
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
button {
|
button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
vertical-align: middle;
|
padding: 0;
|
||||||
width: 28px;
|
|
||||||
height: 28px;
|
|
||||||
|
|
||||||
background-color: white;
|
background-color: white;
|
||||||
|
|
||||||
& > :global(svg),
|
|
||||||
& > :global(img) {
|
|
||||||
fill: currentColor;
|
|
||||||
vertical-align: unset;
|
|
||||||
width: 100%;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: #eee;
|
background-color: #eee;
|
||||||
}
|
}
|
||||||
|
@ -57,14 +50,38 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
|
||||||
|
/* constrain icon */
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
|
||||||
|
& > :global(svg),
|
||||||
|
& > :global(img) {
|
||||||
|
fill: currentColor;
|
||||||
|
vertical-align: unset;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* for DropdownMenu */
|
||||||
|
:global(.dropdown-toggle)::after {
|
||||||
|
margin-right: 0.25rem;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="p-1 {className}"
|
{id}
|
||||||
|
class={className}
|
||||||
|
{...props}
|
||||||
class:active
|
class:active
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
{disabled}
|
{disabled}
|
||||||
on:click={onClick}
|
on:click={onClick}
|
||||||
on:mousedown|preventDefault>
|
on:mousedown|preventDefault>
|
||||||
<slot />
|
<span class="p-1"><slot /></span>
|
||||||
</button>
|
</button>
|
|
@ -7,11 +7,11 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.rainbow {
|
.rainbow {
|
||||||
background: linear-gradient(217deg, rgba(255, 0, 0, 0.8), rgba(255, 0, 0, 0) 70.71%),
|
background: content-box
|
||||||
linear-gradient(127deg, rgba(0, 255, 0, 0.8), rgba(0, 255, 0, 0) 70.71%),
|
linear-gradient(217deg, rgba(255, 0, 0, 0.8), rgba(255, 0, 0, 0) 70.71%),
|
||||||
linear-gradient(336deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 70.71%);
|
content-box
|
||||||
|
linear-gradient(127deg, rgba(0, 255, 0, 0.8), rgba(0, 255, 0, 0) 70.71%),
|
||||||
background-clip: content-box;
|
content-box
|
||||||
/* Boostrap .rounded has .25rem */
|
linear-gradient(336deg, rgba(0, 0, 255, 0.8), rgba(0, 0, 255, 0) 70.71%),
|
||||||
border-radius: 0.375rem;
|
border-box white;
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,12 +20,12 @@ function wrapWithForecolor(color: string): void {
|
||||||
export const forecolorButton = {
|
export const forecolorButton = {
|
||||||
component: IconButton,
|
component: IconButton,
|
||||||
icon: squareFillIcon,
|
icon: squareFillIcon,
|
||||||
className: "forecolor p-1",
|
className: "forecolor",
|
||||||
onClick: () => wrapWithForecolor(getForecolor()),
|
onClick: () => wrapWithForecolor(getForecolor()),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const colorpickerButton = {
|
export const colorpickerButton = {
|
||||||
component: ColorPicker,
|
component: ColorPicker,
|
||||||
className: "rainbow p-1",
|
className: "rainbow",
|
||||||
onChange: ({ currentTarget }) => setForegroundColor(currentTarget.value),
|
onChange: ({ currentTarget }) => setForegroundColor(currentTarget.value),
|
||||||
};
|
};
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { setupI18n, ModuleName } from "anki/i18n";
|
||||||
import EditorToolbarSvelte from "./EditorToolbar.svelte";
|
import EditorToolbarSvelte from "./EditorToolbar.svelte";
|
||||||
|
|
||||||
import LabelButton from "./LabelButton.svelte";
|
import LabelButton from "./LabelButton.svelte";
|
||||||
import DropdownIconButton from "./DropdownIconButton.svelte";
|
import DropdownMenu from "./DropdownMenu.svelte";
|
||||||
|
|
||||||
// @ts-ignore
|
// @ts-ignore
|
||||||
export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte";
|
export { updateActiveButtons, clearActiveButtons } from "./CommandIconButton.svelte";
|
||||||
|
@ -46,8 +46,6 @@ const defaultButtons = [
|
||||||
],
|
],
|
||||||
[forecolorButton, colorpickerButton],
|
[forecolorButton, colorpickerButton],
|
||||||
[attachmentButton, recordButton, clozeButton, mathjaxButton, htmlButton],
|
[attachmentButton, recordButton, clozeButton, mathjaxButton, htmlButton],
|
||||||
[ { component: DropdownIconButton }],
|
|
||||||
|
|
||||||
];
|
];
|
||||||
|
|
||||||
class EditorToolbar extends HTMLElement {
|
class EditorToolbar extends HTMLElement {
|
||||||
|
@ -57,16 +55,18 @@ class EditorToolbar extends HTMLElement {
|
||||||
connectedCallback(): void {
|
connectedCallback(): void {
|
||||||
this.disabled = writable(false);
|
this.disabled = writable(false);
|
||||||
|
|
||||||
setupI18n({ modules: [ModuleName.STATISTICS, ModuleName.SCHEDULING] }).then(() => {
|
setupI18n({ modules: [ModuleName.STATISTICS, ModuleName.SCHEDULING] }).then(
|
||||||
this.component = new EditorToolbarSvelte({
|
() => {
|
||||||
target: this,
|
this.component = new EditorToolbarSvelte({
|
||||||
props: {
|
target: this,
|
||||||
buttons: defaultButtons,
|
props: {
|
||||||
nightMode: checkNightMode(),
|
buttons: defaultButtons,
|
||||||
disabled: this.disabled,
|
nightMode: checkNightMode(),
|
||||||
},
|
disabled: this.disabled,
|
||||||
});
|
},
|
||||||
})
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
enableButtons(): void {
|
enableButtons(): void {
|
||||||
|
|
12
ts/editor-toolbar/types.ts
Normal file
12
ts/editor-toolbar/types.ts
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import type { SvelteComponent } from "svelte/internal";
|
||||||
|
|
||||||
|
export interface ButtonDefinition {
|
||||||
|
component: SvelteComponent;
|
||||||
|
id?: string;
|
||||||
|
className?: string;
|
||||||
|
props?: Record<string, string>;
|
||||||
|
button: HTMLButtonElement;
|
||||||
|
[arg: string]: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Buttons = ButtonDefinition | Buttons[];
|
Loading…
Reference in a new issue