From 3bcbe058f2ca0ba58007ef90109c1daf81dbd3ce Mon Sep 17 00:00:00 2001 From: BearToCode Date: Mon, 13 Nov 2023 22:19:55 +0100 Subject: [PATCH 001/117] fix(plugin-attachment): fix icon size --- .../src/lib/icons/AttachIcon.svelte | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/plugin-attachment/src/lib/icons/AttachIcon.svelte b/packages/plugin-attachment/src/lib/icons/AttachIcon.svelte index 46dc53b..0467f13 100644 --- a/packages/plugin-attachment/src/lib/icons/AttachIcon.svelte +++ b/packages/plugin-attachment/src/lib/icons/AttachIcon.svelte @@ -1,11 +1,13 @@ From 25144519d7d6eb729e90d156fc21328e6fbcd351 Mon Sep 17 00:00:00 2001 From: BearToCode Date: Tue, 14 Nov 2023 00:14:04 +0100 Subject: [PATCH 002/117] fix(plugin-attachment): fix prop mismatch for custom loading overlay --- packages/plugin-attachment/src/lib/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/plugin-attachment/src/lib/index.ts b/packages/plugin-attachment/src/lib/index.ts index 580306c..b207339 100644 --- a/packages/plugin-attachment/src/lib/index.ts +++ b/packages/plugin-attachment/src/lib/index.ts @@ -127,7 +127,7 @@ export const attachment = (options: AttachmentExtensionOptions): CartaExtension component: LoadingOverlay, props: { uploadingFiles, - LoadingOverlay: options.loadingOverlay + loadingOverlay: options.loadingOverlay }, parent: 'input' } From 0d7442aff6b66b29e9ca2e47f1659d1da48c845b Mon Sep 17 00:00:00 2001 From: BearToCode Date: Tue, 14 Nov 2023 15:05:54 +0100 Subject: [PATCH 003/117] feat(core): add `bindToCaret` action --- packages/carta-md/src/lib/internal/carta.ts | 82 ++++++++++++++++----- packages/carta-md/src/lib/internal/input.ts | 82 +++++++++++++++++++-- packages/carta-md/src/lib/internal/utils.ts | 10 +++ 3 files changed, 148 insertions(+), 26 deletions(-) diff --git a/packages/carta-md/src/lib/internal/carta.ts b/packages/carta-md/src/lib/internal/carta.ts index 90e3a7b..bc3c3f1 100644 --- a/packages/carta-md/src/lib/internal/carta.ts +++ b/packages/carta-md/src/lib/internal/carta.ts @@ -1,34 +1,24 @@ -import { Marked, type MarkedExtension } from 'marked'; import type { CartaHistoryOptions } from './history'; +import type { SvelteComponentTyped } from 'svelte'; +import type { ShjLanguageDefinition } from '@speed-highlight/core/index'; +import { Marked, type MarkedExtension } from 'marked'; import { CartaInput } from './input'; import { - defaultKeyboardShortcuts, type DefaultShortcutId, - type KeyboardShortcut + type KeyboardShortcut, + defaultKeyboardShortcuts } from './shortcuts'; import { defaultIcons, type CartaIcon, type DefaultIconId } from './icons'; import { defaultPrefixes, type DefaultPrefixId, type Prefix } from './prefixes'; -import type { SvelteComponentTyped } from 'svelte'; import { CartaRenderer } from './renderer'; import { + type HighlightFunctions, loadCustomLanguage, highlight, highlightAutodetect, - type HighlightFunctions, loadCustomMarkdown } from './highlight.js'; -import type { ShjLanguageDefinition } from '@speed-highlight/core/index'; - -// Node does not implement CustomEvent until v19, so we -// "declare" it ourself for backward compatibility. -class CustomEvent extends Event { - detail: T; - constructor(message: string, data: EventInit & { detail: T }) { - super(message, data); - this.detail = data.detail; - } -} - +import { CustomEvent } from './utils'; /** * Carta-specific event with extra payload. */ @@ -183,6 +173,12 @@ export class Carta { return this._renderer; } + private elementsToBind: { + elem: HTMLElement; + portal: HTMLElement; + callback: (() => void) | undefined; + }[] = []; + public constructor(public readonly options?: CartaOptions) { this.keyboardShortcuts = []; this.icons = []; @@ -318,13 +314,23 @@ export class Carta { * @param callback Update callback. */ public $setInput(textarea: HTMLTextAreaElement, container: HTMLDivElement, callback: () => void) { + // Remove old listeners if any + this.input?.events.removeEventListener('update', callback); + this._input = new CartaInput(textarea, container, { shortcuts: this.keyboardShortcuts, prefixes: this.prefixes, listeners: this.textareaListeners, - callback: callback, historyOpts: this.options?.historyOptions }); + + this._input.events.addEventListener('update', callback); + + // Bind elements + this.elementsToBind.forEach((it) => { + it.callback = this.input?.$bindToCaret(it.elem, it.portal).destroy; + }); + this.elementsToBind = []; } /** @@ -334,4 +340,44 @@ export class Carta { public $setRenderer(container: HTMLDivElement) { this._renderer = new CartaRenderer(container); } + + /** + * Bind an element to the caret position. + * @param element The element to bind. + * @param portal The portal element. + * @returns The unbind function. + * + * @example + * ```svelte + * + * + *
+ * + *
+ * + * ``` + */ + public bindToCaret( + element: HTMLElement, + portal = document.querySelector('body') as HTMLBodyElement + ) { + if (this.input) { + return this.input.$bindToCaret(element, portal); + } else { + let callback: (() => void) | undefined; + + // Bind the element later, when the input is ready + this.elementsToBind.push({ elem: element, portal, callback }); + + return { + destroy() { + if (callback) { + callback(); + } + } + }; + } + } } diff --git a/packages/carta-md/src/lib/internal/input.ts b/packages/carta-md/src/lib/internal/input.ts index de3ea9c..39ba4e3 100644 --- a/packages/carta-md/src/lib/internal/input.ts +++ b/packages/carta-md/src/lib/internal/input.ts @@ -1,7 +1,7 @@ import type { CartaListener } from './carta'; -import { CartaHistory, type CartaHistoryOptions } from './history'; import type { Prefix } from './prefixes'; import type { KeyboardShortcut } from './shortcuts'; +import { CartaHistory, type CartaHistoryOptions } from './history'; import { areEqualSets } from './utils'; /** @@ -22,16 +22,17 @@ export interface InputSettings { readonly prefixes: Prefix[]; // eslint-disable-next-line @typescript-eslint/no-explicit-any readonly listeners: CartaListener[]; - readonly callback: () => void; readonly historyOpts?: Partial; } export class CartaInput { private pressedKeys: Set; - public readonly history: CartaHistory; // Used to detect keys that actually changed the textarea value private onKeyDownValue: string | undefined; + public readonly history: CartaHistory; + public readonly events = new EventTarget(); + constructor( public readonly textarea: HTMLTextAreaElement, public readonly container: HTMLDivElement, @@ -113,7 +114,7 @@ export class CartaInput { // Save state for shortcuts if (!shortcut.preventSave) this.history.saveState(this.textarea.value, this.textarea.selectionStart); - this.settings.callback(); + this.update(); } this.onKeyDownValue = undefined; @@ -166,14 +167,14 @@ export class CartaInput { const line = this.getLine(lineStartingIndex); this.removeAt(lineStartingIndex, line.value.length); this.textarea.setSelectionRange(line.start, line.start); - this.settings.callback(); + this.update(); return; } const newPrefix = prefix.maker(match, line); this.insertAt(cursor, '\n' + newPrefix); - this.settings.callback(); + this.update(); // Update cursor position const newCursorPosition = cursor + newPrefix.length + 1; this.textarea.setSelectionRange(newCursorPosition, newCursorPosition); @@ -317,7 +318,7 @@ export class CartaInput { /** * Update the textarea. */ - public update = () => this.settings.callback(); + public update = () => this.events.dispatchEvent(new Event('update')); /** * Returns x, y coordinates for absolute positioning of a span within a given text input @@ -364,7 +365,7 @@ export class CartaInput { /** * Moves an element next to the caret. Shall be called every time the element - * changes width, height or the caret position changes. + * changes width, height or the caret position changes. Consider using `bindToCaret` instead. * * @example * ```svelte @@ -437,6 +438,71 @@ export class CartaInput { elem.style.bottom = bottom !== undefined ? bottom + 'px' : 'unset'; } + /** + * **Internal**: Svelte action to bind an element to the caret position. + * Use `bindToCaret` from the `carta` instance instead. + * @param elem The element to position. + * @param portal The portal to append the element to. Defaults to `document.body`. + */ + public $bindToCaret(elem: HTMLElement, portal: HTMLElement) { + // Move the element to body + portal.appendChild(elem); + elem.style.position = 'absolute'; + + const callback = () => { + const relativePosition = this.getCursorXY(); + const absolutePosition = { + x: relativePosition.x + this.textarea.getBoundingClientRect().left, + y: relativePosition.y + this.textarea.getBoundingClientRect().top + }; + + const fontSize = this.getRowHeight(); + const width = elem.clientWidth; + const height = elem.clientHeight; + + // Left/Right + let left: number | undefined = absolutePosition.x; + let right: number | undefined; + + if (left + width >= window.innerWidth) { + right = window.innerWidth - left; + left = undefined; + } + + // Top/Bottom + let top: number | undefined = absolutePosition.y; + let bottom: number | undefined; + + if (top + height >= window.innerHeight) { + bottom = window.innerHeight - top; + top = undefined; + } + + elem.style.left = left !== undefined ? left + 'px' : 'unset'; + elem.style.right = right !== undefined ? right + 'px' : 'unset'; + elem.style.top = top !== undefined ? top + fontSize + 'px' : 'unset'; + elem.style.bottom = bottom !== undefined ? bottom + 'px' : 'unset'; + }; + + this.textarea.addEventListener('input', callback); + window.addEventListener('resize', callback); + + // Initial positioning + callback(); + + return { + destroy: () => { + try { + portal.removeChild(elem); + } catch (e: unknown) { + // Ignore + } + this.textarea.removeEventListener('input', callback); + window.removeEventListener('resize', callback); + } + }; + } + /** * Get rough value for a row of the textarea. */ diff --git a/packages/carta-md/src/lib/internal/utils.ts b/packages/carta-md/src/lib/internal/utils.ts index 538d31d..3d1ce8b 100644 --- a/packages/carta-md/src/lib/internal/utils.ts +++ b/packages/carta-md/src/lib/internal/utils.ts @@ -53,3 +53,13 @@ export function mergeDefaultInterface( }); return final; } + +// Node does not implement CustomEvent until v19, so we +// "declare" it ourself for backward compatibility. +export class CustomEvent extends Event { + detail: T; + constructor(message: string, data: EventInit & { detail: T }) { + super(message, data); + this.detail = data.detail; + } +} From 1db7fc42b7ea973f019f7ba5693dfe9072ecaad3 Mon Sep 17 00:00:00 2001 From: BearToCode Date: Tue, 14 Nov 2023 18:31:24 +0100 Subject: [PATCH 004/117] fix: tsconfig TS6310 may not disable emit --- tsconfig.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tsconfig.json b/tsconfig.json index d944659..d8ac207 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -15,5 +15,6 @@ "outDir": "./dist", "module": "ESNext", "target": "ESNext" - } + }, + "files": [] } From 20bc2819c91bb8da604ea6551cece6d5c7935355 Mon Sep 17 00:00:00 2001 From: BearToCode Date: Tue, 14 Nov 2023 18:37:19 +0100 Subject: [PATCH 005/117] fix: remove typescript project references --- packages/plugin-code/tsconfig.json | 3 +-- packages/plugin-math/tsconfig.json | 3 +-- packages/plugin-tikz/tsconfig.json | 3 +-- packages/tsconfig.json | 11 ----------- 4 files changed, 3 insertions(+), 17 deletions(-) delete mode 100644 packages/tsconfig.json diff --git a/packages/plugin-code/tsconfig.json b/packages/plugin-code/tsconfig.json index 0d0b887..ae53f4c 100644 --- a/packages/plugin-code/tsconfig.json +++ b/packages/plugin-code/tsconfig.json @@ -5,6 +5,5 @@ "rootDir": "src", "typeRoots": ["./node_modules/@types"] }, - "include": ["./src"], - "references": [{ "path": "../carta-md" }] + "include": ["./src"] } diff --git a/packages/plugin-math/tsconfig.json b/packages/plugin-math/tsconfig.json index 6b69ac1..6b8bc7f 100644 --- a/packages/plugin-math/tsconfig.json +++ b/packages/plugin-math/tsconfig.json @@ -5,6 +5,5 @@ "rootDir": "src", "typeRoots": ["./node_modules/@types"] }, - "include": ["./src"], - "references": [{ "path": "../carta-md" }] + "include": ["./src"] } diff --git a/packages/plugin-tikz/tsconfig.json b/packages/plugin-tikz/tsconfig.json index 6b69ac1..6b8bc7f 100644 --- a/packages/plugin-tikz/tsconfig.json +++ b/packages/plugin-tikz/tsconfig.json @@ -5,6 +5,5 @@ "rootDir": "src", "typeRoots": ["./node_modules/@types"] }, - "include": ["./src"], - "references": [{ "path": "../carta-md" }] + "include": ["./src"] } diff --git a/packages/tsconfig.json b/packages/tsconfig.json deleted file mode 100644 index dc1da2c..0000000 --- a/packages/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "references": [ - { "path": "carta-md" }, - { "path": "plugin-math" }, - { "path": "plugin-emoji" }, - { "path": "plugin-slash" }, - { "path": "plugin-code" }, - { "path": "plugin-tikz" }, - { "path": "plugin-attachment" } - ] -} From e5e4ad4f06859cd03c081d91ecb26fa260b055fd Mon Sep 17 00:00:00 2001 From: BearToCode Date: Tue, 14 Nov 2023 18:44:18 +0100 Subject: [PATCH 006/117] feat: better caret-bounded components positioning --- packages/plugin-emoji/package.json | 2 +- packages/plugin-emoji/src/lib/Emoji.svelte | 11 +---------- packages/plugin-slash/package.json | 2 +- packages/plugin-slash/src/lib/Slash.svelte | 16 ++-------------- 4 files changed, 5 insertions(+), 26 deletions(-) diff --git a/packages/plugin-emoji/package.json b/packages/plugin-emoji/package.json index ca11132..35f5779 100644 --- a/packages/plugin-emoji/package.json +++ b/packages/plugin-emoji/package.json @@ -36,7 +36,7 @@ "node-emoji": "^1.11.0" }, "peerDependencies": { - "carta-md": "^3.0.0", + "carta-md": "^3.1.0", "marked": "^9.1.5", "svelte": "^3.54.0 || ^4.0.0" }, diff --git a/packages/plugin-emoji/src/lib/Emoji.svelte b/packages/plugin-emoji/src/lib/Emoji.svelte index 7ff9a23..0f910b1 100644 --- a/packages/plugin-emoji/src/lib/Emoji.svelte +++ b/packages/plugin-emoji/src/lib/Emoji.svelte @@ -12,7 +12,6 @@ export let outTransition: (node: Element) => TransitionConfig; let visible = false; - let caretPosition = { left: 0, right: 0, top: 0, bottom: 0 }; let filter = ''; let colonPosition = 0; let hoveringIndex = 0; @@ -75,7 +74,6 @@ } else if (e.key === ':') { // Open visible = true; - caretPosition = carta.input.getCursorXY(); colonPosition = carta.input.textarea.selectionStart; filter = ''; } @@ -107,14 +105,6 @@ carta.input.update(); } - $: { - if (elem) { - // Make statement reactive - caretPosition, elem.clientWidth, elem.clientHeight; - carta.input?.moveElemToCaret(elem); - } - } - $: { // Scroll to make hovering emoji always visible const hovering = emojisElements.at(hoveringIndex); @@ -135,6 +125,7 @@ bind:this={elem} in:inTransition out:outTransition + use:carta.bindToCaret > {#each emojis as emoji, i} + + + + +
+
+
+
+