mirror of
https://github.com/ankitects/anki.git
synced 2025-09-19 14:32:22 -04:00
Fix insert media always insert at the start (on Windows) (#1684)
* Move Trigger into its own file * Try implement HandlerList * Implement new input handler and handler-list * Use new refocus HandlerList in TextColorButton * Fix TextColorButton on windows * Move ColorPicker to editor-toolbar * Change trigger behavior of overwriteSurround * Fix mathjax-overlay flushCaret * Insert image via bridgeCommand return value * Fix invoking color picker with F8 * Have remove format work even when collapsed * Satisfy formatter * Insert media via callback resolved from python * Replace print with web.eval * Fix python formatting * remove unused function (dae)
This commit is contained in:
parent
a0d0f2f8fd
commit
0d83581ab0
22 changed files with 574 additions and 373 deletions
|
@ -460,7 +460,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
self._save_current_note()
|
self._save_current_note()
|
||||||
|
|
||||||
elif cmd in self._links:
|
elif cmd in self._links:
|
||||||
self._links[cmd](self)
|
return self._links[cmd](self)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
print("uncaught cmd", cmd)
|
print("uncaught cmd", cmd)
|
||||||
|
@ -703,16 +703,21 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
||||||
filter=filter,
|
filter=filter,
|
||||||
key="media",
|
key="media",
|
||||||
)
|
)
|
||||||
|
|
||||||
self.parentWindow.activateWindow()
|
self.parentWindow.activateWindow()
|
||||||
|
|
||||||
def addMedia(self, path: str, canDelete: bool = False) -> None:
|
def addMedia(self, path: str, canDelete: bool = False) -> None:
|
||||||
"""canDelete is a legacy arg and is ignored."""
|
"""canDelete is a legacy arg and is ignored."""
|
||||||
|
|
||||||
try:
|
try:
|
||||||
html = self._addMedia(path)
|
html = self._addMedia(path)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
showWarning(str(e))
|
showWarning(str(e))
|
||||||
return
|
return
|
||||||
self.web.eval(f"setFormat('inserthtml', {json.dumps(html)});")
|
|
||||||
|
self.web.eval(
|
||||||
|
f'require("anki/TemplateButtons").mediaResolve({json.dumps(html)})'
|
||||||
|
)
|
||||||
|
|
||||||
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
def _addMedia(self, path: str, canDelete: bool = False) -> str:
|
||||||
"""Add to media folder and return local img or sound tag."""
|
"""Add to media folder and return local img or sound tag."""
|
||||||
|
|
|
@ -1,59 +1,59 @@
|
||||||
{
|
{
|
||||||
"images" : [
|
"images": [
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "1x",
|
"scale": "1x",
|
||||||
"size" : "16x16"
|
"size": "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "2x",
|
"scale": "2x",
|
||||||
"size" : "16x16"
|
"size": "16x16"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "1x",
|
"scale": "1x",
|
||||||
"size" : "32x32"
|
"size": "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "2x",
|
"scale": "2x",
|
||||||
"size" : "32x32"
|
"size": "32x32"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "1x",
|
"scale": "1x",
|
||||||
"size" : "128x128"
|
"size": "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "2x",
|
"scale": "2x",
|
||||||
"size" : "128x128"
|
"size": "128x128"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "1x",
|
"scale": "1x",
|
||||||
"size" : "256x256"
|
"size": "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "2x",
|
"scale": "2x",
|
||||||
"size" : "256x256"
|
"size": "256x256"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"filename" : "round-1024-512.png",
|
"filename": "round-1024-512.png",
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "1x",
|
"scale": "1x",
|
||||||
"size" : "512x512"
|
"size": "512x512"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"idiom" : "mac",
|
"idiom": "mac",
|
||||||
"scale" : "2x",
|
"scale": "2x",
|
||||||
"size" : "512x512"
|
"size": "512x512"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info": {
|
||||||
|
"author": "xcode",
|
||||||
|
"version": 1
|
||||||
}
|
}
|
||||||
],
|
|
||||||
"info" : {
|
|
||||||
"author" : "xcode",
|
|
||||||
"version" : 1
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"info" : {
|
"info": {
|
||||||
"author" : "xcode",
|
"author": "xcode",
|
||||||
"version" : 1
|
"version": 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,10 +11,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
import { updateAllState } from "../components/WithState.svelte";
|
import { updateAllState } from "../components/WithState.svelte";
|
||||||
import actionList from "../sveltelib/action-list";
|
import actionList from "../sveltelib/action-list";
|
||||||
import type { InputManagerAction } from "../sveltelib/input-manager";
|
import type { MirrorAction } from "../sveltelib/dom-mirror";
|
||||||
import type { MirrorAction } from "../sveltelib/mirror-dom";
|
import type { SetupInputHandlerAction } from "../sveltelib/input-handler";
|
||||||
import type { ContentEditableAPI } from "./content-editable";
|
import type { ContentEditableAPI } from "./content-editable";
|
||||||
import { customFocusHandling, preventBuiltinShortcuts } from "./content-editable";
|
import { preventBuiltinShortcuts, useFocusHandler } from "./content-editable";
|
||||||
|
|
||||||
export let resolve: (editable: HTMLElement) => void;
|
export let resolve: (editable: HTMLElement) => void;
|
||||||
|
|
||||||
|
@ -24,15 +24,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
const mirrorAction = actionList(mirrors);
|
const mirrorAction = actionList(mirrors);
|
||||||
const mirrorOptions = { store: nodes };
|
const mirrorOptions = { store: nodes };
|
||||||
|
|
||||||
export let managers: InputManagerAction[];
|
export let inputHandlers: SetupInputHandlerAction[];
|
||||||
|
|
||||||
const managerAction = actionList(managers);
|
const inputHandlerAction = actionList(inputHandlers);
|
||||||
|
|
||||||
export let api: Partial<ContentEditableAPI>;
|
export let api: Partial<ContentEditableAPI>;
|
||||||
|
|
||||||
const { setupFocusHandling, flushCaret } = customFocusHandling();
|
const [focusHandler, setupFocusHandling] = useFocusHandler();
|
||||||
|
|
||||||
Object.assign(api, { flushCaret });
|
Object.assign(api, { focusHandler });
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<anki-editable
|
<anki-editable
|
||||||
|
@ -41,7 +41,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
use:setupFocusHandling
|
use:setupFocusHandling
|
||||||
use:preventBuiltinShortcuts
|
use:preventBuiltinShortcuts
|
||||||
use:mirrorAction={mirrorOptions}
|
use:mirrorAction={mirrorOptions}
|
||||||
use:managerAction={{}}
|
use:inputHandlerAction={{}}
|
||||||
on:focus
|
on:focus
|
||||||
on:blur
|
on:blur
|
||||||
on:click={updateAllState}
|
on:click={updateAllState}
|
||||||
|
|
|
@ -8,55 +8,87 @@ import { bridgeCommand } from "../lib/bridgecommand";
|
||||||
import { on, preventDefault } from "../lib/events";
|
import { on, preventDefault } from "../lib/events";
|
||||||
import { isApplePlatform } from "../lib/platform";
|
import { isApplePlatform } from "../lib/platform";
|
||||||
import { registerShortcut } from "../lib/shortcuts";
|
import { registerShortcut } from "../lib/shortcuts";
|
||||||
|
import type { Callback } from "../lib/typing";
|
||||||
|
import { HandlerList } from "../sveltelib/handler-list";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Workaround: If you try to invoke an IME after calling
|
||||||
|
* `placeCaretAfterContent` on a cE element, the IME will immediately
|
||||||
|
* end and the input character will be duplicated
|
||||||
|
*/
|
||||||
function safePlaceCaretAfterContent(editable: HTMLElement): void {
|
function safePlaceCaretAfterContent(editable: HTMLElement): void {
|
||||||
/**
|
|
||||||
* Workaround: If you try to invoke an IME after calling
|
|
||||||
* `placeCaretAfterContent` on a cE element, the IME will immediately
|
|
||||||
* end and the input character will be duplicated
|
|
||||||
*/
|
|
||||||
placeCaretAfterContent(editable);
|
placeCaretAfterContent(editable);
|
||||||
restoreSelection(editable, saveSelection(editable)!);
|
restoreSelection(editable, saveSelection(editable)!);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onFocus(location: SelectionLocation | null): () => void {
|
function restoreCaret(element: HTMLElement, location: SelectionLocation | null): void {
|
||||||
return function (this: HTMLElement): void {
|
if (!location) {
|
||||||
if (!location) {
|
return safePlaceCaretAfterContent(element);
|
||||||
safePlaceCaretAfterContent(this);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
restoreSelection(this, location);
|
|
||||||
} catch {
|
|
||||||
safePlaceCaretAfterContent(this);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CustomFocusHandlingAPI {
|
|
||||||
setupFocusHandling(element: HTMLElement): { destroy(): void };
|
|
||||||
flushCaret(): void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function customFocusHandling(): CustomFocusHandlingAPI {
|
|
||||||
const focusHandlingEvents: (() => void)[] = [];
|
|
||||||
|
|
||||||
function flushEvents(): void {
|
|
||||||
let removeEvent: (() => void) | undefined;
|
|
||||||
|
|
||||||
while ((removeEvent = focusHandlingEvents.pop())) {
|
|
||||||
removeEvent();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
restoreSelection(element, location);
|
||||||
|
} catch {
|
||||||
|
safePlaceCaretAfterContent(element);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type SetupFocusHandlerAction = (element: HTMLElement) => { destroy(): void };
|
||||||
|
|
||||||
|
export interface FocusHandlerAPI {
|
||||||
|
/**
|
||||||
|
* Prevent the automatic caret restoration, that happens upon field focus
|
||||||
|
*/
|
||||||
|
flushCaret(): void;
|
||||||
|
/**
|
||||||
|
* Executed upon focus event of editable.
|
||||||
|
*/
|
||||||
|
focus: HandlerList<{ event: FocusEvent }>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
|
||||||
|
let latestLocation: SelectionLocation | null = null;
|
||||||
|
let offFocus: Callback | null;
|
||||||
|
let offPointerDown: Callback | null;
|
||||||
|
let flush = false;
|
||||||
|
|
||||||
|
function flushCaret(): void {
|
||||||
|
flush = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const focus = new HandlerList<{ event: FocusEvent }>();
|
||||||
|
|
||||||
function prepareFocusHandling(
|
function prepareFocusHandling(
|
||||||
editable: HTMLElement,
|
editable: HTMLElement,
|
||||||
latestLocation: SelectionLocation | null = null,
|
location: SelectionLocation | null = null,
|
||||||
): void {
|
): void {
|
||||||
const off = on(editable, "focus", onFocus(latestLocation), { once: true });
|
latestLocation = location;
|
||||||
|
|
||||||
focusHandlingEvents.push(off, on(editable, "pointerdown", off, { once: true }));
|
offFocus?.();
|
||||||
|
offFocus = on(
|
||||||
|
editable,
|
||||||
|
"focus",
|
||||||
|
(event: FocusEvent): void => {
|
||||||
|
if (flush) {
|
||||||
|
flush = false;
|
||||||
|
} else {
|
||||||
|
restoreCaret(event.currentTarget as HTMLElement, latestLocation);
|
||||||
|
}
|
||||||
|
|
||||||
|
focus.dispatch({ event });
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
offPointerDown?.();
|
||||||
|
offPointerDown = on(
|
||||||
|
editable,
|
||||||
|
"pointerdown",
|
||||||
|
() => {
|
||||||
|
offFocus?.();
|
||||||
|
offFocus = null;
|
||||||
|
},
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -66,22 +98,26 @@ export function customFocusHandling(): CustomFocusHandlingAPI {
|
||||||
prepareFocusHandling(this, saveSelection(this));
|
prepareFocusHandling(this, saveSelection(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupFocusHandling(editable: HTMLElement): { destroy(): void } {
|
function setupFocusHandler(editable: HTMLElement): { destroy(): void } {
|
||||||
prepareFocusHandling(editable);
|
prepareFocusHandling(editable);
|
||||||
const off = on(editable, "blur", onBlur);
|
const off = on(editable, "blur", onBlur);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
destroy() {
|
destroy() {
|
||||||
flushEvents();
|
|
||||||
off();
|
off();
|
||||||
|
offFocus?.();
|
||||||
|
offPointerDown?.();
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return [
|
||||||
setupFocusHandling,
|
{
|
||||||
flushCaret: flushEvents,
|
flushCaret,
|
||||||
};
|
focus,
|
||||||
|
},
|
||||||
|
setupFocusHandler,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isApplePlatform()) {
|
if (isApplePlatform()) {
|
||||||
|
@ -102,5 +138,5 @@ export interface ContentEditableAPI {
|
||||||
* the ContentEditable. Can be used when you want to set the caret
|
* the ContentEditable. Can be used when you want to set the caret
|
||||||
* yourself.
|
* yourself.
|
||||||
*/
|
*/
|
||||||
flushCaret(): void;
|
focusHandler: FocusHandlerAPI;
|
||||||
}
|
}
|
||||||
|
|
|
@ -326,12 +326,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<svelte:fragment slot="editing-inputs">
|
<svelte:fragment slot="editing-inputs">
|
||||||
<RichTextInput
|
<RichTextInput
|
||||||
hidden={richTextsHidden[index]}
|
hidden={richTextsHidden[index]}
|
||||||
on:focusin={() => {
|
|
||||||
$focusedInput = richTextInputs[index].api;
|
|
||||||
}}
|
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
$focusedInput = null;
|
|
||||||
saveFieldNow();
|
saveFieldNow();
|
||||||
|
$focusedInput = null;
|
||||||
}}
|
}}
|
||||||
bind:this={richTextInputs[index]}
|
bind:this={richTextInputs[index]}
|
||||||
>
|
>
|
||||||
|
@ -341,12 +338,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
<PlainTextInput
|
<PlainTextInput
|
||||||
hidden={plainTextsHidden[index]}
|
hidden={plainTextsHidden[index]}
|
||||||
on:focusin={() => {
|
|
||||||
$focusedInput = plainTextInputs[index].api;
|
|
||||||
}}
|
|
||||||
on:focusout={() => {
|
on:focusout={() => {
|
||||||
$focusedInput = null;
|
|
||||||
saveFieldNow();
|
saveFieldNow();
|
||||||
|
$focusedInput = null;
|
||||||
}}
|
}}
|
||||||
bind:this={plainTextInputs[index]}
|
bind:this={plainTextInputs[index]}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -3,15 +3,18 @@ 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 { createEventDispatcher, onMount } from "svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
|
|
||||||
|
export let keyCombination: string | null = null;
|
||||||
|
|
||||||
let inputRef: HTMLInputElement;
|
let inputRef: HTMLInputElement;
|
||||||
|
|
||||||
const dispatch = createEventDispatcher();
|
|
||||||
onMount(() => dispatch("mount", { input: inputRef }));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<input tabindex="-1" bind:this={inputRef} type="color" on:change />
|
<input bind:this={inputRef} tabindex="-1" type="color" on:input on:change />
|
||||||
|
|
||||||
|
{#if keyCombination}
|
||||||
|
<Shortcut {keyCombination} on:action={() => inputRef.click()} />
|
||||||
|
{/if}
|
||||||
|
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
input {
|
input {
|
|
@ -3,7 +3,6 @@ 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 ColorPicker from "../../components/ColorPicker.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import type {
|
import type {
|
||||||
FormattingNode,
|
FormattingNode,
|
||||||
|
@ -15,6 +14,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import { removeEmptyStyle, Surrounder } from "../surround";
|
import { removeEmptyStyle, Surrounder } from "../surround";
|
||||||
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { arrowIcon, highlightColorIcon } from "./icons";
|
import { arrowIcon, highlightColorIcon } from "./icons";
|
||||||
|
@ -128,11 +128,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
{@html arrowIcon}
|
{@html arrowIcon}
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
on:change={(event) => {
|
on:input={(event) => {
|
||||||
color = setColor(event);
|
color = setColor(event);
|
||||||
bridgeCommand(`lastHighlightColor:${color}`);
|
bridgeCommand(`lastTextColor:${color}`);
|
||||||
setTextColor();
|
|
||||||
}}
|
}}
|
||||||
|
on:change={() => setTextColor()}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</WithColorHelper>
|
</WithColorHelper>
|
||||||
|
|
|
@ -14,8 +14,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import { bridgeCommand } from "../../lib/bridgecommand";
|
import { bridgeCommand } from "../../lib/bridgecommand";
|
||||||
import * as tr from "../../lib/ftl";
|
import * as tr from "../../lib/ftl";
|
||||||
|
import { promiseWithResolver } from "../../lib/promise";
|
||||||
|
import { registerPackage } from "../../lib/runtime-require";
|
||||||
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 { editingInputIsRichText } from "../rich-text-input";
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import ClozeButton from "./ClozeButton.svelte";
|
import ClozeButton from "./ClozeButton.svelte";
|
||||||
import { micIcon, paperclipIcon } from "./icons";
|
import { micIcon, paperclipIcon } from "./icons";
|
||||||
|
@ -23,13 +26,44 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
const { focusedInput } = context.get();
|
const { focusedInput } = context.get();
|
||||||
|
|
||||||
const attachmentKeyCombination = "F3";
|
const attachmentCombination = "F3";
|
||||||
|
|
||||||
|
let mediaPromise: Promise<string>;
|
||||||
|
let resolve: (media: string) => void;
|
||||||
|
|
||||||
|
function mediaResolve(media: string): void {
|
||||||
|
resolve(media);
|
||||||
|
}
|
||||||
|
|
||||||
|
registerPackage("anki/TemplateButtons", { mediaResolve });
|
||||||
|
|
||||||
function onAttachment(): void {
|
function onAttachment(): void {
|
||||||
|
if (!editingInputIsRichText($focusedInput)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[mediaPromise, resolve] = promiseWithResolver<string>();
|
||||||
|
$focusedInput.focusHandler.focus.on(
|
||||||
|
async () => setFormat("inserthtml", await mediaPromise),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
bridgeCommand("attach");
|
bridgeCommand("attach");
|
||||||
}
|
}
|
||||||
|
|
||||||
const recordKeyCombination = "F5";
|
const recordCombination = "F5";
|
||||||
|
|
||||||
function onRecord(): void {
|
function onRecord(): void {
|
||||||
|
if (!editingInputIsRichText($focusedInput)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
[mediaPromise, resolve] = promiseWithResolver<string>();
|
||||||
|
$focusedInput.focusHandler.focus.on(
|
||||||
|
async () => setFormat("inserthtml", await mediaPromise),
|
||||||
|
{ once: true },
|
||||||
|
);
|
||||||
|
|
||||||
bridgeCommand("record");
|
bridgeCommand("record");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -49,7 +83,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingAttachPicturesaudiovideo()} ({getPlatformString(
|
tooltip="{tr.editingAttachPicturesaudiovideo()} ({getPlatformString(
|
||||||
attachmentKeyCombination,
|
attachmentCombination,
|
||||||
)})"
|
)})"
|
||||||
iconSize={70}
|
iconSize={70}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -57,16 +91,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
{@html paperclipIcon}
|
{@html paperclipIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Shortcut
|
<Shortcut keyCombination={attachmentCombination} on:action={onAttachment} />
|
||||||
keyCombination={attachmentKeyCombination}
|
|
||||||
on:action={onAttachment}
|
|
||||||
/>
|
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem>
|
<ButtonGroupItem>
|
||||||
<IconButton
|
<IconButton
|
||||||
tooltip="{tr.editingRecordAudio()} ({getPlatformString(
|
tooltip="{tr.editingRecordAudio()} ({getPlatformString(
|
||||||
recordKeyCombination,
|
recordCombination,
|
||||||
)})"
|
)})"
|
||||||
iconSize={70}
|
iconSize={70}
|
||||||
{disabled}
|
{disabled}
|
||||||
|
@ -74,7 +105,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
{@html micIcon}
|
{@html micIcon}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Shortcut keyCombination={recordKeyCombination} on:action={onRecord} />
|
<Shortcut keyCombination={recordCombination} on:action={onRecord} />
|
||||||
</ButtonGroupItem>
|
</ButtonGroupItem>
|
||||||
|
|
||||||
<ButtonGroupItem id="cloze">
|
<ButtonGroupItem id="cloze">
|
||||||
|
|
|
@ -3,7 +3,6 @@ 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 ColorPicker from "../../components/ColorPicker.svelte";
|
|
||||||
import IconButton from "../../components/IconButton.svelte";
|
import IconButton from "../../components/IconButton.svelte";
|
||||||
import Shortcut from "../../components/Shortcut.svelte";
|
import Shortcut from "../../components/Shortcut.svelte";
|
||||||
import type {
|
import type {
|
||||||
|
@ -18,6 +17,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import { editingInputIsRichText } from "../rich-text-input";
|
import { editingInputIsRichText } from "../rich-text-input";
|
||||||
import { removeEmptyStyle, Surrounder } from "../surround";
|
import { removeEmptyStyle, Surrounder } from "../surround";
|
||||||
|
import ColorPicker from "./ColorPicker.svelte";
|
||||||
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
import type { RemoveFormat } from "./EditorToolbar.svelte";
|
||||||
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
import { context as editorToolbarContext } from "./EditorToolbar.svelte";
|
||||||
import { arrowIcon, textColorIcon } from "./icons";
|
import { arrowIcon, textColorIcon } from "./icons";
|
||||||
|
@ -146,19 +146,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
>
|
>
|
||||||
{@html arrowIcon}
|
{@html arrowIcon}
|
||||||
<ColorPicker
|
<ColorPicker
|
||||||
on:change={(event) => {
|
keyCombination={pickCombination}
|
||||||
|
on:input={(event) => {
|
||||||
color = setColor(event);
|
color = setColor(event);
|
||||||
bridgeCommand(`lastTextColor:${color}`);
|
bridgeCommand(`lastTextColor:${color}`);
|
||||||
setTextColor();
|
|
||||||
}}
|
}}
|
||||||
|
on:change={() => setTextColor()}
|
||||||
/>
|
/>
|
||||||
</IconButton>
|
</IconButton>
|
||||||
<Shortcut
|
|
||||||
keyCombination={pickCombination}
|
|
||||||
on:action={(event) => {
|
|
||||||
color = setColor(event);
|
|
||||||
bridgeCommand(`lastTextColor:${color}`);
|
|
||||||
setTextColor();
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</WithColorHelper>
|
</WithColorHelper>
|
||||||
|
|
|
@ -8,7 +8,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
export let color: string;
|
export let color: string;
|
||||||
|
|
||||||
function setColor({ currentTarget }: Event): string {
|
function setColor({ currentTarget }: Event): string {
|
||||||
return (color = (currentTarget! as HTMLInputElement).value);
|
color = (currentTarget! as HTMLInputElement).value;
|
||||||
|
return color;
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
@ -17,7 +17,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import MathjaxMenu from "./MathjaxMenu.svelte";
|
import MathjaxMenu from "./MathjaxMenu.svelte";
|
||||||
|
|
||||||
const { container, api } = context.get();
|
const { container, api } = context.get();
|
||||||
const { flushCaret, preventResubscription } = api;
|
const { focusHandler, preventResubscription } = api;
|
||||||
|
|
||||||
const code = writable("");
|
const code = writable("");
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
let selectAll = false;
|
let selectAll = false;
|
||||||
|
|
||||||
function placeHandle(after: boolean): void {
|
function placeHandle(after: boolean): void {
|
||||||
flushCaret();
|
focusHandler.flushCaret();
|
||||||
|
|
||||||
if (after) {
|
if (after) {
|
||||||
(mathjaxElement as any).placeCaretAfter();
|
(mathjaxElement as any).placeCaretAfter();
|
||||||
|
|
|
@ -26,6 +26,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import CodeMirror from "../CodeMirror.svelte";
|
import CodeMirror from "../CodeMirror.svelte";
|
||||||
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||||
import { context as editingAreaContext } from "../EditingArea.svelte";
|
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||||
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
|
|
||||||
export let hidden = false;
|
export let hidden = false;
|
||||||
|
|
||||||
|
@ -35,6 +36,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
...gutterOptions,
|
...gutterOptions,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { focusedInput } = noteEditorContext.get();
|
||||||
|
|
||||||
const { editingInputs, content } = editingAreaContext.get();
|
const { editingInputs, content } = editingAreaContext.get();
|
||||||
const decoratedElements = decoratedElementsContext.get();
|
const decoratedElements = decoratedElementsContext.get();
|
||||||
const code = writable($content);
|
const code = writable($content);
|
||||||
|
@ -158,8 +161,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
class="plain-text-input"
|
class="plain-text-input"
|
||||||
class:light-theme={!$pageTheme.isDark}
|
class:light-theme={!$pageTheme.isDark}
|
||||||
class:hidden
|
class:hidden
|
||||||
on:focusin
|
on:focusin={() => ($focusedInput = api)}
|
||||||
on:focusout
|
|
||||||
>
|
>
|
||||||
<CodeMirror
|
<CodeMirror
|
||||||
{configuration}
|
{configuration}
|
||||||
|
|
|
@ -3,13 +3,12 @@ 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 context="module" lang="ts">
|
<script context="module" lang="ts">
|
||||||
|
import type { FocusHandlerAPI } from "../../editable/content-editable";
|
||||||
import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
|
import type { ContentEditableAPI } from "../../editable/ContentEditable.svelte";
|
||||||
import contextProperty from "../../sveltelib/context-property";
|
import useContextProperty from "../../sveltelib/context-property";
|
||||||
import type {
|
import useDOMMirror from "../../sveltelib/dom-mirror";
|
||||||
OnInputCallback,
|
import type { InputHandlerAPI } from "../../sveltelib/input-handler";
|
||||||
OnInsertCallback,
|
import useInputHandler from "../../sveltelib/input-handler";
|
||||||
Trigger,
|
|
||||||
} from "../../sveltelib/input-manager";
|
|
||||||
import { pageTheme } from "../../sveltelib/theme";
|
import { pageTheme } from "../../sveltelib/theme";
|
||||||
import type { EditingInputAPI } from "../EditingArea.svelte";
|
import type { EditingInputAPI } from "../EditingArea.svelte";
|
||||||
import type CustomStyles from "./CustomStyles.svelte";
|
import type CustomStyles from "./CustomStyles.svelte";
|
||||||
|
@ -21,9 +20,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
moveCaretToEnd(): void;
|
moveCaretToEnd(): void;
|
||||||
toggle(): boolean;
|
toggle(): boolean;
|
||||||
preventResubscription(): () => void;
|
preventResubscription(): () => void;
|
||||||
getTriggerOnNextInsert(): Trigger<OnInsertCallback>;
|
inputHandler: InputHandlerAPI;
|
||||||
getTriggerOnInput(): Trigger<OnInputCallback>;
|
focusHandler: FocusHandlerAPI;
|
||||||
getTriggerAfterInput(): Trigger<OnInputCallback>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editingInputIsRichText(
|
export function editingInputIsRichText(
|
||||||
|
@ -39,19 +37,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
}
|
}
|
||||||
|
|
||||||
const key = Symbol("richText");
|
const key = Symbol("richText");
|
||||||
const [context, setContextProperty] = contextProperty<RichTextInputContextAPI>(key);
|
const [context, setContextProperty] =
|
||||||
|
useContextProperty<RichTextInputContextAPI>(key);
|
||||||
|
const [globalInputHandler, setupGlobalInputHandler] = useInputHandler();
|
||||||
|
|
||||||
import getInputManager from "../../sveltelib/input-manager";
|
export { context, globalInputHandler as inputHandler };
|
||||||
import getDOMMirror from "../../sveltelib/mirror-dom";
|
|
||||||
|
|
||||||
const {
|
|
||||||
manager: globalInputManager,
|
|
||||||
getTriggerAfterInput,
|
|
||||||
getTriggerOnInput,
|
|
||||||
getTriggerOnNextInsert,
|
|
||||||
} = getInputManager();
|
|
||||||
|
|
||||||
export { context, getTriggerAfterInput, getTriggerOnInput, getTriggerOnNextInsert };
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
@ -71,11 +61,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
import { nodeStore } from "../../sveltelib/node-store";
|
import { nodeStore } from "../../sveltelib/node-store";
|
||||||
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
import { context as decoratedElementsContext } from "../DecoratedElements.svelte";
|
||||||
import { context as editingAreaContext } from "../EditingArea.svelte";
|
import { context as editingAreaContext } from "../EditingArea.svelte";
|
||||||
|
import { context as noteEditorContext } from "../NoteEditor.svelte";
|
||||||
import RichTextStyles from "./RichTextStyles.svelte";
|
import RichTextStyles from "./RichTextStyles.svelte";
|
||||||
import SetContext from "./SetContext.svelte";
|
import SetContext from "./SetContext.svelte";
|
||||||
|
|
||||||
export let hidden: boolean;
|
export let hidden: boolean;
|
||||||
|
|
||||||
|
const { focusedInput } = noteEditorContext.get();
|
||||||
|
|
||||||
const { content, editingInputs } = editingAreaContext.get();
|
const { content, editingInputs } = editingAreaContext.get();
|
||||||
const decoratedElements = decoratedElementsContext.get();
|
const decoratedElements = decoratedElementsContext.get();
|
||||||
|
|
||||||
|
@ -175,8 +168,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const { mirror, preventResubscription } = getDOMMirror();
|
const { mirror, preventResubscription } = useDOMMirror();
|
||||||
const localInputManager = getInputManager();
|
const [inputHandler, setupInputHandler] = useInputHandler();
|
||||||
|
|
||||||
function moveCaretToEnd() {
|
function moveCaretToEnd() {
|
||||||
richTextPromise.then(placeCaretAfterContent);
|
richTextPromise.then(placeCaretAfterContent);
|
||||||
|
@ -205,9 +198,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
},
|
},
|
||||||
moveCaretToEnd,
|
moveCaretToEnd,
|
||||||
preventResubscription,
|
preventResubscription,
|
||||||
getTriggerOnNextInsert: localInputManager.getTriggerOnNextInsert,
|
inputHandler,
|
||||||
getTriggerOnInput: localInputManager.getTriggerOnInput,
|
|
||||||
getTriggerAfterInput: localInputManager.getTriggerAfterInput,
|
|
||||||
} as RichTextInputAPI;
|
} as RichTextInputAPI;
|
||||||
|
|
||||||
const allContexts = getAllContexts();
|
const allContexts = getAllContexts();
|
||||||
|
@ -216,12 +207,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
stylesDidLoad.then(
|
stylesDidLoad.then(
|
||||||
() =>
|
() =>
|
||||||
new ContentEditable({
|
new ContentEditable({
|
||||||
target: element.shadowRoot!,
|
target: element.shadowRoot,
|
||||||
props: {
|
props: {
|
||||||
nodes,
|
nodes,
|
||||||
resolve,
|
resolve,
|
||||||
mirrors: [mirror],
|
mirrors: [mirror],
|
||||||
managers: [globalInputManager, localInputManager.manager],
|
inputHandlers: [setupInputHandler, setupGlobalInputHandler],
|
||||||
api,
|
api,
|
||||||
},
|
},
|
||||||
context: allContexts,
|
context: allContexts,
|
||||||
|
@ -253,7 +244,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="rich-text-input">
|
<div class="rich-text-input" on:focusin={() => ($focusedInput = api)}>
|
||||||
<RichTextStyles
|
<RichTextStyles
|
||||||
color={$pageTheme.isDark ? "white" : "black"}
|
color={$pageTheme.isDark ? "white" : "black"}
|
||||||
let:attachToShadow={attachStyles}
|
let:attachToShadow={attachStyles}
|
||||||
|
|
|
@ -9,7 +9,7 @@ import type { SurroundFormat } from "../domlib/surround";
|
||||||
import { boolMatcher, reformat, surround, unsurround } from "../domlib/surround";
|
import { boolMatcher, reformat, surround, unsurround } from "../domlib/surround";
|
||||||
import { getRange, getSelection } from "../lib/cross-browser";
|
import { getRange, getSelection } from "../lib/cross-browser";
|
||||||
import { registerPackage } from "../lib/runtime-require";
|
import { registerPackage } from "../lib/runtime-require";
|
||||||
import type { OnInsertCallback, Trigger } from "../sveltelib/input-manager";
|
import type { TriggerItem } from "../sveltelib/handler-list";
|
||||||
import type { RichTextInputAPI } from "./rich-text-input";
|
import type { RichTextInputAPI } from "./rich-text-input";
|
||||||
|
|
||||||
function isSurroundedInner(
|
function isSurroundedInner(
|
||||||
|
@ -63,11 +63,11 @@ export class Surrounder {
|
||||||
}
|
}
|
||||||
|
|
||||||
private api: RichTextInputAPI | null = null;
|
private api: RichTextInputAPI | null = null;
|
||||||
private trigger: Trigger<OnInsertCallback> | null = null;
|
private trigger: TriggerItem<{ event: InputEvent; text: Text }> | null = null;
|
||||||
|
|
||||||
set richText(api: RichTextInputAPI) {
|
set richText(api: RichTextInputAPI) {
|
||||||
this.api = api;
|
this.api = api;
|
||||||
this.trigger = api.getTriggerOnNextInsert();
|
this.trigger = api.inputHandler.insertText.trigger({ once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -76,6 +76,7 @@ export class Surrounder {
|
||||||
*/
|
*/
|
||||||
disable(): void {
|
disable(): void {
|
||||||
this.api = null;
|
this.api = null;
|
||||||
|
this.trigger?.off();
|
||||||
this.trigger = null;
|
this.trigger = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,13 +96,13 @@ export class Surrounder {
|
||||||
exclusive: SurroundFormat<T>[] = [],
|
exclusive: SurroundFormat<T>[] = [],
|
||||||
): void {
|
): void {
|
||||||
if (get(this.trigger!.active)) {
|
if (get(this.trigger!.active)) {
|
||||||
this.trigger!.remove();
|
this.trigger!.off();
|
||||||
} else {
|
} else {
|
||||||
this.trigger!.add(async ({ node }: { node: Node }) => {
|
this.trigger!.on(async ({ text }) => {
|
||||||
const range = new Range();
|
const range = new Range();
|
||||||
range.selectNode(node);
|
range.selectNode(text);
|
||||||
|
|
||||||
const matches = Boolean(findClosest(node, base, matcher));
|
const matches = Boolean(findClosest(text, base, matcher));
|
||||||
const clearedRange = removeFormats(range, base, exclusive);
|
const clearedRange = removeFormats(range, base, exclusive);
|
||||||
surroundAndSelect(matches, clearedRange, base, format, selection);
|
surroundAndSelect(matches, clearedRange, base, format, selection);
|
||||||
selection.collapseToEnd();
|
selection.collapseToEnd();
|
||||||
|
@ -109,6 +110,41 @@ export class Surrounder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _toggleTriggerOverwrite<T>(
|
||||||
|
base: HTMLElement,
|
||||||
|
selection: Selection,
|
||||||
|
format: SurroundFormat<T>,
|
||||||
|
exclusive: SurroundFormat<T>[] = [],
|
||||||
|
): void {
|
||||||
|
this.trigger!.on(async ({ text }) => {
|
||||||
|
const range = new Range();
|
||||||
|
range.selectNode(text);
|
||||||
|
|
||||||
|
const clearedRange = removeFormats(range, base, exclusive);
|
||||||
|
const surroundedRange = surround(clearedRange, base, format);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(surroundedRange);
|
||||||
|
selection.collapseToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _toggleTriggerRemove<T>(
|
||||||
|
base: HTMLElement,
|
||||||
|
selection: Selection,
|
||||||
|
remove: SurroundFormat<T>[],
|
||||||
|
reformat: SurroundFormat<T>[] = [],
|
||||||
|
): void {
|
||||||
|
this.trigger!.on(async ({ text }) => {
|
||||||
|
const range = new Range();
|
||||||
|
range.selectNode(text);
|
||||||
|
|
||||||
|
const clearedRange = removeFormats(range, base, remove, reformat);
|
||||||
|
selection.removeAllRanges();
|
||||||
|
selection.addRange(clearedRange);
|
||||||
|
selection.collapseToEnd();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Use the surround command on the current range of the RichTextInput.
|
* Use the surround command on the current range of the RichTextInput.
|
||||||
* If the range is already surrounded, it will unsurround instead.
|
* If the range is already surrounded, it will unsurround instead.
|
||||||
|
@ -148,14 +184,13 @@ export class Surrounder {
|
||||||
const base = await this._assert_base();
|
const base = await this._assert_base();
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
const matcher = boolMatcher(format);
|
|
||||||
|
|
||||||
if (!range) {
|
if (!range) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (range.collapsed) {
|
if (range.collapsed) {
|
||||||
return this._toggleTrigger(base, selection, matcher, format, exclusive);
|
return this._toggleTriggerOverwrite(base, selection, format, exclusive);
|
||||||
}
|
}
|
||||||
|
|
||||||
const clearedRange = removeFormats(range, base, exclusive);
|
const clearedRange = removeFormats(range, base, exclusive);
|
||||||
|
@ -194,10 +229,14 @@ export class Surrounder {
|
||||||
const selection = getSelection(base)!;
|
const selection = getSelection(base)!;
|
||||||
const range = getRange(selection);
|
const range = getRange(selection);
|
||||||
|
|
||||||
if (!range || range.collapsed) {
|
if (!range) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (range.collapsed) {
|
||||||
|
return this._toggleTriggerRemove(base, selection, formats, reformats);
|
||||||
|
}
|
||||||
|
|
||||||
const surroundedRange = removeFormats(range, base, formats, reformats);
|
const surroundedRange = removeFormats(range, base, formats, reformats);
|
||||||
selection.removeAllRanges();
|
selection.removeAllRanges();
|
||||||
selection.addRange(surroundedRange);
|
selection.addRange(surroundedRange);
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
*/
|
*/
|
||||||
type AnkiPackages =
|
type AnkiPackages =
|
||||||
| "anki/NoteEditor"
|
| "anki/NoteEditor"
|
||||||
|
| "anki/TemplateButtons"
|
||||||
| "anki/packages"
|
| "anki/packages"
|
||||||
| "anki/bridgecommand"
|
| "anki/bridgecommand"
|
||||||
| "anki/shortcuts"
|
| "anki/shortcuts"
|
||||||
|
|
|
@ -4,3 +4,5 @@
|
||||||
export function assertUnreachable(x: never): never {
|
export function assertUnreachable(x: never): never {
|
||||||
throw new Error(`unreachable: ${x}`);
|
throw new Error(`unreachable: ${x}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type Callback = () => void;
|
||||||
|
|
|
@ -41,7 +41,7 @@ function cloneNode(node: Node): DocumentFragment {
|
||||||
* While the element has focus, this connection is tethered.
|
* While the element has focus, this connection is tethered.
|
||||||
* In practice, this will sync changes from PlainTextInput to RichTextInput.
|
* In practice, this will sync changes from PlainTextInput to RichTextInput.
|
||||||
*/
|
*/
|
||||||
function getDOMMirror(): DOMMirrorAPI {
|
function useDOMMirror(): DOMMirrorAPI {
|
||||||
const allowResubscription = writable(true);
|
const allowResubscription = writable(true);
|
||||||
|
|
||||||
function preventResubscription() {
|
function preventResubscription() {
|
||||||
|
@ -128,4 +128,4 @@ function getDOMMirror(): DOMMirrorAPI {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default getDOMMirror;
|
export default useDOMMirror;
|
|
@ -6,7 +6,9 @@
|
||||||
// would not work.
|
// would not work.
|
||||||
|
|
||||||
import * as svelteRuntime from "svelte/internal";
|
import * as svelteRuntime from "svelte/internal";
|
||||||
|
import * as svelteStore from "svelte/store";
|
||||||
|
|
||||||
import { registerPackageRaw } from "../lib/runtime-require";
|
import { registerPackageRaw } from "../lib/runtime-require";
|
||||||
|
|
||||||
registerPackageRaw("svelte/internal", svelteRuntime);
|
registerPackageRaw("svelte/internal", svelteRuntime);
|
||||||
|
registerPackageRaw("svelte/store", svelteStore);
|
||||||
|
|
165
ts/sveltelib/handler-list.ts
Normal file
165
ts/sveltelib/handler-list.ts
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import type { Readable, Writable } from "svelte/store";
|
||||||
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
|
import type { Callback } from "../lib/typing";
|
||||||
|
|
||||||
|
type Handler<T> = (args: T) => Promise<void>;
|
||||||
|
|
||||||
|
interface HandlerAccess<T> {
|
||||||
|
callback: Handler<T>;
|
||||||
|
clear(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
class TriggerItem<T> {
|
||||||
|
#active: Writable<boolean>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private setter: (handler: Handler<T>, clear: Callback) => void,
|
||||||
|
private clear: Callback,
|
||||||
|
) {
|
||||||
|
this.#active = writable(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A store which indicates whether the trigger is currently turned on.
|
||||||
|
*/
|
||||||
|
get active(): Readable<boolean> {
|
||||||
|
return this.#active;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deactivate the trigger. Can be safely called multiple times.
|
||||||
|
*/
|
||||||
|
off(): void {
|
||||||
|
this.#active.set(false);
|
||||||
|
this.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
on(handler: Handler<T>): void {
|
||||||
|
this.setter(handler, () => this.off());
|
||||||
|
this.#active.set(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface HandlerOptions {
|
||||||
|
once: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class HandlerList<T> {
|
||||||
|
#list: HandlerAccess<T>[] = [];
|
||||||
|
|
||||||
|
trigger(options?: Partial<HandlerOptions>): TriggerItem<T> {
|
||||||
|
const once = options?.once ?? false;
|
||||||
|
let handler: Handler<T> | null = null;
|
||||||
|
|
||||||
|
return new TriggerItem(
|
||||||
|
(callback: Handler<T>, doClear: Callback): void => {
|
||||||
|
const handlerAccess = {
|
||||||
|
callback(args: T): Promise<void> {
|
||||||
|
const result = callback(args);
|
||||||
|
if (once) {
|
||||||
|
doClear();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
clear(): void {
|
||||||
|
if (once) {
|
||||||
|
doClear();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
this.#list.push(handlerAccess);
|
||||||
|
handler = handlerAccess.callback;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
if (handler) {
|
||||||
|
this.off(handler);
|
||||||
|
handler = null;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
on(handler: Handler<T>, options?: Partial<HandlerOptions>): Callback {
|
||||||
|
const once = options?.once ?? false;
|
||||||
|
let offHandler: Handler<T> | null = null;
|
||||||
|
|
||||||
|
const off = (): void => {
|
||||||
|
if (offHandler) {
|
||||||
|
this.off(offHandler);
|
||||||
|
offHandler = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handlerAccess = {
|
||||||
|
callback: (args: T): Promise<void> => {
|
||||||
|
const result = handler(args);
|
||||||
|
if (once) {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
clear(): void {
|
||||||
|
if (once) {
|
||||||
|
off();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
offHandler = handlerAccess.callback;
|
||||||
|
|
||||||
|
this.#list.push(handlerAccess);
|
||||||
|
return off;
|
||||||
|
}
|
||||||
|
|
||||||
|
private off(handler: Handler<T>): void {
|
||||||
|
const index = this.#list.findIndex(
|
||||||
|
(value: HandlerAccess<T>): boolean => value.callback === handler,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (index >= 0) {
|
||||||
|
this.#list.splice(index, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get length(): number {
|
||||||
|
return this.#list.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
dispatch(args: T): Promise<void> {
|
||||||
|
const promises: Promise<void>[] = [];
|
||||||
|
|
||||||
|
for (const { callback } of [...this]) {
|
||||||
|
promises.push(callback(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
return Promise.all(promises) as unknown as Promise<void>;
|
||||||
|
}
|
||||||
|
|
||||||
|
clear(): void {
|
||||||
|
for (const { clear } of [...this]) {
|
||||||
|
clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Symbol.iterator](): Iterator<HandlerAccess<T>, null, unknown> {
|
||||||
|
const list = this.#list;
|
||||||
|
let step = 0;
|
||||||
|
|
||||||
|
return {
|
||||||
|
next(): IteratorResult<HandlerAccess<T>, null> {
|
||||||
|
if (step >= list.length) {
|
||||||
|
return { value: null, done: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: list[step++], done: false };
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type { TriggerItem };
|
112
ts/sveltelib/input-handler.ts
Normal file
112
ts/sveltelib/input-handler.ts
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
// Copyright: Ankitects Pty Ltd and contributors
|
||||||
|
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||||
|
|
||||||
|
import { getRange, getSelection } from "../lib/cross-browser";
|
||||||
|
import { on } from "../lib/events";
|
||||||
|
import { keyboardEventIsPrintableKey } from "../lib/keys";
|
||||||
|
import { HandlerList } from "./handler-list";
|
||||||
|
|
||||||
|
const nbsp = "\xa0";
|
||||||
|
|
||||||
|
export type SetupInputHandlerAction = (element: HTMLElement) => { destroy(): void };
|
||||||
|
|
||||||
|
export interface InputEventParams {
|
||||||
|
event: InputEvent;
|
||||||
|
}
|
||||||
|
export interface InsertTextParams {
|
||||||
|
event: InputEvent;
|
||||||
|
text: Text;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface InputHandlerAPI {
|
||||||
|
readonly beforeInput: HandlerList<InputEventParams>;
|
||||||
|
readonly insertText: HandlerList<InsertTextParams>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface that allows Svelte components to attach event listeners via triggers.
|
||||||
|
* They will be attached to the component(s) that install the manager.
|
||||||
|
* Prevents that too many event listeners are attached and allows for some
|
||||||
|
* coordination between them.
|
||||||
|
*/
|
||||||
|
function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
||||||
|
const beforeInput = new HandlerList<InputEventParams>();
|
||||||
|
const insertText = new HandlerList<InsertTextParams>();
|
||||||
|
|
||||||
|
async function onBeforeInput(this: Element, event: InputEvent): Promise<void> {
|
||||||
|
const selection = getSelection(this)!;
|
||||||
|
const range = getRange(selection);
|
||||||
|
|
||||||
|
await beforeInput.dispatch({ event });
|
||||||
|
|
||||||
|
if (!range || event.inputType !== "insertText" || insertText.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
const content = !event.data || event.data === " " ? nbsp : event.data;
|
||||||
|
const text = new Text(content);
|
||||||
|
|
||||||
|
range.deleteContents();
|
||||||
|
range.insertNode(text);
|
||||||
|
range.selectNode(text);
|
||||||
|
range.collapse(false);
|
||||||
|
|
||||||
|
await insertText.dispatch({ event, text });
|
||||||
|
|
||||||
|
range.commonAncestorContainer.normalize();
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearInsertText(): void {
|
||||||
|
insertText.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeydown(event: KeyboardEvent): void {
|
||||||
|
/* using arrow keys should cancel */
|
||||||
|
if (!keyboardEventIsPrintableKey(event)) {
|
||||||
|
clearInsertText();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onInput(this: HTMLElement, event: InputEvent): void {
|
||||||
|
// prevent unwanted <div> from being left behind when clearing field contents
|
||||||
|
if (
|
||||||
|
!event.data &&
|
||||||
|
this.children.length === 1 &&
|
||||||
|
this.children.item(0) instanceof HTMLDivElement &&
|
||||||
|
/^\n?$/.test(this.innerText)
|
||||||
|
) {
|
||||||
|
this.innerHTML = "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupHandler(element: HTMLElement): { destroy(): void } {
|
||||||
|
const beforeInputOff = on(element, "beforeinput", onBeforeInput);
|
||||||
|
const inputOff = on(element, "input" as "beforeinput", onInput);
|
||||||
|
|
||||||
|
const blurOff = on(element, "blur", clearInsertText);
|
||||||
|
const pointerDownOff = on(element, "pointerdown", clearInsertText);
|
||||||
|
const keyDownOff = on(element, "keydown", onKeydown);
|
||||||
|
|
||||||
|
return {
|
||||||
|
destroy() {
|
||||||
|
beforeInputOff();
|
||||||
|
inputOff();
|
||||||
|
blurOff();
|
||||||
|
pointerDownOff();
|
||||||
|
keyDownOff();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
beforeInput,
|
||||||
|
insertText,
|
||||||
|
},
|
||||||
|
setupHandler,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
export default useInputHandler;
|
|
@ -1,176 +0,0 @@
|
||||||
// Copyright: Ankitects Pty Ltd and contributors
|
|
||||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|
||||||
|
|
||||||
import type { Writable } from "svelte/store";
|
|
||||||
import { writable } from "svelte/store";
|
|
||||||
|
|
||||||
import { getSelection } from "../lib/cross-browser";
|
|
||||||
import { on } from "../lib/events";
|
|
||||||
import { id } from "../lib/functional";
|
|
||||||
import { keyboardEventIsPrintableKey } from "../lib/keys";
|
|
||||||
|
|
||||||
export type OnInsertCallback = ({
|
|
||||||
node,
|
|
||||||
event,
|
|
||||||
}: {
|
|
||||||
node: Node;
|
|
||||||
event: InputEvent;
|
|
||||||
}) => Promise<void>;
|
|
||||||
|
|
||||||
export type OnInputCallback = ({ event }: { event: InputEvent }) => Promise<void>;
|
|
||||||
|
|
||||||
export interface Trigger<C> {
|
|
||||||
add(callback: C): void;
|
|
||||||
remove(): void;
|
|
||||||
active: Writable<boolean>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type Managed<C> = Pick<Trigger<C>, "remove"> & { callback: C };
|
|
||||||
|
|
||||||
export type InputManagerAction = (element: HTMLElement) => { destroy(): void };
|
|
||||||
|
|
||||||
interface InputManager {
|
|
||||||
manager: InputManagerAction;
|
|
||||||
getTriggerOnNextInsert(): Trigger<OnInsertCallback>;
|
|
||||||
getTriggerOnInput(): Trigger<OnInputCallback>;
|
|
||||||
getTriggerAfterInput(): Trigger<OnInputCallback>;
|
|
||||||
}
|
|
||||||
|
|
||||||
function trigger<C>(list: Managed<C>[]) {
|
|
||||||
return function getTrigger(): Trigger<C> {
|
|
||||||
const index = list.length++;
|
|
||||||
const active = writable(false);
|
|
||||||
|
|
||||||
function remove() {
|
|
||||||
delete list[index];
|
|
||||||
active.set(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function add(callback: C): void {
|
|
||||||
list[index] = { callback, remove };
|
|
||||||
active.set(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
add,
|
|
||||||
remove,
|
|
||||||
active,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const nbsp = "\xa0";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface that allows Svelte components to attach event listeners via triggers.
|
|
||||||
* They will be attached to the component(s) that install the manager.
|
|
||||||
* Prevents that too many event listeners are attached and allows for some
|
|
||||||
* coordination between them.
|
|
||||||
*/
|
|
||||||
function getInputManager(): InputManager {
|
|
||||||
const beforeInput: Managed<OnInputCallback>[] = [];
|
|
||||||
const beforeInsertText: Managed<OnInsertCallback>[] = [];
|
|
||||||
|
|
||||||
async function onBeforeInput(event: InputEvent): Promise<void> {
|
|
||||||
const selection = getSelection(event.target! as Node)!;
|
|
||||||
const range = selection.getRangeAt(0);
|
|
||||||
|
|
||||||
for (const { callback } of beforeInput.filter(id)) {
|
|
||||||
await callback({ event });
|
|
||||||
}
|
|
||||||
|
|
||||||
const filteredBeforeInsertText = beforeInsertText.filter(id);
|
|
||||||
|
|
||||||
if (event.inputType === "insertText" && filteredBeforeInsertText.length > 0) {
|
|
||||||
event.preventDefault();
|
|
||||||
const textContent = event.data === " " ? nbsp : event.data ?? nbsp;
|
|
||||||
const node = new Text(textContent);
|
|
||||||
|
|
||||||
range.deleteContents();
|
|
||||||
range.insertNode(node);
|
|
||||||
range.selectNode(node);
|
|
||||||
range.collapse(false);
|
|
||||||
|
|
||||||
for (const { callback, remove } of filteredBeforeInsertText) {
|
|
||||||
await callback({ node, event });
|
|
||||||
remove();
|
|
||||||
}
|
|
||||||
|
|
||||||
/* we call explicitly because we prevented default */
|
|
||||||
callAfterInputHooks(event);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const afterInput: Managed<OnInputCallback>[] = [];
|
|
||||||
|
|
||||||
async function callAfterInputHooks(event: InputEvent): Promise<void> {
|
|
||||||
for (const { callback } of afterInput.filter(id)) {
|
|
||||||
await callback({ event });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearInsertText(): void {
|
|
||||||
for (const { remove } of beforeInsertText.filter(id)) {
|
|
||||||
remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearInsertTextIfUnprintableKey(event: KeyboardEvent): void {
|
|
||||||
/* using arrow keys should cancel */
|
|
||||||
if (!keyboardEventIsPrintableKey(event)) {
|
|
||||||
clearInsertText();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onInput(event: Event): void {
|
|
||||||
if (
|
|
||||||
!(event instanceof InputEvent) ||
|
|
||||||
!(event.currentTarget instanceof HTMLElement)
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// prevent unwanted <div> from being left behind when clearing field contents
|
|
||||||
if (
|
|
||||||
(event.data === null || event.data === "") &&
|
|
||||||
event.currentTarget.children.length === 1 &&
|
|
||||||
event.currentTarget.children.item(0) instanceof HTMLDivElement &&
|
|
||||||
/^\n?$/.test(event.currentTarget.innerText)
|
|
||||||
) {
|
|
||||||
event.currentTarget.innerHTML = "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function manager(element: HTMLElement): { destroy(): void } {
|
|
||||||
const removeBeforeInput = on(element, "beforeinput", onBeforeInput);
|
|
||||||
const removeInput = on(
|
|
||||||
element,
|
|
||||||
"input",
|
|
||||||
onInput as unknown as (event: Event) => void,
|
|
||||||
);
|
|
||||||
|
|
||||||
const removeBlur = on(element, "blur", clearInsertText);
|
|
||||||
const removePointerDown = on(element, "pointerdown", clearInsertText);
|
|
||||||
const removeKeyDown = on(element, "keydown", clearInsertTextIfUnprintableKey);
|
|
||||||
|
|
||||||
return {
|
|
||||||
destroy() {
|
|
||||||
removeInput();
|
|
||||||
removeBeforeInput();
|
|
||||||
removeBlur();
|
|
||||||
removePointerDown();
|
|
||||||
removeKeyDown();
|
|
||||||
removeInput();
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
manager,
|
|
||||||
getTriggerOnNextInsert: trigger(beforeInsertText),
|
|
||||||
getTriggerOnInput: trigger(beforeInput),
|
|
||||||
getTriggerAfterInput: trigger(afterInput),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default getInputManager;
|
|
Loading…
Reference in a new issue