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:
Henrik Giesel 2022-09-05 09:20:00 +02:00 committed by GitHub
parent e7af0febb1
commit 3642dc6245
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
65 changed files with 1405 additions and 1301 deletions

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

@ -29,6 +29,7 @@ _ts_deps = [
"@npm//@fluent",
"@npm//@popperjs",
"@npm//@types/jest",
"@npm//@mdi",
"@npm//bootstrap-icons",
"@npm//bootstrap",
"@npm//lodash-es",

View file

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

View file

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

View file

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

View file

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

View file

@ -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 });

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

@ -0,0 +1,4 @@
export interface EventPredicateResult {
reason: string;
originalEvent: Event;
}

View file

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

View file

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

View file

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

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

View file

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

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

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

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

View file

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

View file

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

View file

@ -1,5 +1,9 @@
{
"extends": "../tsconfig.json",
"include": ["*"],
"references": [{ "path": "../lib" }]
"include": ["*", "position/*"],
"references": [
{
"path": "../lib"
}
]
}

View file

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

View file

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

View file

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