@livepreso/react-plugin-textfield 0.6.3 → 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/CHANGELOG.json CHANGED
@@ -1,6 +1,52 @@
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
+ },
38
+ {
39
+ "version": "0.6.4",
40
+ "tag": "@livepreso/react-plugin-textfield_v0.6.4",
41
+ "date": "Tue, 27 Oct 2026 00:36:52 GMT",
42
+ "comments": {
43
+ "dependency": [
44
+ {
45
+ "comment": "Updating dependency \"@livepreso/react-plugin-ui\" to `1.1.2`"
46
+ }
47
+ ]
48
+ }
49
+ },
4
50
  {
5
51
  "version": "0.6.3",
6
52
  "tag": "@livepreso/react-plugin-textfield_v0.6.3",
package/CHANGELOG.md CHANGED
@@ -1,6 +1,24 @@
1
1
  # Change Log - @livepreso/react-plugin-textfield
2
2
 
3
- This log was last generated on Thu, 15 Oct 2026 22:46:44 GMT and should not be manually modified.
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.
17
+
18
+ ## 0.6.4
19
+ Tue, 27 Oct 2026 00:36:52 GMT
20
+
21
+ _Version update only_
4
22
 
5
23
  ## 0.6.3
6
24
  Thu, 15 Oct 2026 22:46:44 GMT
@@ -0,0 +1,7 @@
1
+ // Only the test run uses this. A deck's build reads the babel config at the deck root.
2
+ module.exports = {
3
+ presets: [
4
+ ["@babel/preset-env", { targets: { node: "current" } }],
5
+ ["@babel/preset-react", { runtime: "automatic" }],
6
+ ],
7
+ };
@@ -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
- (focused.closest(`.${style.bubbleMenu}`) ||
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) => new UnscaledBubbleMenuView({ view, ...options }),
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")) value = `https://${value}`;
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) window.clearTimeout(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) return result;
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) return [];
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) return sum + cellMinWidth;
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) return 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) return tr;
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) return tr;
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") return;
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) return acc;
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) return mappedAlignments[0];
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) return BORDER_SIDES;
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) this.syncAttrs(node);
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") return;
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) return false;
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) return false;
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) return false;
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) return false;
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) return false;
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) return false;
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) return { colwidth, width: null };
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) return 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 === "") return [];
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) return { "data-border-sides": null };
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) return null;
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) return editor.isActive("heading", { level });
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
- if (prevPos && editor.view.state.doc.nodeAt(prevPos)) {
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
+ });