@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
|
|
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
package/vue/editor/index.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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) =>
|
|
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",
|