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