@atlaskit/editor-plugin-layout 10.5.0 → 10.6.1

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 (52) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/cjs/layoutPlugin.js +4 -1
  3. package/dist/cjs/pm-plugins/actions.js +87 -64
  4. package/dist/cjs/pm-plugins/main.js +4 -2
  5. package/dist/cjs/pm-plugins/utils/layout-column-selection.js +128 -0
  6. package/dist/cjs/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +7 -4
  7. package/dist/cjs/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +5 -6
  8. package/dist/cjs/ui/LayoutColumnMenu/VerticalAlignDropdownItem.js +9 -7
  9. package/dist/cjs/ui/LayoutColumnMenu/VerticalAlignNestedMenu.js +15 -6
  10. package/dist/cjs/ui/LayoutColumnMenu/index.js +17 -7
  11. package/dist/cjs/ui/LayoutColumnMenu/useSelectedLayoutColumns.js +14 -0
  12. package/dist/es2019/layoutPlugin.js +4 -1
  13. package/dist/es2019/pm-plugins/actions.js +73 -58
  14. package/dist/es2019/pm-plugins/main.js +4 -2
  15. package/dist/es2019/pm-plugins/utils/layout-column-selection.js +118 -0
  16. package/dist/es2019/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +7 -4
  17. package/dist/es2019/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +5 -6
  18. package/dist/es2019/ui/LayoutColumnMenu/VerticalAlignDropdownItem.js +9 -6
  19. package/dist/es2019/ui/LayoutColumnMenu/VerticalAlignNestedMenu.js +15 -5
  20. package/dist/es2019/ui/LayoutColumnMenu/index.js +16 -6
  21. package/dist/es2019/ui/LayoutColumnMenu/useSelectedLayoutColumns.js +6 -0
  22. package/dist/esm/layoutPlugin.js +5 -2
  23. package/dist/esm/pm-plugins/actions.js +87 -64
  24. package/dist/esm/pm-plugins/main.js +4 -2
  25. package/dist/esm/pm-plugins/utils/layout-column-selection.js +122 -0
  26. package/dist/esm/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +7 -4
  27. package/dist/esm/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +5 -6
  28. package/dist/esm/ui/LayoutColumnMenu/VerticalAlignDropdownItem.js +10 -8
  29. package/dist/esm/ui/LayoutColumnMenu/VerticalAlignNestedMenu.js +15 -6
  30. package/dist/esm/ui/LayoutColumnMenu/index.js +17 -7
  31. package/dist/esm/ui/LayoutColumnMenu/useSelectedLayoutColumns.js +8 -0
  32. package/dist/types/layoutPluginType.d.ts +1 -1
  33. package/dist/types/pm-plugins/actions.d.ts +3 -2
  34. package/dist/types/pm-plugins/types.d.ts +1 -0
  35. package/dist/types/pm-plugins/utils/layout-column-selection.d.ts +18 -0
  36. package/dist/types/ui/LayoutColumnMenu/useSelectedLayoutColumns.d.ts +4 -0
  37. package/dist/types-ts4.5/layoutPluginType.d.ts +1 -1
  38. package/dist/types-ts4.5/pm-plugins/actions.d.ts +3 -2
  39. package/dist/types-ts4.5/pm-plugins/types.d.ts +1 -0
  40. package/dist/types-ts4.5/pm-plugins/utils/layout-column-selection.d.ts +18 -0
  41. package/dist/types-ts4.5/ui/LayoutColumnMenu/useSelectedLayoutColumns.d.ts +4 -0
  42. package/package.json +6 -6
  43. package/dist/cjs/ui/LayoutColumnMenu/layoutColumnSelection.js +0 -21
  44. package/dist/cjs/ui/LayoutColumnMenu/useCurrentLayoutColumn.js +0 -20
  45. package/dist/es2019/ui/LayoutColumnMenu/layoutColumnSelection.js +0 -9
  46. package/dist/es2019/ui/LayoutColumnMenu/useCurrentLayoutColumn.js +0 -10
  47. package/dist/esm/ui/LayoutColumnMenu/layoutColumnSelection.js +0 -15
  48. package/dist/esm/ui/LayoutColumnMenu/useCurrentLayoutColumn.js +0 -14
  49. package/dist/types/ui/LayoutColumnMenu/layoutColumnSelection.d.ts +0 -7
  50. package/dist/types/ui/LayoutColumnMenu/useCurrentLayoutColumn.d.ts +0 -5
  51. package/dist/types-ts4.5/ui/LayoutColumnMenu/layoutColumnSelection.d.ts +0 -7
  52. package/dist/types-ts4.5/ui/LayoutColumnMenu/useCurrentLayoutColumn.d.ts +0 -5
@@ -357,7 +357,10 @@ export const layoutPlugin = ({
357
357
  var _api$analytics5;
358
358
  return insertLayoutColumn(side, api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions);
359
359
  },
360
- setLayoutColumnValign,
360
+ setLayoutColumnValign: valign => {
361
+ var _api$analytics6;
362
+ return setLayoutColumnValign(valign, api === null || api === void 0 ? void 0 : (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 ? void 0 : _api$analytics6.actions);
363
+ },
361
364
  toggleLayoutColumnMenu
362
365
  }
363
366
  };
@@ -9,6 +9,7 @@ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equ
9
9
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
10
10
  import { EVEN_DISTRIBUTED_COL_WIDTHS, MAX_LAYOUT_COLUMNS, MAX_STANDARD_LAYOUT_COLUMNS, MIN_LAYOUT_COLUMN_WIDTH_PERCENT } from './consts';
11
11
  import { pluginKey } from './plugin-key';
12
+ import { getSelectedLayoutColumns } from './utils/layout-column-selection';
12
13
  import { redistributeAfterDeletion, redistributeProportionally } from './utils/redistribute-proportionally';
13
14
  export const ONE_COL_LAYOUTS = ['single'];
14
15
  export const TWO_COL_LAYOUTS = ['two_equal', 'two_left_sidebar', 'two_right_sidebar'];
@@ -533,50 +534,26 @@ const formatLayoutName = layout => {
533
534
  export function getEffectiveMaxLayoutColumns() {
534
535
  return editorExperiment('advanced_layouts', true) ? MAX_LAYOUT_COLUMNS : MAX_STANDARD_LAYOUT_COLUMNS;
535
536
  }
536
- const getSelectedLayoutColumnInSection = ({
537
- doc,
538
- selection
539
- }) => {
540
- const {
541
- layoutColumn,
542
- layoutSection
543
- } = doc.type.schema.nodes;
544
- if (!(selection instanceof NodeSelection) || selection.node.type !== layoutColumn) {
545
- return;
546
- }
547
- const {
548
- $from
549
- } = selection;
550
- if ($from.parent.type !== layoutSection) {
551
- return;
552
- }
553
- const selectedColumnIndex = $from.index($from.depth);
554
- const selectedColumnNode = selection.node;
555
- const selectedColumnPos = selection.from;
556
- const layoutSectionPos = $from.before($from.depth);
557
- return {
558
- layoutSectionNode: $from.parent,
559
- layoutSectionPos,
560
- selectedColumnIndex,
561
- selectedColumnNode,
562
- selectedColumnPos
563
- };
564
- };
565
537
  const insertLayoutColumnAt = (side, editorAnalyticsAPI) => ({
566
538
  tr
567
539
  }) => {
568
540
  if (!expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
569
541
  return null;
570
542
  }
571
- const selectedColumn = getSelectedLayoutColumnInSection(tr);
572
- if (!selectedColumn) {
543
+ const selectedLayoutColumns = getSelectedLayoutColumns(tr.selection);
544
+ if (!selectedLayoutColumns) {
573
545
  return null;
574
546
  }
575
547
  const {
576
548
  layoutSectionNode,
577
549
  layoutSectionPos,
578
- selectedColumnIndex
579
- } = selectedColumn;
550
+ selectedColumnIndices,
551
+ selectedColumns
552
+ } = selectedLayoutColumns;
553
+ const startIndex = selectedColumnIndices[0];
554
+ const endIndex = selectedColumnIndices[selectedColumnIndices.length - 1];
555
+ const selectedColumnIndex = side === 'left' ? startIndex : endIndex;
556
+ const selectedColumnCount = selectedColumns.length;
580
557
  if (layoutSectionNode.childCount >= getEffectiveMaxLayoutColumns()) {
581
558
  return null;
582
559
  }
@@ -613,9 +590,11 @@ const insertLayoutColumnAt = (side, editorAnalyticsAPI) => ({
613
590
  actionSubjectId: ACTION_SUBJECT_ID.LAYOUT_COLUMN,
614
591
  attributes: {
615
592
  columnCount: redistributedWidths.length,
593
+ endIndex,
616
594
  inputMethod: INPUT_METHOD.LAYOUT_COLUMN_MENU,
617
- selectedIndex: selectedColumnIndex,
618
- side
595
+ selectedCount: selectedColumnCount,
596
+ side,
597
+ startIndex
619
598
  },
620
599
  eventType: EVENT_TYPE.TRACK
621
600
  })(tr);
@@ -623,38 +602,64 @@ const insertLayoutColumnAt = (side, editorAnalyticsAPI) => ({
623
602
  return tr;
624
603
  };
625
604
  export const insertLayoutColumn = (side, editorAnalyticsAPI) => insertLayoutColumnAt(side, editorAnalyticsAPI);
626
- export const setLayoutColumnValign = valign => ({
605
+ export const setLayoutColumnValign = (valign, editorAnalyticsAPI) => ({
627
606
  tr
628
607
  }) => {
629
608
  if (!expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
630
609
  return null;
631
610
  }
632
- const {
633
- layoutColumn
634
- } = tr.doc.type.schema.nodes;
635
- const selectedColumn = tr.selection instanceof NodeSelection && tr.selection.node.type === layoutColumn ? {
636
- node: tr.selection.node,
637
- pos: tr.selection.from
638
- } : undefined;
639
- if (!selectedColumn) {
611
+ const selectedLayoutColumns = getSelectedLayoutColumns(tr.selection);
612
+ if (!selectedLayoutColumns) {
640
613
  return null;
641
614
  }
642
- if (selectedColumn.node.attrs.valign === valign) {
615
+ const {
616
+ selectedColumnIndices,
617
+ selectedColumns
618
+ } = selectedLayoutColumns;
619
+ const columnsToUpdate = selectedColumns.filter(({
620
+ node
621
+ }) => node.attrs.valign !== valign);
622
+ if (columnsToUpdate.length === 0) {
643
623
  return null;
644
624
  }
645
- tr.setNodeMarkup(selectedColumn.pos, selectedColumn.node.type, {
646
- ...selectedColumn.node.attrs,
647
- valign
625
+ const startIndex = selectedColumnIndices[0];
626
+ const endIndex = selectedColumnIndices[selectedColumnIndices.length - 1];
627
+ const selectedColumnCount = selectedColumns.length;
628
+ const updatedColumnCount = columnsToUpdate.length;
629
+ columnsToUpdate.forEach(({
630
+ node,
631
+ pos
632
+ }) => {
633
+ tr.setNodeMarkup(pos, node.type, {
634
+ ...node.attrs,
635
+ valign
636
+ });
648
637
  });
638
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
639
+ action: ACTION.UPDATED,
640
+ actionSubject: ACTION_SUBJECT.DOCUMENT,
641
+ actionSubjectId: ACTION_SUBJECT_ID.LAYOUT_COLUMN,
642
+ attributes: {
643
+ endIndex,
644
+ inputMethod: INPUT_METHOD.LAYOUT_COLUMN_MENU,
645
+ selectedCount: selectedColumnCount,
646
+ startIndex,
647
+ updatedCount: updatedColumnCount,
648
+ valign
649
+ },
650
+ eventType: EVENT_TYPE.TRACK
651
+ })(tr);
649
652
  tr.setMeta('scrollIntoView', false);
650
653
  return tr;
651
654
  };
652
655
  export const toggleLayoutColumnMenu = ({
656
+ anchorPos,
653
657
  isOpen
654
658
  }) => ({
655
659
  tr
656
660
  }) => {
657
661
  tr.setMeta('toggleLayoutColumnMenu', {
662
+ anchorPos,
658
663
  isOpen
659
664
  });
660
665
  tr.setMeta('scrollIntoView', false);
@@ -666,15 +671,20 @@ export const deleteLayoutColumn = editorAnalyticsAPI => ({
666
671
  if (!expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
667
672
  return null;
668
673
  }
669
- const selectedColumn = getSelectedLayoutColumnInSection(tr);
670
- if (!selectedColumn) {
674
+ const selectedLayoutColumns = getSelectedLayoutColumns(tr.selection);
675
+ if (!selectedLayoutColumns) {
671
676
  return null;
672
677
  }
673
678
  const {
674
679
  layoutSectionNode,
675
680
  layoutSectionPos,
676
- selectedColumnIndex
677
- } = selectedColumn;
681
+ selectedColumnIndices,
682
+ selectedColumns
683
+ } = selectedLayoutColumns;
684
+ const startIndex = selectedColumnIndices[0];
685
+ const endIndex = selectedColumnIndices[selectedColumnIndices.length - 1];
686
+ const selectedColumnCount = selectedColumns.length;
687
+ const selectedColumnIndexSet = new Set(selectedColumnIndices);
678
688
  const emitDeleteColumnAnalytics = columnCount => {
679
689
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
680
690
  action: ACTION.DELETED,
@@ -682,32 +692,37 @@ export const deleteLayoutColumn = editorAnalyticsAPI => ({
682
692
  actionSubjectId: ACTION_SUBJECT_ID.LAYOUT_COLUMN,
683
693
  attributes: {
684
694
  columnCount,
695
+ endIndex,
685
696
  inputMethod: INPUT_METHOD.LAYOUT_COLUMN_MENU,
686
- selectedIndex: selectedColumnIndex
697
+ selectedCount: selectedColumnCount,
698
+ startIndex
687
699
  },
688
700
  eventType: EVENT_TYPE.TRACK
689
701
  })(tr);
690
702
  };
691
703
 
692
- // If only one column remains, remove the entire layoutSection
693
- if (layoutSectionNode.childCount === 1) {
704
+ // If all columns are selected, remove the entire layoutSection
705
+ if (selectedColumnCount === layoutSectionNode.childCount) {
694
706
  tr.delete(layoutSectionPos, layoutSectionPos + layoutSectionNode.nodeSize);
695
707
  emitDeleteColumnAnalytics(0);
696
708
  tr.setMeta('scrollIntoView', false);
697
709
  return tr;
698
710
  }
699
711
 
700
- // Build new column list without the selected column
712
+ // Build new column list without the selected columns
701
713
  const remainingColumns = [];
702
714
  layoutSectionNode.forEach((column, _offset, index) => {
703
- if (index !== selectedColumnIndex) {
715
+ if (!selectedColumnIndexSet.has(index)) {
704
716
  remainingColumns.push(column);
705
717
  }
706
718
  });
707
719
 
708
720
  // Redistribute widths proportionally among remaining columns using shared utility
709
721
  const existingWidths = mapChildren(layoutSectionNode, column => column.attrs.width);
710
- const redistributed = redistributeAfterDeletion(existingWidths, selectedColumnIndex, MIN_LAYOUT_COLUMN_WIDTH_PERCENT);
722
+ const redistributed = selectedColumnIndices.slice()
723
+ // Delete highest indices first so lower original indices still point at the same columns
724
+ // as each redistribution step shrinks the widths array.
725
+ .sort((a, b) => b - a).reduce((widths, selectedIndex) => redistributeAfterDeletion(widths, selectedIndex, MIN_LAYOUT_COLUMN_WIDTH_PERCENT), existingWidths);
711
726
  const updatedLayoutSectionNode = layoutSectionNode.copy(Fragment.fromArray(remainingColumns));
712
727
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributed));
713
728
  emitDeleteColumnAnalytics(redistributed.length);
@@ -67,7 +67,8 @@ const getInitialPluginState = (options, state) => {
67
67
  selectedLayout,
68
68
  allowSingleColumnLayout,
69
69
  isResizing: false,
70
- isLayoutColumnMenuOpen: false
70
+ isLayoutColumnMenuOpen: false,
71
+ layoutColumnMenuAnchorPos: undefined
71
72
  };
72
73
  };
73
74
 
@@ -118,7 +119,8 @@ export default (options => {
118
119
  const columnMenuMeta = tr.getMeta('toggleLayoutColumnMenu');
119
120
  const nextPluginState = columnMenuMeta ? {
120
121
  ...pluginState,
121
- isLayoutColumnMenuOpen: (_columnMenuMeta$isOpe = columnMenuMeta.isOpen) !== null && _columnMenuMeta$isOpe !== void 0 ? _columnMenuMeta$isOpe : !pluginState.isLayoutColumnMenuOpen
122
+ isLayoutColumnMenuOpen: (_columnMenuMeta$isOpe = columnMenuMeta.isOpen) !== null && _columnMenuMeta$isOpe !== void 0 ? _columnMenuMeta$isOpe : !pluginState.isLayoutColumnMenuOpen,
123
+ layoutColumnMenuAnchorPos: columnMenuMeta.isOpen === false ? undefined : columnMenuMeta.anchorPos
122
124
  } : pluginState;
123
125
  const isResizing = editorExperiment('single_column_layouts', true) ? (_tr$getMeta = tr.getMeta('is-resizer-resizing')) !== null && _tr$getMeta !== void 0 ? _tr$getMeta : (_pluginKey$getState = pluginKey.getState(oldState)) === null || _pluginKey$getState === void 0 ? void 0 : _pluginKey$getState.isResizing : false;
124
126
  if (tr.docChanged || tr.selectionSet) {
@@ -0,0 +1,118 @@
1
+ import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
2
+ import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
3
+ const isLayoutColumn = node => (node === null || node === void 0 ? void 0 : node.type.name) === 'layoutColumn';
4
+ const isLayoutSection = node => (node === null || node === void 0 ? void 0 : node.type.name) === 'layoutSection';
5
+ const getLayoutColumnIndexAtPos = $pos => {
6
+ for (let depth = $pos.depth; depth > 0; depth--) {
7
+ if (isLayoutColumn($pos.node(depth)) && isLayoutSection($pos.node(depth - 1))) {
8
+ return $pos.index(depth - 1);
9
+ }
10
+ }
11
+ return undefined;
12
+ };
13
+ const getLayoutSectionDepth = selection => {
14
+ const {
15
+ $from,
16
+ $to
17
+ } = selection;
18
+ const sharedDepth = $from.sharedDepth($to.pos);
19
+ for (let depth = sharedDepth; depth > 0; depth--) {
20
+ if (isLayoutSection($from.node(depth))) {
21
+ return depth;
22
+ }
23
+ }
24
+ return undefined;
25
+ };
26
+ export const getSelectedLayoutColumns = selection => {
27
+ if (!selection) {
28
+ return undefined;
29
+ }
30
+ if (selection instanceof NodeSelection && isLayoutColumn(selection.node)) {
31
+ const {
32
+ $from
33
+ } = selection;
34
+ const layoutSectionNode = $from.parent;
35
+ if (!isLayoutSection(layoutSectionNode)) {
36
+ return undefined;
37
+ }
38
+ const selectedColumnIndex = $from.index($from.depth);
39
+ return {
40
+ layoutSectionNode,
41
+ layoutSectionPos: $from.before($from.depth),
42
+ selectedColumnIndices: [selectedColumnIndex],
43
+ selectedColumns: [{
44
+ index: selectedColumnIndex,
45
+ node: selection.node,
46
+ pos: selection.from
47
+ }]
48
+ };
49
+ }
50
+ if (selection.empty) {
51
+ return undefined;
52
+ }
53
+ if (!editorExperiment('platform_editor_block_menu', true)) {
54
+ return undefined;
55
+ }
56
+ const layoutSectionDepth = getLayoutSectionDepth(selection);
57
+ if (layoutSectionDepth === undefined) {
58
+ return undefined;
59
+ }
60
+ const {
61
+ $from,
62
+ $to
63
+ } = selection;
64
+ const layoutSectionNode = $from.node(layoutSectionDepth);
65
+ const layoutSectionPos = $from.before(layoutSectionDepth);
66
+ const selectedColumns = [];
67
+ let invalidSelection = false;
68
+ layoutSectionNode.forEach((column, offset, index) => {
69
+ const columnStart = layoutSectionPos + 1 + offset;
70
+ const columnEnd = columnStart + column.nodeSize;
71
+ const intersectsColumn = selection.from < columnEnd && selection.to > columnStart;
72
+ if (!intersectsColumn) {
73
+ return;
74
+ }
75
+ if (!isLayoutColumn(column)) {
76
+ invalidSelection = true;
77
+ return;
78
+ }
79
+ selectedColumns.push({
80
+ index,
81
+ node: column,
82
+ pos: columnStart
83
+ });
84
+ });
85
+
86
+ // TextSelection inside a single column is normal text editing, not a selected column set.
87
+ if (invalidSelection || selectedColumns.length < 2) {
88
+ return undefined;
89
+ }
90
+ const firstColumn = selectedColumns[0];
91
+ const lastColumn = selectedColumns[selectedColumns.length - 1];
92
+ const startColumnIndex = getLayoutColumnIndexAtPos($from);
93
+ const endColumnIndex = getLayoutColumnIndexAtPos($to);
94
+ if (startColumnIndex !== undefined && startColumnIndex !== firstColumn.index || endColumnIndex !== undefined && endColumnIndex !== lastColumn.index) {
95
+ return undefined;
96
+ }
97
+ return {
98
+ layoutSectionNode,
99
+ layoutSectionPos,
100
+ selectedColumnIndices: selectedColumns.map(({
101
+ index
102
+ }) => index),
103
+ selectedColumns
104
+ };
105
+ };
106
+ export const getLayoutSectionColumnCount = layoutSection => (layoutSection === null || layoutSection === void 0 ? void 0 : layoutSection.type.name) === 'layoutSection' ? layoutSection.childCount : 0;
107
+ export const getLayoutColumnValign = layoutColumn => layoutColumn === null || layoutColumn === void 0 ? void 0 : layoutColumn.attrs.valign;
108
+ export const getLayoutColumnMenuAnchorPos = (selection, anchorPosFromHandle) => {
109
+ var _clickedSelectedColum, _selectedLayoutColumn;
110
+ const selectedLayoutColumns = getSelectedLayoutColumns(selection);
111
+ if (!selectedLayoutColumns) {
112
+ return undefined;
113
+ }
114
+ const clickedSelectedColumn = selectedLayoutColumns.selectedColumns.find(({
115
+ pos
116
+ }) => pos === anchorPosFromHandle);
117
+ return (_clickedSelectedColum = clickedSelectedColumn === null || clickedSelectedColumn === void 0 ? void 0 : clickedSelectedColumn.pos) !== null && _clickedSelectedColum !== void 0 ? _clickedSelectedColum : (_selectedLayoutColumn = selectedLayoutColumns.selectedColumns[0]) === null || _selectedLayoutColumn === void 0 ? void 0 : _selectedLayoutColumn.pos;
118
+ };
@@ -2,14 +2,14 @@ import React, { useCallback } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { layoutMessages } from '@atlaskit/editor-common/messages';
4
4
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
- import { useCurrentLayoutColumn } from './useCurrentLayoutColumn';
5
+ import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
6
6
  const DeleteColumnDropdownItem = ({
7
7
  api
8
8
  }) => {
9
9
  const {
10
10
  formatMessage
11
11
  } = useIntl();
12
- const currentColumn = useCurrentLayoutColumn(api);
12
+ const selectedLayoutColumns = useSelectedLayoutColumns(api);
13
13
  const onClick = useCallback(() => {
14
14
  var _api$layout, _api$core;
15
15
  const deleteCommand = api === null || api === void 0 ? void 0 : (_api$layout = api.layout) === null || _api$layout === void 0 ? void 0 : _api$layout.commands.deleteLayoutColumn;
@@ -27,11 +27,14 @@ const DeleteColumnDropdownItem = ({
27
27
  return tr;
28
28
  });
29
29
  }, [api]);
30
- if (currentColumn === undefined) {
30
+ if (selectedLayoutColumns === undefined) {
31
31
  return null;
32
32
  }
33
+ const selectedColumnCount = selectedLayoutColumns.selectedColumns.length;
33
34
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
34
35
  onClick: onClick
35
- }, formatMessage(layoutMessages.deleteColumn));
36
+ }, formatMessage(layoutMessages.deleteColumn, {
37
+ count: selectedColumnCount
38
+ }));
36
39
  };
37
40
  export { DeleteColumnDropdownItem };
@@ -3,8 +3,8 @@ import { useIntl } from 'react-intl';
3
3
  import { layoutMessages } from '@atlaskit/editor-common/messages';
4
4
  import { TableColumnAddLeftIcon, TableColumnAddRightIcon, ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
5
  import { getEffectiveMaxLayoutColumns } from '../../pm-plugins/actions';
6
- import { getLayoutSectionColumnCount } from './layoutColumnSelection';
7
- import { useCurrentLayoutColumn, useCurrentLayoutSection } from './useCurrentLayoutColumn';
6
+ import { getLayoutSectionColumnCount } from '../../pm-plugins/utils/layout-column-selection';
7
+ import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
8
8
  const INSERT_COLUMN_OPTIONS = {
9
9
  left: {
10
10
  Icon: TableColumnAddLeftIcon,
@@ -28,11 +28,10 @@ export const InsertColumnDropdownItem = ({
28
28
  Icon,
29
29
  label
30
30
  } = INSERT_COLUMN_OPTIONS[side];
31
- const currentColumn = useCurrentLayoutColumn(api);
32
- const currentLayoutSection = useCurrentLayoutSection(api);
33
- const columnCount = getLayoutSectionColumnCount(currentLayoutSection);
31
+ const selectedLayoutColumns = useSelectedLayoutColumns(api);
32
+ const columnCount = getLayoutSectionColumnCount(selectedLayoutColumns === null || selectedLayoutColumns === void 0 ? void 0 : selectedLayoutColumns.layoutSectionNode);
34
33
  const maxColumnCount = getEffectiveMaxLayoutColumns();
35
- const canInsertColumn = currentColumn !== undefined && columnCount < maxColumnCount;
34
+ const canInsertColumn = selectedLayoutColumns !== undefined && columnCount < maxColumnCount;
36
35
  const onClick = useCallback(() => {
37
36
  var _api$layout, _api$core;
38
37
  const insertCommand = api === null || api === void 0 ? void 0 : (_api$layout = api.layout) === null || _api$layout === void 0 ? void 0 : _api$layout.commands.insertLayoutColumn(side);
@@ -1,19 +1,22 @@
1
- import React, { useCallback, useMemo } from 'react';
1
+ import React, { useCallback } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
4
- import { getLayoutColumnValign } from './layoutColumnSelection';
5
- import { useCurrentLayoutColumn } from './useCurrentLayoutColumn';
4
+ import { getLayoutColumnValign } from '../../pm-plugins/utils/layout-column-selection';
5
+ import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
6
6
  import { VERTICAL_ALIGN_ICONS } from './verticalAlignIcons';
7
7
  export const VerticalAlignDropdownItem = ({
8
8
  api,
9
9
  label,
10
10
  value
11
11
  }) => {
12
+ var _selectedLayoutColumn;
12
13
  const {
13
14
  formatMessage
14
15
  } = useIntl();
15
- const currentColumn = useCurrentLayoutColumn(api);
16
- const currentValign = useMemo(() => getLayoutColumnValign(currentColumn), [currentColumn]);
16
+ const selectedLayoutColumns = useSelectedLayoutColumns(api);
17
+ const isSelected = (_selectedLayoutColumn = selectedLayoutColumns === null || selectedLayoutColumns === void 0 ? void 0 : selectedLayoutColumns.selectedColumns.every(({
18
+ node
19
+ }) => getLayoutColumnValign(node) === value)) !== null && _selectedLayoutColumn !== void 0 ? _selectedLayoutColumn : false;
17
20
  const Icon = VERTICAL_ALIGN_ICONS[value];
18
21
  const onClick = useCallback(() => {
19
22
  var _api$core, _api$layout;
@@ -24,7 +27,7 @@ export const VerticalAlignDropdownItem = ({
24
27
  label: "",
25
28
  size: "small"
26
29
  }),
27
- isSelected: currentValign === value,
30
+ isSelected: isSelected,
28
31
  onClick: onClick
29
32
  }, formatMessage(label));
30
33
  };
@@ -2,8 +2,8 @@ import React, { useMemo } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
3
  import { layoutMessages } from '@atlaskit/editor-common/messages';
4
4
  import { LayoutIcon, NestedDropdownRightIcon, ToolbarNestedDropdownMenu } from '@atlaskit/editor-toolbar';
5
- import { getLayoutColumnValign } from './layoutColumnSelection';
6
- import { useCurrentLayoutColumn } from './useCurrentLayoutColumn';
5
+ import { getLayoutColumnValign } from '../../pm-plugins/utils/layout-column-selection';
6
+ import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
7
7
  import { VERTICAL_ALIGN_ICONS } from './verticalAlignIcons';
8
8
  export const VerticalAlignNestedMenu = ({
9
9
  api,
@@ -12,10 +12,20 @@ export const VerticalAlignNestedMenu = ({
12
12
  const {
13
13
  formatMessage
14
14
  } = useIntl();
15
- const currentColumn = useCurrentLayoutColumn(api);
16
- const currentValign = useMemo(() => getLayoutColumnValign(currentColumn), [currentColumn]);
15
+ const selectedLayoutColumns = useSelectedLayoutColumns(api);
16
+ const currentValign = useMemo(() => {
17
+ const selectedColumns = selectedLayoutColumns === null || selectedLayoutColumns === void 0 ? void 0 : selectedLayoutColumns.selectedColumns;
18
+ const firstColumn = selectedColumns === null || selectedColumns === void 0 ? void 0 : selectedColumns[0];
19
+ const firstValign = getLayoutColumnValign(firstColumn === null || firstColumn === void 0 ? void 0 : firstColumn.node);
20
+ if (!firstValign || !(selectedColumns !== null && selectedColumns !== void 0 && selectedColumns.every(({
21
+ node
22
+ }) => getLayoutColumnValign(node) === firstValign))) {
23
+ return undefined;
24
+ }
25
+ return firstValign;
26
+ }, [selectedLayoutColumns]);
17
27
  const TriggerIcon = currentValign ? VERTICAL_ALIGN_ICONS[currentValign] : LayoutIcon;
18
- if (!currentColumn) {
28
+ if (!selectedLayoutColumns) {
19
29
  return null;
20
30
  }
21
31
  return /*#__PURE__*/React.createElement(ToolbarNestedDropdownMenu, {
@@ -7,9 +7,9 @@ import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners } from
7
7
  import { akEditorFloatingOverlapPanelZIndex } from '@atlaskit/editor-shared-styles';
8
8
  import { ToolbarDropdownMenuProvider } from '@atlaskit/editor-toolbar';
9
9
  import { SurfaceRenderer } from '@atlaskit/editor-ui-control-model';
10
+ import { getLayoutColumnMenuAnchorPos } from '../../pm-plugins/utils/layout-column-selection';
10
11
  import { LAYOUT_COLUMN_MENU_FALLBACKS } from './components';
11
12
  import { LAYOUT_COLUMN_MENU } from './keys';
12
- import { getLayoutColumnAtSelection } from './layoutColumnSelection';
13
13
  const PopupWithListeners = withReactEditorViewOuterListeners(Popup);
14
14
  const FALLBACK_MENU_HEIGHT = 300;
15
15
  const LAYOUT_COLUMN_MENU_POPUP_OFFSET = [0, 4];
@@ -17,12 +17,13 @@ const LAYOUT_COLUMN_MENU_POPUP_OFFSET = [0, 4];
17
17
  /**
18
18
  * Returns the drag handle button for the selected layout column.
19
19
  */
20
- const getLayoutColumnMenuTarget = (editorView, selection) => {
20
+ const getLayoutColumnMenuTarget = (editorView, selection, anchorPosFromHandle) => {
21
21
  var _columnDomRef$parentE;
22
- if (!getLayoutColumnAtSelection(selection) || (selection === null || selection === void 0 ? void 0 : selection.from) === undefined) {
22
+ const anchorPos = getLayoutColumnMenuAnchorPos(selection, anchorPosFromHandle);
23
+ if (anchorPos === undefined) {
23
24
  return null;
24
25
  }
25
- const columnDomRef = editorView.nodeDOM(selection.from);
26
+ const columnDomRef = editorView.nodeDOM(anchorPos);
26
27
  if (!(columnDomRef instanceof HTMLElement)) {
27
28
  return null;
28
29
  }
@@ -39,11 +40,13 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
39
40
  var _api$uiControlRegistr, _api$uiControlRegistr2;
40
41
  const {
41
42
  isLayoutColumnMenuOpen,
43
+ layoutColumnMenuAnchorPos,
42
44
  selection
43
45
  } = useSharedPluginStateWithSelector(api, ['layout', 'selection'], states => {
44
- var _states$layoutState$i, _states$layoutState, _states$selectionStat;
46
+ var _states$layoutState$i, _states$layoutState, _states$layoutState2, _states$selectionStat;
45
47
  return {
46
48
  isLayoutColumnMenuOpen: (_states$layoutState$i = (_states$layoutState = states.layoutState) === null || _states$layoutState === void 0 ? void 0 : _states$layoutState.isLayoutColumnMenuOpen) !== null && _states$layoutState$i !== void 0 ? _states$layoutState$i : false,
49
+ layoutColumnMenuAnchorPos: (_states$layoutState2 = states.layoutState) === null || _states$layoutState2 === void 0 ? void 0 : _states$layoutState2.layoutColumnMenuAnchorPos,
47
50
  selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection
48
51
  };
49
52
  });
@@ -72,6 +75,13 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
72
75
  if (event.target instanceof Element && event.target.closest('[data-toolbar-nested-dropdown-menu]')) {
73
76
  return;
74
77
  }
78
+
79
+ // Clicking a drag handle should let the drag handle's own click handler
80
+ // update selection/menu state. Treating it as a generic outside click
81
+ // races that transaction and can immediately close the layout column menu.
82
+ if (event.target instanceof Element && event.target.closest(DRAG_HANDLE_SELECTOR)) {
83
+ return;
84
+ }
75
85
  closeLayoutColumnMenu();
76
86
  }, [closeLayoutColumnMenu]);
77
87
  const handleSetIsOpen = useCallback(isOpen => {
@@ -87,7 +97,7 @@ export const LayoutColumnMenu = /*#__PURE__*/React.memo(function LayoutColumnMen
87
97
  }
88
98
  }, [setOutsideClickTargetRef]);
89
99
  const components = (_api$uiControlRegistr = api === null || api === void 0 ? void 0 : (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(LAYOUT_COLUMN_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
90
- const target = useMemo(() => isLayoutColumnMenuOpen ? getLayoutColumnMenuTarget(editorView, selection) : null, [editorView, isLayoutColumnMenuOpen, selection]);
100
+ const target = useMemo(() => isLayoutColumnMenuOpen ? getLayoutColumnMenuTarget(editorView, selection, layoutColumnMenuAnchorPos) : null, [editorView, isLayoutColumnMenuOpen, layoutColumnMenuAnchorPos, selection]);
91
101
  const hasValidTarget = target instanceof HTMLElement;
92
102
  useEffect(() => {
93
103
  if (isLayoutColumnMenuOpen && (!hasValidTarget || components.length === 0)) {
@@ -0,0 +1,6 @@
1
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
2
+ import { getSelectedLayoutColumns } from '../../pm-plugins/utils/layout-column-selection';
3
+ export const useSelectedLayoutColumns = api => useSharedPluginStateWithSelector(api, ['selection'], states => {
4
+ var _states$selectionStat;
5
+ return getSelectedLayoutColumns((_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection);
6
+ });
@@ -11,7 +11,7 @@ import { fg } from '@atlaskit/platform-feature-flags';
11
11
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
12
12
  import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
13
13
  import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
14
- import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn as _deleteLayoutColumn, insertLayoutColumn as _insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
14
+ import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn as _deleteLayoutColumn, insertLayoutColumn as _insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnValign as _setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
15
15
  import { default as createLayoutPlugin } from './pm-plugins/main';
16
16
  import { pluginKey } from './pm-plugins/plugin-key';
17
17
  import { default as createLayoutResizingPlugin } from './pm-plugins/resizing';
@@ -378,7 +378,10 @@ export var layoutPlugin = function layoutPlugin(_ref) {
378
378
  var _api$analytics5;
379
379
  return _insertLayoutColumn(side, api === null || api === void 0 || (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions);
380
380
  },
381
- setLayoutColumnValign: setLayoutColumnValign,
381
+ setLayoutColumnValign: function setLayoutColumnValign(valign) {
382
+ var _api$analytics6;
383
+ return _setLayoutColumnValign(valign, api === null || api === void 0 || (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 ? void 0 : _api$analytics6.actions);
384
+ },
382
385
  toggleLayoutColumnMenu: toggleLayoutColumnMenu
383
386
  }
384
387
  };