@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.
@@ -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, NodeViewContent, NodeViewWrapper, VueNodeViewRenderer, nodeViewProps, useEditor } from "@tiptap/vue-3";
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>;