@atlaskit/editor-plugin-list 9.0.27 → 9.0.28

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,13 @@
1
1
  # @atlaskit/editor-plugin-list
2
2
 
3
+ ## 9.0.28
4
+
5
+ ### Patch Changes
6
+
7
+ - [`c15b7a3426c66`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/c15b7a3426c66) -
8
+ [ux] EDITOR-5626 Flexible list behaviour
9
+ - Updated dependencies
10
+
3
11
  ## 9.0.27
4
12
 
5
13
  ### Patch Changes
@@ -0,0 +1,68 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.moveSelectedListItems = moveSelectedListItems;
7
+ var _types = require("../../types");
8
+ var _find = require("../utils/find");
9
+ var _listIndentation = require("../utils/list-indentation");
10
+ /**
11
+ * Moves the selected list items n levels up (negative delta) or down (positive delta).
12
+ */
13
+ function moveSelectedListItems(tr, delta) {
14
+ var originalSelection = tr.selection;
15
+
16
+ // Find the root list so depth adjustments are absolute
17
+ var rootListResolved = (0, _find.findRootParentListNode)(originalSelection.$from);
18
+ if (!rootListResolved) {
19
+ return;
20
+ }
21
+ var rootList = (0, _find.findFirstParentListNode)(rootListResolved);
22
+ if (!rootList) {
23
+ return;
24
+ }
25
+ var rootListStart = rootList.pos;
26
+ var rootListEnd = rootListStart + rootList.node.nodeSize;
27
+ var result = (0, _listIndentation.flattenList)({
28
+ doc: tr.doc,
29
+ rootListStart: rootListStart,
30
+ rootListEnd: rootListEnd,
31
+ selectionFrom: originalSelection.from,
32
+ selectionTo: originalSelection.to,
33
+ delta: delta
34
+ });
35
+ if (!result || result.maxDepth >= _types.MAX_NESTED_LIST_INDENTATION) {
36
+ return;
37
+ }
38
+ var elements = result.elements,
39
+ startIndex = result.startIndex,
40
+ endIndex = result.endIndex;
41
+
42
+ // Build replacement — handles both indent (all depths >= 0)
43
+ // and outdent (some depths may be < 0, producing extracted paragraphs).
44
+ var _buildReplacementFrag = (0, _listIndentation.buildReplacementFragment)(elements, tr.doc.type.schema),
45
+ fragment = _buildReplacementFrag.fragment,
46
+ contentStartOffsets = _buildReplacementFrag.contentStartOffsets;
47
+ if (fragment.size === 0) {
48
+ return;
49
+ }
50
+ tr.replaceWith(rootListStart, rootListEnd, fragment);
51
+ var fromContentStart = elements[startIndex].pos + 1;
52
+ var toContentStart = elements[endIndex].pos + 1;
53
+ var fromOffset = originalSelection.from - fromContentStart;
54
+ var toOffset = originalSelection.to - toContentStart;
55
+ var clamp = function clamp(pos) {
56
+ return Math.min(Math.max(0, pos), tr.doc.content.size);
57
+ };
58
+ var from = clamp(rootListStart + contentStartOffsets[startIndex] + fromOffset);
59
+ var to = clamp(rootListStart + contentStartOffsets[endIndex] + toOffset);
60
+
61
+ // Restore selection using the positional offsets from the rebuild.
62
+ (0, _listIndentation.restoreSelection)({
63
+ tr: tr,
64
+ originalSelection: originalSelection,
65
+ from: from,
66
+ to: to
67
+ });
68
+ }
@@ -11,12 +11,40 @@ var _lists = require("@atlaskit/editor-common/lists");
11
11
  var _preset = require("@atlaskit/editor-common/preset");
12
12
  var _utils = require("@atlaskit/editor-common/utils");
13
13
  var _prosemirrorHistory = require("@atlaskit/prosemirror-history");
14
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
14
15
  var _types = require("../../types");
15
16
  var _indentListItemsSelected = require("../actions/indent-list-items-selected");
17
+ var _moveSelectedListItems = require("../actions/move-selected-list-items");
16
18
  var _find = require("../utils/find");
17
19
  var _selection = require("../utils/selection");
18
20
  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; }
19
21
  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; }
22
+ /**
23
+ * Handler for flexible list indentation.
24
+ * Allows indenting the first item by creating wrapper structures.
25
+ */
26
+ var handleIndentListItems = function handleIndentListItems(tr, editorAnalyticsAPI, inputMethod) {
27
+ var _findFirstParentListN;
28
+ (0, _moveSelectedListItems.moveSelectedListItems)(tr, 1);
29
+
30
+ // If no changes were made, return PassiveTransaction to prevent browser from handling this as a tab key event (e.g. moving focus)
31
+ if (!tr.docChanged) {
32
+ return new _preset.PassiveTransaction();
33
+ }
34
+ var $from = tr.selection.$from;
35
+ var currentListNode = (_findFirstParentListN = (0, _find.findFirstParentListNode)($from)) === null || _findFirstParentListN === void 0 ? void 0 : _findFirstParentListN.node;
36
+ var actionSubjectId = currentListNode && (0, _utils.isBulletList)(currentListNode) ? _analytics.ACTION_SUBJECT_ID.FORMAT_LIST_BULLET : _analytics.ACTION_SUBJECT_ID.FORMAT_LIST_NUMBER;
37
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
38
+ action: _analytics.ACTION.INDENTED,
39
+ actionSubject: _analytics.ACTION_SUBJECT.LIST,
40
+ actionSubjectId: actionSubjectId,
41
+ eventType: _analytics.EVENT_TYPE.TRACK,
42
+ attributes: _objectSpread(_objectSpread({}, (0, _lists.getCommonListAnalyticsAttributes)(tr)), {}, {
43
+ inputMethod: inputMethod
44
+ })
45
+ })(tr);
46
+ return tr;
47
+ };
20
48
  var indentList = exports.indentList = function indentList(editorAnalyticsAPI) {
21
49
  return function () {
22
50
  var inputMethod = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _analytics.INPUT_METHOD.KEYBOARD;
@@ -31,6 +59,11 @@ var indentList = exports.indentList = function indentList(editorAnalyticsAPI) {
31
59
 
32
60
  // Save the history, so it could undo/revert to the same state before the indent, see https://product-fabric.atlassian.net/browse/ED-14753
33
61
  (0, _prosemirrorHistory.closeHistory)(tr);
62
+
63
+ // Route to new or original implementation based on feature flag
64
+ if ((0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
65
+ return handleIndentListItems(tr, editorAnalyticsAPI, inputMethod);
66
+ }
34
67
  var firstListItemSelectedAttributes = (0, _lists.getListItemAttributes)($from);
35
68
  var parentListNode = (0, _find.findFirstParentListNode)($from);
36
69
  if (!parentListNode || firstListItemSelectedAttributes && firstListItemSelectedAttributes.indentLevel === 0 && firstListItemSelectedAttributes.itemIndex === 0) {
@@ -11,12 +11,55 @@ var _lists = require("@atlaskit/editor-common/lists");
11
11
  var _preset = require("@atlaskit/editor-common/preset");
12
12
  var _utils = require("@atlaskit/editor-common/utils");
13
13
  var _prosemirrorHistory = require("@atlaskit/prosemirror-history");
14
+ var _expValEqualsNoExposure = require("@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure");
15
+ var _moveSelectedListItems = require("../actions/move-selected-list-items");
14
16
  var _outdentListItemsSelected = require("../actions/outdent-list-items-selected");
15
17
  var _analytics2 = require("../utils/analytics");
16
18
  var _find = require("../utils/find");
17
19
  var _selection = require("../utils/selection");
18
20
  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; }
19
21
  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; }
22
+ /**
23
+ * Handler for flexible list outdentation.
24
+ * Lifts items independently and cleans up wrapper structures.
25
+ */
26
+ var handleOutdentListItems = function handleOutdentListItems(tr, editorAnalyticsAPI, inputMethod) {
27
+ var _findFirstParentListN;
28
+ (0, _moveSelectedListItems.moveSelectedListItems)(tr, -1);
29
+
30
+ // If no changes were made, handle based on context
31
+ if (!tr.docChanged) {
32
+ // If inside table cell and can't outdent list, then let it handle by table keymap
33
+ return !(0, _selection.isInsideTableCell)(tr) ? new _preset.PassiveTransaction() : null;
34
+ }
35
+
36
+ // Determine the action subject ID from the parent list type
37
+ var $from = tr.selection.$from;
38
+ var currentListNode = (_findFirstParentListN = (0, _find.findFirstParentListNode)($from)) === null || _findFirstParentListN === void 0 ? void 0 : _findFirstParentListN.node;
39
+ var actionSubjectId = currentListNode && (0, _utils.isBulletList)(currentListNode) ? _analytics.ACTION_SUBJECT_ID.FORMAT_LIST_BULLET : _analytics.ACTION_SUBJECT_ID.FORMAT_LIST_NUMBER;
40
+
41
+ // Get restart list attributes for analytics
42
+ var restartListsAttributes = {};
43
+ var _getRestartListsAttri = (0, _analytics2.getRestartListsAttributes)(tr),
44
+ outdentScenario = _getRestartListsAttri.outdentScenario,
45
+ splitListStartNumber = _getRestartListsAttri.splitListStartNumber;
46
+ if (outdentScenario === _analytics.OUTDENT_SCENARIOS.SPLIT_LIST) {
47
+ restartListsAttributes.outdentScenario = outdentScenario;
48
+ restartListsAttributes.splitListStartNumber = splitListStartNumber;
49
+ }
50
+
51
+ // Attach analytics event with flexibleIndentation attribute
52
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 || editorAnalyticsAPI.attachAnalyticsEvent({
53
+ action: _analytics.ACTION.OUTDENTED,
54
+ actionSubject: _analytics.ACTION_SUBJECT.LIST,
55
+ actionSubjectId: actionSubjectId,
56
+ eventType: _analytics.EVENT_TYPE.TRACK,
57
+ attributes: _objectSpread(_objectSpread(_objectSpread({}, (0, _lists.getCommonListAnalyticsAttributes)(tr)), restartListsAttributes), {}, {
58
+ inputMethod: inputMethod
59
+ })
60
+ })(tr);
61
+ return tr;
62
+ };
20
63
  var outdentList = exports.outdentList = function outdentList(editorAnalyticsAPI) {
21
64
  return function () {
22
65
  var inputMethod = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : _analytics.INPUT_METHOD.KEYBOARD;
@@ -34,6 +77,11 @@ var outdentList = exports.outdentList = function outdentList(editorAnalyticsAPI)
34
77
 
35
78
  // Save the history, so it could undo/revert to the same state before the outdent, see https://product-fabric.atlassian.net/browse/ED-14753
36
79
  (0, _prosemirrorHistory.closeHistory)(tr);
80
+
81
+ // Route to new or original implementation based on feature flag
82
+ if ((0, _expValEqualsNoExposure.expValEqualsNoExposure)('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
83
+ return handleOutdentListItems(tr, editorAnalyticsAPI, inputMethod);
84
+ }
37
85
  var actionSubjectId = (0, _utils.isBulletList)(parentListNode.node) ? _analytics.ACTION_SUBJECT_ID.FORMAT_LIST_BULLET : _analytics.ACTION_SUBJECT_ID.FORMAT_LIST_NUMBER;
38
86
  var customTr = tr;
39
87
  (0, _outdentListItemsSelected.outdentListItemsSelected)(customTr);
@@ -43,9 +91,9 @@ var outdentList = exports.outdentList = function outdentList(editorAnalyticsAPI)
43
91
  return !(0, _selection.isInsideTableCell)(customTr) ? new _preset.PassiveTransaction() : null;
44
92
  }
45
93
  var restartListsAttributes = {};
46
- var _getRestartListsAttri = (0, _analytics2.getRestartListsAttributes)(customTr),
47
- outdentScenario = _getRestartListsAttri.outdentScenario,
48
- splitListStartNumber = _getRestartListsAttri.splitListStartNumber;
94
+ var _getRestartListsAttri2 = (0, _analytics2.getRestartListsAttributes)(customTr),
95
+ outdentScenario = _getRestartListsAttri2.outdentScenario,
96
+ splitListStartNumber = _getRestartListsAttri2.splitListStartNumber;
49
97
  if (outdentScenario === _analytics.OUTDENT_SCENARIOS.SPLIT_LIST) {
50
98
  restartListsAttributes.outdentScenario = outdentScenario;
51
99
  restartListsAttributes.splitListStartNumber = splitListStartNumber;
@@ -0,0 +1,305 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.buildReplacementFragment = buildReplacementFragment;
7
+ exports.flattenList = flattenList;
8
+ exports.restoreSelection = restoreSelection;
9
+ var _selection = require("@atlaskit/editor-common/selection");
10
+ var _utils = require("@atlaskit/editor-common/utils");
11
+ var _model = require("@atlaskit/editor-prosemirror/model");
12
+ var _state = require("@atlaskit/editor-prosemirror/state");
13
+ function _createForOfIteratorHelper(r, e) { var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"]; if (!t) { if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e && r && "number" == typeof r.length) { t && (r = t); var _n = 0, F = function F() {}; return { s: F, n: function n() { return _n >= r.length ? { done: !0 } : { done: !1, value: r[_n++] }; }, e: function e(r) { throw r; }, f: F }; } throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method."); } var o, a = !0, u = !1; return { s: function s() { t = t.call(r); }, n: function n() { var r = t.next(); return a = r.done, r; }, e: function e(r) { u = !0, o = r; }, f: function f() { try { a || null == t.return || t.return(); } finally { if (u) throw o; } } }; }
14
+ function _unsupportedIterableToArray(r, a) { if (r) { if ("string" == typeof r) return _arrayLikeToArray(r, a); var t = {}.toString.call(r).slice(8, -1); return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0; } }
15
+ function _arrayLikeToArray(r, a) { (null == a || a > r.length) && (a = r.length); for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e]; return n; }
16
+ /**
17
+ * A single content-bearing list element extracted from the PM tree.
18
+ * Wrapper `listItem` nodes (those with no non-list children) are discarded;
19
+ * only items the user can actually see and select are represented.
20
+ */
21
+
22
+ /**
23
+ * Returns true if a listItem has at least one non-list child (paragraph, etc.).
24
+ */
25
+ function hasContentChildren(listItem) {
26
+ return listItem.children.some(function (child) {
27
+ return !(0, _utils.isListNode)(child);
28
+ });
29
+ }
30
+
31
+ /**
32
+ * Compute the size of non-list (content) children of a listItem, which
33
+ * represents the "visible" bounds of the item for selection purposes.
34
+ */
35
+ function contentSize(listItem) {
36
+ return listItem.children.reduce(function (size, child) {
37
+ return size + ((0, _utils.isListNode)(child) ? 0 : child.nodeSize);
38
+ }, 0);
39
+ }
40
+ /**
41
+ * Flatten a root list into a flat array of content-bearing `ListElement`
42
+ * objects and simultaneously determine which elements intersect the user's
43
+ * selection.
44
+ * Selection intersection is checked against each item's content-only
45
+ * span (excluding nested lists).
46
+ */
47
+ function flattenList(_ref) {
48
+ var doc = _ref.doc,
49
+ rootListStart = _ref.rootListStart,
50
+ rootListEnd = _ref.rootListEnd,
51
+ selectionFrom = _ref.selectionFrom,
52
+ selectionTo = _ref.selectionTo,
53
+ delta = _ref.delta;
54
+ var elements = [];
55
+ var startIndex = -1;
56
+ var endIndex = -1;
57
+ var maxDepth = 0;
58
+ var rootDepth = doc.resolve(rootListStart).depth;
59
+ doc.nodesBetween(rootListStart, rootListEnd, function (node, pos, parent) {
60
+ if (!(0, _utils.isListItemNode)(node) || !hasContentChildren(node) || !(0, _utils.isListNode)(parent)) {
61
+ return true;
62
+ }
63
+
64
+ // Check selection intersection using content-only bounds
65
+ var cStart = pos + 1;
66
+ var cEnd = cStart + contentSize(node);
67
+ var isSelected = cStart < selectionTo && cEnd > selectionFrom;
68
+ var depth = (doc.resolve(pos).depth - rootDepth - 1) / 2 + (isSelected ? delta : 0);
69
+ elements.push({
70
+ node: node,
71
+ pos: pos,
72
+ depth: depth,
73
+ listType: parent.type.name
74
+ });
75
+ if (isSelected) {
76
+ var index = elements.length - 1;
77
+ if (startIndex === -1) {
78
+ startIndex = index;
79
+ }
80
+ endIndex = index;
81
+ maxDepth = Math.max(maxDepth, depth);
82
+ }
83
+ return true;
84
+ });
85
+ if (elements.length === 0 || startIndex === -1) {
86
+ return null;
87
+ }
88
+ return {
89
+ elements: elements,
90
+ startIndex: startIndex,
91
+ endIndex: endIndex,
92
+ maxDepth: maxDepth
93
+ };
94
+ }
95
+
96
+ /**
97
+ * Extract non-list (content) children from a listItem node.
98
+ */
99
+ function extractContentChildren(listItem) {
100
+ var children = [];
101
+ for (var i = 0; i < listItem.childCount; i++) {
102
+ var child = listItem.child(i);
103
+ if (!(0, _utils.isListNode)(child)) {
104
+ children.push(child);
105
+ }
106
+ }
107
+ return children;
108
+ }
109
+
110
+ /**
111
+ * Rebuild a ProseMirror list tree from a flat array of `ListElement` objects
112
+ * using a bottom-up stack approach.
113
+ *
114
+ * The algorithm tracks open list/listItem wrappers on a stack. As depth
115
+ * transitions occur between consecutive elements, wrapper nodes are opened
116
+ * (depth increase) or closed (depth decrease).
117
+ */
118
+ function rebuildPMList(elements, schema) {
119
+ if (elements.length === 0) {
120
+ return null;
121
+ }
122
+
123
+ // Each stack frame represents an open list at a given depth.
124
+ // items[] accumulates the PMNode children (listItem nodes) for that list.
125
+
126
+ var stack = [];
127
+ function openList(listType) {
128
+ stack.push({
129
+ listType: listType,
130
+ items: []
131
+ });
132
+ }
133
+
134
+ /**
135
+ * Close lists on the stack down to `targetDepth`, wrapping each closed
136
+ * list into the last listItem of its parent.
137
+ */
138
+ function closeToDepth(targetDepth) {
139
+ var _loop = function _loop() {
140
+ var closed = stack.pop();
141
+ if (!closed) {
142
+ return 1; // break
143
+ }
144
+ var listNode = schema.nodes[closed.listType].create(null, closed.items);
145
+
146
+ // Attach the closed list to the last listItem on the parent frame
147
+ var parentFrame = stack[stack.length - 1];
148
+ var lastItem = parentFrame.items[parentFrame.items.length - 1];
149
+ if (lastItem) {
150
+ // Append the nested list to this listItem's children
151
+ var newContent = [];
152
+ lastItem.forEach(function (child) {
153
+ return newContent.push(child);
154
+ });
155
+ newContent.push(listNode);
156
+ parentFrame.items[parentFrame.items.length - 1] = schema.nodes.listItem.create(lastItem.attrs, newContent);
157
+ } else {
158
+ // Edge case: no listItem to attach to. Create a wrapper.
159
+ var wrapperItem = schema.nodes.listItem.create(null, [listNode]);
160
+ parentFrame.items.push(wrapperItem);
161
+ }
162
+ };
163
+ while (stack.length > targetDepth + 1) {
164
+ if (_loop()) break;
165
+ }
166
+ }
167
+
168
+ // Seed the root list
169
+ openList(elements[0].listType);
170
+ var _iterator = _createForOfIteratorHelper(elements),
171
+ _step;
172
+ try {
173
+ for (_iterator.s(); !(_step = _iterator.n()).done;) {
174
+ var el = _step.value;
175
+ var targetDepth = el.depth;
176
+
177
+ // Close lists if we're going shallower
178
+ if (stack.length > targetDepth + 1) {
179
+ closeToDepth(targetDepth);
180
+ }
181
+
182
+ // Open lists if we need to go deeper.
183
+ // We do NOT create wrapper listItems here — closeToDepth handles
184
+ // creating wrappers that contain only the nested list (no empty paragraph).
185
+ while (stack.length < targetDepth + 1) {
186
+ openList(el.listType);
187
+ }
188
+
189
+ // Build the listItem for this element using its content children
190
+ var contentChildren = extractContentChildren(el.node);
191
+ var listItem = schema.nodes.listItem.create(el.node.attrs, contentChildren);
192
+ stack[stack.length - 1].items.push(listItem);
193
+ }
194
+
195
+ // Close all remaining open lists
196
+ } catch (err) {
197
+ _iterator.e(err);
198
+ } finally {
199
+ _iterator.f();
200
+ }
201
+ closeToDepth(0);
202
+ var root = stack[0];
203
+ return schema.nodes[root.listType].create(null, root.items);
204
+ }
205
+ /**
206
+ * Build a replacement Fragment from a flat array of `ListElement` objects.
207
+ *
208
+ * Elements with depth >= 0 are grouped into consecutive list segments
209
+ * and rebuilt via `rebuildPMList`. Elements with depth < 0 (extracted
210
+ * past the root) are converted to their content children (paragraphs).
211
+ * The result interleaves list nodes and extracted content in document order.
212
+ */
213
+ function buildReplacementFragment(elements, schema) {
214
+ var fragment = _model.Fragment.empty;
215
+ var pendingListSegment = [];
216
+ var pendingStartIdx = 0;
217
+ var contentStartOffsets = new Array(elements.length);
218
+ var flushListSegment = function flushListSegment() {
219
+ if (pendingListSegment.length > 0) {
220
+ var fragmentOffset = fragment.size;
221
+ var rebuilt = rebuildPMList(pendingListSegment, schema);
222
+ if (rebuilt) {
223
+ // Walk the rebuilt tree to find content-bearing listItem positions.
224
+ // descendants() visits in document order matching the element order.
225
+ var segIdx = 0;
226
+ rebuilt.descendants(function (node, pos) {
227
+ if ((0, _utils.isListItemNode)(node) && hasContentChildren(node)) {
228
+ // pos is relative to rebuilt's content start;
229
+ // +1 for rebuilt's opening tag, +1 for listItem's opening tag
230
+ contentStartOffsets[pendingStartIdx + segIdx] = fragmentOffset + 1 + pos + 1;
231
+ segIdx++;
232
+ }
233
+ return true;
234
+ });
235
+ fragment = fragment.addToEnd(rebuilt);
236
+ }
237
+ pendingListSegment = [];
238
+ }
239
+ };
240
+ var elIdx = 0;
241
+ var _iterator2 = _createForOfIteratorHelper(elements),
242
+ _step2;
243
+ try {
244
+ for (_iterator2.s(); !(_step2 = _iterator2.n()).done;) {
245
+ var el = _step2.value;
246
+ if (el.depth < 0) {
247
+ flushListSegment();
248
+ // Extracted element — content children become top-level nodes.
249
+ // Record offset of first content child.
250
+ contentStartOffsets[elIdx] = fragment.size;
251
+ var _iterator3 = _createForOfIteratorHelper(extractContentChildren(el.node)),
252
+ _step3;
253
+ try {
254
+ for (_iterator3.s(); !(_step3 = _iterator3.n()).done;) {
255
+ var node = _step3.value;
256
+ fragment = fragment.addToEnd(node);
257
+ }
258
+ } catch (err) {
259
+ _iterator3.e(err);
260
+ } finally {
261
+ _iterator3.f();
262
+ }
263
+ } else {
264
+ if (pendingListSegment.length === 0) {
265
+ pendingStartIdx = elIdx;
266
+ }
267
+ pendingListSegment.push(el);
268
+ }
269
+ elIdx++;
270
+ }
271
+ } catch (err) {
272
+ _iterator2.e(err);
273
+ } finally {
274
+ _iterator2.f();
275
+ }
276
+ flushListSegment();
277
+ return {
278
+ fragment: fragment,
279
+ contentStartOffsets: contentStartOffsets
280
+ };
281
+ }
282
+ /**
283
+ * Restore the transaction's selection after a list structural change.
284
+ *
285
+ * Uses the content start offsets computed during fragment rebuild to
286
+ * map each selection endpoint to its new absolute position.
287
+ */
288
+ function restoreSelection(_ref2) {
289
+ var tr = _ref2.tr,
290
+ originalSelection = _ref2.originalSelection,
291
+ from = _ref2.from,
292
+ to = _ref2.to;
293
+ var maxPos = tr.doc.content.size;
294
+ if (originalSelection instanceof _state.NodeSelection) {
295
+ try {
296
+ tr.setSelection(_state.NodeSelection.create(tr.doc, Math.min(from, maxPos - 1)));
297
+ } catch (_unused) {
298
+ tr.setSelection(_state.Selection.near(tr.doc.resolve(from)));
299
+ }
300
+ } else if (originalSelection instanceof _selection.GapCursorSelection) {
301
+ tr.setSelection(new _selection.GapCursorSelection(tr.doc.resolve(from), originalSelection.side));
302
+ } else {
303
+ tr.setSelection(_state.TextSelection.create(tr.doc, from, to));
304
+ }
305
+ }
@@ -0,0 +1,64 @@
1
+ import { MAX_NESTED_LIST_INDENTATION } from '../../types';
2
+ import { findFirstParentListNode, findRootParentListNode } from '../utils/find';
3
+ import { buildReplacementFragment, flattenList, restoreSelection } from '../utils/list-indentation';
4
+
5
+ /**
6
+ * Moves the selected list items n levels up (negative delta) or down (positive delta).
7
+ */
8
+ export function moveSelectedListItems(tr, delta) {
9
+ const originalSelection = tr.selection;
10
+
11
+ // Find the root list so depth adjustments are absolute
12
+ const rootListResolved = findRootParentListNode(originalSelection.$from);
13
+ if (!rootListResolved) {
14
+ return;
15
+ }
16
+ const rootList = findFirstParentListNode(rootListResolved);
17
+ if (!rootList) {
18
+ return;
19
+ }
20
+ const rootListStart = rootList.pos;
21
+ const rootListEnd = rootListStart + rootList.node.nodeSize;
22
+ const result = flattenList({
23
+ doc: tr.doc,
24
+ rootListStart,
25
+ rootListEnd,
26
+ selectionFrom: originalSelection.from,
27
+ selectionTo: originalSelection.to,
28
+ delta
29
+ });
30
+ if (!result || result.maxDepth >= MAX_NESTED_LIST_INDENTATION) {
31
+ return;
32
+ }
33
+ const {
34
+ elements,
35
+ startIndex,
36
+ endIndex
37
+ } = result;
38
+
39
+ // Build replacement — handles both indent (all depths >= 0)
40
+ // and outdent (some depths may be < 0, producing extracted paragraphs).
41
+ const {
42
+ fragment,
43
+ contentStartOffsets
44
+ } = buildReplacementFragment(elements, tr.doc.type.schema);
45
+ if (fragment.size === 0) {
46
+ return;
47
+ }
48
+ tr.replaceWith(rootListStart, rootListEnd, fragment);
49
+ const fromContentStart = elements[startIndex].pos + 1;
50
+ const toContentStart = elements[endIndex].pos + 1;
51
+ const fromOffset = originalSelection.from - fromContentStart;
52
+ const toOffset = originalSelection.to - toContentStart;
53
+ const clamp = pos => Math.min(Math.max(0, pos), tr.doc.content.size);
54
+ const from = clamp(rootListStart + contentStartOffsets[startIndex] + fromOffset);
55
+ const to = clamp(rootListStart + contentStartOffsets[endIndex] + toOffset);
56
+
57
+ // Restore selection using the positional offsets from the rebuild.
58
+ restoreSelection({
59
+ tr,
60
+ originalSelection,
61
+ from,
62
+ to
63
+ });
64
+ }
@@ -3,10 +3,43 @@ import { getCommonListAnalyticsAttributes, getListItemAttributes, hasValidListIn
3
3
  import { PassiveTransaction } from '@atlaskit/editor-common/preset';
4
4
  import { isBulletList } from '@atlaskit/editor-common/utils';
5
5
  import { closeHistory } from '@atlaskit/prosemirror-history';
6
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
6
7
  import { MAX_NESTED_LIST_INDENTATION } from '../../types';
7
8
  import { indentListItemsSelected as indentListAction } from '../actions/indent-list-items-selected';
9
+ import { moveSelectedListItems } from '../actions/move-selected-list-items';
8
10
  import { findFirstParentListNode } from '../utils/find';
9
11
  import { isInsideListItem, isInsideTableCell } from '../utils/selection';
12
+ /**
13
+ * Handler for flexible list indentation.
14
+ * Allows indenting the first item by creating wrapper structures.
15
+ */
16
+ const handleIndentListItems = (tr, editorAnalyticsAPI, inputMethod) => {
17
+ var _findFirstParentListN;
18
+ moveSelectedListItems(tr, 1);
19
+
20
+ // If no changes were made, return PassiveTransaction to prevent browser from handling this as a tab key event (e.g. moving focus)
21
+ if (!tr.docChanged) {
22
+ return new PassiveTransaction();
23
+ }
24
+ const {
25
+ selection: {
26
+ $from
27
+ }
28
+ } = tr;
29
+ const currentListNode = (_findFirstParentListN = findFirstParentListNode($from)) === null || _findFirstParentListN === void 0 ? void 0 : _findFirstParentListN.node;
30
+ const actionSubjectId = currentListNode && isBulletList(currentListNode) ? ACTION_SUBJECT_ID.FORMAT_LIST_BULLET : ACTION_SUBJECT_ID.FORMAT_LIST_NUMBER;
31
+ editorAnalyticsAPI === null || editorAnalyticsAPI === void 0 ? void 0 : editorAnalyticsAPI.attachAnalyticsEvent({
32
+ action: ACTION.INDENTED,
33
+ actionSubject: ACTION_SUBJECT.LIST,
34
+ actionSubjectId,
35
+ eventType: EVENT_TYPE.TRACK,
36
+ attributes: {
37
+ ...getCommonListAnalyticsAttributes(tr),
38
+ inputMethod
39
+ }
40
+ })(tr);
41
+ return tr;
42
+ };
10
43
  export const indentList = editorAnalyticsAPI => (inputMethod = INPUT_METHOD.KEYBOARD) => {
11
44
  return function ({
12
45
  tr
@@ -24,6 +57,11 @@ export const indentList = editorAnalyticsAPI => (inputMethod = INPUT_METHOD.KEYB
24
57
 
25
58
  // Save the history, so it could undo/revert to the same state before the indent, see https://product-fabric.atlassian.net/browse/ED-14753
26
59
  closeHistory(tr);
60
+
61
+ // Route to new or original implementation based on feature flag
62
+ if (expValEqualsNoExposure('platform_editor_flexible_list_indentation', 'isEnabled', true)) {
63
+ return handleIndentListItems(tr, editorAnalyticsAPI, inputMethod);
64
+ }
27
65
  const firstListItemSelectedAttributes = getListItemAttributes($from);
28
66
  const parentListNode = findFirstParentListNode($from);
29
67
  if (!parentListNode || firstListItemSelectedAttributes && firstListItemSelectedAttributes.indentLevel === 0 && firstListItemSelectedAttributes.itemIndex === 0) {