mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Use WithFloating for MathjaxOverlay (#2011)
* Allow passing in reference into WithFloating as prop
* Fix WithAutocomplete
* Fix WithFloating for MathjaxOverlay
* Add resize-store
* Allow passing debug=True to jest_test for debugger support (#2013)
* Disable auto-closing of HTML tags
https://forums.ankiweb.net/t/set-html-editor-as-a-default-editor-instead-of-visual-editor/20988/3
Closes #1963
* Add slight margin to MathjaxEditor
* Enable passing offset and shift to WithFloating
* Hide overflow of mathjax editor
* Add automatic hide functionality to sveltelib/position
* Last polishes for Surrounder class (#2017)
* Make private properties in Surrounder truly private
* Fix remove logic of Surrounder
* No reason for toggleTriggerRemove to be async
* Allow using alt-shift to set all remove formats but this one
* modifyFormat => updateFormat
* Fix formatting
* Fix field descriptions blocking cursor from being set (#2018)
- happens when focus is in HTML editor
* Remove hiding functionality again until it's really useful
* Add support for autoPlacement
* Implement new WithFloating that supports manually calling position()
* Implement hide mechanisms
* Add option in math dropdown to toggle MathJax rendering (#2014)
* Add option in math dropdown to toggle MathJax rendering
Closes #1942
* Hackily redraw the page when toggling MathJax
* Add Fluent string
* Default input setting in fields dialog (#1987) (kleinerpirat)
* Introduce field setting to use plain text editor by default (kleinerpirat)
* Remove leftover function from #1476
* Use boolean instead of string
* Simplify clear_other_field_duplicates
* Convert plain text key to camelCase
* Move HTML item below the existing checkbox, instead of to the right (dae)
Showing it on the right is more space efficient, but feels a bit
cluttered IMHO.
* Fix not being able to scroll when mouse hovers PlainTextInput (#2019)
* Remove overscroll-behavior: none for * (all elements)
* Revert "Remove overscroll-behavior: none for * (all elements)"
This reverts commit 189358908c
.
* Use body instead of *, but keep CSS rule
* Unify two CSS rules
* Remove console.logs
* Reposition mathjax menu on switching between inline/block
* Implement WithOverlay
* Implement FloatingArrow
* Display overlay with padding and brighter background
* Rename to MathjaxOverlay
* Simplify MathjaxOverlay component overall
* Rename ImageHandle to image overlay
* Generally fix ImageOverlay again
* Increase z-index of StickyContainer
* Fix setting block or inline on mathjax
* Add reasons in closing-{click,keyup}
* Have both WithFloating and WithOverlay use a simple show flag instead of a store
* Remove subscribe-trigger
* Fix clicking from one mathjax element to another
* Check before executing cleanup
* Do not wait for elements to mount before slotting in With{Floating,Overlay}
* Allow using reference slot for WithFloating and WithOveray
* Add inline argument to options
* Add support for inline slot in WithOvelay
* Use WithFloating for RemoveFormatButton
* Remove last uses of DropdownMenu and WithDropdown
* Remove all of the bootstrap dropdown components
* Fix closing behavior of several buttons and ImageOverlay
* Increase popover padding to 6px
* Find a different way to create some padding at the bottom of the fields
...before the tag editor
@kleinerpirat I think is what this css what trying to achieve?
* Satisfy tests
* Use removeStyleProperties in ImageOverlay
* Use notify function in WithOverlay and WithFloating
* Do not use portal for WithFloating and WithOverlay
Allows for scrolling
* Set hidden to default false in Rich/Plain TextInput
* Reset handle when changing mathjax elements via click
* Restrict size of empty mathjax image
* Prevent sticky labels from obscuring menus
* Remove several overflow-hidden
* Fix empty string being falsy bug when editing mathjax
* Do not import portal anymore
* Use { reason, originalEvent } instead of symbol as update to modified event store
* Fix closing behavior of image overlay (do not close after resize)
* Simplify Collapsible
* Use removeStyleProperties in Collapsible
* Satisfy eslint
* Fix latex shortcuts being mounted
* Fix mathjax overlay not focusable in first field
* Neither hide image overlay on escaped
* Fix Block ButtonDropdown wrapping
* Bring back portal to fix tag editor
This commit is contained in:
parent
e7af0febb1
commit
3642dc6245
65 changed files with 1405 additions and 1301 deletions
|
@ -1,45 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { setContext } from "svelte";
|
||||
|
||||
import ButtonToolbar from "./ButtonToolbar.svelte";
|
||||
import { dropdownKey } from "./context-keys";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
setContext(dropdownKey, null);
|
||||
</script>
|
||||
|
||||
<ButtonToolbar {id} class="dropdown-menu btn-dropdown-menu {className}" wrap={false}>
|
||||
<div on:mousedown|preventDefault|stopPropagation on:click>
|
||||
<slot />
|
||||
</div>
|
||||
</ButtonToolbar>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
:global(.dropdown-menu.btn-dropdown-menu) {
|
||||
display: none;
|
||||
min-width: 0;
|
||||
padding: calc(var(--buttons-size) / 10) 0;
|
||||
|
||||
background-color: var(--window-bg);
|
||||
border-color: var(--medium-border);
|
||||
|
||||
:global(.btn-group) {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
:global(.dropdown-menu.btn-dropdown-menu.show) {
|
||||
display: flex;
|
||||
}
|
||||
</style>
|
|
@ -3,105 +3,53 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { promiseWithResolver } from "../lib/promise";
|
||||
import { cubicOut } from "svelte/easing";
|
||||
import { tweened } from "svelte/motion";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
import { removeStyleProperties } from "../lib/styling";
|
||||
|
||||
export let collapsed = false;
|
||||
let isCollapsed = false;
|
||||
let hidden = collapsed;
|
||||
export let duration = 300;
|
||||
|
||||
const [outerPromise, outerResolve] = promiseWithResolver<HTMLElement>();
|
||||
const [innerPromise, innerResolve] = promiseWithResolver<HTMLElement>();
|
||||
export let collapse = false;
|
||||
let collapsed = false;
|
||||
|
||||
let style: string;
|
||||
function setStyle(height: number, duration: number) {
|
||||
style = `--collapse-height: -${height}px; --duration: ${duration}ms`;
|
||||
}
|
||||
const size = tweened<number>(undefined, {
|
||||
duration,
|
||||
easing: cubicOut,
|
||||
});
|
||||
|
||||
/* The following two functions use synchronous DOM-manipulation,
|
||||
because Editor field inputs would lose focus when using tick() */
|
||||
|
||||
function getRequiredHeight(el: HTMLElement): number {
|
||||
el.style.setProperty("position", "absolute");
|
||||
el.style.setProperty("visibility", "hidden");
|
||||
el.removeAttribute("hidden");
|
||||
|
||||
const height = el.clientHeight;
|
||||
|
||||
el.setAttribute("hidden", "");
|
||||
el.style.removeProperty("position");
|
||||
el.style.removeProperty("visibility");
|
||||
|
||||
return height;
|
||||
}
|
||||
|
||||
async function transition(collapse: boolean) {
|
||||
const outer = await outerPromise;
|
||||
const inner = await innerPromise;
|
||||
|
||||
outer.style.setProperty("overflow", "hidden");
|
||||
isCollapsed = true;
|
||||
|
||||
const height = collapse ? inner.clientHeight : getRequiredHeight(inner);
|
||||
|
||||
/* This function practically caps the maximum time at around 200ms,
|
||||
but still allows to differentiate between small and large contents */
|
||||
const duration = 10 + Math.pow(height, 1 / 4) * 20;
|
||||
|
||||
setStyle(height, duration);
|
||||
|
||||
if (!collapse) {
|
||||
inner.removeAttribute("hidden");
|
||||
isCollapsed = false;
|
||||
function doCollapse(collapse: boolean): void {
|
||||
if (collapse) {
|
||||
size.set(0);
|
||||
} else {
|
||||
collapsed = false;
|
||||
size.set(1, { duration: 0 });
|
||||
}
|
||||
|
||||
inner.addEventListener(
|
||||
"transitionend",
|
||||
() => {
|
||||
inner.toggleAttribute("hidden", collapse);
|
||||
outer.style.removeProperty("overflow");
|
||||
hidden = collapse;
|
||||
},
|
||||
{ once: true },
|
||||
);
|
||||
}
|
||||
|
||||
/* prevent transition on mount for performance reasons */
|
||||
let firstTransition = true;
|
||||
$: doCollapse(collapse);
|
||||
|
||||
$: {
|
||||
transition(collapsed);
|
||||
firstTransition = false;
|
||||
let collapsibleElement: HTMLElement;
|
||||
let clientHeight: number;
|
||||
|
||||
function updateHeight(percentage: number): void {
|
||||
collapsibleElement.style.overflow = "hidden";
|
||||
|
||||
if (percentage === 1) {
|
||||
removeStyleProperties(collapsibleElement, "height", "overflow");
|
||||
} else if (percentage === 0) {
|
||||
collapsed = true;
|
||||
removeStyleProperties(collapsibleElement, "height", "overflow");
|
||||
} else {
|
||||
collapsibleElement.style.height = `${percentage * clientHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
$: if (collapsibleElement) {
|
||||
updateHeight($size);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div {id} class="collapsible-container {className}" use:outerResolve>
|
||||
<div
|
||||
class="collapsible-inner"
|
||||
class:collapsed={isCollapsed}
|
||||
class:no-transition={firstTransition}
|
||||
use:innerResolve
|
||||
{style}
|
||||
>
|
||||
<slot {hidden} />
|
||||
</div>
|
||||
<div bind:this={collapsibleElement} class="collapsible" bind:clientHeight>
|
||||
<slot {collapsed} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.collapsible-container {
|
||||
position: relative;
|
||||
}
|
||||
.collapsible-inner {
|
||||
transition: margin-top var(--duration) ease-in;
|
||||
|
||||
&.collapsed {
|
||||
margin-top: var(--collapse-height);
|
||||
}
|
||||
&.no-transition {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,8 +3,6 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
|
@ -13,17 +11,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let tabbable: boolean = false;
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
</script>
|
||||
|
||||
<button
|
||||
{id}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
bind:this={buttonRef}
|
||||
class="dropdown-item btn {className}"
|
||||
class:btn-day={!$pageTheme.isDark}
|
||||
class:btn-night={$pageTheme.isDark}
|
||||
|
|
|
@ -1,39 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { setContext } from "svelte";
|
||||
|
||||
import { dropdownKey } from "./context-keys";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
export { className as class };
|
||||
|
||||
export let labelledby: string | undefined = undefined;
|
||||
export let show = false;
|
||||
|
||||
setContext(dropdownKey, null);
|
||||
</script>
|
||||
|
||||
<div
|
||||
{id}
|
||||
class="dropdown-menu"
|
||||
class:show
|
||||
aria-labelledby={labelledby}
|
||||
on:mousedown|preventDefault|stopPropagation
|
||||
>
|
||||
<div class="dropdown-content {className}">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.dropdown-menu {
|
||||
border-radius: 5px;
|
||||
background-color: var(--frame-bg);
|
||||
border-color: var(--medium-border);
|
||||
min-width: 1rem;
|
||||
}
|
||||
</style>
|
38
ts/components/FloatingArrow.svelte
Normal file
38
ts/components/FloatingArrow.svelte
Normal file
|
@ -0,0 +1,38 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
</script>
|
||||
|
||||
<div class="arrow" class:dark={$pageTheme.isDark} />
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as elevation;
|
||||
|
||||
.arrow {
|
||||
background-color: var(--frame-bg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
z-index: 60;
|
||||
|
||||
/* outer border */
|
||||
border: 1px solid #b6b6b6;
|
||||
|
||||
&.dark {
|
||||
border-color: #060606;
|
||||
}
|
||||
|
||||
/* Rotate the box to indicate the different directions */
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 1px 1px 0 0 #565656;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -3,11 +3,7 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
import { dropdownKey } from "./context-keys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
import IconConstrain from "./IconConstrain.svelte";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
|
@ -22,25 +18,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let iconSize = 75;
|
||||
export let widthMultiplier = 1;
|
||||
export let flipX = false;
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
onMount(() => dispatch("mount", { button: buttonRef }));
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
class="icon-button btn {className}"
|
||||
class:active
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
class:btn-day={!$pageTheme.isDark}
|
||||
class:btn-night={$pageTheme.isDark}
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
{disabled}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
on:click
|
||||
|
@ -64,8 +50,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
@include button.btn-day;
|
||||
@include button.btn-night;
|
||||
|
||||
.dropdown-toggle::after {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
<!--
|
||||
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";
|
||||
|
@ -19,7 +17,9 @@ Alternative to DropdownMenu that avoids Bootstrap
|
|||
min-width: 1rem;
|
||||
max-width: 95vw;
|
||||
|
||||
padding: 0.5rem 0;
|
||||
/* Needs enough space for FloatingArrow to be positioned */
|
||||
padding: 6px;
|
||||
|
||||
font-size: 1rem;
|
||||
color: var(--text-fg);
|
||||
|
||||
|
|
|
@ -27,7 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
z-index: 10;
|
||||
z-index: 50;
|
||||
|
||||
background: var(--sticky-bg, var(--window-bg));
|
||||
border-style: solid;
|
||||
|
|
|
@ -1,112 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import { onDestroy, setContext } from "svelte";
|
||||
|
||||
import { dropdownKey } from "./context-keys";
|
||||
|
||||
export let autoOpen = false;
|
||||
export let autoClose: boolean | "inside" | "outside" = true;
|
||||
|
||||
export let toggleOpen = true;
|
||||
export let drop: "down" | "up" = "down";
|
||||
export let align: "start" | "end" | "auto" = "auto";
|
||||
|
||||
let placement: string;
|
||||
|
||||
$: {
|
||||
let blockPlacement: string;
|
||||
|
||||
switch (drop) {
|
||||
case "down":
|
||||
blockPlacement = "bottom";
|
||||
break;
|
||||
case "up":
|
||||
blockPlacement = "top";
|
||||
break;
|
||||
}
|
||||
|
||||
let inlinePlacement: string;
|
||||
|
||||
switch (align) {
|
||||
case "start":
|
||||
case "end":
|
||||
inlinePlacement = `-${align}`;
|
||||
break;
|
||||
default:
|
||||
inlinePlacement = "";
|
||||
break;
|
||||
}
|
||||
|
||||
placement = `${blockPlacement}${inlinePlacement}`;
|
||||
}
|
||||
|
||||
$: dropClass = `drop${drop}`;
|
||||
|
||||
export let skidding = 0;
|
||||
export let distance = 2;
|
||||
|
||||
setContext(dropdownKey, {
|
||||
dropdown: true,
|
||||
"data-bs-toggle": "dropdown",
|
||||
});
|
||||
|
||||
let dropdown: Dropdown;
|
||||
let api: Dropdown & { isVisible: () => boolean };
|
||||
|
||||
function isVisible() {
|
||||
return (dropdown as any)._menu
|
||||
? (dropdown as any)._menu.classList.contains("show")
|
||||
: false;
|
||||
}
|
||||
|
||||
const noop = () => {};
|
||||
function createDropdown(toggle: HTMLElement): Dropdown {
|
||||
/* avoid focusing element toggle on menu activation */
|
||||
toggle.focus = noop;
|
||||
|
||||
if (!toggleOpen) {
|
||||
/* do not open on clicking toggle */
|
||||
toggle.addEventListener = noop;
|
||||
}
|
||||
|
||||
dropdown = new Dropdown(toggle, {
|
||||
autoClose,
|
||||
offset: [skidding, distance],
|
||||
popperConfig: { placement },
|
||||
} as any);
|
||||
|
||||
if (autoOpen) {
|
||||
dropdown.show();
|
||||
}
|
||||
|
||||
api = {
|
||||
show: dropdown.show.bind(dropdown),
|
||||
// TODO this is quite confusing, but having a noop function fixes Bootstrap
|
||||
// in the deck-options when not including Bootstrap via <script />
|
||||
toggle: () => {},
|
||||
/* toggle: dropdown.toggle.bind(dropdown), */
|
||||
hide: dropdown.hide.bind(dropdown),
|
||||
update: dropdown.update.bind(dropdown),
|
||||
dispose: dropdown.dispose.bind(dropdown),
|
||||
isVisible,
|
||||
} as any;
|
||||
|
||||
return api;
|
||||
}
|
||||
|
||||
onDestroy(() => dropdown?.dispose());
|
||||
</script>
|
||||
|
||||
<div class="with-dropdown {dropClass}">
|
||||
<slot {createDropdown} dropdownObject={api} />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
display: contents;
|
||||
}
|
||||
</style>
|
|
@ -3,83 +3,169 @@ 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 type { FloatingElement, Placement } from "@floating-ui/dom";
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
import type { Callback } from "../lib/typing";
|
||||
import { singleCallback } from "../lib/typing";
|
||||
import isClosingClick from "../sveltelib/closing-click";
|
||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||
import type { EventPredicateResult } from "../sveltelib/event-predicate";
|
||||
import { documentClick, documentKeyup } from "../sveltelib/event-store";
|
||||
import portal from "../sveltelib/portal";
|
||||
import type { PositionArgs } from "../sveltelib/position";
|
||||
import position from "../sveltelib/position";
|
||||
import subscribeTrigger from "../sveltelib/subscribe-trigger";
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
import toggleable from "../sveltelib/toggleable";
|
||||
import type { PositioningCallback } from "../sveltelib/position/auto-update";
|
||||
import autoUpdate from "../sveltelib/position/auto-update";
|
||||
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
||||
import positionFloating from "../sveltelib/position/position-floating";
|
||||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||
import FloatingArrow from "./FloatingArrow.svelte";
|
||||
|
||||
export let portalTarget: HTMLElement | null = null;
|
||||
|
||||
export let placement: Placement | "auto" = "bottom";
|
||||
export let offset = 5;
|
||||
export let shift = 5;
|
||||
export let inline = false;
|
||||
export let hideIfEscaped = false;
|
||||
export let hideIfReferenceHidden = false;
|
||||
|
||||
/** This may be passed in for more fine-grained control */
|
||||
export let show = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let arrow: HTMLElement;
|
||||
|
||||
$: positionCurried = positionFloating({
|
||||
placement,
|
||||
offset,
|
||||
shift,
|
||||
inline,
|
||||
arrow,
|
||||
hideIfEscaped,
|
||||
hideIfReferenceHidden,
|
||||
hideCallback: (reason: string) => dispatch("close", { reason }),
|
||||
});
|
||||
|
||||
let autoAction: ActionReturn = {};
|
||||
|
||||
$: {
|
||||
positionCurried;
|
||||
autoAction.update?.(positioningCallback);
|
||||
}
|
||||
|
||||
export let placement: Placement = "bottom";
|
||||
export let closeOnInsideClick = false;
|
||||
export let keepOnKeyup = false;
|
||||
|
||||
/** This may be passed in for more fine-grained control */
|
||||
export let show = writable(false);
|
||||
export let reference: HTMLElement | undefined = undefined;
|
||||
let floating: FloatingElement;
|
||||
|
||||
let reference: HTMLElement;
|
||||
let floating: HTMLElement;
|
||||
let arrow: HTMLElement;
|
||||
|
||||
const { toggle, on, off } = toggleable(show);
|
||||
|
||||
let args: PositionArgs;
|
||||
$: args = {
|
||||
floating: $show ? floating : null,
|
||||
placement,
|
||||
arrow,
|
||||
};
|
||||
|
||||
let update: (args: PositionArgs) => void;
|
||||
$: update?.(args);
|
||||
|
||||
function asReference(element: HTMLElement) {
|
||||
const pos = position(element, args);
|
||||
reference = element;
|
||||
update = pos.update;
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
pos.destroy();
|
||||
},
|
||||
};
|
||||
function applyPosition(
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
): Promise<void> {
|
||||
return position(reference, floating);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
const triggers = [
|
||||
isClosingClick(documentClick, {
|
||||
reference,
|
||||
floating,
|
||||
inside: closeOnInsideClick,
|
||||
outside: true,
|
||||
}),
|
||||
async function position(
|
||||
callback: (
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
) => Promise<void> = applyPosition,
|
||||
): Promise<void> {
|
||||
if (reference && floating) {
|
||||
return callback(reference, floating, positionCurried);
|
||||
}
|
||||
}
|
||||
|
||||
function asReference(referenceArgument: HTMLElement) {
|
||||
reference = referenceArgument;
|
||||
}
|
||||
|
||||
function positioningCallback(
|
||||
reference: HTMLElement,
|
||||
callback: PositioningCallback,
|
||||
): Callback {
|
||||
const innerFloating = floating;
|
||||
return callback(reference, innerFloating, () =>
|
||||
positionCurried(reference, innerFloating),
|
||||
);
|
||||
}
|
||||
|
||||
let cleanup: Callback | null = null;
|
||||
|
||||
function updateFloating(
|
||||
reference: HTMLElement | undefined,
|
||||
floating: FloatingElement,
|
||||
isShowing: boolean,
|
||||
) {
|
||||
cleanup?.();
|
||||
cleanup = null;
|
||||
|
||||
if (!reference || !floating || !isShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closingClick = isClosingClick(documentClick, {
|
||||
reference,
|
||||
floating,
|
||||
inside: closeOnInsideClick,
|
||||
outside: true,
|
||||
});
|
||||
|
||||
const subscribers = [
|
||||
subscribeToUpdates(closingClick, (event: EventPredicateResult) =>
|
||||
dispatch("close", event),
|
||||
),
|
||||
];
|
||||
|
||||
if (!keepOnKeyup) {
|
||||
triggers.push(
|
||||
isClosingKeyup(documentKeyup, {
|
||||
reference,
|
||||
floating,
|
||||
}),
|
||||
const closingKeyup = isClosingKeyup(documentKeyup, {
|
||||
reference,
|
||||
floating,
|
||||
});
|
||||
|
||||
subscribers.push(
|
||||
subscribeToUpdates(closingKeyup, (event: EventPredicateResult) =>
|
||||
dispatch("close", event),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
subscribeTrigger(show, ...triggers);
|
||||
});
|
||||
autoAction = autoUpdate(reference, positioningCallback);
|
||||
cleanup = singleCallback(...subscribers, autoAction.destroy!);
|
||||
}
|
||||
|
||||
$: updateFloating(reference, floating, show);
|
||||
|
||||
onDestroy(() => cleanup?.());
|
||||
</script>
|
||||
|
||||
<slot name="reference" {show} {toggle} {on} {off} {asReference} />
|
||||
<slot {position} {asReference} />
|
||||
|
||||
<div bind:this={floating} class="floating" hidden={!$show} use:portal>
|
||||
<slot name="floating" />
|
||||
<div bind:this={arrow} class="arrow" class:dark={$pageTheme.isDark} />
|
||||
{#if $$slots.reference}
|
||||
{#if inline}
|
||||
<span class="floating-reference" use:asReference>
|
||||
<slot name="reference" />
|
||||
</span>
|
||||
{:else}
|
||||
<div class="floating-reference" use:asReference>
|
||||
<slot name="reference" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div bind:this={floating} class="floating" use:portal={portalTarget}>
|
||||
{#if show}
|
||||
<slot name="floating" />
|
||||
{/if}
|
||||
|
||||
<div bind:this={arrow} class="floating-arrow" hidden={!show}>
|
||||
<FloatingArrow />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
|
@ -89,33 +175,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
position: absolute;
|
||||
border-radius: 5px;
|
||||
|
||||
z-index: 90;
|
||||
z-index: 890;
|
||||
@include elevation.elevation(8);
|
||||
}
|
||||
|
||||
.arrow {
|
||||
position: absolute;
|
||||
background-color: var(--frame-bg);
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
z-index: 60;
|
||||
|
||||
/* outer border */
|
||||
border: 1px solid #b6b6b6;
|
||||
|
||||
&.dark {
|
||||
border-color: #060606;
|
||||
}
|
||||
|
||||
/* Rotate the box to indicate the different directions */
|
||||
border-right: none;
|
||||
border-bottom: none;
|
||||
|
||||
/* inner border */
|
||||
box-shadow: inset 1px 1px 0 0 #eeeeee;
|
||||
|
||||
&.dark {
|
||||
box-shadow: inset 1px 1px 0 0 #565656;
|
||||
&-arrow {
|
||||
position: absolute;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
159
ts/components/WithOverlay.svelte
Normal file
159
ts/components/WithOverlay.svelte
Normal file
|
@ -0,0 +1,159 @@
|
|||
<!--
|
||||
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 { FloatingElement } from "@floating-ui/dom";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
import type { Callback } from "../lib/typing";
|
||||
import { singleCallback } from "../lib/typing";
|
||||
import isClosingClick from "../sveltelib/closing-click";
|
||||
import isClosingKeyup from "../sveltelib/closing-keyup";
|
||||
import type { EventPredicateResult } from "../sveltelib/event-predicate";
|
||||
import { documentClick, documentKeyup } from "../sveltelib/event-store";
|
||||
import type { PositioningCallback } from "../sveltelib/position/auto-update";
|
||||
import autoUpdate from "../sveltelib/position/auto-update";
|
||||
import type { PositionAlgorithm } from "../sveltelib/position/position-algorithm";
|
||||
import positionOverlay from "../sveltelib/position/position-overlay";
|
||||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||
|
||||
export let padding = 0;
|
||||
export let inline = false;
|
||||
|
||||
/** This may be passed in for more fine-grained control */
|
||||
export let show = true;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
$: positionCurried = positionOverlay({
|
||||
padding,
|
||||
inline,
|
||||
hideCallback: (reason: string) => dispatch("close", { reason }),
|
||||
});
|
||||
|
||||
let autoAction: ActionReturn = {};
|
||||
|
||||
$: {
|
||||
positionCurried;
|
||||
autoAction.update?.(positioningCallback);
|
||||
}
|
||||
|
||||
export let closeOnInsideClick = false;
|
||||
export let keepOnKeyup = false;
|
||||
|
||||
export let reference: HTMLElement | undefined = undefined;
|
||||
let floating: FloatingElement;
|
||||
|
||||
function applyPosition(
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
): Promise<void> {
|
||||
return position(reference, floating);
|
||||
}
|
||||
|
||||
async function position(
|
||||
callback: (
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
) => Promise<void> = applyPosition,
|
||||
): Promise<void> {
|
||||
if (reference && floating) {
|
||||
return callback(reference, floating, positionCurried);
|
||||
}
|
||||
}
|
||||
|
||||
function asReference(referenceArgument: HTMLElement) {
|
||||
reference = referenceArgument;
|
||||
}
|
||||
|
||||
function positioningCallback(
|
||||
reference: HTMLElement,
|
||||
callback: PositioningCallback,
|
||||
): Callback {
|
||||
const innerFloating = floating;
|
||||
return callback(reference, innerFloating, () =>
|
||||
positionCurried(reference, innerFloating),
|
||||
);
|
||||
}
|
||||
|
||||
let cleanup: Callback;
|
||||
|
||||
function updateFloating(
|
||||
reference: HTMLElement | undefined,
|
||||
floating: FloatingElement,
|
||||
isShowing: boolean,
|
||||
) {
|
||||
cleanup?.();
|
||||
|
||||
if (!reference || !floating || !isShowing) {
|
||||
return;
|
||||
}
|
||||
|
||||
const closingClick = isClosingClick(documentClick, {
|
||||
reference,
|
||||
floating,
|
||||
inside: closeOnInsideClick,
|
||||
outside: false,
|
||||
});
|
||||
|
||||
const subscribers = [
|
||||
subscribeToUpdates(closingClick, (event: EventPredicateResult) =>
|
||||
dispatch("close", event),
|
||||
),
|
||||
];
|
||||
|
||||
if (!keepOnKeyup) {
|
||||
const closingKeyup = isClosingKeyup(documentKeyup, {
|
||||
reference,
|
||||
floating,
|
||||
});
|
||||
|
||||
subscribers.push(
|
||||
subscribeToUpdates(closingKeyup, (event: EventPredicateResult) =>
|
||||
dispatch("close", event),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
autoAction = autoUpdate(reference, positioningCallback);
|
||||
cleanup = singleCallback(...subscribers, autoAction.destroy!);
|
||||
}
|
||||
|
||||
$: updateFloating(reference, floating, show);
|
||||
</script>
|
||||
|
||||
<slot {position} {asReference} />
|
||||
|
||||
{#if $$slots.reference}
|
||||
{#if inline}
|
||||
<span class="overlay-reference" use:asReference>
|
||||
<slot name="reference" />
|
||||
</span>
|
||||
{:else}
|
||||
<div class="overlay-reference" use:asReference>
|
||||
<slot name="reference" />
|
||||
</div>
|
||||
{/if}
|
||||
{/if}
|
||||
|
||||
<div bind:this={floating} class="overlay">
|
||||
{#if show}
|
||||
<slot name="overlay" />
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as elevation;
|
||||
|
||||
.overlay {
|
||||
position: absolute;
|
||||
border-radius: 5px;
|
||||
|
||||
z-index: 40;
|
||||
@include elevation.elevation(5);
|
||||
}
|
||||
</style>
|
|
@ -5,7 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="ts" context="module">
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
type KeyType = Symbol | string;
|
||||
type KeyType = symbol | string;
|
||||
type UpdaterMap = Map<KeyType, (event: Event) => Promise<boolean>>;
|
||||
type StateMap = Map<KeyType, Promise<boolean>>;
|
||||
|
||||
|
|
|
@ -29,6 +29,7 @@ _ts_deps = [
|
|||
"@npm//@fluent",
|
||||
"@npm//@popperjs",
|
||||
"@npm//@types/jest",
|
||||
"@npm//@mdi",
|
||||
"@npm//bootstrap-icons",
|
||||
"@npm//bootstrap",
|
||||
"@npm//lodash-es",
|
||||
|
|
|
@ -3,15 +3,14 @@ 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 Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import { cloneDeep, isEqual as isEqualLodash } from "lodash-es";
|
||||
import { getContext } from "svelte";
|
||||
|
||||
import Badge from "../components/Badge.svelte";
|
||||
import { touchDeviceKey } from "../components/context-keys";
|
||||
import DropdownItem from "../components/DropdownItem.svelte";
|
||||
import DropdownMenu from "../components/DropdownMenu.svelte";
|
||||
import WithDropdown from "../components/WithDropdown.svelte";
|
||||
import Popover from "../components/Popover.svelte";
|
||||
import WithFloating from "../components/WithFloating.svelte";
|
||||
import * as tr from "../lib/ftl";
|
||||
import { revertIcon } from "./icons";
|
||||
|
||||
|
@ -35,40 +34,45 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let modified: boolean;
|
||||
$: modified = !isEqual(value, defaultValue);
|
||||
|
||||
let dropdown: Dropdown;
|
||||
let showFloating = false;
|
||||
|
||||
const isTouchDevice = getContext<boolean>(touchDeviceKey);
|
||||
|
||||
function revert(): void {
|
||||
value = cloneDeep(defaultValue);
|
||||
dropdown.hide();
|
||||
showFloating = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<WithDropdown let:createDropdown>
|
||||
<div class:hide={!modified}>
|
||||
<WithFloating
|
||||
show={showFloating}
|
||||
closeOnInsideClick
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
let:asReference
|
||||
>
|
||||
<div class:hide={!modified} use:asReference>
|
||||
<Badge
|
||||
class="p-1"
|
||||
on:mount={(event) => (dropdown = createDropdown(event.detail.span))}
|
||||
on:click={() => {
|
||||
if (modified) {
|
||||
dropdown.toggle();
|
||||
showFloating = !showFloating;
|
||||
}
|
||||
}}
|
||||
>
|
||||
{@html revertIcon}
|
||||
</Badge>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownItem
|
||||
class={`spinner ${isTouchDevice ? "spin-always" : ""}`}
|
||||
on:click={() => revert()}
|
||||
>
|
||||
{tr.deckConfigRevertButtonTooltip()}<Badge>{@html revertIcon}</Badge>
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
</WithDropdown>
|
||||
|
||||
<Popover slot="floating">
|
||||
<DropdownItem
|
||||
class={`spinner ${isTouchDevice ? "spin-always" : ""}`}
|
||||
on:click={() => revert()}
|
||||
>
|
||||
{tr.deckConfigRevertButtonTooltip()}<Badge>{@html revertIcon}</Badge>
|
||||
</DropdownItem>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
:global(.spinner:hover .badge, .spinner.spin-always .badge) {
|
||||
|
|
|
@ -3,19 +3,20 @@ 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 Dropdown from "bootstrap/js/dist/dropdown";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import { createEventDispatcher, tick } from "svelte";
|
||||
|
||||
import ButtonGroup from "../components/ButtonGroup.svelte";
|
||||
import DropdownDivider from "../components/DropdownDivider.svelte";
|
||||
import DropdownItem from "../components/DropdownItem.svelte";
|
||||
import DropdownMenu from "../components/DropdownMenu.svelte";
|
||||
import IconButton from "../components/IconButton.svelte";
|
||||
import LabelButton from "../components/LabelButton.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 { withCollapsedWhitespace } from "../lib/i18n";
|
||||
import { getPlatformString } from "../lib/shortcuts";
|
||||
import { chevronDown } from "./icons";
|
||||
import type { DeckOptionsState } from "./lib";
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
@ -28,27 +29,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
function removeConfig(): void {
|
||||
async function removeConfig(): Promise<void> {
|
||||
// show pop-up after dropdown has gone away
|
||||
setTimeout(() => {
|
||||
if (state.defaultConfigSelected()) {
|
||||
alert(tr.schedulingTheDefaultConfigurationCantBeRemoved());
|
||||
return;
|
||||
await tick();
|
||||
|
||||
if (state.defaultConfigSelected()) {
|
||||
alert(tr.schedulingTheDefaultConfigurationCantBeRemoved());
|
||||
return;
|
||||
}
|
||||
|
||||
const msg =
|
||||
(state.removalWilLForceFullSync()
|
||||
? tr.deckConfigWillRequireFullSync() + " "
|
||||
: "") +
|
||||
tr.deckConfigConfirmRemoveName({ name: state.getCurrentName() });
|
||||
|
||||
if (confirm(withCollapsedWhitespace(msg))) {
|
||||
try {
|
||||
state.removeCurrentConfig();
|
||||
dispatch("remove");
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
const msg =
|
||||
(state.removalWilLForceFullSync()
|
||||
? tr.deckConfigWillRequireFullSync() + " "
|
||||
: "") +
|
||||
tr.deckConfigConfirmRemoveName({ name: state.getCurrentName() });
|
||||
if (confirm(withCollapsedWhitespace(msg))) {
|
||||
try {
|
||||
state.removeCurrentConfig();
|
||||
dispatch("remove");
|
||||
} catch (err) {
|
||||
alert(err);
|
||||
}
|
||||
}
|
||||
}, 100);
|
||||
}
|
||||
}
|
||||
|
||||
function save(applyToChildDecks: boolean): void {
|
||||
|
@ -56,8 +59,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
state.save(applyToChildDecks);
|
||||
}
|
||||
|
||||
let dropdown: Dropdown;
|
||||
const saveKeyCombination = "Control+Enter";
|
||||
|
||||
let showFloating = false;
|
||||
</script>
|
||||
|
||||
<ButtonGroup>
|
||||
|
@ -69,12 +73,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
>
|
||||
<Shortcut keyCombination={saveKeyCombination} on:action={() => save(false)} />
|
||||
|
||||
<WithDropdown let:createDropdown --border-right-radius="5px">
|
||||
<LabelButton
|
||||
on:click={() => dropdown.toggle()}
|
||||
on:mount={(event) => (dropdown = createDropdown(event.detail.button))}
|
||||
/>
|
||||
<DropdownMenu>
|
||||
<WithFloating
|
||||
show={showFloating}
|
||||
closeOnInsideClick
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
>
|
||||
<IconButton
|
||||
slot="reference"
|
||||
widthMultiplier={0.5}
|
||||
iconSize={120}
|
||||
--border-right-radius="5px"
|
||||
on:click={() => (showFloating = !showFloating)}
|
||||
>
|
||||
{@html chevronDown}
|
||||
</IconButton>
|
||||
|
||||
<Popover slot="floating">
|
||||
<DropdownItem on:click={() => dispatch("add")}
|
||||
>{tr.deckConfigAddGroup()}</DropdownItem
|
||||
>
|
||||
|
@ -91,6 +106,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<DropdownItem on:click={() => save(true)}>
|
||||
{tr.deckConfigSaveToAllSubdecks()}
|
||||
</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
</ButtonGroup>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
|
||||
/// <reference types="../lib/image-import" />
|
||||
|
||||
// Import icons from bootstrap
|
||||
export { default as chevronDown } from "@mdi/svg/svg/chevron-down.svg";
|
||||
export { default as revertIcon } from "bootstrap-icons/icons/arrow-counterclockwise.svg";
|
||||
export { default as gearIcon } from "bootstrap-icons/icons/gear.svg";
|
||||
export { default as infoCircle } from "bootstrap-icons/icons/info-circle.svg";
|
||||
|
|
|
@ -84,7 +84,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
src="data:image/svg+xml,{encoded}"
|
||||
class:block
|
||||
class:empty
|
||||
style="--vertical-center: {verticalCenter}px;"
|
||||
style:--vertical-center="{verticalCenter}px"
|
||||
style:--font-size="{fontSize}px"
|
||||
alt="Mathjax"
|
||||
{title}
|
||||
data-anki="mathjax"
|
||||
|
@ -105,9 +106,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
.block {
|
||||
display: block;
|
||||
margin: 1rem auto;
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.empty {
|
||||
vertical-align: sub;
|
||||
vertical-align: text-bottom;
|
||||
|
||||
width: var(--font-size);
|
||||
height: var(--font-size);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -89,7 +89,7 @@ export const Mathjax: DecoratedElementConstructor = class Mathjax
|
|||
break;
|
||||
|
||||
case "data-mathjax":
|
||||
if (!newValue) {
|
||||
if (typeof newValue !== "string") {
|
||||
return;
|
||||
}
|
||||
this.component?.$set({ mathjax: newValue });
|
||||
|
|
|
@ -65,8 +65,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const editor = await editorPromise;
|
||||
setupCodeMirror(editor, code);
|
||||
editor.on("change", () => dispatch("change", editor.getValue()));
|
||||
editor.on("focus", () => dispatch("focus"));
|
||||
editor.on("blur", () => dispatch("blur"));
|
||||
editor.on("focus", (codeMirror, event) =>
|
||||
dispatch("focus", { codeMirror, event }),
|
||||
);
|
||||
editor.on("blur", (codeMirror, event) =>
|
||||
dispatch("blur", { codeMirror, event }),
|
||||
);
|
||||
editor.on("keydown", (codeMirror, event) => {
|
||||
if (event.code === "Tab") {
|
||||
dispatch("tab", { codeMirror, event });
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
|
@ -92,6 +101,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
:global(.CodeMirror) {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
:global(.CodeMirror-wrap pre) {
|
||||
word-break: break-word;
|
||||
}
|
||||
|
|
|
@ -85,18 +85,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
onDestroy(() => api?.destroy());
|
||||
</script>
|
||||
|
||||
<div
|
||||
use:elementResolve
|
||||
class="editor-field"
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:click={() => editingArea.focus?.()}
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
>
|
||||
<slot name="field-label" />
|
||||
<slot name="field-label" />
|
||||
|
||||
<Collapsible {collapsed}>
|
||||
<Collapsible collapse={collapsed} let:collapsed={hidden}>
|
||||
<div
|
||||
use:elementResolve
|
||||
class="editor-field"
|
||||
on:focusin
|
||||
on:focusout
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
{hidden}
|
||||
>
|
||||
<EditingArea
|
||||
{content}
|
||||
fontFamily={field.fontFamily}
|
||||
|
@ -111,8 +111,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<slot name="plain-text-input" />
|
||||
{/if}
|
||||
</EditingArea>
|
||||
</Collapsible>
|
||||
</div>
|
||||
</div>
|
||||
</Collapsible>
|
||||
|
||||
<style lang="scss">
|
||||
.editor-field {
|
||||
|
|
|
@ -40,8 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
opacity: 0.4;
|
||||
pointer-events: none;
|
||||
|
||||
/* stay a on single line */
|
||||
overflow-x: hidden;
|
||||
/* Stay a on single line */
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
|
|
@ -12,16 +12,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
grid-auto-rows: min-content;
|
||||
grid-gap: 6px;
|
||||
|
||||
padding: 0 3px;
|
||||
/* set height to 100% for rich text widgets */
|
||||
height: 100%;
|
||||
|
||||
/* moves the scrollbar inside the editor */
|
||||
overflow-x: hidden;
|
||||
|
||||
> :global(:last-child) {
|
||||
/* bottom padding is eaten by overflow-x */
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
padding: 0 3px 5px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -12,12 +12,5 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow-x: hidden;
|
||||
|
||||
/* replace with "gap: 5px" once it's available
|
||||
- required: Chromium 84 (Qt6 only) and iOS 14.1 */
|
||||
> :global(*) {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,29 +3,22 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
export let tooltip: string | undefined = undefined;
|
||||
|
||||
let background: HTMLDivElement;
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => dispatch("mount", { background }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
bind:this={background}
|
||||
class="handle-background"
|
||||
title={tooltip}
|
||||
on:mousedown|preventDefault
|
||||
on:click|stopPropagation
|
||||
on:dblclick
|
||||
/>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
.handle-background {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
background-color: var(--handle-background-color, #aaa);
|
||||
border-radius: 5px;
|
||||
opacity: 0.2;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -23,7 +23,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<div
|
||||
class="d-contents"
|
||||
class="handle-control"
|
||||
style="--offsetX: {offsetX}px; --offsetY: {offsetY}px; --activeSize: {activeSize}px;"
|
||||
>
|
||||
<div
|
||||
|
@ -66,7 +66,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.d-contents {
|
||||
.handle-control {
|
||||
display: contents;
|
||||
}
|
||||
|
||||
|
|
|
@ -4,23 +4,19 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
-->
|
||||
<script lang="ts">
|
||||
import { getContext } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
import type { Readable } from "svelte/store";
|
||||
|
||||
import { directionKey } from "../lib/context-keys";
|
||||
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
onMount(() => dispatch("mount"));
|
||||
</script>
|
||||
|
||||
<div class="image-handle-dimensions" class:is-rtl={$direction === "rtl"}>
|
||||
<div class="handle-label" class:is-rtl={$direction === "rtl"}>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
.handle-label {
|
||||
position: absolute;
|
||||
width: fit-content;
|
||||
|
||||
|
@ -33,7 +29,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
font-size: 13px;
|
||||
color: white;
|
||||
background-color: rgba(0 0 0 / 0.3);
|
||||
background-color: rgba(0 0 0 / 0.4);
|
||||
border-color: black;
|
||||
border-radius: 5px;
|
||||
padding: 0 5px;
|
||||
|
|
|
@ -1,73 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
export let container: HTMLElement;
|
||||
export let image: HTMLImageElement;
|
||||
|
||||
export let offsetX = 0;
|
||||
export let offsetY = 0;
|
||||
|
||||
let left: number;
|
||||
let top: number;
|
||||
let width: number;
|
||||
let height: number;
|
||||
|
||||
function setSelection(): void {
|
||||
const containerRect = container.getBoundingClientRect();
|
||||
const imageRect = image!.getBoundingClientRect();
|
||||
|
||||
const containerLeft = containerRect.left;
|
||||
const containerTop = containerRect.top;
|
||||
|
||||
left = imageRect!.left - containerLeft;
|
||||
top = imageRect!.top - containerTop;
|
||||
width = image!.clientWidth;
|
||||
height = image!.clientHeight;
|
||||
}
|
||||
|
||||
export function updateSelection(): Promise<void> {
|
||||
let updateResolve: () => void;
|
||||
const afterUpdate: Promise<void> = new Promise((resolve) => {
|
||||
updateResolve = resolve;
|
||||
});
|
||||
|
||||
setSelection();
|
||||
setTimeout(() => updateResolve());
|
||||
|
||||
return afterUpdate;
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let selectionRef: HTMLDivElement;
|
||||
function initSelection(selection: HTMLDivElement): void {
|
||||
setSelection();
|
||||
selectionRef = selection;
|
||||
}
|
||||
|
||||
onMount(() => dispatch("mount", { selection: selectionRef }));
|
||||
</script>
|
||||
|
||||
<div
|
||||
use:initSelection
|
||||
on:click={(event) =>
|
||||
/* prevent triggering Bootstrap dropdown */ event.stopImmediatePropagation()}
|
||||
style="--left: {left}px; --top: {top}px; --width: {width}px; --height: {height}px; --offsetX: {offsetX}px; --offsetY: {offsetY}px;"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
div {
|
||||
position: absolute;
|
||||
|
||||
left: calc(var(--left, 0px) - var(--offsetX, 0px));
|
||||
top: calc(var(--top, 0px) - var(--offsetY, 0px));
|
||||
width: calc(var(--width) + 2 * var(--offsetX, 0px));
|
||||
height: calc(var(--height) + 2 * var(--offsetY, 0px));
|
||||
}
|
||||
</style>
|
|
@ -46,7 +46,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
to cover field borders on scroll */
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
z-index: 3;
|
||||
z-index: 10;
|
||||
background: var(--label-color);
|
||||
|
||||
.clickable {
|
||||
|
|
|
@ -434,7 +434,10 @@ the AddCards dialog) should be implemented in the user of this component.
|
|||
</LabelContainer>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="rich-text-input">
|
||||
<Collapsible collapsed={richTextsHidden[index]} let:hidden>
|
||||
<Collapsible
|
||||
collapse={richTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<RichTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
|
@ -452,7 +455,10 @@ the AddCards dialog) should be implemented in the user of this component.
|
|||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="plain-text-input">
|
||||
<Collapsible collapsed={plainTextsHidden[index]} let:hidden>
|
||||
<Collapsible
|
||||
collapse={plainTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<PlainTextInput
|
||||
{hidden}
|
||||
isDefault={plainTextDefaults[index]}
|
||||
|
|
|
@ -3,17 +3,18 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonGroupItem, {
|
||||
createProps,
|
||||
setSlotHostContext,
|
||||
updatePropsList,
|
||||
} from "../../components/ButtonGroupItem.svelte";
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||
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 { execCommand } from "../../domlib";
|
||||
import { getListItem } from "../../lib/dom";
|
||||
import * as tr from "../../lib/ftl";
|
||||
|
@ -54,7 +55,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
const { focusedInput } = context.get();
|
||||
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
|
||||
let showFloating = false;
|
||||
</script>
|
||||
|
||||
<ButtonGroup>
|
||||
|
@ -82,80 +86,95 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</ButtonGroupItem>
|
||||
|
||||
<ButtonGroupItem>
|
||||
<WithDropdown let:createDropdown>
|
||||
<IconButton
|
||||
{disabled}
|
||||
on:mount={(event) => createDropdown(event.detail.button)}
|
||||
>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
<WithFloating
|
||||
show={showFloating && !disabled}
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
let:asReference
|
||||
>
|
||||
<span class="block-buttons" use:asReference>
|
||||
<IconButton
|
||||
{disabled}
|
||||
on:click={() => (showFloating = !showFloating)}
|
||||
>
|
||||
{@html listOptionsIcon}
|
||||
</IconButton>
|
||||
</span>
|
||||
|
||||
<ButtonDropdown>
|
||||
<ButtonGroup>
|
||||
<CommandIconButton
|
||||
key="justifyLeft"
|
||||
tooltip={tr.editingAlignLeft()}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="0px"
|
||||
>{@html justifyLeftIcon}</CommandIconButton
|
||||
>
|
||||
<Popover slot="floating">
|
||||
<ButtonToolbar wrap={false}>
|
||||
<ButtonGroup>
|
||||
<CommandIconButton
|
||||
key="justifyLeft"
|
||||
tooltip={tr.editingAlignLeft()}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="0px"
|
||||
>{@html justifyLeftIcon}</CommandIconButton
|
||||
>
|
||||
|
||||
<CommandIconButton
|
||||
key="justifyCenter"
|
||||
tooltip={tr.editingCenter()}
|
||||
>{@html justifyCenterIcon}</CommandIconButton
|
||||
>
|
||||
<CommandIconButton
|
||||
key="justifyCenter"
|
||||
tooltip={tr.editingCenter()}
|
||||
>{@html justifyCenterIcon}</CommandIconButton
|
||||
>
|
||||
|
||||
<CommandIconButton
|
||||
key="justifyRight"
|
||||
tooltip={tr.editingAlignRight()}
|
||||
>{@html justifyRightIcon}</CommandIconButton
|
||||
>
|
||||
<CommandIconButton
|
||||
key="justifyRight"
|
||||
tooltip={tr.editingAlignRight()}
|
||||
>{@html justifyRightIcon}</CommandIconButton
|
||||
>
|
||||
|
||||
<CommandIconButton
|
||||
key="justifyFull"
|
||||
tooltip={tr.editingJustify()}
|
||||
--border-right-radius="5px"
|
||||
>{@html justifyFullIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroup>
|
||||
<CommandIconButton
|
||||
key="justifyFull"
|
||||
tooltip={tr.editingJustify()}
|
||||
--border-right-radius="5px"
|
||||
>{@html justifyFullIcon}</CommandIconButton
|
||||
>
|
||||
</ButtonGroup>
|
||||
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
tooltip="{tr.editingOutdent()} ({getPlatformString(
|
||||
outdentKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={outdentListItem}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="0px"
|
||||
>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
<ButtonGroup>
|
||||
<IconButton
|
||||
tooltip="{tr.editingOutdent()} ({getPlatformString(
|
||||
outdentKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={outdentListItem}
|
||||
--border-left-radius="5px"
|
||||
--border-right-radius="0px"
|
||||
>
|
||||
{@html outdentIcon}
|
||||
</IconButton>
|
||||
|
||||
<Shortcut
|
||||
keyCombination={outdentKeyCombination}
|
||||
on:action={outdentListItem}
|
||||
/>
|
||||
<Shortcut
|
||||
keyCombination={outdentKeyCombination}
|
||||
on:action={outdentListItem}
|
||||
/>
|
||||
|
||||
<IconButton
|
||||
tooltip="{tr.editingIndent()} ({getPlatformString(
|
||||
indentKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={indentListItem}
|
||||
--border-right-radius="5px"
|
||||
>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
<IconButton
|
||||
tooltip="{tr.editingIndent()} ({getPlatformString(
|
||||
indentKeyCombination,
|
||||
)})"
|
||||
{disabled}
|
||||
on:click={indentListItem}
|
||||
--border-right-radius="5px"
|
||||
>
|
||||
{@html indentIcon}
|
||||
</IconButton>
|
||||
|
||||
<Shortcut
|
||||
keyCombination={indentKeyCombination}
|
||||
on:action={indentListItem}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</ButtonDropdown>
|
||||
</WithDropdown>
|
||||
<Shortcut
|
||||
keyCombination={indentKeyCombination}
|
||||
on:action={indentListItem}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
</ButtonToolbar>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
</ButtonGroupItem>
|
||||
</DynamicallySlottable>
|
||||
</ButtonGroup>
|
||||
|
||||
<style lang="scss">
|
||||
.block-buttons {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -11,10 +11,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import * as tr from "../../lib/ftl";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { chevronDown } from "../icons";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { arrowIcon, highlightColorIcon } from "./icons";
|
||||
import { highlightColorIcon } from "./icons";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
|
||||
export let color: string;
|
||||
|
@ -121,9 +122,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
tooltip={tr.editingChangeColor()}
|
||||
{disabled}
|
||||
widthMultiplier={0.5}
|
||||
iconSize={120}
|
||||
--border-right-radius="5px"
|
||||
>
|
||||
{@html arrowIcon}
|
||||
{@html chevronDown}
|
||||
<ColorPicker
|
||||
on:input={(event) => {
|
||||
color = setColor(event);
|
||||
|
|
|
@ -3,8 +3,6 @@ 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 IconButton from "../../components/IconButton.svelte";
|
||||
import Popover from "../../components/Popover.svelte";
|
||||
|
@ -70,22 +68,18 @@ 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;
|
||||
}
|
||||
let showFloating = false;
|
||||
</script>
|
||||
|
||||
<WithFloating show={showDropdown} closeOnInsideClick>
|
||||
<span
|
||||
class="latex-button"
|
||||
slot="reference"
|
||||
let:asReference
|
||||
use:asReference
|
||||
let:toggle
|
||||
>
|
||||
<IconButton slot="reference" {disabled} on:click={toggle}>
|
||||
<WithFloating
|
||||
show={showFloating && !disabled}
|
||||
closeOnInsideClick
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
let:asReference
|
||||
>
|
||||
<span class="latex-button" use:asReference>
|
||||
<IconButton {disabled} on:click={() => (showFloating = !showFloating)}>
|
||||
{@html functionIcon}
|
||||
</IconButton>
|
||||
</span>
|
||||
|
@ -93,25 +87,29 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<Popover slot="floating">
|
||||
{#each dropdownItems as [callback, keyCombination, label]}
|
||||
<DropdownItem on:click={callback}>
|
||||
{label}
|
||||
<span>{label}</span>
|
||||
<span class="ms-auto ps-2 shortcut"
|
||||
>{getPlatformString(keyCombination)}</span
|
||||
>
|
||||
</DropdownItem>
|
||||
<Shortcut {keyCombination} on:action={callback} />
|
||||
{/each}
|
||||
|
||||
<DropdownItem on:click={toggleShowMathjax}>
|
||||
<span>{tr.editingToggleMathjaxRendering()}</span>
|
||||
</DropdownItem>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
.latex-button {
|
||||
line-height: 1;
|
||||
}
|
||||
{#each dropdownItems as [callback, keyCombination]}
|
||||
<Shortcut {keyCombination} on:action={callback} />
|
||||
{/each}
|
||||
|
||||
<style lang="scss">
|
||||
.shortcut {
|
||||
font: Verdana;
|
||||
}
|
||||
|
||||
.latex-button {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -7,21 +7,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import CheckBox from "../../components/CheckBox.svelte";
|
||||
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 type { MatchType } from "../../domlib/surround";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { altPressed, shiftPressed } from "../../lib/keys";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { chevronDown } from "../icons";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { eraserIcon } from "./icons";
|
||||
import { arrowIcon } from "./icons";
|
||||
|
||||
const { removeFormats } = editorToolbarContext.get();
|
||||
|
||||
|
@ -62,6 +61,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const keyCombination = "Control+R";
|
||||
|
||||
let disabled: boolean;
|
||||
let showFloating = false;
|
||||
|
||||
onMount(() => {
|
||||
const surroundElement = document.createElement("span");
|
||||
|
@ -114,34 +114,37 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<Shortcut {keyCombination} on:action={remove} />
|
||||
|
||||
<div class="hide-after">
|
||||
<WithDropdown autoClose="outside" let:createDropdown --border-right-radius="5px">
|
||||
<WithFloating
|
||||
show={showFloating && !disabled}
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
let:asReference
|
||||
>
|
||||
<span use:asReference class="remove-format-button">
|
||||
<IconButton
|
||||
tooltip={tr.editingSelectRemoveFormatting()}
|
||||
{disabled}
|
||||
widthMultiplier={0.5}
|
||||
on:mount={withButton(createDropdown)}
|
||||
iconSize={120}
|
||||
--border-right-radius="5px"
|
||||
on:click={() => (showFloating = !showFloating)}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
{@html chevronDown}
|
||||
</IconButton>
|
||||
</span>
|
||||
|
||||
<DropdownMenu on:mousedown={(event) => event.preventDefault()}>
|
||||
{#each showFormats as format (format.name)}
|
||||
<DropdownItem on:click={(event) => onItemClick(event, format)}>
|
||||
<CheckBox bind:value={format.active} />
|
||||
<span class="d-flex-inline ps-3">{format.name}</span>
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</DropdownMenu>
|
||||
</WithDropdown>
|
||||
</div>
|
||||
<Popover slot="floating">
|
||||
{#each showFormats as format (format.name)}
|
||||
<DropdownItem on:click={(event) => onItemClick(event, format)}>
|
||||
<CheckBox bind:value={format.active} />
|
||||
<span class="d-flex-inline ps-3">{format.name}</span>
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<style lang="scss">
|
||||
.hide-after {
|
||||
display: contents;
|
||||
|
||||
:global(.dropdown-toggle::after) {
|
||||
display: none;
|
||||
}
|
||||
.remove-format-button {
|
||||
line-height: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -14,10 +14,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { withFontColor } from "../helpers";
|
||||
import { chevronDown } from "../icons";
|
||||
import { surrounder } from "../rich-text-input";
|
||||
import ColorPicker from "./ColorPicker.svelte";
|
||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||
import { arrowIcon, textColorIcon } from "./icons";
|
||||
import { textColorIcon } from "./icons";
|
||||
import WithColorHelper from "./WithColorHelper.svelte";
|
||||
|
||||
export let color: string;
|
||||
|
@ -140,8 +141,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
tooltip="{tr.editingChangeColor()} ({getPlatformString(pickCombination)})"
|
||||
{disabled}
|
||||
widthMultiplier={0.5}
|
||||
iconSize={120}
|
||||
>
|
||||
{@html arrowIcon}
|
||||
{@html chevronDown}
|
||||
<ColorPicker
|
||||
keyCombination={pickCombination}
|
||||
on:input={(event) => {
|
||||
|
|
|
@ -8,10 +8,13 @@ export { default as highlightColorIcon } from "@mdi/svg/svg/format-color-highlig
|
|||
export { default as textColorIcon } from "@mdi/svg/svg/format-color-text.svg";
|
||||
export { default as subscriptIcon } from "@mdi/svg/svg/format-subscript.svg";
|
||||
export { default as superscriptIcon } from "@mdi/svg/svg/format-superscript.svg";
|
||||
export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg";
|
||||
export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg";
|
||||
export { default as eraserIcon } from "bootstrap-icons/icons/eraser.svg";
|
||||
export { default as justifyFullIcon } from "bootstrap-icons/icons/justify.svg";
|
||||
export { default as olIcon } from "bootstrap-icons/icons/list-ol.svg";
|
||||
export { default as ulIcon } from "bootstrap-icons/icons/list-ul.svg";
|
||||
export { default as micIcon } from "bootstrap-icons/icons/mic.svg";
|
||||
export { default as justifyCenterIcon } from "bootstrap-icons/icons/text-center.svg";
|
||||
export { default as indentIcon } from "bootstrap-icons/icons/text-indent-left.svg";
|
||||
export { default as outdentIcon } from "bootstrap-icons/icons/text-indent-right.svg";
|
||||
|
@ -21,9 +24,3 @@ export { default as justifyRightIcon } from "bootstrap-icons/icons/text-right.sv
|
|||
export { default as boldIcon } from "bootstrap-icons/icons/type-bold.svg";
|
||||
export { default as italicIcon } from "bootstrap-icons/icons/type-italic.svg";
|
||||
export { default as underlineIcon } from "bootstrap-icons/icons/type-underline.svg";
|
||||
export const arrowIcon =
|
||||
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="transparent" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2 5l6 6 6-6"/></svg>';
|
||||
|
||||
export { default as functionIcon } from "@mdi/svg/svg/function-variant.svg";
|
||||
export { default as paperclipIcon } from "@mdi/svg/svg/paperclip.svg";
|
||||
export { default as micIcon } from "bootstrap-icons/icons/mic.svg";
|
||||
|
|
|
@ -11,10 +11,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import IconButton from "../../components/IconButton.svelte";
|
||||
import { directionKey } from "../../lib/context-keys";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import { floatLeftIcon, floatNoneIcon, floatRightIcon } from "./icons";
|
||||
|
||||
export let image: HTMLImageElement;
|
||||
|
||||
$: floatStyle = getComputedStyle(image).float;
|
||||
|
||||
const direction = getContext<Readable<"ltr" | "rtl">>(directionKey);
|
||||
const [inlineStartIcon, inlineEndIcon] =
|
||||
$direction === "ltr"
|
||||
|
@ -27,7 +30,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<ButtonGroup size={1.6} wrap={false}>
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatLeft()}
|
||||
active={image.style.float === "left"}
|
||||
active={floatStyle === "left"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "left";
|
||||
|
@ -38,22 +41,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatNone()}
|
||||
active={image.style.float === "" || image.style.float === "none"}
|
||||
active={floatStyle === "none"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.removeProperty("float");
|
||||
|
||||
if (image.getAttribute("style")?.length === 0) {
|
||||
image.removeAttribute("style");
|
||||
}
|
||||
|
||||
// We shortly set to none, because simply unsetting float will not
|
||||
// trigger floatStyle being reset
|
||||
image.style.float = "none";
|
||||
removeStyleProperties(image, "float");
|
||||
setTimeout(() => dispatch("update"));
|
||||
}}>{@html floatNoneIcon}</IconButton
|
||||
>
|
||||
|
||||
<IconButton
|
||||
tooltip={tr.editingFloatRight()}
|
||||
active={image.style.float === "right"}
|
||||
active={floatStyle === "right"}
|
||||
flipX={$direction === "rtl"}
|
||||
on:click={() => {
|
||||
image.style.float = "right";
|
||||
|
|
|
@ -5,15 +5,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="ts">
|
||||
import { onMount, tick } from "svelte";
|
||||
|
||||
import ButtonDropdown from "../../components/ButtonDropdown.svelte";
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import Popover from "../../components/Popover.svelte";
|
||||
import WithFloating from "../../components/WithFloating.svelte";
|
||||
import WithOverlay from "../../components/WithOverlay.svelte";
|
||||
import { on } from "../../lib/events";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleControl from "../HandleControl.svelte";
|
||||
import HandleLabel from "../HandleLabel.svelte";
|
||||
import HandleSelection from "../HandleSelection.svelte";
|
||||
import { context } from "../rich-text-input";
|
||||
import FloatButtons from "./FloatButtons.svelte";
|
||||
import SizeSelect from "./SizeSelect.svelte";
|
||||
|
@ -45,8 +46,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
async function maybeShowHandle(event: Event): Promise<void> {
|
||||
await resetHandle();
|
||||
|
||||
if (event.target instanceof HTMLImageElement) {
|
||||
const image = event.target;
|
||||
|
||||
|
@ -176,12 +175,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
* image.[dimension], there would be no visible effect on the image.
|
||||
* To avoid confusion with users we'll clear image.style.[dimension] (for now).
|
||||
*/
|
||||
activeImage!.style.removeProperty("width");
|
||||
activeImage!.style.removeProperty("height");
|
||||
if (activeImage!.getAttribute("style")?.length === 0) {
|
||||
activeImage!.removeAttribute("style");
|
||||
}
|
||||
|
||||
removeStyleProperties(activeImage!, "width", "height");
|
||||
activeImage!.width = width;
|
||||
}
|
||||
|
||||
|
@ -205,12 +199,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
|
||||
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
|
||||
|
||||
return singleCallback(
|
||||
on(container, "click", maybeShowHandle),
|
||||
on(container, "blur", resetHandle),
|
||||
on(container, "key" as any, resetHandle),
|
||||
on(container, "paste", resetHandle),
|
||||
);
|
||||
return on(container, "click", maybeShowHandle);
|
||||
});
|
||||
|
||||
let shrinkingDisabled: boolean;
|
||||
|
@ -230,36 +219,73 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
attributeFilter: ["width"],
|
||||
})
|
||||
: widthObserver.disconnect();
|
||||
|
||||
let imageOverlay: HTMLElement;
|
||||
</script>
|
||||
|
||||
<WithDropdown
|
||||
drop="down"
|
||||
autoOpen={true}
|
||||
autoClose={false}
|
||||
distance={3}
|
||||
let:createDropdown
|
||||
let:dropdownObject
|
||||
>
|
||||
<div bind:this={imageOverlay} class="image-overlay">
|
||||
{#if activeImage}
|
||||
{#await element then container}
|
||||
<HandleSelection
|
||||
bind:updateSelection
|
||||
{container}
|
||||
image={activeImage}
|
||||
on:mount={(event) => createDropdown(event.detail.selection)}
|
||||
<WithOverlay reference={activeImage} inline let:position={positionOverlay}>
|
||||
<WithFloating
|
||||
reference={activeImage}
|
||||
placement="auto"
|
||||
offset={20}
|
||||
inline
|
||||
hideIfReferenceHidden
|
||||
let:position={positionFloating}
|
||||
on:close={async ({ detail }) => {
|
||||
const { reason, originalEvent } = detail;
|
||||
|
||||
if (reason === "outsideClick") {
|
||||
// If the click is still in the overlay, we do not want
|
||||
// to reset the handle either
|
||||
if (!originalEvent.path.includes(imageOverlay)) {
|
||||
await resetHandle();
|
||||
}
|
||||
} else {
|
||||
await resetHandle();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<Popover slot="floating">
|
||||
<ButtonToolbar>
|
||||
<FloatButtons
|
||||
image={activeImage}
|
||||
on:update={async () => {
|
||||
positionOverlay();
|
||||
positionFloating();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SizeSelect
|
||||
{shrinkingDisabled}
|
||||
{restoringDisabled}
|
||||
{isSizeConstrained}
|
||||
on:imagetoggle={() => {
|
||||
toggleActualSize();
|
||||
positionOverlay();
|
||||
}}
|
||||
on:imageclear={() => {
|
||||
clearActualSize();
|
||||
positionOverlay();
|
||||
}}
|
||||
/>
|
||||
</ButtonToolbar>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<svelte:fragment slot="overlay">
|
||||
<HandleBackground
|
||||
on:dblclick={() => {
|
||||
if (shrinkingDisabled) {
|
||||
return;
|
||||
}
|
||||
toggleActualSize();
|
||||
updateSizesWithDimensions();
|
||||
dropdownObject.update();
|
||||
positionOverlay();
|
||||
}}
|
||||
/>
|
||||
|
||||
<HandleLabel on:mount={updateDimensions}>
|
||||
<HandleLabel>
|
||||
{#if isSizeConstrained}
|
||||
<span>{tr.editingDoubleClickToExpand()}</span>
|
||||
{:else}
|
||||
|
@ -283,30 +309,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}}
|
||||
on:pointermove={(event) => {
|
||||
resize(event);
|
||||
updateSizesWithDimensions();
|
||||
dropdownObject.update();
|
||||
}}
|
||||
/>
|
||||
</HandleSelection>
|
||||
{/await}
|
||||
|
||||
<ButtonDropdown on:click={updateSizesWithDimensions}>
|
||||
<FloatButtons image={activeImage} on:update={dropdownObject.update} />
|
||||
<SizeSelect
|
||||
{shrinkingDisabled}
|
||||
{restoringDisabled}
|
||||
{isSizeConstrained}
|
||||
on:imagetoggle={() => {
|
||||
toggleActualSize();
|
||||
updateSizesWithDimensions();
|
||||
dropdownObject.update();
|
||||
}}
|
||||
on:imageclear={() => {
|
||||
clearActualSize();
|
||||
updateSizesWithDimensions();
|
||||
dropdownObject.update();
|
||||
}}
|
||||
/>
|
||||
</ButtonDropdown>
|
||||
</svelte:fragment>
|
||||
</WithOverlay>
|
||||
{/if}
|
||||
</WithDropdown>
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import ImageHandle from "./ImageHandle.svelte";
|
||||
import ImageOverlay from "./ImageOverlay.svelte";
|
||||
|
||||
export default ImageHandle;
|
||||
export default ImageOverlay;
|
||||
|
|
|
@ -8,18 +8,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ButtonGroup from "../../components/ButtonGroup.svelte";
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import IconButton from "../../components/IconButton.svelte";
|
||||
import { hasBlockAttribute } from "../../lib/dom";
|
||||
import * as tr from "../../lib/ftl";
|
||||
import ClozeButtons from "../ClozeButtons.svelte";
|
||||
import { blockIcon, deleteIcon, inlineIcon } from "./icons";
|
||||
|
||||
export let element: Element;
|
||||
|
||||
$: isBlock = hasBlockAttribute(element);
|
||||
|
||||
function updateBlock() {
|
||||
element.setAttribute("block", String(isBlock));
|
||||
}
|
||||
export let isBlock: boolean;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
@ -29,24 +22,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<IconButton
|
||||
tooltip={tr.editingMathjaxInline()}
|
||||
active={!isBlock}
|
||||
on:click={() => {
|
||||
isBlock = false;
|
||||
updateBlock();
|
||||
}}
|
||||
on:click
|
||||
--border-left-radius="5px">{@html inlineIcon}</IconButton
|
||||
on:click={() => dispatch("setinline")}
|
||||
--border-left-radius="5px"
|
||||
>
|
||||
{@html inlineIcon}
|
||||
</IconButton>
|
||||
|
||||
<IconButton
|
||||
tooltip={tr.editingMathjaxBlock()}
|
||||
active={isBlock}
|
||||
on:click={() => {
|
||||
isBlock = true;
|
||||
updateBlock();
|
||||
}}
|
||||
on:click
|
||||
--border-right-radius="5px">{@html blockIcon}</IconButton
|
||||
on:click={() => dispatch("setblock")}
|
||||
--border-right-radius="5px"
|
||||
>
|
||||
{@html blockIcon}
|
||||
</IconButton>
|
||||
</ButtonGroup>
|
||||
|
||||
<ClozeButtons on:surround />
|
||||
|
|
|
@ -10,6 +10,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import * as tr from "../../lib/ftl";
|
||||
import { noop } from "../../lib/functional";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { pageTheme } from "../../sveltelib/theme";
|
||||
import { baseOptions, focusAndSetCaret, latex } from "../code-mirror";
|
||||
import type { CodeMirrorAPI } from "../CodeMirror.svelte";
|
||||
import CodeMirror from "../CodeMirror.svelte";
|
||||
|
@ -44,12 +45,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
onMount(async () => {
|
||||
const editor = await codeMirror.editor;
|
||||
|
||||
focusAndSetCaret(editor, position);
|
||||
|
||||
if (selectAll) {
|
||||
editor.execCommand("selectAll");
|
||||
}
|
||||
|
||||
let direction: "start" | "end" | undefined = undefined;
|
||||
|
||||
editor.on(
|
||||
|
@ -86,16 +81,24 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
direction = undefined;
|
||||
},
|
||||
);
|
||||
|
||||
setTimeout(() => {
|
||||
focusAndSetCaret(editor, position);
|
||||
|
||||
if (selectAll) {
|
||||
editor.execCommand("selectAll");
|
||||
}
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
<div class="mathjax-editor">
|
||||
<div class="mathjax-editor" class:light-theme={!$pageTheme.isDark}>
|
||||
<CodeMirror
|
||||
{code}
|
||||
{configuration}
|
||||
bind:api={codeMirror}
|
||||
on:change={({ detail: mathjaxText }) => code.set(mathjaxText)}
|
||||
on:blur
|
||||
on:tab
|
||||
/>
|
||||
</div>
|
||||
|
||||
|
@ -103,12 +106,21 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<style lang="scss">
|
||||
.mathjax-editor {
|
||||
margin: 0 1px;
|
||||
overflow: hidden;
|
||||
|
||||
:global(.CodeMirror) {
|
||||
max-width: 28rem;
|
||||
min-width: 14rem;
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
&.light-theme :global(.CodeMirror) {
|
||||
border-width: 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
:global(.CodeMirror-placeholder) {
|
||||
font-family: sans-serif;
|
||||
font-size: 55%;
|
||||
|
|
|
@ -1,186 +0,0 @@
|
|||
<!--
|
||||
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 CodeMirrorLib from "codemirror";
|
||||
import { onDestroy, onMount, tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import WithDropdown from "../../components/WithDropdown.svelte";
|
||||
import { escapeSomeEntities, unescapeSomeEntities } from "../../editable/mathjax";
|
||||
import { Mathjax } from "../../editable/mathjax-element";
|
||||
import { on } from "../../lib/events";
|
||||
import { noop } from "../../lib/functional";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleControl from "../HandleControl.svelte";
|
||||
import HandleSelection from "../HandleSelection.svelte";
|
||||
import { context } from "../rich-text-input";
|
||||
import MathjaxMenu from "./MathjaxMenu.svelte";
|
||||
|
||||
const { editable, element, preventResubscription } = context.get();
|
||||
|
||||
let activeImage: HTMLImageElement | null = null;
|
||||
let mathjaxElement: HTMLElement | null = null;
|
||||
let allow = noop;
|
||||
let unsubscribe = noop;
|
||||
|
||||
let selectAll = false;
|
||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Will contain the Mathjax text with unescaped entities.
|
||||
* This is the text displayed in the actual editor window.
|
||||
*/
|
||||
const code = writable("");
|
||||
|
||||
function showHandle(image: HTMLImageElement, pos?: CodeMirrorLib.Position): void {
|
||||
allow = preventResubscription();
|
||||
position = pos;
|
||||
|
||||
/* Setting the activeImage and mathjaxElement to a non-nullish value is
|
||||
* what triggers the Mathjax editor to show */
|
||||
activeImage = image;
|
||||
mathjaxElement = activeImage.closest(Mathjax.tagName)!;
|
||||
|
||||
code.set(unescapeSomeEntities(mathjaxElement.dataset.mathjax ?? ""));
|
||||
unsubscribe = code.subscribe((value: string) => {
|
||||
mathjaxElement!.dataset.mathjax = escapeSomeEntities(value);
|
||||
});
|
||||
}
|
||||
|
||||
function placeHandle(after: boolean): void {
|
||||
editable.focusHandler.flushCaret();
|
||||
|
||||
if (after) {
|
||||
(mathjaxElement as any).placeCaretAfter();
|
||||
} else {
|
||||
(mathjaxElement as any).placeCaretBefore();
|
||||
}
|
||||
}
|
||||
|
||||
async function resetHandle(): Promise<void> {
|
||||
selectAll = false;
|
||||
position = undefined;
|
||||
|
||||
if (activeImage && mathjaxElement) {
|
||||
unsubscribe();
|
||||
activeImage = null;
|
||||
mathjaxElement = null;
|
||||
}
|
||||
|
||||
await tick();
|
||||
allow();
|
||||
}
|
||||
|
||||
async function maybeShowHandle({ target }: Event): Promise<void> {
|
||||
await resetHandle();
|
||||
|
||||
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
||||
showHandle(target);
|
||||
}
|
||||
}
|
||||
|
||||
async function showAutofocusHandle({
|
||||
detail,
|
||||
}: CustomEvent<{
|
||||
image: HTMLImageElement;
|
||||
position?: [number, number];
|
||||
}>): Promise<void> {
|
||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||
|
||||
await resetHandle();
|
||||
|
||||
if (detail.position) {
|
||||
const [line, ch] = detail.position;
|
||||
position = { line, ch };
|
||||
}
|
||||
|
||||
showHandle(detail.image, position);
|
||||
}
|
||||
|
||||
async function showSelectAll({
|
||||
detail,
|
||||
}: CustomEvent<HTMLImageElement>): Promise<void> {
|
||||
await resetHandle();
|
||||
selectAll = true;
|
||||
showHandle(detail);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const container = await element;
|
||||
|
||||
return singleCallback(
|
||||
on(container, "click", maybeShowHandle),
|
||||
on(container, "movecaretafter" as any, showAutofocusHandle),
|
||||
on(container, "selectall" as any, showSelectAll),
|
||||
);
|
||||
});
|
||||
|
||||
let updateSelection: () => Promise<void>;
|
||||
let errorMessage: string;
|
||||
let dropdownApi: any;
|
||||
|
||||
async function onImageResize(): Promise<void> {
|
||||
errorMessage = activeImage!.title;
|
||||
await updateSelection();
|
||||
dropdownApi.update();
|
||||
}
|
||||
|
||||
const resizeObserver = new ResizeObserver(onImageResize);
|
||||
|
||||
let clearResize = noop;
|
||||
async function handleImageResizing(activeImage: HTMLImageElement | null) {
|
||||
const container = await element;
|
||||
|
||||
if (activeImage) {
|
||||
resizeObserver.observe(container);
|
||||
clearResize = on(activeImage, "resize", onImageResize);
|
||||
} else {
|
||||
resizeObserver.unobserve(container);
|
||||
clearResize();
|
||||
}
|
||||
}
|
||||
|
||||
$: handleImageResizing(activeImage);
|
||||
|
||||
onDestroy(() => {
|
||||
resizeObserver.disconnect();
|
||||
clearResize();
|
||||
});
|
||||
</script>
|
||||
|
||||
<WithDropdown drop="down" autoOpen autoClose={false} distance={4} let:createDropdown>
|
||||
{#if activeImage && mathjaxElement}
|
||||
<MathjaxMenu
|
||||
element={mathjaxElement}
|
||||
{code}
|
||||
{selectAll}
|
||||
{position}
|
||||
bind:updateSelection
|
||||
on:reset={resetHandle}
|
||||
on:moveoutstart={() => {
|
||||
placeHandle(false);
|
||||
resetHandle();
|
||||
}}
|
||||
on:moveoutend={() => {
|
||||
placeHandle(true);
|
||||
resetHandle();
|
||||
}}
|
||||
>
|
||||
{#await element then container}
|
||||
<HandleSelection
|
||||
image={activeImage}
|
||||
{container}
|
||||
bind:updateSelection
|
||||
on:mount={(event) =>
|
||||
(dropdownApi = createDropdown(event.detail.selection))}
|
||||
>
|
||||
<HandleBackground tooltip={errorMessage} />
|
||||
<HandleControl offsetX={1} offsetY={1} />
|
||||
</HandleSelection>
|
||||
{/await}
|
||||
</MathjaxMenu>
|
||||
{/if}
|
||||
</WithDropdown>
|
|
@ -1,91 +0,0 @@
|
|||
<!--
|
||||
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 CodeMirrorLib from "codemirror";
|
||||
import { createEventDispatcher } from "svelte";
|
||||
import type { Writable } from "svelte/store";
|
||||
|
||||
import DropdownMenu from "../../components/DropdownMenu.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import { placeCaretAfter } from "../../domlib/place-caret";
|
||||
import { pageTheme } from "../../sveltelib/theme";
|
||||
import MathjaxButtons from "./MathjaxButtons.svelte";
|
||||
import MathjaxEditor from "./MathjaxEditor.svelte";
|
||||
|
||||
export let element: Element;
|
||||
export let code: Writable<string>;
|
||||
|
||||
export let selectAll: boolean;
|
||||
export let position: CodeMirrorLib.Position | undefined;
|
||||
|
||||
const acceptShortcut = "Enter";
|
||||
const newlineShortcut = "Shift+Enter";
|
||||
|
||||
export let updateSelection: () => Promise<void>;
|
||||
let dropdownApi: any;
|
||||
|
||||
export async function update() {
|
||||
await updateSelection?.();
|
||||
dropdownApi.update();
|
||||
}
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
</script>
|
||||
|
||||
<div class="mathjax-menu" class:light-theme={!$pageTheme.isDark}>
|
||||
<slot />
|
||||
|
||||
<DropdownMenu>
|
||||
<MathjaxEditor
|
||||
{acceptShortcut}
|
||||
{newlineShortcut}
|
||||
{code}
|
||||
{selectAll}
|
||||
{position}
|
||||
on:blur={() => dispatch("reset")}
|
||||
on:moveoutstart
|
||||
on:moveoutend
|
||||
let:editor={mathjaxEditor}
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={acceptShortcut}
|
||||
on:action={() => dispatch("moveoutend")}
|
||||
/>
|
||||
|
||||
<MathjaxButtons
|
||||
{element}
|
||||
on:delete={() => {
|
||||
placeCaretAfter(element);
|
||||
element.remove();
|
||||
dispatch("reset");
|
||||
}}
|
||||
on:surround={async ({ detail }) => {
|
||||
const editor = await mathjaxEditor.editor;
|
||||
const { prefix, suffix } = detail;
|
||||
|
||||
editor.replaceSelection(prefix + editor.getSelection() + suffix);
|
||||
}}
|
||||
/>
|
||||
</MathjaxEditor>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.mathjax-menu :global(.dropdown-menu) {
|
||||
border-color: var(--border);
|
||||
}
|
||||
|
||||
.light-theme {
|
||||
:global(.dropdown-menu) {
|
||||
background-color: var(--window-bg);
|
||||
}
|
||||
|
||||
:global(.CodeMirror) {
|
||||
border-width: 1px 0;
|
||||
border-style: solid;
|
||||
border-color: var(--border);
|
||||
}
|
||||
}
|
||||
</style>
|
244
ts/editor/mathjax-overlay/MathjaxOverlay.svelte
Normal file
244
ts/editor/mathjax-overlay/MathjaxOverlay.svelte
Normal file
|
@ -0,0 +1,244 @@
|
|||
<!--
|
||||
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 CodeMirrorLib from "codemirror";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import Popover from "../../components/Popover.svelte";
|
||||
import Shortcut from "../../components/Shortcut.svelte";
|
||||
import WithFloating from "../../components/WithFloating.svelte";
|
||||
import WithOverlay from "../../components/WithOverlay.svelte";
|
||||
import { placeCaretAfter } from "../../domlib/place-caret";
|
||||
import { escapeSomeEntities, unescapeSomeEntities } from "../../editable/mathjax";
|
||||
import { Mathjax } from "../../editable/mathjax-element";
|
||||
import { hasBlockAttribute } from "../../lib/dom";
|
||||
import { on } from "../../lib/events";
|
||||
import { noop } from "../../lib/functional";
|
||||
import type { Callback } from "../../lib/typing";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import { context } from "../rich-text-input";
|
||||
import MathjaxButtons from "./MathjaxButtons.svelte";
|
||||
import MathjaxEditor from "./MathjaxEditor.svelte";
|
||||
|
||||
const { editable, element, preventResubscription } = context.get();
|
||||
|
||||
let activeImage: HTMLImageElement | null = null;
|
||||
let mathjaxElement: HTMLElement | null = null;
|
||||
let allow = noop;
|
||||
let unsubscribe = noop;
|
||||
|
||||
let selectAll = false;
|
||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||
|
||||
/**
|
||||
* Will contain the Mathjax text with unescaped entities.
|
||||
* This is the text displayed in the actual editor window.
|
||||
*/
|
||||
const code = writable("");
|
||||
|
||||
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position): void {
|
||||
allow = preventResubscription();
|
||||
position = pos;
|
||||
|
||||
/* Setting the activeImage and mathjaxElement to a non-nullish value is
|
||||
* what triggers the Mathjax editor to show */
|
||||
activeImage = image;
|
||||
mathjaxElement = activeImage.closest(Mathjax.tagName)!;
|
||||
|
||||
code.set(unescapeSomeEntities(mathjaxElement.dataset.mathjax ?? ""));
|
||||
unsubscribe = code.subscribe((value: string) => {
|
||||
mathjaxElement!.dataset.mathjax = escapeSomeEntities(value);
|
||||
});
|
||||
}
|
||||
|
||||
function placeHandle(after: boolean): void {
|
||||
editable.focusHandler.flushCaret();
|
||||
|
||||
if (after) {
|
||||
(mathjaxElement as any).placeCaretAfter();
|
||||
} else {
|
||||
(mathjaxElement as any).placeCaretBefore();
|
||||
}
|
||||
}
|
||||
|
||||
async function resetHandle(): Promise<void> {
|
||||
selectAll = false;
|
||||
position = undefined;
|
||||
|
||||
if (activeImage && mathjaxElement) {
|
||||
unsubscribe();
|
||||
activeImage = null;
|
||||
mathjaxElement = null;
|
||||
}
|
||||
|
||||
allow();
|
||||
|
||||
// Wait for a tick, so that moving from one Mathjax element to
|
||||
// another will remount the MathjaxEditor
|
||||
await tick();
|
||||
}
|
||||
|
||||
let errorMessage: string;
|
||||
let cleanup: Callback | null = null;
|
||||
|
||||
async function updateErrorMessage(): Promise<void> {
|
||||
errorMessage = activeImage!.title;
|
||||
}
|
||||
|
||||
async function updateImageErrorCallback(image: HTMLImageElement | null) {
|
||||
cleanup?.();
|
||||
cleanup = null;
|
||||
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup = on(image, "resize", updateErrorMessage);
|
||||
}
|
||||
|
||||
$: updateImageErrorCallback(activeImage);
|
||||
|
||||
async function showOverlayIfMathjaxClicked({ target }: Event): Promise<void> {
|
||||
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
||||
await resetHandle();
|
||||
showOverlay(target);
|
||||
}
|
||||
}
|
||||
|
||||
async function showOnAutofocus({
|
||||
detail,
|
||||
}: CustomEvent<{
|
||||
image: HTMLImageElement;
|
||||
position?: [number, number];
|
||||
}>): Promise<void> {
|
||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||
|
||||
if (detail.position) {
|
||||
const [line, ch] = detail.position;
|
||||
position = { line, ch };
|
||||
}
|
||||
|
||||
showOverlay(detail.image, position);
|
||||
}
|
||||
|
||||
async function showSelectAll({
|
||||
detail,
|
||||
}: CustomEvent<HTMLImageElement>): Promise<void> {
|
||||
selectAll = true;
|
||||
showOverlay(detail);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const container = await element;
|
||||
|
||||
return singleCallback(
|
||||
on(container, "click", showOverlayIfMathjaxClicked),
|
||||
on(container, "movecaretafter" as any, showOnAutofocus),
|
||||
on(container, "selectall" as any, showSelectAll),
|
||||
);
|
||||
});
|
||||
|
||||
let isBlock: boolean;
|
||||
$: isBlock = mathjaxElement ? hasBlockAttribute(mathjaxElement) : false;
|
||||
|
||||
async function updateBlockAttribute(): Promise<void> {
|
||||
mathjaxElement!.setAttribute("block", String(isBlock));
|
||||
|
||||
// We assume that by the end of this tick, the image will have
|
||||
// adjusted its styling to either block or inline
|
||||
await tick();
|
||||
}
|
||||
|
||||
const acceptShortcut = "Enter";
|
||||
const newlineShortcut = "Shift+Enter";
|
||||
</script>
|
||||
|
||||
<div class="mathjax-overlay">
|
||||
{#if activeImage && mathjaxElement}
|
||||
<WithOverlay
|
||||
reference={activeImage}
|
||||
padding={isBlock ? 10 : 3}
|
||||
keepOnKeyup
|
||||
let:position={positionOverlay}
|
||||
>
|
||||
<WithFloating
|
||||
reference={activeImage}
|
||||
placement="auto"
|
||||
offset={20}
|
||||
keepOnKeyup
|
||||
let:position={positionFloating}
|
||||
on:close={resetHandle}
|
||||
>
|
||||
<Popover slot="floating">
|
||||
<MathjaxEditor
|
||||
{acceptShortcut}
|
||||
{newlineShortcut}
|
||||
{code}
|
||||
{selectAll}
|
||||
{position}
|
||||
on:moveoutstart={async () => {
|
||||
placeHandle(false);
|
||||
await resetHandle();
|
||||
}}
|
||||
on:moveoutend={async () => {
|
||||
placeHandle(true);
|
||||
await resetHandle();
|
||||
}}
|
||||
on:tab={async () => {
|
||||
// Instead of resetting on blur, we reset on tab
|
||||
// Otherwise, when clicking from Mathjax element to another,
|
||||
// the user has to click twice (focus is called before blur?)
|
||||
await resetHandle();
|
||||
}}
|
||||
let:editor={mathjaxEditor}
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={acceptShortcut}
|
||||
on:action={async () => {
|
||||
placeHandle(true);
|
||||
await resetHandle();
|
||||
}}
|
||||
/>
|
||||
|
||||
<MathjaxButtons
|
||||
{isBlock}
|
||||
on:setinline={async () => {
|
||||
isBlock = false;
|
||||
await updateBlockAttribute();
|
||||
positionOverlay();
|
||||
positionFloating();
|
||||
}}
|
||||
on:setblock={async () => {
|
||||
isBlock = true;
|
||||
await updateBlockAttribute();
|
||||
positionOverlay();
|
||||
positionFloating();
|
||||
}}
|
||||
on:delete={async () => {
|
||||
placeCaretAfter(activeImage);
|
||||
activeImage.remove();
|
||||
await resetHandle();
|
||||
}}
|
||||
on:surround={async ({ detail }) => {
|
||||
const editor = await mathjaxEditor.editor;
|
||||
const { prefix, suffix } = detail;
|
||||
|
||||
editor.replaceSelection(
|
||||
prefix + editor.getSelection() + suffix,
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</MathjaxEditor>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
|
||||
<svelte:fragment slot="overlay">
|
||||
<HandleBackground tooltip={errorMessage} />
|
||||
</svelte:fragment>
|
||||
</WithOverlay>
|
||||
{/if}
|
||||
</div>
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import MathjaxHandle from "./MathjaxHandle.svelte";
|
||||
import MathjaxOverlay from "./MathjaxOverlay.svelte";
|
||||
|
||||
export default MathjaxHandle;
|
||||
export default MathjaxOverlay;
|
||||
|
|
|
@ -40,7 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import { storedToUndecorated, undecoratedToStored } from "./transform";
|
||||
|
||||
export let isDefault: boolean;
|
||||
export let hidden: boolean;
|
||||
export let hidden = false;
|
||||
export let richTextHidden: boolean;
|
||||
|
||||
const configuration = {
|
||||
|
@ -148,6 +148,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
class:is-default={isDefault}
|
||||
class:alone={richTextHidden}
|
||||
on:focusin={() => ($focusedInput = api)}
|
||||
{hidden}
|
||||
>
|
||||
<CodeMirror
|
||||
{configuration}
|
||||
|
@ -160,8 +161,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<style lang="scss">
|
||||
.plain-text-input {
|
||||
overflow: hidden;
|
||||
|
||||
border-top: 1px solid var(--border);
|
||||
border-radius: 0 0 5px 5px;
|
||||
|
||||
|
@ -170,13 +169,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
border-bottom: 1px solid var(--border);
|
||||
border-radius: 5px 5px 0 0;
|
||||
}
|
||||
|
||||
&.alone {
|
||||
border: none;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
:global(.CodeMirror) {
|
||||
background: var(--code-bg);
|
||||
}
|
||||
|
||||
:global(.CodeMirror-lines) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
|
|
@ -77,7 +77,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import RichTextStyles from "./RichTextStyles.svelte";
|
||||
import { fragmentToStored, storedToFragment } from "./transform";
|
||||
|
||||
export let hidden: boolean;
|
||||
export let hidden = false;
|
||||
|
||||
const { focusedInput } = noteEditorContext.get();
|
||||
const { content, editingInputs } = editingAreaContext.get();
|
||||
|
@ -211,7 +211,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
setupLifecycleHooks(api);
|
||||
</script>
|
||||
|
||||
<div class="rich-text-input" on:focusin={setFocus} on:focusout={removeFocus}>
|
||||
<div class="rich-text-input" on:focusin={setFocus} on:focusout={removeFocus} {hidden}>
|
||||
<RichTextStyles
|
||||
color={$pageTheme.isDark ? "white" : "black"}
|
||||
fontFamily={$fontFamily}
|
||||
|
@ -246,8 +246,4 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
position: relative;
|
||||
margin: 6px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -6,6 +6,7 @@ export function assertUnreachable(x: never): never {
|
|||
}
|
||||
|
||||
export type Callback = () => void;
|
||||
export type AsyncCallback = () => Promise<void>;
|
||||
|
||||
export function singleCallback(...callbacks: Callback[]): Callback {
|
||||
return () => {
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import type { Readable } from "svelte/store";
|
||||
import { derived } from "svelte/store";
|
||||
|
||||
import type { EventPredicateResult } from "./event-predicate";
|
||||
|
||||
/**
|
||||
* Typically the right-sided mouse button.
|
||||
*/
|
||||
|
@ -31,34 +33,43 @@ interface ClosingClickArgs {
|
|||
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;
|
||||
): Readable<EventPredicateResult> {
|
||||
function isTriggerClick(path: EventTarget[]): string | false {
|
||||
// Reference element was clicked, e.g. the button.
|
||||
// The reference element needs to handle opening/closing itself.
|
||||
if (path.includes(reference)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isTriggerClick(event.composedPath())) {
|
||||
return true;
|
||||
if (inside && path.includes(floating)) {
|
||||
return "insideClick";
|
||||
}
|
||||
|
||||
if (outside && !path.includes(floating)) {
|
||||
return "outsideClick";
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return derived(store, (event: MouseEvent, set: (value: symbol) => void): void => {
|
||||
if (shouldClose(event)) {
|
||||
set(Symbol());
|
||||
function shouldClose(event: MouseEvent): string | false {
|
||||
if (isSecondaryButton(event)) {
|
||||
return "secondaryButton";
|
||||
}
|
||||
});
|
||||
|
||||
return isTriggerClick(event.composedPath());
|
||||
}
|
||||
|
||||
return derived(
|
||||
store,
|
||||
(event: MouseEvent, set: (value: EventPredicateResult) => void): void => {
|
||||
const reason = shouldClose(event);
|
||||
|
||||
if (reason) {
|
||||
set({ reason, originalEvent: event });
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default isClosingClick;
|
||||
|
|
|
@ -4,6 +4,8 @@
|
|||
import type { Readable } from "svelte/store";
|
||||
import { derived } from "svelte/store";
|
||||
|
||||
import type { EventPredicateResult } from "./event-predicate";
|
||||
|
||||
interface ClosingKeyupArgs {
|
||||
/**
|
||||
* Clicking on the reference element should not close.
|
||||
|
@ -22,24 +24,26 @@ interface ClosingKeyupArgs {
|
|||
function isClosingKeyup(
|
||||
store: Readable<KeyboardEvent>,
|
||||
_args: ClosingKeyupArgs,
|
||||
): Readable<symbol> {
|
||||
): Readable<EventPredicateResult> {
|
||||
// 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) {
|
||||
function shouldClose(event: KeyboardEvent): string | false {
|
||||
if (event.key === "Tab") {
|
||||
// Allow Tab navigation.
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return "keyup";
|
||||
}
|
||||
|
||||
return derived(
|
||||
store,
|
||||
(event: KeyboardEvent, set: (value: symbol) => void): void => {
|
||||
if (shouldClose(event)) {
|
||||
set(Symbol());
|
||||
(event: KeyboardEvent, set: (value: EventPredicateResult) => void): void => {
|
||||
const reason = shouldClose(event);
|
||||
|
||||
if (reason) {
|
||||
set({ reason, originalEvent: event });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
|
4
ts/sveltelib/event-predicate.d.ts
vendored
Normal file
4
ts/sveltelib/event-predicate.d.ts
vendored
Normal file
|
@ -0,0 +1,4 @@
|
|||
export interface EventPredicateResult {
|
||||
reason: string;
|
||||
originalEvent: Event;
|
||||
}
|
|
@ -21,7 +21,7 @@ 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
|
||||
* Store needs an initial value. This should probably be a freshly
|
||||
* constructed event, e.g. `new MouseEvent("click")`.
|
||||
*/
|
||||
constructor: Init<EventTargetToMap<T>[K]>,
|
||||
|
|
|
@ -9,10 +9,15 @@ function portal(
|
|||
element: HTMLElement,
|
||||
targetElement: Element = document.body,
|
||||
): { update(target: Element): void; destroy(): void } {
|
||||
let target: Element = targetElement;
|
||||
let target: Element;
|
||||
|
||||
async function update(newTarget: Element) {
|
||||
target = newTarget;
|
||||
|
||||
if (!target) {
|
||||
return;
|
||||
}
|
||||
|
||||
target.append(element);
|
||||
}
|
||||
|
||||
|
@ -20,7 +25,7 @@ function portal(
|
|||
element.remove();
|
||||
}
|
||||
|
||||
update(target);
|
||||
update(targetElement);
|
||||
|
||||
return {
|
||||
update,
|
||||
|
|
|
@ -1,111 +0,0 @@
|
|||
// 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,
|
||||
inline,
|
||||
offset,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
|
||||
export 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: [
|
||||
inline(),
|
||||
offset(5),
|
||||
shift({ padding: 5 }),
|
||||
arrow({ element: args.arrow, padding: 5 }),
|
||||
],
|
||||
placement: args.placement,
|
||||
},
|
||||
);
|
||||
|
||||
let rotation: number;
|
||||
let arrowX: number | undefined;
|
||||
let arrowY: number | undefined;
|
||||
|
||||
if (args.placement.startsWith("bottom")) {
|
||||
rotation = 45;
|
||||
arrowX = middlewareData.arrow?.x;
|
||||
arrowY = -5;
|
||||
} else if (args.placement.startsWith("left")) {
|
||||
rotation = 135;
|
||||
arrowX = args.floating!.offsetWidth - 5;
|
||||
arrowY = middlewareData.arrow?.y;
|
||||
} else if (args.placement.startsWith("top")) {
|
||||
rotation = 225;
|
||||
arrowX = middlewareData.arrow?.x;
|
||||
arrowY = args.floating!.offsetHeight - 5;
|
||||
} /* if (args.placement.startsWith("right")) */ else {
|
||||
rotation = 315;
|
||||
arrowX = -5;
|
||||
arrowY = middlewareData.arrow?.y;
|
||||
}
|
||||
|
||||
Object.assign(args.arrow.style, {
|
||||
left: arrowX ? `${arrowX}px` : "",
|
||||
top: arrowY ? `${arrowY}px` : "",
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
});
|
||||
|
||||
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;
|
57
ts/sveltelib/position/auto-update.ts
Normal file
57
ts/sveltelib/position/auto-update.ts
Normal file
|
@ -0,0 +1,57 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { FloatingElement } from "@floating-ui/dom";
|
||||
import { autoUpdate as floatingUiAutoUpdate } from "@floating-ui/dom";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
import type { Callback } from "../../lib/typing";
|
||||
|
||||
/**
|
||||
* The interface of `autoUpdate` of floating-ui.
|
||||
* This means PositioningCallback can be used with that, but also invoked as it is.
|
||||
*
|
||||
* @example ```
|
||||
* // Invoke the positioning algorithm handily
|
||||
* position(myReference, (_, _, callback) => {
|
||||
* callback();
|
||||
* })`
|
||||
*/
|
||||
export type PositioningCallback = (
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
position: Callback,
|
||||
) => Callback;
|
||||
|
||||
/**
|
||||
* The interface of a function that calls `computePosition` of floating-ui.
|
||||
*/
|
||||
export type PositionFunc = (
|
||||
reference: HTMLElement,
|
||||
callback: PositioningCallback,
|
||||
) => Callback;
|
||||
|
||||
function autoUpdate(
|
||||
reference: HTMLElement,
|
||||
/**
|
||||
* The method to position the floating element.
|
||||
*/
|
||||
position: PositionFunc,
|
||||
): ActionReturn<PositionFunc> {
|
||||
let cleanup: Callback;
|
||||
|
||||
function destroy() {
|
||||
cleanup?.();
|
||||
}
|
||||
|
||||
function update(position: PositionFunc): void {
|
||||
destroy();
|
||||
cleanup = position(reference, floatingUiAutoUpdate);
|
||||
}
|
||||
|
||||
update(position);
|
||||
|
||||
return { destroy, update };
|
||||
}
|
||||
|
||||
export default autoUpdate;
|
0
ts/sveltelib/position/index.ts
Normal file
0
ts/sveltelib/position/index.ts
Normal file
12
ts/sveltelib/position/position-algorithm.d.ts
vendored
Normal file
12
ts/sveltelib/position/position-algorithm.d.ts
vendored
Normal file
|
@ -0,0 +1,12 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { FloatingElement } from "@floating-ui/dom";
|
||||
|
||||
/**
|
||||
* The interface of a function that calls `computePosition` of floating-ui.
|
||||
*/
|
||||
export type PositionAlgorithm = (
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
) => Promise<void>;
|
125
ts/sveltelib/position/position-floating.ts
Normal file
125
ts/sveltelib/position/position-floating.ts
Normal file
|
@ -0,0 +1,125 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type {
|
||||
ComputePositionConfig,
|
||||
FloatingElement,
|
||||
Middleware,
|
||||
Placement,
|
||||
} from "@floating-ui/dom";
|
||||
import {
|
||||
arrow,
|
||||
autoPlacement,
|
||||
computePosition,
|
||||
hide,
|
||||
inline,
|
||||
offset,
|
||||
shift,
|
||||
} from "@floating-ui/dom";
|
||||
|
||||
import type { PositionAlgorithm } from "./position-algorithm";
|
||||
|
||||
export interface PositionFloatingArgs {
|
||||
placement: Placement | "auto";
|
||||
arrow: HTMLElement;
|
||||
shift: number;
|
||||
offset: number;
|
||||
inline: boolean;
|
||||
hideIfEscaped: boolean;
|
||||
hideIfReferenceHidden: boolean;
|
||||
hideCallback: (reason: string) => void;
|
||||
}
|
||||
|
||||
function positionFloating({
|
||||
placement,
|
||||
arrow: arrowElement,
|
||||
shift: shiftArg,
|
||||
offset: offsetArg,
|
||||
inline: inlineArg,
|
||||
hideIfEscaped,
|
||||
hideIfReferenceHidden,
|
||||
hideCallback,
|
||||
}: PositionFloatingArgs): PositionAlgorithm {
|
||||
return async function (
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
): Promise<void> {
|
||||
const middleware: Middleware[] = [
|
||||
offset(offsetArg),
|
||||
shift({ padding: shiftArg }),
|
||||
arrow({ element: arrowElement, padding: 5 }),
|
||||
];
|
||||
|
||||
if (inlineArg) {
|
||||
middleware.unshift(inline());
|
||||
}
|
||||
|
||||
const computeArgs: Partial<ComputePositionConfig> = {
|
||||
middleware,
|
||||
};
|
||||
|
||||
if (placement !== "auto") {
|
||||
computeArgs.placement = placement;
|
||||
} else {
|
||||
middleware.push(autoPlacement());
|
||||
}
|
||||
|
||||
if (hideIfEscaped) {
|
||||
middleware.push(hide({ strategy: "escaped" }));
|
||||
}
|
||||
|
||||
if (hideIfReferenceHidden) {
|
||||
middleware.push(hide({ strategy: "referenceHidden" }));
|
||||
}
|
||||
|
||||
const {
|
||||
x,
|
||||
y,
|
||||
middlewareData,
|
||||
placement: computedPlacement,
|
||||
} = await computePosition(reference, floating, computeArgs);
|
||||
|
||||
if (middlewareData.hide?.escaped) {
|
||||
return hideCallback("escaped");
|
||||
}
|
||||
|
||||
if (middlewareData.hide?.referenceHidden) {
|
||||
return hideCallback("referenceHidden");
|
||||
}
|
||||
|
||||
Object.assign(floating.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
});
|
||||
|
||||
let rotation: number;
|
||||
let arrowX: number | undefined;
|
||||
let arrowY: number | undefined;
|
||||
|
||||
if (computedPlacement.startsWith("bottom")) {
|
||||
rotation = 45;
|
||||
arrowX = middlewareData.arrow?.x;
|
||||
arrowY = -5;
|
||||
} else if (computedPlacement.startsWith("left")) {
|
||||
rotation = 135;
|
||||
arrowX = floating.offsetWidth - 5;
|
||||
arrowY = middlewareData.arrow?.y;
|
||||
} else if (computedPlacement.startsWith("top")) {
|
||||
rotation = 225;
|
||||
arrowX = middlewareData.arrow?.x;
|
||||
arrowY = floating.offsetHeight - 5;
|
||||
} /* if (computedPlacement.startsWith("right")) */ else {
|
||||
rotation = 315;
|
||||
arrowX = -5;
|
||||
arrowY = middlewareData.arrow?.y;
|
||||
}
|
||||
|
||||
Object.assign(arrowElement.style, {
|
||||
left: arrowX ? `${arrowX}px` : "",
|
||||
top: arrowY ? `${arrowY}px` : "",
|
||||
transform: `rotate(${rotation}deg)`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default positionFloating;
|
67
ts/sveltelib/position/position-overlay.ts
Normal file
67
ts/sveltelib/position/position-overlay.ts
Normal file
|
@ -0,0 +1,67 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type {
|
||||
ComputePositionConfig,
|
||||
FloatingElement,
|
||||
Middleware,
|
||||
} from "@floating-ui/dom";
|
||||
import { computePosition, inline, offset } from "@floating-ui/dom";
|
||||
|
||||
import type { PositionAlgorithm } from "./position-algorithm";
|
||||
|
||||
export interface PositionOverlayArgs {
|
||||
padding: number;
|
||||
inline: boolean;
|
||||
hideCallback: (reason: string) => void;
|
||||
}
|
||||
|
||||
function positionOverlay({
|
||||
padding,
|
||||
inline: inlineArg,
|
||||
hideCallback,
|
||||
}: PositionOverlayArgs): PositionAlgorithm {
|
||||
return async function (
|
||||
reference: HTMLElement,
|
||||
floating: FloatingElement,
|
||||
): Promise<void> {
|
||||
const middleware: Middleware[] = inlineArg ? [inline()] : [];
|
||||
|
||||
const { width, height } = reference.getBoundingClientRect();
|
||||
|
||||
middleware.push(
|
||||
offset({
|
||||
mainAxis: -(height + padding),
|
||||
}),
|
||||
);
|
||||
|
||||
const computeArgs: Partial<ComputePositionConfig> = {
|
||||
middleware,
|
||||
};
|
||||
|
||||
const { x, y, middlewareData } = await computePosition(
|
||||
reference,
|
||||
floating,
|
||||
computeArgs,
|
||||
);
|
||||
|
||||
// console.log(x, y)
|
||||
|
||||
if (middlewareData.hide?.escaped) {
|
||||
hideCallback("escaped");
|
||||
}
|
||||
|
||||
if (middlewareData.hide?.referenceHidden) {
|
||||
hideCallback("referenceHidden");
|
||||
}
|
||||
|
||||
Object.assign(floating.style, {
|
||||
left: `${x}px`,
|
||||
top: `${y}px`,
|
||||
width: `${width + 2 * padding}px`,
|
||||
height: `${height + 2 * padding}px`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export default positionOverlay;
|
45
ts/sveltelib/resize-store.ts
Normal file
45
ts/sveltelib/resize-store.ts
Normal file
|
@ -0,0 +1,45 @@
|
|||
// 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 { Callback } from "../lib/typing";
|
||||
|
||||
interface ResizeObserverArgs {
|
||||
entries: ResizeObserverEntry[];
|
||||
observer: ResizeObserver;
|
||||
}
|
||||
|
||||
export type ResizeStore = Readable<ResizeObserverArgs>;
|
||||
|
||||
/**
|
||||
* A store wrapping a ResizeObserver. Automatically observes the target upon
|
||||
* first/last subscriber.
|
||||
*
|
||||
* @remarks
|
||||
* Should probably always be used in conjunction with `subscribeToUpdates`.
|
||||
*/
|
||||
function resizeStore(target: Element): ResizeStore {
|
||||
let setter: (args: ResizeObserverArgs) => void;
|
||||
|
||||
const observer = new ResizeObserver(
|
||||
(entries: ResizeObserverEntry[], observer: ResizeObserver): void =>
|
||||
setter({
|
||||
entries,
|
||||
observer,
|
||||
}),
|
||||
);
|
||||
|
||||
return readable(
|
||||
{ entries: [], observer },
|
||||
(set: Subscriber<ResizeObserverArgs>): Callback => {
|
||||
setter = set;
|
||||
observer.observe(target);
|
||||
|
||||
return () => observer.unobserve(target);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
export default resizeStore;
|
|
@ -1,44 +0,0 @@
|
|||
// 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;
|
|
@ -2,27 +2,36 @@
|
|||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { Writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export interface Toggleable {
|
||||
export interface Toggleable extends Writable<boolean> {
|
||||
toggle: () => void;
|
||||
on: () => void;
|
||||
off: () => void;
|
||||
}
|
||||
|
||||
function toggleable(store: Writable<boolean>): Toggleable {
|
||||
function toggleable(defaultValue: boolean): Toggleable {
|
||||
const store = writable(defaultValue) as Toggleable;
|
||||
|
||||
function toggle(): void {
|
||||
store.update((value) => !value);
|
||||
}
|
||||
|
||||
store.toggle = toggle;
|
||||
|
||||
function on(): void {
|
||||
store.set(true);
|
||||
}
|
||||
|
||||
store.on = on;
|
||||
|
||||
function off(): void {
|
||||
store.set(false);
|
||||
}
|
||||
|
||||
return { toggle, on, off };
|
||||
store.off = off;
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
export default toggleable;
|
||||
|
|
|
@ -1,5 +1,9 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"include": ["*"],
|
||||
"references": [{ "path": "../lib" }]
|
||||
"include": ["*", "position/*"],
|
||||
"references": [
|
||||
{
|
||||
"path": "../lib"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
@ -426,7 +426,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
splitTag(index, detail.chosen.length, detail.chosen.length);
|
||||
}}
|
||||
let:createAutocomplete
|
||||
let:hide
|
||||
>
|
||||
<TagInput
|
||||
id={tag.id}
|
||||
|
@ -441,7 +440,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
on:keydown={onKeydown}
|
||||
on:keyup={() => {
|
||||
if (activeName.length === 0) {
|
||||
hide?.();
|
||||
show?.set(false);
|
||||
}
|
||||
}}
|
||||
on:taginput={() => updateTagName(tag)}
|
||||
|
|
|
@ -124,14 +124,16 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
</script>
|
||||
|
||||
<WithFloating keepOnKeyup {show} placement="top-start" let:toggle let:hide let:show>
|
||||
<span
|
||||
class="autocomplete-reference"
|
||||
slot="reference"
|
||||
let:asReference
|
||||
use:asReference
|
||||
>
|
||||
<slot {createAutocomplete} {toggle} {hide} {show} />
|
||||
<WithFloating
|
||||
keepOnKeyup
|
||||
show={$show}
|
||||
placement="top-start"
|
||||
portalTarget={document.body}
|
||||
let:asReference
|
||||
on:close={() => show.set(false)}
|
||||
>
|
||||
<span class="autocomplete-reference" use:asReference>
|
||||
<slot {createAutocomplete} />
|
||||
</span>
|
||||
|
||||
<Popover slot="floating">
|
||||
|
|
|
@ -16,20 +16,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
let show = false;
|
||||
|
||||
const allShortcut = "Control+A";
|
||||
const copyShortcut = "Control+C";
|
||||
const removeShortcut = "Backspace";
|
||||
</script>
|
||||
|
||||
<WithFloating placement="top">
|
||||
<div
|
||||
class="tags-selected-button"
|
||||
slot="reference"
|
||||
let:asReference
|
||||
use:asReference
|
||||
let:toggle
|
||||
on:click={toggle}
|
||||
>
|
||||
<WithFloating {show} placement="top" let:asReference>
|
||||
<div class="tags-selected-button" use:asReference on:click={() => (show = !show)}>
|
||||
<IconConstrain>{@html dotsIcon}</IconConstrain>
|
||||
</div>
|
||||
|
||||
|
|
Loading…
Reference in a new issue