mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
* Move up MathjaxOverlay to be initialized only once
* Move ImageOverlay to NoteEditor root
* Move Symbols Overlay to NoteEditor root
* Refactor image overlay to not require second mutation observer
* Use elevation + overflow:hidden in Editorfield
* Make it possible to show input next to each other again
* Set handle background color to code bg
* Make Collapsible unmount the component
* Simplify how decorated elements are mounted
* Set RichTextInput background to frame-bg again
* Strip out FocusTrap code
* Revert "Make Collapsible unmount the component"
This reverts commit 52722065ea
.
* Allow clicking on label container to unfocus field
* Fix mathjax overlay resetting too its api too soon
* Allow scrolling on overlays
* Set focus-border border-color in focused field
* Fix background color of fields
* Add back grid-gap
removed it during merge to see if margin-top would behave any differently - which is not the case.
* Fix double border issue within Collapsible.svelte
* Format
* Edit appearance of focused fields a bit
* Remove unused properties
* Include elevation in button_mixins_lib
* Give label-container a background color
Co-authored-by: Henrik Giesel <hengiesel@gmail.com>
This commit is contained in:
parent
7942518d64
commit
68fa661b53
28 changed files with 463 additions and 548 deletions
|
@ -83,6 +83,7 @@ sass_library(
|
||||||
visibility = ["//visibility:public"],
|
visibility = ["//visibility:public"],
|
||||||
deps = [
|
deps = [
|
||||||
"vars_lib",
|
"vars_lib",
|
||||||
|
"elevation_lib",
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -59,7 +59,7 @@ $umbra-opacity: 0.2;
|
||||||
$penumbra-opacity: 0.14;
|
$penumbra-opacity: 0.14;
|
||||||
$ambient-opacity: 0.12;
|
$ambient-opacity: 0.12;
|
||||||
|
|
||||||
@function box-shadow($level, $opacity-boost: 0, $color: black) {
|
@function box-shadow($level, $opacity-boost, $color) {
|
||||||
$umbra-z-value: map.get($umbra-map, $level);
|
$umbra-z-value: map.get($umbra-map, $level);
|
||||||
$penumbra-z-value: map.get($penumbra-map, $level);
|
$penumbra-z-value: map.get($penumbra-map, $level);
|
||||||
$ambient-z-value: map.get($ambient-map, $level);
|
$ambient-z-value: map.get($ambient-map, $level);
|
||||||
|
@ -75,6 +75,10 @@ $ambient-opacity: 0.12;
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@mixin elevation($level, $other: ()) {
|
@mixin elevation($level, $opacity-boost: 0, $color: black) {
|
||||||
box-shadow: list.join(box-shadow($level), $other);
|
box-shadow: box-shadow($level, $opacity-boost, $color);
|
||||||
|
}
|
||||||
|
|
||||||
|
@mixin elevation-transition() {
|
||||||
|
transition: box-shadow 80ms cubic-bezier(0.33, 1, 0.68, 1);
|
||||||
}
|
}
|
||||||
|
|
|
@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { tweened } from "svelte/motion";
|
import { tweened } from "svelte/motion";
|
||||||
|
|
||||||
export let collapse = false;
|
export let collapse = false;
|
||||||
|
export let toggleDisplay = false;
|
||||||
export let animated = !document.body.classList.contains("reduced-motion");
|
export let animated = !document.body.classList.contains("reduced-motion");
|
||||||
|
|
||||||
let collapsed = false;
|
let collapsed = false;
|
||||||
|
@ -62,6 +63,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
class="collapsible"
|
class="collapsible"
|
||||||
class:animated
|
class:animated
|
||||||
class:expanded
|
class:expanded
|
||||||
|
class:full-hide={toggleDisplay}
|
||||||
|
class:collapsed={!expanded}
|
||||||
class:measuring
|
class:measuring
|
||||||
class:transitioning
|
class:transitioning
|
||||||
style:--height="{height}px"
|
style:--height="{height}px"
|
||||||
|
@ -75,8 +78,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
.collapsible.full-hide {
|
||||||
|
&.collapsed {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
&.transitioning {
|
||||||
|
display: initial;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.collapsible.animated {
|
.collapsible.animated {
|
||||||
&.measuring {
|
&.measuring {
|
||||||
|
display: unset;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,12 +15,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let disabled = false;
|
export let disabled = false;
|
||||||
|
|
||||||
$: if (buttonRef && active) {
|
$: if (buttonRef && active) {
|
||||||
/* buttonRef.scrollIntoView({ behavior: "smooth", block: "start" }); */
|
setTimeout(() =>
|
||||||
/* TODO will not work on Gecko */
|
buttonRef.scrollIntoView({
|
||||||
(buttonRef as any).scrollIntoViewIfNeeded({
|
behavior: "smooth",
|
||||||
behavior: "smooth",
|
block: "nearest",
|
||||||
block: "start",
|
}),
|
||||||
});
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export let tabbable = false;
|
export let tabbable = false;
|
||||||
|
|
|
@ -67,7 +67,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !editingInputIsRichText($focusedInput);
|
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||||
|
|
||||||
const incrementKeyCombination = "Control+Shift+C";
|
const incrementKeyCombination = "Control+Shift+C";
|
||||||
const sameKeyCombination = "Control+Alt+Shift+C";
|
const sameKeyCombination = "Control+Alt+Shift+C";
|
||||||
|
|
|
@ -98,6 +98,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.code-mirror {
|
.code-mirror {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
:global(.CodeMirror) {
|
:global(.CodeMirror) {
|
||||||
height: auto;
|
height: auto;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,13 +0,0 @@
|
||||||
<!--
|
|
||||||
Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
-->
|
|
||||||
<script context="module" lang="ts">
|
|
||||||
import { CustomElementArray } from "../editable/decorated";
|
|
||||||
|
|
||||||
const decoratedElements = new CustomElementArray();
|
|
||||||
|
|
||||||
export { decoratedElements };
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<slot />
|
|
|
@ -46,11 +46,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { setContext as svelteSetContext, tick } from "svelte";
|
import { setContext as svelteSetContext } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
||||||
import FocusTrap from "./FocusTrap.svelte";
|
|
||||||
|
|
||||||
export let fontFamily: string;
|
export let fontFamily: string;
|
||||||
const fontFamilyStore = writable(fontFamily);
|
const fontFamilyStore = writable(fontFamily);
|
||||||
|
@ -65,7 +64,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let content: Writable<string>;
|
export let content: Writable<string>;
|
||||||
|
|
||||||
let editingArea: HTMLElement;
|
let editingArea: HTMLElement;
|
||||||
let focusTrap: FocusTrap;
|
|
||||||
|
|
||||||
const inputsStore = writable<EditingInputAPI[]>([]);
|
const inputsStore = writable<EditingInputAPI[]>([]);
|
||||||
$: editingInputs = $inputsStore;
|
$: editingInputs = $inputsStore;
|
||||||
|
@ -74,39 +72,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return editingInputs.find((input) => input.focusable);
|
return editingInputs.find((input) => input.focusable);
|
||||||
}
|
}
|
||||||
|
|
||||||
function focusEditingInputIfAvailable(): boolean {
|
|
||||||
const availableInput = getAvailableInput();
|
|
||||||
|
|
||||||
if (availableInput) {
|
|
||||||
availableInput.focus();
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusEditingInputIfFocusTrapFocused(): void {
|
|
||||||
if (focusTrap && focusTrap.isFocusTrap(document.activeElement!)) {
|
|
||||||
focusEditingInputIfAvailable();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: {
|
|
||||||
$inputsStore;
|
|
||||||
/**
|
|
||||||
* Triggers when all editing inputs are hidden,
|
|
||||||
* the editor field has focus, and then some
|
|
||||||
* editing input is shown
|
|
||||||
*/
|
|
||||||
focusEditingInputIfFocusTrapFocused();
|
|
||||||
}
|
|
||||||
|
|
||||||
function focus(): void {
|
function focus(): void {
|
||||||
if (editingArea.contains(document.activeElement)) {
|
editingArea.contains(document.activeElement);
|
||||||
// do nothing
|
|
||||||
} else if (!focusEditingInputIfAvailable()) {
|
|
||||||
focusTrap.focus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function refocus(): void {
|
function refocus(): void {
|
||||||
|
@ -114,51 +81,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
if (availableInput) {
|
if (availableInput) {
|
||||||
availableInput.refocus();
|
availableInput.refocus();
|
||||||
} else {
|
|
||||||
focusTrap.blur();
|
|
||||||
focusTrap.focus();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function focusEditingInputInsteadIfAvailable(event: FocusEvent): void {
|
|
||||||
if (focusEditingInputIfAvailable()) {
|
|
||||||
event.preventDefault();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Prevents editor field being entirely deselected when
|
|
||||||
// closing active field.
|
|
||||||
async function trapFocusOnBlurOut(event: FocusEvent): Promise<void> {
|
|
||||||
if (event.relatedTarget) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
const oldInputElement = event.target;
|
|
||||||
|
|
||||||
await tick();
|
|
||||||
|
|
||||||
let focusableInput: FocusableInputAPI | null = null;
|
|
||||||
|
|
||||||
const focusableInputs = editingInputs.filter(
|
|
||||||
(input: EditingInputAPI): boolean => input.focusable,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (oldInputElement) {
|
|
||||||
for (const input of focusableInputs) {
|
|
||||||
focusableInput = await input.getInputAPI(oldInputElement);
|
|
||||||
|
|
||||||
if (focusableInput) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (focusableInput || (focusableInput = focusableInputs[0])) {
|
|
||||||
focusableInput.focus();
|
|
||||||
} else {
|
|
||||||
focusTrap.focus();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,9 +97,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
setContextProperty(api);
|
setContextProperty(api);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<FocusTrap bind:this={focusTrap} on:focus={focusEditingInputInsteadIfAvailable} />
|
<div bind:this={editingArea} class="editing-area">
|
||||||
|
|
||||||
<div bind:this={editingArea} class="editing-area" on:focusout={trapFocusOnBlurOut}>
|
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -187,29 +107,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
/* TODO allow configuration of grid #1503 */
|
/* TODO allow configuration of grid #1503 */
|
||||||
/* grid-template-columns: repeat(2, 1fr); */
|
/* grid-template-columns: repeat(2, 1fr); */
|
||||||
|
|
||||||
position: relative;
|
/* This defines the border between inputs */
|
||||||
background: var(--canvas-elevated);
|
grid-gap: 1px;
|
||||||
border-radius: 5px;
|
background-color: var(--border);
|
||||||
|
|
||||||
/* Pseudo-element required to display
|
|
||||||
inset focus box-shadow above field contents */
|
|
||||||
&::after {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
pointer-events: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
box-shadow: 0 0 0 3px var(--dupes-color), 0 0 2px 1px var(--shadow);
|
|
||||||
transition: box-shadow 80ms cubic-bezier(0.33, 1, 0.68, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
outline: none;
|
|
||||||
&::after {
|
|
||||||
border: none;
|
|
||||||
inset: 1px;
|
|
||||||
box-shadow: 0 0 0 3px var(--dupes-color), 0 0 0 2px var(--border-focus);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -109,9 +109,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
|
@use "sass/elevation" as *;
|
||||||
|
|
||||||
.editor-field {
|
.editor-field {
|
||||||
position: relative;
|
overflow: hidden;
|
||||||
padding: 0 3px;
|
margin: 0 3px;
|
||||||
--border-color: var(--border);
|
|
||||||
|
border-radius: 5px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
|
||||||
|
@include elevation-transition;
|
||||||
|
@include elevation(1);
|
||||||
|
|
||||||
|
&:focus-within {
|
||||||
|
border-color: var(--border-focus);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -20,6 +20,10 @@ Contains the fields. This contains the scrollable area.
|
||||||
/* Add space after the last field and the start of the tag editor */
|
/* Add space after the last field and the start of the tag editor */
|
||||||
padding-bottom: 5px;
|
padding-bottom: 5px;
|
||||||
|
|
||||||
|
/* Move the scrollbar for the NoteEditor into this element */
|
||||||
|
position: relative;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
/* Push the tag editor to the bottom of the note editor */
|
/* Push the tag editor to the bottom of the note editor */
|
||||||
flex-grow: 1;
|
flex-grow: 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,41 +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">
|
|
||||||
let input: HTMLInputElement;
|
|
||||||
|
|
||||||
export function focus(): void {
|
|
||||||
input.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function blur(): void {
|
|
||||||
input.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function isFocusTrap(element: Element): boolean {
|
|
||||||
return element === input;
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<!--
|
|
||||||
@component
|
|
||||||
Allows "focusing" an EditingArea, even though it has no open editing inputs.
|
|
||||||
-->
|
|
||||||
<input bind:this={input} class="focus-trap" readonly tabindex="-1" on:focus />
|
|
||||||
|
|
||||||
<style lang="scss">
|
|
||||||
.focus-trap {
|
|
||||||
display: block;
|
|
||||||
width: 0px;
|
|
||||||
height: 0;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
border: none;
|
|
||||||
outline: none;
|
|
||||||
-webkit-appearance: none;
|
|
||||||
background: none;
|
|
||||||
resize: none;
|
|
||||||
appearance: none;
|
|
||||||
}
|
|
||||||
</style>
|
|
|
@ -1,20 +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 { FrameElement } from "../editable/frame-element";
|
|
||||||
|
|
||||||
customElements.define(FrameElement.tagName, FrameElement);
|
|
||||||
|
|
||||||
import { FrameEnd, FrameStart } from "../editable/frame-handle";
|
|
||||||
|
|
||||||
customElements.define(FrameStart.tagName, FrameStart);
|
|
||||||
customElements.define(FrameEnd.tagName, FrameEnd);
|
|
||||||
|
|
||||||
import { BLOCK_ELEMENTS } from "../lib/dom";
|
|
||||||
|
|
||||||
/* This will ensure that they are not targeted by surrounding algorithms */
|
|
||||||
BLOCK_ELEMENTS.push(FrameStart.tagName.toUpperCase());
|
|
||||||
BLOCK_ELEMENTS.push(FrameEnd.tagName.toUpperCase());
|
|
||||||
</script>
|
|
|
@ -20,7 +20,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="label-container" on:mousedown|preventDefault>
|
<div class="label-container">
|
||||||
<span
|
<span
|
||||||
class="clickable"
|
class="clickable"
|
||||||
title={tooltip}
|
title={tooltip}
|
||||||
|
@ -38,11 +38,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
.label-container {
|
.label-container {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
|
background: var(--canvas);
|
||||||
padding: 0 3px 1px;
|
padding: 0 3px 1px;
|
||||||
|
|
||||||
position: sticky;
|
position: sticky;
|
||||||
top: 0;
|
top: 0;
|
||||||
z-index: 10;
|
z-index: 50;
|
||||||
|
|
||||||
.clickable {
|
.clickable {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
|
@ -1,14 +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" context="module">
|
|
||||||
import { Mathjax } from "../editable/mathjax-element";
|
|
||||||
import { decoratedElements } from "./DecoratedElements.svelte";
|
|
||||||
|
|
||||||
decoratedElements.push(Mathjax);
|
|
||||||
|
|
||||||
import { parsingInstructions } from "./plain-text-input";
|
|
||||||
|
|
||||||
parsingInstructions.push("<style>anki-mathjax { white-space: pre; }</style>");
|
|
||||||
</script>
|
|
|
@ -52,7 +52,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { TagEditor } from "../tag-editor";
|
import { TagEditor } from "../tag-editor";
|
||||||
import TagAddButton from "../tag-editor/tag-options-button/TagAddButton.svelte";
|
import TagAddButton from "../tag-editor/tag-options-button/TagAddButton.svelte";
|
||||||
import { ChangeTimer } from "./change-timer";
|
import { ChangeTimer } from "./change-timer";
|
||||||
import DecoratedElements from "./DecoratedElements.svelte";
|
|
||||||
import { clearableArray } from "./destroyable";
|
import { clearableArray } from "./destroyable";
|
||||||
import DuplicateLink from "./DuplicateLink.svelte";
|
import DuplicateLink from "./DuplicateLink.svelte";
|
||||||
import EditorToolbar from "./editor-toolbar";
|
import EditorToolbar from "./editor-toolbar";
|
||||||
|
@ -60,12 +59,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import EditorField from "./EditorField.svelte";
|
import EditorField from "./EditorField.svelte";
|
||||||
import FieldDescription from "./FieldDescription.svelte";
|
import FieldDescription from "./FieldDescription.svelte";
|
||||||
import Fields from "./Fields.svelte";
|
import Fields from "./Fields.svelte";
|
||||||
import FrameElement from "./FrameElement.svelte";
|
|
||||||
import { alertIcon } from "./icons";
|
import { alertIcon } from "./icons";
|
||||||
import ImageHandle from "./image-overlay";
|
import ImageOverlay from "./image-overlay";
|
||||||
import { shrinkImagesByDefault } from "./image-overlay/ImageOverlay.svelte";
|
import { shrinkImagesByDefault } from "./image-overlay/ImageOverlay.svelte";
|
||||||
import MathjaxHandle from "./mathjax-overlay";
|
import MathjaxOverlay from "./mathjax-overlay";
|
||||||
import MathjaxElement from "./MathjaxElement.svelte";
|
|
||||||
import Notification from "./Notification.svelte";
|
import Notification from "./Notification.svelte";
|
||||||
import PlainTextInput from "./plain-text-input";
|
import PlainTextInput from "./plain-text-input";
|
||||||
import { closeHTMLTags } from "./plain-text-input/PlainTextInput.svelte";
|
import { closeHTMLTags } from "./plain-text-input/PlainTextInput.svelte";
|
||||||
|
@ -412,161 +409,154 @@ the AddCards dialog) should be implemented in the user of this component.
|
||||||
>
|
>
|
||||||
<PaneContent>
|
<PaneContent>
|
||||||
<Fields>
|
<Fields>
|
||||||
<DecoratedElements>
|
{#each fieldsData as field, index}
|
||||||
{#each fieldsData as field, index}
|
{@const content = fieldStores[index]}
|
||||||
{@const content = fieldStores[index]}
|
|
||||||
|
|
||||||
<EditorField
|
<EditorField
|
||||||
{field}
|
{field}
|
||||||
{content}
|
{content}
|
||||||
flipInputs={plainTextDefaults[index]}
|
flipInputs={plainTextDefaults[index]}
|
||||||
api={fields[index]}
|
api={fields[index]}
|
||||||
on:focusin={() => {
|
on:focusin={() => {
|
||||||
$focusedField = fields[index];
|
$focusedField = fields[index];
|
||||||
bridgeCommand(`focus:${index}`);
|
bridgeCommand(`focus:${index}`);
|
||||||
}}
|
}}
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
$focusedField = null;
|
$focusedField = null;
|
||||||
bridgeCommand(
|
bridgeCommand(
|
||||||
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
|
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
|
||||||
get(content),
|
get(content),
|
||||||
)}`,
|
)}`,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
on:mouseenter={() => {
|
on:mouseenter={() => {
|
||||||
$hoveredField = fields[index];
|
$hoveredField = fields[index];
|
||||||
}}
|
}}
|
||||||
on:mouseleave={() => {
|
on:mouseleave={() => {
|
||||||
$hoveredField = null;
|
$hoveredField = null;
|
||||||
}}
|
}}
|
||||||
collapsed={fieldsCollapsed[index]}
|
collapsed={fieldsCollapsed[index]}
|
||||||
--dupes-color={cols[index] === "dupe"
|
--dupes-color={cols[index] === "dupe"
|
||||||
? "var(--accent-danger)"
|
? "var(--accent-danger)"
|
||||||
: "transparent"}
|
: "transparent"}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="field-label">
|
<svelte:fragment slot="field-label">
|
||||||
<LabelContainer
|
<LabelContainer
|
||||||
collapsed={fieldsCollapsed[index]}
|
collapsed={fieldsCollapsed[index]}
|
||||||
on:toggle={async () => {
|
on:toggle={async () => {
|
||||||
fieldsCollapsed[index] =
|
fieldsCollapsed[index] = !fieldsCollapsed[index];
|
||||||
!fieldsCollapsed[index];
|
|
||||||
|
|
||||||
const defaultInput = !plainTextDefaults[index]
|
const defaultInput = !plainTextDefaults[index]
|
||||||
? richTextInputs[index]
|
? richTextInputs[index]
|
||||||
: plainTextInputs[index];
|
: plainTextInputs[index];
|
||||||
|
|
||||||
if (!fieldsCollapsed[index]) {
|
if (!fieldsCollapsed[index]) {
|
||||||
refocusInput(defaultInput.api);
|
refocusInput(defaultInput.api);
|
||||||
} else if (!plainTextDefaults[index]) {
|
} else if (!plainTextDefaults[index]) {
|
||||||
plainTextsHidden[index] = true;
|
plainTextsHidden[index] = true;
|
||||||
} else {
|
} else {
|
||||||
richTextsHidden[index] = true;
|
richTextsHidden[index] = true;
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<svelte:fragment slot="field-name">
|
<svelte:fragment slot="field-name">
|
||||||
<LabelName>
|
<LabelName>
|
||||||
{field.name}
|
{field.name}
|
||||||
</LabelName>
|
</LabelName>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
<FieldState>
|
<FieldState>
|
||||||
{#if cols[index] === "dupe"}
|
{#if cols[index] === "dupe"}
|
||||||
<DuplicateLink />
|
<DuplicateLink />
|
||||||
{/if}
|
{/if}
|
||||||
{#if plainTextDefaults[index]}
|
{#if plainTextDefaults[index]}
|
||||||
<RichTextBadge
|
<RichTextBadge
|
||||||
visible={!fieldsCollapsed[index] &&
|
visible={!fieldsCollapsed[index] &&
|
||||||
(fields[index] === $hoveredField ||
|
(fields[index] === $hoveredField ||
|
||||||
fields[index] ===
|
fields[index] === $focusedField)}
|
||||||
$focusedField)}
|
bind:off={richTextsHidden[index]}
|
||||||
bind:off={richTextsHidden[index]}
|
on:toggle={async () => {
|
||||||
on:toggle={async () => {
|
richTextsHidden[index] =
|
||||||
richTextsHidden[index] =
|
!richTextsHidden[index];
|
||||||
!richTextsHidden[index];
|
|
||||||
|
|
||||||
if (!richTextsHidden[index]) {
|
if (!richTextsHidden[index]) {
|
||||||
refocusInput(
|
refocusInput(
|
||||||
richTextInputs[index].api,
|
richTextInputs[index].api,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
/>
|
|
||||||
{:else}
|
|
||||||
<PlainTextBadge
|
|
||||||
visible={!fieldsCollapsed[index] &&
|
|
||||||
(fields[index] === $hoveredField ||
|
|
||||||
fields[index] ===
|
|
||||||
$focusedField)}
|
|
||||||
bind:off={plainTextsHidden[index]}
|
|
||||||
on:toggle={async () => {
|
|
||||||
plainTextsHidden[index] =
|
|
||||||
!plainTextsHidden[index];
|
|
||||||
|
|
||||||
if (!plainTextsHidden[index]) {
|
|
||||||
refocusInput(
|
|
||||||
plainTextInputs[index].api,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
<slot
|
|
||||||
name="field-state"
|
|
||||||
{field}
|
|
||||||
{index}
|
|
||||||
visible={fields[index] === $hoveredField ||
|
|
||||||
fields[index] === $focusedField}
|
|
||||||
/>
|
/>
|
||||||
</FieldState>
|
{:else}
|
||||||
</LabelContainer>
|
<PlainTextBadge
|
||||||
</svelte:fragment>
|
visible={!fieldsCollapsed[index] &&
|
||||||
<svelte:fragment slot="rich-text-input">
|
(fields[index] === $hoveredField ||
|
||||||
<Collapsible
|
fields[index] === $focusedField)}
|
||||||
collapse={richTextsHidden[index]}
|
bind:off={plainTextsHidden[index]}
|
||||||
let:collapsed={hidden}
|
on:toggle={async () => {
|
||||||
>
|
plainTextsHidden[index] =
|
||||||
<RichTextInput
|
!plainTextsHidden[index];
|
||||||
{hidden}
|
|
||||||
on:focusout={() => {
|
|
||||||
saveFieldNow();
|
|
||||||
$focusedInput = null;
|
|
||||||
}}
|
|
||||||
bind:this={richTextInputs[index]}
|
|
||||||
>
|
|
||||||
<ImageHandle maxWidth={250} maxHeight={125} />
|
|
||||||
<MathjaxHandle />
|
|
||||||
{#if insertSymbols}
|
|
||||||
<SymbolsOverlay />
|
|
||||||
{/if}
|
|
||||||
<FieldDescription>
|
|
||||||
{field.description}
|
|
||||||
</FieldDescription>
|
|
||||||
</RichTextInput>
|
|
||||||
</Collapsible>
|
|
||||||
</svelte:fragment>
|
|
||||||
<svelte:fragment slot="plain-text-input">
|
|
||||||
<Collapsible
|
|
||||||
collapse={plainTextsHidden[index]}
|
|
||||||
let:collapsed={hidden}
|
|
||||||
>
|
|
||||||
<PlainTextInput
|
|
||||||
{hidden}
|
|
||||||
isDefault={plainTextDefaults[index]}
|
|
||||||
richTextHidden={richTextsHidden[index]}
|
|
||||||
on:focusout={() => {
|
|
||||||
saveFieldNow();
|
|
||||||
$focusedInput = null;
|
|
||||||
}}
|
|
||||||
bind:this={plainTextInputs[index]}
|
|
||||||
/>
|
|
||||||
</Collapsible>
|
|
||||||
</svelte:fragment>
|
|
||||||
</EditorField>
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<MathjaxElement />
|
if (!plainTextsHidden[index]) {
|
||||||
<FrameElement />
|
refocusInput(
|
||||||
</DecoratedElements>
|
plainTextInputs[index].api,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
{/if}
|
||||||
|
<slot
|
||||||
|
name="field-state"
|
||||||
|
{field}
|
||||||
|
{index}
|
||||||
|
visible={fields[index] === $hoveredField ||
|
||||||
|
fields[index] === $focusedField}
|
||||||
|
/>
|
||||||
|
</FieldState>
|
||||||
|
</LabelContainer>
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="rich-text-input">
|
||||||
|
<Collapsible
|
||||||
|
collapse={richTextsHidden[index]}
|
||||||
|
let:collapsed={hidden}
|
||||||
|
toggleDisplay
|
||||||
|
>
|
||||||
|
<RichTextInput
|
||||||
|
{hidden}
|
||||||
|
on:focusout={() => {
|
||||||
|
saveFieldNow();
|
||||||
|
$focusedInput = null;
|
||||||
|
}}
|
||||||
|
bind:this={richTextInputs[index]}
|
||||||
|
>
|
||||||
|
<FieldDescription>
|
||||||
|
{field.description}
|
||||||
|
</FieldDescription>
|
||||||
|
</RichTextInput>
|
||||||
|
</Collapsible>
|
||||||
|
</svelte:fragment>
|
||||||
|
<svelte:fragment slot="plain-text-input">
|
||||||
|
<Collapsible
|
||||||
|
collapse={plainTextsHidden[index]}
|
||||||
|
let:collapsed={hidden}
|
||||||
|
toggleDisplay
|
||||||
|
>
|
||||||
|
<PlainTextInput
|
||||||
|
{hidden}
|
||||||
|
on:focusout={() => {
|
||||||
|
saveFieldNow();
|
||||||
|
$focusedInput = null;
|
||||||
|
}}
|
||||||
|
bind:this={plainTextInputs[index]}
|
||||||
|
/>
|
||||||
|
</Collapsible>
|
||||||
|
</svelte:fragment>
|
||||||
|
</EditorField>
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<MathjaxOverlay />
|
||||||
|
<ImageOverlay maxWidth={250} maxHeight={125} />
|
||||||
|
{#if insertSymbols}
|
||||||
|
<SymbolsOverlay />
|
||||||
|
{/if}
|
||||||
</Fields>
|
</Fields>
|
||||||
</PaneContent>
|
</PaneContent>
|
||||||
</Pane>
|
</Pane>
|
||||||
|
|
31
ts/editor/decorated-elements.ts
Normal file
31
ts/editor/decorated-elements.ts
Normal file
|
@ -0,0 +1,31 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import { CustomElementArray } from "../editable/decorated";
|
||||||
|
import { FrameElement } from "../editable/frame-element";
|
||||||
|
import { FrameEnd, FrameStart } from "../editable/frame-handle";
|
||||||
|
import { Mathjax } from "../editable/mathjax-element";
|
||||||
|
import { BLOCK_ELEMENTS } from "../lib/dom";
|
||||||
|
import { parsingInstructions } from "./plain-text-input";
|
||||||
|
|
||||||
|
const decoratedElements = new CustomElementArray();
|
||||||
|
|
||||||
|
function registerMathjax() {
|
||||||
|
decoratedElements.push(Mathjax);
|
||||||
|
parsingInstructions.push("<style>anki-mathjax { white-space: pre; }</style>");
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerFrameElement() {
|
||||||
|
customElements.define(FrameElement.tagName, FrameElement);
|
||||||
|
customElements.define(FrameStart.tagName, FrameStart);
|
||||||
|
customElements.define(FrameEnd.tagName, FrameEnd);
|
||||||
|
|
||||||
|
/* This will ensure that they are not targeted by surrounding algorithms */
|
||||||
|
BLOCK_ELEMENTS.push(FrameStart.tagName.toUpperCase());
|
||||||
|
BLOCK_ELEMENTS.push(FrameEnd.tagName.toUpperCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
registerMathjax();
|
||||||
|
registerFrameElement();
|
||||||
|
|
||||||
|
export { decoratedElements };
|
|
@ -69,7 +69,7 @@ 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 = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||||
|
|
||||||
let showFloating = false;
|
let showFloating = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -25,7 +25,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
execCommand(key);
|
execCommand(key);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !editingInputIsRichText($focusedInput);
|
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if withoutState}
|
{#if withoutState}
|
||||||
|
|
|
@ -72,7 +72,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
[onLatexMathEnv, "Control+T, M", tr.editingLatexMathEnv()],
|
[onLatexMathEnv, "Control+T, M", tr.editingLatexMathEnv()],
|
||||||
];
|
];
|
||||||
|
|
||||||
$: disabled = !editingInputIsRichText($focusedInput);
|
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||||
|
|
||||||
let showFloating = false;
|
let showFloating = false;
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -19,7 +19,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { getPlatformString } from "../../lib/shortcuts";
|
import { getPlatformString } from "../../lib/shortcuts";
|
||||||
import { context } from "../NoteEditor.svelte";
|
import { context } from "../NoteEditor.svelte";
|
||||||
import { setFormat } from "../old-editor-adapter";
|
import { setFormat } from "../old-editor-adapter";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { editingInputIsRichText, RichTextInputAPI } from "../rich-text-input";
|
||||||
import { micIcon, paperclipIcon } from "./icons";
|
import { micIcon, paperclipIcon } from "./icons";
|
||||||
import LatexButton from "./LatexButton.svelte";
|
import LatexButton from "./LatexButton.svelte";
|
||||||
|
|
||||||
|
@ -35,12 +35,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
function attachMediaOnFocus(): void {
|
function attachMediaOnFocus(): void {
|
||||||
if (!editingInputIsRichText($focusedInput)) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[mediaPromise, resolve] = promiseWithResolver<string>();
|
[mediaPromise, resolve] = promiseWithResolver<string>();
|
||||||
$focusedInput.editable.focusHandler.focus.on(
|
($focusedInput as RichTextInputAPI).editable.focusHandler.focus.on(
|
||||||
async () => setFormat("inserthtml", await mediaPromise),
|
async () => setFormat("inserthtml", await mediaPromise),
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
|
@ -55,12 +55,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const recordCombination = "F5";
|
const recordCombination = "F5";
|
||||||
|
|
||||||
function attachRecordingOnFocus(): void {
|
function attachRecordingOnFocus(): void {
|
||||||
if (!editingInputIsRichText($focusedInput)) {
|
if (disabled) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
[mediaPromise, resolve] = promiseWithResolver<string>();
|
[mediaPromise, resolve] = promiseWithResolver<string>();
|
||||||
$focusedInput.editable.focusHandler.focus.on(
|
($focusedInput as RichTextInputAPI).editable.focusHandler.focus.on(
|
||||||
async () => setFormat("inserthtml", await mediaPromise),
|
async () => setFormat("inserthtml", await mediaPromise),
|
||||||
{ once: true },
|
{ once: true },
|
||||||
);
|
);
|
||||||
|
@ -68,7 +68,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
bridgeCommand("record");
|
bridgeCommand("record");
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !editingInputIsRichText($focusedInput);
|
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||||
|
|
||||||
export let api = {};
|
export let api = {};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -9,7 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { onMount, tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
|
|
||||||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||||
import Popover from "../../components/Popover.svelte";
|
import Popover from "../../components/Popover.svelte";
|
||||||
|
@ -18,17 +18,47 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { on } from "../../lib/events";
|
import { on } from "../../lib/events";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
import { removeStyleProperties } from "../../lib/styling";
|
import { removeStyleProperties } from "../../lib/styling";
|
||||||
|
import type { Callback } from "../../lib/typing";
|
||||||
|
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||||
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 { context } from "../rich-text-input";
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import {
|
||||||
|
editingInputIsRichText,
|
||||||
|
lifecycle as richTextLifecycle,
|
||||||
|
} from "../rich-text-input";
|
||||||
import FloatButtons from "./FloatButtons.svelte";
|
import FloatButtons from "./FloatButtons.svelte";
|
||||||
import SizeSelect from "./SizeSelect.svelte";
|
import SizeSelect from "./SizeSelect.svelte";
|
||||||
|
|
||||||
export let maxWidth: number;
|
export let maxWidth: number;
|
||||||
export let maxHeight: number;
|
export let maxHeight: number;
|
||||||
|
|
||||||
const { element } = context.get();
|
richTextLifecycle.onMount(({ element }: RichTextInputAPI): void => {
|
||||||
|
(async () => {
|
||||||
|
const container = await element;
|
||||||
|
|
||||||
|
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
|
||||||
|
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
|
||||||
|
})();
|
||||||
|
});
|
||||||
|
|
||||||
|
const { focusedInput } = context.get();
|
||||||
|
|
||||||
|
let cleanup: Callback;
|
||||||
|
|
||||||
|
async function initialize(input: EditingInputAPI | null): Promise<void> {
|
||||||
|
cleanup?.();
|
||||||
|
|
||||||
|
if (!input || !editingInputIsRichText(input)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup = on(await input.element, "click", maybeShowHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
$: initialize($focusedInput);
|
||||||
|
|
||||||
let activeImage: HTMLImageElement | null = null;
|
let activeImage: HTMLImageElement | null = null;
|
||||||
|
|
||||||
|
@ -80,23 +110,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
await tick();
|
await tick();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function maybeShowHandle(event: Event): Promise<void> {
|
let naturalWidth: number;
|
||||||
if (event.target instanceof HTMLImageElement) {
|
let naturalHeight: number;
|
||||||
const image = event.target;
|
let aspectRatio: number;
|
||||||
|
|
||||||
if (!image.dataset.anki) {
|
|
||||||
activeImage = image;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$: naturalWidth = activeImage?.naturalWidth;
|
|
||||||
$: naturalHeight = activeImage?.naturalHeight;
|
|
||||||
$: aspectRatio = naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
|
||||||
|
|
||||||
let customDimensions: boolean = false;
|
|
||||||
let actualWidth = "";
|
|
||||||
let actualHeight = "";
|
|
||||||
|
|
||||||
function updateDimensions() {
|
function updateDimensions() {
|
||||||
/* we do not want the actual width, but rather the intended display width */
|
/* we do not want the actual width, but rather the intended display width */
|
||||||
|
@ -121,31 +137,26 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let updateSelection: () => Promise<void>;
|
async function maybeShowHandle(event: Event): Promise<void> {
|
||||||
|
if (event.target instanceof HTMLImageElement) {
|
||||||
|
const image = event.target;
|
||||||
|
|
||||||
async function updateSizesWithDimensions() {
|
if (!image.dataset.anki) {
|
||||||
await updateSelection?.();
|
activeImage = image;
|
||||||
updateDimensions();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* window resizing */
|
naturalWidth = activeImage?.naturalWidth;
|
||||||
const resizeObserver = new ResizeObserver(async () => {
|
naturalHeight = activeImage?.naturalHeight;
|
||||||
await updateSizesWithDimensions();
|
aspectRatio =
|
||||||
});
|
naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
||||||
|
|
||||||
$: observes = Boolean(activeImage);
|
updateDimensions();
|
||||||
|
}
|
||||||
async function toggleResizeObserver(observes: boolean) {
|
|
||||||
const container = await element;
|
|
||||||
|
|
||||||
if (observes) {
|
|
||||||
resizeObserver.observe(container);
|
|
||||||
} else {
|
|
||||||
resizeObserver.unobserve(container);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$: toggleResizeObserver(observes);
|
let customDimensions: boolean = false;
|
||||||
|
let actualWidth = "";
|
||||||
|
let actualHeight = "";
|
||||||
|
|
||||||
/* memoized position of image on resize start
|
/* memoized position of image on resize start
|
||||||
* prevents frantic behavior when image shift into the next/previous line */
|
* prevents frantic behavior when image shift into the next/previous line */
|
||||||
|
@ -228,15 +239,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
activeImage!.removeAttribute("width");
|
activeImage!.removeAttribute("width");
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(async () => {
|
|
||||||
const container = await element;
|
|
||||||
|
|
||||||
container.style.setProperty("--editor-shrink-max-width", `${maxWidth}px`);
|
|
||||||
container.style.setProperty("--editor-shrink-max-height", `${maxHeight}px`);
|
|
||||||
|
|
||||||
return on(container, "click", maybeShowHandle);
|
|
||||||
});
|
|
||||||
|
|
||||||
let shrinkingDisabled: boolean;
|
let shrinkingDisabled: boolean;
|
||||||
$: shrinkingDisabled =
|
$: shrinkingDisabled =
|
||||||
Number(actualWidth) <= maxWidth && Number(actualHeight) <= maxHeight;
|
Number(actualWidth) <= maxWidth && Number(actualHeight) <= maxHeight;
|
||||||
|
@ -244,9 +246,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let restoringDisabled: boolean;
|
let restoringDisabled: boolean;
|
||||||
$: restoringDisabled = !activeImage?.hasAttribute("width") ?? true;
|
$: restoringDisabled = !activeImage?.hasAttribute("width") ?? true;
|
||||||
|
|
||||||
const widthObserver = new MutationObserver(
|
const widthObserver = new MutationObserver(() => {
|
||||||
() => (restoringDisabled = !activeImage!.hasAttribute("width")),
|
restoringDisabled = !activeImage!.hasAttribute("width");
|
||||||
);
|
updateDimensions();
|
||||||
|
});
|
||||||
|
|
||||||
$: activeImage
|
$: activeImage
|
||||||
? widthObserver.observe(activeImage, {
|
? widthObserver.observe(activeImage, {
|
||||||
|
|
|
@ -4,7 +4,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type CodeMirrorLib from "codemirror";
|
import type CodeMirrorLib from "codemirror";
|
||||||
import { onMount, tick } from "svelte";
|
import { tick } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
import Popover from "../../components/Popover.svelte";
|
import Popover from "../../components/Popover.svelte";
|
||||||
|
@ -16,20 +16,58 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { Mathjax } from "../../editable/mathjax-element";
|
import { Mathjax } from "../../editable/mathjax-element";
|
||||||
import { hasBlockAttribute } from "../../lib/dom";
|
import { hasBlockAttribute } from "../../lib/dom";
|
||||||
import { on } from "../../lib/events";
|
import { on } from "../../lib/events";
|
||||||
import { noop } from "../../lib/functional";
|
import { promiseWithResolver } from "../../lib/promise";
|
||||||
import type { Callback } from "../../lib/typing";
|
import type { Callback } from "../../lib/typing";
|
||||||
import { singleCallback } from "../../lib/typing";
|
import { singleCallback } from "../../lib/typing";
|
||||||
|
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||||
import HandleBackground from "../HandleBackground.svelte";
|
import HandleBackground from "../HandleBackground.svelte";
|
||||||
import { context } from "../rich-text-input";
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import type { RichTextInputAPI } from "../rich-text-input";
|
||||||
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import MathjaxButtons from "./MathjaxButtons.svelte";
|
import MathjaxButtons from "./MathjaxButtons.svelte";
|
||||||
import MathjaxEditor from "./MathjaxEditor.svelte";
|
import MathjaxEditor from "./MathjaxEditor.svelte";
|
||||||
|
|
||||||
const { editable, element, preventResubscription } = context.get();
|
const { focusedInput } = context.get();
|
||||||
|
|
||||||
|
let cleanup: Callback;
|
||||||
|
let richTextInput: RichTextInputAPI | null = null;
|
||||||
|
let allowPromise = Promise.resolve();
|
||||||
|
|
||||||
|
async function initialize(input: EditingInputAPI | null): Promise<void> {
|
||||||
|
cleanup?.();
|
||||||
|
|
||||||
|
const isRichText = input && editingInputIsRichText(input);
|
||||||
|
|
||||||
|
// Setup the new field, so that clicking from one mathjax to another
|
||||||
|
// will immediately open the overlay
|
||||||
|
if (isRichText) {
|
||||||
|
const container = await input.element;
|
||||||
|
|
||||||
|
cleanup = singleCallback(
|
||||||
|
on(container, "click", showOverlayIfMathjaxClicked),
|
||||||
|
on(container, "movecaretafter" as any, showOnAutofocus),
|
||||||
|
on(container, "selectall" as any, showSelectAll),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait if the mathjax overlay is still active
|
||||||
|
await allowPromise;
|
||||||
|
|
||||||
|
if (!isRichText) {
|
||||||
|
richTextInput = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
richTextInput = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: initialize($focusedInput);
|
||||||
|
|
||||||
let activeImage: HTMLImageElement | null = null;
|
let activeImage: HTMLImageElement | null = null;
|
||||||
let mathjaxElement: HTMLElement | null = null;
|
let mathjaxElement: HTMLElement | null = null;
|
||||||
let allow = noop;
|
|
||||||
let unsubscribe = noop;
|
let allowResubscription: Callback;
|
||||||
|
let unsubscribe: Callback;
|
||||||
|
|
||||||
let selectAll = false;
|
let selectAll = false;
|
||||||
let position: CodeMirrorLib.Position | undefined = undefined;
|
let position: CodeMirrorLib.Position | undefined = undefined;
|
||||||
|
@ -40,8 +78,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
*/
|
*/
|
||||||
const code = writable("");
|
const code = writable("");
|
||||||
|
|
||||||
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position): void {
|
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position) {
|
||||||
allow = preventResubscription();
|
const [promise, allowResolve] = promiseWithResolver<void>();
|
||||||
|
|
||||||
|
allowPromise = promise;
|
||||||
|
allowResubscription = singleCallback(
|
||||||
|
richTextInput!.preventResubscription(),
|
||||||
|
allowResolve,
|
||||||
|
);
|
||||||
|
|
||||||
position = pos;
|
position = pos;
|
||||||
|
|
||||||
/* Setting the activeImage and mathjaxElement to a non-nullish value is
|
/* Setting the activeImage and mathjaxElement to a non-nullish value is
|
||||||
|
@ -56,7 +101,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
function placeHandle(after: boolean): void {
|
function placeHandle(after: boolean): void {
|
||||||
editable.focusHandler.flushCaret();
|
richTextInput!.editable.focusHandler.flushCaret();
|
||||||
|
|
||||||
if (after) {
|
if (after) {
|
||||||
(mathjaxElement as any).placeCaretAfter();
|
(mathjaxElement as any).placeCaretAfter();
|
||||||
|
@ -65,50 +110,46 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function resetHandle(): Promise<void> {
|
||||||
|
selectAll = false;
|
||||||
|
position = undefined;
|
||||||
|
|
||||||
|
allowResubscription?.();
|
||||||
|
|
||||||
|
if (activeImage && mathjaxElement) {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function clear(): void {
|
function clear(): void {
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
activeImage = null;
|
activeImage = null;
|
||||||
mathjaxElement = null;
|
mathjaxElement = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function resetHandle(): Promise<void> {
|
|
||||||
selectAll = false;
|
|
||||||
position = undefined;
|
|
||||||
|
|
||||||
if (activeImage && mathjaxElement) {
|
|
||||||
clear();
|
|
||||||
}
|
|
||||||
|
|
||||||
allow();
|
|
||||||
|
|
||||||
// Wait for a tick, so that moving from one Mathjax element to
|
|
||||||
// another will remount the MathjaxEditor
|
|
||||||
await tick();
|
|
||||||
}
|
|
||||||
|
|
||||||
let errorMessage: string;
|
let errorMessage: string;
|
||||||
let cleanup: Callback | null = null;
|
let cleanupImageError: Callback | null = null;
|
||||||
|
|
||||||
async function updateErrorMessage(): Promise<void> {
|
async function updateErrorMessage(): Promise<void> {
|
||||||
errorMessage = activeImage!.title;
|
errorMessage = activeImage!.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateImageErrorCallback(image: HTMLImageElement | null) {
|
async function updateImageErrorCallback(image: HTMLImageElement | null) {
|
||||||
cleanup?.();
|
cleanupImageError?.();
|
||||||
cleanup = null;
|
cleanupImageError = null;
|
||||||
|
|
||||||
if (!image) {
|
if (!image) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanup = on(image, "resize", updateErrorMessage);
|
cleanupImageError = on(image, "resize", updateErrorMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: updateImageErrorCallback(activeImage);
|
$: updateImageErrorCallback(activeImage);
|
||||||
|
|
||||||
async function showOverlayIfMathjaxClicked({ target }: Event): Promise<void> {
|
async function showOverlayIfMathjaxClicked({ target }: Event): Promise<void> {
|
||||||
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
||||||
await resetHandle();
|
resetHandle();
|
||||||
showOverlay(target);
|
showOverlay(target);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -136,16 +177,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
showOverlay(detail);
|
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;
|
let isBlock: boolean;
|
||||||
$: isBlock = mathjaxElement ? hasBlockAttribute(mathjaxElement) : false;
|
$: isBlock = mathjaxElement ? hasBlockAttribute(mathjaxElement) : false;
|
||||||
|
|
||||||
|
@ -184,24 +215,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
{code}
|
{code}
|
||||||
{selectAll}
|
{selectAll}
|
||||||
{position}
|
{position}
|
||||||
on:moveoutstart={async () => {
|
on:moveoutstart={() => {
|
||||||
placeHandle(false);
|
placeHandle(false);
|
||||||
await resetHandle();
|
resetHandle();
|
||||||
}}
|
}}
|
||||||
on:moveoutend={async () => {
|
on:moveoutend={() => {
|
||||||
placeHandle(true);
|
placeHandle(true);
|
||||||
await resetHandle();
|
resetHandle();
|
||||||
}}
|
}}
|
||||||
on:blur={async () => {
|
on:tab={async () => {
|
||||||
await resetHandle();
|
// 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?)
|
||||||
|
resetHandle();
|
||||||
}}
|
}}
|
||||||
let:editor={mathjaxEditor}
|
let:editor={mathjaxEditor}
|
||||||
>
|
>
|
||||||
<Shortcut
|
<Shortcut
|
||||||
keyCombination={acceptShortcut}
|
keyCombination={acceptShortcut}
|
||||||
on:action={async () => {
|
on:action={() => {
|
||||||
placeHandle(true);
|
placeHandle(true);
|
||||||
await resetHandle();
|
resetHandle();
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
@ -238,7 +272,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
</WithFloating>
|
</WithFloating>
|
||||||
|
|
||||||
<svelte:fragment slot="overlay">
|
<svelte:fragment slot="overlay">
|
||||||
<HandleBackground tooltip={errorMessage} />
|
<HandleBackground
|
||||||
|
tooltip={errorMessage}
|
||||||
|
--handle-background-color="var(--code-bg)"
|
||||||
|
/>
|
||||||
</svelte:fragment>
|
</svelte:fragment>
|
||||||
</WithOverlay>
|
</WithOverlay>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
@ -40,9 +40,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import removeProhibitedTags from "./remove-prohibited";
|
import removeProhibitedTags from "./remove-prohibited";
|
||||||
import { storedToUndecorated, undecoratedToStored } from "./transform";
|
import { storedToUndecorated, undecoratedToStored } from "./transform";
|
||||||
|
|
||||||
export let isDefault: boolean;
|
|
||||||
export let hidden = false;
|
export let hidden = false;
|
||||||
export let richTextHidden: boolean;
|
|
||||||
|
|
||||||
$: configuration = {
|
$: configuration = {
|
||||||
mode: htmlanki,
|
mode: htmlanki,
|
||||||
|
@ -147,8 +145,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<div
|
<div
|
||||||
class="plain-text-input"
|
class="plain-text-input"
|
||||||
class:light-theme={!$pageTheme.isDark}
|
class:light-theme={!$pageTheme.isDark}
|
||||||
class:is-default={isDefault}
|
|
||||||
class:alone={richTextHidden}
|
|
||||||
on:focusin={() => ($focusedInput = api)}
|
on:focusin={() => ($focusedInput = api)}
|
||||||
{hidden}
|
{hidden}
|
||||||
>
|
>
|
||||||
|
@ -163,31 +159,11 @@ 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 {
|
||||||
border-top: 1px solid var(--border);
|
height: 100%;
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
|
|
||||||
:global(.CodeMirror) {
|
:global(.CodeMirror) {
|
||||||
|
height: 100%;
|
||||||
background: var(--canvas-code);
|
background: var(--canvas-code);
|
||||||
border-radius: 0 0 5px 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.is-default {
|
|
||||||
border-top: none;
|
|
||||||
border-bottom: 1px solid var(--border);
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
|
|
||||||
:global(.CodeMirror) {
|
|
||||||
border-radius: 5px 5px 0 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.alone {
|
|
||||||
border: none;
|
|
||||||
border-radius: 5px;
|
|
||||||
|
|
||||||
:global(.CodeMirror) {
|
|
||||||
border-radius: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
:global(.CodeMirror-lines) {
|
:global(.CodeMirror-lines) {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
// 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 { decoratedElements } from "../DecoratedElements.svelte";
|
import { decoratedElements } from "../decorated-elements";
|
||||||
|
|
||||||
export function storedToUndecorated(html: string): string {
|
export function storedToUndecorated(html: string): string {
|
||||||
return decoratedElements.toUndecorated(html);
|
return decoratedElements.toUndecorated(html);
|
||||||
|
|
|
@ -25,9 +25,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
function editingInputIsRichText(
|
function editingInputIsRichText(
|
||||||
editingInput: EditingInputAPI | null,
|
editingInput: EditingInputAPI,
|
||||||
): editingInput is RichTextInputAPI {
|
): editingInput is RichTextInputAPI {
|
||||||
return editingInput?.name === "rich-text";
|
return editingInput.name === "rich-text";
|
||||||
}
|
}
|
||||||
|
|
||||||
import { registerPackage } from "../../lib/runtime-require";
|
import { registerPackage } from "../../lib/runtime-require";
|
||||||
|
@ -54,6 +54,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
context,
|
context,
|
||||||
editingInputIsRichText,
|
editingInputIsRichText,
|
||||||
globalInputHandler as inputHandler,
|
globalInputHandler as inputHandler,
|
||||||
|
lifecycle,
|
||||||
surrounder,
|
surrounder,
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
@ -126,7 +127,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return hidden;
|
return hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
const className = "rich-text-editable";
|
|
||||||
let richTextDiv: HTMLElement;
|
let richTextDiv: HTMLElement;
|
||||||
|
|
||||||
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
|
async function getInputAPI(target: EventTarget): Promise<FocusableInputAPI | null> {
|
||||||
|
@ -221,29 +221,35 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let:attachToShadow={attachStyles}
|
let:attachToShadow={attachStyles}
|
||||||
let:stylesDidLoad
|
let:stylesDidLoad
|
||||||
>
|
>
|
||||||
<div
|
<div class="rich-text-relative">
|
||||||
bind:this={richTextDiv}
|
<div
|
||||||
class={className}
|
class="rich-text-editable"
|
||||||
class:night-mode={$pageTheme.isDark}
|
bind:this={richTextDiv}
|
||||||
use:attachShadow
|
use:attachShadow
|
||||||
use:attachStyles
|
use:attachStyles
|
||||||
use:attachContentEditable={{ stylesDidLoad }}
|
use:attachContentEditable={{ stylesDidLoad }}
|
||||||
on:focusin
|
on:focusin
|
||||||
on:focusout
|
on:focusout
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{#await Promise.all([richTextPromise, stylesDidLoad]) then _}
|
{#await Promise.all([richTextPromise, stylesDidLoad]) then _}
|
||||||
<div class="rich-text-widgets">
|
<div class="rich-text-widgets">
|
||||||
<slot />
|
<slot />
|
||||||
</div>
|
</div>
|
||||||
{/await}
|
{/await}
|
||||||
|
</div>
|
||||||
</RichTextStyles>
|
</RichTextStyles>
|
||||||
<slot name="plain-text-badge" />
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.rich-text-input {
|
.rich-text-input {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
background-color: var(--canvas-elevated);
|
||||||
|
padding: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.rich-text-relative {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: 6px;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
import type { DecoratedElement } from "../../editable/decorated";
|
import type { DecoratedElement } from "../../editable/decorated";
|
||||||
import type { NodeStore } from "../../sveltelib/node-store";
|
import type { NodeStore } from "../../sveltelib/node-store";
|
||||||
import { nodeStore } from "../../sveltelib/node-store";
|
import { nodeStore } from "../../sveltelib/node-store";
|
||||||
import { decoratedElements } from "../DecoratedElements.svelte";
|
import { decoratedElements } from "../decorated-elements";
|
||||||
|
|
||||||
function normalizeFragment(fragment: DocumentFragment): void {
|
function normalizeFragment(fragment: DocumentFragment): void {
|
||||||
fragment.normalize();
|
fragment.normalize();
|
||||||
|
|
|
@ -7,7 +7,7 @@ import {
|
||||||
nodeIsElement,
|
nodeIsElement,
|
||||||
} from "../../lib/dom";
|
} from "../../lib/dom";
|
||||||
import { createDummyDoc } from "../../lib/parsing";
|
import { createDummyDoc } from "../../lib/parsing";
|
||||||
import { decoratedElements } from "../DecoratedElements.svelte";
|
import { decoratedElements } from "../decorated-elements";
|
||||||
|
|
||||||
function adjustInputHTML(html: string): string {
|
function adjustInputHTML(html: string): string {
|
||||||
for (const component of decoratedElements) {
|
for (const component of decoratedElements) {
|
||||||
|
|
|
@ -3,7 +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 { getContext, onMount } from "svelte";
|
import { getContext } from "svelte";
|
||||||
import type { Readable } from "svelte/store";
|
import type { Readable } from "svelte/store";
|
||||||
|
|
||||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||||
|
@ -15,7 +15,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import type { Callback } from "../../lib/typing";
|
import type { Callback } from "../../lib/typing";
|
||||||
import { singleCallback } from "../../lib/typing";
|
import { singleCallback } from "../../lib/typing";
|
||||||
import type { SpecialKeyParams } from "../../sveltelib/input-handler";
|
import type { SpecialKeyParams } from "../../sveltelib/input-handler";
|
||||||
import { context } from "../rich-text-input";
|
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||||
|
import { context } from "../NoteEditor.svelte";
|
||||||
|
import {
|
||||||
|
editingInputIsRichText,
|
||||||
|
RichTextInputAPI,
|
||||||
|
} from "../rich-text-input/RichTextInput.svelte";
|
||||||
import { findSymbols, getAutoInsertSymbol, getExactSymbol } from "./symbols-table";
|
import { findSymbols, getAutoInsertSymbol, getExactSymbol } from "./symbols-table";
|
||||||
import type {
|
import type {
|
||||||
SymbolsEntry as SymbolsEntryType,
|
SymbolsEntry as SymbolsEntryType,
|
||||||
|
@ -29,19 +34,39 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const whitespaceCharacters = [" ", "\u00a0"];
|
const whitespaceCharacters = [" ", "\u00a0"];
|
||||||
|
|
||||||
const { editable, inputHandler } = context.get();
|
const { focusedInput } = context.get();
|
||||||
|
|
||||||
|
let cleanup: Callback;
|
||||||
|
let richTextInput: RichTextInputAPI | null = null;
|
||||||
|
|
||||||
|
async function initialize(input: EditingInputAPI | null): Promise<void> {
|
||||||
|
cleanup?.();
|
||||||
|
|
||||||
|
if (!input || !editingInputIsRichText(input)) {
|
||||||
|
richTextInput = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanup = input.inputHandler.beforeInput.on(
|
||||||
|
async (input: { event: Event }): Promise<void> => onBeforeInput(input),
|
||||||
|
);
|
||||||
|
richTextInput = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
$: initialize($focusedInput);
|
||||||
|
|
||||||
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
const fontFamily = getContext<Readable<string>>(fontFamilyKey);
|
||||||
|
|
||||||
let foundSymbols: SymbolsTable = [];
|
let foundSymbols: SymbolsTable = [];
|
||||||
|
|
||||||
let referenceRange: Range | undefined = undefined;
|
let referenceRange: Range | null = null;
|
||||||
let activeItem = 0;
|
let activeItem: number | null = null;
|
||||||
let cleanup: Callback;
|
let cleanupReferenceRange: Callback;
|
||||||
|
|
||||||
function unsetReferenceRange() {
|
function unsetReferenceRange() {
|
||||||
referenceRange = undefined;
|
referenceRange = null;
|
||||||
activeItem = 0;
|
activeItem = null;
|
||||||
cleanup?.();
|
cleanupReferenceRange?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceText(selection: Selection, text: Text, nodes: Node[]): void {
|
function replaceText(selection: Selection, text: Text, nodes: Node[]): void {
|
||||||
|
@ -90,7 +115,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
replacementLength,
|
replacementLength,
|
||||||
);
|
);
|
||||||
|
|
||||||
inputHandler.insertText.on(
|
richTextInput!.inputHandler.insertText.on(
|
||||||
async ({ text }) => {
|
async ({ text }) => {
|
||||||
replaceText(
|
replaceText(
|
||||||
selection,
|
selection,
|
||||||
|
@ -217,13 +242,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
if (foundSymbols.length > 0) {
|
if (foundSymbols.length > 0) {
|
||||||
referenceRange = currentRange;
|
referenceRange = currentRange;
|
||||||
cleanup = singleCallback(
|
activeItem = 0;
|
||||||
editable.focusHandler.blur.on(async () => unsetReferenceRange(), {
|
|
||||||
once: true,
|
cleanupReferenceRange = singleCallback(
|
||||||
}),
|
richTextInput!.editable.focusHandler.blur.on(
|
||||||
inputHandler.pointerDown.on(async () => unsetReferenceRange()),
|
async () => unsetReferenceRange(),
|
||||||
inputHandler.specialKey.on(async (input: SpecialKeyParams) =>
|
{
|
||||||
onSpecialKey(input),
|
once: true,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
richTextInput!.inputHandler.pointerDown.on(async () =>
|
||||||
|
unsetReferenceRange(),
|
||||||
|
),
|
||||||
|
richTextInput!.inputHandler.specialKey.on(
|
||||||
|
async (input: SpecialKeyParams) => onSpecialKey(input),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -292,7 +324,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
replacementLength,
|
replacementLength,
|
||||||
);
|
);
|
||||||
|
|
||||||
inputHandler.insertText.on(
|
richTextInput!.inputHandler.insertText.on(
|
||||||
async ({ text }) =>
|
async ({ text }) =>
|
||||||
replaceText(selection, text, symbolsEntryToReplacement(symbolEntry)),
|
replaceText(selection, text, symbolsEntryToReplacement(symbolEntry)),
|
||||||
{
|
{
|
||||||
|
@ -314,7 +346,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
// We have to wait for afterInput to update the symbols, because we also
|
// We have to wait for afterInput to update the symbols, because we also
|
||||||
// want to update in the case of a deletion
|
// want to update in the case of a deletion
|
||||||
inputHandler.afterInput.on(
|
richTextInput!.inputHandler.afterInput.on(
|
||||||
async (): Promise<void> => {
|
async (): Promise<void> => {
|
||||||
const currentRange = getRange(selection)!;
|
const currentRange = getRange(selection)!;
|
||||||
const query = findValidSearchQuery(selection, currentRange);
|
const query = findValidSearchQuery(selection, currentRange);
|
||||||
|
@ -344,12 +376,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
maybeShowOverlay(selection, event);
|
maybeShowOverlay(selection, event);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() =>
|
|
||||||
inputHandler.beforeInput.on(
|
|
||||||
async (input: { event: Event }): Promise<void> => onBeforeInput(input),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="symbols-overlay">
|
<div class="symbols-overlay">
|
||||||
|
@ -392,8 +418,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
max-height: 15rem;
|
max-height: 15rem;
|
||||||
|
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
overflow-x: hidden;
|
overflow: hidden auto;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
overflow-y: auto;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Reference in a new issue