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