@threadlabs/looma 0.18.0 → 0.18.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/components/ui-button/ui-button.html +1 -1
- package/components/ui-icon-button/ui-icon-button.html +10 -0
- package/components/ui-menu/ui-menu.js +14 -2
- package/components/ui-menu-item/ui-menu-item.html +3 -0
- package/components/ui-popover/ui-popover.js +13 -2
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/styles/ui-button.css +1 -1
- package/styles/ui-icon-button.css +10 -0
- package/styles/ui-menu-item.css +3 -0
- package/vue/UiButton.vue +1 -1
- package/vue/UiIconButton.vue +13 -0
- package/vue/UiMenuItem.vue +3 -0
- package/vue/chunks/UiButton.js +1 -1
- package/vue/chunks/UiIconButton.js +1 -1
- package/vue/chunks/UiMenuItem.js +1 -1
- package/vue/components.css +117 -101
- package/vue/editor/index.js +95 -68
- package/vue/editor/code-block-view.d.ts +0 -29
package/vue/editor/index.js
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { computed, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, useId, watch } from "vue";
|
|
2
2
|
import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, resolveTableCellAt, shouldShowTextFormattingToolbar } from "@threadlabs/looma/editor";
|
|
3
3
|
import { Combobox, EditorInsertTableGrid, EditorMentionMenu, EditorSlashMenu, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, IconButton, Menu, MenuItem, Popover, Tooltip } from "@threadlabs/looma/vue";
|
|
4
|
-
import { BubbleMenu, EditorContent,
|
|
4
|
+
import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
|
|
5
|
+
import { closeHistory } from "@tiptap/pm/history";
|
|
6
|
+
import { createLowlight } from "lowlight";
|
|
5
7
|
import { Plugin, PluginKey, TextSelection } from "@tiptap/pm/state";
|
|
6
8
|
import { Extension } from "@tiptap/core";
|
|
7
9
|
import { Decoration, DecorationSet } from "@tiptap/pm/view";
|
|
8
|
-
import { closeHistory } from "@tiptap/pm/history";
|
|
9
|
-
import { createLowlight } from "lowlight";
|
|
10
10
|
export * from "@threadlabs/looma/editor";
|
|
11
11
|
//#region src/vue/editor/primitives.ts
|
|
12
12
|
/** Each Vue editor component's element tag, for tooling that enumerates them. */
|
|
@@ -180,69 +180,6 @@ function imageUploadContent(image) {
|
|
|
180
180
|
};
|
|
181
181
|
}
|
|
182
182
|
//#endregion
|
|
183
|
-
//#region src/vue/editor/code-block-view.ts
|
|
184
|
-
/** Per-block language control; the document stores only the code and explicit language. */
|
|
185
|
-
function createCodeBlockView(languages, isEditable) {
|
|
186
|
-
const lowlight = createLowlight(languages);
|
|
187
|
-
const names = Object.keys(languages).sort();
|
|
188
|
-
return defineComponent({
|
|
189
|
-
name: "LoomaCodeBlockView",
|
|
190
|
-
props: nodeViewProps,
|
|
191
|
-
setup(props) {
|
|
192
|
-
const selected = computed(() => {
|
|
193
|
-
const language = props.node.attrs.language;
|
|
194
|
-
return typeof language === "string" && language ? language : "auto";
|
|
195
|
-
});
|
|
196
|
-
const detected = computed(() => {
|
|
197
|
-
if (!props.node.textContent.trim()) return null;
|
|
198
|
-
const language = lowlight.highlightAuto(props.node.textContent).data?.language;
|
|
199
|
-
return typeof language === "string" ? language : null;
|
|
200
|
-
});
|
|
201
|
-
const options = computed(() => [
|
|
202
|
-
{
|
|
203
|
-
id: "auto",
|
|
204
|
-
value: "auto",
|
|
205
|
-
label: detected.value ? `Auto (${detected.value.toUpperCase()})` : "Auto"
|
|
206
|
-
},
|
|
207
|
-
...selected.value !== "auto" && !Object.hasOwn(languages, selected.value) ? [{
|
|
208
|
-
id: selected.value,
|
|
209
|
-
value: selected.value,
|
|
210
|
-
label: `${selected.value.toUpperCase()} (unavailable)`
|
|
211
|
-
}] : [],
|
|
212
|
-
...names.map((name) => ({
|
|
213
|
-
id: name,
|
|
214
|
-
value: name,
|
|
215
|
-
label: name.toUpperCase()
|
|
216
|
-
}))
|
|
217
|
-
]);
|
|
218
|
-
const choose = (detail) => {
|
|
219
|
-
if (detail.kind !== "selection" || !detail.value) return;
|
|
220
|
-
const language = detail.value === "auto" ? null : detail.value;
|
|
221
|
-
if (language !== null && !Object.hasOwn(languages, language)) return;
|
|
222
|
-
props.editor.view.dispatch(closeHistory(props.editor.state.tr));
|
|
223
|
-
props.updateAttributes({ language });
|
|
224
|
-
props.editor.view.dispatch(closeHistory(props.editor.state.tr));
|
|
225
|
-
};
|
|
226
|
-
const showControl = () => names.length > 0 && isEditable() && props.editor.isEditable;
|
|
227
|
-
return () => h(NodeViewWrapper, {
|
|
228
|
-
as: "pre",
|
|
229
|
-
class: showControl() ? "looma-editor__code-block--has-language" : void 0
|
|
230
|
-
}, () => [showControl() ? h("div", {
|
|
231
|
-
class: "looma-editor__code-language",
|
|
232
|
-
contenteditable: "false"
|
|
233
|
-
}, [h(Combobox, {
|
|
234
|
-
label: "Code language",
|
|
235
|
-
labelVisibility: "sr-only",
|
|
236
|
-
size: "sm",
|
|
237
|
-
disclosure: true,
|
|
238
|
-
filter: "label",
|
|
239
|
-
value: selected.value,
|
|
240
|
-
onValueChange: choose
|
|
241
|
-
}, () => options.value.map((option) => h("option", { value: option.value }, option.label)))]) : null, h(NodeViewContent, { as: "code" })]);
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
}
|
|
245
|
-
//#endregion
|
|
246
183
|
//#region src/vue/editor/LoomaEditor.ts
|
|
247
184
|
var EMPTY_DOCUMENT = {
|
|
248
185
|
type: "doc",
|
|
@@ -459,6 +396,7 @@ var LoomaEditor = defineComponent({
|
|
|
459
396
|
const tableOverlayShell = ref(null);
|
|
460
397
|
const tableMenuShell = ref(null);
|
|
461
398
|
const mobileToolbarShell = ref(null);
|
|
399
|
+
const codeLanguageShell = ref(null);
|
|
462
400
|
const dragOver = ref(false);
|
|
463
401
|
const uploading = ref(false);
|
|
464
402
|
const failedUpload = ref(null);
|
|
@@ -583,8 +521,7 @@ var LoomaEditor = defineComponent({
|
|
|
583
521
|
placeholder: props.placeholder,
|
|
584
522
|
mention: mentionExtension ?? false,
|
|
585
523
|
disableHighlight: props.disableHighlight,
|
|
586
|
-
codeLanguages: props.codeLanguages
|
|
587
|
-
codeBlockNodeView: VueNodeViewRenderer(createCodeBlockView(props.codeLanguages, () => props.editable))
|
|
524
|
+
codeLanguages: props.codeLanguages
|
|
588
525
|
}),
|
|
589
526
|
imageDelivery.extension,
|
|
590
527
|
slashExtension,
|
|
@@ -794,6 +731,77 @@ var LoomaEditor = defineComponent({
|
|
|
794
731
|
};
|
|
795
732
|
});
|
|
796
733
|
};
|
|
734
|
+
const codeLanguageNames = Object.keys(props.codeLanguages).sort();
|
|
735
|
+
const codeLowlight = createLowlight(props.codeLanguages);
|
|
736
|
+
const codeUi = reactive({
|
|
737
|
+
open: false,
|
|
738
|
+
pos: 0,
|
|
739
|
+
language: "auto",
|
|
740
|
+
text: "",
|
|
741
|
+
style: {}
|
|
742
|
+
});
|
|
743
|
+
let codeBlurTimer;
|
|
744
|
+
const updateCodeUi = (focusSettled = false) => {
|
|
745
|
+
const instance = editor.value?.isDestroyed ? void 0 : editor.value;
|
|
746
|
+
const selection = instance?.state.selection;
|
|
747
|
+
const block = selection?.$from.parent;
|
|
748
|
+
const dom = instance && block?.type.name === "codeBlock" && selection.$from.sameParent(selection.$to) ? instance.view.nodeDOM(selection.$from.before()) : null;
|
|
749
|
+
if (!instance || !props.editable || !codeLanguageNames.length || !(dom instanceof HTMLElement)) {
|
|
750
|
+
codeUi.open = false;
|
|
751
|
+
return;
|
|
752
|
+
}
|
|
753
|
+
if (!instance.isFocused && !codeLanguageShell.value?.contains(document.activeElement)) {
|
|
754
|
+
clearTimeout(codeBlurTimer);
|
|
755
|
+
if (codeUi.open && !focusSettled) codeBlurTimer = setTimeout(() => updateCodeUi(true), 0);
|
|
756
|
+
else codeUi.open = false;
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const rect = dom.getBoundingClientRect();
|
|
760
|
+
const above = rect.top > 76;
|
|
761
|
+
codeUi.open = true;
|
|
762
|
+
codeUi.pos = selection.$from.before();
|
|
763
|
+
codeUi.language = typeof block.attrs.language === "string" && block.attrs.language ? block.attrs.language : "auto";
|
|
764
|
+
codeUi.text = block.textContent;
|
|
765
|
+
codeUi.style = {
|
|
766
|
+
top: `${above ? rect.top - 8 : rect.bottom + 8}px`,
|
|
767
|
+
left: `${rect.left + rect.width / 2}px`,
|
|
768
|
+
transform: `translate(-50%, ${above ? "-100%" : "0"})`
|
|
769
|
+
};
|
|
770
|
+
};
|
|
771
|
+
const codeLanguageOptions = computed(() => {
|
|
772
|
+
const detected = codeUi.open && codeUi.text.trim() ? codeLowlight.highlightAuto(codeUi.text).data?.language : null;
|
|
773
|
+
return [
|
|
774
|
+
{
|
|
775
|
+
value: "auto",
|
|
776
|
+
label: typeof detected === "string" ? `Auto (${detected.toUpperCase()})` : "Auto"
|
|
777
|
+
},
|
|
778
|
+
...codeUi.language !== "auto" && !Object.hasOwn(props.codeLanguages, codeUi.language) ? [{
|
|
779
|
+
value: codeUi.language,
|
|
780
|
+
label: `${codeUi.language.toUpperCase()} (unavailable)`
|
|
781
|
+
}] : [],
|
|
782
|
+
...codeLanguageNames.map((name) => ({
|
|
783
|
+
value: name,
|
|
784
|
+
label: name.toUpperCase()
|
|
785
|
+
}))
|
|
786
|
+
];
|
|
787
|
+
});
|
|
788
|
+
const chooseCodeLanguage = (detail) => {
|
|
789
|
+
const instance = editor.value;
|
|
790
|
+
if (!instance || detail.kind !== "selection" || !detail.value) return;
|
|
791
|
+
const language = detail.value === "auto" ? null : detail.value;
|
|
792
|
+
if (language !== null && !Object.hasOwn(props.codeLanguages, language)) return;
|
|
793
|
+
const node = instance.state.doc.nodeAt(codeUi.pos);
|
|
794
|
+
if (node?.type.name !== "codeBlock") return;
|
|
795
|
+
instance.view.dispatch(closeHistory(instance.state.tr));
|
|
796
|
+
instance.view.dispatch(instance.state.tr.setNodeMarkup(codeUi.pos, void 0, {
|
|
797
|
+
...node.attrs,
|
|
798
|
+
language
|
|
799
|
+
}));
|
|
800
|
+
instance.view.dispatch(closeHistory(instance.state.tr));
|
|
801
|
+
};
|
|
802
|
+
const onCodeTransaction = () => updateCodeUi();
|
|
803
|
+
watch(() => props.editable, () => nextTick(onCodeTransaction));
|
|
804
|
+
onBeforeUnmount(() => clearTimeout(codeBlurTimer));
|
|
797
805
|
const updateTableUi = () => {
|
|
798
806
|
const instance = editor.value;
|
|
799
807
|
if (!instance || !props.editable || !instance.isFocused && !tableInteractionActive && !hoveredTableCell) {
|
|
@@ -840,10 +848,12 @@ var LoomaEditor = defineComponent({
|
|
|
840
848
|
const onEditorFocus = () => {
|
|
841
849
|
editorFocused.value = true;
|
|
842
850
|
updateTableUi();
|
|
851
|
+
updateCodeUi();
|
|
843
852
|
updateMobileViewport();
|
|
844
853
|
};
|
|
845
854
|
const onEditorBlur = () => {
|
|
846
855
|
updateTableUi();
|
|
856
|
+
updateCodeUi();
|
|
847
857
|
setTimeout(() => {
|
|
848
858
|
if (!tableInteractionActive && !editor.value?.isFocused) editorFocused.value = false;
|
|
849
859
|
}, 0);
|
|
@@ -852,6 +862,7 @@ var LoomaEditor = defineComponent({
|
|
|
852
862
|
if (!instance) return;
|
|
853
863
|
instance.on("selectionUpdate", updateTableUi);
|
|
854
864
|
instance.on("transaction", updateTableUi);
|
|
865
|
+
instance.on("transaction", onCodeTransaction);
|
|
855
866
|
instance.on("focus", onEditorFocus);
|
|
856
867
|
instance.on("blur", onEditorBlur);
|
|
857
868
|
nextTick(updateTableUi);
|
|
@@ -860,6 +871,7 @@ var LoomaEditor = defineComponent({
|
|
|
860
871
|
if (!instance) return;
|
|
861
872
|
instance.off("selectionUpdate", updateTableUi);
|
|
862
873
|
instance.off("transaction", updateTableUi);
|
|
874
|
+
instance.off("transaction", onCodeTransaction);
|
|
863
875
|
instance.off("focus", onEditorFocus);
|
|
864
876
|
instance.off("blur", onEditorBlur);
|
|
865
877
|
};
|
|
@@ -870,6 +882,7 @@ var LoomaEditor = defineComponent({
|
|
|
870
882
|
const onViewportChange = () => {
|
|
871
883
|
updateMobileViewport();
|
|
872
884
|
updateTableUi();
|
|
885
|
+
updateCodeUi();
|
|
873
886
|
tableUi.menuOpen = false;
|
|
874
887
|
};
|
|
875
888
|
const onDocumentPointerDown = (event) => {
|
|
@@ -1317,6 +1330,20 @@ var LoomaEditor = defineComponent({
|
|
|
1317
1330
|
"aria-label": "Editor tools"
|
|
1318
1331
|
}, [renderToolbar(instance, false)]) : null,
|
|
1319
1332
|
instance ? h(EditorContent, { editor: instance }) : null,
|
|
1333
|
+
codeUi.open ? h("div", {
|
|
1334
|
+
ref: codeLanguageShell,
|
|
1335
|
+
class: "looma-editor__code-language",
|
|
1336
|
+
style: codeUi.style,
|
|
1337
|
+
onFocusout: () => updateCodeUi()
|
|
1338
|
+
}, [h(Combobox, {
|
|
1339
|
+
label: "Code language",
|
|
1340
|
+
labelVisibility: "sr-only",
|
|
1341
|
+
size: "sm",
|
|
1342
|
+
disclosure: true,
|
|
1343
|
+
filter: "label",
|
|
1344
|
+
value: codeUi.language,
|
|
1345
|
+
onValueChange: chooseCodeLanguage
|
|
1346
|
+
}, () => codeLanguageOptions.value.map((option) => h("option", { value: option.value }, option.label)))]) : null,
|
|
1320
1347
|
instance && props.editable && mobile.value && editorFocused.value ? h("div", {
|
|
1321
1348
|
ref: mobileToolbarShell,
|
|
1322
1349
|
class: "looma-editor__mobile-toolbar-shell",
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { LoomaCodeLanguages } from "@threadlabs/looma/editor";
|
|
2
|
-
/** Per-block language control; the document stores only the code and explicit language. */
|
|
3
|
-
export declare function createCodeBlockView(languages: LoomaCodeLanguages, isEditable: () => boolean): import("vue").DefineComponent<{
|
|
4
|
-
decorations: readonly import("@tiptap/core").DecorationWithType[];
|
|
5
|
-
selected: boolean;
|
|
6
|
-
updateAttributes: (attributes: Record<string, any>) => void;
|
|
7
|
-
deleteNode: () => void;
|
|
8
|
-
node: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[0];
|
|
9
|
-
view: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[1];
|
|
10
|
-
getPos: () => number;
|
|
11
|
-
innerDecorations: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[4];
|
|
12
|
-
editor: import("@tiptap/core").Editor;
|
|
13
|
-
extension: import("@tiptap/core").Node;
|
|
14
|
-
HTMLAttributes: Record<string, any>;
|
|
15
|
-
}, () => import("vue").VNode<import("vue").RendererNode, import("vue").RendererElement, {
|
|
16
|
-
[key: string]: any;
|
|
17
|
-
}>, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<{
|
|
18
|
-
decorations: readonly import("@tiptap/core").DecorationWithType[];
|
|
19
|
-
selected: boolean;
|
|
20
|
-
updateAttributes: (attributes: Record<string, any>) => void;
|
|
21
|
-
deleteNode: () => void;
|
|
22
|
-
node: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[0];
|
|
23
|
-
view: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[1];
|
|
24
|
-
getPos: () => number;
|
|
25
|
-
innerDecorations: Parameters<import("@tiptap/pm/view").NodeViewConstructor>[4];
|
|
26
|
-
editor: import("@tiptap/core").Editor;
|
|
27
|
-
extension: import("@tiptap/core").Node;
|
|
28
|
-
HTMLAttributes: Record<string, any>;
|
|
29
|
-
}> & Readonly<{}>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, true, {}, any>;
|