mirror of
https://github.com/ankitects/anki.git
synced 2025-09-24 16:56:36 -04:00
Merge branch 'main' into collapsible-patch
This commit is contained in:
commit
11423b290f
39 changed files with 829 additions and 305 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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -678,7 +678,7 @@ class AnkiQt(QMainWindow):
|
|||
self.bottomWeb.show()
|
||||
gui_hooks.state_did_change(state, oldState)
|
||||
|
||||
def _deckBrowserState(self, oldState: str) -> None:
|
||||
def _deckBrowserState(self, oldState: MainWindowState) -> None:
|
||||
self.deckBrowser.show()
|
||||
|
||||
def _selectedDeck(self) -> DeckDict | None:
|
||||
|
@ -688,15 +688,15 @@ class AnkiQt(QMainWindow):
|
|||
return None
|
||||
return self.col.decks.get(did)
|
||||
|
||||
def _overviewState(self, oldState: str) -> None:
|
||||
def _overviewState(self, oldState: MainWindowState) -> None:
|
||||
if not self._selectedDeck():
|
||||
return self.moveToState("deckBrowser")
|
||||
self.overview.show()
|
||||
|
||||
def _reviewState(self, oldState: str) -> None:
|
||||
def _reviewState(self, oldState: MainWindowState) -> None:
|
||||
self.reviewer.show()
|
||||
|
||||
def _reviewCleanup(self, newState: str) -> None:
|
||||
def _reviewCleanup(self, newState: MainWindowState) -> None:
|
||||
if newState != "resetRequired" and newState != "review":
|
||||
self.reviewer.cleanup()
|
||||
|
||||
|
|
|
@ -531,18 +531,27 @@ hooks = [
|
|||
# these refer to things like deckbrowser, overview and reviewer state,
|
||||
Hook(
|
||||
name="state_will_change",
|
||||
args=["new_state: str", "old_state: str"],
|
||||
args=[
|
||||
"new_state: aqt.main.MainWindowState",
|
||||
"old_state: aqt.main.MainWindowState",
|
||||
],
|
||||
legacy_hook="beforeStateChange",
|
||||
),
|
||||
Hook(
|
||||
name="state_did_change",
|
||||
args=["new_state: str", "old_state: str"],
|
||||
args=[
|
||||
"new_state: aqt.main.MainWindowState",
|
||||
"old_state: aqt.main.MainWindowState",
|
||||
],
|
||||
legacy_hook="afterStateChange",
|
||||
),
|
||||
# different sig to original
|
||||
Hook(
|
||||
name="state_shortcuts_will_change",
|
||||
args=["state: str", "shortcuts: list[tuple[str, Callable]]"],
|
||||
args=[
|
||||
"state: aqt.main.MainWindowState",
|
||||
"shortcuts: list[tuple[str, Callable]]",
|
||||
],
|
||||
),
|
||||
# UI state/refreshing
|
||||
###################
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
@ -23,7 +27,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
export let portalTarget: HTMLElement | null = null;
|
||||
|
||||
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(
|
||||
|
|
|
@ -111,6 +111,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<style lang="scss">
|
||||
.editor-field {
|
||||
position: relative;
|
||||
padding: 0 3px;
|
||||
--border-color: var(--border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -43,5 +43,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
/* Stay a on single line */
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
/* The field description is placed absolutely on top of the editor field */
|
||||
/* So we need to make sure it does not escape the editor field if the */
|
||||
/* description is too long */
|
||||
overflow: hidden;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -2,6 +2,10 @@
|
|||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<!--
|
||||
@component
|
||||
Contains the fields. This contains the scrollable area.
|
||||
-->
|
||||
<div class="fields">
|
||||
<slot />
|
||||
</div>
|
||||
|
@ -12,6 +16,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
grid-auto-rows: min-content;
|
||||
grid-gap: 6px;
|
||||
|
||||
padding: 0 3px 5px;
|
||||
/* Add space after the last field and the start of the tag editor */
|
||||
padding-bottom: 5px;
|
||||
|
||||
/* Move the scrollbar for the NoteEditor into this element */
|
||||
overflow-y: auto;
|
||||
|
||||
/* Push the tag editor to the bottom of the note editor */
|
||||
flex-grow: 1;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,22 +0,0 @@
|
|||
<!--
|
||||
Copyright: Ankitects Pty Ltd and contributors
|
||||
License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
||||
-->
|
||||
<div class="fields-editor">
|
||||
<slot />
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.fields-editor {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
|
||||
/* replace with "gap: 5px" once it's available
|
||||
- required: Chromium 84 (Qt6 only) and iOS 14.1 */
|
||||
> :global(*) {
|
||||
margin: 5px 0;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -37,16 +37,13 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
<style lang="scss">
|
||||
.label-container {
|
||||
display: flex;
|
||||
position: sticky;
|
||||
justify-content: space-between;
|
||||
top: 0;
|
||||
padding-bottom: 1px;
|
||||
padding: 0 3px 1px;
|
||||
|
||||
/* slightly wider than EditingArea
|
||||
to cover field borders on scroll */
|
||||
left: -1px;
|
||||
right: -1px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
|
||||
background: var(--label-color);
|
||||
|
||||
.clickable {
|
||||
|
|
|
@ -44,7 +44,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
|
||||
import Absolute from "../components/Absolute.svelte";
|
||||
import Badge from "../components/Badge.svelte";
|
||||
import StickyContainer from "../components/StickyContainer.svelte";
|
||||
import { bridgeCommand } from "../lib/bridgecommand";
|
||||
import { TagEditor } from "../tag-editor";
|
||||
import { ChangeTimer } from "./change-timer";
|
||||
|
@ -56,7 +55,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import EditorField from "./EditorField.svelte";
|
||||
import FieldDescription from "./FieldDescription.svelte";
|
||||
import Fields from "./Fields.svelte";
|
||||
import FieldsEditor from "./FieldsEditor.svelte";
|
||||
import FrameElement from "./FrameElement.svelte";
|
||||
import { alertIcon } from "./icons";
|
||||
import ImageHandle from "./image-overlay";
|
||||
|
@ -67,6 +65,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 +174,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 +282,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
setNoteId,
|
||||
wrap,
|
||||
setMathjaxEnabled,
|
||||
setInsertSymbolsEnabled,
|
||||
...oldEditorAdapter,
|
||||
});
|
||||
|
||||
|
@ -313,182 +319,187 @@ Functionality exclusive to specifc note-editing views (e.g. in the browser or
|
|||
the AddCards dialog) should be implemented in the user of this component.
|
||||
-->
|
||||
<div class="note-editor">
|
||||
<FieldsEditor>
|
||||
<EditorToolbar {size} {wrap} api={toolbar}>
|
||||
<slot slot="notetypeButtons" name="notetypeButtons" />
|
||||
</EditorToolbar>
|
||||
<EditorToolbar {size} {wrap} api={toolbar}>
|
||||
<slot slot="notetypeButtons" name="notetypeButtons" />
|
||||
</EditorToolbar>
|
||||
|
||||
{#if hint}
|
||||
<Absolute bottom right --margin="10px">
|
||||
<Notification>
|
||||
<Badge --badge-color="tomato" --icon-align="top"
|
||||
>{@html alertIcon}</Badge
|
||||
>
|
||||
<span>{@html hint}</span>
|
||||
</Notification>
|
||||
</Absolute>
|
||||
{/if}
|
||||
{#if hint}
|
||||
<Absolute bottom right --margin="10px">
|
||||
<Notification>
|
||||
<Badge --badge-color="tomato" --icon-align="top"
|
||||
>{@html alertIcon}</Badge
|
||||
>
|
||||
<span>{@html hint}</span>
|
||||
</Notification>
|
||||
</Absolute>
|
||||
{/if}
|
||||
|
||||
<Fields>
|
||||
<DecoratedElements>
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
<Fields>
|
||||
<DecoratedElements>
|
||||
{#each fieldsData as field, index}
|
||||
{@const content = fieldStores[index]}
|
||||
|
||||
<EditorField
|
||||
{field}
|
||||
{content}
|
||||
flipInputs={plainTextDefaults[index]}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
bridgeCommand(`focus:${index}`);
|
||||
}}
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
bridgeCommand(
|
||||
`blur:${index}:${getNoteId()}:${get(content)}`,
|
||||
);
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
$hoveredField = fields[index];
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
$hoveredField = null;
|
||||
}}
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
--label-color={cols[index] === "dupe"
|
||||
? "var(--flag1-bg)"
|
||||
: "var(--window-bg)"}
|
||||
>
|
||||
<svelte:fragment slot="field-label">
|
||||
<LabelContainer
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
on:toggle={async () => {
|
||||
fieldsCollapsed[index] = !fieldsCollapsed[index];
|
||||
<EditorField
|
||||
{field}
|
||||
{content}
|
||||
flipInputs={plainTextDefaults[index]}
|
||||
api={fields[index]}
|
||||
on:focusin={() => {
|
||||
$focusedField = fields[index];
|
||||
bridgeCommand(`focus:${index}`);
|
||||
}}
|
||||
on:focusout={() => {
|
||||
$focusedField = null;
|
||||
bridgeCommand(`blur:${index}:${getNoteId()}:${get(content)}`);
|
||||
}}
|
||||
on:mouseenter={() => {
|
||||
$hoveredField = fields[index];
|
||||
}}
|
||||
on:mouseleave={() => {
|
||||
$hoveredField = null;
|
||||
}}
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
--label-color={cols[index] === "dupe"
|
||||
? "var(--flag1-bg)"
|
||||
: "var(--window-bg)"}
|
||||
>
|
||||
<svelte:fragment slot="field-label">
|
||||
<LabelContainer
|
||||
collapsed={fieldsCollapsed[index]}
|
||||
on:toggle={async () => {
|
||||
fieldsCollapsed[index] = !fieldsCollapsed[index];
|
||||
|
||||
const defaultInput = !plainTextDefaults[index]
|
||||
? richTextInputs[index]
|
||||
: plainTextInputs[index];
|
||||
const defaultInput = !plainTextDefaults[index]
|
||||
? richTextInputs[index]
|
||||
: plainTextInputs[index];
|
||||
|
||||
if (!fieldsCollapsed[index]) {
|
||||
refocusInput(defaultInput.api);
|
||||
} else if (!plainTextDefaults[index]) {
|
||||
plainTextsHidden[index] = true;
|
||||
} else {
|
||||
richTextsHidden[index] = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="field-name">
|
||||
<LabelName>
|
||||
{field.name}
|
||||
</LabelName>
|
||||
</svelte:fragment>
|
||||
<FieldState>
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
{#if plainTextDefaults[index]}
|
||||
<RichTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={richTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
richTextsHidden[index] =
|
||||
!richTextsHidden[index];
|
||||
if (!fieldsCollapsed[index]) {
|
||||
refocusInput(defaultInput.api);
|
||||
} else if (!plainTextDefaults[index]) {
|
||||
plainTextsHidden[index] = true;
|
||||
} else {
|
||||
richTextsHidden[index] = true;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<svelte:fragment slot="field-name">
|
||||
<LabelName>
|
||||
{field.name}
|
||||
</LabelName>
|
||||
</svelte:fragment>
|
||||
<FieldState>
|
||||
{#if cols[index] === "dupe"}
|
||||
<DuplicateLink />
|
||||
{/if}
|
||||
{#if plainTextDefaults[index]}
|
||||
<RichTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={richTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
richTextsHidden[index] =
|
||||
!richTextsHidden[index];
|
||||
|
||||
if (!richTextsHidden[index]) {
|
||||
refocusInput(
|
||||
richTextInputs[index].api,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{:else}
|
||||
<PlainTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={plainTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
plainTextsHidden[index] =
|
||||
!plainTextsHidden[index];
|
||||
|
||||
if (!plainTextsHidden[index]) {
|
||||
refocusInput(
|
||||
plainTextInputs[index].api,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<slot
|
||||
name="field-state"
|
||||
{field}
|
||||
{index}
|
||||
visible={fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField}
|
||||
if (!richTextsHidden[index]) {
|
||||
refocusInput(richTextInputs[index].api);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</FieldState>
|
||||
</LabelContainer>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="rich-text-input">
|
||||
<Collapsible
|
||||
collapse={richTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<RichTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={richTextInputs[index]}
|
||||
>
|
||||
<ImageHandle maxWidth={250} maxHeight={125} />
|
||||
<MathjaxHandle />
|
||||
<FieldDescription>
|
||||
{field.description}
|
||||
</FieldDescription>
|
||||
</RichTextInput>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="plain-text-input">
|
||||
<Collapsible
|
||||
collapse={plainTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<PlainTextInput
|
||||
{hidden}
|
||||
isDefault={plainTextDefaults[index]}
|
||||
richTextHidden={richTextsHidden[index]}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={plainTextInputs[index]}
|
||||
{:else}
|
||||
<PlainTextBadge
|
||||
visible={!fieldsCollapsed[index] &&
|
||||
(fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField)}
|
||||
bind:off={plainTextsHidden[index]}
|
||||
on:toggle={async () => {
|
||||
plainTextsHidden[index] =
|
||||
!plainTextsHidden[index];
|
||||
|
||||
if (!plainTextsHidden[index]) {
|
||||
refocusInput(
|
||||
plainTextInputs[index].api,
|
||||
);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
{/if}
|
||||
<slot
|
||||
name="field-state"
|
||||
{field}
|
||||
{index}
|
||||
visible={fields[index] === $hoveredField ||
|
||||
fields[index] === $focusedField}
|
||||
/>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
</EditorField>
|
||||
{/each}
|
||||
</FieldState>
|
||||
</LabelContainer>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="rich-text-input">
|
||||
<Collapsible
|
||||
collapse={richTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<RichTextInput
|
||||
{hidden}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={richTextInputs[index]}
|
||||
>
|
||||
<ImageHandle maxWidth={250} maxHeight={125} />
|
||||
<MathjaxHandle />
|
||||
{#if insertSymbols}
|
||||
<SymbolsOverlay />
|
||||
{/if}
|
||||
<FieldDescription>
|
||||
{field.description}
|
||||
</FieldDescription>
|
||||
</RichTextInput>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
<svelte:fragment slot="plain-text-input">
|
||||
<Collapsible
|
||||
collapse={plainTextsHidden[index]}
|
||||
let:collapsed={hidden}
|
||||
>
|
||||
<PlainTextInput
|
||||
{hidden}
|
||||
isDefault={plainTextDefaults[index]}
|
||||
richTextHidden={richTextsHidden[index]}
|
||||
on:focusout={() => {
|
||||
saveFieldNow();
|
||||
$focusedInput = null;
|
||||
}}
|
||||
bind:this={plainTextInputs[index]}
|
||||
/>
|
||||
</Collapsible>
|
||||
</svelte:fragment>
|
||||
</EditorField>
|
||||
{/each}
|
||||
|
||||
<MathjaxElement />
|
||||
<FrameElement />
|
||||
</DecoratedElements>
|
||||
</Fields>
|
||||
</FieldsEditor>
|
||||
<MathjaxElement />
|
||||
<FrameElement />
|
||||
</DecoratedElements>
|
||||
</Fields>
|
||||
|
||||
<StickyContainer --gutter-block="0.1rem" --sticky-borders="1px 0 0" class="d-flex">
|
||||
<div class="note-editor-tag-editor">
|
||||
<TagEditor {tags} on:tagsupdate={saveTags} />
|
||||
</StickyContainer>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.note-editor {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.note-editor-tag-editor {
|
||||
padding: 2px 0 0;
|
||||
|
||||
border-width: thin 0 0;
|
||||
border-style: solid;
|
||||
border-color: var(--medium-border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -53,7 +53,6 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
import ButtonToolbar from "../../components/ButtonToolbar.svelte";
|
||||
import DynamicallySlottable from "../../components/DynamicallySlottable.svelte";
|
||||
import Item from "../../components/Item.svelte";
|
||||
import StickyContainer from "../../components/StickyContainer.svelte";
|
||||
import BlockButtons from "./BlockButtons.svelte";
|
||||
import InlineButtons from "./InlineButtons.svelte";
|
||||
import NotetypeButtons from "./NotetypeButtons.svelte";
|
||||
|
@ -85,7 +84,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
setContextProperty(api);
|
||||
</script>
|
||||
|
||||
<StickyContainer --gutter-block="0.1rem" --sticky-borders="0 0 1px">
|
||||
<div class="editor-toolbar">
|
||||
<ButtonToolbar {size} {wrap}>
|
||||
<DynamicallySlottable slotHost={Item} api={toolbar}>
|
||||
<Item id="notetype">
|
||||
|
@ -111,4 +110,14 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</Item>
|
||||
</DynamicallySlottable>
|
||||
</ButtonToolbar>
|
||||
</StickyContainer>
|
||||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.editor-toolbar {
|
||||
padding: 0 0 2px;
|
||||
|
||||
border-width: 0 0 thin;
|
||||
border-style: solid;
|
||||
border-color: var(--medium-border);
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
|
|
|
@ -7,11 +7,11 @@
|
|||
*/
|
||||
function portal(
|
||||
element: HTMLElement,
|
||||
targetElement: Element = document.body,
|
||||
targetElement: Element | null = document.body,
|
||||
): { update(target: Element): void; destroy(): void } {
|
||||
let target: Element;
|
||||
let target: Element | null;
|
||||
|
||||
async function update(newTarget: Element) {
|
||||
async function update(newTarget: Element | null) {
|
||||
target = newTarget;
|
||||
|
||||
if (!target) {
|
||||
|
|
|
@ -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;
|
||||
|
@ -381,7 +382,7 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
$: anyTagsSelected = tagTypes.some((tag) => tag.selected);
|
||||
</script>
|
||||
|
||||
<div class="tag-editor-area" on:focusout={deselectIfLeave} bind:offsetHeight={height}>
|
||||
<div class="tag-editor" on:focusout={deselectIfLeave} bind:offsetHeight={height}>
|
||||
<TagOptionsButton
|
||||
bind:badgeHeight
|
||||
tagsSelected={anyTagsSelected}
|
||||
|
@ -482,13 +483,10 @@ License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html
|
|||
</div>
|
||||
|
||||
<style lang="scss">
|
||||
.tag-editor-area {
|
||||
.tag-editor {
|
||||
display: flex;
|
||||
flex-flow: row wrap;
|
||||
align-items: flex-end;
|
||||
padding: 0 1px 1px;
|
||||
overflow: hidden;
|
||||
margin-bottom: 3px;
|
||||
}
|
||||
|
||||
.tag-relative {
|
||||
|
|
|
@ -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