@livepreso/react-plugin-textfield 0.5.1 → 0.6.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.
Files changed (177) hide show
  1. package/.rush/temp/shrinkwrap-deps.json +2 -2
  2. package/CHANGELOG.json +37 -0
  3. package/CHANGELOG.md +15 -1
  4. package/components/BorderEdgeToggle.js +72 -0
  5. package/components/BubbleMenu.js +107 -19
  6. package/components/BubbleMenu.module.scss +0 -1
  7. package/components/InsertTokenDialog.js +1 -1
  8. package/components/InsertTokenDialog.module.scss +1 -1
  9. package/components/LinkEditDialog.js +2 -2
  10. package/components/LinkEditDialog.module.scss +1 -1
  11. package/components/TableDensityPicker.js +48 -0
  12. package/components/{color-picker/ColorPicker.module.scss → TableDensityPicker.module.scss} +4 -3
  13. package/components/TableToolbar.js +4 -0
  14. package/components/VerticalAlignToggle.js +8 -8
  15. package/components/color-picker/ColorPickerCombo.js +5 -5
  16. package/components/color-picker/ColorPickerCombo.module.scss +1 -1
  17. package/components/editor-toolbars/EditorMenu.js +2 -1
  18. package/components/editor-toolbars/EditorMenu.module.scss +1 -1
  19. package/components/editor-toolbars/EditorToolbar.js +10 -7
  20. package/components/editor-toolbars/EditorToolbar.module.scss +11 -68
  21. package/components/editor-toolbars/SimpleButton.module.scss +1 -1
  22. package/components/editor-toolbars/SubMenu.js +59 -3
  23. package/components/table-creator/TableCreatorCombo.js +4 -4
  24. package/components/table-creator/TableCreatorCombo.module.scss +1 -1
  25. package/components/tiptap/Table.js +172 -5
  26. package/components/tiptap/TableCells.js +33 -0
  27. package/components/tiptap/token/Token.module.scss +1 -1
  28. package/components/tiptap/token/TokenList.module.scss +1 -1
  29. package/components/utils.js +0 -49
  30. package/configs/table-toolbar-configuration.js +87 -8
  31. package/configs/toolbar-configuration.js +3 -3
  32. package/constants.js +16 -1
  33. package/index.js +11 -4
  34. package/index.module.scss +63 -22
  35. package/package.json +3 -4
  36. package/utils/generate-border-icon.js +22 -0
  37. package/utils/generate-vertical-alignment-icon.js +1 -1
  38. package/components/Popover.js +0 -43
  39. package/components/Popover.module.scss +0 -80
  40. package/components/Select/Select.js +0 -71
  41. package/components/Select/Select.module.scss +0 -100
  42. package/components/Select/SelectGroup.js +0 -37
  43. package/components/Select/index.js +0 -1
  44. package/components/Tooltip.js +0 -34
  45. package/components/Tooltip.module.scss +0 -87
  46. package/components/color-picker/ColorPicker.js +0 -28
  47. package/components/color-picker/ColorPickerChip.js +0 -22
  48. package/components/color-picker/ColorPickerChip.module.scss +0 -25
  49. package/components/editor-toolbars/ToolbarButton.js +0 -26
  50. package/components/editor-toolbars/ToolbarToggle.js +0 -35
  51. package/components/editor-toolbars/ToolbarToggleGroup.js +0 -43
  52. package/components/style.module.scss +0 -63
  53. package/icons/AddColumnLeft.js +0 -15
  54. package/icons/AddColumnRight.js +0 -15
  55. package/icons/AddRowAbove.js +0 -15
  56. package/icons/AddRowBelow.js +0 -15
  57. package/icons/AlignHorizontalCenter.js +0 -13
  58. package/icons/AlignHorizontalLeft.js +0 -13
  59. package/icons/AlignHorizontalRight.js +0 -13
  60. package/icons/AlignVerticalBottom.js +0 -13
  61. package/icons/AlignVerticalCenter.js +0 -13
  62. package/icons/AlignVerticalTop.js +0 -13
  63. package/icons/Backspace.js +0 -13
  64. package/icons/Bold.js +0 -14
  65. package/icons/BorderAll.js +0 -13
  66. package/icons/BorderBottom.js +0 -13
  67. package/icons/BorderClear.js +0 -13
  68. package/icons/BorderHorizontal.js +0 -13
  69. package/icons/BorderInner.js +0 -13
  70. package/icons/BorderLeft.js +0 -13
  71. package/icons/BorderOuter.js +0 -13
  72. package/icons/BorderRight.js +0 -13
  73. package/icons/BorderTop.js +0 -13
  74. package/icons/BorderVertical.js +0 -13
  75. package/icons/Close.js +0 -13
  76. package/icons/Delete.js +0 -13
  77. package/icons/EvenlyDistribute.js +0 -14
  78. package/icons/FitWidth.js +0 -13
  79. package/icons/FitWidthArrows.js +0 -21
  80. package/icons/FormatAlignCenter.js +0 -13
  81. package/icons/FormatAlignJustify.js +0 -13
  82. package/icons/FormatAlignLeft.js +0 -13
  83. package/icons/FormatAlignRight.js +0 -13
  84. package/icons/FormatBold.js +0 -13
  85. package/icons/FormatClear.js +0 -13
  86. package/icons/FormatColorFill.js +0 -13
  87. package/icons/FormatColorText.js +0 -13
  88. package/icons/FormatItalic.js +0 -13
  89. package/icons/FormatLineSpacing.js +0 -13
  90. package/icons/FormatListBulleted.js +0 -13
  91. package/icons/FormatListNumbered.js +0 -13
  92. package/icons/FormatStrikethrough.js +0 -13
  93. package/icons/FormatUnderlined.js +0 -13
  94. package/icons/HelpCenter.js +0 -13
  95. package/icons/HorizontalRule.js +0 -13
  96. package/icons/Italic.js +0 -14
  97. package/icons/ItalicIcon.js +0 -18
  98. package/icons/Link.js +0 -13
  99. package/icons/LinkOff.js +0 -13
  100. package/icons/MergeCells.js +0 -14
  101. package/icons/MoreVert.js +0 -13
  102. package/icons/Redo.js +0 -13
  103. package/icons/RemoveColumnOutline.js +0 -28
  104. package/icons/RemoveRowOutline.js +0 -25
  105. package/icons/SplitCells.js +0 -14
  106. package/icons/SplitScene.js +0 -13
  107. package/icons/Subscript.js +0 -13
  108. package/icons/Superscript.js +0 -13
  109. package/icons/Underline.js +0 -14
  110. package/icons/Undo.js +0 -13
  111. package/icons/VerticalAlignBottom.js +0 -13
  112. package/icons/VerticalAlignCenter.js +0 -13
  113. package/icons/VerticalAlignTop.js +0 -13
  114. package/icons/WandStars.js +0 -13
  115. package/icons/add_column_left.svg +0 -6
  116. package/icons/add_column_right.svg +0 -6
  117. package/icons/add_row_above.svg +0 -6
  118. package/icons/add_row_below.svg +0 -6
  119. package/icons/align_horizontal_center.svg +0 -1
  120. package/icons/align_horizontal_left.svg +0 -1
  121. package/icons/align_horizontal_right.svg +0 -1
  122. package/icons/align_vertical_bottom.svg +0 -1
  123. package/icons/align_vertical_center.svg +0 -1
  124. package/icons/align_vertical_top.svg +0 -1
  125. package/icons/backspace.svg +0 -1
  126. package/icons/bold.svg +0 -1
  127. package/icons/border_all.svg +0 -1
  128. package/icons/border_bottom.svg +0 -1
  129. package/icons/border_clear.svg +0 -1
  130. package/icons/border_horizontal.svg +0 -1
  131. package/icons/border_inner.svg +0 -1
  132. package/icons/border_left.svg +0 -1
  133. package/icons/border_outer.svg +0 -1
  134. package/icons/border_right.svg +0 -1
  135. package/icons/border_top.svg +0 -1
  136. package/icons/border_vertical.svg +0 -1
  137. package/icons/close.svg +0 -1
  138. package/icons/delete.svg +0 -1
  139. package/icons/evenly_distribute.svg +0 -5
  140. package/icons/fit_width.svg +0 -1
  141. package/icons/fit_width_arrows.svg +0 -12
  142. package/icons/format_align_center.svg +0 -1
  143. package/icons/format_align_justify.svg +0 -1
  144. package/icons/format_align_left.svg +0 -1
  145. package/icons/format_align_right.svg +0 -1
  146. package/icons/format_bold.svg +0 -1
  147. package/icons/format_clear.svg +0 -1
  148. package/icons/format_color_fill.svg +0 -1
  149. package/icons/format_italic.svg +0 -1
  150. package/icons/format_line_spacing.svg +0 -1
  151. package/icons/format_list_bulleted.svg +0 -1
  152. package/icons/format_list_numbered.svg +0 -1
  153. package/icons/format_strikethrough.svg +0 -1
  154. package/icons/format_underlined.svg +0 -1
  155. package/icons/help_center.svg +0 -1
  156. package/icons/horizontal_rule.svg +0 -1
  157. package/icons/index.js +0 -199
  158. package/icons/italic.svg +0 -1
  159. package/icons/link.svg +0 -1
  160. package/icons/link_off.svg +0 -1
  161. package/icons/merge_cells.svg +0 -5
  162. package/icons/more_vert.svg +0 -1
  163. package/icons/redo.svg +0 -1
  164. package/icons/remove_column_outline.svg +0 -20
  165. package/icons/remove_row_outline.svg +0 -17
  166. package/icons/split_cells.svg +0 -5
  167. package/icons/split_scene.svg +0 -1
  168. package/icons/subscript.svg +0 -1
  169. package/icons/superscript.svg +0 -1
  170. package/icons/underline.svg +0 -1
  171. package/icons/undo.svg +0 -1
  172. package/icons/vertical_align_bottom.svg +0 -1
  173. package/icons/vertical_align_center.svg +0 -1
  174. package/icons/vertical_align_top.svg +0 -1
  175. package/icons/wand_stars.svg +0 -1
  176. package/scripts/extract-svg.js +0 -290
  177. package/variables.scss +0 -6
@@ -1,4 +1,10 @@
1
- import React, { Fragment } from "react";
1
+ import React, {
2
+ Fragment,
3
+ useCallback,
4
+ useEffect,
5
+ useRef,
6
+ useState,
7
+ } from "react";
2
8
  import classNames from "classnames";
3
9
  import { DropdownMenu } from "radix-ui";
4
10
 
@@ -6,18 +12,67 @@ import { ChevronRightIcon } from "@radix-ui/react-icons";
6
12
  import { MenuItem } from "./MenuItem";
7
13
  import style from "./EditorMenu.module.scss";
8
14
 
15
+ // Delay to prevent the submenu closing just from the user
16
+ // rolling between the trigger and the content.
17
+ const POINTER_LEAVE_CLOSE_DELAY = 200;
18
+
9
19
  export function SubMenu({
10
20
  label,
11
21
  icon,
12
22
  items = [],
13
23
  disabled = false,
14
24
  renderItem = null,
25
+ // For submenus whose contents run editor commands.
26
+ // Prevents focus jumping back to ProseMirror and closing
27
+ // a submenu that we want to keep open (eg. border cell selector).
28
+ // Submenu stays open until the user rolls off the trigger.
29
+ keepOpenOnInteract = false,
15
30
  }) {
16
31
  const div = window.Bridge.UI.overlay;
17
32
 
33
+ const [open, setOpen] = useState(false);
34
+ const closeTimerRef = useRef(null);
35
+
36
+ const cancelClose = useCallback(() => {
37
+ if (closeTimerRef.current) window.clearTimeout(closeTimerRef.current);
38
+ closeTimerRef.current = null;
39
+ }, []);
40
+
41
+ const scheduleClose = useCallback(() => {
42
+ cancelClose();
43
+ closeTimerRef.current = window.setTimeout(() => {
44
+ closeTimerRef.current = null;
45
+ setOpen(false);
46
+ }, POINTER_LEAVE_CLOSE_DELAY);
47
+ }, [cancelClose]);
48
+
49
+ // Mirror every Radix-initiated change (hover-open, arrow keys, Escape,
50
+ // click-outside) so the controlled value never fights the primitive.
51
+ const handleOpenChange = useCallback(
52
+ (nextOpen) => {
53
+ cancelClose();
54
+ setOpen(nextOpen);
55
+ },
56
+ [cancelClose],
57
+ );
58
+
59
+ useEffect(() => cancelClose, [cancelClose]);
60
+
61
+ // Spread conditionally so submenus that don't opt in stay uncontrolled, ie.
62
+ // exactly as they behaved before.
63
+ const subProps = keepOpenOnInteract
64
+ ? { open, onOpenChange: handleOpenChange }
65
+ : {};
66
+ const hoverProps = keepOpenOnInteract
67
+ ? { onPointerEnter: cancelClose, onPointerLeave: scheduleClose }
68
+ : {};
69
+ const contentProps = keepOpenOnInteract
70
+ ? { ...hoverProps, onFocusOutside: (event) => event.preventDefault() }
71
+ : {};
72
+
18
73
  return (
19
- <DropdownMenu.Sub>
20
- <DropdownMenu.SubTrigger className={style.item}>
74
+ <DropdownMenu.Sub {...subProps}>
75
+ <DropdownMenu.SubTrigger className={style.item} {...hoverProps}>
21
76
  <div className={style.itemIcon}>{icon}</div>
22
77
  <div className={style.itemLabel}>{label}</div>
23
78
  <div className={classNames(style.itemIcon, style.itemIconRight)}>
@@ -31,6 +86,7 @@ export function SubMenu({
31
86
  sideOffset={-30}
32
87
  alignOffset={-5}
33
88
  collisionBoundary={div}
89
+ {...contentProps}
34
90
  >
35
91
  {items.map((item) => {
36
92
  if (typeof renderItem === "function") {
@@ -1,10 +1,10 @@
1
1
  import React from "react";
2
2
 
3
3
  import { TableCreator } from "./TableCreator";
4
- import { Popover } from "../Popover";
5
- import toolbarStyle from "../editor-toolbars/EditorToolbar.module.scss";
6
- import { BorderAll } from "../../icons";
7
- import { ToolbarButton } from "../editor-toolbars/ToolbarButton";
4
+ import { Popover } from "@livepreso/react-plugin-ui/popover";
5
+ import toolbarStyle from "@livepreso/react-plugin-ui/toolbar/toolbar.module.scss";
6
+ import { BorderAll } from "@livepreso/react-plugin-ui/icons";
7
+ import { ToolbarButton } from "@livepreso/react-plugin-ui/toolbar";
8
8
 
9
9
  export function TableCreatorCombo({ disabled, onChange }) {
10
10
  return (
@@ -1,4 +1,4 @@
1
- @import "../style.module.scss";
1
+ @import "@livepreso/react-plugin-ui/styles/placeholders.module.scss";
2
2
 
3
3
  :global(.sp-presenter-overlay) {
4
4
  .indicator {
@@ -1,13 +1,21 @@
1
1
  import { CellSelection, TableMap } from "@tiptap/pm/tables";
2
- import { Table as TableBase } from "@tiptap/extension-table";
2
+ import { Table as TableBase, TableView } from "@tiptap/extension-table";
3
+ import {
4
+ getAttributesFromExtensions,
5
+ getRenderedAttributes,
6
+ } from "@tiptap/core";
3
7
  import { getDomWidth } from "../utils";
4
- import { CELL_MIN_WIDTH } from "../../constants";
8
+ import { CELL_MIN_WIDTH, BORDER_SIDES } from "../../constants";
5
9
 
6
10
  export const CELL_VALIGN_TOP = "top";
7
11
  export const CELL_VALIGN_MIDDLE = "middle";
8
12
  export const CELL_VALIGN_BOTTOM = "bottom";
9
13
  export const CELL_VALIGN_MIXED = "mixed";
10
14
 
15
+ export const TABLE_DENSITY_SPACIOUS = "spacious";
16
+ export const TABLE_DENSITY_NORMAL = "normal";
17
+ export const TABLE_DENSITY_DENSE = "dense";
18
+
11
19
  /**
12
20
  * Finds the table node and its position if the current selection is inside a table
13
21
  * @param state The current editor state
@@ -48,7 +56,7 @@ export function findCellsInSelection(state) {
48
56
 
49
57
  if (!table) return result;
50
58
  const cells = [];
51
- selection.forEachCell((node, pos) => {
59
+ selection.forEachCell((node) => {
52
60
  cells.push(node);
53
61
  });
54
62
  return cells;
@@ -199,7 +207,7 @@ function setColumnWidths({
199
207
  let rowPos = tablePos + 1; // Start at the first row
200
208
 
201
209
  // Process each row in the table
202
- tableNode.forEach((row, rowOffset) => {
210
+ tableNode.forEach((row) => {
203
211
  if (row.type.name !== "tableRow") return;
204
212
 
205
213
  let cellPos = rowPos + 1; // Start at the first cell in this row
@@ -255,6 +263,84 @@ export function getCellAlignment(editor) {
255
263
  return CELL_VALIGN_MIXED;
256
264
  }
257
265
 
266
+ /**
267
+ * Summarises which border sides are active across the currently selected cells.
268
+ * A side counts as active only if every selected cell has it active (intersection),
269
+ * so the toggle group reflects a side as "on" only when the whole selection agrees.
270
+ */
271
+ export function getCellBorderSides(editor) {
272
+ const values = getCellAttributes(editor, "borderSides");
273
+ const mappedValues = values.map((v) => v || BORDER_SIDES);
274
+ if (mappedValues.length === 0) return BORDER_SIDES;
275
+
276
+ return BORDER_SIDES.filter((side) =>
277
+ mappedValues.every((sides) => sides.includes(side)),
278
+ );
279
+ }
280
+
281
+ /**
282
+ * Returns the density of the table the selection is currently in, defaulting
283
+ * to normal if there's no table in the selection or it predates this attribute.
284
+ */
285
+ export function getTableDensity(editor) {
286
+ const table = findTable(editor.state);
287
+ return table?.node?.attrs?.density || TABLE_DENSITY_NORMAL;
288
+ }
289
+
290
+ /**
291
+ * Returns whether the table the selection is currently in is set to full width.
292
+ */
293
+ export function getTableFullWidth(editor) {
294
+ const table = findTable(editor.state);
295
+ return Boolean(table?.node?.attrs?.fullWidth);
296
+ }
297
+
298
+ /**
299
+ * When resizable, the table extension's columnResizing plugin builds the
300
+ * <table> DOM element itself via a NodeView (TableView) rather than the node's
301
+ * renderHTML - and TableView#update only ever re-syncs colgroup/width, never
302
+ * any other attribute. Any table-level attribute change (eg. density) is
303
+ * saved correctly but never reaches the live DOM without this override
304
+ * re-applying it ourselves.
305
+ *
306
+ * getRenderedAttributes/getAttributesFromExtensions are the same helpers
307
+ * Tiptap uses internally to build the non-NodeView renderHTML output, so any
308
+ * attribute added via addAttributes() on the table node flows through here
309
+ * automatically.
310
+ */
311
+ class TableViewWithAttrs extends TableView {
312
+ constructor(node, cellMinWidth, view) {
313
+ super(node, cellMinWidth);
314
+ // Cached once - the extension list doesn't change during the editor's lifetime.
315
+ this.extensionAttributes = getAttributesFromExtensions(
316
+ view?.dom?.editor?.extensionManager.extensions || [],
317
+ );
318
+ this.syncAttrs(node);
319
+ }
320
+
321
+ update(node) {
322
+ const updated = super.update(node);
323
+ if (updated) this.syncAttrs(node);
324
+ return updated;
325
+ }
326
+
327
+ syncAttrs(node) {
328
+ const attrs = getRenderedAttributes(node, this.extensionAttributes);
329
+
330
+ Object.entries(attrs).forEach(([key, value]) => {
331
+ // width/min-width is already recalculated more precisely by
332
+ // updateColumns() above, from live column widths - don't clobber it.
333
+ if (key === "style") return;
334
+
335
+ if (value === null || value === undefined) {
336
+ this.table.removeAttribute(key);
337
+ } else {
338
+ this.table.setAttribute(key, value);
339
+ }
340
+ });
341
+ }
342
+ }
343
+
258
344
  /**
259
345
  * Extension that adds equal column width functionality to tables
260
346
  */
@@ -265,6 +351,37 @@ export const TableExtended = TableBase.extend({
265
351
  cellMinWidth: CELL_MIN_WIDTH,
266
352
  lastColumnResizable: true,
267
353
  resizable: true,
354
+ View: TableViewWithAttrs,
355
+ };
356
+ },
357
+ addAttributes() {
358
+ return {
359
+ ...this.parent?.(),
360
+ // Rendered as a class rather than inline styles so a deck importing this
361
+ // component can override the padding values per density state via CSS.
362
+ density: {
363
+ default: TABLE_DENSITY_NORMAL,
364
+ parseHTML: (element) =>
365
+ element.getAttribute("data-density") || TABLE_DENSITY_NORMAL,
366
+ renderHTML: ({ density }) => ({
367
+ "data-density": density,
368
+ class: `tiptap-table-density-${density}`,
369
+ }),
370
+ },
371
+ // !FEATURE CURRENTLY NOT IN USE!
372
+ // Rendered as a class (rather than a `width: 100%` style) because the
373
+ // resizable table's own colgroup logic (updateColumns) reasserts an
374
+ // inline width/min-width on every update, based on `node.attrs.style`
375
+ // specifically - a style-based width here would be silently clobbered.
376
+ // See setFullWidthLegacy below for the previous, column-width-based approach.
377
+ fullWidth: {
378
+ default: false,
379
+ parseHTML: (element) => element.hasAttribute("data-full-width"),
380
+ renderHTML: ({ fullWidth }) =>
381
+ fullWidth
382
+ ? { "data-full-width": "true", class: "tiptap-table-full-width" }
383
+ : {},
384
+ },
268
385
  };
269
386
  },
270
387
  addCommands() {
@@ -296,7 +413,12 @@ export const TableExtended = TableBase.extend({
296
413
 
297
414
  return true;
298
415
  },
299
- setFullWidth:
416
+ /**
417
+ * Original full-width implementation - redistributes column widths to
418
+ * fill the available space. Plan is to replace this with the simpler
419
+ * `setFullWidth` (CSS-class-based) approach below, when it's ready.
420
+ */
421
+ setFullWidthLegacy:
300
422
  () =>
301
423
  ({ state, tr, dispatch, editor }) => {
302
424
  const table = findTable(state);
@@ -317,6 +439,30 @@ export const TableExtended = TableBase.extend({
317
439
  dispatch(newTr);
318
440
  }
319
441
 
442
+ return true;
443
+ },
444
+ /**
445
+ * !FEATURE CURRENTLY NOT IN USE!
446
+ * Toggle full width for the current table.
447
+ * Plan was to replace legacy in the future, but there
448
+ * is some bugginess that needs ironing out first.
449
+ */
450
+ setFullWidth:
451
+ (fullWidth) =>
452
+ ({ state, tr, dispatch }) => {
453
+ const table = findTable(state);
454
+
455
+ if (!table) return false;
456
+
457
+ if (dispatch) {
458
+ dispatch(
459
+ tr.setNodeMarkup(table.pos, null, {
460
+ ...table.node.attrs,
461
+ fullWidth,
462
+ }),
463
+ );
464
+ }
465
+
320
466
  return true;
321
467
  },
322
468
  cleanWidth:
@@ -350,6 +496,27 @@ export const TableExtended = TableBase.extend({
350
496
  dispatch(newTr);
351
497
  }
352
498
 
499
+ return true;
500
+ },
501
+ /**
502
+ * Set the padding density for the current table
503
+ */
504
+ setTableDensity:
505
+ (density) =>
506
+ ({ state, tr, dispatch }) => {
507
+ const table = findTable(state);
508
+
509
+ if (!table) return false;
510
+
511
+ if (dispatch) {
512
+ dispatch(
513
+ tr.setNodeMarkup(table.pos, null, {
514
+ ...table.node.attrs,
515
+ density,
516
+ }),
517
+ );
518
+ }
519
+
353
520
  return true;
354
521
  },
355
522
  applyStyleToCells:
@@ -1,6 +1,7 @@
1
1
  import { TableCell as TableCellBase } from "@tiptap/extension-table";
2
2
  import { TableHeader as TableHeaderBase } from "@tiptap/extension-table";
3
3
  import { hexIsDark } from "../../utils/color-utils";
4
+ import { BORDER_SIDES, BORDER_SIDES_NONE } from "../../constants";
4
5
 
5
6
  const tableCellAttributes = {
6
7
  colwidth: {
@@ -78,6 +79,38 @@ const tableCellAttributes = {
78
79
  return attrs;
79
80
  },
80
81
  },
82
+ // Sides with a visible border. An absent attribute means "all sides" (default),
83
+ // `"none"` means no sides at all. Only sides missing from the list get an explicit
84
+ // `border-<side>: hidden`, remaining sides inherit default CSS border styling.
85
+ // Note: `hidden` is used instead of `none`, because the table is `border-collapse: collapse`,
86
+ // otherwise the neighbouring cell's border would keep painting the shared edge.
87
+ borderSides: {
88
+ default: null,
89
+ parseHTML: (element) => {
90
+ const value = element.getAttribute("data-border-sides");
91
+ if (value === null) return null;
92
+ // `""` is only produced by builds predating BORDER_SIDES_NONE.
93
+ if (value === BORDER_SIDES_NONE || value === "") return [];
94
+ return value.split(",").filter((side) => BORDER_SIDES.includes(side));
95
+ },
96
+ renderHTML: ({ borderSides }) => {
97
+ if (!borderSides) return { "data-border-sides": null };
98
+
99
+ const hiddenSides = BORDER_SIDES.filter(
100
+ (side) => !borderSides.includes(side),
101
+ );
102
+ const style = hiddenSides
103
+ .map((side) => `border-${side}: hidden;`)
104
+ .join(" ");
105
+
106
+ return {
107
+ "data-border-sides": borderSides.length
108
+ ? borderSides.join(",")
109
+ : BORDER_SIDES_NONE,
110
+ style,
111
+ };
112
+ },
113
+ },
81
114
  };
82
115
 
83
116
  export const TableCell = TableCellBase.extend({
@@ -1,4 +1,4 @@
1
- @import "../../../variables.scss";
1
+ @import "@livepreso/react-plugin-ui/styles/variables.scss";
2
2
 
3
3
  .token,
4
4
  :global(.sp-presenter-overlay) .token {
@@ -1,4 +1,4 @@
1
- @import "../../style.module.scss";
1
+ @import "@livepreso/react-plugin-ui/styles/placeholders.module.scss";
2
2
 
3
3
  :global(.sp-presenter-overlay) {
4
4
  .root {
@@ -33,52 +33,3 @@ export function getDomWidth(dom) {
33
33
  const bounds = dom?.getBoundingClientRect() || { width: 0 };
34
34
  return bounds.width ? bounds.width / getContentRatio() : null;
35
35
  }
36
-
37
- /**
38
- * Tests if the element is contained within the bounds of a mask element
39
- * and returns appropriate rect combining the element and mask rects.
40
- *
41
- * If the test is outside of the vertical limits of the mask, the mask's
42
- * top, bottom and height will replace the testRect's, while textRect
43
- * maintains its width, left and right.
44
- *
45
- * @param {Object} element - return of getBoundingClientRect()
46
- * @param {Object} parent - return of getBoundingClientRect()
47
- * @returns {Object}
48
- */
49
- export const getTestRect = (testRect, maskRect) => {
50
- const newRect = {
51
- y: testRect.y,
52
- x: testRect.x,
53
- width: testRect.width,
54
- height: testRect.height,
55
- top: testRect.top,
56
- bottom: testRect.bottom,
57
- left: testRect.left,
58
- right: testRect.right,
59
- };
60
-
61
- if (testRect.top < maskRect.top) {
62
- newRect.y = maskRect.y;
63
- newRect.top = maskRect.top;
64
- newRect.height = testRect.height - (maskRect.top - testRect.top);
65
- }
66
-
67
- if (testRect.bottom > maskRect.bottom) {
68
- newRect.bottom = maskRect.bottom;
69
- newRect.height = testRect.height - (testRect.bottom - maskRect.bottom);
70
- }
71
-
72
- if (testRect.left < maskRect.left) {
73
- newRect.x = maskRect.x;
74
- newRect.left = maskRect.left;
75
- newRect.width = testRect.width - (maskRect.left - testRect.left);
76
- }
77
-
78
- if (testRect.right > maskRect.right) {
79
- newRect.right = maskRect.right;
80
- newRect.width = testRect.width - (testRect.right - maskRect.right);
81
- }
82
-
83
- return newRect;
84
- };
@@ -1,21 +1,69 @@
1
1
  import React from "react";
2
- import icons from "../icons";
2
+ import icons from "@livepreso/react-plugin-ui/icons";
3
3
  import {
4
4
  TOOLBAR_COMPONENT_TYPES,
5
5
  TABLE_TOOLBAR_IDS,
6
6
  TOOLBAR_SCOPES,
7
+ BORDER_SIDES,
7
8
  } from "../constants";
8
- import { ColorPicker } from "../components/color-picker/ColorPicker";
9
+ import { ColorPickerBasic } from "@livepreso/react-plugin-ui/color-picker";
9
10
  import { VerticalAlignToggle } from "../components/VerticalAlignToggle";
10
- import { findCellsInSelection } from "../components/tiptap/Table";
11
+ import { BorderEdgeToggle } from "../components/BorderEdgeToggle";
12
+ import { TableDensityPicker } from "../components/TableDensityPicker";
13
+ import {
14
+ findCellsInSelection,
15
+ getCellBorderSides,
16
+ getTableDensity,
17
+ getTableFullWidth,
18
+ } from "../components/tiptap/Table";
11
19
  import { generateVerticalAlignmentIcon } from "../utils/generate-vertical-alignment-icon";
20
+ import { generateBorderIcon } from "../utils/generate-border-icon";
12
21
 
13
22
  // Table-wide options
14
- const fullWidthToggle = () => ({
23
+ const tableDensityPicker = () => ({
24
+ id: TABLE_TOOLBAR_IDS.TABLE_DENSITY,
25
+ label: "Table density",
26
+ type: TOOLBAR_COMPONENT_TYPES.CUSTOM,
27
+ scope: TOOLBAR_SCOPES.TABLE,
28
+ value: (editor) => getTableDensity(editor),
29
+ renderContent: ({ editor, value }) => (
30
+ <TableDensityPicker
31
+ value={value}
32
+ onChange={(density) =>
33
+ editor.chain().focus().setTableDensity(density).run()
34
+ }
35
+ />
36
+ ),
37
+ });
38
+
39
+ /**
40
+ * Original "Fit width" control - a one-shot button that redistributes column
41
+ * widths to fill the available space. Plan is to replace with the `fullWidthToggle`
42
+ * below, but the new function needs more work and testing.
43
+ */
44
+ const fullWidthToggleLegacy = () => ({
15
45
  id: TABLE_TOOLBAR_IDS.FULL_WIDTH,
16
46
  label: "Fit width",
17
47
  type: TOOLBAR_COMPONENT_TYPES.BUTTON,
18
- command: (editor) => editor.chain().focus().setFullWidth().run(),
48
+ command: (editor) => editor.chain().focus().setFullWidthLegacy().run(),
49
+ icon: <icons.FitWidthArrows />,
50
+ scope: TOOLBAR_SCOPES.TABLE,
51
+ });
52
+
53
+ /**
54
+ * !FEATURE CURRENTLY NOT IN USE!
55
+ * Trialled full-width toggle, worked well in initial tests, but proved problematic
56
+ * in preview / present modes, and resizing of columns in certain situations.
57
+ * Will revisit in the future.
58
+ */
59
+ /* eslint-disable-next-line no-unused-vars */
60
+ const fullWidthToggle = () => ({
61
+ id: TABLE_TOOLBAR_IDS.FULL_WIDTH,
62
+ label: "Fit width",
63
+ type: TOOLBAR_COMPONENT_TYPES.TOGGLE,
64
+ active: (editor) => getTableFullWidth(editor),
65
+ command: (editor) =>
66
+ editor.chain().focus().setFullWidth(!getTableFullWidth(editor)).run(),
19
67
  icon: <icons.FitWidthArrows />,
20
68
  scope: TOOLBAR_SCOPES.TABLE,
21
69
  });
@@ -132,7 +180,7 @@ const cellBgColorPicker = ({ colors }) => ({
132
180
  type: TOOLBAR_COMPONENT_TYPES.SUB,
133
181
  items: [{ id: "color-picker" }],
134
182
  renderItem: (item, { editor } = {}) => (
135
- <ColorPicker
183
+ <ColorPickerBasic
136
184
  colors={colors}
137
185
  onChange={(color) =>
138
186
  editor.chain().focus().setCellAttribute("backgroundColor", color).run()
@@ -153,7 +201,7 @@ const cellValignPicker = () => ({
153
201
  if (!cells) return null;
154
202
  return cells[0]?.attrs?.verticalAlign;
155
203
  },
156
- renderItem: (item, { editor, editorState, value } = {}) => (
204
+ renderItem: (item, { editor, value } = {}) => (
157
205
  <VerticalAlignToggle
158
206
  id={TABLE_TOOLBAR_IDS.CELL_VALIGN}
159
207
  value={value}
@@ -167,9 +215,39 @@ const cellValignPicker = () => ({
167
215
  />
168
216
  ),
169
217
  });
218
+
219
+ const cellBorderPicker = () => ({
220
+ id: TABLE_TOOLBAR_IDS.CELL_BORDER,
221
+ label: "Border",
222
+ renderIcon: generateBorderIcon,
223
+ scope: TOOLBAR_SCOPES.TABLE_CELL,
224
+ type: TOOLBAR_COMPONENT_TYPES.SUB,
225
+ items: [{ id: "border-sides" }],
226
+ // Four independent toggles - the submenu has to survive repeated clicks.
227
+ keepOpenOnInteract: true,
228
+ value: (editor) => getCellBorderSides(editor),
229
+ renderItem: (item, { editor, value } = {}) => (
230
+ <BorderEdgeToggle
231
+ id={TABLE_TOOLBAR_IDS.CELL_BORDER}
232
+ value={value}
233
+ onChange={(sides) =>
234
+ editor
235
+ .chain()
236
+ .focus()
237
+ .setCellAttribute(
238
+ "borderSides",
239
+ sides.length === BORDER_SIDES.length ? null : sides,
240
+ )
241
+ .run()
242
+ }
243
+ />
244
+ ),
245
+ });
246
+
170
247
  export const tableItemGenerators = {
171
248
  // Table-wide options
172
- [TABLE_TOOLBAR_IDS.FULL_WIDTH]: fullWidthToggle,
249
+ [TABLE_TOOLBAR_IDS.TABLE_DENSITY]: tableDensityPicker,
250
+ [TABLE_TOOLBAR_IDS.FULL_WIDTH]: fullWidthToggleLegacy,
173
251
  [TABLE_TOOLBAR_IDS.EQUAL_WIDTH_COLUMNS]: distributeColumnsButton,
174
252
  [TABLE_TOOLBAR_IDS.DELETE_TABLE]: deleteTableButton,
175
253
  // Internal table options
@@ -184,4 +262,5 @@ export const tableItemGenerators = {
184
262
  [TABLE_TOOLBAR_IDS.SPLIT_CELLS]: splitCellsButton,
185
263
  [TABLE_TOOLBAR_IDS.CELL_BG_COLOR]: cellBgColorPicker,
186
264
  [TABLE_TOOLBAR_IDS.CELL_VALIGN]: cellValignPicker,
265
+ [TABLE_TOOLBAR_IDS.CELL_BORDER]: cellBorderPicker,
187
266
  };
@@ -12,12 +12,12 @@ import { Heading } from "@tiptap/extension-heading";
12
12
  import Underline from "@tiptap/extension-underline";
13
13
  import { UndoRedo } from "@tiptap/extensions";
14
14
 
15
- import icons from "../icons";
15
+ import icons from "@livepreso/react-plugin-ui/icons";
16
16
  import { TableRow } from "@tiptap/extension-table";
17
17
  import { TextStyle, Color } from "@tiptap/extension-text-style";
18
18
  import { TableCell, TableHeader } from "../components/tiptap/TableCells";
19
19
  import { TableExtended } from "../components/tiptap/Table";
20
- import { Select } from "../components/Select/Select";
20
+ import { Select } from "@livepreso/react-plugin-ui/select";
21
21
  import { PRIMARY_TOOLBAR_IDS, TOOLBAR_COMPONENT_TYPES } from "../constants";
22
22
  import LinkEditDialog from "../components/LinkEditDialog";
23
23
  import { Gapcursor } from "@tiptap/extensions";
@@ -343,7 +343,7 @@ const insertTableButton = () => ({
343
343
  cols,
344
344
  withHeaderRow: true,
345
345
  })
346
- .setFullWidth()
346
+ .setFullWidthLegacy()
347
347
  .run();
348
348
  }}
349
349
  />
package/constants.js CHANGED
@@ -26,6 +26,14 @@ export const TOOLBAR_SCOPES = Object.freeze({
26
26
  TABLE_CELL: "table_cell",
27
27
  });
28
28
 
29
+ export const BORDER_SIDES = ["top", "right", "bottom", "left"];
30
+
31
+ // An empty list can't be written as an empty `data-border-sides` attribute,
32
+ // we needed null/empty to represent default cell border style (for backwards
33
+ // compatibility) - as such, a specific "none" was needed to prevent "none"
34
+ // looking like default (generally "all").
35
+ export const BORDER_SIDES_NONE = "none";
36
+
29
37
  export const EDITABLE_OUTER_SELECTOR = "sp-textfield";
30
38
 
31
39
  // Primary groups
@@ -61,6 +69,7 @@ export const PRIMARY_TOOLBAR_IDS = Object.freeze({
61
69
  });
62
70
 
63
71
  // Table editing & formatting groups
72
+ export const TABLE_DENSITY = "tableDensity";
64
73
  export const TABLE_SIZE = "tableSize";
65
74
  export const DELETE_TABLE = "deleteTable";
66
75
  export const COLUMN = "column";
@@ -68,6 +77,7 @@ export const ROW = "row";
68
77
  export const CELL = "cell";
69
78
 
70
79
  export const TABLE_TOOLBAR_IDS = Object.freeze({
80
+ TABLE_DENSITY: TABLE_DENSITY,
71
81
  FULL_WIDTH: "fullWidthTable",
72
82
  EQUAL_WIDTH_COLUMNS: "equalWidthColumns",
73
83
  DELETE_TABLE: DELETE_TABLE,
@@ -82,6 +92,7 @@ export const TABLE_TOOLBAR_IDS = Object.freeze({
82
92
  SPLIT_CELLS: "splitCells",
83
93
  CELL_BG_COLOR: "cellBackgroundColor",
84
94
  CELL_VALIGN: "cellVerticalAlignment",
95
+ CELL_BORDER: "cellBorderSides",
85
96
  });
86
97
 
87
98
  // Default group configurations
@@ -125,6 +136,9 @@ const PRIMARY_TOOLBAR_GROUPS = Object.freeze({
125
136
  });
126
137
 
127
138
  const TABLE_TOOLBAR_GROUPS = Object.freeze({
139
+ [TABLE_DENSITY]: {
140
+ items: [TABLE_TOOLBAR_IDS.TABLE_DENSITY],
141
+ },
128
142
  [TABLE_SIZE]: {
129
143
  items: [
130
144
  TABLE_TOOLBAR_IDS.FULL_WIDTH,
@@ -158,6 +172,7 @@ const TABLE_MENU_GROUPS = Object.freeze({
158
172
  TABLE_TOOLBAR_IDS.SPLIT_CELLS,
159
173
  TABLE_TOOLBAR_IDS.CELL_BG_COLOR,
160
174
  TABLE_TOOLBAR_IDS.CELL_VALIGN,
175
+ TABLE_TOOLBAR_IDS.CELL_BORDER,
161
176
  ],
162
177
  },
163
178
  });
@@ -169,7 +184,7 @@ export const DEFAULT_TOOLBAR_GROUPS = {
169
184
  };
170
185
 
171
186
  // Default group order of table-wide and table-cell editors
172
- const TABLE_OPTIONS = [TABLE_SIZE, DELETE_TABLE];
187
+ const TABLE_OPTIONS = [TABLE_DENSITY, TABLE_SIZE, DELETE_TABLE];
173
188
  const TABLE_CELL_OPTIONS = [COLUMN, ROW, CELL];
174
189
 
175
190
  // Complete set of toolbar configurations