@livepreso/react-plugin-textfield 0.6.4 → 1.0.0
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/.lintstagedrc.js +4 -4
- package/.rush/temp/shrinkwrap-deps.json +344 -49
- package/CHANGELOG.json +34 -0
- package/CHANGELOG.md +14 -1
- package/babel.config.js +7 -0
- package/components/BubbleMenu.js +33 -4
- package/components/LinkEditDialog.js +3 -1
- package/components/editor-toolbars/SubMenu.js +3 -1
- package/components/tiptap/Table.js +54 -18
- package/components/tiptap/TableCells.js +12 -4
- package/configs/table-toolbar-configuration.js +3 -1
- package/configs/toolbar-configuration.js +8 -3
- package/configs/toolbar-configuration.spec.js +70 -0
- package/index.js +141 -54
- package/jest.config.js +8 -0
- package/package.json +10 -4
- package/permits-table.js +59 -0
- package/test/style-stub.js +1 -0
- package/text-actions.js +147 -0
- package/text-actions.spec.js +37 -0
- package/utils/generate-border-icon.js +6 -2
package/CHANGELOG.json
CHANGED
|
@@ -1,6 +1,40 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@livepreso/react-plugin-textfield",
|
|
3
3
|
"entries": [
|
|
4
|
+
{
|
|
5
|
+
"version": "1.0.0",
|
|
6
|
+
"tag": "@livepreso/react-plugin-textfield_v1.0.0",
|
|
7
|
+
"date": "Tue, 03 Nov 2026 22:02:25 GMT",
|
|
8
|
+
"comments": {
|
|
9
|
+
"minor": [
|
|
10
|
+
{
|
|
11
|
+
"comment": "Adds text actions, so the deck interface can read and change a text field."
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"comment": "The toolbar a text field declares decides what the field permits, and the build reads it."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"comment": "A heading field permits the tag it is drawn as, so a person's edit to an <h4> field is no longer refused."
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
"major": [
|
|
21
|
+
{
|
|
22
|
+
"comment": "ManagedTextField stores one value per placement of a slide by default. The scope prop set to \"deck\" shares one value instead."
|
|
23
|
+
}
|
|
24
|
+
],
|
|
25
|
+
"dependency": [
|
|
26
|
+
{
|
|
27
|
+
"comment": "Updating dependency \"@livepreso/content-react\" to `3.0.0`"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"comment": "Updating dependency \"@livepreso/webpack-deck\" to `4.0.0`"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"comment": "Updating dependency \"@livepreso/react-plugin-ui\" to `2.0.0`"
|
|
34
|
+
}
|
|
35
|
+
]
|
|
36
|
+
}
|
|
37
|
+
},
|
|
4
38
|
{
|
|
5
39
|
"version": "0.6.4",
|
|
6
40
|
"tag": "@livepreso/react-plugin-textfield_v0.6.4",
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# Change Log - @livepreso/react-plugin-textfield
|
|
2
2
|
|
|
3
|
-
This log was last generated on Tue,
|
|
3
|
+
This log was last generated on Tue, 03 Nov 2026 22:02:25 GMT and should not be manually modified.
|
|
4
|
+
|
|
5
|
+
## 1.0.0
|
|
6
|
+
Tue, 03 Nov 2026 22:02:25 GMT
|
|
7
|
+
|
|
8
|
+
### Breaking changes
|
|
9
|
+
|
|
10
|
+
- ManagedTextField stores one value per placement of a slide by default. The scope prop set to "deck" shares one value instead.
|
|
11
|
+
|
|
12
|
+
### Minor changes
|
|
13
|
+
|
|
14
|
+
- Adds text actions, so the deck interface can read and change a text field.
|
|
15
|
+
- The toolbar a text field declares decides what the field permits, and the build reads it.
|
|
16
|
+
- A heading field permits the tag it is drawn as, so a person's edit to an <h4> field is no longer refused.
|
|
4
17
|
|
|
5
18
|
## 0.6.4
|
|
6
19
|
Tue, 27 Oct 2026 00:36:52 GMT
|
package/babel.config.js
ADDED
package/components/BubbleMenu.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useEffect, useRef } from "react";
|
|
1
|
+
import React, { useEffect, useRef, useState } from "react";
|
|
2
2
|
|
|
3
3
|
import { BubbleMenuView } from "@tiptap/extension-bubble-menu";
|
|
4
4
|
import { useCurrentEditor } from "@tiptap/react";
|
|
@@ -17,6 +17,8 @@ class UnscaledBubbleMenuView extends BubbleMenuView {
|
|
|
17
17
|
constructor(options) {
|
|
18
18
|
super(options);
|
|
19
19
|
|
|
20
|
+
this.onVisibilityChange = options.onVisibilityChange;
|
|
21
|
+
|
|
20
22
|
// The presenter rescales on container resize without a window resize event.
|
|
21
23
|
window.Bridge?.Event?.on("resize", this.resizeHandler, "deck");
|
|
22
24
|
|
|
@@ -76,7 +78,8 @@ class UnscaledBubbleMenuView extends BubbleMenuView {
|
|
|
76
78
|
// A toolbar of this editor, or a popover/select opened from one.
|
|
77
79
|
if (
|
|
78
80
|
focused instanceof Element &&
|
|
79
|
-
|
|
81
|
+
// A production class name can contain `=`, which is invalid unescaped.
|
|
82
|
+
(focused.closest(`.${CSS.escape(style.bubbleMenu)}`) ||
|
|
80
83
|
focused.closest(POPPER_CONTENT_SELECTOR))
|
|
81
84
|
) {
|
|
82
85
|
return;
|
|
@@ -101,6 +104,7 @@ class UnscaledBubbleMenuView extends BubbleMenuView {
|
|
|
101
104
|
this.element.parentNode?.appendChild(this.element);
|
|
102
105
|
|
|
103
106
|
this.isVisible = true;
|
|
107
|
+
this.onVisibilityChange?.(true);
|
|
104
108
|
}
|
|
105
109
|
|
|
106
110
|
hide() {
|
|
@@ -111,6 +115,7 @@ class UnscaledBubbleMenuView extends BubbleMenuView {
|
|
|
111
115
|
this.element.style.visibility = "hidden";
|
|
112
116
|
this.element.style.opacity = "0";
|
|
113
117
|
this.isVisible = false;
|
|
118
|
+
this.onVisibilityChange?.(false);
|
|
114
119
|
}
|
|
115
120
|
|
|
116
121
|
updatePosition() {
|
|
@@ -185,7 +190,13 @@ export const UnscaledBubbleMenuPlugin = (options) => {
|
|
|
185
190
|
typeof options.pluginKey === "string"
|
|
186
191
|
? new PluginKey(options.pluginKey)
|
|
187
192
|
: options.pluginKey,
|
|
188
|
-
view: (view) =>
|
|
193
|
+
view: (view) => {
|
|
194
|
+
const menu = new UnscaledBubbleMenuView({ view, ...options });
|
|
195
|
+
|
|
196
|
+
options.onMenuCreated?.(menu);
|
|
197
|
+
|
|
198
|
+
return menu;
|
|
199
|
+
},
|
|
189
200
|
});
|
|
190
201
|
};
|
|
191
202
|
|
|
@@ -200,10 +211,23 @@ export const BubbleMenu = ({
|
|
|
200
211
|
...restProps
|
|
201
212
|
}) => {
|
|
202
213
|
const menuEl = useRef();
|
|
214
|
+
const menu = useRef(null);
|
|
215
|
+
|
|
216
|
+
// Only the focused field shows a toolbar, so its few hundred nodes wait until then.
|
|
217
|
+
const [isShown, setIsShown] = useState(false);
|
|
203
218
|
|
|
204
219
|
const { entered } = useTransitions();
|
|
205
220
|
const { editor: currentEditor } = useCurrentEditor();
|
|
206
221
|
|
|
222
|
+
useEffect(() => {
|
|
223
|
+
if (!isShown) {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// The toolbar had no size until now, so the placement taken on show was wrong.
|
|
228
|
+
menu.current?.updatePosition();
|
|
229
|
+
}, [isShown]);
|
|
230
|
+
|
|
207
231
|
useEffect(() => {
|
|
208
232
|
const bubbleMenuElement = menuEl.current;
|
|
209
233
|
|
|
@@ -236,12 +260,17 @@ export const BubbleMenu = ({
|
|
|
236
260
|
pluginKey,
|
|
237
261
|
shouldShow,
|
|
238
262
|
options,
|
|
263
|
+
onVisibilityChange: setIsShown,
|
|
264
|
+
onMenuCreated: (created) => {
|
|
265
|
+
menu.current = created;
|
|
266
|
+
},
|
|
239
267
|
});
|
|
240
268
|
|
|
241
269
|
attachToEditor.registerPlugin(plugin);
|
|
242
270
|
|
|
243
271
|
return () => {
|
|
244
272
|
attachToEditor.unregisterPlugin(pluginKey);
|
|
273
|
+
menu.current = null;
|
|
245
274
|
};
|
|
246
275
|
}, [entered, editor?.isDestroyed]);
|
|
247
276
|
|
|
@@ -257,7 +286,7 @@ export const BubbleMenu = ({
|
|
|
257
286
|
className={`${style.bubbleMenu} interactive`}
|
|
258
287
|
{...restProps}
|
|
259
288
|
>
|
|
260
|
-
{children}
|
|
289
|
+
{isShown ? children : null}
|
|
261
290
|
</div>
|
|
262
291
|
</OverlayPortal>
|
|
263
292
|
);
|
|
@@ -73,7 +73,9 @@ const LinkEditDialog = ({
|
|
|
73
73
|
value={urlInput}
|
|
74
74
|
onChange={(e) => {
|
|
75
75
|
let value = e.target.value;
|
|
76
|
-
if (!value.startsWith("http"))
|
|
76
|
+
if (!value.startsWith("http")) {
|
|
77
|
+
value = `https://${value}`;
|
|
78
|
+
}
|
|
77
79
|
setUrlInput(value);
|
|
78
80
|
}}
|
|
79
81
|
onKeyDown={checkForEnter}
|
|
@@ -34,7 +34,9 @@ export function SubMenu({
|
|
|
34
34
|
const closeTimerRef = useRef(null);
|
|
35
35
|
|
|
36
36
|
const cancelClose = useCallback(() => {
|
|
37
|
-
if (closeTimerRef.current)
|
|
37
|
+
if (closeTimerRef.current) {
|
|
38
|
+
window.clearTimeout(closeTimerRef.current);
|
|
39
|
+
}
|
|
38
40
|
closeTimerRef.current = null;
|
|
39
41
|
}, []);
|
|
40
42
|
|
|
@@ -54,7 +54,9 @@ export function findCellsInSelection(state) {
|
|
|
54
54
|
if (selection instanceof CellSelection) {
|
|
55
55
|
const table = findTable(state);
|
|
56
56
|
|
|
57
|
-
if (!table)
|
|
57
|
+
if (!table) {
|
|
58
|
+
return result;
|
|
59
|
+
}
|
|
58
60
|
const cells = [];
|
|
59
61
|
selection.forEachCell((node) => {
|
|
60
62
|
cells.push(node);
|
|
@@ -88,7 +90,9 @@ function getColwidths({
|
|
|
88
90
|
let hasColwidth = false;
|
|
89
91
|
// Get the first row to determine the number of columns
|
|
90
92
|
const firstRow = tableNode.firstChild;
|
|
91
|
-
if (!firstRow)
|
|
93
|
+
if (!firstRow) {
|
|
94
|
+
return [];
|
|
95
|
+
}
|
|
92
96
|
|
|
93
97
|
let colwidths = [];
|
|
94
98
|
// Iterate through cells in the first row to calculate total width
|
|
@@ -99,7 +103,9 @@ function getColwidths({
|
|
|
99
103
|
colwidths.push(
|
|
100
104
|
cell.attrs.colwidth.reduce((sum, width) => {
|
|
101
105
|
// colwidth 0, return min cell width
|
|
102
|
-
if (!width)
|
|
106
|
+
if (!width) {
|
|
107
|
+
return sum + cellMinWidth;
|
|
108
|
+
}
|
|
103
109
|
return sum + width;
|
|
104
110
|
}, 0),
|
|
105
111
|
);
|
|
@@ -133,7 +139,9 @@ function getTableWidth({ tableNode, cellMinWidth = CELL_MIN_WIDTH }) {
|
|
|
133
139
|
const tableMap = TableMap.get(tableNode);
|
|
134
140
|
const columnCount = tableMap.width;
|
|
135
141
|
|
|
136
|
-
if (columnCount === 0)
|
|
142
|
+
if (columnCount === 0) {
|
|
143
|
+
return 0;
|
|
144
|
+
}
|
|
137
145
|
|
|
138
146
|
// Calculate total width from existing column widths
|
|
139
147
|
const currColwidths = getColwidths({
|
|
@@ -172,7 +180,9 @@ function setColumnWidths({
|
|
|
172
180
|
const tableMap = TableMap.get(tableNode);
|
|
173
181
|
const columnCount = tableMap.width;
|
|
174
182
|
|
|
175
|
-
if (columnCount === 0)
|
|
183
|
+
if (columnCount === 0) {
|
|
184
|
+
return tr;
|
|
185
|
+
}
|
|
176
186
|
|
|
177
187
|
// Calculate total width from existing column widths
|
|
178
188
|
const currColwidths = getColwidths({
|
|
@@ -181,7 +191,9 @@ function setColumnWidths({
|
|
|
181
191
|
columnCount,
|
|
182
192
|
});
|
|
183
193
|
|
|
184
|
-
if (!currColwidths.length)
|
|
194
|
+
if (!currColwidths.length) {
|
|
195
|
+
return tr;
|
|
196
|
+
}
|
|
185
197
|
|
|
186
198
|
const currWidth = currColwidths.reduce((sum, width) => sum + width, 0);
|
|
187
199
|
|
|
@@ -208,7 +220,9 @@ function setColumnWidths({
|
|
|
208
220
|
|
|
209
221
|
// Process each row in the table
|
|
210
222
|
tableNode.forEach((row) => {
|
|
211
|
-
if (row.type.name !== "tableRow")
|
|
223
|
+
if (row.type.name !== "tableRow") {
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
212
226
|
|
|
213
227
|
let cellPos = rowPos + 1; // Start at the first cell in this row
|
|
214
228
|
let colIndex = 0;
|
|
@@ -246,7 +260,9 @@ function getCellAttributes(editor, attributeName) {
|
|
|
246
260
|
const selectedCells = findCellsInSelection(editor.state);
|
|
247
261
|
const foundValues = selectedCells.reduce((acc, cell) => {
|
|
248
262
|
const newVal = cell.attrs[attributeName];
|
|
249
|
-
if (acc.indexOf(newVal) > -1)
|
|
263
|
+
if (acc.indexOf(newVal) > -1) {
|
|
264
|
+
return acc;
|
|
265
|
+
}
|
|
250
266
|
return [...acc, newVal];
|
|
251
267
|
}, []);
|
|
252
268
|
return foundValues;
|
|
@@ -259,7 +275,9 @@ export function getCellAlignment(editor) {
|
|
|
259
275
|
const mappedAlignments = alignments
|
|
260
276
|
.map((v) => v || CELL_VALIGN_TOP)
|
|
261
277
|
.filter((v, index, all) => all.indexOf(v) === index);
|
|
262
|
-
if (mappedAlignments.length === 1)
|
|
278
|
+
if (mappedAlignments.length === 1) {
|
|
279
|
+
return mappedAlignments[0];
|
|
280
|
+
}
|
|
263
281
|
return CELL_VALIGN_MIXED;
|
|
264
282
|
}
|
|
265
283
|
|
|
@@ -271,7 +289,9 @@ export function getCellAlignment(editor) {
|
|
|
271
289
|
export function getCellBorderSides(editor) {
|
|
272
290
|
const values = getCellAttributes(editor, "borderSides");
|
|
273
291
|
const mappedValues = values.map((v) => v || BORDER_SIDES);
|
|
274
|
-
if (mappedValues.length === 0)
|
|
292
|
+
if (mappedValues.length === 0) {
|
|
293
|
+
return BORDER_SIDES;
|
|
294
|
+
}
|
|
275
295
|
|
|
276
296
|
return BORDER_SIDES.filter((side) =>
|
|
277
297
|
mappedValues.every((sides) => sides.includes(side)),
|
|
@@ -320,7 +340,9 @@ class TableViewWithAttrs extends TableView {
|
|
|
320
340
|
|
|
321
341
|
update(node) {
|
|
322
342
|
const updated = super.update(node);
|
|
323
|
-
if (updated)
|
|
343
|
+
if (updated) {
|
|
344
|
+
this.syncAttrs(node);
|
|
345
|
+
}
|
|
324
346
|
return updated;
|
|
325
347
|
}
|
|
326
348
|
|
|
@@ -330,7 +352,9 @@ class TableViewWithAttrs extends TableView {
|
|
|
330
352
|
Object.entries(attrs).forEach(([key, value]) => {
|
|
331
353
|
// width/min-width is already recalculated more precisely by
|
|
332
354
|
// updateColumns() above, from live column widths - don't clobber it.
|
|
333
|
-
if (key === "style")
|
|
355
|
+
if (key === "style") {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
334
358
|
|
|
335
359
|
if (value === null || value === undefined) {
|
|
336
360
|
this.table.removeAttribute(key);
|
|
@@ -395,7 +419,9 @@ export const TableExtended = TableBase.extend({
|
|
|
395
419
|
({ state, tr, dispatch, editor }) => {
|
|
396
420
|
const table = findTable(state);
|
|
397
421
|
|
|
398
|
-
if (!table)
|
|
422
|
+
if (!table) {
|
|
423
|
+
return false;
|
|
424
|
+
}
|
|
399
425
|
|
|
400
426
|
const tableDom = editor.editorView.domAtPos(table.pos);
|
|
401
427
|
|
|
@@ -423,7 +449,9 @@ export const TableExtended = TableBase.extend({
|
|
|
423
449
|
({ state, tr, dispatch, editor }) => {
|
|
424
450
|
const table = findTable(state);
|
|
425
451
|
|
|
426
|
-
if (!table)
|
|
452
|
+
if (!table) {
|
|
453
|
+
return false;
|
|
454
|
+
}
|
|
427
455
|
|
|
428
456
|
const tableDom = editor.editorView.domAtPos(table.pos);
|
|
429
457
|
|
|
@@ -452,7 +480,9 @@ export const TableExtended = TableBase.extend({
|
|
|
452
480
|
({ state, tr, dispatch }) => {
|
|
453
481
|
const table = findTable(state);
|
|
454
482
|
|
|
455
|
-
if (!table)
|
|
483
|
+
if (!table) {
|
|
484
|
+
return false;
|
|
485
|
+
}
|
|
456
486
|
|
|
457
487
|
if (dispatch) {
|
|
458
488
|
dispatch(
|
|
@@ -470,7 +500,9 @@ export const TableExtended = TableBase.extend({
|
|
|
470
500
|
({ state, tr, dispatch, editor }) => {
|
|
471
501
|
const table = findTable(state);
|
|
472
502
|
|
|
473
|
-
if (!table)
|
|
503
|
+
if (!table) {
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
474
506
|
|
|
475
507
|
const tableDom = editor.editorView.domAtPos(table.pos);
|
|
476
508
|
const tableWidth = getTableWidth({
|
|
@@ -506,7 +538,9 @@ export const TableExtended = TableBase.extend({
|
|
|
506
538
|
({ state, tr, dispatch }) => {
|
|
507
539
|
const table = findTable(state);
|
|
508
540
|
|
|
509
|
-
if (!table)
|
|
541
|
+
if (!table) {
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
510
544
|
|
|
511
545
|
if (dispatch) {
|
|
512
546
|
dispatch(
|
|
@@ -524,7 +558,9 @@ export const TableExtended = TableBase.extend({
|
|
|
524
558
|
({ tr, state, dispatch }) => {
|
|
525
559
|
const cells = findCellsInSelection(state);
|
|
526
560
|
|
|
527
|
-
if (cells.length === 0)
|
|
561
|
+
if (cells.length === 0) {
|
|
562
|
+
return false;
|
|
563
|
+
}
|
|
528
564
|
|
|
529
565
|
const newTr = tr.setNodes(
|
|
530
566
|
cells.map(({ node }) => ({
|
|
@@ -39,7 +39,9 @@ const tableCellAttributes = {
|
|
|
39
39
|
return element.getAttribute("width") || null;
|
|
40
40
|
},
|
|
41
41
|
renderHTML: ({ width, colwidth }) => {
|
|
42
|
-
if (colwidth)
|
|
42
|
+
if (colwidth) {
|
|
43
|
+
return { colwidth, width: null };
|
|
44
|
+
}
|
|
43
45
|
|
|
44
46
|
return {
|
|
45
47
|
width,
|
|
@@ -88,13 +90,19 @@ const tableCellAttributes = {
|
|
|
88
90
|
default: null,
|
|
89
91
|
parseHTML: (element) => {
|
|
90
92
|
const value = element.getAttribute("data-border-sides");
|
|
91
|
-
if (value === null)
|
|
93
|
+
if (value === null) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
92
96
|
// `""` is only produced by builds predating BORDER_SIDES_NONE.
|
|
93
|
-
if (value === BORDER_SIDES_NONE || value === "")
|
|
97
|
+
if (value === BORDER_SIDES_NONE || value === "") {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
94
100
|
return value.split(",").filter((side) => BORDER_SIDES.includes(side));
|
|
95
101
|
},
|
|
96
102
|
renderHTML: ({ borderSides }) => {
|
|
97
|
-
if (!borderSides)
|
|
103
|
+
if (!borderSides) {
|
|
104
|
+
return { "data-border-sides": null };
|
|
105
|
+
}
|
|
98
106
|
|
|
99
107
|
const hiddenSides = BORDER_SIDES.filter(
|
|
100
108
|
(side) => !borderSides.includes(side),
|
|
@@ -198,7 +198,9 @@ const cellValignPicker = () => ({
|
|
|
198
198
|
items: [{ id: "vertical-align" }],
|
|
199
199
|
value: (editor) => {
|
|
200
200
|
const cells = findCellsInSelection(editor.state);
|
|
201
|
-
if (!cells)
|
|
201
|
+
if (!cells) {
|
|
202
|
+
return null;
|
|
203
|
+
}
|
|
202
204
|
return cells[0]?.attrs?.verticalAlign;
|
|
203
205
|
},
|
|
204
206
|
renderItem: (item, { editor, value } = {}) => (
|
|
@@ -33,7 +33,9 @@ function generateTextStyleTest(textStyles) {
|
|
|
33
33
|
);
|
|
34
34
|
const match = sortedTextStyles.find((ts) => {
|
|
35
35
|
const level = Number(ts.block);
|
|
36
|
-
if (level > 0)
|
|
36
|
+
if (level > 0) {
|
|
37
|
+
return editor.isActive("heading", { level });
|
|
38
|
+
}
|
|
37
39
|
return editor.isActive("paragraph");
|
|
38
40
|
});
|
|
39
41
|
return match?.block;
|
|
@@ -74,8 +76,9 @@ const textStyleSelect = ({ textStyles = [] } = {}) => ({
|
|
|
74
76
|
|
|
75
77
|
const onChange = (value) => {
|
|
76
78
|
const level = Number(value);
|
|
77
|
-
if (!value || level === 0)
|
|
79
|
+
if (!value || level === 0) {
|
|
78
80
|
return editor.chain().focus().setParagraph().run();
|
|
81
|
+
}
|
|
79
82
|
|
|
80
83
|
return editor
|
|
81
84
|
.chain()
|
|
@@ -301,7 +304,9 @@ const insertTokenButton = ({ tokens = [] }) =>
|
|
|
301
304
|
|
|
302
305
|
// Add a space before the token trigger if necessary.
|
|
303
306
|
// Without a leading space the token functionality won't run.
|
|
304
|
-
|
|
307
|
+
// Selecting the whole field starts it at 0, and prosemirror
|
|
308
|
+
// throws on the position before that.
|
|
309
|
+
if (prevPos > 0 && editor.view.state.doc.nodeAt(prevPos)) {
|
|
305
310
|
const charBefore = editor.view.state.doc.textBetween(
|
|
306
311
|
prevPos,
|
|
307
312
|
$from.pos,
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { PRIMARY_TOOLBAR_IDS } from "../constants";
|
|
2
|
+
import { primaryItemGenerators } from "./toolbar-configuration";
|
|
3
|
+
|
|
4
|
+
function createEditor(
|
|
5
|
+
pos,
|
|
6
|
+
{ nodeAt = () => ({}), textBetween = () => "x" } = {},
|
|
7
|
+
) {
|
|
8
|
+
const inserted = [];
|
|
9
|
+
const chain = {
|
|
10
|
+
focus: () => chain,
|
|
11
|
+
insertContent: (content) => {
|
|
12
|
+
inserted.push(content);
|
|
13
|
+
return chain;
|
|
14
|
+
},
|
|
15
|
+
run: () => true,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
return {
|
|
19
|
+
inserted,
|
|
20
|
+
asked: [],
|
|
21
|
+
view: {
|
|
22
|
+
state: {
|
|
23
|
+
selection: { $from: { pos } },
|
|
24
|
+
doc: {
|
|
25
|
+
nodeAt(at) {
|
|
26
|
+
this.asked.push(at);
|
|
27
|
+
return nodeAt(at);
|
|
28
|
+
},
|
|
29
|
+
textBetween,
|
|
30
|
+
asked: [],
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
chain: () => chain,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function insertToken(editor) {
|
|
39
|
+
const button = primaryItemGenerators[PRIMARY_TOOLBAR_IDS.INSERT_TOKEN]({
|
|
40
|
+
tokens: [{ id: "date", label: "Date" }],
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return button.command(editor);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
describe("insertTokenButton", () => {
|
|
47
|
+
it("should insert the trigger alone after a space", () => {
|
|
48
|
+
const editor = createEditor(5, { textBetween: () => " " });
|
|
49
|
+
|
|
50
|
+
insertToken(editor);
|
|
51
|
+
|
|
52
|
+
expect(editor.inserted).toEqual(["@"]);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should lead with a space where another character precedes it", () => {
|
|
56
|
+
const editor = createEditor(5, { textBetween: () => "t" });
|
|
57
|
+
|
|
58
|
+
insertToken(editor);
|
|
59
|
+
|
|
60
|
+
expect(editor.inserted).toEqual([" @"]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should insert the trigger with the whole field selected", () => {
|
|
64
|
+
const editor = createEditor(0);
|
|
65
|
+
|
|
66
|
+
expect(() => insertToken(editor)).not.toThrow();
|
|
67
|
+
expect(editor.view.state.doc.asked).toEqual([]);
|
|
68
|
+
expect(editor.inserted).toEqual(["@"]);
|
|
69
|
+
});
|
|
70
|
+
});
|