mirror of
https://github.com/ankitects/anki.git
synced 2025-09-18 14:02:21 -04:00
Insert symbols overlay (#2051)
* Add flag for enabling insert symbols feature * Add symbols overlay directory * Detect if :xy is inserted into editable * Allow naive updating of overlay, and special handling of ':' * First step towards better Virtual Element support * Update floating to reference range on insert text * Position SymbolsOverlay always on top or bottom * Add a data-provider to emulate API * Show correct suggestions in symbols overlay * Rename to replacementLength * Allow replacing via clicking in menu * Optionally remove inline padding of Popover * Hide Symbols overlay on blur of content editable * Add specialKey to inputHandler and generalize how arrow movement is detected - This way macOS users can use Ctrl-N to mean down, etc. * Detect special key from within SymbolsOverlay * Implement full backwards search while typing * Allow navigating symbol menu and accepting with enter * Add some entries to data-provider * Satisfy eslint * Generate symbolsTable from sources * Use other github source, allow multiple names In return, symbol must be unique * Automatically scroll in symbols dropdown * Use from npm packages rather than downloading from URL * Remove console.log * Remove print * Add pointerDown event to input-handler - so that SymbolsOverlay can reset on field click * Make tab do the same as enter * Make font a bit smaller but increase relative icon size * Satisfy type requirement of handlerlist * Revert changing default size of DropdownItems * Remove some now unused code for bootstrap dropdowns
This commit is contained in:
parent
414ff5db1c
commit
8f8f3bd465
30 changed files with 614 additions and 97 deletions
|
@ -64,12 +64,14 @@
|
|||
"@types/marked": "^4.0.1",
|
||||
"bootstrap": "=5.0.2",
|
||||
"bootstrap-icons": "^1.4.0",
|
||||
"character-entities": "^2.0.2",
|
||||
"codemirror": "^5.63.1",
|
||||
"css-browser-selector": "^0.6.5",
|
||||
"d3": "^7.0.0",
|
||||
"eslint-plugin-import": "^2.25.4",
|
||||
"eslint-plugin-simple-import-sort": "^7.0.0",
|
||||
"eslint-plugin-svelte3": "^3.4.0",
|
||||
"gemoji": "^7.1.0",
|
||||
"intl-pluralrules": "^1.2.2",
|
||||
"jquery": "^3.5.1",
|
||||
"jquery-ui-dist": "^1.12.1",
|
||||
|
|
|
@ -533,7 +533,7 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
setNoteId({});
|
||||
setColorButtons({});
|
||||
setTags({});
|
||||
setMathjaxEnabled({});
|
||||
setMathjaxEnabled({});
|
||||
""".format(
|
||||
json.dumps(data),
|
||||
json.dumps(collapsed),
|
||||
|
@ -551,6 +551,9 @@ require("anki/ui").loaded.then(() => require("anki/NoteEditor").instances[0].too
|
|||
sticky = [field["sticky"] for field in self.note.note_type()["flds"]]
|
||||
js += " setSticky(%s);" % json.dumps(sticky)
|
||||
|
||||
if os.getenv("ANKI_EDITOR_INSERT_SYMBOLS"):
|
||||
js += " setInsertSymbolsEnabled();"
|
||||
|
||||
js = gui_hooks.editor_will_load_note(js, self.note, self)
|
||||
self.web.evalWithCallback(
|
||||
f'require("anki/ui").loaded.then(() => {{ {js} }})', oncallback
|
||||
|
|
|
@ -3,10 +3,8 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
import { dropdownKey } from "./context-keys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
import IconConstrain from "./IconConstrain.svelte";
|
||||
|
||||
let className = "";
|
||||
|
@ -21,8 +19,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
let spanRef: HTMLSpanElement;
|
||||
|
||||
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||
|
||||
onMount(() => {
|
||||
dispatch("mount", { span: spanRef });
|
||||
});
|
||||
|
@ -32,8 +28,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
bind:this={spanRef}
|
||||
title={tooltip}
|
||||
class="badge {className}"
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
{...dropdownProps}
|
||||
on:click
|
||||
on:mouseenter
|
||||
on:mouseleave
|
||||
|
|
|
@ -9,14 +9,30 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let className = "";
|
||||
export { className as class };
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
export let tooltip: string | undefined = undefined;
|
||||
export let tabbable: boolean = false;
|
||||
|
||||
export let active = false;
|
||||
|
||||
$: if (buttonRef && active) {
|
||||
/* buttonRef.scrollIntoView({ behavior: "smooth", block: "start" }); */
|
||||
/* TODO will not work on Gecko */
|
||||
(buttonRef as any).scrollIntoViewIfNeeded({
|
||||
behavior: "smooth",
|
||||
block: "start",
|
||||
});
|
||||
}
|
||||
|
||||
export let tabbable = false;
|
||||
</script>
|
||||
|
||||
<button
|
||||
bind:this={buttonRef}
|
||||
{id}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
class="dropdown-item btn {className}"
|
||||
class="dropdown-item {className}"
|
||||
class:active
|
||||
class:btn-day={!$pageTheme.isDark}
|
||||
class:btn-night={$pageTheme.isDark}
|
||||
title={tooltip}
|
||||
|
@ -36,7 +52,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
display: flex;
|
||||
justify-content: start;
|
||||
|
||||
font-size: calc(var(--base-font-size) * 0.8);
|
||||
font-size: var(--dropdown-font-size, calc(0.8 * var(--base-font-size)));
|
||||
|
||||
background: none;
|
||||
box-shadow: none !important;
|
||||
|
|
|
@ -3,11 +3,9 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { createEventDispatcher, getContext, onMount } from "svelte";
|
||||
import { createEventDispatcher, onMount } from "svelte";
|
||||
|
||||
import { pageTheme } from "../sveltelib/theme";
|
||||
import { dropdownKey } from "./context-keys";
|
||||
import type { DropdownProps } from "./dropdown";
|
||||
|
||||
export let id: string | undefined = undefined;
|
||||
let className: string = "";
|
||||
|
@ -23,8 +21,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let disabled = false;
|
||||
export let tabbable = false;
|
||||
|
||||
const dropdownProps = getContext<DropdownProps>(dropdownKey) ?? { dropdown: false };
|
||||
|
||||
let buttonRef: HTMLButtonElement;
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
@ -36,11 +32,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
{id}
|
||||
class="label-button {extendClassName(className, theme)}"
|
||||
class:active
|
||||
class:dropdown-toggle={dropdownProps.dropdown}
|
||||
class:btn-day={theme === "anki" && !$pageTheme.isDark}
|
||||
class:btn-night={theme === "anki" && $pageTheme.isDark}
|
||||
title={tooltip}
|
||||
{...dropdownProps}
|
||||
{disabled}
|
||||
tabindex={tabbable ? 0 : -1}
|
||||
on:click
|
||||
|
|
|
@ -17,8 +17,8 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
min-width: 1rem;
|
||||
max-width: 95vw;
|
||||
|
||||
/* Needs enough space for FloatingArrow to be positioned */
|
||||
padding: 6px;
|
||||
/* Needs this much space for FloatingArrow to be positioned */
|
||||
padding: var(--popover-padding-block, 6px) var(--popover-padding-inline, 6px);
|
||||
|
||||
font-size: 1rem;
|
||||
color: var(--text-fg);
|
||||
|
|
|
@ -3,7 +3,11 @@ Copyright: Ankitects Pty Ltd and contributors
|
|||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import type { FloatingElement, Placement } from "@floating-ui/dom";
|
||||
import type {
|
||||
FloatingElement,
|
||||
Placement,
|
||||
ReferenceElement,
|
||||
} from "@floating-ui/dom";
|
||||
import { createEventDispatcher, onDestroy } from "svelte";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
|
@ -21,9 +25,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import subscribeToUpdates from "../sveltelib/subscribe-updates";
|
||||
import FloatingArrow from "./FloatingArrow.svelte";
|
||||
|
||||
export let portalTarget: HTMLElement | null = null;
|
||||
export let portalTarget: HTMLElement | undefined = undefined;
|
||||
|
||||
export let placement: Placement | "auto" = "bottom";
|
||||
export let placement: Placement | Placement[] | "auto" = "bottom";
|
||||
export let offset = 5;
|
||||
export let shift = 5;
|
||||
export let inline = false;
|
||||
|
@ -58,11 +62,11 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
export let closeOnInsideClick = false;
|
||||
export let keepOnKeyup = false;
|
||||
|
||||
export let reference: HTMLElement | undefined = undefined;
|
||||
export let reference: ReferenceElement | undefined = undefined;
|
||||
let floating: FloatingElement;
|
||||
|
||||
function applyPosition(
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
): Promise<void> {
|
||||
|
@ -71,7 +75,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
async function position(
|
||||
callback: (
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
position: PositionAlgorithm,
|
||||
) => Promise<void> = applyPosition,
|
||||
|
@ -81,12 +85,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
}
|
||||
|
||||
function asReference(referenceArgument: HTMLElement) {
|
||||
function asReference(referenceArgument: Element) {
|
||||
reference = referenceArgument;
|
||||
}
|
||||
|
||||
function positioningCallback(
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
callback: PositioningCallback,
|
||||
): Callback {
|
||||
const innerFloating = floating;
|
||||
|
@ -98,7 +102,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
let cleanup: Callback | null = null;
|
||||
|
||||
function updateFloating(
|
||||
reference: HTMLElement | undefined,
|
||||
reference: ReferenceElement | undefined,
|
||||
floating: FloatingElement,
|
||||
isShowing: boolean,
|
||||
) {
|
||||
|
@ -109,6 +113,15 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
return;
|
||||
}
|
||||
|
||||
autoAction = autoUpdate(reference, positioningCallback);
|
||||
|
||||
// For virtual references, we cannot provide any
|
||||
// default closing behavior
|
||||
if (!(reference instanceof EventTarget)) {
|
||||
cleanup = autoAction.destroy!;
|
||||
return;
|
||||
}
|
||||
|
||||
const closingClick = isClosingClick(documentClick, {
|
||||
reference,
|
||||
floating,
|
||||
|
@ -135,7 +148,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
);
|
||||
}
|
||||
|
||||
autoAction = autoUpdate(reference, positioningCallback);
|
||||
cleanup = singleCallback(...subscribers, autoAction.destroy!);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
export interface DropdownProps {
|
||||
dropdown: boolean;
|
||||
"data-bs-toggle"?: string;
|
||||
"aria-expanded"?: boolean | "true" | "false";
|
||||
}
|
|
@ -44,6 +44,10 @@ export interface FocusHandlerAPI {
|
|||
* Executed upon focus event of editable.
|
||||
*/
|
||||
focus: HandlerList<{ event: FocusEvent }>;
|
||||
/**
|
||||
* Executed upon blur event of editable.
|
||||
*/
|
||||
blur: HandlerList<{ event: FocusEvent }>;
|
||||
}
|
||||
|
||||
export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
|
||||
|
@ -57,6 +61,7 @@ export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
|
|||
}
|
||||
|
||||
const focus = new HandlerList<{ event: FocusEvent }>();
|
||||
const blur = new HandlerList<{ event: FocusEvent }>();
|
||||
|
||||
function prepareFocusHandling(
|
||||
editable: HTMLElement,
|
||||
|
@ -79,6 +84,7 @@ export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
|
|||
},
|
||||
{ once: true },
|
||||
);
|
||||
|
||||
offPointerDown?.();
|
||||
offPointerDown = on(
|
||||
editable,
|
||||
|
@ -94,8 +100,9 @@ export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
|
|||
/**
|
||||
* Must execute before DOMMirror.
|
||||
*/
|
||||
function onBlur(this: HTMLElement): void {
|
||||
function onBlur(this: HTMLElement, event: FocusEvent): void {
|
||||
prepareFocusHandling(this, saveSelection(this));
|
||||
blur.dispatch({ event });
|
||||
}
|
||||
|
||||
function setupFocusHandler(editable: HTMLElement): { destroy(): void } {
|
||||
|
@ -115,6 +122,7 @@ export function useFocusHandler(): [FocusHandlerAPI, SetupFocusHandlerAction] {
|
|||
{
|
||||
flushCaret,
|
||||
focus,
|
||||
blur,
|
||||
},
|
||||
setupFocusHandler,
|
||||
];
|
||||
|
|
|
@ -31,6 +31,8 @@ _ts_deps = [
|
|||
"@npm//@types/codemirror",
|
||||
"@npm//codemirror",
|
||||
"@npm//svelte",
|
||||
"@npm//character-entities",
|
||||
"@npm//gemoji",
|
||||
]
|
||||
|
||||
compile_svelte(
|
||||
|
|
|
@ -67,6 +67,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import PlainTextBadge from "./PlainTextBadge.svelte";
|
||||
import RichTextInput, { editingInputIsRichText } from "./rich-text-input";
|
||||
import RichTextBadge from "./RichTextBadge.svelte";
|
||||
import SymbolsOverlay from "./symbols-overlay";
|
||||
|
||||
function quoteFontFamily(fontFamily: string): string {
|
||||
// generic families (e.g. sans-serif) must not be quoted
|
||||
|
@ -175,6 +176,12 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
noteId = ntid;
|
||||
}
|
||||
|
||||
let insertSymbols = false;
|
||||
|
||||
function setInsertSymbolsEnabled() {
|
||||
insertSymbols = true;
|
||||
}
|
||||
|
||||
function getNoteId(): number | null {
|
||||
return noteId;
|
||||
}
|
||||
|
@ -277,6 +284,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
setNoteId,
|
||||
wrap,
|
||||
setMathjaxEnabled,
|
||||
setInsertSymbolsEnabled,
|
||||
...oldEditorAdapter,
|
||||
});
|
||||
|
||||
|
@ -448,6 +456,9 @@ the AddCards dialog) should be implemented in the user of this component.
|
|||
>
|
||||
<ImageHandle maxWidth={250} maxHeight={125} />
|
||||
<MathjaxHandle />
|
||||
{#if insertSymbols}
|
||||
<SymbolsOverlay />
|
||||
{/if}
|
||||
<FieldDescription>
|
||||
{field.description}
|
||||
</FieldDescription>
|
||||
|
|
|
@ -88,6 +88,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<ButtonGroupItem>
|
||||
<WithFloating
|
||||
show={showFloating && !disabled}
|
||||
placement="bottom"
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
let:asReference
|
||||
|
@ -101,7 +102,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</IconButton>
|
||||
</span>
|
||||
|
||||
<Popover slot="floating">
|
||||
<Popover slot="floating" --popover-padding-inline="0">
|
||||
<ButtonToolbar wrap={false}>
|
||||
<ButtonGroup>
|
||||
<CommandIconButton
|
||||
|
|
|
@ -116,6 +116,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
<WithFloating
|
||||
show={showFloating && !disabled}
|
||||
placement="bottom"
|
||||
inline
|
||||
on:close={() => (showFloating = false)}
|
||||
let:asReference
|
||||
|
@ -133,7 +134,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</IconButton>
|
||||
</span>
|
||||
|
||||
<Popover slot="floating">
|
||||
<Popover slot="floating" --popover-padding-inline="0">
|
||||
{#each showFormats as format (format.name)}
|
||||
<DropdownItem on:click={(event) => onItemClick(event, format)}>
|
||||
<CheckBox bind:value={format.active} />
|
||||
|
|
|
@ -9,6 +9,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import * as tr from "../../lib/ftl";
|
||||
import { noop } from "../../lib/functional";
|
||||
import { isArrowLeft, isArrowRight } from "../../lib/keys";
|
||||
import { getPlatformString } from "../../lib/shortcuts";
|
||||
import { pageTheme } from "../../sveltelib/theme";
|
||||
import { baseOptions, focusAndSetCaret, latex } from "../code-mirror";
|
||||
|
@ -50,9 +51,9 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
editor.on(
|
||||
"keydown",
|
||||
(_instance: CodeMirrorLib.Editor, event: KeyboardEvent): void => {
|
||||
if (event.key === "ArrowLeft") {
|
||||
if (isArrowLeft(event)) {
|
||||
direction = "start";
|
||||
} else if (event.key === "ArrowRight") {
|
||||
} else if (isArrowRight(event)) {
|
||||
direction = "end";
|
||||
}
|
||||
},
|
||||
|
|
297
ts/editor/symbols-overlay/SymbolsOverlay.svelte
Normal file
297
ts/editor/symbols-overlay/SymbolsOverlay.svelte
Normal file
|
@ -0,0 +1,297 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
|
||||
import DropdownItem from "../../components/DropdownItem.svelte";
|
||||
import Popover from "../../components/Popover.svelte";
|
||||
import WithFloating from "../../components/WithFloating.svelte";
|
||||
import {
|
||||
getRange,
|
||||
getSelection,
|
||||
isSelectionCollapsed,
|
||||
} from "../../lib/cross-browser";
|
||||
import type { Callback } from "../../lib/typing";
|
||||
import { singleCallback } from "../../lib/typing";
|
||||
import { context } from "../rich-text-input";
|
||||
import type { SymbolsTable } from "./data-provider";
|
||||
import { getSymbolExact, getSymbols } from "./data-provider";
|
||||
|
||||
const SYMBOLS_DELIMITER = ":";
|
||||
|
||||
const { inputHandler, editable } = context.get();
|
||||
|
||||
let referenceRange: Range | undefined = undefined;
|
||||
let cleanup: Callback;
|
||||
let query: string = "";
|
||||
let activeItem = 0;
|
||||
|
||||
let foundSymbols: SymbolsTable = [];
|
||||
|
||||
function unsetReferenceRange() {
|
||||
referenceRange = undefined;
|
||||
activeItem = 0;
|
||||
cleanup?.();
|
||||
}
|
||||
|
||||
async function maybeShowOverlay(
|
||||
selection: Selection,
|
||||
event: InputEvent,
|
||||
): Promise<void> {
|
||||
if (
|
||||
event.inputType !== "insertText" ||
|
||||
event.data === SYMBOLS_DELIMITER ||
|
||||
!isSelectionCollapsed(selection)
|
||||
) {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
const currentRange = getRange(selection)!;
|
||||
const offset = currentRange.endOffset;
|
||||
|
||||
if (!(currentRange.commonAncestorContainer instanceof Text) || offset < 2) {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
const wholeText = currentRange.commonAncestorContainer.wholeText;
|
||||
|
||||
for (let index = offset - 1; index >= 0; index--) {
|
||||
const currentCharacter = wholeText[index];
|
||||
|
||||
if (currentCharacter === " ") {
|
||||
return unsetReferenceRange();
|
||||
} else if (currentCharacter === SYMBOLS_DELIMITER) {
|
||||
const possibleQuery =
|
||||
wholeText.substring(index + 1, offset) + event.data;
|
||||
|
||||
if (possibleQuery.length < 2) {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
query = possibleQuery;
|
||||
referenceRange = currentRange;
|
||||
foundSymbols = await getSymbols(query);
|
||||
|
||||
cleanup = editable.focusHandler.blur.on(
|
||||
async () => unsetReferenceRange(),
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function replaceText(
|
||||
selection: Selection,
|
||||
text: Text,
|
||||
symbolCharacter: string,
|
||||
): Promise<void> {
|
||||
text.replaceData(0, text.length, symbolCharacter);
|
||||
unsetReferenceRange();
|
||||
|
||||
// Place caret behind it
|
||||
const range = new Range();
|
||||
range.selectNode(text);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
selection.collapseToEnd();
|
||||
}
|
||||
|
||||
function replaceTextOnDemand(symbolCharacter: string): void {
|
||||
const commonAncestor = referenceRange!.commonAncestorContainer as Text;
|
||||
const selection = getSelection(commonAncestor)!;
|
||||
|
||||
const replacementLength =
|
||||
commonAncestor.data
|
||||
.substring(0, referenceRange!.endOffset)
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("")
|
||||
.indexOf(SYMBOLS_DELIMITER) + 1;
|
||||
|
||||
const newOffset = referenceRange!.endOffset - replacementLength + 1;
|
||||
|
||||
commonAncestor.replaceData(
|
||||
referenceRange!.endOffset - replacementLength,
|
||||
replacementLength + 1,
|
||||
symbolCharacter,
|
||||
);
|
||||
|
||||
// Place caret behind it
|
||||
const range = new Range();
|
||||
range.setEnd(commonAncestor, newOffset);
|
||||
range.collapse(false);
|
||||
|
||||
selection.removeAllRanges();
|
||||
selection.addRange(range);
|
||||
|
||||
unsetReferenceRange();
|
||||
}
|
||||
|
||||
async function updateOverlay(
|
||||
selection: Selection,
|
||||
event: InputEvent,
|
||||
): Promise<void> {
|
||||
if (event.inputType !== "insertText") {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
const data = event.data;
|
||||
referenceRange = getRange(selection)!;
|
||||
|
||||
if (data === SYMBOLS_DELIMITER && query) {
|
||||
const symbol = await getSymbolExact(query);
|
||||
|
||||
if (!symbol) {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
const currentRange = getRange(selection)!;
|
||||
const offset = currentRange.endOffset;
|
||||
|
||||
if (!(currentRange.commonAncestorContainer instanceof Text) || offset < 2) {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
const commonAncestor = currentRange.commonAncestorContainer;
|
||||
|
||||
const replacementLength =
|
||||
commonAncestor.data
|
||||
.substring(0, currentRange.endOffset)
|
||||
.split("")
|
||||
.reverse()
|
||||
.join("")
|
||||
.indexOf(SYMBOLS_DELIMITER) + 1;
|
||||
|
||||
commonAncestor.deleteData(
|
||||
currentRange.endOffset - replacementLength,
|
||||
replacementLength,
|
||||
);
|
||||
|
||||
inputHandler.insertText.on(
|
||||
async ({ text }) => replaceText(selection, text, symbol),
|
||||
{
|
||||
once: true,
|
||||
},
|
||||
);
|
||||
} else if (query) {
|
||||
query += data!;
|
||||
foundSymbols = await getSymbols(query);
|
||||
}
|
||||
}
|
||||
|
||||
async function onBeforeInput({ event }): Promise<void> {
|
||||
const selection = getSelection(event.target)!;
|
||||
|
||||
if (referenceRange) {
|
||||
await updateOverlay(selection, event);
|
||||
} else {
|
||||
await maybeShowOverlay(selection, event);
|
||||
}
|
||||
}
|
||||
|
||||
$: showSymbolsOverlay = referenceRange && foundSymbols.length > 0;
|
||||
|
||||
async function onSpecialKey({ event, action }): Promise<void> {
|
||||
if (!showSymbolsOverlay) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (["caretLeft", "caretRight"].includes(action)) {
|
||||
return unsetReferenceRange();
|
||||
}
|
||||
|
||||
event.preventDefault();
|
||||
|
||||
if (action === "caretUp") {
|
||||
if (activeItem === 0) {
|
||||
activeItem = foundSymbols.length - 1;
|
||||
} else {
|
||||
activeItem--;
|
||||
}
|
||||
} else if (action === "caretDown") {
|
||||
if (activeItem >= foundSymbols.length - 1) {
|
||||
activeItem = 0;
|
||||
} else {
|
||||
activeItem++;
|
||||
}
|
||||
} else if (action === "enter" || action === "tab") {
|
||||
replaceTextOnDemand(foundSymbols[activeItem].symbol);
|
||||
} else if (action === "escape") {
|
||||
unsetReferenceRange();
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() =>
|
||||
singleCallback(
|
||||
inputHandler.beforeInput.on(onBeforeInput),
|
||||
inputHandler.specialKey.on(onSpecialKey),
|
||||
inputHandler.pointerDown.on(async () => unsetReferenceRange()),
|
||||
),
|
||||
);
|
||||
</script>
|
||||
|
||||
<div class="symbols-overlay">
|
||||
{#if showSymbolsOverlay}
|
||||
<WithFloating
|
||||
reference={referenceRange}
|
||||
placement={["top", "bottom"]}
|
||||
offset={10}
|
||||
>
|
||||
<Popover slot="floating" --popover-padding-inline="0">
|
||||
<div class="symbols-menu">
|
||||
{#each foundSymbols as found, index (found.symbol)}
|
||||
<DropdownItem
|
||||
active={index === activeItem}
|
||||
on:click={() => replaceTextOnDemand(found.symbol)}
|
||||
>
|
||||
<div class="symbol">{found.symbol}</div>
|
||||
<div class="description">
|
||||
{#each found.names as name}
|
||||
<span class="name">
|
||||
{SYMBOLS_DELIMITER}{name}{SYMBOLS_DELIMITER}
|
||||
</span>
|
||||
{/each}
|
||||
</div>
|
||||
</DropdownItem>
|
||||
{/each}
|
||||
</div>
|
||||
</Popover>
|
||||
</WithFloating>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.symbols-menu {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
|
||||
min-width: 140px;
|
||||
max-height: 15rem;
|
||||
|
||||
font-size: 12px;
|
||||
overflow-x: hidden;
|
||||
text-overflow: ellipsis;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
transform: scale(1.1);
|
||||
font-size: 150%;
|
||||
/* The widest emojis I could find were couple_with_heart_ */
|
||||
width: 38px;
|
||||
}
|
||||
|
||||
.description {
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.name {
|
||||
margin-left: 3px;
|
||||
}
|
||||
</style>
|
60
ts/editor/symbols-overlay/data-provider.ts
Normal file
60
ts/editor/symbols-overlay/data-provider.ts
Normal file
|
@ -0,0 +1,60 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import { characterEntities } from "character-entities";
|
||||
import { gemoji } from "gemoji";
|
||||
|
||||
interface SymbolsEntry {
|
||||
symbol: string;
|
||||
names: string[];
|
||||
tags: string[];
|
||||
containsHTML?: boolean;
|
||||
autoInsert?: boolean;
|
||||
}
|
||||
|
||||
export type SymbolsTable = SymbolsEntry[];
|
||||
|
||||
const symbolsTable: SymbolsTable = [];
|
||||
|
||||
const characterTable: Record<string, string[]> = {};
|
||||
|
||||
// Not all characters work well in the editor field
|
||||
delete characterEntities["Tab"];
|
||||
|
||||
for (const [name, character] of Object.entries(characterEntities)) {
|
||||
if (character in characterTable) {
|
||||
characterTable[character].push(name);
|
||||
} else {
|
||||
characterTable[character] = [name];
|
||||
}
|
||||
}
|
||||
|
||||
for (const [character, names] of Object.entries(characterTable)) {
|
||||
symbolsTable.push({
|
||||
symbol: character,
|
||||
names,
|
||||
tags: [],
|
||||
});
|
||||
}
|
||||
|
||||
for (const { emoji, names, tags } of gemoji) {
|
||||
symbolsTable.push({
|
||||
symbol: emoji,
|
||||
names,
|
||||
tags,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSymbols(query: string): Promise<SymbolsTable> {
|
||||
return symbolsTable.filter(
|
||||
({ names, tags }) =>
|
||||
names.some((name) => name.includes(query)) ||
|
||||
tags.some((tag) => tag.includes(query)),
|
||||
);
|
||||
}
|
||||
|
||||
export async function getSymbolExact(query: string): Promise<string | null> {
|
||||
const found = symbolsTable.find(({ names }) => names.includes(query));
|
||||
|
||||
return found ? found.symbol : null;
|
||||
}
|
6
ts/editor/symbols-overlay/index.ts
Normal file
6
ts/editor/symbols-overlay/index.ts
Normal file
|
@ -0,0 +1,6 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import SymbolsOverlay from "./SymbolsOverlay.svelte";
|
||||
|
||||
export default SymbolsOverlay;
|
|
@ -4,6 +4,7 @@
|
|||
"*",
|
||||
"image-overlay/*",
|
||||
"mathjax-overlay/*",
|
||||
"symbols-overlay/*",
|
||||
"plain-text-input/*",
|
||||
"rich-text-input/*",
|
||||
"editor-toolbar/*"
|
||||
|
|
|
@ -62,6 +62,7 @@ const modifierPressed =
|
|||
export const controlPressed = modifierPressed("Control");
|
||||
export const shiftPressed = modifierPressed("Shift");
|
||||
export const altPressed = modifierPressed("Alt");
|
||||
export const metaPressed = modifierPressed("Meta");
|
||||
|
||||
export function modifiersToPlatformString(modifiers: string[]): string {
|
||||
const displayModifiers = isApplePlatform()
|
||||
|
@ -90,3 +91,35 @@ export function keyToPlatformString(key: string): string {
|
|||
return key;
|
||||
}
|
||||
}
|
||||
|
||||
export function isArrowLeft(event: KeyboardEvent): boolean {
|
||||
if (event.code === "ArrowLeft") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isApplePlatform() && metaPressed(event) && event.code === "KeyB";
|
||||
}
|
||||
|
||||
export function isArrowRight(event: KeyboardEvent): boolean {
|
||||
if (event.code === "ArrowRight") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isApplePlatform() && metaPressed(event) && event.code === "KeyF";
|
||||
}
|
||||
|
||||
export function isArrowUp(event: KeyboardEvent): boolean {
|
||||
if (event.code === "ArrowUp") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isApplePlatform() && metaPressed(event) && event.code === "KeyP";
|
||||
}
|
||||
|
||||
export function isArrowDown(event: KeyboardEvent): boolean {
|
||||
if (event.code === "ArrowDown") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isApplePlatform() && metaPressed(event) && event.code === "KeyN";
|
||||
}
|
||||
|
|
|
@ -361,6 +361,15 @@
|
|||
"path": "node_modules/chalk",
|
||||
"licenseFile": "node_modules/chalk/license"
|
||||
},
|
||||
"character-entities@2.0.2": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/wooorm/character-entities",
|
||||
"publisher": "Titus Wormer",
|
||||
"email": "tituswormer@gmail.com",
|
||||
"url": "https://wooorm.com",
|
||||
"path": "node_modules/character-entities",
|
||||
"licenseFile": "node_modules/character-entities/license"
|
||||
},
|
||||
"codemirror@5.65.2": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/codemirror/CodeMirror",
|
||||
|
@ -996,6 +1005,15 @@
|
|||
"path": "node_modules/functional-red-black-tree",
|
||||
"licenseFile": "node_modules/functional-red-black-tree/LICENSE"
|
||||
},
|
||||
"gemoji@7.1.0": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/wooorm/gemoji",
|
||||
"publisher": "Titus Wormer",
|
||||
"email": "tituswormer@gmail.com",
|
||||
"url": "https://wooorm.com",
|
||||
"path": "node_modules/gemoji",
|
||||
"licenseFile": "node_modules/gemoji/license"
|
||||
},
|
||||
"get-intrinsic@1.1.1": {
|
||||
"licenses": "MIT",
|
||||
"repository": "https://github.com/ljharb/get-intrinsic",
|
||||
|
|
|
@ -51,6 +51,11 @@ interface HandlerOptions {
|
|||
export class HandlerList<T> {
|
||||
#list: HandlerAccess<T>[] = [];
|
||||
|
||||
/**
|
||||
* Returns a `TriggerItem`, which can be used to attach event handlers.
|
||||
* This TriggerItem exposes an additional `active` store. This can be
|
||||
* useful, if other components need to react to the input handler being active.
|
||||
*/
|
||||
trigger(options?: Partial<HandlerOptions>): TriggerItem<T> {
|
||||
const once = options?.once ?? false;
|
||||
let handler: Handler<T> | null = null;
|
||||
|
@ -84,6 +89,11 @@ export class HandlerList<T> {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Attaches an event handler.
|
||||
* @returns a callback, which removes the event handler. Alternatively,
|
||||
* you can call `off` on the HandlerList.
|
||||
*/
|
||||
on(handler: Handler<T>, options?: Partial<HandlerOptions>): Callback {
|
||||
const once = options?.once ?? false;
|
||||
let offHandler: Handler<T> | null = null;
|
||||
|
|
|
@ -3,6 +3,8 @@
|
|||
|
||||
import { getRange, getSelection } from "../lib/cross-browser";
|
||||
import { on } from "../lib/events";
|
||||
import { isArrowDown, isArrowLeft, isArrowRight, isArrowUp } from "../lib/keys";
|
||||
import { singleCallback } from "../lib/typing";
|
||||
import { HandlerList } from "./handler-list";
|
||||
|
||||
const nbsp = "\xa0";
|
||||
|
@ -12,14 +14,30 @@ export type SetupInputHandlerAction = (element: HTMLElement) => { destroy(): voi
|
|||
export interface InputEventParams {
|
||||
event: InputEvent;
|
||||
}
|
||||
|
||||
export interface InsertTextParams {
|
||||
event: InputEvent;
|
||||
text: Text;
|
||||
}
|
||||
|
||||
type SpecialKeyAction =
|
||||
| "caretUp"
|
||||
| "caretDown"
|
||||
| "caretLeft"
|
||||
| "caretRight"
|
||||
| "enter"
|
||||
| "tab";
|
||||
|
||||
export interface SpecialKeyParams {
|
||||
event: KeyboardEvent;
|
||||
action: SpecialKeyAction;
|
||||
}
|
||||
|
||||
export interface InputHandlerAPI {
|
||||
readonly beforeInput: HandlerList<InputEventParams>;
|
||||
readonly insertText: HandlerList<InsertTextParams>;
|
||||
readonly pointerDown: HandlerList<{ event: PointerEvent }>;
|
||||
readonly specialKey: HandlerList<SpecialKeyParams>;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -57,31 +75,53 @@ function useInputHandler(): [InputHandlerAPI, SetupInputHandlerAction] {
|
|||
range.commonAncestorContainer.normalize();
|
||||
}
|
||||
|
||||
const pointerDown = new HandlerList<{ event: PointerEvent }>();
|
||||
|
||||
function clearInsertText(): void {
|
||||
insertText.clear();
|
||||
}
|
||||
|
||||
function onPointerDown(event: PointerEvent): void {
|
||||
pointerDown.dispatch({ event });
|
||||
clearInsertText();
|
||||
}
|
||||
|
||||
const specialKey = new HandlerList<SpecialKeyParams>();
|
||||
|
||||
async function onKeyDown(this: Element, event: KeyboardEvent): Promise<void> {
|
||||
if (isArrowDown(event)) {
|
||||
specialKey.dispatch({ event, action: "caretDown" });
|
||||
} else if (isArrowUp(event)) {
|
||||
specialKey.dispatch({ event, action: "caretUp" });
|
||||
} else if (isArrowRight(event)) {
|
||||
specialKey.dispatch({ event, action: "caretRight" });
|
||||
} else if (isArrowLeft(event)) {
|
||||
specialKey.dispatch({ event, action: "caretLeft" });
|
||||
} else if (event.code === "Enter" || event.code === "NumpadEnter") {
|
||||
specialKey.dispatch({ event, action: "enter" });
|
||||
} else if (event.code === "Tab") {
|
||||
specialKey.dispatch({ event, action: "tab" });
|
||||
}
|
||||
}
|
||||
|
||||
function setupHandler(element: HTMLElement): { destroy(): void } {
|
||||
const beforeInputOff = on(element, "beforeinput", onBeforeInput);
|
||||
const destroy = singleCallback(
|
||||
on(element, "beforeinput", onBeforeInput),
|
||||
on(element, "blur", clearInsertText),
|
||||
on(element, "pointerdown", onPointerDown),
|
||||
on(element, "keydown", onKeyDown),
|
||||
on(document, "selectionchange", clearInsertText),
|
||||
);
|
||||
|
||||
const blurOff = on(element, "blur", clearInsertText);
|
||||
const pointerDownOff = on(element, "pointerdown", clearInsertText);
|
||||
const selectionChangeOff = on(document, "selectionchange", clearInsertText);
|
||||
|
||||
return {
|
||||
destroy() {
|
||||
beforeInputOff();
|
||||
blurOff();
|
||||
pointerDownOff();
|
||||
selectionChangeOff();
|
||||
},
|
||||
};
|
||||
return { destroy };
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
beforeInput,
|
||||
insertText,
|
||||
specialKey,
|
||||
pointerDown,
|
||||
},
|
||||
setupHandler,
|
||||
];
|
||||
|
|
|
@ -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 type { FloatingElement } from "@floating-ui/dom";
|
||||
import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
||||
import { autoUpdate as floatingUiAutoUpdate } from "@floating-ui/dom";
|
||||
import type { ActionReturn } from "svelte/action";
|
||||
|
||||
|
@ -18,7 +18,7 @@ import type { Callback } from "../../lib/typing";
|
|||
* })`
|
||||
*/
|
||||
export type PositioningCallback = (
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
position: Callback,
|
||||
) => Callback;
|
||||
|
@ -27,12 +27,12 @@ export type PositioningCallback = (
|
|||
* The interface of a function that calls `computePosition` of floating-ui.
|
||||
*/
|
||||
export type PositionFunc = (
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
callback: PositioningCallback,
|
||||
) => Callback;
|
||||
|
||||
function autoUpdate(
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
/**
|
||||
* The method to position the floating element.
|
||||
*/
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
// Copyright: Ankitects Pty Ltd and contributors
|
||||
// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
|
||||
import type { FloatingElement } from "@floating-ui/dom";
|
||||
import type { FloatingElement, ReferenceElement } from "@floating-ui/dom";
|
||||
|
||||
/**
|
||||
* The interface of a function that calls `computePosition` of floating-ui.
|
||||
*/
|
||||
export type PositionAlgorithm = (
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
) => Promise<void>;
|
||||
|
|
|
@ -6,6 +6,7 @@ import type {
|
|||
FloatingElement,
|
||||
Middleware,
|
||||
Placement,
|
||||
ReferenceElement,
|
||||
} from "@floating-ui/dom";
|
||||
import {
|
||||
arrow,
|
||||
|
@ -20,7 +21,7 @@ import {
|
|||
import type { PositionAlgorithm } from "./position-algorithm";
|
||||
|
||||
export interface PositionFloatingArgs {
|
||||
placement: Placement | "auto";
|
||||
placement: Placement | Placement[] | "auto";
|
||||
arrow: HTMLElement;
|
||||
shift: number;
|
||||
offset: number;
|
||||
|
@ -41,7 +42,7 @@ function positionFloating({
|
|||
hideCallback,
|
||||
}: PositionFloatingArgs): PositionAlgorithm {
|
||||
return async function (
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
): Promise<void> {
|
||||
const middleware: Middleware[] = [
|
||||
|
@ -58,10 +59,14 @@ function positionFloating({
|
|||
middleware,
|
||||
};
|
||||
|
||||
if (placement !== "auto") {
|
||||
computeArgs.placement = placement;
|
||||
} else {
|
||||
if (Array.isArray(placement)) {
|
||||
const allowedPlacements = placement;
|
||||
|
||||
middleware.push(autoPlacement({ allowedPlacements }));
|
||||
} else if (placement === "auto") {
|
||||
middleware.push(autoPlacement());
|
||||
} else {
|
||||
computeArgs.placement = placement;
|
||||
}
|
||||
|
||||
if (hideIfEscaped) {
|
||||
|
|
|
@ -5,6 +5,7 @@ import type {
|
|||
ComputePositionConfig,
|
||||
FloatingElement,
|
||||
Middleware,
|
||||
ReferenceElement,
|
||||
} from "@floating-ui/dom";
|
||||
import { computePosition, inline, offset } from "@floating-ui/dom";
|
||||
|
||||
|
@ -22,7 +23,7 @@ function positionOverlay({
|
|||
hideCallback,
|
||||
}: PositionOverlayArgs): PositionAlgorithm {
|
||||
return async function (
|
||||
reference: HTMLElement,
|
||||
reference: ReferenceElement,
|
||||
floating: FloatingElement,
|
||||
): Promise<void> {
|
||||
const middleware: Middleware[] = inlineArg ? [inline()] : [];
|
||||
|
|
|
@ -8,6 +8,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import { writable } from "svelte/store";
|
||||
|
||||
import { execCommand } from "../domlib";
|
||||
import { isArrowDown, isArrowUp } from "../lib/keys";
|
||||
import { Tags, tags as tagsService } from "../lib/proto";
|
||||
import { TagOptionsButton } from "./tag-options-button";
|
||||
import TagEditMode from "./TagEditMode.svelte";
|
||||
|
@ -257,17 +258,17 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
}
|
||||
|
||||
function onKeydown(event: KeyboardEvent): void {
|
||||
if (isArrowUp(event)) {
|
||||
autocomplete.selectPrevious();
|
||||
event.preventDefault();
|
||||
return;
|
||||
} else if (isArrowDown(event)) {
|
||||
autocomplete.selectNext();
|
||||
event.preventDefault();
|
||||
return;
|
||||
}
|
||||
|
||||
switch (event.code) {
|
||||
case "ArrowUp":
|
||||
autocomplete.selectPrevious();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case "ArrowDown":
|
||||
autocomplete.selectNext();
|
||||
event.preventDefault();
|
||||
break;
|
||||
|
||||
case "Tab":
|
||||
if (!$show) {
|
||||
break;
|
||||
|
|
|
@ -5,6 +5,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<script lang="ts">
|
||||
import { createEventDispatcher, onMount, tick } from "svelte";
|
||||
|
||||
import { isArrowLeft, isArrowRight } from "../lib/keys";
|
||||
import { registerShortcut } from "../lib/shortcuts";
|
||||
import {
|
||||
delimChar,
|
||||
|
@ -168,27 +169,23 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
onDelete(event);
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowLeft":
|
||||
if (isEmpty()) {
|
||||
joinWithPreviousTag(event);
|
||||
} else if (caretAtStart()) {
|
||||
dispatch("tagmoveprevious");
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
|
||||
case "ArrowRight":
|
||||
if (isEmpty()) {
|
||||
joinWithNextTag(event);
|
||||
} else if (caretAtEnd()) {
|
||||
dispatch("tagmovenext");
|
||||
event.preventDefault();
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (event.key === " ") {
|
||||
if (isArrowLeft(event)) {
|
||||
if (isEmpty()) {
|
||||
joinWithPreviousTag(event);
|
||||
} else if (caretAtStart()) {
|
||||
dispatch("tagmoveprevious");
|
||||
event.preventDefault();
|
||||
}
|
||||
} else if (isArrowRight(event)) {
|
||||
if (isEmpty()) {
|
||||
joinWithNextTag(event);
|
||||
} else if (caretAtEnd()) {
|
||||
dispatch("tagmovenext");
|
||||
event.preventDefault();
|
||||
}
|
||||
} else if (event.key === " ") {
|
||||
onDelimiter(event, false);
|
||||
} else if (event.key === ":") {
|
||||
onDelimiter(event, true);
|
||||
|
|
|
@ -136,7 +136,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<slot {createAutocomplete} />
|
||||
</span>
|
||||
|
||||
<Popover slot="floating">
|
||||
<Popover slot="floating" --popover-padding-inline="0">
|
||||
<div class="autocomplete-menu">
|
||||
{#each suggestionsItems as suggestion, index}
|
||||
{#if index === selected}
|
||||
|
|
10
yarn.lock
10
yarn.lock
|
@ -1609,6 +1609,11 @@ char-regex@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf"
|
||||
integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==
|
||||
|
||||
character-entities@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22"
|
||||
integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==
|
||||
|
||||
"chokidar@>=3.0.0 <4.0.0", chokidar@^3.4.1:
|
||||
version "3.5.3"
|
||||
resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd"
|
||||
|
@ -2614,6 +2619,11 @@ functional-red-black-tree@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz#1b0ab3bd553b2a0d6399d29c0e3ea0b252078327"
|
||||
integrity sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=
|
||||
|
||||
gemoji@^7.1.0:
|
||||
version "7.1.0"
|
||||
resolved "https://registry.yarnpkg.com/gemoji/-/gemoji-7.1.0.tgz#165403777681a9690d649aabd104da037bdd7739"
|
||||
integrity sha512-wI0YWDIfQraQMDs0yXAVQiVBZeMm/rIYssf8LZlMDdssKF19YqJKOHkv4zvwtVQTBJ0LNmErv1S+DqlVUudz8g==
|
||||
|
||||
gensync@^1.0.0-beta.2:
|
||||
version "1.0.0-beta.2"
|
||||
resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0"
|
||||
|
|
Loading…
Reference in a new issue