@threadlabs/looma 0.28.10 → 0.28.11

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.
@@ -0,0 +1,9 @@
1
+ import type { Editor } from "@tiptap/core";
2
+ /**
3
+ * Applies a block menu action to the top-level block holding the caret, then focuses the editor:
4
+ * insert a paragraph below, duplicate the block, or delete it. Deleting the only block leaves an
5
+ * empty paragraph.
6
+ *
7
+ * @failure Returns false, and changes nothing, for an unknown action or a caret outside any block.
8
+ */
9
+ export declare function runBlockAction(editor: Editor, action: string): boolean;
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { Editor, Range } from "@tiptap/core";
6
6
  import type { TableContextMenuActionEventDetail } from "../table-context-menu.js";
7
- import type { TableOverlayActionEventDetail } from "../table-overlay.js";
7
+ import { type TableOverlayActionEventDetail } from "../table-overlay.js";
8
8
  import { type TableCellAlignment, type TableCellBackground } from "./table-formatting.js";
9
9
  /**
10
10
  * Bounded initial shape for table insertion.
@@ -120,3 +120,9 @@ export declare function normalizeActiveTableColumnWidths(editor: Editor, tableEl
120
120
  * return false before dispatching a transaction.
121
121
  */
122
122
  export declare function handleTableOverlayAction(editor: Editor, detail: TableOverlayActionEventDetail): boolean;
123
+ /**
124
+ * Moves the caret to the cell an overlay action names when the action comes from a table other
125
+ * than the selected one, so table commands act on that table. A cell menu also selects its cell,
126
+ * unless that cell is already part of the selection.
127
+ */
128
+ export declare function focusTableOverlayTarget(editor: Editor, table: HTMLTableElement, detail: TableOverlayActionEventDetail): void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadlabs/looma",
3
- "version": "0.28.10",
3
+ "version": "0.28.11",
4
4
  "type": "module",
5
5
  "description": "Looma components as HTML, Vue, and plain DOM, with design tokens.",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  import { computed, createApp, defineComponent, h, nextTick, onBeforeUnmount, onMounted, reactive, ref, shallowRef, useId, watch } from "vue";
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 } from "@threadlabs/looma/editor";
2
+ import { DEFAULT_MENTION_RESULT_LIMIT, LOOMA_CHIP_COLORS, LOOMA_ICONS, createLoomaMentionExtension, createLoomaSlashCommandExtension, getActiveTableUiState, getDefaultEditorExtensions, getDefaultSlashCommands, getTableOfContentsEntries, getVisualViewportRect, handleTableAction, handleTableOverlayAction, measureTableOverlayGeometry, normalizeActiveTableColumnWidths, normalizeLoomaChipColor, normalizeTableOfContentsConfig } from "@threadlabs/looma/editor";
3
3
  import { Button, Callout, Card, Checkbox, Cluster, Disclosure, EditorBlockActions, EditorCodeLanguage, EditorImageToolbar, EditorInsertTableGrid, EditorLinkCard, EditorLinkForm, EditorMentionMenu, EditorMentionMenuItem, EditorSlashMenu, EditorSlashMenuGroup, EditorSlashMenuItem, EditorSurface, EditorTableContextMenu, EditorTableOverlay, EditorTableToolbar, EditorToolbar, FormField, Icon, IconButton, Image, Input, List, Menu, MenuItem, Popover, Separator, Stack, Text, Tooltip } from "@threadlabs/looma/vue";
4
4
  import { Editor, Extension, Node as Node$1, getText, getTextSerializersFromSchema, isNodeSelection, mergeAttributes, nodeInputRule, posToDOMRect } from "@tiptap/core";
5
5
  import { Editor as Editor$1 } from "@tiptap/vue-3";
@@ -7,6 +7,7 @@ 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";
9
9
  import { createLowlight } from "lowlight";
10
+ import "@tiptap/pm/tables";
10
11
  import "@tiptap/pm/transform";
11
12
  import { DOMParser, DOMSerializer, Fragment as Fragment$1 } from "@tiptap/pm/model";
12
13
  export * from "@threadlabs/looma/editor";
@@ -1094,6 +1095,125 @@ function createCodeHighlighting(codeLanguages, onGrammarsLoaded) {
1094
1095
  };
1095
1096
  }
1096
1097
  //#endregion
1098
+ //#region src/editor/block-actions.ts
1099
+ /**
1100
+ * Applies a block menu action to the top-level block holding the caret, then focuses the editor:
1101
+ * insert a paragraph below, duplicate the block, or delete it. Deleting the only block leaves an
1102
+ * empty paragraph.
1103
+ *
1104
+ * @failure Returns false, and changes nothing, for an unknown action or a caret outside any block.
1105
+ */
1106
+ function runBlockAction(editor, action) {
1107
+ const head = editor.state.selection.$head;
1108
+ if (head.depth === 0) return false;
1109
+ const { state } = editor;
1110
+ const position = head.before(1);
1111
+ const block = state.doc.nodeAt(position);
1112
+ if (!block) return false;
1113
+ const end = position + block.nodeSize;
1114
+ const transaction = state.tr;
1115
+ let focusAt = end + 1;
1116
+ if (action === "insert-below") transaction.insert(end, state.schema.nodes.paragraph.create());
1117
+ else if (action === "duplicate") transaction.insert(end, block);
1118
+ else if (action === "delete") {
1119
+ if (state.doc.childCount === 1) {
1120
+ transaction.replaceWith(position, end, state.schema.nodes.paragraph.create());
1121
+ focusAt = 1;
1122
+ } else {
1123
+ transaction.delete(position, end);
1124
+ focusAt = Math.min(position + 1, transaction.doc.content.size);
1125
+ }
1126
+ } else return false;
1127
+ transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(focusAt, transaction.doc.content.size))));
1128
+ editor.view.dispatch(transaction);
1129
+ editor.commands.focus();
1130
+ return true;
1131
+ }
1132
+ //#endregion
1133
+ //#region src/editor/table-overlay.ts
1134
+ function mapTableCells(table) {
1135
+ const rows = Array.from(table.rows);
1136
+ const grid = [];
1137
+ const coordinates = /* @__PURE__ */ new Map();
1138
+ rows.forEach((row, rowIndex) => {
1139
+ grid[rowIndex] ??= [];
1140
+ let columnIndex = 0;
1141
+ for (const cell of Array.from(row.cells)) {
1142
+ while (grid[rowIndex]?.[columnIndex]) columnIndex += 1;
1143
+ coordinates.set(cell, {
1144
+ rowIndex,
1145
+ columnIndex
1146
+ });
1147
+ const rowSpan = cell.rowSpan === 0 ? rows.length - rowIndex : Math.max(1, cell.rowSpan);
1148
+ const columnSpan = Math.max(1, cell.colSpan);
1149
+ for (let rowOffset = 0; rowOffset < rowSpan; rowOffset += 1) {
1150
+ const gridRow = grid[rowIndex + rowOffset] ??= [];
1151
+ for (let columnOffset = 0; columnOffset < columnSpan; columnOffset += 1) gridRow[columnIndex + columnOffset] = cell;
1152
+ }
1153
+ columnIndex += columnSpan;
1154
+ }
1155
+ });
1156
+ return {
1157
+ grid,
1158
+ coordinates
1159
+ };
1160
+ }
1161
+ /**
1162
+ * Resolves a logical grid coordinate to its owning DOM cell.
1163
+ * A spanning cell can therefore be returned for more than one coordinate; this
1164
+ * is intentional and prevents overlay actions from inventing nonexistent cells.
1165
+ *
1166
+ * @contract Coordinates outside the expanded table grid return null; coordinates
1167
+ * covered by a span resolve to the single DOM cell that owns them.
1168
+ */
1169
+ function resolveTableCellAt(table, rowIndex, columnIndex) {
1170
+ return mapTableCells(table).grid[rowIndex]?.[columnIndex] ?? null;
1171
+ }
1172
+ //#endregion
1173
+ //#region src/editor/selection-geometry.ts
1174
+ /** A plain copy of a client rectangle, the anchor shape the slash and mention menus accept as a prop. */
1175
+ function menuAnchorRect(rect) {
1176
+ if (!rect) return null;
1177
+ return {
1178
+ left: rect.left,
1179
+ top: rect.top,
1180
+ right: rect.right,
1181
+ bottom: rect.bottom,
1182
+ x: rect.x,
1183
+ y: rect.y,
1184
+ width: rect.width,
1185
+ height: rect.height
1186
+ };
1187
+ }
1188
+ function selectionElement(editor) {
1189
+ const { node } = editor.view.domAtPos(editor.state.selection.from);
1190
+ return node instanceof HTMLElement ? node : node.parentElement;
1191
+ }
1192
+ /** The rendered table that holds the selection's start. */
1193
+ function selectedTableElement(editor) {
1194
+ return selectionElement(editor)?.closest("table");
1195
+ }
1196
+ /** The rendered cell that holds the selection's start. */
1197
+ function selectedTableCellElement(editor) {
1198
+ return selectionElement(editor)?.closest("td, th");
1199
+ }
1200
+ //#endregion
1201
+ //#region src/editor/extensions/table-commands.ts
1202
+ /**
1203
+ * Moves the caret to the cell an overlay action names when the action comes from a table other
1204
+ * than the selected one, so table commands act on that table. A cell menu also selects its cell,
1205
+ * unless that cell is already part of the selection.
1206
+ */
1207
+ function focusTableOverlayTarget(editor, table, detail) {
1208
+ const target = "rowIndex" in detail ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) : "fromIndex" in detail ? resolveTableCellAt(table, detail.action === "reorder-row" ? detail.fromIndex : 0, detail.action === "reorder-column" ? detail.fromIndex : 0) : resolveTableCellAt(table, detail.action.startsWith("add-row") ? Math.max(0, detail.boundaryIndex - 1) : 0, detail.action.startsWith("add-column") ? Math.max(0, detail.boundaryIndex - 1) : 0);
1209
+ if (target && selectedTableElement(editor) !== table) editor.chain().focus().setTextSelection(editor.view.posAtDOM(target, 0) + 1).run();
1210
+ if (detail.action !== "open-cell-menu") return;
1211
+ const selected = selectedTableCellElement(editor);
1212
+ const cell = resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) ?? selected;
1213
+ if (cell && selected !== cell && !cell.classList.contains("selectedCell")) editor.chain().focus().setTextSelection(editor.view.posAtDOM(cell, 0) + 1).run();
1214
+ else editor.commands.focus();
1215
+ }
1216
+ //#endregion
1097
1217
  //#region src/editor/content.ts
1098
1218
  /** Whether a controlled document already matches the editor, so applying it would only reset the selection. */
1099
1219
  function sameDocument(left, right) {
@@ -1389,34 +1509,6 @@ function createLinkEditing(options) {
1389
1509
  };
1390
1510
  }
1391
1511
  //#endregion
1392
- //#region src/editor/selection-geometry.ts
1393
- /** A plain copy of a client rectangle, the anchor shape the slash and mention menus accept as a prop. */
1394
- function menuAnchorRect(rect) {
1395
- if (!rect) return null;
1396
- return {
1397
- left: rect.left,
1398
- top: rect.top,
1399
- right: rect.right,
1400
- bottom: rect.bottom,
1401
- x: rect.x,
1402
- y: rect.y,
1403
- width: rect.width,
1404
- height: rect.height
1405
- };
1406
- }
1407
- function selectionElement(editor) {
1408
- const { node } = editor.view.domAtPos(editor.state.selection.from);
1409
- return node instanceof HTMLElement ? node : node.parentElement;
1410
- }
1411
- /** The rendered table that holds the selection's start. */
1412
- function selectedTableElement(editor) {
1413
- return selectionElement(editor)?.closest("table");
1414
- }
1415
- /** The rendered cell that holds the selection's start. */
1416
- function selectedTableCellElement(editor) {
1417
- return selectionElement(editor)?.closest("td, th");
1418
- }
1419
- //#endregion
1420
1512
  //#region src/editor/selection-surface.ts
1421
1513
  var surfaceSequence = 0;
1422
1514
  /**
@@ -3145,32 +3237,6 @@ var LoomaEditor = defineComponent({
3145
3237
  onRemove: () => linkEditing?.remove()
3146
3238
  });
3147
3239
  };
3148
- const runBlockAction = (value) => {
3149
- const instance = editor.value;
3150
- const head = instance?.state.selection.$head;
3151
- if (!instance || !head || head.depth === 0 || !props.editable) return;
3152
- const { state } = instance;
3153
- const position = head.before(1);
3154
- const block = state.doc.nodeAt(position);
3155
- if (!block) return;
3156
- const end = position + block.nodeSize;
3157
- const transaction = state.tr;
3158
- let focusAt = end + 1;
3159
- if (value === "insert-below") transaction.insert(end, state.schema.nodes.paragraph.create());
3160
- else if (value === "duplicate") transaction.insert(end, block);
3161
- else if (value === "delete") {
3162
- if (state.doc.childCount === 1) {
3163
- transaction.replaceWith(position, end, state.schema.nodes.paragraph.create());
3164
- focusAt = 1;
3165
- } else {
3166
- transaction.delete(position, end);
3167
- focusAt = Math.min(position + 1, transaction.doc.content.size);
3168
- }
3169
- } else return;
3170
- transaction.setSelection(TextSelection.near(transaction.doc.resolve(Math.min(focusAt, transaction.doc.content.size))));
3171
- instance.view.dispatch(transaction);
3172
- instance.commands.focus();
3173
- };
3174
3240
  watch([() => props.slashCommands, () => props.uploadImage], () => {
3175
3241
  slashExtension.options.commands = resolveSlashCommands();
3176
3242
  editor.value?.commands.dismissLoomaSlashMenu();
@@ -3760,13 +3826,8 @@ var LoomaEditor = defineComponent({
3760
3826
  if (!instance) return;
3761
3827
  const table = overlayTableElement ?? selectedTableElement(instance);
3762
3828
  if (!table) return;
3763
- const targetCell = "rowIndex" in detail ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) : "fromIndex" in detail ? resolveTableCellAt(table, detail.action === "reorder-row" ? detail.fromIndex : 0, detail.action === "reorder-column" ? detail.fromIndex : 0) : resolveTableCellAt(table, detail.action.startsWith("add-row") ? Math.max(0, detail.boundaryIndex - 1) : 0, detail.action.startsWith("add-column") ? Math.max(0, detail.boundaryIndex - 1) : 0);
3764
- if (targetCell && selectedTableElement(instance) !== table) instance.chain().focus().setTextSelection(instance.view.posAtDOM(targetCell, 0) + 1).run();
3829
+ focusTableOverlayTarget(instance, table, detail);
3765
3830
  if (detail.action === "open-cell-menu") {
3766
- const selectedCell = selectedTableCellElement(instance);
3767
- const cell = table ? resolveTableCellAt(table, detail.rowIndex, detail.columnIndex) ?? selectedCell : selectedCell;
3768
- if (cell && selectedCell !== cell && !cell.classList.contains("selectedCell")) instance.chain().focus().setTextSelection(instance.view.posAtDOM(cell, 0) + 1).run();
3769
- else instance.commands.focus();
3770
3831
  updateTableUi();
3771
3832
  tableUi.menuScope = "cell";
3772
3833
  tableUi.menuStyle = {
@@ -4276,7 +4337,9 @@ var LoomaEditor = defineComponent({
4276
4337
  h(EditorBlockActions, {
4277
4338
  for: blockActionAnchorId,
4278
4339
  placement: mobile.value ? "top-start" : "bottom-start",
4279
- onAction: (event) => runBlockAction(event.detail.action)
4340
+ onAction: (event) => {
4341
+ if (props.editable && editor.value) runBlockAction(editor.value, event.detail.action);
4342
+ }
4280
4343
  }),
4281
4344
  h(Popover, {
4282
4345
  class: "looma-editor__table-picker-popover",