@threadlabs/looma 0.28.1 → 0.28.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/editor/session.d.ts +39 -0
- package/package.json +1 -1
- package/vue/editor/compact/index.js +2 -2
- package/vue/editor/index.js +149 -107
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Editor, type EditorOptions, type JSONContent } from "@tiptap/core";
|
|
2
|
+
import { type Selection, type SelectionBookmark } from "@tiptap/pm/state";
|
|
3
|
+
/** A selection remembered so a controlled document replacement can put it back. */
|
|
4
|
+
export interface LoomaSelectionSnapshot {
|
|
5
|
+
bookmark: SelectionBookmark;
|
|
6
|
+
from: number;
|
|
7
|
+
to: number;
|
|
8
|
+
text: boolean;
|
|
9
|
+
}
|
|
10
|
+
export declare function selectionSnapshot(selection: Selection): LoomaSelectionSnapshot;
|
|
11
|
+
/**
|
|
12
|
+
* One editor's Tiptap instance, independent of any component framework.
|
|
13
|
+
*
|
|
14
|
+
* @ownership The session owns the `Editor` from creation until `destroy()`. A host renders an
|
|
15
|
+
* element for the document and hands it to `attach()`; it never creates or destroys the editor.
|
|
16
|
+
* @lifecycle The editor, its document, and its undo history survive `attach()` to a new element,
|
|
17
|
+
* so a host can remount its view without losing state.
|
|
18
|
+
*/
|
|
19
|
+
export interface LoomaEditorSession<E extends Editor = Editor> {
|
|
20
|
+
readonly editor: E;
|
|
21
|
+
/** Moves the editor's view into `element` (idempotent). */
|
|
22
|
+
attach(element: HTMLElement): void;
|
|
23
|
+
/**
|
|
24
|
+
* Applies a controlled document.
|
|
25
|
+
*
|
|
26
|
+
* @contract Returns false, and leaves the editor untouched, when the document already matches.
|
|
27
|
+
* A focused editor keeps `selection` (or its current selection) in the same transaction.
|
|
28
|
+
* @failure A selection the new document cannot hold becomes the closest valid text range.
|
|
29
|
+
*/
|
|
30
|
+
setContent(content: JSONContent, selection?: LoomaSelectionSnapshot | null): boolean;
|
|
31
|
+
/** Calls `listener` after every transaction; returns the unsubscribe function. */
|
|
32
|
+
subscribe(listener: () => void): () => void;
|
|
33
|
+
destroy(): void;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Creates the session's editor from `options`. A framework layer may pass its own `Editor`
|
|
37
|
+
* subclass, such as `@tiptap/vue-3`'s reactive one, so the editor it exposes keeps that behavior.
|
|
38
|
+
*/
|
|
39
|
+
export declare function createEditorSession<E extends Editor = Editor>(options: Partial<EditorOptions>, EditorClass?: new (options: Partial<EditorOptions>) => E): LoomaEditorSession<E>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { defineComponent, h, onBeforeUnmount, onMounted, shallowRef, watch } from "vue";
|
|
2
2
|
import { Extension, Mark, Node, markInputRule, markPasteRule, mergeAttributes } from "@tiptap/core";
|
|
3
|
-
import { Editor, EditorContent } from "@tiptap/vue-3";
|
|
3
|
+
import { Editor as Editor$1, EditorContent } from "@tiptap/vue-3";
|
|
4
4
|
import { closeHistory, history, redo, undo } from "@tiptap/pm/history";
|
|
5
5
|
import { Plugin } from "@tiptap/pm/state";
|
|
6
6
|
//#region ../../node_modules/.pnpm/@tiptap+extension-document@2.27.2_@tiptap+core@2.27.2_@tiptap+pm@2.27.2_/node_modules/@tiptap/extension-document/dist/index.js
|
|
@@ -498,7 +498,7 @@ var LoomaCompactEditor = defineComponent({
|
|
|
498
498
|
setup(props, { attrs, emit, expose }) {
|
|
499
499
|
const editor = shallowRef();
|
|
500
500
|
onMounted(() => {
|
|
501
|
-
editor.value = new Editor({
|
|
501
|
+
editor.value = new Editor$1({
|
|
502
502
|
extensions: [...getCompactEditorExtensions(), ...props.extensions],
|
|
503
503
|
content: props.modelValue,
|
|
504
504
|
editable: props.editable,
|
package/vue/editor/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
|
|
2
2
|
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig, resolveTableCellAt, siteRelativeHref } from "@threadlabs/looma/editor";
|
|
3
3
|
import { Button, Callout, Card, Checkbox, Cluster, Combobox, Disclosure, EditorInsertTableGrid, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, EditorToolbar as EditorToolbar$1, FormField, Icon, IconButton, Image, Input, InputGroup, List, Menu, MenuItem, Popover, ScrollArea, SearchResultRow, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
|
|
4
|
-
import { Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
|
|
5
|
-
import { BubbleMenu,
|
|
4
|
+
import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
|
|
5
|
+
import { BubbleMenu, Editor as Editor$1 } from "@tiptap/vue-3";
|
|
6
6
|
import { closeHistory } from "@tiptap/pm/history";
|
|
7
7
|
import { AllSelection, NodeSelection, Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
8
8
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
@@ -737,6 +737,63 @@ function selectedTableCellElement(editor) {
|
|
|
737
737
|
return selectionElement(editor)?.closest("td, th");
|
|
738
738
|
}
|
|
739
739
|
//#endregion
|
|
740
|
+
//#region src/editor/session.ts
|
|
741
|
+
function selectionSnapshot(selection) {
|
|
742
|
+
return {
|
|
743
|
+
bookmark: selection.getBookmark(),
|
|
744
|
+
from: selection.from,
|
|
745
|
+
to: selection.to,
|
|
746
|
+
text: selection instanceof TextSelection
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
/**
|
|
750
|
+
* Creates the session's editor from `options`. A framework layer may pass its own `Editor`
|
|
751
|
+
* subclass, such as `@tiptap/vue-3`'s reactive one, so the editor it exposes keeps that behavior.
|
|
752
|
+
*/
|
|
753
|
+
function createEditorSession(options, EditorClass = Editor) {
|
|
754
|
+
const editor = new EditorClass(options);
|
|
755
|
+
return {
|
|
756
|
+
editor,
|
|
757
|
+
attach(element) {
|
|
758
|
+
const current = editor.options.element;
|
|
759
|
+
if (editor.isDestroyed || current === element || !current.firstChild) return;
|
|
760
|
+
element.append(...current.childNodes);
|
|
761
|
+
editor.setOptions({ element });
|
|
762
|
+
editor.createNodeViews();
|
|
763
|
+
},
|
|
764
|
+
setContent(content, selection) {
|
|
765
|
+
if (editor.isDestroyed || sameDocument(editor.getJSON(), content)) return false;
|
|
766
|
+
const previous = selection ?? selectionSnapshot(editor.view.state.selection);
|
|
767
|
+
if (!editor.isFocused) {
|
|
768
|
+
editor.commands.setContent(content, false);
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
editor.chain().setContent(content, false).command(({ tr }) => {
|
|
772
|
+
try {
|
|
773
|
+
tr.setSelection(previous.text ? TextSelection.create(tr.doc, previous.from, previous.to) : previous.bookmark.resolve(tr.doc));
|
|
774
|
+
} catch {
|
|
775
|
+
const max = tr.doc.content.size;
|
|
776
|
+
const from = Math.max(1, Math.min(previous.from, max));
|
|
777
|
+
const to = Math.max(from, Math.min(previous.to, max));
|
|
778
|
+
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
|
779
|
+
}
|
|
780
|
+
return true;
|
|
781
|
+
}).run();
|
|
782
|
+
editor.view.focus();
|
|
783
|
+
return true;
|
|
784
|
+
},
|
|
785
|
+
subscribe(listener) {
|
|
786
|
+
editor.on("transaction", listener);
|
|
787
|
+
return () => {
|
|
788
|
+
editor.off("transaction", listener);
|
|
789
|
+
};
|
|
790
|
+
},
|
|
791
|
+
destroy() {
|
|
792
|
+
editor.destroy();
|
|
793
|
+
}
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
//#endregion
|
|
740
797
|
//#region src/editor/shortcuts.ts
|
|
741
798
|
/**
|
|
742
799
|
* Shows a key binding in ProseMirror notation ("Mod-Shift-8") the way the reader's platform writes
|
|
@@ -2206,13 +2263,7 @@ var LoomaEditor = defineComponent({
|
|
|
2206
2263
|
let linkPressedSelection = null;
|
|
2207
2264
|
const rememberSelection = (instance) => {
|
|
2208
2265
|
if (!instance.isFocused) return;
|
|
2209
|
-
|
|
2210
|
-
lastFocusedSelection = {
|
|
2211
|
-
bookmark: selection.getBookmark(),
|
|
2212
|
-
from: selection.from,
|
|
2213
|
-
to: selection.to,
|
|
2214
|
-
text: selection instanceof TextSelection
|
|
2215
|
-
};
|
|
2266
|
+
lastFocusedSelection = selectionSnapshot(instance.view.state.selection);
|
|
2216
2267
|
};
|
|
2217
2268
|
const authorHighlight = !props.disableHighlight;
|
|
2218
2269
|
const codeHighlighting = createCodeHighlighting(props.codeLanguages, () => {
|
|
@@ -2228,83 +2279,96 @@ var LoomaEditor = defineComponent({
|
|
|
2228
2279
|
style: {}
|
|
2229
2280
|
});
|
|
2230
2281
|
const loadForCodeBlock = codeHighlighting.loadForCodeBlock;
|
|
2231
|
-
const editor =
|
|
2232
|
-
|
|
2233
|
-
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
|
|
2265
|
-
|
|
2266
|
-
|
|
2267
|
-
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
if (view.isDestroyed) return;
|
|
2273
|
-
editorStateVersion.value += 1;
|
|
2274
|
-
const instance = editor.value;
|
|
2275
|
-
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
|
|
2276
|
-
linkPopup?.setProps({ getReferenceClientRect: () => link.getBoundingClientRect() });
|
|
2277
|
-
linkPopup?.show();
|
|
2282
|
+
const editor = shallowRef();
|
|
2283
|
+
let session;
|
|
2284
|
+
onMounted(() => {
|
|
2285
|
+
session = createEditorSession({
|
|
2286
|
+
extensions: [
|
|
2287
|
+
...getDefaultEditorExtensions({
|
|
2288
|
+
placeholder: props.placeholder,
|
|
2289
|
+
linkBaseUrl: props.linkBaseUrl,
|
|
2290
|
+
mention: mentionExtension ?? false,
|
|
2291
|
+
disableHighlight: props.disableHighlight,
|
|
2292
|
+
codeLanguages: props.codeLanguages ?? void 0,
|
|
2293
|
+
codeLowlight: codeHighlighting.lowlight,
|
|
2294
|
+
tableOfContentsNodeView,
|
|
2295
|
+
expandNodeView,
|
|
2296
|
+
dividerNodeView,
|
|
2297
|
+
calloutNodeView,
|
|
2298
|
+
imageNodeView
|
|
2299
|
+
}),
|
|
2300
|
+
imageDelivery.extension,
|
|
2301
|
+
slashExtension,
|
|
2302
|
+
...props.extensions
|
|
2303
|
+
],
|
|
2304
|
+
content: props.modelValue,
|
|
2305
|
+
editable: props.editable,
|
|
2306
|
+
editorProps: {
|
|
2307
|
+
attributes: {
|
|
2308
|
+
role: "textbox",
|
|
2309
|
+
"aria-multiline": "true",
|
|
2310
|
+
"aria-label": props.label
|
|
2311
|
+
},
|
|
2312
|
+
handleKeyDown: (_view, event) => onFormattingKeyDown(event),
|
|
2313
|
+
handleDOMEvents: { click: (view, event) => {
|
|
2314
|
+
if (!(event.target instanceof Element) || event.button !== 0) return false;
|
|
2315
|
+
const link = event.target.closest("a[href]");
|
|
2316
|
+
if (!link || !view.dom.contains(link)) return false;
|
|
2317
|
+
const href = validLinkHref(link.getAttribute("href") ?? "");
|
|
2318
|
+
if (!href) return false;
|
|
2319
|
+
if (isApplePlatform() ? event.metaKey : event.ctrlKey) {
|
|
2320
|
+
event.preventDefault();
|
|
2321
|
+
window.open(href, "_blank", "noopener,noreferrer");
|
|
2322
|
+
return true;
|
|
2278
2323
|
}
|
|
2324
|
+
dismissedFormattingSelection = null;
|
|
2325
|
+
requestAnimationFrame(() => {
|
|
2326
|
+
if (view.isDestroyed) return;
|
|
2327
|
+
editorStateVersion.value += 1;
|
|
2328
|
+
const instance = editor.value;
|
|
2329
|
+
if (instance?.isFocused && instance.state.selection.empty && instance.isActive("link") && !linkOpen.value) {
|
|
2330
|
+
linkPopup?.setProps({ getReferenceClientRect: () => link.getBoundingClientRect() });
|
|
2331
|
+
linkPopup?.show();
|
|
2332
|
+
}
|
|
2333
|
+
});
|
|
2334
|
+
return false;
|
|
2335
|
+
} }
|
|
2336
|
+
},
|
|
2337
|
+
onCreate: ({ editor: instance }) => {
|
|
2338
|
+
emit("ready", instance);
|
|
2339
|
+
instance.state.doc.descendants((node) => {
|
|
2340
|
+
if (node.type.name === "codeBlock") loadForCodeBlock(instance, node.attrs.language, node.textContent);
|
|
2279
2341
|
});
|
|
2280
|
-
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
const
|
|
2294
|
-
|
|
2295
|
-
|
|
2296
|
-
if (!formattingSelectionActive(instance)) formattingPopup?.hide();
|
|
2342
|
+
},
|
|
2343
|
+
onFocus: ({ editor: instance }) => rememberSelection(instance),
|
|
2344
|
+
onSelectionUpdate: ({ editor: instance }) => {
|
|
2345
|
+
rememberSelection(instance);
|
|
2346
|
+
if (floatingFormattingMode()) {
|
|
2347
|
+
const selection = instance.state.selection;
|
|
2348
|
+
if (!dismissedFormattingSelection?.eq(selection)) dismissedFormattingSelection = null;
|
|
2349
|
+
if (!heldFormattingSelection?.eq(selection)) heldFormattingSelection = null;
|
|
2350
|
+
if (!formattingSelectionActive(instance)) formattingPopup?.hide();
|
|
2351
|
+
}
|
|
2352
|
+
if (linkContextEditing.value && instance.state.selection.from !== linkSelection?.from) linkContextEditing.value = false;
|
|
2353
|
+
},
|
|
2354
|
+
onUpdate: ({ editor: instance }) => {
|
|
2355
|
+
const value = instance.getJSON();
|
|
2356
|
+
emit("update:modelValue", value);
|
|
2357
|
+
emit("update", value);
|
|
2297
2358
|
}
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
onTransaction: () => {
|
|
2359
|
+
}, Editor$1);
|
|
2360
|
+
session.subscribe(() => {
|
|
2301
2361
|
editorStateVersion.value += 1;
|
|
2302
|
-
}
|
|
2303
|
-
|
|
2304
|
-
|
|
2305
|
-
|
|
2306
|
-
|
|
2307
|
-
|
|
2362
|
+
});
|
|
2363
|
+
editor.value = session.editor;
|
|
2364
|
+
});
|
|
2365
|
+
const attachDocument = (element) => {
|
|
2366
|
+
if (element instanceof HTMLElement) session?.attach(element);
|
|
2367
|
+
};
|
|
2368
|
+
onBeforeUnmount(() => {
|
|
2369
|
+
const element = session?.editor.options.element;
|
|
2370
|
+
element?.parentNode?.replaceChild(element.cloneNode(true), element);
|
|
2371
|
+
session?.destroy();
|
|
2308
2372
|
});
|
|
2309
2373
|
watch(() => {
|
|
2310
2374
|
editorStateVersion.value;
|
|
@@ -2773,31 +2837,9 @@ var LoomaEditor = defineComponent({
|
|
|
2773
2837
|
});
|
|
2774
2838
|
watch(() => props.modelValue, (value) => {
|
|
2775
2839
|
const instance = editor.value;
|
|
2776
|
-
if (!instance ||
|
|
2840
|
+
if (!instance || !session) return;
|
|
2777
2841
|
const wasFocused = instance.isFocused;
|
|
2778
|
-
|
|
2779
|
-
bookmark: instance.view.state.selection.getBookmark(),
|
|
2780
|
-
from: instance.view.state.selection.from,
|
|
2781
|
-
to: instance.view.state.selection.to,
|
|
2782
|
-
text: instance.view.state.selection instanceof TextSelection
|
|
2783
|
-
};
|
|
2784
|
-
if (!wasFocused) {
|
|
2785
|
-
instance.commands.setContent(value, false);
|
|
2786
|
-
return;
|
|
2787
|
-
}
|
|
2788
|
-
instance.chain().setContent(value, false).command(({ tr }) => {
|
|
2789
|
-
try {
|
|
2790
|
-
tr.setSelection(previousSelection.text ? TextSelection.create(tr.doc, previousSelection.from, previousSelection.to) : previousSelection.bookmark.resolve(tr.doc));
|
|
2791
|
-
} catch {
|
|
2792
|
-
const max = tr.doc.content.size;
|
|
2793
|
-
const from = Math.max(1, Math.min(previousSelection.from, max));
|
|
2794
|
-
const to = Math.max(from, Math.min(previousSelection.to, max));
|
|
2795
|
-
tr.setSelection(TextSelection.create(tr.doc, from, to));
|
|
2796
|
-
}
|
|
2797
|
-
return true;
|
|
2798
|
-
}).run();
|
|
2799
|
-
instance.view.focus();
|
|
2800
|
-
rememberSelection(instance);
|
|
2842
|
+
if (session.setContent(value, lastFocusedSelection) && wasFocused) rememberSelection(instance);
|
|
2801
2843
|
}, { deep: true });
|
|
2802
2844
|
const closeTableUi = () => {
|
|
2803
2845
|
tableUi.toolbarOpen = false;
|
|
@@ -3710,7 +3752,7 @@ var LoomaEditor = defineComponent({
|
|
|
3710
3752
|
}
|
|
3711
3753
|
}
|
|
3712
3754
|
}, { default: () => h(ImageControls, { editor: instance }) }) : null,
|
|
3713
|
-
instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h(
|
|
3755
|
+
instance ? h(EditorSurface, { density: props.editable ? "comfortable" : props.contentDensity }, () => h("div", { ref: attachDocument })) : null,
|
|
3714
3756
|
codeUi.open ? h("div", {
|
|
3715
3757
|
ref: codeLanguageRef,
|
|
3716
3758
|
class: "looma-editor__code-language",
|