Replace (some) Bootstrap dropdowns with Floating UI (#1695)
* Implement a first version of WithFloating and Portal * Add outside slot for Portal * Execute computePosition from WithFloating * Set up a first example of new WithFloating with the Latex menu * Use autoUpdate in WithFloating * Create sveltelib/position * Add event-store * Use event-store in close-on-click * Implement subscribeToUpdates * Introduce sass/elevation * Split close-on-click to closing-click and subscribe-trigger * Have closing-* stores return a symbol - This way they act more of an EventEmitter than a store * Allow passing show store * Remove styling on float on updatePosition removal * Implement a nice border for dropdowns * Apply different border and box-shadow to Popover in dark/light theme * Fix Ctrl+Shift+T not working * Satisfy formatters and tests * Add copyright header * move copyright header to top (dae)
This commit is contained in:
parent
1219dd8f9e
commit
88217c5e7d
|
@ -56,6 +56,7 @@
|
|||
"postinstall": "patch-package --patch-dir ts/patches"
|
||||
},
|
||||
"dependencies": {
|
||||
"@floating-ui/dom": "^0.3.0",
|
||||
"@fluent/bundle": "^0.17.0",
|
||||
"@mdi/svg": "^6.1.95",
|
||||
"@popperjs/core": "^2.9.2",
|
||||
|
|
|
@ -75,6 +75,14 @@ sass_library(
|
|||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
sass_library(
|
||||
name = "elevation_lib",
|
||||
srcs = [
|
||||
"elevation.scss",
|
||||
],
|
||||
visibility = ["//visibility:public"],
|
||||
)
|
||||
|
||||
exports_files(
|
||||
["_vars.scss"],
|
||||
visibility = ["//visibility:public"],
|
||||
|
|
|
@ -0,0 +1,80 @@
|
|||
// Heavily inspired by https://github.com/material-components/material-components-web/tree/master/packages/mdc-elevation
|
||||
@use "sass:map";
|
||||
@use "sass:list";
|
||||
|
||||
/**
|
||||
* The maps correspond to dp levels:
|
||||
* 0: 0dp
|
||||
* 1: 1dp
|
||||
* 2: 2dp
|
||||
* 3: 3dp
|
||||
* 4: 4dp
|
||||
* 5: 6dp
|
||||
* 6: 8dp
|
||||
* 7: 12dp
|
||||
* 8: 16dp
|
||||
* 9: 24dp
|
||||
*/
|
||||
|
||||
$umbra-map: (
|
||||
0: "0px 0px 0px 0px",
|
||||
1: "0px 2px 1px -1px",
|
||||
2: "0px 3px 1px -2px",
|
||||
3: "0px 3px 3px -2px",
|
||||
4: "0px 2px 4px -1px",
|
||||
5: "0px 3px 5px -1px",
|
||||
6: "0px 5px 5px -3px",
|
||||
7: "0px 7px 8px -4px",
|
||||
8: "0px 8px 10px -5px",
|
||||
9: "0px 11px 15px -7px",
|
||||
);
|
||||
|
||||
$penumbra-map: (
|
||||
0: "0px 0px 0px 0px",
|
||||
1: "0px 1px 1px 0px",
|
||||
2: "0px 2px 2px 0px",
|
||||
3: "0px 3px 4px 0px",
|
||||
4: "0px 4px 5px 0px",
|
||||
5: "0px 6px 10px 0px",
|
||||
6: "0px 8px 10px 1px",
|
||||
7: "0px 12px 17px 2px",
|
||||
8: "0px 16px 24px 2px",
|
||||
9: "0px 24px 38px 3px",
|
||||
);
|
||||
|
||||
$ambient-map: (
|
||||
0: "0px 0px 0px 0px",
|
||||
1: "0px 1px 3px 0px",
|
||||
2: "0px 1px 5px 0px",
|
||||
3: "0px 1px 8px 0px",
|
||||
4: "0px 1px 10px 0px",
|
||||
5: "0px 1px 18px 0px",
|
||||
6: "0px 3px 14px 2px",
|
||||
7: "0px 5px 22px 4px",
|
||||
8: "0px 6px 30px 5px",
|
||||
9: "0px 9px 46px 8px",
|
||||
);
|
||||
|
||||
$umbra-opacity: 0.2;
|
||||
$penumbra-opacity: 0.14;
|
||||
$ambient-opacity: 0.12;
|
||||
|
||||
@function box-shadow($level, $opacity-boost: 0, $color: black) {
|
||||
$umbra-z-value: map.get($umbra-map, $level);
|
||||
$penumbra-z-value: map.get($penumbra-map, $level);
|
||||
$ambient-z-value: map.get($ambient-map, $level);
|
||||
|
||||
$umbra-color: rgba($color, $umbra-opacity + $opacity-boost);
|
||||
$penumbra-color: rgba($color, $penumbra-opacity + $opacity-boost);
|
||||
$ambient-color: rgba($color, $ambient-opacity + $opacity-boost);
|
||||
|
||||
@return (
|
||||
#{"#{$umbra-z-value} #{$umbra-color}"},
|
||||
#{"#{$penumbra-z-value} #{$penumbra-color}"},
|
||||
#{$ambient-z-value} $ambient-color
|
||||
);
|
||||
}
|
||||
|
||||
@mixin elevation($level, $other: ()) {
|
||||
box-shadow: list.join(box-shadow($level), $other);
|
||||
}
|
|
@ -8,6 +8,7 @@ _ts_deps = [
|
|||
"//ts/sveltelib",
|
||||
"@npm//@popperjs/core",
|
||||
"@npm//@types/bootstrap",
|
||||
"@npm//@floating-ui/dom",
|
||||
"@npm//bootstrap",
|
||||
"@npm//svelte",
|
||||
]
|
||||
|
@ -40,6 +41,7 @@ svelte_check(
|
|||
"//sass:button_mixins_lib",
|
||||
"//sass:scrollbar_lib",
|
||||
"//sass:breakpoints_lib",
|
||||
"//sass:elevation_lib",
|
||||
"//sass/bootstrap",
|
||||
"@npm//@types/bootstrap",
|
||||
"//ts/lib:lib_pkg",
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
Alternative to DropdownMenu that avoids Bootstrap
|
||||
-->
|
||||
<script>
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
</script>
|
||||
|
||||
<div class="popover" class:dark={$pageTheme.isDark} on:mousedown|preventDefault>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.popover {
|
||||
border-radius: 5px;
|
||||
background-color: var(--frame-bg);
|
||||
min-width: 1rem;
|
||||
|
||||
padding: 0.5rem 0;
|
||||
font-size: 1rem;
|
||||
color: var(--text-fg);
|
||||
|
||||
/* outer border */
|
||||
border: 1px solid #b6b6b6;
|
||||
|
||||
&.dark {
|
||||
border-color: #060606;
|
||||
}
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 0 0 0 1px #eeeeee;
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 0 0 0 1px #565656;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,107 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
import { onMount } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import isClosingClick from "../sveltelib/closing-click";
|
||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||
import { documentClick, documentKeyup } from "../sveltelib/event-store";
|
||||
import portal from "../sveltelib/portal";
|
||||
import position from "../sveltelib/position";
|
||||
import subscribeTrigger from "../sveltelib/subscribe-trigger";
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
import toggleable from "../sveltelib/toggleable";
|
||||
|
||||
/** TODO at the moment we only dropdowns which are placed actually below the reference */
|
||||
const placement: Placement = "bottom";
|
||||
|
||||
export let closeOnInsideClick = false;
|
||||
|
||||
/** This may be passed in for more fine-grained control */
|
||||
export let show = writable(false);
|
||||
|
||||
let reference: HTMLElement;
|
||||
let floating: HTMLElement;
|
||||
let arrow: HTMLElement;
|
||||
|
||||
const { toggle, on, off } = toggleable(show);
|
||||
|
||||
onMount(() =>
|
||||
subscribeTrigger(
|
||||
show,
|
||||
isClosingClick(documentClick, {
|
||||
reference,
|
||||
floating,
|
||||
inside: closeOnInsideClick,
|
||||
outside: true,
|
||||
}),
|
||||
isClosingKeyup(documentKeyup, {
|
||||
reference,
|
||||
floating,
|
||||
}),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={reference}
|
||||
class="reference"
|
||||
use:position={{ floating: $show ? floating : null, placement, arrow }}
|
||||
>
|
||||
<slot name="reference" {show} {toggle} {on} {off} />
|
||||
</div>
|
||||
|
||||
<div bind:this={floating} class="floating" hidden={!$show} use:portal>
|
||||
<slot name="floating" />
|
||||
|
||||
<div bind:this={arrow} class="arrow" class:dark={$pageTheme.isDark} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as elevation;
|
||||
|
||||
.reference {
|
||||
/* TODO This should not be necessary */
|
||||
line-height: normal;
|
||||
}
|
||||
|
||||
.floating {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
|
||||
z-index: 90;
|
||||
@include elevation.elevation(8);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
background-color: var(--frame-bg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
transform: rotate(45deg);
|
||||
z-index: 60;
|
||||
|
||||
/* outer border */
|
||||
border: 1px solid #b6b6b6;
|
||||
|
||||
&.dark {
|
||||
border-color: #060606;
|
||||
}
|
||||
|
||||
/* These are dependant on which edge the arrow is supposed to be */
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
||||
/* lightmode box-shadow: inset 1px 1px 0 0 #eee; */
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 0 0 0 1px #565656;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -3,12 +3,13 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||
import { withButton } from "../../components/helpers";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import Popover from "../../components/Popover.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import WithFloating from "../../components/WithFloating.svelte";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { wrapInternal } from "../../lib/wrap";
|
||||
|
@ -61,14 +62,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
];
|
||||
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
|
||||
const showDropdown = writable(false);
|
||||
|
||||
$: if (disabled) {
|
||||
$showDropdown = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithDropdown let:createDropdown>
|
||||
<IconButton {disabled} on:mount={withButton(createDropdown)}>
|
||||
<WithFloating show={showDropdown} closeOnInsideClick let:toggle>
|
||||
<IconButton slot="reference" {disabled} on:click={toggle}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
|
||||
<DropdownMenu>
|
||||
<Popover slot="floating">
|
||||
{#each dropdownItems as [callback, keyCombination, label]}
|
||||
<DropdownItem on:click={callback}>
|
||||
{label}
|
||||
|
@ -78,8 +85,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</DropdownItem>
|
||||
<Shortcut {keyCombination} on:action={callback} />
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
.shortcut {
|
||||
|
|
|
@ -421,7 +421,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
on:tagselectall={selectAllTags}
|
||||
on:tagcopy={copySelectedTags}
|
||||
on:tagdelete={deleteSelectedTags}
|
||||
on:click={appendEmptyTag}
|
||||
on:tagappend={appendEmptyTag}
|
||||
/>
|
||||
{/if}
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
type EventTargetToMap<A extends EventTarget> = A extends HTMLElement
|
||||
export type EventTargetToMap<A extends EventTarget> = A extends HTMLElement
|
||||
? HTMLElementEventMap
|
||||
: A extends Document
|
||||
? DocumentEventMap
|
||||
|
|
|
@ -6,3 +6,11 @@ export function assertUnreachable(x: never): never {
|
|||
}
|
||||
|
||||
export type Callback = () => void;
|
||||
|
||||
export function singleCallback(...callbacks: Callback[]): Callback {
|
||||
return () => {
|
||||
for (const cb of callbacks) {
|
||||
cb();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
|
|
@ -30,6 +30,20 @@
|
|||
"path": "node_modules/@eslint/eslintrc",
|
||||
"licenseFile": "node_modules/@eslint/eslintrc/LICENSE"
|
||||
},
|
||||
"@floating-ui/core@0.5.0": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/floating-ui/floating-ui",
|
||||
"publisher": "atomiks",
|
||||
"path": "node_modules/@floating-ui/core",
|
||||
"licenseFile": "node_modules/@floating-ui/core/README.md"
|
||||
},
|
||||
"@floating-ui/dom@0.3.0": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/floating-ui/floating-ui",
|
||||
"publisher": "atomiks",
|
||||
"path": "node_modules/@floating-ui/dom",
|
||||
"licenseFile": "node_modules/@floating-ui/dom/README.md"
|
||||
},
|
||||
"@fluent/bundle@0.17.1": {
|
||||
"licenses": "Apache-2.0",
|
||||
"repository": "https://github.com/projectfluent/fluent.js",
|
||||
|
|
|
@ -8,6 +8,7 @@ typescript(
|
|||
"//ts/lib",
|
||||
"@npm//svelte",
|
||||
"@npm//tslib",
|
||||
"@npm//@floating-ui/dom",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Readable } from "svelte/store";
|
||||
import { derived } from "svelte/store";
|
||||
|
||||
/**
|
||||
* Typically the right-sided mouse button.
|
||||
*/
|
||||
function isSecondaryButton(event: MouseEvent): boolean {
|
||||
return event.button === 2;
|
||||
}
|
||||
|
||||
interface ClosingClickArgs {
|
||||
/**
|
||||
* Clicking on the reference element should not close.
|
||||
* The reference should handle this itself.
|
||||
*/
|
||||
reference: EventTarget;
|
||||
floating: EventTarget;
|
||||
inside: boolean;
|
||||
outside: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a derived store, which translates `MouseEvent`s into a boolean
|
||||
* indicating whether they constitue a click that should close `floating`.
|
||||
*
|
||||
* @param: Should be an event store wrapping document.click.
|
||||
*/
|
||||
function isClosingClick(
|
||||
store: Readable<MouseEvent>,
|
||||
{ reference, floating, inside, outside }: ClosingClickArgs,
|
||||
): Readable<symbol> {
|
||||
function isTriggerClick(path: EventTarget[]): boolean {
|
||||
return (
|
||||
// Reference element was clicked, e.g. the button.
|
||||
// The reference element needs to handle opening/closing itself.
|
||||
!path.includes(reference) &&
|
||||
((inside && path.includes(floating)) ||
|
||||
(outside && !path.includes(floating)))
|
||||
);
|
||||
}
|
||||
|
||||
function shouldClose(event: MouseEvent): boolean {
|
||||
if (isSecondaryButton(event)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (isTriggerClick(event.composedPath())) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return derived(store, (event: MouseEvent, set: (value: symbol) => void): void => {
|
||||
if (shouldClose(event)) {
|
||||
set(Symbol());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default isClosingClick;
|
|
@ -0,0 +1,48 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Readable } from "svelte/store";
|
||||
import { derived } from "svelte/store";
|
||||
|
||||
interface ClosingKeyupArgs {
|
||||
/**
|
||||
* Clicking on the reference element should not close.
|
||||
* The reference should handle this itself.
|
||||
*/
|
||||
reference: Node;
|
||||
floating: Node;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a derived store, which translates `MouseEvent`s into a boolean
|
||||
* indicating whether they constitue a click that should close `floating`.
|
||||
*
|
||||
* @param: Should be an event store wrapping document.click.
|
||||
*/
|
||||
function isClosingKeyup(
|
||||
store: Readable<KeyboardEvent>,
|
||||
_args: ClosingKeyupArgs,
|
||||
): Readable<symbol> {
|
||||
// TODO there needs to be special treatment, whether the keyup happens
|
||||
// inside the floating element or outside, but I'll defer until we actually
|
||||
// use this for a popover with an input field
|
||||
function shouldClose(event: KeyboardEvent) {
|
||||
if (event.key === "Tab") {
|
||||
// Allow Tab navigation.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return derived(
|
||||
store,
|
||||
(event: KeyboardEvent, set: (value: symbol) => void): void => {
|
||||
if (shouldClose(event)) {
|
||||
set(Symbol());
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default isClosingKeyup;
|
|
@ -0,0 +1,42 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Readable, Subscriber } from "svelte/store";
|
||||
import { readable } from "svelte/store";
|
||||
|
||||
import type { EventTargetToMap } from "../lib/events";
|
||||
import { on } from "../lib/events";
|
||||
import type { Callback } from "../lib/typing";
|
||||
|
||||
type Init<T> = { new (type: string): T; prototype: T };
|
||||
|
||||
/**
|
||||
* A store wrapping an event. Automatically adds/removes event handler upon
|
||||
* first/last subscriber.
|
||||
*
|
||||
* @remarks
|
||||
* Should probably always be used in conjunction with `subscribeToUpdates`.
|
||||
*/
|
||||
function eventStore<T extends EventTarget, K extends keyof EventTargetToMap<T>>(
|
||||
target: T,
|
||||
eventType: Exclude<K, symbol | number>,
|
||||
/**
|
||||
* Store need an initial value. This should probably be a freshly
|
||||
* constructed event, e.g. `new MouseEvent("click")`.
|
||||
*/
|
||||
constructor: Init<EventTargetToMap<T>[K]>,
|
||||
): Readable<EventTargetToMap<T>[K]> {
|
||||
const initEvent = new constructor(eventType);
|
||||
return readable(
|
||||
initEvent,
|
||||
(set: Subscriber<EventTargetToMap<T>[K]>): Callback =>
|
||||
on(target, eventType, set),
|
||||
);
|
||||
}
|
||||
|
||||
export default eventStore;
|
||||
|
||||
const documentClick = eventStore(document, "click", MouseEvent);
|
||||
const documentKeyup = eventStore(document, "keyup", KeyboardEvent);
|
||||
|
||||
export { documentClick, documentKeyup };
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
/**
|
||||
* @param element: The element to be moved.
|
||||
* @param target DOM Element where element is going to be appended
|
||||
*/
|
||||
function portal(
|
||||
element: HTMLElement,
|
||||
targetElement: Element = document.body,
|
||||
): { update(target: Element): void; destroy(): void } {
|
||||
let target: Element = targetElement;
|
||||
|
||||
async function update(newTarget: Element) {
|
||||
target = newTarget;
|
||||
target.append(element);
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
element.remove();
|
||||
}
|
||||
|
||||
update(target);
|
||||
|
||||
return {
|
||||
update,
|
||||
destroy,
|
||||
};
|
||||
}
|
||||
|
||||
export default portal;
|
|
@ -0,0 +1,82 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Placement } from "@floating-ui/dom";
|
||||
import { arrow, autoUpdate, computePosition, offset, shift } from "@floating-ui/dom";
|
||||
|
||||
interface PositionArgs {
|
||||
/**
|
||||
* The floating element which is positioned relative to `reference`.
|
||||
*/
|
||||
floating: HTMLElement | null;
|
||||
placement: Placement;
|
||||
arrow: HTMLElement;
|
||||
}
|
||||
|
||||
function position(
|
||||
reference: HTMLElement,
|
||||
positionArgs: PositionArgs,
|
||||
): { update(args: PositionArgs): void; destroy(): void } {
|
||||
let args = positionArgs;
|
||||
|
||||
async function updateInner(): Promise<void> {
|
||||
const { x, y, middlewareData } = await computePosition(
|
||||
reference,
|
||||
args.floating!,
|
||||
{
|
||||
middleware: [
|
||||
offset(5),
|
||||
shift({ padding: 5 }),
|
||||
arrow({ element: args.arrow }),
|
||||
],
|
||||
placement: args.placement,
|
||||
},
|
||||
);
|
||||
|
||||
const arrowX = middlewareData.arrow?.x ?? "";
|
||||
|
||||
Object.assign(args.arrow.style, {
|
||||
left: `${arrowX}px`,
|
||||
top: `-5px`,
|
||||
});
|
||||
|
||||
Object.assign(args.floating!.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
}
|
||||
|
||||
let cleanup: (() => void) | null = null;
|
||||
|
||||
function destroy(): void {
|
||||
cleanup?.();
|
||||
cleanup = null;
|
||||
|
||||
if (!args.floating) {
|
||||
return;
|
||||
}
|
||||
|
||||
args.floating.style.removeProperty("left");
|
||||
args.floating.style.removeProperty("top");
|
||||
}
|
||||
|
||||
function update(updateArgs: PositionArgs): void {
|
||||
destroy();
|
||||
args = updateArgs;
|
||||
|
||||
if (!args.floating) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup = autoUpdate(reference, args.floating, updateInner);
|
||||
}
|
||||
|
||||
update(args);
|
||||
|
||||
return {
|
||||
update,
|
||||
destroy,
|
||||
};
|
||||
}
|
||||
|
||||
export default position;
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Readable, Writable } from "svelte/store";
|
||||
|
||||
import { Callback, singleCallback } from "../lib/typing";
|
||||
import subscribeToUpdates from "./subscribe-updates";
|
||||
|
||||
/**
|
||||
* The goal of this action is to turn itself inactive.
|
||||
* Once `active` is `true`, it will unsubscribe from `store`.
|
||||
*
|
||||
* @param active: If `active` is `true`, all stores will be subscribed to.
|
||||
* @param stores: If any `store` updates to a true value, active will be set to false.
|
||||
*/
|
||||
function subscribeTrigger(
|
||||
active: Writable<boolean>,
|
||||
...stores: Readable<unknown>[]
|
||||
): Callback {
|
||||
function shouldUnset(): void {
|
||||
active.set(false);
|
||||
}
|
||||
|
||||
let destroy: Callback | null;
|
||||
|
||||
function doDestroy(): void {
|
||||
destroy?.();
|
||||
destroy = null;
|
||||
}
|
||||
|
||||
active.subscribe((value: boolean): void => {
|
||||
if (value && !destroy) {
|
||||
destroy = singleCallback(
|
||||
...stores.map((store) => subscribeToUpdates(store, shouldUnset)),
|
||||
);
|
||||
} else if (!value) {
|
||||
doDestroy();
|
||||
}
|
||||
});
|
||||
|
||||
return doDestroy;
|
||||
}
|
||||
|
||||
export default subscribeTrigger;
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Readable, Subscriber, Unsubscriber } from "svelte/store";
|
||||
|
||||
/**
|
||||
* In some cases, we only care for updates, and not the initial
|
||||
* value of a store, e.g. when the store wraps events.
|
||||
* This also means, we can not use the special store syntax.
|
||||
*/
|
||||
function subscribeToUpdates<T>(
|
||||
store: Readable<T>,
|
||||
subscription: Subscriber<T>,
|
||||
): Unsubscriber {
|
||||
let first = true;
|
||||
|
||||
return store.subscribe((value: T): void => {
|
||||
if (first) {
|
||||
first = false;
|
||||
} else {
|
||||
subscription(value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export default subscribeToUpdates;
|
|
@ -0,0 +1,28 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
export interface Toggleable {
|
||||
toggle: () => void;
|
||||
on: () => void;
|
||||
off: () => void;
|
||||
}
|
||||
|
||||
function toggleable(store: Writable<boolean>): Toggleable {
|
||||
function toggle(): void {
|
||||
store.update((value) => !value);
|
||||
}
|
||||
|
||||
function on(): void {
|
||||
store.set(true);
|
||||
}
|
||||
|
||||
function off(): void {
|
||||
store.set(false);
|
||||
}
|
||||
|
||||
return { toggle, on, off };
|
||||
}
|
||||
|
||||
export default toggleable;
|
12
yarn.lock
12
yarn.lock
|
@ -338,6 +338,18 @@
|
|||
minimatch "^3.0.4"
|
||||
strip-json-comments "^3.1.1"
|
||||
|
||||
"@floating-ui/core@^0.5.0":
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-0.5.0.tgz#d3d378412c525e663f41ced012e4e13049b31908"
|
||||
integrity sha512-Ouka4Ck1mnPFZhgpfyFb+YBflGdk0o/SW8uSwByoRC6VqRzCHpW/7mJbFt/TWjl1lDcRbImJ2ZWOqwlIP71LDg==
|
||||
|
||||
"@floating-ui/dom@^0.3.0":
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-0.3.0.tgz#0fb41e390a109a6382d0946d159d800798fb878b"
|
||||
integrity sha512-5wU64oa5rLNqgOD8cLq2lU25eiOosHAb2Ryt6kADgt5aCyqr9RYDQ3VTtefL9qd6MbZ3fZxPkp1+tNtjKnmTXw==
|
||||
dependencies:
|
||||
"@floating-ui/core" "^0.5.0"
|
||||
|
||||
"@fluent/bundle@^0.17.0":
|
||||
version "0.17.1"
|
||||
resolved "https://registry.yarnpkg.com/@fluent/bundle/-/bundle-0.17.1.tgz#239388f55f115dca593f268d48a5ab8936dc9556"
|
||||
|
|
Loading…
Reference in New Issue