@atlaskit/editor-plugin-layout 11.0.4 → 11.1.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 (39) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/layoutPlugin.js +19 -4
  3. package/dist/cjs/pm-plugins/actions.js +30 -10
  4. package/dist/cjs/pm-plugins/keymap.js +31 -0
  5. package/dist/cjs/pm-plugins/main.js +90 -21
  6. package/dist/cjs/pm-plugins/utils/layout-column-selection.js +36 -12
  7. package/dist/cjs/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +25 -5
  8. package/dist/cjs/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +7 -1
  9. package/dist/cjs/ui/LayoutColumnMenu/index.js +51 -4
  10. package/dist/cjs/ui/global-styles.js +11 -1
  11. package/dist/es2019/layoutPlugin.js +18 -5
  12. package/dist/es2019/pm-plugins/actions.js +26 -11
  13. package/dist/es2019/pm-plugins/keymap.js +26 -0
  14. package/dist/es2019/pm-plugins/main.js +97 -25
  15. package/dist/es2019/pm-plugins/utils/layout-column-selection.js +33 -8
  16. package/dist/es2019/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +26 -6
  17. package/dist/es2019/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +8 -2
  18. package/dist/es2019/ui/LayoutColumnMenu/index.js +52 -5
  19. package/dist/es2019/ui/global-styles.js +9 -1
  20. package/dist/esm/layoutPlugin.js +20 -5
  21. package/dist/esm/pm-plugins/actions.js +30 -10
  22. package/dist/esm/pm-plugins/keymap.js +25 -0
  23. package/dist/esm/pm-plugins/main.js +90 -21
  24. package/dist/esm/pm-plugins/utils/layout-column-selection.js +35 -11
  25. package/dist/esm/ui/LayoutColumnMenu/DeleteColumnDropdownItem.js +26 -6
  26. package/dist/esm/ui/LayoutColumnMenu/InsertColumnDropdownItem.js +8 -2
  27. package/dist/esm/ui/LayoutColumnMenu/index.js +52 -5
  28. package/dist/esm/ui/global-styles.js +11 -1
  29. package/dist/types/layoutPluginType.d.ts +5 -7
  30. package/dist/types/pm-plugins/actions.d.ts +8 -4
  31. package/dist/types/pm-plugins/keymap.d.ts +10 -0
  32. package/dist/types/pm-plugins/types.d.ts +2 -0
  33. package/dist/types/pm-plugins/utils/layout-column-selection.d.ts +1 -0
  34. package/dist/types-ts4.5/layoutPluginType.d.ts +5 -7
  35. package/dist/types-ts4.5/pm-plugins/actions.d.ts +8 -4
  36. package/dist/types-ts4.5/pm-plugins/keymap.d.ts +10 -0
  37. package/dist/types-ts4.5/pm-plugins/types.d.ts +2 -0
  38. package/dist/types-ts4.5/pm-plugins/utils/layout-column-selection.d.ts +1 -0
  39. package/package.json +4 -4
@@ -6,9 +6,11 @@ Object.defineProperty(exports, "__esModule", {
6
6
  });
7
7
  exports.LayoutColumnMenu = void 0;
8
8
  var _react = _interopRequireWildcard(require("react"));
9
+ var _bindEventListener = require("bind-event-listener");
9
10
  var _hooks = require("@atlaskit/editor-common/hooks");
10
11
  var _styles = require("@atlaskit/editor-common/styles");
11
12
  var _ui = require("@atlaskit/editor-common/ui");
13
+ var _uiMenu = require("@atlaskit/editor-common/ui-menu");
12
14
  var _uiReact = require("@atlaskit/editor-common/ui-react");
13
15
  var _userIntent = require("@atlaskit/editor-common/user-intent");
14
16
  var _editorSharedStyles = require("@atlaskit/editor-shared-styles");
@@ -21,6 +23,7 @@ function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r
21
23
  var PopupWithListeners = (0, _uiReact.withReactEditorViewOuterListeners)(_ui.Popup);
22
24
  var LAYOUT_COLUMN_MENU_POPUP_OFFSET = [0, 4];
23
25
  var TOOLBAR_MENU_SELECTOR = '[data-toolbar-component="menu"]';
26
+ var NESTED_DROPDOWN_MENU_SELECTOR = '[data-toolbar-nested-dropdown-menu]';
24
27
 
25
28
  /**
26
29
  * Returns the drag handle button for the selected layout column.
@@ -38,6 +41,9 @@ var getLayoutColumnMenuTarget = function getLayoutColumnMenuTarget(editorView, s
38
41
  var dragHandleContainer = (_columnDomRef$parentE = columnDomRef.parentElement) === null || _columnDomRef$parentE === void 0 ? void 0 : _columnDomRef$parentE.querySelector(':scope > [data-blocks-drag-handle-container]');
39
42
  return dragHandleContainer === null || dragHandleContainer === void 0 ? void 0 : dragHandleContainer.querySelector(_styles.DRAG_HANDLE_SELECTOR);
40
43
  };
44
+ var focusTrap = {
45
+ initialFocus: undefined
46
+ };
41
47
  var LayoutColumnMenu = exports.LayoutColumnMenu = /*#__PURE__*/_react.default.memo(function LayoutColumnMenu(_ref) {
42
48
  var _api$uiControlRegistr, _api$uiControlRegistr2;
43
49
  var api = _ref.api,
@@ -46,15 +52,17 @@ var LayoutColumnMenu = exports.LayoutColumnMenu = /*#__PURE__*/_react.default.me
46
52
  boundariesElement = _ref.boundariesElement,
47
53
  scrollableElement = _ref.scrollableElement;
48
54
  var _useSharedPluginState = (0, _hooks.useSharedPluginStateWithSelector)(api, ['layout', 'selection'], function (states) {
49
- var _states$layoutState$i, _states$layoutState, _states$layoutState2, _states$selectionStat;
55
+ var _states$layoutState$i, _states$layoutState, _states$layoutState2, _states$layoutState$l, _states$layoutState3, _states$selectionStat;
50
56
  return {
51
57
  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,
52
58
  layoutColumnMenuAnchorPos: (_states$layoutState2 = states.layoutState) === null || _states$layoutState2 === void 0 ? void 0 : _states$layoutState2.layoutColumnMenuAnchorPos,
59
+ openedViaKeyboard: (_states$layoutState$l = (_states$layoutState3 = states.layoutState) === null || _states$layoutState3 === void 0 ? void 0 : _states$layoutState3.layoutColumnMenuOpenedViaKeyboard) !== null && _states$layoutState$l !== void 0 ? _states$layoutState$l : false,
53
60
  selection: (_states$selectionStat = states.selectionState) === null || _states$selectionStat === void 0 ? void 0 : _states$selectionStat.selection
54
61
  };
55
62
  }),
56
63
  isLayoutColumnMenuOpen = _useSharedPluginState.isLayoutColumnMenuOpen,
57
64
  layoutColumnMenuAnchorPos = _useSharedPluginState.layoutColumnMenuAnchorPos,
65
+ openedViaKeyboard = _useSharedPluginState.openedViaKeyboard,
58
66
  selection = _useSharedPluginState.selection;
59
67
  var closeLayoutColumnMenu = (0, _react.useCallback)(function () {
60
68
  var _api$core, _api$layout;
@@ -63,7 +71,7 @@ var LayoutColumnMenu = exports.LayoutColumnMenu = /*#__PURE__*/_react.default.me
63
71
  }));
64
72
  }, [api]);
65
73
  var handleClickOutside = (0, _react.useCallback)(function (event) {
66
- if (event.target instanceof Element && (event.target.closest(TOOLBAR_MENU_SELECTOR) || event.target.closest('[data-toolbar-nested-dropdown-menu]'))) {
74
+ if (event.target instanceof Element && (event.target.closest(TOOLBAR_MENU_SELECTOR) || event.target.closest(NESTED_DROPDOWN_MENU_SELECTOR))) {
67
75
  return;
68
76
  }
69
77
 
@@ -80,6 +88,38 @@ var LayoutColumnMenu = exports.LayoutColumnMenu = /*#__PURE__*/_react.default.me
80
88
  closeLayoutColumnMenu();
81
89
  }
82
90
  }, [closeLayoutColumnMenu]);
91
+ var handleArrowKeyNavigationClose = (0, _react.useCallback)(function (event) {
92
+ event.preventDefault();
93
+ closeLayoutColumnMenu();
94
+ }, [closeLayoutColumnMenu]);
95
+ var shouldDisableArrowKeyNavigation = (0, _react.useCallback)(function (event) {
96
+ if (event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
97
+ return false;
98
+ }
99
+ var target = event.target;
100
+ if (!(target instanceof HTMLElement)) {
101
+ return false;
102
+ }
103
+ return target.closest(NESTED_DROPDOWN_MENU_SELECTOR) !== null;
104
+ }, []);
105
+ var menuWrapperRef = (0, _react.useRef)(null);
106
+ var handleMenuKeyDown = (0, _react.useCallback)(function (event) {
107
+ // Keep menu keyboard events scoped to the menu while preserving Escape and
108
+ // ArrowUp/ArrowDown handling from Popup and ArrowKeyNavigationProvider.
109
+ if (event.key !== 'Escape' && event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
110
+ event.stopPropagation();
111
+ }
112
+ }, []);
113
+ (0, _react.useEffect)(function () {
114
+ var menuWrapper = menuWrapperRef.current;
115
+ if (!isLayoutColumnMenuOpen || !menuWrapper) {
116
+ return;
117
+ }
118
+ return (0, _bindEventListener.bind)(menuWrapper, {
119
+ type: 'keydown',
120
+ listener: handleMenuKeyDown
121
+ });
122
+ }, [handleMenuKeyDown, isLayoutColumnMenuOpen]);
83
123
  var components = (_api$uiControlRegistr = api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 ? void 0 : _api$uiControlRegistr2.actions.getComponents(_keys.LAYOUT_COLUMN_MENU.key)) !== null && _api$uiControlRegistr !== void 0 ? _api$uiControlRegistr : [];
84
124
  var target = (0, _react.useMemo)(function () {
85
125
  return isLayoutColumnMenuOpen ? getLayoutColumnMenuTarget(editorView, selection, layoutColumnMenuAnchorPos) : null;
@@ -104,16 +144,23 @@ var LayoutColumnMenu = exports.LayoutColumnMenu = /*#__PURE__*/_react.default.me
104
144
  stick: true,
105
145
  offset: LAYOUT_COLUMN_MENU_POPUP_OFFSET,
106
146
  handleClickOutside: handleClickOutside,
107
- handleEscapeKeydown: closeLayoutColumnMenu
147
+ handleEscapeKeydown: closeLayoutColumnMenu,
148
+ focusTrap: openedViaKeyboard ? focusTrap : undefined
149
+ }, /*#__PURE__*/_react.default.createElement("div", {
150
+ ref: menuWrapperRef
108
151
  }, /*#__PURE__*/_react.default.createElement(_userIntent.UserIntentPopupWrapper, {
109
152
  api: api,
110
153
  userIntent: "layoutColumnMenuPopupOpen"
111
154
  }, /*#__PURE__*/_react.default.createElement(_editorToolbar.ToolbarDropdownMenuProvider, {
112
155
  isOpen: isLayoutColumnMenuOpen,
113
156
  setIsOpen: handleSetIsOpen
157
+ }, /*#__PURE__*/_react.default.createElement(_uiMenu.ArrowKeyNavigationProvider, {
158
+ type: _uiMenu.ArrowKeyNavigationType.MENU,
159
+ handleClose: handleArrowKeyNavigationClose,
160
+ disableArrowKeyNavigation: shouldDisableArrowKeyNavigation
114
161
  }, /*#__PURE__*/_react.default.createElement(_editorUiControlModel.SurfaceRenderer, {
115
162
  components: components,
116
163
  fallbacks: _components.LAYOUT_COLUMN_MENU_FALLBACKS,
117
164
  surface: _keys.LAYOUT_COLUMN_MENU
118
- }))));
165
+ }))))));
119
166
  });
@@ -19,6 +19,13 @@ var _experiments = require("@atlaskit/tmp-editor-statsig/experiments");
19
19
  // eslint-disable-next-line @atlaskit/ui-styling-standard/no-global-styles, @atlaskit/ui-styling-standard/use-compiled, @typescript-eslint/consistent-type-imports
20
20
 
21
21
  var PLACEHOLDER_SELECTOR = '.ProseMirror-focused .layoutSectionView-content-wrap.selected [data-layout-column] > [data-layout-content] > p:only-child:has(.ProseMirror-trailingBreak:only-child)';
22
+ var layoutColumnDangerPreviewStyle = (0, _react2.css)({
23
+ // eslint-disable-next-line @atlaskit/ui-styling-standard/no-nested-selectors, @atlaskit/ui-styling-standard/no-unsafe-selectors -- Ignored via go/DSP-18766
24
+ '.ProseMirror [data-layout-column].layout-column-danger-preview': {
25
+ backgroundColor: "var(--ds-background-danger, #FFECEB)",
26
+ boxShadow: "inset 0 0 0 2px ".concat("var(--ds-border-danger, #E2483D)")
27
+ }
28
+ });
22
29
  var getPlaceholderStyle = function getPlaceholderStyle(message) {
23
30
  if ((0, _experiments.editorExperiment)('platform_editor_controls', 'variant1')) {
24
31
  return (0, _react2.css)((0, _defineProperty2.default)({}, "".concat(PLACEHOLDER_SELECTOR), {
@@ -56,7 +63,10 @@ var GlobalStylesWrapper = exports.GlobalStylesWrapper = function GlobalStylesWra
56
63
  var placeholderText = (0, _experiments.editorExperiment)('platform_editor_controls', 'variant1') ? _messages.layoutMessages.controlslayoutPlaceholder : _messages.layoutMessages.layoutPlaceholder;
57
64
  return getPlaceholderStyle(formatMessage(placeholderText));
58
65
  }, [formatMessage]);
66
+ var globalStyles = (0, _react.useMemo)(function () {
67
+ return [placeholderStyle, layoutColumnDangerPreviewStyle];
68
+ }, [placeholderStyle]);
59
69
  return (0, _react2.jsx)(_react2.Global, {
60
- styles: placeholderStyle
70
+ styles: globalStyles
61
71
  });
62
72
  };
@@ -11,7 +11,8 @@ 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, distributeLayoutColumns, insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
14
+ import { createDefaultLayoutSection, createMultiColumnLayoutSection, deleteLayoutColumn, distributeLayoutColumns, insertLayoutColumn, insertLayoutColumnsWithAnalytics, setLayoutColumnDangerPreview, setLayoutColumnValign, toggleLayoutColumnMenu } from './pm-plugins/actions';
15
+ import { default as createLayoutKeymapPlugin } from './pm-plugins/keymap';
15
16
  import { default as createLayoutPlugin } from './pm-plugins/main';
16
17
  import { pluginKey } from './pm-plugins/plugin-key';
17
18
  import { default as createLayoutResizingPlugin } from './pm-plugins/resizing';
@@ -52,7 +53,7 @@ export const layoutPlugin = ({
52
53
  config: options = {},
53
54
  api
54
55
  }) => {
55
- var _api$analytics2, _api$analytics5;
56
+ var _api$analytics2;
56
57
  const allowAdvancedSingleColumnLayout = editorExperiment('advanced_layouts', true) && editorExperiment('single_column_layouts', true, {
57
58
  exposure: true
58
59
  });
@@ -101,6 +102,14 @@ export const layoutPlugin = ({
101
102
  return createLayoutPlugin(options, api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions);
102
103
  }
103
104
  }];
105
+ if (expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
106
+ plugins.push({
107
+ name: 'layoutKeymap',
108
+ plugin: () => createLayoutKeymapPlugin({
109
+ api
110
+ })
111
+ });
112
+ }
104
113
  if ((options.editorAppearance === 'full-page' || options.editorAppearance === 'full-width' || options.editorAppearance === 'max' && editorExperiment('platform_editor_layout_column_resize_handle', true)) && api && editorExperiment('advanced_layouts', true)) {
105
114
  plugins.push({
106
115
  name: 'layoutResizing',
@@ -353,15 +362,19 @@ export const layoutPlugin = ({
353
362
  return pluginKey.getState(editorState);
354
363
  },
355
364
  commands: {
356
- deleteLayoutColumn: deleteLayoutColumn(api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions, api),
365
+ deleteLayoutColumn: inputMethod => {
366
+ var _api$analytics5;
367
+ return deleteLayoutColumn(api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions, api, inputMethod);
368
+ },
357
369
  distributeLayoutColumns: options => {
358
370
  var _api$analytics6;
359
371
  return distributeLayoutColumns(api === null || api === void 0 ? void 0 : (_api$analytics6 = api.analytics) === null || _api$analytics6 === void 0 ? void 0 : _api$analytics6.actions, api)(options);
360
372
  },
361
- insertLayoutColumn: side => {
373
+ insertLayoutColumn: (side, inputMethod) => {
362
374
  var _api$analytics7;
363
- return insertLayoutColumn(side, api === null || api === void 0 ? void 0 : (_api$analytics7 = api.analytics) === null || _api$analytics7 === void 0 ? void 0 : _api$analytics7.actions, api);
375
+ return insertLayoutColumn(side, api === null || api === void 0 ? void 0 : (_api$analytics7 = api.analytics) === null || _api$analytics7 === void 0 ? void 0 : _api$analytics7.actions, api, inputMethod);
364
376
  },
377
+ setLayoutColumnDangerPreview,
365
378
  setLayoutColumnValign: valign => {
366
379
  var _api$analytics8;
367
380
  return setLayoutColumnValign(valign, api === null || api === void 0 ? void 0 : (_api$analytics8 = api.analytics) === null || _api$analytics8 === void 0 ? void 0 : _api$analytics8.actions, api);
@@ -11,7 +11,7 @@ import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
11
11
  import { EVEN_DISTRIBUTED_COL_WIDTHS, MAX_LAYOUT_COLUMNS, MAX_STANDARD_LAYOUT_COLUMNS, MIN_LAYOUT_COLUMN_WIDTH_PERCENT } from './consts';
12
12
  import { pluginKey } from './plugin-key';
13
13
  import { calculateDistribution, isDistributedUniformly, redistributeAfterDeletion, redistributeProportionally } from './utils/layout-column-distribution';
14
- import { getAllLayoutColumnsFromSelection, getSelectedLayoutColumnsFromSelection } from './utils/layout-column-selection';
14
+ import { getAllLayoutColumnsFromSelection, getLayoutColumnsFromContentSelection, getSelectedLayoutColumnsFromSelection } from './utils/layout-column-selection';
15
15
  export const ONE_COL_LAYOUTS = ['single'];
16
16
  export const TWO_COL_LAYOUTS = ['two_equal', 'two_left_sidebar', 'two_right_sidebar'];
17
17
  export const THREE_COL_LAYOUTS = ['three_equal', 'three_with_sidebars'];
@@ -556,13 +556,13 @@ const mapLayoutColumnPreservedSelection = (tr, api) => {
556
556
  export function getEffectiveMaxLayoutColumns() {
557
557
  return editorExperiment('advanced_layouts', true) ? MAX_LAYOUT_COLUMNS : MAX_STANDARD_LAYOUT_COLUMNS;
558
558
  }
559
- const insertLayoutColumnAt = (side, editorAnalyticsAPI) => ({
559
+ const insertLayoutColumnAt = (side, editorAnalyticsAPI, inputMethod = INPUT_METHOD.LAYOUT_COLUMN_MENU) => ({
560
560
  tr
561
561
  }) => {
562
562
  if (!expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
563
563
  return null;
564
564
  }
565
- const selectedLayoutColumnsResult = getSelectedLayoutColumnsFromSelection(tr.selection);
565
+ const selectedLayoutColumnsResult = getLayoutColumnsFromContentSelection(tr.selection);
566
566
  if (!selectedLayoutColumnsResult || selectedLayoutColumnsResult.selectedLayoutColumns.length === 0) {
567
567
  return null;
568
568
  }
@@ -624,7 +624,7 @@ const insertLayoutColumnAt = (side, editorAnalyticsAPI) => ({
624
624
  attributes: {
625
625
  columnCount: redistributedWidths.length,
626
626
  endIndex,
627
- inputMethod: INPUT_METHOD.LAYOUT_COLUMN_MENU,
627
+ inputMethod,
628
628
  selectedCount: selectedColumnCount,
629
629
  side,
630
630
  startIndex
@@ -634,10 +634,10 @@ const insertLayoutColumnAt = (side, editorAnalyticsAPI) => ({
634
634
  tr.setMeta('scrollIntoView', false);
635
635
  return tr;
636
636
  };
637
- export const insertLayoutColumn = (side, editorAnalyticsAPI, api) => ({
637
+ export const insertLayoutColumn = (side, editorAnalyticsAPI, api, inputMethod = INPUT_METHOD.LAYOUT_COLUMN_MENU) => ({
638
638
  tr
639
639
  }) => {
640
- const result = insertLayoutColumnAt(side, editorAnalyticsAPI)({
640
+ const result = insertLayoutColumnAt(side, editorAnalyticsAPI, inputMethod)({
641
641
  tr
642
642
  });
643
643
  if (result) {
@@ -777,25 +777,40 @@ export const distributeLayoutColumns = (editorAnalyticsAPI, api) => ({
777
777
  };
778
778
  export const toggleLayoutColumnMenu = ({
779
779
  anchorPos,
780
- isOpen
780
+ isOpen,
781
+ openedViaKeyboard
781
782
  }) => ({
782
783
  tr
783
784
  }) => {
784
785
  tr.setMeta('toggleLayoutColumnMenu', {
785
786
  anchorPos,
786
- isOpen
787
+ isOpen,
788
+ openedViaKeyboard
787
789
  });
788
790
  tr.setMeta('scrollIntoView', false);
789
791
  return tr;
790
792
  };
791
- export const deleteLayoutColumn = (editorAnalyticsAPI, api) => ({
793
+ export const setLayoutColumnDangerPreview = show => ({
794
+ tr
795
+ }) => {
796
+ var _selectedLayoutColumn;
797
+ const selectedLayoutColumnsResult = getSelectedLayoutColumnsFromSelection(tr.selection);
798
+ const positions = show ? (_selectedLayoutColumn = selectedLayoutColumnsResult === null || selectedLayoutColumnsResult === void 0 ? void 0 : selectedLayoutColumnsResult.selectedLayoutColumns.map(({
799
+ pos
800
+ }) => pos)) !== null && _selectedLayoutColumn !== void 0 ? _selectedLayoutColumn : [] : null;
801
+ tr.setMeta('layoutColumnDangerPreview', positions);
802
+ tr.setMeta('addToHistory', false);
803
+ tr.setMeta('scrollIntoView', false);
804
+ return tr;
805
+ };
806
+ export const deleteLayoutColumn = (editorAnalyticsAPI, api, inputMethod = INPUT_METHOD.LAYOUT_COLUMN_MENU) => ({
792
807
  tr
793
808
  }) => {
794
809
  var _api$blockControls4;
795
810
  if (!expValEqualsNoExposure('platform_editor_layout_column_menu', 'isEnabled', true)) {
796
811
  return null;
797
812
  }
798
- const selectedLayoutColumnsResult = getSelectedLayoutColumnsFromSelection(tr.selection);
813
+ const selectedLayoutColumnsResult = getLayoutColumnsFromContentSelection(tr.selection);
799
814
  if (!selectedLayoutColumnsResult || selectedLayoutColumnsResult.selectedLayoutColumns.length === 0) {
800
815
  return null;
801
816
  }
@@ -814,7 +829,7 @@ export const deleteLayoutColumn = (editorAnalyticsAPI, api) => ({
814
829
  attributes: {
815
830
  columnCount,
816
831
  endIndex,
817
- inputMethod: INPUT_METHOD.LAYOUT_COLUMN_MENU,
832
+ inputMethod,
818
833
  selectedCount: selectedLayoutColumns.length,
819
834
  startIndex
820
835
  },
@@ -0,0 +1,26 @@
1
+ import { INPUT_METHOD } from '@atlaskit/editor-common/analytics';
2
+ import { addColumnAfter, addColumnAfterVO, addColumnBefore, addColumnBeforeVO, bindKeymapWithEditorCommand, deleteColumn, keymap } from '@atlaskit/editor-common/keymaps';
3
+ import { deleteLayoutColumn, insertLayoutColumn } from './actions';
4
+ const bindLayoutColumnShortcut = (shortcut, command, list) => {
5
+ if (!shortcut) {
6
+ return;
7
+ }
8
+ bindKeymapWithEditorCommand(shortcut, command, list);
9
+ };
10
+
11
+ /**
12
+ * Creates shortcut handlers for layout column actions.
13
+ */
14
+ function keymapPlugin({
15
+ api
16
+ }) {
17
+ var _api$analytics, _api$analytics2, _api$analytics3, _api$analytics4, _api$analytics5;
18
+ const list = {};
19
+ bindLayoutColumnShortcut(addColumnBefore.common, insertLayoutColumn('left', api === null || api === void 0 ? void 0 : (_api$analytics = api.analytics) === null || _api$analytics === void 0 ? void 0 : _api$analytics.actions, api, INPUT_METHOD.KEYBOARD), list);
20
+ bindLayoutColumnShortcut(addColumnBeforeVO.common, insertLayoutColumn('left', api === null || api === void 0 ? void 0 : (_api$analytics2 = api.analytics) === null || _api$analytics2 === void 0 ? void 0 : _api$analytics2.actions, api, INPUT_METHOD.KEYBOARD), list);
21
+ bindLayoutColumnShortcut(addColumnAfter.common, insertLayoutColumn('right', api === null || api === void 0 ? void 0 : (_api$analytics3 = api.analytics) === null || _api$analytics3 === void 0 ? void 0 : _api$analytics3.actions, api, INPUT_METHOD.KEYBOARD), list);
22
+ bindLayoutColumnShortcut(addColumnAfterVO.common, insertLayoutColumn('right', api === null || api === void 0 ? void 0 : (_api$analytics4 = api.analytics) === null || _api$analytics4 === void 0 ? void 0 : _api$analytics4.actions, api, INPUT_METHOD.KEYBOARD), list);
23
+ bindLayoutColumnShortcut(deleteColumn.common, deleteLayoutColumn(api === null || api === void 0 ? void 0 : (_api$analytics5 = api.analytics) === null || _api$analytics5 === void 0 ? void 0 : _api$analytics5.actions, api, INPUT_METHOD.KEYBOARD), list);
24
+ return keymap(list);
25
+ }
26
+ export default keymapPlugin;
@@ -53,6 +53,18 @@ const moveCursorToNextColumn = (state, dispatch) => {
53
53
  const getNodeDecoration = (pos, node) => [Decoration.node(pos, pos + node.nodeSize, {
54
54
  class: 'selected'
55
55
  })];
56
+ const getDangerPreviewDecorations = (state, positions) => {
57
+ var _positions$flatMap;
58
+ return (_positions$flatMap = positions === null || positions === void 0 ? void 0 : positions.flatMap(pos => {
59
+ const node = state.doc.nodeAt(pos);
60
+ if (!node) {
61
+ return [];
62
+ }
63
+ return [Decoration.node(pos, pos + node.nodeSize, {
64
+ class: 'layout-column-danger-preview'
65
+ })];
66
+ })) !== null && _positions$flatMap !== void 0 ? _positions$flatMap : [];
67
+ };
56
68
  const getInitialPluginState = (options, state) => {
57
69
  const maybeLayoutSection = getMaybeLayoutSection(state);
58
70
  const allowBreakout = options.allowBreakout || false;
@@ -68,9 +80,58 @@ const getInitialPluginState = (options, state) => {
68
80
  allowSingleColumnLayout,
69
81
  isResizing: false,
70
82
  isLayoutColumnMenuOpen: false,
71
- layoutColumnMenuAnchorPos: undefined
83
+ layoutColumnMenuOpenedViaKeyboard: false,
84
+ layoutColumnMenuAnchorPos: undefined,
85
+ dangerPreviewLayoutColumnPositions: undefined
72
86
  };
73
87
  };
88
+ const reduceLayoutColumnMenuState = (pluginState, action) => {
89
+ switch (action.type) {
90
+ case 'toggleLayoutColumnMenu':
91
+ {
92
+ const {
93
+ anchorPos,
94
+ isOpen,
95
+ openedViaKeyboard
96
+ } = action.meta;
97
+ const nextIsOpen = isOpen !== null && isOpen !== void 0 ? isOpen : !pluginState.isLayoutColumnMenuOpen;
98
+ return {
99
+ ...pluginState,
100
+ isLayoutColumnMenuOpen: nextIsOpen,
101
+ layoutColumnMenuOpenedViaKeyboard: nextIsOpen ? openedViaKeyboard !== null && openedViaKeyboard !== void 0 ? openedViaKeyboard : false : false,
102
+ layoutColumnMenuAnchorPos: nextIsOpen ? anchorPos : undefined,
103
+ dangerPreviewLayoutColumnPositions: nextIsOpen ? pluginState.dangerPreviewLayoutColumnPositions : undefined
104
+ };
105
+ }
106
+ case 'setDangerPreview':
107
+ return {
108
+ ...pluginState,
109
+ dangerPreviewLayoutColumnPositions: action.positions
110
+ };
111
+ case 'clearDangerPreview':
112
+ return {
113
+ ...pluginState,
114
+ dangerPreviewLayoutColumnPositions: undefined
115
+ };
116
+ case 'setResizeState':
117
+ return {
118
+ ...pluginState,
119
+ isResizing: action.isResizing
120
+ };
121
+ case 'syncSelectionState':
122
+ {
123
+ const maybeLayoutSection = getMaybeLayoutSection(action.state);
124
+ return {
125
+ ...pluginState,
126
+ pos: maybeLayoutSection ? maybeLayoutSection.pos : null,
127
+ selectedLayout: getSelectedLayout(maybeLayoutSection && maybeLayoutSection.node,
128
+ // Ignored via go/ees005
129
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
130
+ pluginState.selectedLayout)
131
+ };
132
+ }
133
+ }
134
+ };
74
135
 
75
136
  // To prevent a single-column layout,
76
137
  // if a user attempts to delete a layout column and
@@ -115,31 +176,39 @@ export default ((options, editorAnalyticsAPI) => {
115
176
  state: {
116
177
  init: (_, state) => getInitialPluginState(options, state),
117
178
  apply: (tr, pluginState, oldState, newState) => {
118
- var _columnMenuMeta$isOpe, _tr$getMeta, _pluginKey$getState;
179
+ var _tr$getMeta, _pluginKey$getState;
180
+ let nextPluginState = pluginState;
119
181
  const columnMenuMeta = tr.getMeta('toggleLayoutColumnMenu');
120
- const nextPluginState = columnMenuMeta ? {
121
- ...pluginState,
122
- isLayoutColumnMenuOpen: (_columnMenuMeta$isOpe = columnMenuMeta.isOpen) !== null && _columnMenuMeta$isOpe !== void 0 ? _columnMenuMeta$isOpe : !pluginState.isLayoutColumnMenuOpen,
123
- layoutColumnMenuAnchorPos: columnMenuMeta.isOpen === false ? undefined : columnMenuMeta.anchorPos
124
- } : pluginState;
182
+ const dangerPreviewMeta = tr.getMeta('layoutColumnDangerPreview');
183
+ if (columnMenuMeta) {
184
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
185
+ meta: columnMenuMeta,
186
+ type: 'toggleLayoutColumnMenu'
187
+ });
188
+ }
189
+ if (tr.getMeta('layoutColumnDangerPreview') !== undefined) {
190
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
191
+ positions: dangerPreviewMeta !== null && dangerPreviewMeta !== void 0 ? dangerPreviewMeta : undefined,
192
+ type: 'setDangerPreview'
193
+ });
194
+ }
195
+ if (tr.docChanged) {
196
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
197
+ type: 'clearDangerPreview'
198
+ });
199
+ }
125
200
  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;
201
+ nextPluginState = reduceLayoutColumnMenuState(nextPluginState, {
202
+ isResizing,
203
+ type: 'setResizeState'
204
+ });
126
205
  if (tr.docChanged || tr.selectionSet) {
127
- const maybeLayoutSection = getMaybeLayoutSection(newState);
128
- const newPluginState = {
129
- ...nextPluginState,
130
- pos: maybeLayoutSection ? maybeLayoutSection.pos : null,
131
- isResizing,
132
- selectedLayout: getSelectedLayout(maybeLayoutSection && maybeLayoutSection.node,
133
- // Ignored via go/ees005
134
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
135
- pluginState.selectedLayout)
136
- };
137
- return newPluginState;
206
+ return reduceLayoutColumnMenuState(nextPluginState, {
207
+ state: newState,
208
+ type: 'syncSelectionState'
209
+ });
138
210
  }
139
- return {
140
- ...nextPluginState,
141
- isResizing
142
- };
211
+ return nextPluginState;
143
212
  }
144
213
  },
145
214
  props: {
@@ -149,14 +218,17 @@ export default ((options, editorAnalyticsAPI) => {
149
218
  if (editorExperiment('advanced_layouts', true) && editorExperiment('platform_editor_layout_column_resize_handle', true) && isLayoutResizingPluginAvailable) {
150
219
  const dividerDecorations = getColumnDividerDecorations(state, editorViewRef, editorAnalyticsAPI);
151
220
  const selectedDecorations = layoutState.pos !== null ? getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)) : [];
152
- const allDecorations = [...selectedDecorations, ...dividerDecorations];
221
+ const dangerPreviewDecorations = getDangerPreviewDecorations(state, layoutState.dangerPreviewLayoutColumnPositions);
222
+ const allDecorations = [...selectedDecorations, ...dividerDecorations, ...dangerPreviewDecorations];
153
223
  if (allDecorations.length > 0) {
154
224
  return DecorationSet.create(state.doc, allDecorations);
155
225
  }
156
226
  return undefined;
157
227
  }
158
- if (layoutState.pos !== null) {
159
- return DecorationSet.create(state.doc, getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)));
228
+ const dangerPreviewDecorations = getDangerPreviewDecorations(state, layoutState.dangerPreviewLayoutColumnPositions);
229
+ if (layoutState.pos !== null || dangerPreviewDecorations.length > 0) {
230
+ const selectedDecorations = layoutState.pos !== null ? getNodeDecoration(layoutState.pos, state.doc.nodeAt(layoutState.pos)) : [];
231
+ return DecorationSet.create(state.doc, [...selectedDecorations, ...dangerPreviewDecorations]);
160
232
  }
161
233
  return undefined;
162
234
  },
@@ -22,7 +22,7 @@ const findLayoutColumnsFromLayoutSection = (layoutSectionNode, layoutSectionPos
22
22
  pos: pos + layoutSectionPos + 1
23
23
  }));
24
24
  };
25
- export const getSelectedLayoutColumnsFromSelection = selection => {
25
+ const getSelectedLayoutColumns = (selection, isColumnSelected) => {
26
26
  const layoutSection = findLayoutSectionFromSelection(selection);
27
27
  if (!layoutSection) {
28
28
  return undefined;
@@ -37,18 +37,15 @@ export const getSelectedLayoutColumnsFromSelection = selection => {
37
37
  }
38
38
  let startIndex = -1;
39
39
  let endIndex = -1;
40
- const selectedLayoutColumns = allLayoutColumns.filter(({
41
- node,
42
- pos
43
- }, index) => {
44
- const isSelected = selection.from <= pos && selection.to >= pos + node.nodeSize;
45
- if (isSelected) {
40
+ const selectedLayoutColumns = allLayoutColumns.filter((column, index) => {
41
+ if (isColumnSelected(column, index)) {
46
42
  if (startIndex === -1) {
47
43
  startIndex = index;
48
44
  }
49
45
  endIndex = index;
46
+ return true;
50
47
  }
51
- return isSelected;
48
+ return false;
52
49
  });
53
50
  return {
54
51
  layoutSectionNode,
@@ -58,6 +55,34 @@ export const getSelectedLayoutColumnsFromSelection = selection => {
58
55
  endIndex
59
56
  };
60
57
  };
58
+ export const getSelectedLayoutColumnsFromSelection = selection => {
59
+ return getSelectedLayoutColumns(selection, ({
60
+ node,
61
+ pos
62
+ }) => {
63
+ // NodeSelection on a layout column is clearly selected.
64
+ if (selection instanceof NodeSelection && selection.node === node) {
65
+ return true;
66
+ }
67
+
68
+ // For TextSelection, only count columns that are fully contained within the selection
69
+ // (not partial text selections inside a column).
70
+ const nodeEndPos = pos + node.nodeSize;
71
+ return !selection.empty && selection.from <= pos && selection.to >= nodeEndPos;
72
+ });
73
+ };
74
+ export const getLayoutColumnsFromContentSelection = selection => {
75
+ return getSelectedLayoutColumns(selection, ({
76
+ node,
77
+ pos
78
+ }) => {
79
+ if (selection instanceof NodeSelection && selection.node === node) {
80
+ return true;
81
+ }
82
+ const nodeEndPos = pos + node.nodeSize;
83
+ return selection.empty ? selection.from > pos && selection.from < nodeEndPos : selection.from < nodeEndPos && selection.to > pos;
84
+ });
85
+ };
61
86
  export const getAllLayoutColumnsFromSelection = selection => {
62
87
  const layoutSection = findLayoutSectionFromSelection(selection);
63
88
  if (!layoutSection) {
@@ -1,25 +1,37 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { useIntl } from 'react-intl';
3
+ import { deleteColumn, getAriaKeyshortcuts, tooltip } from '@atlaskit/editor-common/keymaps';
3
4
  import { layoutMessages } from '@atlaskit/editor-common/messages';
4
- import { DeleteIcon, ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
+ import { DeleteIcon, ToolbarDropdownItem, ToolbarKeyboardShortcutHint } from '@atlaskit/editor-toolbar';
5
6
  import { useSelectedLayoutColumns } from './useSelectedLayoutColumns';
6
7
  const DeleteColumnDropdownItem = ({
7
8
  api
8
9
  }) => {
10
+ var _tooltip;
9
11
  const {
10
12
  formatMessage
11
13
  } = useIntl();
12
14
  const selectedLayoutColumns = useSelectedLayoutColumns(api);
15
+ const setDangerPreview = useCallback(show => {
16
+ var _api$core, _api$layout;
17
+ api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(api === null || api === void 0 ? void 0 : (_api$layout = api.layout) === null || _api$layout === void 0 ? void 0 : _api$layout.commands.setLayoutColumnDangerPreview(show));
18
+ }, [api]);
19
+ const showDangerPreview = useCallback(() => {
20
+ setDangerPreview(true);
21
+ }, [setDangerPreview]);
22
+ const hideDangerPreview = useCallback(() => {
23
+ setDangerPreview(false);
24
+ }, [setDangerPreview]);
13
25
  const onClick = useCallback(() => {
14
- var _api$layout, _api$core;
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;
16
- api === null || api === void 0 ? void 0 : (_api$core = api.core) === null || _api$core === void 0 ? void 0 : _api$core.actions.execute(props => {
17
- var _api$layout2;
26
+ var _api$layout2, _api$core2;
27
+ const deleteCommand = api === null || api === void 0 ? void 0 : (_api$layout2 = api.layout) === null || _api$layout2 === void 0 ? void 0 : _api$layout2.commands.deleteLayoutColumn();
28
+ api === null || api === void 0 ? void 0 : (_api$core2 = api.core) === null || _api$core2 === void 0 ? void 0 : _api$core2.actions.execute(props => {
29
+ var _api$layout3;
18
30
  const tr = deleteCommand === null || deleteCommand === void 0 ? void 0 : deleteCommand(props);
19
31
  if (!tr) {
20
32
  return tr !== null && tr !== void 0 ? tr : null;
21
33
  }
22
- api === null || api === void 0 ? void 0 : (_api$layout2 = api.layout) === null || _api$layout2 === void 0 ? void 0 : _api$layout2.commands.toggleLayoutColumnMenu({
34
+ api === null || api === void 0 ? void 0 : (_api$layout3 = api.layout) === null || _api$layout3 === void 0 ? void 0 : _api$layout3.commands.toggleLayoutColumnMenu({
23
35
  isOpen: false
24
36
  })({
25
37
  tr
@@ -32,11 +44,19 @@ const DeleteColumnDropdownItem = ({
32
44
  }
33
45
  const selectedColumnCount = selectedLayoutColumns.selectedLayoutColumns.length;
34
46
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
47
+ ariaKeyshortcuts: getAriaKeyshortcuts(deleteColumn),
35
48
  onClick: onClick,
49
+ onFocus: showDangerPreview,
50
+ onMouseEnter: showDangerPreview,
51
+ onBlur: hideDangerPreview,
52
+ onMouseLeave: hideDangerPreview,
36
53
  elemBefore: /*#__PURE__*/React.createElement(DeleteIcon, {
37
54
  color: "currentColor",
38
55
  label: "",
39
56
  size: "small"
57
+ }),
58
+ elemAfter: /*#__PURE__*/React.createElement(ToolbarKeyboardShortcutHint, {
59
+ shortcut: (_tooltip = tooltip(deleteColumn)) !== null && _tooltip !== void 0 ? _tooltip : ''
40
60
  })
41
61
  }, formatMessage(layoutMessages.deleteColumn, {
42
62
  count: selectedColumnCount