mirror of
https://github.com/ankitects/anki.git
synced 2025-09-21 23:42:23 -04:00
Add back wrap function (#1551)
* Remove surround from RichText and PlainText * Export wrap
This commit is contained in:
parent
b714974464
commit
4966f3cc44
9 changed files with 83 additions and 62 deletions
|
@ -69,9 +69,9 @@ samp {
|
||||||
--base-font-size: 14px;
|
--base-font-size: 14px;
|
||||||
}
|
}
|
||||||
|
|
||||||
[dir=rtl] {
|
[dir="rtl"] {
|
||||||
.form-select {
|
.form-select {
|
||||||
/* flip <select>'s arrow */
|
/* flip <select>'s arrow */
|
||||||
background-position: left .75rem center;
|
background-position: left 0.75rem center;
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -7,10 +7,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import WithShortcut from "../components/WithShortcut.svelte";
|
import WithShortcut from "../components/WithShortcut.svelte";
|
||||||
|
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
|
import { wrapInternal } from "../lib/wrap";
|
||||||
import { withButton } from "../components/helpers";
|
import { withButton } from "../components/helpers";
|
||||||
import { ellipseIcon } from "./icons";
|
import { ellipseIcon } from "./icons";
|
||||||
import { get } from "svelte/store";
|
import { get } from "svelte/store";
|
||||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||||
|
import type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||||
|
|
||||||
const noteEditor = getNoteEditor();
|
const noteEditor = getNoteEditor();
|
||||||
const { focusInRichText, activeInput } = noteEditor;
|
const { focusInRichText, activeInput } = noteEditor;
|
||||||
|
@ -40,9 +42,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
return Math.max(1, highest);
|
return Math.max(1, highest);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCloze(event: KeyboardEvent | MouseEvent): void {
|
$: richTextAPI = $activeInput as RichTextInputAPI;
|
||||||
|
|
||||||
|
async function onCloze(event: KeyboardEvent | MouseEvent): Promise<void> {
|
||||||
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
const highestCloze = getCurrentHighestCloze(!event.getModifierState("Alt"));
|
||||||
$activeInput?.surround(`{{c${highestCloze}::`, "}}");
|
const richText = await richTextAPI.element;
|
||||||
|
wrapInternal(richText, `{{c${highestCloze}::`, "}}", false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !$focusInRichText;
|
||||||
|
|
|
@ -8,10 +8,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
export interface EditingInputAPI {
|
export interface EditingInputAPI {
|
||||||
readonly name: string;
|
readonly name: string;
|
||||||
focusable: boolean;
|
|
||||||
focus(): void;
|
focus(): void;
|
||||||
moveCaretToEnd(): void;
|
|
||||||
refocus(): void;
|
refocus(): void;
|
||||||
|
focusable: boolean;
|
||||||
|
moveCaretToEnd(): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditingAreaAPI {
|
export interface EditingAreaAPI {
|
||||||
|
|
|
@ -4,17 +4,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
-->
|
-->
|
||||||
<script context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
import type { EditingInputAPI } from "./EditingArea.svelte";
|
import type { EditingInputAPI } from "./EditingArea.svelte";
|
||||||
|
import CodeMirror from "./CodeMirror.svelte";
|
||||||
|
|
||||||
export interface PlainTextInputAPI extends EditingInputAPI {
|
export interface PlainTextInputAPI extends EditingInputAPI {
|
||||||
name: "plain-text";
|
name: "plain-text";
|
||||||
moveCaretToEnd(): void;
|
moveCaretToEnd(): void;
|
||||||
toggle(): boolean;
|
toggle(): boolean;
|
||||||
surround(before: string, after: string): void;
|
getEditor(): CodeMirror.Editor;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import CodeMirror from "./CodeMirror.svelte";
|
|
||||||
import type { CodeMirrorAPI } from "./CodeMirror.svelte";
|
import type { CodeMirrorAPI } from "./CodeMirror.svelte";
|
||||||
import { tick, onMount } from "svelte";
|
import { tick, onMount } from "svelte";
|
||||||
import { writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
@ -88,9 +88,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
codeMirror?.editor.setCursor(codeMirror.editor.lineCount(), 0);
|
codeMirror?.editor.setCursor(codeMirror.editor.lineCount(), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
function surround(before: string, after: string): void {
|
function toggle(): boolean {
|
||||||
const selection = codeMirror?.editor.getSelection();
|
hidden = !hidden;
|
||||||
codeMirror?.editor.replaceSelection(before + selection + after);
|
return hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getEditor(): CodeMirror.Editor {
|
||||||
|
return codeMirror?.editor;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const api = {
|
export const api = {
|
||||||
|
@ -99,11 +103,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
focusable: !hidden,
|
focusable: !hidden,
|
||||||
moveCaretToEnd,
|
moveCaretToEnd,
|
||||||
refocus,
|
refocus,
|
||||||
toggle(): boolean {
|
toggle,
|
||||||
hidden = !hidden;
|
getEditor,
|
||||||
return hidden;
|
|
||||||
},
|
|
||||||
surround,
|
|
||||||
} as PlainTextInputAPI;
|
} as PlainTextInputAPI;
|
||||||
|
|
||||||
function pushUpdate(): void {
|
function pushUpdate(): void {
|
||||||
|
|
|
@ -16,7 +16,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
moveCaretToEnd(): void;
|
moveCaretToEnd(): void;
|
||||||
refocus(): void;
|
refocus(): void;
|
||||||
toggle(): boolean;
|
toggle(): boolean;
|
||||||
surround(before: string, after: string): void;
|
|
||||||
preventResubscription(): () => void;
|
preventResubscription(): () => void;
|
||||||
getTriggerOnNextInsert(): OnNextInsertTrigger;
|
getTriggerOnNextInsert(): OnNextInsertTrigger;
|
||||||
}
|
}
|
||||||
|
@ -50,7 +49,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { getEditingArea } from "./EditingArea.svelte";
|
import { getEditingArea } from "./EditingArea.svelte";
|
||||||
import { promiseWithResolver } from "../lib/promise";
|
import { promiseWithResolver } from "../lib/promise";
|
||||||
import { bridgeCommand } from "../lib/bridgecommand";
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
import { wrapInternal } from "../lib/wrap";
|
|
||||||
import { on } from "../lib/events";
|
import { on } from "../lib/events";
|
||||||
import { nodeStore } from "../sveltelib/node-store";
|
import { nodeStore } from "../sveltelib/node-store";
|
||||||
import type { DecoratedElement } from "../editable/decorated";
|
import type { DecoratedElement } from "../editable/decorated";
|
||||||
|
@ -198,17 +196,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
richText.focus();
|
richText.focus();
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
moveCaretToEnd,
|
|
||||||
focusable: !hidden,
|
focusable: !hidden,
|
||||||
toggle(): boolean {
|
toggle(): boolean {
|
||||||
hidden = !hidden;
|
hidden = !hidden;
|
||||||
return hidden;
|
return hidden;
|
||||||
},
|
},
|
||||||
surround(before: string, after: string) {
|
moveCaretToEnd,
|
||||||
richTextPromise.then((richText) =>
|
|
||||||
wrapInternal(richText.getRootNode() as any, before, after, false),
|
|
||||||
);
|
|
||||||
},
|
|
||||||
preventResubscription,
|
preventResubscription,
|
||||||
getTriggerOnNextInsert,
|
getTriggerOnNextInsert,
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,10 +14,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import * as tr from "../lib/ftl";
|
import * as tr from "../lib/ftl";
|
||||||
import { bridgeCommand } from "../lib/bridgecommand";
|
import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
|
import { wrapInternal } from "../lib/wrap";
|
||||||
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
import { getNoteEditor } from "./OldEditorAdapter.svelte";
|
||||||
import { appendInParentheses } from "./helpers";
|
import { appendInParentheses } from "./helpers";
|
||||||
import { withButton } from "../components/helpers";
|
import { withButton } from "../components/helpers";
|
||||||
import { paperclipIcon, micIcon, functionIcon } from "./icons";
|
import { paperclipIcon, micIcon, functionIcon } from "./icons";
|
||||||
|
import type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||||
|
|
||||||
export let api = {};
|
export let api = {};
|
||||||
const { focusInRichText, activeInput } = getNoteEditor();
|
const { focusInRichText, activeInput } = getNoteEditor();
|
||||||
|
@ -30,7 +32,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
bridgeCommand("record");
|
bridgeCommand("record");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$: richTextAPI = $activeInput as RichTextInputAPI;
|
||||||
$: disabled = !$focusInRichText;
|
$: disabled = !$focusInRichText;
|
||||||
|
|
||||||
|
async function surround(front: string, back: string): Promise<void> {
|
||||||
|
const element = await richTextAPI.element;
|
||||||
|
wrapInternal(element, front, back, false);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<ButtonGroup {api}>
|
<ButtonGroup {api}>
|
||||||
|
@ -83,10 +91,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
$activeInput?.surround(
|
surround("<anki-mathjax focusonmount>", "</anki-mathjax>")}
|
||||||
"<anki-mathjax focusonmount>",
|
|
||||||
"</anki-mathjax>",
|
|
||||||
)}
|
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingMathjaxInline()}
|
{tr.editingMathjaxInline()}
|
||||||
|
@ -101,7 +106,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
$activeInput?.surround(
|
surround(
|
||||||
'<anki-mathjax block="true" focusonmount>',
|
'<anki-mathjax block="true" focusonmount>',
|
||||||
"</anki-matjax>",
|
"</anki-matjax>",
|
||||||
)}
|
)}
|
||||||
|
@ -119,7 +124,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() =>
|
on:click={() =>
|
||||||
$activeInput?.surround(
|
surround(
|
||||||
"<anki-mathjax focusonmount>\\ce{",
|
"<anki-mathjax focusonmount>\\ce{",
|
||||||
"}</anki-mathjax>",
|
"}</anki-mathjax>",
|
||||||
)}
|
)}
|
||||||
|
@ -136,7 +141,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let:shortcutLabel
|
let:shortcutLabel
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => $activeInput?.surround("[latex]", "[/latex]")}
|
on:click={() => surround("[latex]", "[/latex]")}
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingLatex()}
|
{tr.editingLatex()}
|
||||||
|
@ -150,7 +155,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let:shortcutLabel
|
let:shortcutLabel
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => $activeInput?.surround("[$]", "[/$]")}
|
on:click={() => surround("[$]", "[/$]")}
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingLatexEquation()}
|
{tr.editingLatexEquation()}
|
||||||
|
@ -164,7 +169,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let:shortcutLabel
|
let:shortcutLabel
|
||||||
>
|
>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
on:click={() => $activeInput?.surround("[$$]", "[/$$]")}
|
on:click={() => surround("[$$]", "[/$$]")}
|
||||||
on:mount={withButton(createShortcut)}
|
on:mount={withButton(createShortcut)}
|
||||||
>
|
>
|
||||||
{tr.editingLatexMathEnv()}
|
{tr.editingLatexMathEnv()}
|
||||||
|
|
|
@ -92,5 +92,21 @@ async function setupNoteEditor(): Promise<NoteEditorAPI> {
|
||||||
return api as NoteEditorAPI;
|
return api as NoteEditorAPI;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
import { get } from "svelte/store";
|
||||||
|
import { wrapInternal } from "../lib/wrap";
|
||||||
|
import type { RichTextInputAPI } from "./RichTextInput.svelte";
|
||||||
|
|
||||||
|
export async function wrap(before: string, after: string): Promise<void> {
|
||||||
|
const noteEditor = await noteEditorPromise;
|
||||||
|
|
||||||
|
if (!get(noteEditor.focusInRichText)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const activeInput = get(noteEditor.activeInput) as RichTextInputAPI;
|
||||||
|
const element = await activeInput.element;
|
||||||
|
wrapInternal(element, before, after, false);
|
||||||
|
}
|
||||||
|
|
||||||
export const noteEditorPromise = setupNoteEditor();
|
export const noteEditorPromise = setupNoteEditor();
|
||||||
export { editorToolbar } from "./EditorToolbar.svelte";
|
export { editorToolbar } from "./EditorToolbar.svelte";
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
checkModifiers,
|
checkModifiers,
|
||||||
checkIfInputKey,
|
checkIfInputKey,
|
||||||
} from "./keys";
|
} from "./keys";
|
||||||
|
import { on } from "./events";
|
||||||
|
|
||||||
const keyCodeLookup = {
|
const keyCodeLookup = {
|
||||||
Backspace: 8,
|
Backspace: 8,
|
||||||
|
@ -83,7 +84,6 @@ function removeTrailing(modifier: string): string {
|
||||||
return modifier.substring(0, modifier.length - 1);
|
return modifier.substring(0, modifier.length - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// function checkModifiers(event: KeyboardEvent, modifiers: string[]): boolean {
|
|
||||||
function separateRequiredOptionalModifiers(
|
function separateRequiredOptionalModifiers(
|
||||||
modifiers: string[],
|
modifiers: string[],
|
||||||
): [Modifier[], Modifier[]] {
|
): [Modifier[], Modifier[]] {
|
||||||
|
@ -97,11 +97,12 @@ function separateRequiredOptionalModifiers(
|
||||||
}
|
}
|
||||||
|
|
||||||
const check =
|
const check =
|
||||||
(keyCode: number, modifiers: string[]) =>
|
(keyCode: number, requiredModifiers: Modifier[], optionalModifiers: Modifier[]) =>
|
||||||
(event: KeyboardEvent): boolean => {
|
(event: KeyboardEvent): boolean => {
|
||||||
const [required, optional] = separateRequiredOptionalModifiers(modifiers);
|
return (
|
||||||
|
checkKey(event, keyCode) &&
|
||||||
return checkKey(event, keyCode) && checkModifiers(required, optional)(event);
|
checkModifiers(requiredModifiers, optionalModifiers)(event)
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function keyToCode(key: string): number {
|
function keyToCode(key: string): number {
|
||||||
|
@ -112,9 +113,11 @@ function keyCombinationToCheck(
|
||||||
keyCombination: string[],
|
keyCombination: string[],
|
||||||
): (event: KeyboardEvent) => boolean {
|
): (event: KeyboardEvent) => boolean {
|
||||||
const keyCode = keyToCode(keyCombination[keyCombination.length - 1]);
|
const keyCode = keyToCode(keyCombination[keyCombination.length - 1]);
|
||||||
const modifiers = keyCombination.slice(0, -1);
|
const [required, optional] = separateRequiredOptionalModifiers(
|
||||||
|
keyCombination.slice(0, -1),
|
||||||
|
);
|
||||||
|
|
||||||
return check(keyCode, modifiers);
|
return check(keyCode, required, optional);
|
||||||
}
|
}
|
||||||
|
|
||||||
function innerShortcut(
|
function innerShortcut(
|
||||||
|
@ -123,23 +126,20 @@ function innerShortcut(
|
||||||
callback: (event: KeyboardEvent) => void,
|
callback: (event: KeyboardEvent) => void,
|
||||||
...checks: ((event: KeyboardEvent) => boolean)[]
|
...checks: ((event: KeyboardEvent) => boolean)[]
|
||||||
): void {
|
): void {
|
||||||
let interval: number;
|
|
||||||
|
|
||||||
if (checks.length === 0) {
|
if (checks.length === 0) {
|
||||||
callback(lastEvent);
|
return callback(lastEvent);
|
||||||
} else {
|
}
|
||||||
const [nextCheck, ...restChecks] = checks;
|
|
||||||
const handler = (event: KeyboardEvent): void => {
|
|
||||||
if (nextCheck(event)) {
|
|
||||||
innerShortcut(target, event, callback, ...restChecks);
|
|
||||||
clearTimeout(interval);
|
|
||||||
} else if (checkIfInputKey(event)) {
|
|
||||||
// Any non-modifier key will cancel the shortcut sequence
|
|
||||||
document.removeEventListener("keydown", handler);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
document.addEventListener("keydown", handler, { once: true });
|
const [nextCheck, ...restChecks] = checks;
|
||||||
|
const remove = on(document, "keydown", handler, { once: true });
|
||||||
|
|
||||||
|
function handler(event: KeyboardEvent): void {
|
||||||
|
if (nextCheck(event)) {
|
||||||
|
innerShortcut(target, event, callback, ...restChecks);
|
||||||
|
} else if (checkIfInputKey(event)) {
|
||||||
|
// Any non-modifier key will cancel the shortcut sequence
|
||||||
|
remove();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -151,14 +151,13 @@ export function registerShortcut(
|
||||||
const [check, ...restChecks] =
|
const [check, ...restChecks] =
|
||||||
splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
|
splitKeyCombinationString(keyCombinationString).map(keyCombinationToCheck);
|
||||||
|
|
||||||
const handler = (event: KeyboardEvent): void => {
|
function handler(event: KeyboardEvent): void {
|
||||||
if (check(event)) {
|
if (check(event)) {
|
||||||
innerShortcut(target, event, callback, ...restChecks);
|
innerShortcut(target, event, callback, ...restChecks);
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
target.addEventListener("keydown", handler as EventListener);
|
return on(target, "keydown", handler);
|
||||||
return (): void => target.removeEventListener("keydown", handler as EventListener);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
registerPackage("anki/shortcuts", {
|
registerPackage("anki/shortcuts", {
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
// 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 { getSelection } from "./cross-browser";
|
||||||
|
|
||||||
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
function wrappedExceptForWhitespace(text: string, front: string, back: string): string {
|
||||||
const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
|
const match = text.match(/^(\s*)([^]*?)(\s*)$/)!;
|
||||||
return match[1] + front + match[2] + back + match[3];
|
return match[1] + front + match[2] + back + match[3];
|
||||||
|
@ -15,12 +17,12 @@ function moveCursorPastPostfix(selection: Selection, postfix: string): void {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function wrapInternal(
|
export function wrapInternal(
|
||||||
root: DocumentOrShadowRoot,
|
base: Element,
|
||||||
front: string,
|
front: string,
|
||||||
back: string,
|
back: string,
|
||||||
plainText: boolean,
|
plainText: boolean,
|
||||||
): void {
|
): void {
|
||||||
const selection = root.getSelection()!;
|
const selection = getSelection(base)!;
|
||||||
const range = selection.getRangeAt(0);
|
const range = selection.getRangeAt(0);
|
||||||
const content = range.cloneContents();
|
const content = range.cloneContents();
|
||||||
const span = document.createElement("span");
|
const span = document.createElement("span");
|
||||||
|
|
Loading…
Reference in a new issue