Finish #2070: Single overlay instead of per field (#2144)

* 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:
Matthias Metelka 2022-10-27 01:11:36 +02:00 committed by GitHub
parent 7942518d64
commit 68fa661b53
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
28 changed files with 463 additions and 548 deletions

View file

@ -83,6 +83,7 @@ sass_library(
visibility = ["//visibility:public"], visibility = ["//visibility:public"],
deps = [ deps = [
"vars_lib", "vars_lib",
"elevation_lib",
], ],
) )

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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