@threadlabs/looma 0.1.5 → 0.1.7

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.
Files changed (62) hide show
  1. package/README.md +5 -4
  2. package/dist/{chunk-3PASQ7DE.js → chunk-UMFO4QNN.js} +26 -26
  3. package/dist/cjs/loader.cjs.js +1 -1
  4. package/dist/cjs/looma.cjs.js +1 -1
  5. package/dist/cjs/ui-dialog.cjs.entry.js +3 -1
  6. package/dist/collection/components/ui-dialog/ui-dialog.js +22 -1
  7. package/dist/components/ui-dialog.js +1 -1
  8. package/dist/esm/loader.js +1 -1
  9. package/dist/esm/looma.js +1 -1
  10. package/dist/esm/ui-dialog.entry.js +3 -1
  11. package/dist/index.cjs +4 -2
  12. package/dist/index.js +2 -2
  13. package/dist/looma/looma.esm.js +1 -1
  14. package/dist/looma/p-5df568d8.entry.js +1 -0
  15. package/dist/types/components/ui-dialog/ui-dialog.d.ts +2 -0
  16. package/dist/types/components.d.ts +9 -0
  17. package/dist/{ui-avatar-group.entry-DBVLQ4KB.js → ui-avatar-group.entry-S6DFXYB7.js} +1 -1
  18. package/dist/{ui-avatar.entry-2WTBM2EC.js → ui-avatar.entry-MDRGXLWQ.js} +1 -1
  19. package/dist/{ui-badge.entry-XKIGBK5V.js → ui-badge.entry-RLL4YKPD.js} +1 -1
  20. package/dist/{ui-button.entry-PWT56MPD.js → ui-button.entry-IYMYLBFY.js} +1 -1
  21. package/dist/{ui-checkbox.entry-QT5S7EEL.js → ui-checkbox.entry-K5FGWDG2.js} +1 -1
  22. package/dist/{ui-context-menu.entry-NGWR3LOZ.js → ui-context-menu.entry-KWXXJ3ZV.js} +1 -1
  23. package/dist/{ui-dialog.entry-PEDDIALQ.js → ui-dialog.entry-RKHLQLQM.js} +4 -2
  24. package/dist/{ui-disclosure.entry-UKO7MAZM.js → ui-disclosure.entry-FRZKGEJP.js} +1 -1
  25. package/dist/{ui-floating-action-button.entry-SSGHRMCO.js → ui-floating-action-button.entry-BH4FDSTL.js} +1 -1
  26. package/dist/{ui-form-field.entry-6TZCWPYL.js → ui-form-field.entry-3CR2KSUH.js} +1 -1
  27. package/dist/{ui-icon-button.entry-WMCYMK2H.js → ui-icon-button.entry-LB54U25M.js} +1 -1
  28. package/dist/{ui-input.entry-AZ5BGQNA.js → ui-input.entry-5PQR37HS.js} +1 -1
  29. package/dist/{ui-menu-item.entry-L4NJDBKF.js → ui-menu-item.entry-2T3QWHLK.js} +1 -1
  30. package/dist/{ui-menu.entry-DGUYAE3Q.js → ui-menu.entry-Q5Z5YO5P.js} +1 -1
  31. package/dist/{ui-popover.entry-A54EKVZG.js → ui-popover.entry-4P2NWWAY.js} +1 -1
  32. package/dist/{ui-radio-group.entry-6YP4LY4N.js → ui-radio-group.entry-QKPRNV26.js} +1 -1
  33. package/dist/{ui-radio.entry-CGYNK7PN.js → ui-radio.entry-O4DOMFNU.js} +1 -1
  34. package/dist/{ui-search-result-row.entry-DGKSI2QM.js → ui-search-result-row.entry-BR5O3UCH.js} +1 -1
  35. package/dist/{ui-search-shell.entry-3F5ASGTW.js → ui-search-shell.entry-2ZBP7SA3.js} +1 -1
  36. package/dist/{ui-select.entry-SXSZPVXA.js → ui-select.entry-L75HQY72.js} +1 -1
  37. package/dist/{ui-switch.entry-AD4EVUWM.js → ui-switch.entry-L5QWD3BT.js} +1 -1
  38. package/dist/{ui-tabs.entry-WLKO5G5N.js → ui-tabs.entry-KWRD6Y7J.js} +1 -1
  39. package/dist/{ui-textarea.entry-IHPC7Q4R.js → ui-textarea.entry-JC3HPEBS.js} +1 -1
  40. package/dist/{ui-toast-region.entry-GCW2LJAC.js → ui-toast-region.entry-C6KK4OM4.js} +1 -1
  41. package/dist/{ui-tooltip.entry-EU2G6BH5.js → ui-tooltip.entry-G5EOCWP4.js} +1 -1
  42. package/dist/{ui-top-bar.entry-NMAFXVET.js → ui-top-bar.entry-YLKVAQFH.js} +1 -1
  43. package/editor/chunk-CX4QDKSV.js +22 -0
  44. package/editor/{chunk-YGJLGDQC.js → chunk-NJ2KAMS2.js} +2203 -17
  45. package/editor/{chunk-CWCXYQMT.js → chunk-OK5NARJI.js} +59 -28
  46. package/editor/extensions/index.d.ts +74 -20
  47. package/editor/extensions/index.js +22 -4
  48. package/editor/index.d.ts +3 -3
  49. package/editor/index.js +25 -5
  50. package/editor/table-overlay-Bq0F5xPg.d.ts +64 -0
  51. package/editor/ui.d.ts +2 -12
  52. package/editor/ui.js +9 -1
  53. package/editor.css +375 -4
  54. package/package.json +9 -2
  55. package/tokens.css +17 -0
  56. package/vue/chunk-KDA4RFLQ.js +203 -0
  57. package/vue/editor/index.d.ts +93 -3
  58. package/vue/editor/index.js +565 -30
  59. package/vue/index.js +38 -82
  60. package/dist/looma/p-a7be33fd.entry.js +0 -1
  61. package/editor/table-overlay-DTr3KC9e.d.ts +0 -12
  62. package/vue/chunk-P72L3GWE.js +0 -84
@@ -1,11 +1,14 @@
1
1
  import {
2
+ IconButton,
2
3
  createAdapterComponent,
3
4
  toHTMLElement
4
- } from "../chunk-P72L3GWE.js";
5
+ } from "../chunk-KDA4RFLQ.js";
5
6
 
6
7
  // src/editor/index.ts
7
8
  import "@threadlabs/looma/editor";
8
9
  export * from "@threadlabs/looma/editor";
10
+
11
+ // src/editor/primitives.ts
9
12
  import {
10
13
  defineComponent,
11
14
  h,
@@ -35,9 +38,7 @@ var EditorSlashMenu = defineComponent({
35
38
  if (element.open !== props.open) element.open = props.open;
36
39
  if (element.query !== props.query) element.query = props.query;
37
40
  if (element.items !== props.items) element.items = props.items;
38
- if (element.selectedIndex !== props.selectedIndex) {
39
- element.selectedIndex = props.selectedIndex;
40
- }
41
+ if (element.selectedIndex !== props.selectedIndex) element.selectedIndex = props.selectedIndex;
41
42
  if (element.anchorRect !== props.anchorRect) element.anchorRect = props.anchorRect;
42
43
  });
43
44
  watchEffect((onCleanup) => {
@@ -81,31 +82,11 @@ var EditorSlashMenu = defineComponent({
81
82
  };
82
83
  }
83
84
  });
84
- var EditorToolbar = createAdapterComponent(
85
- "ui-editor-toolbar",
86
- "EditorToolbar",
87
- EDITOR_EVENT_BINDINGS
88
- );
89
- var EditorTableContextMenu = createAdapterComponent(
90
- "ui-editor-table-context-menu",
91
- "EditorTableContextMenu",
92
- EDITOR_EVENT_BINDINGS
93
- );
94
- var EditorTableToolbar = createAdapterComponent(
95
- "ui-editor-table-toolbar",
96
- "EditorTableToolbar",
97
- EDITOR_EVENT_BINDINGS
98
- );
99
- var EditorInsertTableGrid = createAdapterComponent(
100
- "ui-editor-insert-table-grid",
101
- "EditorInsertTableGrid",
102
- EDITOR_EVENT_BINDINGS
103
- );
104
- var EditorTableOverlay = createAdapterComponent(
105
- "ui-editor-table-overlay",
106
- "EditorTableOverlay",
107
- EDITOR_EVENT_BINDINGS
108
- );
85
+ var EditorToolbar = createAdapterComponent("ui-editor-toolbar", "EditorToolbar", EDITOR_EVENT_BINDINGS);
86
+ var EditorTableContextMenu = createAdapterComponent("ui-editor-table-context-menu", "EditorTableContextMenu", EDITOR_EVENT_BINDINGS);
87
+ var EditorTableToolbar = createAdapterComponent("ui-editor-table-toolbar", "EditorTableToolbar", EDITOR_EVENT_BINDINGS);
88
+ var EditorInsertTableGrid = createAdapterComponent("ui-editor-insert-table-grid", "EditorInsertTableGrid", EDITOR_EVENT_BINDINGS);
89
+ var EditorTableOverlay = createAdapterComponent("ui-editor-table-overlay", "EditorTableOverlay", EDITOR_EVENT_BINDINGS);
109
90
  var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
110
91
  EditorToolbar: "ui-editor-toolbar",
111
92
  EditorSlashMenu: "ui-editor-slash-menu",
@@ -114,6 +95,559 @@ var EDITOR_ADAPTER_COMPONENT_TAG_MAP = {
114
95
  EditorInsertTableGrid: "ui-editor-insert-table-grid",
115
96
  EditorTableOverlay: "ui-editor-table-overlay"
116
97
  };
98
+
99
+ // src/editor/LoomaEditor.ts
100
+ import { BubbleMenu, EditorContent, useEditor } from "@tiptap/vue-3";
101
+ import {
102
+ defineComponent as defineComponent2,
103
+ h as h2,
104
+ nextTick,
105
+ onBeforeUnmount,
106
+ onMounted,
107
+ reactive,
108
+ ref,
109
+ watch
110
+ } from "vue";
111
+ import {
112
+ createLoomaSlashCommandExtension,
113
+ getActiveTableUiState,
114
+ getDefaultEditorExtensions,
115
+ handleTableAction,
116
+ handleTableOverlayAction,
117
+ normalizeActiveTableColumnWidths,
118
+ shouldShowTextFormattingToolbar
119
+ } from "@threadlabs/looma/editor";
120
+ var EMPTY_DOCUMENT = { type: "doc", content: [] };
121
+ var EMPTY_CAPABILITIES = {
122
+ canAddRowBefore: false,
123
+ canAddRowAfter: false,
124
+ canAddColumnBefore: false,
125
+ canAddColumnAfter: false,
126
+ canDeleteRow: false,
127
+ canDeleteColumn: false,
128
+ canDeleteTable: false,
129
+ canMergeCells: false,
130
+ canSplitCell: false
131
+ };
132
+ function sameDocument(left, right) {
133
+ return JSON.stringify(left) === JSON.stringify(right);
134
+ }
135
+ function selectedTableElement(editor) {
136
+ const { node } = editor.view.domAtPos(editor.state.selection.from);
137
+ const element = node instanceof HTMLElement ? node : node.parentElement;
138
+ return element?.closest("table");
139
+ }
140
+ function selectedTableNode(editor) {
141
+ const { $from } = editor.state.selection;
142
+ for (let depth = $from.depth; depth > 0; depth -= 1) {
143
+ const node = $from.node(depth);
144
+ if (node.type.name === "table") return node;
145
+ }
146
+ return null;
147
+ }
148
+ var LoomaEditor = defineComponent2({
149
+ name: "LoomaEditor",
150
+ inheritAttrs: false,
151
+ props: {
152
+ modelValue: {
153
+ type: Object,
154
+ default: () => ({ ...EMPTY_DOCUMENT })
155
+ },
156
+ editable: { type: Boolean, default: true },
157
+ placeholder: {
158
+ type: String,
159
+ default: "Type \u201C/\u201D for commands, or start writing\u2026"
160
+ },
161
+ extensions: {
162
+ type: Array,
163
+ default: () => []
164
+ },
165
+ uploadImage: {
166
+ type: Function,
167
+ default: void 0
168
+ }
169
+ },
170
+ emits: {
171
+ "update:modelValue": (_value) => true,
172
+ update: (_value) => true,
173
+ ready: (_editor) => true,
174
+ uploadError: (_error, _file) => true
175
+ },
176
+ setup(props, { attrs, emit, expose }) {
177
+ const root = ref(null);
178
+ const fileInput = ref(null);
179
+ const tablePickerAnchor = ref(null);
180
+ const tablePickerPopover = ref(null);
181
+ const tableToolbarShell = ref(null);
182
+ const tableOverlayShell = ref(null);
183
+ const tableMenuShell = ref(null);
184
+ const mobileToolbarShell = ref(null);
185
+ const dragOver = ref(false);
186
+ const uploading = ref(false);
187
+ const tablePickerOpen = ref(false);
188
+ const tablePickerStyle = ref({});
189
+ const mobileToolbarStyle = ref({});
190
+ const mobile = ref(false);
191
+ const editorFocused = ref(false);
192
+ const mobileToolbarMode = ref("formatting");
193
+ let dragLeaveTimer = null;
194
+ let tableResizeActive = false;
195
+ let tableInteractionActive = false;
196
+ let mobileTableControlsDismissed = false;
197
+ const slash = reactive({
198
+ active: false,
199
+ items: [],
200
+ selectedIndex: 0,
201
+ query: "",
202
+ rect: null,
203
+ select: null
204
+ });
205
+ const tableUi = reactive({
206
+ toolbarOpen: false,
207
+ overlayOpen: false,
208
+ menuOpen: false,
209
+ alignment: "left",
210
+ background: null,
211
+ rows: 0,
212
+ cols: 0,
213
+ toolbarStyle: {},
214
+ overlayStyle: {},
215
+ menuStyle: {},
216
+ capabilities: { ...EMPTY_CAPABILITIES }
217
+ });
218
+ const slashExtension = createLoomaSlashCommandExtension({
219
+ onOpenImagePicker: () => fileInput.value?.click(),
220
+ onStateChange: (state) => {
221
+ Object.assign(slash, state);
222
+ }
223
+ });
224
+ const editor = useEditor({
225
+ extensions: [
226
+ ...getDefaultEditorExtensions({ placeholder: props.placeholder }),
227
+ slashExtension,
228
+ ...props.extensions
229
+ ],
230
+ content: props.modelValue,
231
+ editable: props.editable,
232
+ onCreate: ({ editor: instance }) => emit("ready", instance),
233
+ onUpdate: ({ editor: instance }) => {
234
+ const value = instance.getJSON();
235
+ emit("update:modelValue", value);
236
+ emit("update", value);
237
+ }
238
+ });
239
+ watch(() => props.editable, (editable) => editor.value?.setEditable(editable));
240
+ watch(() => props.modelValue, (value) => {
241
+ const instance = editor.value;
242
+ if (!instance || sameDocument(instance.getJSON(), value)) return;
243
+ instance.commands.setContent(value, false);
244
+ }, { deep: true });
245
+ const closeTableUi = () => {
246
+ tableUi.toolbarOpen = false;
247
+ tableUi.overlayOpen = false;
248
+ tableUi.menuOpen = false;
249
+ tableUi.alignment = "left";
250
+ tableUi.background = null;
251
+ tableUi.capabilities = { ...EMPTY_CAPABILITIES };
252
+ mobileTableControlsDismissed = false;
253
+ mobileToolbarMode.value = "formatting";
254
+ };
255
+ const updateMobileViewport = () => {
256
+ mobile.value = window.innerWidth <= 767;
257
+ if (!mobile.value) {
258
+ mobileToolbarStyle.value = {};
259
+ return;
260
+ }
261
+ nextTick(() => {
262
+ const viewport = window.visualViewport;
263
+ const viewportLeft = viewport?.offsetLeft ?? 0;
264
+ const viewportTop = viewport?.offsetTop ?? 0;
265
+ const viewportWidth = viewport?.width ?? window.innerWidth;
266
+ const viewportHeight = viewport?.height ?? window.innerHeight;
267
+ const toolbarHeight = mobileToolbarShell.value?.getBoundingClientRect().height || 56;
268
+ mobileToolbarStyle.value = {
269
+ left: `${viewportLeft}px`,
270
+ top: `${Math.max(viewportTop, viewportTop + viewportHeight - toolbarHeight)}px`,
271
+ width: `${viewportWidth}px`
272
+ };
273
+ });
274
+ };
275
+ const updateTableUi = () => {
276
+ const instance = editor.value;
277
+ if (!instance || !props.editable || !instance.isFocused && !tableInteractionActive) {
278
+ closeTableUi();
279
+ return;
280
+ }
281
+ const state = getActiveTableUiState(instance);
282
+ const table = selectedTableElement(instance);
283
+ const tableNode = selectedTableNode(instance);
284
+ if (!state.active || !table || !tableNode) {
285
+ closeTableUi();
286
+ return;
287
+ }
288
+ if (!mobileTableControlsDismissed) mobileToolbarMode.value = "table";
289
+ const rect = table.getBoundingClientRect();
290
+ const maxToolbarWidth = Math.min(420, window.innerWidth - 24);
291
+ const toolbarLeft = Math.min(
292
+ Math.max(12, rect.left + (rect.width - maxToolbarWidth) / 2),
293
+ Math.max(12, window.innerWidth - maxToolbarWidth - 12)
294
+ );
295
+ tableUi.toolbarOpen = state.showToolbar && !mobile.value;
296
+ tableUi.overlayOpen = !mobile.value;
297
+ tableUi.alignment = state.cellAlignment;
298
+ tableUi.background = state.cellBackground;
299
+ tableUi.capabilities = state.capabilities;
300
+ tableUi.rows = tableNode.childCount;
301
+ tableUi.cols = tableNode.childCount ? tableNode.child(0).childCount : 0;
302
+ tableUi.toolbarStyle = {
303
+ top: `${rect.top > 76 ? rect.top - 52 : rect.bottom + 12}px`,
304
+ left: `${toolbarLeft}px`,
305
+ maxWidth: `${maxToolbarWidth}px`
306
+ };
307
+ tableUi.overlayStyle = {
308
+ top: `${rect.top}px`,
309
+ left: `${rect.left}px`,
310
+ width: `${rect.width}px`,
311
+ height: `${rect.height}px`
312
+ };
313
+ };
314
+ const onEditorFocus = () => {
315
+ editorFocused.value = true;
316
+ updateTableUi();
317
+ updateMobileViewport();
318
+ };
319
+ const onEditorBlur = () => {
320
+ updateTableUi();
321
+ setTimeout(() => {
322
+ if (!tableInteractionActive && !editor.value?.isFocused) editorFocused.value = false;
323
+ }, 0);
324
+ };
325
+ const bindEditorUi = (instance) => {
326
+ if (!instance) return;
327
+ instance.on("selectionUpdate", updateTableUi);
328
+ instance.on("transaction", updateTableUi);
329
+ instance.on("focus", onEditorFocus);
330
+ instance.on("blur", onEditorBlur);
331
+ nextTick(updateTableUi);
332
+ };
333
+ const unbindEditorUi = (instance) => {
334
+ if (!instance) return;
335
+ instance.off("selectionUpdate", updateTableUi);
336
+ instance.off("transaction", updateTableUi);
337
+ instance.off("focus", onEditorFocus);
338
+ instance.off("blur", onEditorBlur);
339
+ };
340
+ watch(editor, (instance, previous) => {
341
+ unbindEditorUi(previous);
342
+ bindEditorUi(instance);
343
+ }, { immediate: true });
344
+ const updateTablePickerPosition = () => {
345
+ const anchor = tablePickerAnchor.value;
346
+ if (!anchor) return;
347
+ const rect = anchor.getBoundingClientRect();
348
+ const viewport = window.visualViewport;
349
+ const viewportLeft = viewport?.offsetLeft ?? 0;
350
+ const viewportTop = viewport?.offsetTop ?? 0;
351
+ const viewportWidth = viewport?.width ?? window.innerWidth;
352
+ const viewportHeight = viewport?.height ?? window.innerHeight;
353
+ const width = 220;
354
+ const left = Math.min(
355
+ Math.max(viewportLeft + 12, rect.left),
356
+ Math.max(viewportLeft + 12, viewportLeft + viewportWidth - width - 12)
357
+ );
358
+ const popoverHeight = tablePickerPopover.value?.getBoundingClientRect().height || 300;
359
+ const top = Math.min(
360
+ Math.max(viewportTop + 12, rect.bottom + 8),
361
+ Math.max(viewportTop + 12, viewportTop + viewportHeight - popoverHeight - 12)
362
+ );
363
+ tablePickerStyle.value = window.innerWidth <= 767 ? {
364
+ position: "fixed",
365
+ left: `${left}px`,
366
+ top: `${Math.max(viewportTop + 12, viewportTop + viewportHeight - popoverHeight - 64)}px`
367
+ } : { position: "fixed", top: `${top}px`, left: `${left}px` };
368
+ };
369
+ const onViewportChange = () => {
370
+ updateMobileViewport();
371
+ updateTableUi();
372
+ updateTablePickerPosition();
373
+ tableUi.menuOpen = false;
374
+ };
375
+ const onDocumentPointerDown = (event) => {
376
+ const path = typeof event.composedPath === "function" ? event.composedPath() : [];
377
+ const inElement = (element) => Boolean(
378
+ element && (path.includes(element) || event.target instanceof Node && element.contains(event.target))
379
+ );
380
+ const inTableUi = inElement(tableToolbarShell.value) || inElement(tableOverlayShell.value) || inElement(tableMenuShell.value) || inElement(mobileToolbarShell.value);
381
+ tableInteractionActive = inTableUi;
382
+ if (inTableUi) setTimeout(() => {
383
+ tableInteractionActive = false;
384
+ }, 0);
385
+ if (tablePickerOpen.value && !inElement(tablePickerAnchor.value) && !inElement(tablePickerPopover.value)) {
386
+ tablePickerOpen.value = false;
387
+ }
388
+ if ((tableUi.toolbarOpen || tableUi.overlayOpen || tableUi.menuOpen) && !inElement(root.value) && !inTableUi) {
389
+ closeTableUi();
390
+ }
391
+ };
392
+ const onResizePointerDown = (event) => {
393
+ tableResizeActive = event.target instanceof HTMLElement && event.target.classList.contains("column-resize-handle");
394
+ };
395
+ const onResizePointerUp = () => {
396
+ if (!tableResizeActive) return;
397
+ tableResizeActive = false;
398
+ requestAnimationFrame(() => {
399
+ const instance = editor.value;
400
+ const table = instance ? selectedTableElement(instance) : null;
401
+ if (instance && table) normalizeActiveTableColumnWidths(instance, table);
402
+ nextTick(updateTableUi);
403
+ });
404
+ };
405
+ onMounted(() => {
406
+ updateMobileViewport();
407
+ window.addEventListener("resize", onViewportChange);
408
+ window.addEventListener("scroll", onViewportChange, true);
409
+ window.visualViewport?.addEventListener("resize", onViewportChange);
410
+ window.visualViewport?.addEventListener("scroll", onViewportChange);
411
+ document.addEventListener("pointerdown", onDocumentPointerDown, true);
412
+ document.addEventListener("pointerdown", onResizePointerDown, true);
413
+ document.addEventListener("pointerup", onResizePointerUp, true);
414
+ });
415
+ onBeforeUnmount(() => {
416
+ window.removeEventListener("resize", onViewportChange);
417
+ window.removeEventListener("scroll", onViewportChange, true);
418
+ window.visualViewport?.removeEventListener("resize", onViewportChange);
419
+ window.visualViewport?.removeEventListener("scroll", onViewportChange);
420
+ document.removeEventListener("pointerdown", onDocumentPointerDown, true);
421
+ document.removeEventListener("pointerdown", onResizePointerDown, true);
422
+ document.removeEventListener("pointerup", onResizePointerUp, true);
423
+ unbindEditorUi(editor.value);
424
+ if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
425
+ });
426
+ const insertImage = async (file) => {
427
+ if (!props.uploadImage || !editor.value) return;
428
+ uploading.value = true;
429
+ try {
430
+ const result = await props.uploadImage(file);
431
+ const image = typeof result === "string" ? { url: result } : result;
432
+ editor.value.chain().focus().setImage({ src: image.url, alt: image.alt ?? file.name }).run();
433
+ } catch (error) {
434
+ emit("uploadError", error, file);
435
+ } finally {
436
+ uploading.value = false;
437
+ }
438
+ };
439
+ const onFileChange = async (event) => {
440
+ const input = event.target;
441
+ const files = Array.from(input.files ?? []);
442
+ input.value = "";
443
+ for (const file of files) await insertImage(file);
444
+ };
445
+ const onDrop = async (event) => {
446
+ event.preventDefault();
447
+ dragOver.value = false;
448
+ if (!props.editable) return;
449
+ const files = Array.from(event.dataTransfer?.files ?? []).filter((file) => file.type.startsWith("image/"));
450
+ for (const file of files) await insertImage(file);
451
+ };
452
+ const onContextMenu = (event) => {
453
+ const instance = editor.value;
454
+ const cell = event.target instanceof HTMLElement ? event.target.closest("td, th") : null;
455
+ if (!props.editable || !instance || !(cell instanceof HTMLElement)) {
456
+ tableUi.menuOpen = false;
457
+ return;
458
+ }
459
+ event.preventDefault();
460
+ instance.chain().focus().setTextSelection(instance.view.posAtDOM(cell, 0) + 1).run();
461
+ updateTableUi();
462
+ tableUi.menuOpen = true;
463
+ tableUi.menuStyle = { top: `${event.clientY}px`, left: `${event.clientX}px` };
464
+ };
465
+ const runTableAction = (detail) => {
466
+ if (!editor.value) return;
467
+ handleTableAction(editor.value, detail);
468
+ tableUi.menuOpen = false;
469
+ nextTick(updateTableUi);
470
+ };
471
+ const runOverlayAction = (detail) => {
472
+ if (!editor.value) return;
473
+ handleTableOverlayAction(editor.value, detail);
474
+ nextTick(updateTableUi);
475
+ };
476
+ const commandButton = (label, icon, active, disabled, run) => h2(IconButton, {
477
+ class: "looma-editor__toolbar-button",
478
+ label,
479
+ title: label,
480
+ size: "sm",
481
+ variant: active ? "solid" : "ghost",
482
+ disabled,
483
+ "data-active": active ? "true" : "false",
484
+ onClick: run
485
+ }, () => h2("span", { class: "looma-editor__toolbar-glyph", "aria-hidden": "true" }, icon));
486
+ const renderToolbar = (instance) => {
487
+ const buttons = [
488
+ commandButton("Bold", "B", instance.isActive("bold"), !instance.can().toggleBold(), () => instance.chain().focus().toggleBold().run()),
489
+ commandButton("Italic", "I", instance.isActive("italic"), !instance.can().toggleItalic(), () => instance.chain().focus().toggleItalic().run()),
490
+ commandButton("Underline", "U", instance.isActive("underline"), !instance.can().toggleUnderline(), () => instance.chain().focus().toggleUnderline().run()),
491
+ commandButton("Strike", "S", instance.isActive("strike"), !instance.can().toggleStrike(), () => instance.chain().focus().toggleStrike().run()),
492
+ commandButton("Highlight", "\u25B0", instance.isActive("highlight"), !instance.can().toggleHighlight(), () => instance.chain().focus().toggleHighlight().run()),
493
+ commandButton("Inline code", "</>", instance.isActive("code"), !instance.can().toggleCode(), () => instance.chain().focus().toggleCode().run()),
494
+ h2("span", { class: "ui-editor-toolbar__divider", "aria-hidden": "true" }),
495
+ commandButton("Heading 1", "H1", instance.isActive("heading", { level: 1 }), false, () => instance.chain().focus().toggleHeading({ level: 1 }).run()),
496
+ commandButton("Heading 2", "H2", instance.isActive("heading", { level: 2 }), false, () => instance.chain().focus().toggleHeading({ level: 2 }).run()),
497
+ commandButton("Heading 3", "H3", instance.isActive("heading", { level: 3 }), false, () => instance.chain().focus().toggleHeading({ level: 3 }).run()),
498
+ commandButton("Bullet list", "\u2022", instance.isActive("bulletList"), false, () => instance.chain().focus().toggleBulletList().run()),
499
+ commandButton("Numbered list", "1.", instance.isActive("orderedList"), false, () => instance.chain().focus().toggleOrderedList().run()),
500
+ commandButton("Checklist", "\u2611", instance.isActive("taskList"), false, () => instance.chain().focus().toggleTaskList().run()),
501
+ commandButton("Blockquote", "\u275D", instance.isActive("blockquote"), !instance.can().toggleBlockquote(), () => instance.chain().focus().toggleBlockquote().run()),
502
+ commandButton("Code block", "{ }", instance.isActive("codeBlock"), !instance.can().toggleCodeBlock(), () => instance.chain().focus().toggleCodeBlock().run()),
503
+ commandButton("Divider", "\u2014", false, !instance.can().setHorizontalRule(), () => instance.chain().focus().setHorizontalRule().run()),
504
+ h2("span", { class: "ui-editor-toolbar__divider", "aria-hidden": "true" }),
505
+ h2("span", { ref: tablePickerAnchor, class: "looma-editor__table-picker-anchor" }, [
506
+ commandButton("Insert table", "\u229E", false, false, () => {
507
+ tablePickerOpen.value = !tablePickerOpen.value;
508
+ nextTick(updateTablePickerPosition);
509
+ })
510
+ ]),
511
+ commandButton(uploading.value ? "Uploading image" : "Insert image", "\u25A7", false, uploading.value || !props.uploadImage, () => fileInput.value?.click()),
512
+ h2("span", { class: "ui-editor-toolbar__divider", "aria-hidden": "true" }),
513
+ commandButton("Undo", "\u21B6", false, !instance.can().undo(), () => instance.chain().focus().undo().run()),
514
+ commandButton("Redo", "\u21B7", false, !instance.can().redo(), () => instance.chain().focus().redo().run())
515
+ ];
516
+ return h2(EditorToolbar, { floating: "" }, () => buttons);
517
+ };
518
+ const focus = (position = "start") => {
519
+ editor.value?.commands.focus(position);
520
+ };
521
+ expose({ editor, focus });
522
+ return () => {
523
+ const instance = editor.value;
524
+ const tableProps = {
525
+ open: true,
526
+ "cell-alignment": tableUi.alignment,
527
+ "cell-background": tableUi.background ?? void 0,
528
+ "can-add-row-before": tableUi.capabilities.canAddRowBefore,
529
+ "can-add-row-after": tableUi.capabilities.canAddRowAfter,
530
+ "can-add-column-before": tableUi.capabilities.canAddColumnBefore,
531
+ "can-add-column-after": tableUi.capabilities.canAddColumnAfter,
532
+ "can-delete-row": tableUi.capabilities.canDeleteRow,
533
+ "can-delete-column": tableUi.capabilities.canDeleteColumn,
534
+ "can-delete-table": tableUi.capabilities.canDeleteTable,
535
+ "can-merge-cells": tableUi.capabilities.canMergeCells,
536
+ "can-split-cell": tableUi.capabilities.canSplitCell,
537
+ onTableAction: runTableAction
538
+ };
539
+ return h2("div", {
540
+ ...attrs,
541
+ ref: root,
542
+ class: ["looma-editor", attrs.class, { "looma-editor--readonly": !props.editable, "looma-editor--drag-over": dragOver.value }],
543
+ onContextmenu: onContextMenu,
544
+ onDragover: (event) => {
545
+ event.preventDefault();
546
+ if (props.editable && Array.from(event.dataTransfer?.types ?? []).includes("Files")) dragOver.value = true;
547
+ },
548
+ onDragleave: () => {
549
+ if (dragLeaveTimer) clearTimeout(dragLeaveTimer);
550
+ dragLeaveTimer = setTimeout(() => {
551
+ dragOver.value = false;
552
+ }, 100);
553
+ },
554
+ onDrop
555
+ }, [
556
+ instance && props.editable && !mobile.value ? h2(BubbleMenu, {
557
+ editor: instance,
558
+ pluginKey: "looma-text-formatting-menu",
559
+ shouldShow: ({ editor: menuEditor, from, to }) => shouldShowTextFormattingToolbar(menuEditor, from, to),
560
+ tippyOptions: {
561
+ appendTo: () => root.value ?? document.body,
562
+ duration: 100,
563
+ maxWidth: "none",
564
+ placement: "top"
565
+ }
566
+ }, { default: () => renderToolbar(instance) }) : null,
567
+ instance ? h2(EditorContent, { editor: instance }) : null,
568
+ instance && props.editable && mobile.value && editorFocused.value ? h2("div", {
569
+ ref: mobileToolbarShell,
570
+ class: "looma-editor__mobile-toolbar-shell",
571
+ "data-mode": mobileToolbarMode.value,
572
+ style: mobileToolbarStyle.value,
573
+ role: "toolbar",
574
+ "aria-label": mobileToolbarMode.value === "table" ? "Table editing" : "Text formatting"
575
+ }, mobileToolbarMode.value === "table" ? [
576
+ h2("button", {
577
+ class: "looma-editor__mobile-toolbar-back",
578
+ type: "button",
579
+ "aria-label": "Formatting tools",
580
+ onClick: () => {
581
+ mobileTableControlsDismissed = true;
582
+ mobileToolbarMode.value = "formatting";
583
+ editor.value?.commands.focus();
584
+ nextTick(updateMobileViewport);
585
+ }
586
+ }, "\u2190 Formatting"),
587
+ h2(EditorTableToolbar, tableProps)
588
+ ] : [renderToolbar(instance)]) : null,
589
+ h2("input", {
590
+ ref: fileInput,
591
+ class: "looma-editor__file-input",
592
+ type: "file",
593
+ accept: "image/jpeg,image/png,image/gif,image/webp,image/svg+xml",
594
+ multiple: true,
595
+ tabindex: -1,
596
+ "aria-hidden": "true",
597
+ onChange: onFileChange
598
+ }),
599
+ dragOver.value ? h2("div", { class: "looma-editor__drop-overlay", "aria-hidden": "true" }, [
600
+ h2("span", { class: "looma-editor__drop-glyph" }, "\u25A7"),
601
+ h2("span", "Drop image to upload")
602
+ ]) : null,
603
+ tablePickerOpen.value ? h2("div", {
604
+ ref: tablePickerPopover,
605
+ class: "looma-editor__table-picker-popover",
606
+ style: tablePickerStyle.value
607
+ }, [h2(EditorInsertTableGrid, {
608
+ open: true,
609
+ onInsertTable: (detail) => {
610
+ instance?.chain().focus().insertTable(detail).run();
611
+ tablePickerOpen.value = false;
612
+ }
613
+ })]) : null,
614
+ slash.active && slash.items.length > 0 ? h2(EditorSlashMenu, {
615
+ open: true,
616
+ query: slash.query,
617
+ items: slash.items,
618
+ selectedIndex: slash.selectedIndex,
619
+ anchorRect: slash.rect,
620
+ onSlashMenuHighlight: ({ index }) => {
621
+ slash.selectedIndex = index;
622
+ },
623
+ onSlashMenuSelect: ({ index }) => {
624
+ slash.select?.(index);
625
+ }
626
+ }) : null,
627
+ tableUi.toolbarOpen ? h2("div", {
628
+ ref: tableToolbarShell,
629
+ class: "looma-editor__table-toolbar-shell",
630
+ style: tableUi.toolbarStyle
631
+ }, [h2(EditorTableToolbar, tableProps)]) : null,
632
+ tableUi.overlayOpen ? h2("div", {
633
+ ref: tableOverlayShell,
634
+ class: "looma-editor__table-overlay-shell",
635
+ style: tableUi.overlayStyle
636
+ }, [h2(EditorTableOverlay, {
637
+ open: true,
638
+ rows: tableUi.rows,
639
+ cols: tableUi.cols,
640
+ onTableOverlayAction: runOverlayAction
641
+ })]) : null,
642
+ tableUi.menuOpen ? h2("div", {
643
+ ref: tableMenuShell,
644
+ class: "looma-editor__table-menu-shell",
645
+ style: tableUi.menuStyle
646
+ }, [h2(EditorTableContextMenu, tableProps)]) : null
647
+ ]);
648
+ };
649
+ }
650
+ });
117
651
  export {
118
652
  EDITOR_ADAPTER_COMPONENT_TAG_MAP,
119
653
  EditorInsertTableGrid,
@@ -121,5 +655,6 @@ export {
121
655
  EditorTableContextMenu,
122
656
  EditorTableOverlay,
123
657
  EditorTableToolbar,
124
- EditorToolbar
658
+ EditorToolbar,
659
+ LoomaEditor
125
660
  };