mirror of
https://github.com/ankitects/anki.git
synced 2025-11-07 21:27:14 -05:00
* NF: replace `disabled` by `enabled` This allows to remove the negations and, in my opinion, make the code easier to understand and edit. * Cloze button get disabled outside of cloze field More specifically, if the user focus in a field that is not a cloze field, the button are still there but appear as disabled. The shortcut instead of adding the cloze context shows an alert explaining why this can't be done. While this message is already displayed when the user tries to add a note with cloze in non-cloze field, I suspect it will save time to stop the user as soon as possible from making mistake. This should make very clear what is authorized and what is not. It'll also be a reminder of whether the current field is a cloze or not. In order to do this, I added a back-end method (that I expect we may reuse in ankidroid) to get the index of the fields used in cloze. This set is sent to the note editor, which propagates it where needed. In mathjax, the cloze symbol is removed when the selected field is not a cloze field.
289 lines
9.7 KiB
Svelte
289 lines
9.7 KiB
Svelte
<!--
|
|
Copyright: Ankitects Pty Ltd and contributors
|
|
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
-->
|
|
<script lang="ts">
|
|
import { hasBlockAttribute } from "@tslib/dom";
|
|
import { on } from "@tslib/events";
|
|
import { promiseWithResolver } from "@tslib/promise";
|
|
import type { Callback } from "@tslib/typing";
|
|
import { singleCallback } from "@tslib/typing";
|
|
import type CodeMirrorLib from "codemirror";
|
|
import { tick } from "svelte";
|
|
import { writable } from "svelte/store";
|
|
|
|
import Popover from "$lib/components/Popover.svelte";
|
|
import Shortcut from "$lib/components/Shortcut.svelte";
|
|
import WithFloating from "$lib/components/WithFloating.svelte";
|
|
import WithOverlay from "$lib/components/WithOverlay.svelte";
|
|
import { placeCaretAfter } from "$lib/domlib/place-caret";
|
|
import { isComposing } from "$lib/sveltelib/composition";
|
|
|
|
import { escapeSomeEntities, unescapeSomeEntities } from "../../editable/mathjax";
|
|
import { Mathjax } from "../../editable/mathjax-element.svelte";
|
|
import type { EditingInputAPI } from "../EditingArea.svelte";
|
|
import HandleBackground from "../HandleBackground.svelte";
|
|
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 { focusedInput } = context.get();
|
|
|
|
let cleanup: Callback;
|
|
let richTextInput: RichTextInputAPI | null = null;
|
|
let allowPromise = Promise.resolve();
|
|
// Whether the last focused input field corresponds to a cloze field.
|
|
let isClozeField: boolean = true;
|
|
|
|
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),
|
|
);
|
|
isClozeField = input.isClozeField;
|
|
}
|
|
|
|
// 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 allowResubscription: Callback;
|
|
let unsubscribe: Callback;
|
|
|
|
let selectAll = false;
|
|
let position: CodeMirrorLib.Position | undefined = undefined;
|
|
|
|
/**
|
|
* Will contain the Mathjax text with unescaped entities.
|
|
* This is the text displayed in the actual editor window.
|
|
*/
|
|
const code = writable("");
|
|
|
|
function showOverlay(image: HTMLImageElement, pos?: CodeMirrorLib.Position) {
|
|
if ($isComposing) {
|
|
// Should be canceled while an IME composition session is active
|
|
return;
|
|
}
|
|
|
|
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
|
|
* what triggers the Mathjax editor to show */
|
|
activeImage = image;
|
|
mathjaxElement = activeImage.closest(Mathjax.tagName)!;
|
|
|
|
code.set(unescapeSomeEntities(mathjaxElement.dataset.mathjax ?? ""));
|
|
unsubscribe = code.subscribe((value: string) => {
|
|
mathjaxElement!.dataset.mathjax = escapeSomeEntities(value);
|
|
});
|
|
}
|
|
|
|
function placeHandle(after: boolean): void {
|
|
richTextInput!.editable.focusHandler.flushCaret();
|
|
|
|
if (after) {
|
|
(mathjaxElement as any).placeCaretAfter();
|
|
} else {
|
|
(mathjaxElement as any).placeCaretBefore();
|
|
}
|
|
}
|
|
|
|
async function resetHandle(): Promise<void> {
|
|
selectAll = false;
|
|
position = undefined;
|
|
|
|
allowResubscription?.();
|
|
|
|
if (activeImage && mathjaxElement) {
|
|
clear();
|
|
}
|
|
}
|
|
|
|
function clear(): void {
|
|
unsubscribe();
|
|
activeImage = null;
|
|
mathjaxElement = null;
|
|
}
|
|
|
|
let errorMessage: string;
|
|
let cleanupImageError: Callback | null = null;
|
|
|
|
async function updateErrorMessage(): Promise<void> {
|
|
errorMessage = activeImage!.title;
|
|
}
|
|
|
|
async function updateImageErrorCallback(image: HTMLImageElement | null) {
|
|
cleanupImageError?.();
|
|
cleanupImageError = null;
|
|
|
|
if (!image) {
|
|
return;
|
|
}
|
|
|
|
cleanupImageError = on(image, "resize", updateErrorMessage);
|
|
}
|
|
|
|
$: updateImageErrorCallback(activeImage);
|
|
|
|
async function showOverlayIfMathjaxClicked({ target }: Event): Promise<void> {
|
|
if (target instanceof HTMLImageElement && target.dataset.anki === "mathjax") {
|
|
resetHandle();
|
|
showOverlay(target);
|
|
}
|
|
}
|
|
|
|
async function showOnAutofocus({
|
|
detail,
|
|
}: CustomEvent<{
|
|
image: HTMLImageElement;
|
|
position?: [number, number];
|
|
}>): Promise<void> {
|
|
let position: CodeMirrorLib.Position | undefined = undefined;
|
|
|
|
if (detail.position) {
|
|
const [line, ch] = detail.position;
|
|
position = { line, ch };
|
|
}
|
|
|
|
showOverlay(detail.image, position);
|
|
}
|
|
|
|
async function showSelectAll({
|
|
detail,
|
|
}: CustomEvent<HTMLImageElement>): Promise<void> {
|
|
selectAll = true;
|
|
showOverlay(detail);
|
|
}
|
|
|
|
let isBlock: boolean;
|
|
$: isBlock = mathjaxElement ? hasBlockAttribute(mathjaxElement) : false;
|
|
|
|
async function updateBlockAttribute(): Promise<void> {
|
|
mathjaxElement!.setAttribute("block", String(isBlock));
|
|
|
|
// We assume that by the end of this tick, the image will have
|
|
// adjusted its styling to either block or inline
|
|
await tick();
|
|
}
|
|
|
|
const acceptShortcut = "Enter";
|
|
const newlineShortcut = "Shift+Enter";
|
|
</script>
|
|
|
|
<div class="mathjax-overlay">
|
|
{#if activeImage && mathjaxElement}
|
|
<WithOverlay
|
|
reference={activeImage}
|
|
padding={isBlock ? 10 : 3}
|
|
keepOnKeyup
|
|
let:position={positionOverlay}
|
|
>
|
|
<WithFloating
|
|
reference={activeImage}
|
|
offset={20}
|
|
keepOnKeyup
|
|
portalTarget={document.body}
|
|
on:close={resetHandle}
|
|
>
|
|
<Popover slot="floating" let:position={positionFloating}>
|
|
<MathjaxEditor
|
|
{acceptShortcut}
|
|
{newlineShortcut}
|
|
{code}
|
|
{selectAll}
|
|
{position}
|
|
on:moveoutstart={() => {
|
|
placeHandle(false);
|
|
resetHandle();
|
|
}}
|
|
on:moveoutend={() => {
|
|
placeHandle(true);
|
|
resetHandle();
|
|
}}
|
|
on:close={resetHandle}
|
|
let:editor={mathjaxEditor}
|
|
>
|
|
<Shortcut
|
|
keyCombination={acceptShortcut}
|
|
on:action={() => {
|
|
placeHandle(true);
|
|
resetHandle();
|
|
}}
|
|
/>
|
|
|
|
<MathjaxButtons
|
|
{isBlock}
|
|
{isClozeField}
|
|
on:setinline={async () => {
|
|
isBlock = false;
|
|
await updateBlockAttribute();
|
|
positionOverlay();
|
|
positionFloating();
|
|
}}
|
|
on:setblock={async () => {
|
|
isBlock = true;
|
|
await updateBlockAttribute();
|
|
positionOverlay();
|
|
positionFloating();
|
|
}}
|
|
on:delete={async () => {
|
|
if (activeImage) {
|
|
placeCaretAfter(activeImage);
|
|
mathjaxElement?.remove();
|
|
clear();
|
|
}
|
|
}}
|
|
on:surround={async ({ detail }) => {
|
|
const editor = await mathjaxEditor.editor;
|
|
const { prefix, suffix } = detail;
|
|
|
|
editor.replaceSelection(
|
|
prefix + editor.getSelection() + suffix,
|
|
);
|
|
}}
|
|
/>
|
|
</MathjaxEditor>
|
|
</Popover>
|
|
</WithFloating>
|
|
|
|
<svelte:fragment slot="overlay">
|
|
<HandleBackground
|
|
tooltip={errorMessage}
|
|
--handle-background-color="var(--code-bg)"
|
|
/>
|
|
</svelte:fragment>
|
|
</WithOverlay>
|
|
{/if}
|
|
</div>
|