mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 08:46:37 -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"],
|
||||
deps = [
|
||||
"vars_lib",
|
||||
"elevation_lib",
|
||||
],
|
||||
)
|
||||
|
||||
|
|
|
@ -59,7 +59,7 @@ $umbra-opacity: 0.2;
|
|||
$penumbra-opacity: 0.14;
|
||||
$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);
|
||||
$penumbra-z-value: map.get($penumbra-map, $level);
|
||||
$ambient-z-value: map.get($ambient-map, $level);
|
||||
|
@ -75,6 +75,10 @@ $ambient-opacity: 0.12;
|
|||
);
|
||||
}
|
||||
|
||||
@mixin elevation($level, $other: ()) {
|
||||
box-shadow: list.join(box-shadow($level), $other);
|
||||
@mixin elevation($level, $opacity-boost: 0, $color: black) {
|
||||
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";
|
||||
|
||||
export let collapse = false;
|
||||
export let toggleDisplay = false;
|
||||
export let animated = !document.body.classList.contains("reduced-motion");
|
||||
|
||||
let collapsed = false;
|
||||
|
@ -62,6 +63,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
class="collapsible"
|
||||
class:animated
|
||||
class:expanded
|
||||
class:full-hide={toggleDisplay}
|
||||
class:collapsed={!expanded}
|
||||
class:measuring
|
||||
class:transitioning
|
||||
style:--height="{height}px"
|
||||
|
@ -75,8 +78,18 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{/if}
|
||||
|
||||
<style lang="scss">
|
||||
.collapsible.full-hide {
|
||||
&.collapsed {
|
||||
display: none;
|
||||
}
|
||||
&.transitioning {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
||||
.collapsible.animated {
|
||||
&.measuring {
|
||||
display: unset;
|
||||
position: absolute;
|
||||
opacity: 0;
|
||||
}
|
||||
|
|
|
@ -15,12 +15,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let disabled = false;
|
||||
|
||||
$: if (buttonRef && active) {
|
||||
/* buttonRef.scrollIntoView({ behavior: "smooth", block: "start" }); */
|
||||
/* TODO will not work on Gecko */
|
||||
(buttonRef as any).scrollIntoViewIfNeeded({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
setTimeout(() =>
|
||||
buttonRef.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
block: "nearest",
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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 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">
|
||||
.code-mirror {
|
||||
height: 100%;
|
||||
|
||||
:global(.CodeMirror) {
|
||||
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 lang="ts">
|
||||
import { setContext as svelteSetContext, tick } from "svelte";
|
||||
import { setContext as svelteSetContext } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
import { fontFamilyKey, fontSizeKey } from "../lib/context-keys";
|
||||
import FocusTrap from "./FocusTrap.svelte";
|
||||
|
||||
export let fontFamily: string;
|
||||
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>;
|
||||
|
||||
let editingArea: HTMLElement;
|
||||
let focusTrap: FocusTrap;
|
||||
|
||||
const inputsStore = writable<EditingInputAPI[]>([]);
|
||||
$: 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);
|
||||
}
|
||||
|
||||
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 {
|
||||
if (editingArea.contains(document.activeElement)) {
|
||||
// do nothing
|
||||
} else if (!focusEditingInputIfAvailable()) {
|
||||
focusTrap.focus();
|
||||
}
|
||||
editingArea.contains(document.activeElement);
|
||||
}
|
||||
|
||||
function refocus(): void {
|
||||
|
@ -114,51 +81,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
if (availableInput) {
|
||||
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);
|
||||
</script>
|
||||
|
||||
<FocusTrap bind:this={focusTrap} on:focus={focusEditingInputInsteadIfAvailable} />
|
||||
|
||||
<div bind:this={editingArea} class="editing-area" on:focusout={trapFocusOnBlurOut}>
|
||||
<div bind:this={editingArea} class="editing-area">
|
||||
<slot />
|
||||
</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 */
|
||||
/* grid-template-columns: repeat(2, 1fr); */
|
||||
|
||||
position: relative;
|
||||
background: var(--canvas-elevated);
|
||||
border-radius: 5px;
|
||||
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
/* This defines the border between inputs */
|
||||
grid-gap: 1px;
|
||||
background-color: var(--border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -109,9 +109,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
@use "sass/elevation" as *;
|
||||
|
||||
.editor-field {
|
||||
position: relative;
|
||||
padding: 0 3px;
|
||||
--border-color: var(--border);
|
||||
overflow: hidden;
|
||||
margin: 0 3px;
|
||||
|
||||
border-radius: 5px;
|
||||
border: 1px solid var(--border);
|
||||
|
||||
@include elevation-transition;
|
||||
@include elevation(1);
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--border-focus);
|
||||
}
|
||||
}
|
||||
</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 */
|
||||
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 */
|
||||
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>
|
||||
|
||||
<div class="label-container" on:mousedown|preventDefault>
|
||||
<div class="label-container">
|
||||
<span
|
||||
class="clickable"
|
||||
title={tooltip}
|
||||
|
@ -38,11 +38,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
.label-container {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
background: var(--canvas);
|
||||
padding: 0 3px 1px;
|
||||
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
z-index: 50;
|
||||
|
||||
.clickable {
|
||||
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 TagAddButton from "../tag-editor/tag-options-button/TagAddButton.svelte";
|
||||
import { ChangeTimer } from "./change-timer";
|
||||
import DecoratedElements from "./DecoratedElements.svelte";
|
||||
import { clearableArray } from "./destroyable";
|
||||
import DuplicateLink from "./DuplicateLink.svelte";
|
||||
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 FieldDescription from "./FieldDescription.svelte";
|
||||
import Fields from "./Fields.svelte";
|
||||
import FrameElement from "./FrameElement.svelte";
|
||||
import { alertIcon } from "./icons";
|
||||
import ImageHandle from "./image-overlay";
|
||||
import ImageOverlay from "./image-overlay";
|
||||
import { shrinkImagesByDefault } from "./image-overlay/ImageOverlay.svelte";
|
||||
import MathjaxHandle from "./mathjax-overlay";
|
||||
import MathjaxElement from "./MathjaxElement.svelte";
|
||||
import MathjaxOverlay from "./mathjax-overlay";
|
||||
import Notification from "./Notification.svelte";
|
||||
import PlainTextInput from "./plain-text-input";
|
||||
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>
|
||||
<Fields>
|
||||
<DecoratedElements>
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
|
||||
<EditorField
|
||||
{field}
|
||||
{content}
|
||||
flipInputs={plainTextDefaults[index]}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
bridgeCommand(`focus:${index}`);
|
||||
}}
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
bridgeCommand(
|
||||
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
|
||||
get(content),
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
$hoveredField = fields[index];
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
$hoveredField = null;
|
||||
}}
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
--dupes-color={cols[index] === "dupe"
|
||||
? "var(--accent-danger)"
|
||||
: "transparent"}
|
||||
>
|
||||
<svelte:fragment slot="field-label">
|
||||
<LabelContainer
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
on:toggle={async () => {
|
||||
fieldsCollapsed[index] =
|
||||
!fieldsCollapsed[index];
|
||||
<EditorField
|
||||
{field}
|
||||
{content}
|
||||
flipInputs={plainTextDefaults[index]}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
bridgeCommand(`focus:${index}`);
|
||||
}}
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
bridgeCommand(
|
||||
`blur:${index}:${getNoteId()}:${transformContentBeforeSave(
|
||||
get(content),
|
||||
)}`,
|
||||
);
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
$hoveredField = fields[index];
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
$hoveredField = null;
|
||||
}}
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
--dupes-color={cols[index] === "dupe"
|
||||
? "var(--accent-danger)"
|
||||
: "transparent"}
|
||||
>
|
||||
<svelte:fragment slot="field-label">
|
||||
<LabelContainer
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
on:toggle={async () => {
|
||||
fieldsCollapsed[index] = !fieldsCollapsed[index];
|
||||
|
||||
const defaultInput = !plainTextDefaults[index]
|
||||
? richTextInputs[index]
|
||||
: plainTextInputs[index];
|
||||
const defaultInput = !plainTextDefaults[index]
|
||||
? richTextInputs[index]
|
||||
: plainTextInputs[index];
|
||||
|
||||
if (!fieldsCollapsed[index]) {
|
||||
refocusInput(defaultInput.api);
|
||||
} else if (!plainTextDefaults[index]) {
|
||||
plainTextsHidden[index] = true;
|
||||
} else {
|
||||
richTextsHidden[index] = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="field-name">
|
||||
<LabelName>
|
||||
{field.name}
|
||||
</LabelName>
|
||||
</svelte:fragment>
|
||||
<FieldState>
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
{#if plainTextDefaults[index]}
|
||||
<RichTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] ===
|
||||
$focusedField)}
|
||||
bind:off={richTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
richTextsHidden[index] =
|
||||
!richTextsHidden[index];
|
||||
if (!fieldsCollapsed[index]) {
|
||||
refocusInput(defaultInput.api);
|
||||
} else if (!plainTextDefaults[index]) {
|
||||
plainTextsHidden[index] = true;
|
||||
} else {
|
||||
richTextsHidden[index] = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="field-name">
|
||||
<LabelName>
|
||||
{field.name}
|
||||
</LabelName>
|
||||
</svelte:fragment>
|
||||
<FieldState>
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
{#if plainTextDefaults[index]}
|
||||
<RichTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={richTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
richTextsHidden[index] =
|
||||
!richTextsHidden[index];
|
||||
|
||||
if (!richTextsHidden[index]) {
|
||||
refocusInput(
|
||||
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}
|
||||
if (!richTextsHidden[index]) {
|
||||
refocusInput(
|
||||
richTextInputs[index].api,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FieldState>
|
||||
</LabelContainer>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="rich-text-input">
|
||||
<Collapsible
|
||||
collapse={richTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<RichTextInput
|
||||
{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}
|
||||
{:else}
|
||||
<PlainTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={plainTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
plainTextsHidden[index] =
|
||||
!plainTextsHidden[index];
|
||||
|
||||
<MathjaxElement />
|
||||
<FrameElement />
|
||||
</DecoratedElements>
|
||||
if (!plainTextsHidden[index]) {
|
||||
refocusInput(
|
||||
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>
|
||||
</PaneContent>
|
||||
</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();
|
||||
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||
|
||||
let showFloating = false;
|
||||
</script>
|
||||
|
|
|
@ -25,7 +25,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
execCommand(key);
|
||||
}
|
||||
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||
</script>
|
||||
|
||||
{#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()],
|
||||
];
|
||||
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||
|
||||
let showFloating = false;
|
||||
</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 { context } from "../NoteEditor.svelte";
|
||||
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 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 {
|
||||
if (!editingInputIsRichText($focusedInput)) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
[mediaPromise, resolve] = promiseWithResolver<string>();
|
||||
$focusedInput.editable.focusHandler.focus.on(
|
||||
($focusedInput as RichTextInputAPI).editable.focusHandler.focus.on(
|
||||
async () => setFormat("inserthtml", await mediaPromise),
|
||||
{ once: true },
|
||||
);
|
||||
|
@ -55,12 +55,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
const recordCombination = "F5";
|
||||
|
||||
function attachRecordingOnFocus(): void {
|
||||
if (!editingInputIsRichText($focusedInput)) {
|
||||
if (disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
[mediaPromise, resolve] = promiseWithResolver<string>();
|
||||
$focusedInput.editable.focusHandler.focus.on(
|
||||
($focusedInput as RichTextInputAPI).editable.focusHandler.focus.on(
|
||||
async () => setFormat("inserthtml", await mediaPromise),
|
||||
{ once: true },
|
||||
);
|
||||
|
@ -68,7 +68,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
bridgeCommand("record");
|
||||
}
|
||||
|
||||
$: disabled = !editingInputIsRichText($focusedInput);
|
||||
$: disabled = !$focusedInput || !editingInputIsRichText($focusedInput);
|
||||
|
||||
export let api = {};
|
||||
</script>
|
||||
|
|
|
@ -9,7 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</script>
|
||||
|
||||
<script lang="ts">
|
||||
import { onMount, tick } from "svelte";
|
||||
import { tick } from "svelte";
|
||||
|
||||
import ButtonToolbar from "../../components/ButtonToolbar.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 * as tr from "../../lib/ftl";
|
||||
import { removeStyleProperties } from "../../lib/styling";
|
||||
import type { Callback } from "../../lib/typing";
|
||||
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||
import HandleBackground from "../HandleBackground.svelte";
|
||||
import HandleControl from "../HandleControl.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 SizeSelect from "./SizeSelect.svelte";
|
||||
|
||||
export let maxWidth: 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;
|
||||
|
||||
|
@ -80,23 +110,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
await tick();
|
||||
}
|
||||
|
||||
async function maybeShowHandle(event: Event): Promise<void> {
|
||||
if (event.target instanceof HTMLImageElement) {
|
||||
const image = event.target;
|
||||
|
||||
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 = "";
|
||||
let naturalWidth: number;
|
||||
let naturalHeight: number;
|
||||
let aspectRatio: number;
|
||||
|
||||
function updateDimensions() {
|
||||
/* 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() {
|
||||
await updateSelection?.();
|
||||
updateDimensions();
|
||||
}
|
||||
if (!image.dataset.anki) {
|
||||
activeImage = image;
|
||||
|
||||
/* window resizing */
|
||||
const resizeObserver = new ResizeObserver(async () => {
|
||||
await updateSizesWithDimensions();
|
||||
});
|
||||
naturalWidth = activeImage?.naturalWidth;
|
||||
naturalHeight = activeImage?.naturalHeight;
|
||||
aspectRatio =
|
||||
naturalWidth && naturalHeight ? naturalWidth / naturalHeight : NaN;
|
||||
|
||||
$: observes = Boolean(activeImage);
|
||||
|
||||
async function toggleResizeObserver(observes: boolean) {
|
||||
const container = await element;
|
||||
|
||||
if (observes) {
|
||||
resizeObserver.observe(container);
|
||||
} else {
|
||||
resizeObserver.unobserve(container);
|
||||
updateDimensions();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$: toggleResizeObserver(observes);
|
||||
let customDimensions: boolean = false;
|
||||
let actualWidth = "";
|
||||
let actualHeight = "";
|
||||
|
||||
/* memoized position of image on resize start
|
||||
* 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");
|
||||
}
|
||||
|
||||
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;
|
||||
$: shrinkingDisabled =
|
||||
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;
|
||||
$: restoringDisabled = !activeImage?.hasAttribute("width") ?? true;
|
||||
|
||||
const widthObserver = new MutationObserver(
|
||||
() => (restoringDisabled = !activeImage!.hasAttribute("width")),
|
||||
);
|
||||
const widthObserver = new MutationObserver(() => {
|
||||
restoringDisabled = !activeImage!.hasAttribute("width");
|
||||
updateDimensions();
|
||||
});
|
||||
|
||||
$: 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">
|
||||
import type CodeMirrorLib from "codemirror";
|
||||
import { onMount, tick } from "svelte";
|
||||
import { tick } from "svelte";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
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 { hasBlockAttribute } from "../../lib/dom";
|
||||
import { on } from "../../lib/events";
|
||||
import { noop } from "../../lib/functional";
|
||||
import { promiseWithResolver } from "../../lib/promise";
|
||||
import type { Callback } from "../../lib/typing";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import type { EditingInputAPI } from "../EditingArea.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 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 mathjaxElement: HTMLElement | null = null;
|
||||
let allow = noop;
|
||||
let unsubscribe = noop;
|
||||
|
||||
let allowResubscription: Callback;
|
||||
let unsubscribe: Callback;
|
||||
|
||||
let selectAll = false;
|
||||
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("");
|
||||
|
||||
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position): void {
|
||||
allow = preventResubscription();
|
||||
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position) {
|
||||
const [promise, allowResolve] = promiseWithResolver<void>();
|
||||
|
||||
allowPromise = promise;
|
||||
allowResubscription = singleCallback(
|
||||
richTextInput!.preventResubscription(),
|
||||
allowResolve,
|
||||
);
|
||||
|
||||
position = pos;
|
||||
|
||||
/* 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 {
|
||||
editable.focusHandler.flushCaret();
|
||||
richTextInput!.editable.focusHandler.flushCaret();
|
||||
|
||||
if (after) {
|
||||
(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 {
|
||||
unsubscribe();
|
||||
activeImage = 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 cleanup: Callback | null = null;
|
||||
let cleanupImageError: Callback | null = null;
|
||||
|
||||
async function updateErrorMessage(): Promise<void> {
|
||||
errorMessage = activeImage!.title;
|
||||
}
|
||||
|
||||
async function updateImageErrorCallback(image: HTMLImageElement | null) {
|
||||
cleanup?.();
|
||||
cleanup = null;
|
||||
cleanupImageError?.();
|
||||
cleanupImageError = null;
|
||||
|
||||
if (!image) {
|
||||
return;
|
||||
}
|
||||
|
||||
cleanup = on(image, "resize", updateErrorMessage);
|
||||
cleanupImageError = on(image, "resize", updateErrorMessage);
|
||||
}
|
||||
|
||||
$: updateImageErrorCallback(activeImage);
|
||||
|
||||
async function showOverlayIfMathjaxClicked({ target }: Event): Promise<void> {
|
||||
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
||||
await resetHandle();
|
||||
resetHandle();
|
||||
showOverlay(target);
|
||||
}
|
||||
}
|
||||
|
@ -136,16 +177,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
showOverlay(detail);
|
||||
}
|
||||
|
||||
onMount(async () => {
|
||||
const container = await element;
|
||||
|
||||
return singleCallback(
|
||||
on(container, "click", showOverlayIfMathjaxClicked),
|
||||
on(container, "movecaretafter" as any, showOnAutofocus),
|
||||
on(container, "selectall" as any, showSelectAll),
|
||||
);
|
||||
});
|
||||
|
||||
let isBlock: boolean;
|
||||
$: isBlock = mathjaxElement ? hasBlockAttribute(mathjaxElement) : false;
|
||||
|
||||
|
@ -184,24 +215,27 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{code}
|
||||
{selectAll}
|
||||
{position}
|
||||
on:moveoutstart={async () => {
|
||||
on:moveoutstart={() => {
|
||||
placeHandle(false);
|
||||
await resetHandle();
|
||||
resetHandle();
|
||||
}}
|
||||
on:moveoutend={async () => {
|
||||
on:moveoutend={() => {
|
||||
placeHandle(true);
|
||||
await resetHandle();
|
||||
resetHandle();
|
||||
}}
|
||||
on:blur={async () => {
|
||||
await resetHandle();
|
||||
on:tab={async () => {
|
||||
// Instead of resetting on blur, we reset on tab
|
||||
// Otherwise, when clicking from Mathjax element to another,
|
||||
// the user has to click twice (focus is called before blur?)
|
||||
resetHandle();
|
||||
}}
|
||||
let:editor={mathjaxEditor}
|
||||
>
|
||||
<Shortcut
|
||||
keyCombination={acceptShortcut}
|
||||
on:action={async () => {
|
||||
on:action={() => {
|
||||
placeHandle(true);
|
||||
await resetHandle();
|
||||
resetHandle();
|
||||
}}
|
||||
/>
|
||||
|
||||
|
@ -238,7 +272,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</WithFloating>
|
||||
|
||||
<svelte:fragment slot="overlay">
|
||||
<HandleBackground tooltip={errorMessage} />
|
||||
<HandleBackground
|
||||
tooltip={errorMessage}
|
||||
--handle-background-color="var(--code-bg)"
|
||||
/>
|
||||
</svelte:fragment>
|
||||
</WithOverlay>
|
||||
{/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 { storedToUndecorated, undecoratedToStored } from "./transform";
|
||||
|
||||
export let isDefault: boolean;
|
||||
export let hidden = false;
|
||||
export let richTextHidden: boolean;
|
||||
|
||||
$: configuration = {
|
||||
mode: htmlanki,
|
||||
|
@ -147,8 +145,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<div
|
||||
class="plain-text-input"
|
||||
class:light-theme={!$pageTheme.isDark}
|
||||
class:is-default={isDefault}
|
||||
class:alone={richTextHidden}
|
||||
on:focusin={() => ($focusedInput = api)}
|
||||
{hidden}
|
||||
>
|
||||
|
@ -163,31 +159,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<style lang="scss">
|
||||
.plain-text-input {
|
||||
border-top: 1px solid var(--border);
|
||||
border-radius: 0 0 5px 5px;
|
||||
height: 100%;
|
||||
|
||||
:global(.CodeMirror) {
|
||||
height: 100%;
|
||||
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) {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// 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 {
|
||||
return decoratedElements.toUndecorated(html);
|
||||
|
|
|
@ -25,9 +25,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function editingInputIsRichText(
|
||||
editingInput: EditingInputAPI | null,
|
||||
editingInput: EditingInputAPI,
|
||||
): editingInput is RichTextInputAPI {
|
||||
return editingInput?.name === "rich-text";
|
||||
return editingInput.name === "rich-text";
|
||||
}
|
||||
|
||||
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,
|
||||
editingInputIsRichText,
|
||||
globalInputHandler as inputHandler,
|
||||
lifecycle,
|
||||
surrounder,
|
||||
};
|
||||
</script>
|
||||
|
@ -126,7 +127,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
return hidden;
|
||||
}
|
||||
|
||||
const className = "rich-text-editable";
|
||||
let richTextDiv: HTMLElement;
|
||||
|
||||
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:stylesDidLoad
|
||||
>
|
||||
<div
|
||||
bind:this={richTextDiv}
|
||||
class={className}
|
||||
class:night-mode={$pageTheme.isDark}
|
||||
use:attachShadow
|
||||
use:attachStyles
|
||||
use:attachContentEditable={{ stylesDidLoad }}
|
||||
on:focusin
|
||||
on:focusout
|
||||
/>
|
||||
<div class="rich-text-relative">
|
||||
<div
|
||||
class="rich-text-editable"
|
||||
bind:this={richTextDiv}
|
||||
use:attachShadow
|
||||
use:attachStyles
|
||||
use:attachContentEditable={{ stylesDidLoad }}
|
||||
on:focusin
|
||||
on:focusout
|
||||
/>
|
||||
|
||||
{#await Promise.all([richTextPromise, stylesDidLoad]) then _}
|
||||
<div class="rich-text-widgets">
|
||||
<slot />
|
||||
</div>
|
||||
{/await}
|
||||
{#await Promise.all([richTextPromise, stylesDidLoad]) then _}
|
||||
<div class="rich-text-widgets">
|
||||
<slot />
|
||||
</div>
|
||||
{/await}
|
||||
</div>
|
||||
</RichTextStyles>
|
||||
<slot name="plain-text-badge" />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.rich-text-input {
|
||||
height: 100%;
|
||||
|
||||
background-color: var(--canvas-elevated);
|
||||
padding: 6px;
|
||||
}
|
||||
|
||||
.rich-text-relative {
|
||||
position: relative;
|
||||
margin: 6px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
import type { DecoratedElement } from "../../editable/decorated";
|
||||
import type { 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 {
|
||||
fragment.normalize();
|
||||
|
|
|
@ -7,7 +7,7 @@ import {
|
|||
nodeIsElement,
|
||||
} from "../../lib/dom";
|
||||
import { createDummyDoc } from "../../lib/parsing";
|
||||
import { decoratedElements } from "../DecoratedElements.svelte";
|
||||
import { decoratedElements } from "../decorated-elements";
|
||||
|
||||
function adjustInputHTML(html: string): string {
|
||||
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
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { getContext, onMount } from "svelte";
|
||||
import { getContext } from "svelte";
|
||||
import type { Readable } from "svelte/store";
|
||||
|
||||
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 { singleCallback } from "../../lib/typing";
|
||||
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 type {
|
||||
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 { 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);
|
||||
|
||||
let foundSymbols: SymbolsTable = [];
|
||||
|
||||
let referenceRange: Range | undefined = undefined;
|
||||
let activeItem = 0;
|
||||
let cleanup: Callback;
|
||||
let referenceRange: Range | null = null;
|
||||
let activeItem: number | null = null;
|
||||
let cleanupReferenceRange: Callback;
|
||||
|
||||
function unsetReferenceRange() {
|
||||
referenceRange = undefined;
|
||||
activeItem = 0;
|
||||
cleanup?.();
|
||||
referenceRange = null;
|
||||
activeItem = null;
|
||||
cleanupReferenceRange?.();
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
|
||||
inputHandler.insertText.on(
|
||||
richTextInput!.inputHandler.insertText.on(
|
||||
async ({ text }) => {
|
||||
replaceText(
|
||||
selection,
|
||||
|
@ -217,13 +242,20 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
if (foundSymbols.length > 0) {
|
||||
referenceRange = currentRange;
|
||||
cleanup = singleCallback(
|
||||
editable.focusHandler.blur.on(async () => unsetReferenceRange(), {
|
||||
once: true,
|
||||
}),
|
||||
inputHandler.pointerDown.on(async () => unsetReferenceRange()),
|
||||
inputHandler.specialKey.on(async (input: SpecialKeyParams) =>
|
||||
onSpecialKey(input),
|
||||
activeItem = 0;
|
||||
|
||||
cleanupReferenceRange = singleCallback(
|
||||
richTextInput!.editable.focusHandler.blur.on(
|
||||
async () => unsetReferenceRange(),
|
||||
{
|
||||
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,
|
||||
);
|
||||
|
||||
inputHandler.insertText.on(
|
||||
richTextInput!.inputHandler.insertText.on(
|
||||
async ({ text }) =>
|
||||
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
|
||||
// want to update in the case of a deletion
|
||||
inputHandler.afterInput.on(
|
||||
richTextInput!.inputHandler.afterInput.on(
|
||||
async (): Promise<void> => {
|
||||
const currentRange = getRange(selection)!;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() =>
|
||||
inputHandler.beforeInput.on(
|
||||
async (input: { event: Event }): Promise<void> => onBeforeInput(input),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<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;
|
||||
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
overflow: hidden auto;
|
||||
text-overflow: ellipsis;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
|
Loading…
Reference in a new issue