@atlaskit/editor-plugin-layout 13.2.3 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,20 @@
1
1
  # @atlaskit/editor-plugin-layout
2
2
 
3
+ ## 13.3.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [`5b844f57bfad8`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/5b844f57bfad8) -
8
+ [ED-7731] Preserve the editor selection when inserting, deleting, or resizing layout columns with
9
+ the cursor inside a column, instead of moving it out of the layout. Gated behind
10
+ platform_editor_layout_column_menu_kill_switch_1.
11
+
12
+ ## 13.2.4
13
+
14
+ ### Patch Changes
15
+
16
+ - Updated dependencies
17
+
3
18
  ## 13.2.3
4
19
 
5
20
  ### Patch Changes
@@ -566,6 +566,38 @@ var getPreviousLayoutColumnValign = function getPreviousLayoutColumnValign(selec
566
566
  var hasLayoutColumnContent = function hasLayoutColumnContent(node) {
567
567
  return !(0, _utils.isEmptyDocument)(node);
568
568
  };
569
+
570
+ /**
571
+ * Remaps a selection through a position mapping, preserving its type. Used after replacing a
572
+ * layout section's contents so the selection stays in its column instead of being mapped out
573
+ * of the layout. Returns `undefined` if no valid selection can be derived (NodeSelection
574
+ * whose node is no longer selectable falls back to a nearby caret).
575
+ */
576
+ var remapSelectionThroughMapping = function remapSelectionThroughMapping(selection, mapping, doc) {
577
+ var docSize = doc.content.size;
578
+ var clamp = function clamp(pos) {
579
+ return Math.min(Math.max(pos, 0), docSize);
580
+ };
581
+ if (selection instanceof _state.NodeSelection) {
582
+ var _TextSelection$findFr;
583
+ var mappedPos = clamp(mapping.map(selection.from));
584
+ var nodeAtPos = doc.nodeAt(mappedPos);
585
+ if (nodeAtPos && _state.NodeSelection.isSelectable(nodeAtPos)) {
586
+ return _state.NodeSelection.create(doc, mappedPos);
587
+ }
588
+ return (_TextSelection$findFr = _state.TextSelection.findFrom(doc.resolve(mappedPos), 1, true)) !== null && _TextSelection$findFr !== void 0 ? _TextSelection$findFr : undefined;
589
+ }
590
+ if (selection instanceof _state.TextSelection) {
591
+ var mappedFrom = clamp(mapping.map(selection.from));
592
+ var mappedTo = clamp(mapping.map(selection.to));
593
+ try {
594
+ return _state.TextSelection.create(doc, mappedFrom, mappedTo);
595
+ } catch (_unused) {
596
+ return undefined;
597
+ }
598
+ }
599
+ return undefined;
600
+ };
569
601
  var mapLayoutColumnPreservedSelection = function mapLayoutColumnPreservedSelection(tr, api) {
570
602
  var insertMeta = tr.getMeta(LAYOUT_COLUMN_INSERT_META);
571
603
  if (insertMeta) {
@@ -651,7 +683,22 @@ var insertLayoutColumnAt = function insertLayoutColumnAt(side, editorAnalyticsAP
651
683
  insertedColumnPos: insertedColumnPos,
652
684
  side: side
653
685
  });
686
+
687
+ // Capture the selection before the section content is replaced (the replace below maps
688
+ // it out of the layout by default). The menu path restores its own preserved selection
689
+ // afterwards, so this restoration only matters for the cursor-in-column case.
690
+ var originalSelection = tr.selection;
654
691
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributedWidths));
692
+
693
+ // Inserting left shifts positions at/after the new column right by its size; inserting
694
+ // right leaves them unchanged. Remap the original selection through that mapping.
695
+ if (!(0, _platformFeatureFlags.fg)('platform_editor_layout_column_menu_kill_switch_1')) {
696
+ var insertMapping = side === 'left' ? new _transform.Mapping([new _transform.StepMap([insertedColumnPos, 0, newColumn.nodeSize])]) : new _transform.Mapping();
697
+ var restoredSelection = remapSelectionThroughMapping(originalSelection, insertMapping, tr.doc);
698
+ if (restoredSelection) {
699
+ tr.setSelection(restoredSelection);
700
+ }
701
+ }
655
702
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
656
703
  action: _analytics.ACTION.INSERTED,
657
704
  actionSubject: _analytics.ACTION_SUBJECT.DOCUMENT,
@@ -927,7 +974,35 @@ var deleteLayoutColumn = exports.deleteLayoutColumn = function deleteLayoutColum
927
974
  return (0, _layoutColumnDistribution.redistributeAfterDeletion)(widths, selectedIndex, _consts.MIN_LAYOUT_COLUMN_WIDTH_PERCENT);
928
975
  }, existingWidths);
929
976
  var updatedLayoutSectionNode = layoutSectionNode.copy(_model.Fragment.fromArray(remainingColumns));
977
+
978
+ // The cursor-in-column (keyboard) path has a plain text selection; the menu path has a
979
+ // column NodeSelection whose post-delete landing is owned by the block-controls
980
+ // preserved-selection plugin, so we only restore the caret for the former.
981
+ var hadTextSelection = tr.selection instanceof _state.TextSelection;
930
982
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributed));
983
+
984
+ // Land the caret in a remaining column — the one now occupying the deleted slot, or the
985
+ // last column when the deleted slot no longer exists. Otherwise the replace above maps
986
+ // the caret out of the layout to the following paragraph.
987
+ var remainingColumnCount = remainingColumns.length;
988
+ if (hadTextSelection && !(0, _platformFeatureFlags.fg)('platform_editor_layout_column_menu_kill_switch_1') && remainingColumnCount > 0) {
989
+ var targetColumnIndex = Math.min(startIndex, remainingColumnCount - 1);
990
+ var updatedSectionNode = tr.doc.nodeAt(layoutSectionPos);
991
+ if (updatedSectionNode) {
992
+ var columnOffset = 1;
993
+ for (var columnIndex = 0; columnIndex < targetColumnIndex; columnIndex++) {
994
+ columnOffset += updatedSectionNode.child(columnIndex).nodeSize;
995
+ }
996
+ // +1 to land inside the column's first child rather than on the column boundary.
997
+ var caretPos = layoutSectionPos + columnOffset + 1;
998
+ if (caretPos >= 0 && caretPos <= tr.doc.content.size) {
999
+ var caretSelection = _state.TextSelection.findFrom(tr.doc.resolve(caretPos), 1, true);
1000
+ if (caretSelection) {
1001
+ tr.setSelection(caretSelection);
1002
+ }
1003
+ }
1004
+ }
1005
+ }
931
1006
  emitDeleteColumnAnalytics(redistributed.length);
932
1007
  tr.setMeta('scrollIntoView', false);
933
1008
  api === null || api === void 0 || (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 || _api$blockControls4.commands.stopPreservingSelection()({
@@ -9,11 +9,41 @@ var _defineProperty2 = _interopRequireDefault(require("@babel/runtime/helpers/de
9
9
  var _bindEventListener = require("bind-event-listener");
10
10
  var _analytics = require("@atlaskit/editor-common/analytics");
11
11
  var _model = require("@atlaskit/editor-prosemirror/model");
12
+ var _state = require("@atlaskit/editor-prosemirror/state");
12
13
  var _view = require("@atlaskit/editor-prosemirror/view");
13
14
  var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
14
15
  var _consts = require("./consts");
15
16
  function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
16
17
  function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { (0, _defineProperty2.default)(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
18
+ /**
19
+ * Re-creates a selection at the same positions against the resized document. A resize only
20
+ * changes width attrs (not node count, order, or size), so the original positions are still
21
+ * valid. Returns `undefined` if no valid selection can be made.
22
+ */
23
+ var remapResizeSelection = function remapResizeSelection(selection, doc) {
24
+ var docSize = doc.content.size;
25
+ var clamp = function clamp(pos) {
26
+ return Math.min(Math.max(pos, 0), docSize);
27
+ };
28
+ if (selection instanceof _state.NodeSelection) {
29
+ var _TextSelection$findFr;
30
+ var pos = clamp(selection.from);
31
+ var nodeAtPos = doc.nodeAt(pos);
32
+ if (nodeAtPos && _state.NodeSelection.isSelectable(nodeAtPos)) {
33
+ return _state.NodeSelection.create(doc, pos);
34
+ }
35
+ return (_TextSelection$findFr = _state.TextSelection.findFrom(doc.resolve(pos), 1, true)) !== null && _TextSelection$findFr !== void 0 ? _TextSelection$findFr : undefined;
36
+ }
37
+ if (selection instanceof _state.TextSelection) {
38
+ try {
39
+ return _state.TextSelection.create(doc, clamp(selection.from), clamp(selection.to));
40
+ } catch (_unused) {
41
+ return undefined;
42
+ }
43
+ }
44
+ return undefined;
45
+ };
46
+
17
47
  // Class names for the column resize divider widget — must stay in sync with layout.ts in editor-core
18
48
  var layoutColumnDividerClassName = 'layout-column-divider';
19
49
  var layoutColumnDividerRailClassName = 'layout-column-divider-rail';
@@ -50,9 +80,20 @@ var dispatchColumnWidths = function dispatchColumnWidths(view, sectionPos, leftC
50
80
  newColumns.push(child);
51
81
  }
52
82
  });
83
+
84
+ // Capture the selection before the replace below, which otherwise maps the caret out of
85
+ // the layout to the following paragraph. Restored at the same positions afterwards.
86
+ var shouldPreserveSelection = !(0, _platformFeatureFlags.fg)('platform_editor_layout_column_menu_kill_switch_1');
87
+ var selectionBeforeResize = shouldPreserveSelection ? state.selection : null;
53
88
  tr.replaceWith(sectionPos + 1, sectionPos + sectionNode.nodeSize - 1, _model.Fragment.from(newColumns));
54
89
  tr.setMeta('layoutColumnResize', true);
55
90
  tr.setMeta('scrollIntoView', false);
91
+ if (selectionBeforeResize) {
92
+ var restoredSelection = remapResizeSelection(selectionBeforeResize, tr.doc);
93
+ if (restoredSelection) {
94
+ tr.setSelection(restoredSelection);
95
+ }
96
+ }
56
97
  if ((0, _platformFeatureFlags.fg)('platform_editor_layout_resize_analytics')) {
57
98
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
58
99
  action: _analytics.ACTION.DRAGGED,
@@ -539,6 +539,36 @@ const getPreviousLayoutColumnValign = selectedLayoutColumns => {
539
539
  return hasMixedValign ? 'mixed' : firstValign !== null && firstValign !== void 0 ? firstValign : DEFAULT_LAYOUT_COLUMN_VALIGN;
540
540
  };
541
541
  const hasLayoutColumnContent = node => !isEmptyDocument(node);
542
+
543
+ /**
544
+ * Remaps a selection through a position mapping, preserving its type. Used after replacing a
545
+ * layout section's contents so the selection stays in its column instead of being mapped out
546
+ * of the layout. Returns `undefined` if no valid selection can be derived (NodeSelection
547
+ * whose node is no longer selectable falls back to a nearby caret).
548
+ */
549
+ const remapSelectionThroughMapping = (selection, mapping, doc) => {
550
+ const docSize = doc.content.size;
551
+ const clamp = pos => Math.min(Math.max(pos, 0), docSize);
552
+ if (selection instanceof NodeSelection) {
553
+ var _TextSelection$findFr;
554
+ const mappedPos = clamp(mapping.map(selection.from));
555
+ const nodeAtPos = doc.nodeAt(mappedPos);
556
+ if (nodeAtPos && NodeSelection.isSelectable(nodeAtPos)) {
557
+ return NodeSelection.create(doc, mappedPos);
558
+ }
559
+ return (_TextSelection$findFr = TextSelection.findFrom(doc.resolve(mappedPos), 1, true)) !== null && _TextSelection$findFr !== void 0 ? _TextSelection$findFr : undefined;
560
+ }
561
+ if (selection instanceof TextSelection) {
562
+ const mappedFrom = clamp(mapping.map(selection.from));
563
+ const mappedTo = clamp(mapping.map(selection.to));
564
+ try {
565
+ return TextSelection.create(doc, mappedFrom, mappedTo);
566
+ } catch {
567
+ return undefined;
568
+ }
569
+ }
570
+ return undefined;
571
+ };
542
572
  const mapLayoutColumnPreservedSelection = (tr, api) => {
543
573
  const insertMeta = tr.getMeta(LAYOUT_COLUMN_INSERT_META);
544
574
  if (insertMeta) {
@@ -625,7 +655,22 @@ const insertLayoutColumnAt = (side, editorAnalyticsAPI, inputMethod = INPUT_METH
625
655
  insertedColumnPos,
626
656
  side
627
657
  });
658
+
659
+ // Capture the selection before the section content is replaced (the replace below maps
660
+ // it out of the layout by default). The menu path restores its own preserved selection
661
+ // afterwards, so this restoration only matters for the cursor-in-column case.
662
+ const originalSelection = tr.selection;
628
663
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributedWidths));
664
+
665
+ // Inserting left shifts positions at/after the new column right by its size; inserting
666
+ // right leaves them unchanged. Remap the original selection through that mapping.
667
+ if (!fg('platform_editor_layout_column_menu_kill_switch_1')) {
668
+ const insertMapping = side === 'left' ? new Mapping([new StepMap([insertedColumnPos, 0, newColumn.nodeSize])]) : new Mapping();
669
+ const restoredSelection = remapSelectionThroughMapping(originalSelection, insertMapping, tr.doc);
670
+ if (restoredSelection) {
671
+ tr.setSelection(restoredSelection);
672
+ }
673
+ }
629
674
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
630
675
  action: ACTION.INSERTED,
631
676
  actionSubject: ACTION_SUBJECT.DOCUMENT,
@@ -888,7 +933,35 @@ export const deleteLayoutColumn = ({
888
933
  // as each redistribution step shrinks the widths array.
889
934
  .reverse().reduce((widths, selectedIndex) => redistributeAfterDeletion(widths, selectedIndex, MIN_LAYOUT_COLUMN_WIDTH_PERCENT), existingWidths);
890
935
  const updatedLayoutSectionNode = layoutSectionNode.copy(Fragment.fromArray(remainingColumns));
936
+
937
+ // The cursor-in-column (keyboard) path has a plain text selection; the menu path has a
938
+ // column NodeSelection whose post-delete landing is owned by the block-controls
939
+ // preserved-selection plugin, so we only restore the caret for the former.
940
+ const hadTextSelection = tr.selection instanceof TextSelection;
891
941
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributed));
942
+
943
+ // Land the caret in a remaining column — the one now occupying the deleted slot, or the
944
+ // last column when the deleted slot no longer exists. Otherwise the replace above maps
945
+ // the caret out of the layout to the following paragraph.
946
+ const remainingColumnCount = remainingColumns.length;
947
+ if (hadTextSelection && !fg('platform_editor_layout_column_menu_kill_switch_1') && remainingColumnCount > 0) {
948
+ const targetColumnIndex = Math.min(startIndex, remainingColumnCount - 1);
949
+ const updatedSectionNode = tr.doc.nodeAt(layoutSectionPos);
950
+ if (updatedSectionNode) {
951
+ let columnOffset = 1;
952
+ for (let columnIndex = 0; columnIndex < targetColumnIndex; columnIndex++) {
953
+ columnOffset += updatedSectionNode.child(columnIndex).nodeSize;
954
+ }
955
+ // +1 to land inside the column's first child rather than on the column boundary.
956
+ const caretPos = layoutSectionPos + columnOffset + 1;
957
+ if (caretPos >= 0 && caretPos <= tr.doc.content.size) {
958
+ const caretSelection = TextSelection.findFrom(tr.doc.resolve(caretPos), 1, true);
959
+ if (caretSelection) {
960
+ tr.setSelection(caretSelection);
961
+ }
962
+ }
963
+ }
964
+ }
892
965
  emitDeleteColumnAnalytics(redistributed.length);
893
966
  tr.setMeta('scrollIntoView', false);
894
967
  api === null || api === void 0 ? void 0 : (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 ? void 0 : _api$blockControls4.commands.stopPreservingSelection()({
@@ -1,10 +1,38 @@
1
1
  import { bind } from 'bind-event-listener';
2
2
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
3
3
  import { Fragment } from '@atlaskit/editor-prosemirror/model';
4
+ import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
4
5
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
5
6
  import { fg } from '@atlaskit/platform-feature-flags';
6
7
  import { MIN_LAYOUT_COLUMN_WIDTH_PERCENT } from './consts';
7
8
 
9
+ /**
10
+ * Re-creates a selection at the same positions against the resized document. A resize only
11
+ * changes width attrs (not node count, order, or size), so the original positions are still
12
+ * valid. Returns `undefined` if no valid selection can be made.
13
+ */
14
+ const remapResizeSelection = (selection, doc) => {
15
+ const docSize = doc.content.size;
16
+ const clamp = pos => Math.min(Math.max(pos, 0), docSize);
17
+ if (selection instanceof NodeSelection) {
18
+ var _TextSelection$findFr;
19
+ const pos = clamp(selection.from);
20
+ const nodeAtPos = doc.nodeAt(pos);
21
+ if (nodeAtPos && NodeSelection.isSelectable(nodeAtPos)) {
22
+ return NodeSelection.create(doc, pos);
23
+ }
24
+ return (_TextSelection$findFr = TextSelection.findFrom(doc.resolve(pos), 1, true)) !== null && _TextSelection$findFr !== void 0 ? _TextSelection$findFr : undefined;
25
+ }
26
+ if (selection instanceof TextSelection) {
27
+ try {
28
+ return TextSelection.create(doc, clamp(selection.from), clamp(selection.to));
29
+ } catch {
30
+ return undefined;
31
+ }
32
+ }
33
+ return undefined;
34
+ };
35
+
8
36
  // Class names for the column resize divider widget — must stay in sync with layout.ts in editor-core
9
37
  const layoutColumnDividerClassName = 'layout-column-divider';
10
38
  const layoutColumnDividerRailClassName = 'layout-column-divider-rail';
@@ -46,9 +74,20 @@ const dispatchColumnWidths = (view, sectionPos, leftColIndex, leftWidth, rightWi
46
74
  newColumns.push(child);
47
75
  }
48
76
  });
77
+
78
+ // Capture the selection before the replace below, which otherwise maps the caret out of
79
+ // the layout to the following paragraph. Restored at the same positions afterwards.
80
+ const shouldPreserveSelection = !fg('platform_editor_layout_column_menu_kill_switch_1');
81
+ const selectionBeforeResize = shouldPreserveSelection ? state.selection : null;
49
82
  tr.replaceWith(sectionPos + 1, sectionPos + sectionNode.nodeSize - 1, Fragment.from(newColumns));
50
83
  tr.setMeta('layoutColumnResize', true);
51
84
  tr.setMeta('scrollIntoView', false);
85
+ if (selectionBeforeResize) {
86
+ const restoredSelection = remapResizeSelection(selectionBeforeResize, tr.doc);
87
+ if (restoredSelection) {
88
+ tr.setSelection(restoredSelection);
89
+ }
90
+ }
52
91
  if (fg('platform_editor_layout_resize_analytics')) {
53
92
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
54
93
  action: ACTION.DRAGGED,
@@ -556,6 +556,38 @@ var getPreviousLayoutColumnValign = function getPreviousLayoutColumnValign(selec
556
556
  var hasLayoutColumnContent = function hasLayoutColumnContent(node) {
557
557
  return !isEmptyDocument(node);
558
558
  };
559
+
560
+ /**
561
+ * Remaps a selection through a position mapping, preserving its type. Used after replacing a
562
+ * layout section's contents so the selection stays in its column instead of being mapped out
563
+ * of the layout. Returns `undefined` if no valid selection can be derived (NodeSelection
564
+ * whose node is no longer selectable falls back to a nearby caret).
565
+ */
566
+ var remapSelectionThroughMapping = function remapSelectionThroughMapping(selection, mapping, doc) {
567
+ var docSize = doc.content.size;
568
+ var clamp = function clamp(pos) {
569
+ return Math.min(Math.max(pos, 0), docSize);
570
+ };
571
+ if (selection instanceof NodeSelection) {
572
+ var _TextSelection$findFr;
573
+ var mappedPos = clamp(mapping.map(selection.from));
574
+ var nodeAtPos = doc.nodeAt(mappedPos);
575
+ if (nodeAtPos && NodeSelection.isSelectable(nodeAtPos)) {
576
+ return NodeSelection.create(doc, mappedPos);
577
+ }
578
+ return (_TextSelection$findFr = TextSelection.findFrom(doc.resolve(mappedPos), 1, true)) !== null && _TextSelection$findFr !== void 0 ? _TextSelection$findFr : undefined;
579
+ }
580
+ if (selection instanceof TextSelection) {
581
+ var mappedFrom = clamp(mapping.map(selection.from));
582
+ var mappedTo = clamp(mapping.map(selection.to));
583
+ try {
584
+ return TextSelection.create(doc, mappedFrom, mappedTo);
585
+ } catch (_unused) {
586
+ return undefined;
587
+ }
588
+ }
589
+ return undefined;
590
+ };
559
591
  var mapLayoutColumnPreservedSelection = function mapLayoutColumnPreservedSelection(tr, api) {
560
592
  var insertMeta = tr.getMeta(LAYOUT_COLUMN_INSERT_META);
561
593
  if (insertMeta) {
@@ -641,7 +673,22 @@ var insertLayoutColumnAt = function insertLayoutColumnAt(side, editorAnalyticsAP
641
673
  insertedColumnPos: insertedColumnPos,
642
674
  side: side
643
675
  });
676
+
677
+ // Capture the selection before the section content is replaced (the replace below maps
678
+ // it out of the layout by default). The menu path restores its own preserved selection
679
+ // afterwards, so this restoration only matters for the cursor-in-column case.
680
+ var originalSelection = tr.selection;
644
681
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributedWidths));
682
+
683
+ // Inserting left shifts positions at/after the new column right by its size; inserting
684
+ // right leaves them unchanged. Remap the original selection through that mapping.
685
+ if (!fg('platform_editor_layout_column_menu_kill_switch_1')) {
686
+ var insertMapping = side === 'left' ? new Mapping([new StepMap([insertedColumnPos, 0, newColumn.nodeSize])]) : new Mapping();
687
+ var restoredSelection = remapSelectionThroughMapping(originalSelection, insertMapping, tr.doc);
688
+ if (restoredSelection) {
689
+ tr.setSelection(restoredSelection);
690
+ }
691
+ }
645
692
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
646
693
  action: ACTION.INSERTED,
647
694
  actionSubject: ACTION_SUBJECT.DOCUMENT,
@@ -917,7 +964,35 @@ export var deleteLayoutColumn = function deleteLayoutColumn() {
917
964
  return redistributeAfterDeletion(widths, selectedIndex, MIN_LAYOUT_COLUMN_WIDTH_PERCENT);
918
965
  }, existingWidths);
919
966
  var updatedLayoutSectionNode = layoutSectionNode.copy(Fragment.fromArray(remainingColumns));
967
+
968
+ // The cursor-in-column (keyboard) path has a plain text selection; the menu path has a
969
+ // column NodeSelection whose post-delete landing is owned by the block-controls
970
+ // preserved-selection plugin, so we only restore the caret for the former.
971
+ var hadTextSelection = tr.selection instanceof TextSelection;
920
972
  tr.replaceWith(layoutSectionPos + 1, layoutSectionPos + layoutSectionNode.nodeSize - 1, columnWidth(updatedLayoutSectionNode, tr.doc.type.schema, redistributed));
973
+
974
+ // Land the caret in a remaining column — the one now occupying the deleted slot, or the
975
+ // last column when the deleted slot no longer exists. Otherwise the replace above maps
976
+ // the caret out of the layout to the following paragraph.
977
+ var remainingColumnCount = remainingColumns.length;
978
+ if (hadTextSelection && !fg('platform_editor_layout_column_menu_kill_switch_1') && remainingColumnCount > 0) {
979
+ var targetColumnIndex = Math.min(startIndex, remainingColumnCount - 1);
980
+ var updatedSectionNode = tr.doc.nodeAt(layoutSectionPos);
981
+ if (updatedSectionNode) {
982
+ var columnOffset = 1;
983
+ for (var columnIndex = 0; columnIndex < targetColumnIndex; columnIndex++) {
984
+ columnOffset += updatedSectionNode.child(columnIndex).nodeSize;
985
+ }
986
+ // +1 to land inside the column's first child rather than on the column boundary.
987
+ var caretPos = layoutSectionPos + columnOffset + 1;
988
+ if (caretPos >= 0 && caretPos <= tr.doc.content.size) {
989
+ var caretSelection = TextSelection.findFrom(tr.doc.resolve(caretPos), 1, true);
990
+ if (caretSelection) {
991
+ tr.setSelection(caretSelection);
992
+ }
993
+ }
994
+ }
995
+ }
921
996
  emitDeleteColumnAnalytics(redistributed.length);
922
997
  tr.setMeta('scrollIntoView', false);
923
998
  api === null || api === void 0 || (_api$blockControls4 = api.blockControls) === null || _api$blockControls4 === void 0 || _api$blockControls4.commands.stopPreservingSelection()({
@@ -4,10 +4,40 @@ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t =
4
4
  import { bind } from 'bind-event-listener';
5
5
  import { ACTION, ACTION_SUBJECT, ACTION_SUBJECT_ID, EVENT_TYPE, INPUT_METHOD } from '@atlaskit/editor-common/analytics';
6
6
  import { Fragment } from '@atlaskit/editor-prosemirror/model';
7
+ import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
7
8
  import { Decoration } from '@atlaskit/editor-prosemirror/view';
8
9
  import { fg } from '@atlaskit/platform-feature-flags';
9
10
  import { MIN_LAYOUT_COLUMN_WIDTH_PERCENT } from './consts';
10
11
 
12
+ /**
13
+ * Re-creates a selection at the same positions against the resized document. A resize only
14
+ * changes width attrs (not node count, order, or size), so the original positions are still
15
+ * valid. Returns `undefined` if no valid selection can be made.
16
+ */
17
+ var remapResizeSelection = function remapResizeSelection(selection, doc) {
18
+ var docSize = doc.content.size;
19
+ var clamp = function clamp(pos) {
20
+ return Math.min(Math.max(pos, 0), docSize);
21
+ };
22
+ if (selection instanceof NodeSelection) {
23
+ var _TextSelection$findFr;
24
+ var pos = clamp(selection.from);
25
+ var nodeAtPos = doc.nodeAt(pos);
26
+ if (nodeAtPos && NodeSelection.isSelectable(nodeAtPos)) {
27
+ return NodeSelection.create(doc, pos);
28
+ }
29
+ return (_TextSelection$findFr = TextSelection.findFrom(doc.resolve(pos), 1, true)) !== null && _TextSelection$findFr !== void 0 ? _TextSelection$findFr : undefined;
30
+ }
31
+ if (selection instanceof TextSelection) {
32
+ try {
33
+ return TextSelection.create(doc, clamp(selection.from), clamp(selection.to));
34
+ } catch (_unused) {
35
+ return undefined;
36
+ }
37
+ }
38
+ return undefined;
39
+ };
40
+
11
41
  // Class names for the column resize divider widget — must stay in sync with layout.ts in editor-core
12
42
  var layoutColumnDividerClassName = 'layout-column-divider';
13
43
  var layoutColumnDividerRailClassName = 'layout-column-divider-rail';
@@ -44,9 +74,20 @@ var dispatchColumnWidths = function dispatchColumnWidths(view, sectionPos, leftC
44
74
  newColumns.push(child);
45
75
  }
46
76
  });
77
+
78
+ // Capture the selection before the replace below, which otherwise maps the caret out of
79
+ // the layout to the following paragraph. Restored at the same positions afterwards.
80
+ var shouldPreserveSelection = !fg('platform_editor_layout_column_menu_kill_switch_1');
81
+ var selectionBeforeResize = shouldPreserveSelection ? state.selection : null;
47
82
  tr.replaceWith(sectionPos + 1, sectionPos + sectionNode.nodeSize - 1, Fragment.from(newColumns));
48
83
  tr.setMeta('layoutColumnResize', true);
49
84
  tr.setMeta('scrollIntoView', false);
85
+ if (selectionBeforeResize) {
86
+ var restoredSelection = remapResizeSelection(selectionBeforeResize, tr.doc);
87
+ if (restoredSelection) {
88
+ tr.setSelection(restoredSelection);
89
+ }
90
+ }
50
91
  if (fg('platform_editor_layout_resize_analytics')) {
51
92
  editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
52
93
  action: ACTION.DRAGGED,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-layout",
3
- "version": "13.2.3",
3
+ "version": "13.3.0",
4
4
  "description": "Layout plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -37,19 +37,19 @@
37
37
  "@atlaskit/editor-plugin-width": "^13.0.0",
38
38
  "@atlaskit/editor-prosemirror": "^8.0.0",
39
39
  "@atlaskit/editor-shared-styles": "^4.0.0",
40
- "@atlaskit/editor-toolbar": "^2.0.0",
41
- "@atlaskit/editor-ui-control-model": "^2.0.0",
42
- "@atlaskit/icon": "^36.0.0",
43
- "@atlaskit/icon-lab": "^7.1.0",
40
+ "@atlaskit/editor-toolbar": "^2.1.0",
41
+ "@atlaskit/editor-ui-control-model": "^2.1.0",
42
+ "@atlaskit/icon": "^36.1.0",
43
+ "@atlaskit/icon-lab": "^7.2.0",
44
44
  "@atlaskit/platform-feature-flags": "^2.0.0",
45
- "@atlaskit/tmp-editor-statsig": "^113.0.0",
46
- "@atlaskit/tokens": "^15.0.0",
45
+ "@atlaskit/tmp-editor-statsig": "^114.0.0",
46
+ "@atlaskit/tokens": "^15.1.0",
47
47
  "@babel/runtime": "^7.0.0",
48
48
  "@emotion/react": "^11.7.1",
49
49
  "bind-event-listener": "^3.0.0"
50
50
  },
51
51
  "peerDependencies": {
52
- "@atlaskit/editor-common": "^116.13.0",
52
+ "@atlaskit/editor-common": "^116.15.0",
53
53
  "react": "^18.2.0",
54
54
  "react-intl": "^5.25.1 || ^6.0.0 || ^7.0.0"
55
55
  },