@atlaskit/editor-plugin-block-menu 5.1.6 → 5.1.8
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 +18 -0
- package/dist/cjs/editor-commands/transform-node-utils/flattenListStep.js +9 -32
- package/dist/cjs/editor-commands/transform-node-utils/transform.js +5 -4
- package/dist/cjs/editor-commands/transform-node-utils/unwrapListStep.js +16 -1
- package/dist/cjs/editor-commands/transform-node-utils/utils.js +30 -1
- package/dist/cjs/editor-commands/transform-node-utils/wrapMixedContentStep.js +141 -0
- package/dist/cjs/editor-commands/transformNode.js +6 -6
- package/dist/cjs/ui/block-menu-components.js +29 -24
- package/dist/cjs/ui/suggested-items-renderer.js +62 -0
- package/dist/cjs/ui/utils/suggested-items-rank.js +66 -0
- package/dist/es2019/editor-commands/transform-node-utils/flattenListStep.js +9 -32
- package/dist/es2019/editor-commands/transform-node-utils/transform.js +5 -4
- package/dist/es2019/editor-commands/transform-node-utils/unwrapListStep.js +16 -1
- package/dist/es2019/editor-commands/transform-node-utils/utils.js +29 -1
- package/dist/es2019/editor-commands/transform-node-utils/wrapMixedContentStep.js +135 -0
- package/dist/es2019/editor-commands/transformNode.js +2 -2
- package/dist/es2019/ui/block-menu-components.js +12 -9
- package/dist/es2019/ui/suggested-items-renderer.js +48 -0
- package/dist/es2019/ui/utils/suggested-items-rank.js +130 -0
- package/dist/esm/editor-commands/transform-node-utils/flattenListStep.js +9 -32
- package/dist/esm/editor-commands/transform-node-utils/transform.js +5 -4
- package/dist/esm/editor-commands/transform-node-utils/unwrapListStep.js +16 -1
- package/dist/esm/editor-commands/transform-node-utils/utils.js +30 -1
- package/dist/esm/editor-commands/transform-node-utils/wrapMixedContentStep.js +135 -0
- package/dist/esm/editor-commands/transformNode.js +4 -4
- package/dist/esm/ui/block-menu-components.js +30 -25
- package/dist/esm/ui/suggested-items-renderer.js +54 -0
- package/dist/esm/ui/utils/suggested-items-rank.js +60 -0
- package/dist/types/editor-commands/transform-node-utils/flattenListStep.d.ts +0 -18
- package/dist/types/editor-commands/transform-node-utils/unwrapListStep.d.ts +16 -1
- package/dist/types/editor-commands/transform-node-utils/utils.d.ts +12 -0
- package/dist/types/editor-commands/transform-node-utils/wrapMixedContentStep.d.ts +20 -0
- package/dist/types/ui/suggested-items-renderer.d.ts +8 -0
- package/dist/types/ui/utils/suggested-items-rank.d.ts +45 -0
- package/dist/types-ts4.5/editor-commands/transform-node-utils/flattenListStep.d.ts +0 -18
- package/dist/types-ts4.5/editor-commands/transform-node-utils/unwrapListStep.d.ts +16 -1
- package/dist/types-ts4.5/editor-commands/transform-node-utils/utils.d.ts +12 -0
- package/dist/types-ts4.5/editor-commands/transform-node-utils/wrapMixedContentStep.d.ts +20 -0
- package/dist/types-ts4.5/ui/suggested-items-renderer.d.ts +8 -0
- package/dist/types-ts4.5/ui/utils/suggested-items-rank.d.ts +45 -0
- package/package.json +4 -4
|
@@ -1,33 +1,28 @@
|
|
|
1
1
|
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
-
const extractNestedLists = (node, listTypes, itemTypes) => {
|
|
2
|
+
const extractNestedLists = (node, listTypes, itemTypes, schema) => {
|
|
3
3
|
const items = [];
|
|
4
|
+
const paragraph = schema.nodes.paragraph;
|
|
4
5
|
const extract = currentNode => {
|
|
5
6
|
currentNode.forEach(child => {
|
|
6
|
-
// list item -> take content without nested lists, then recurse into nested lists
|
|
7
7
|
if (itemTypes.some(type => child.type === type)) {
|
|
8
|
-
// Filter out nested list nodes from the list item's content
|
|
9
8
|
const contentWithoutNestedLists = [];
|
|
10
9
|
const nestedLists = [];
|
|
11
10
|
child.forEach(grandChild => {
|
|
12
11
|
if (listTypes.some(type => grandChild.type === type)) {
|
|
13
|
-
// This is a nested list - collect it for later processing
|
|
14
12
|
nestedLists.push(grandChild);
|
|
15
13
|
} else {
|
|
16
|
-
|
|
17
|
-
|
|
14
|
+
if (grandChild.isText) {
|
|
15
|
+
contentWithoutNestedLists.push(paragraph.createAndFill({}, grandChild));
|
|
16
|
+
} else {
|
|
17
|
+
contentWithoutNestedLists.push(grandChild);
|
|
18
|
+
}
|
|
18
19
|
}
|
|
19
20
|
});
|
|
20
|
-
|
|
21
|
-
// Add the list item with only its non-list content
|
|
22
21
|
items.push(child.copy(Fragment.from(contentWithoutNestedLists)));
|
|
23
|
-
|
|
24
|
-
// Now process nested lists to maintain document order
|
|
25
22
|
nestedLists.forEach(nestedList => {
|
|
26
23
|
extract(nestedList);
|
|
27
24
|
});
|
|
28
|
-
}
|
|
29
|
-
// lists -> keep operating
|
|
30
|
-
else if (listTypes.some(type => child.type === type)) {
|
|
25
|
+
} else if (listTypes.some(type => child.type === type)) {
|
|
31
26
|
extract(child);
|
|
32
27
|
}
|
|
33
28
|
});
|
|
@@ -41,24 +36,6 @@ const extractNestedLists = (node, listTypes, itemTypes) => {
|
|
|
41
36
|
* to it's first ancestor list, maintaining document order.
|
|
42
37
|
*
|
|
43
38
|
* @example
|
|
44
|
-
* Input:
|
|
45
|
-
* - bulletList
|
|
46
|
-
* - listItem "A"
|
|
47
|
-
* - listItem "B"
|
|
48
|
-
* - bulletList
|
|
49
|
-
* - listItem "C"
|
|
50
|
-
* - listItem "D"
|
|
51
|
-
* - listItem "E"
|
|
52
|
-
*
|
|
53
|
-
* Output:
|
|
54
|
-
* - bulletList
|
|
55
|
-
* - listItem "A"
|
|
56
|
-
* - listItem "B"
|
|
57
|
-
* - listItem "C"
|
|
58
|
-
* - listItem "D"
|
|
59
|
-
* - listItem "E"
|
|
60
|
-
*
|
|
61
|
-
* @example
|
|
62
39
|
* Input (deeply nested):
|
|
63
40
|
* - bulletList
|
|
64
41
|
* - listItem "1"
|
|
@@ -87,7 +64,7 @@ export const flattenListStep = (nodes, context) => {
|
|
|
87
64
|
const listTypes = [context.schema.nodes.bulletList, context.schema.nodes.orderedList, context.schema.nodes.taskList];
|
|
88
65
|
return nodes.map(node => {
|
|
89
66
|
if (listTypes.some(type => node.type === type)) {
|
|
90
|
-
return node.copy(Fragment.from(extractNestedLists(node, listTypes, [context.schema.nodes.listItem, context.schema.nodes.taskItem])));
|
|
67
|
+
return node.copy(Fragment.from(extractNestedLists(node, listTypes, [context.schema.nodes.listItem, context.schema.nodes.taskItem], context.schema)));
|
|
91
68
|
}
|
|
92
69
|
return node;
|
|
93
70
|
});
|
|
@@ -8,6 +8,7 @@ import { unwrapExpandStep } from './unwrapExpandStep';
|
|
|
8
8
|
import { unwrapListStep } from './unwrapListStep';
|
|
9
9
|
import { unwrapStep } from './unwrapStep';
|
|
10
10
|
import { wrapIntoLayoutStep } from './wrapIntoLayoutStep';
|
|
11
|
+
import { wrapMixedContentStep } from './wrapMixedContentStep';
|
|
11
12
|
import { wrapStep } from './wrapStep';
|
|
12
13
|
|
|
13
14
|
// Exampled step for overrides:
|
|
@@ -57,15 +58,15 @@ const TRANSFORM_STEPS_OVERRIDE = {
|
|
|
57
58
|
codeBlock: [unwrapStep, flattenStep, wrapStep]
|
|
58
59
|
},
|
|
59
60
|
expand: {
|
|
60
|
-
panel: [unwrapExpandStep,
|
|
61
|
-
blockquote: [unwrapExpandStep,
|
|
61
|
+
panel: [unwrapExpandStep, wrapMixedContentStep],
|
|
62
|
+
blockquote: [unwrapExpandStep, wrapMixedContentStep],
|
|
62
63
|
layoutSection: [unwrapExpandStep, wrapIntoLayoutStep],
|
|
63
64
|
paragraph: [unwrapExpandStep],
|
|
64
65
|
codeBlock: [unwrapExpandStep, flattenStep, wrapStep]
|
|
65
66
|
},
|
|
66
67
|
nestedExpand: {
|
|
67
|
-
panel: [unwrapExpandStep,
|
|
68
|
-
blockquote: [unwrapExpandStep,
|
|
68
|
+
panel: [unwrapExpandStep, wrapMixedContentStep],
|
|
69
|
+
blockquote: [unwrapExpandStep, wrapMixedContentStep],
|
|
69
70
|
paragraph: [unwrapExpandStep],
|
|
70
71
|
codeBlock: [unwrapExpandStep, flattenStep, wrapStep]
|
|
71
72
|
},
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Given an array of nodes,
|
|
2
|
+
* Given an array of nodes, processes each list removing all parent list nodes and
|
|
3
|
+
* just returning their child contents.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* Input:
|
|
7
|
+
* - bulletList
|
|
8
|
+
* - listItem "1"
|
|
9
|
+
* - paragraph "1"
|
|
10
|
+
* - listItem "2"
|
|
11
|
+
* - paragraph "2"
|
|
12
|
+
*
|
|
13
|
+
* Output:
|
|
14
|
+
* - paragraph "1"
|
|
15
|
+
* - paragraph "2"
|
|
16
|
+
*
|
|
3
17
|
* @param nodes
|
|
18
|
+
* @param context
|
|
4
19
|
* @returns
|
|
5
20
|
*/
|
|
6
21
|
export const unwrapListStep = (nodes, context) => {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { expandToBlockRange } from '@atlaskit/editor-common/selection';
|
|
1
2
|
import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
|
|
2
|
-
import { findParentNodeOfType } from '@atlaskit/editor-prosemirror/utils';
|
|
3
|
+
import { findParentNodeOfType, hasParentNode } from '@atlaskit/editor-prosemirror/utils';
|
|
3
4
|
import { CellSelection } from '@atlaskit/editor-tables';
|
|
4
5
|
export const getSelectedNode = selection => {
|
|
5
6
|
if (selection instanceof NodeSelection) {
|
|
@@ -49,4 +50,31 @@ export const getTargetNodeTypeNameInContext = (nodeTypeName, isNested) => {
|
|
|
49
50
|
return 'nestedExpand';
|
|
50
51
|
}
|
|
51
52
|
return nodeTypeName;
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Use common expandToBlockRange function, but account for edge cases with lists.
|
|
57
|
+
*
|
|
58
|
+
* @param selection
|
|
59
|
+
* @param schema
|
|
60
|
+
* @returns
|
|
61
|
+
*/
|
|
62
|
+
export const expandSelectionToBlockRange = (selection, schema) => {
|
|
63
|
+
const isListInSelection = hasParentNode(node => node.type === schema.nodes.bulletList || node.type === schema.nodes.orderedList)(selection);
|
|
64
|
+
const {
|
|
65
|
+
$from,
|
|
66
|
+
$to
|
|
67
|
+
} = expandToBlockRange(selection.$from, selection.$to, node => {
|
|
68
|
+
if (!isListInSelection) {
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
if (node.type === schema.nodes.bulletList || node.type === schema.nodes.orderedList) {
|
|
72
|
+
return true;
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
$from,
|
|
78
|
+
$to
|
|
79
|
+
};
|
|
52
80
|
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
+
import { NODE_CATEGORY_BY_TYPE } from './types';
|
|
3
|
+
import { unwrapStep } from './unwrapStep';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Determines if a node can be flattened (unwrapped and its contents merged).
|
|
7
|
+
*
|
|
8
|
+
* According to the text transformations list, flattenable nodes are:
|
|
9
|
+
* - Bulleted list, Numbered list, Task list
|
|
10
|
+
* - Text nodes (heading, paragraph)
|
|
11
|
+
*
|
|
12
|
+
* Containers (panels, expands, layouts, blockquotes) and atomic nodes (tables, media, macros) break out.
|
|
13
|
+
*/
|
|
14
|
+
const canFlatten = node => {
|
|
15
|
+
const category = NODE_CATEGORY_BY_TYPE[node.type.name];
|
|
16
|
+
// Text and list nodes can be flattened (converted to simpler forms)
|
|
17
|
+
return category === 'text' || category === 'list';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Flattens a node by extracting its contents using the appropriate unwrap step.
|
|
22
|
+
* This is only called for text and list nodes that can be converted to simpler forms.
|
|
23
|
+
* Uses unwrapStep to extract children from list containers.
|
|
24
|
+
*/
|
|
25
|
+
const flattenNode = (node, context) => {
|
|
26
|
+
return unwrapStep([node], context);
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Determines if a node can be wrapped in the target container type.
|
|
31
|
+
* Uses the schema's validContent to check if the target container can hold this node.
|
|
32
|
+
*
|
|
33
|
+
* Note: What can be wrapped depends on the target container type - for example:
|
|
34
|
+
* - Tables and media CAN go inside expand nodes
|
|
35
|
+
* - Tables CANNOT go inside panels or blockquotes
|
|
36
|
+
*/
|
|
37
|
+
const canWrapInTarget = (node, targetNodeType, targetNodeTypeName) => {
|
|
38
|
+
// Same-type containers should break out as separate containers
|
|
39
|
+
if (node.type.name === targetNodeTypeName) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Use the schema to determine if this node can be contained in the target
|
|
44
|
+
try {
|
|
45
|
+
return targetNodeType.validContent(Fragment.from(node));
|
|
46
|
+
} catch {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Converts a nestedExpand to a regular expand node.
|
|
53
|
+
* NestedExpands can only exist inside expands, so when breaking out they must be converted.
|
|
54
|
+
*/
|
|
55
|
+
const convertNestedExpandToExpand = (node, schema) => {
|
|
56
|
+
var _node$attrs;
|
|
57
|
+
const expandType = schema.nodes.expand;
|
|
58
|
+
if (!expandType) {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
return expandType.createAndFill({
|
|
62
|
+
title: ((_node$attrs = node.attrs) === null || _node$attrs === void 0 ? void 0 : _node$attrs.title) || ''
|
|
63
|
+
}, node.content);
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A wrap step that handles mixed content according to the Compatibility Matrix:
|
|
68
|
+
* - Wraps consecutive compatible nodes into the target container
|
|
69
|
+
* - Same-type containers break out as separate containers (preserved as-is)
|
|
70
|
+
* - NestedExpands break out as regular expands (converted since nestedExpand can't exist outside expand)
|
|
71
|
+
* - Container structures that can't be nested in target break out (not flattened)
|
|
72
|
+
* - Text/list nodes that can't be wrapped are flattened and merged into the container
|
|
73
|
+
* - Atomic nodes (tables, media, macros) break out
|
|
74
|
+
*
|
|
75
|
+
* What can be wrapped depends on the target container's schema:
|
|
76
|
+
* - expand → panel: tables break out, nestedExpands convert to expands and break out
|
|
77
|
+
* - expand → blockquote: tables/media break out, nestedExpands convert to expands and break out
|
|
78
|
+
* - expand → expand: tables/media stay inside (expands can contain them)
|
|
79
|
+
*
|
|
80
|
+
* Example: expand(p('a'), table(), p('b')) → panel: [panel(p('a')), table(), panel(p('b'))]
|
|
81
|
+
* Example: expand(p('a'), panel(p('x')), p('b')) → panel: [panel(p('a')), panel(p('x')), panel(p('b'))]
|
|
82
|
+
* Example: expand(p('a'), nestedExpand({title: 'inner'})(p('x')), p('b')) → panel: [panel(p('a')), expand({title: 'inner'})(p('x')), panel(p('b'))]
|
|
83
|
+
*/
|
|
84
|
+
export const wrapMixedContentStep = (nodes, context) => {
|
|
85
|
+
const {
|
|
86
|
+
schema,
|
|
87
|
+
targetNodeTypeName
|
|
88
|
+
} = context;
|
|
89
|
+
const targetNodeType = schema.nodes[targetNodeTypeName];
|
|
90
|
+
if (!targetNodeType) {
|
|
91
|
+
return nodes;
|
|
92
|
+
}
|
|
93
|
+
const result = [];
|
|
94
|
+
let currentContainerContent = [];
|
|
95
|
+
const flushCurrentContainer = () => {
|
|
96
|
+
if (currentContainerContent.length > 0) {
|
|
97
|
+
const containerNode = targetNodeType.createAndFill({}, Fragment.fromArray(currentContainerContent));
|
|
98
|
+
if (containerNode) {
|
|
99
|
+
result.push(containerNode);
|
|
100
|
+
}
|
|
101
|
+
currentContainerContent = [];
|
|
102
|
+
}
|
|
103
|
+
};
|
|
104
|
+
nodes.forEach(node => {
|
|
105
|
+
if (canWrapInTarget(node, targetNodeType, targetNodeTypeName)) {
|
|
106
|
+
// Node can be wrapped - add to current container content
|
|
107
|
+
currentContainerContent.push(node);
|
|
108
|
+
} else if (node.type.name === targetNodeTypeName) {
|
|
109
|
+
// Same-type container - breaks out as a separate container (preserved as-is)
|
|
110
|
+
// This handles: "If there's a panel in the expand, it breaks out into a separate panel"
|
|
111
|
+
flushCurrentContainer();
|
|
112
|
+
result.push(node);
|
|
113
|
+
} else if (node.type.name === 'nestedExpand') {
|
|
114
|
+
// NestedExpand can't be wrapped and can't exist outside an expand
|
|
115
|
+
// Convert to regular expand and break out
|
|
116
|
+
flushCurrentContainer();
|
|
117
|
+
const expandNode = convertNestedExpandToExpand(node, schema);
|
|
118
|
+
if (expandNode) {
|
|
119
|
+
result.push(expandNode);
|
|
120
|
+
}
|
|
121
|
+
} else if (canFlatten(node)) {
|
|
122
|
+
// Node cannot be wrapped but CAN be flattened - flatten and add to container
|
|
123
|
+
const flattenedNodes = flattenNode(node, context);
|
|
124
|
+
currentContainerContent.push(...flattenedNodes);
|
|
125
|
+
} else {
|
|
126
|
+
// Node cannot be wrapped AND cannot be flattened (containers, tables, media, macros) - break out
|
|
127
|
+
flushCurrentContainer();
|
|
128
|
+
result.push(node);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
// Flush any remaining content into a container
|
|
133
|
+
flushCurrentContainer();
|
|
134
|
+
return result.length > 0 ? result : nodes;
|
|
135
|
+
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { expandToBlockRange } from '@atlaskit/editor-common/selection';
|
|
2
1
|
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
3
2
|
import { isNestedNode } from '../ui/utils/isNestedNode';
|
|
4
3
|
import { getOutputNodes } from './transform-node-utils/transform';
|
|
4
|
+
import { expandSelectionToBlockRange } from './transform-node-utils/utils';
|
|
5
5
|
import { isListNode } from './transforms/utils';
|
|
6
6
|
export const transformNode = api =>
|
|
7
7
|
// eslint-disable-next-line no-unused-vars
|
|
@@ -17,7 +17,7 @@ export const transformNode = api =>
|
|
|
17
17
|
const {
|
|
18
18
|
$from,
|
|
19
19
|
$to
|
|
20
|
-
} =
|
|
20
|
+
} = expandSelectionToBlockRange(preservedSelection, tr.doc.type.schema);
|
|
21
21
|
const isNested = isNestedNode(preservedSelection, '');
|
|
22
22
|
const selectedParent = $from.parent;
|
|
23
23
|
let fragment = Fragment.empty;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { BLOCK_ACTIONS_COPY_LINK_TO_BLOCK_MENU_ITEM, BLOCK_ACTIONS_MENU_SECTION, BLOCK_ACTIONS_MENU_SECTION_RANK, DELETE_MENU_SECTION, DELETE_MENU_SECTION_RANK, DELETE_MENU_ITEM, POSITION_MENU_SECTION, POSITION_MENU_SECTION_RANK, POSITION_MOVE_DOWN_MENU_ITEM, POSITION_MOVE_UP_MENU_ITEM, TRANSFORM_MENU_ITEM, TRANSFORM_MENU_ITEM_RANK, TRANSFORM_MENU_SECTION, TRANSFORM_MENU_SECTION_RANK, TRANSFORM_CREATE_MENU_SECTION, TRANSFORM_SUGGESTED_MENU_SECTION, TRANSFORM_STRUCTURE_MENU_SECTION, TRANSFORM_HEADINGS_MENU_SECTION, MAIN_BLOCK_MENU_SECTION_RANK } from '@atlaskit/editor-common/block-menu';
|
|
2
|
+
import { BLOCK_ACTIONS_COPY_LINK_TO_BLOCK_MENU_ITEM, BLOCK_ACTIONS_MENU_SECTION, BLOCK_ACTIONS_MENU_SECTION_RANK, DELETE_MENU_SECTION, DELETE_MENU_SECTION_RANK, DELETE_MENU_ITEM, POSITION_MENU_SECTION, POSITION_MENU_SECTION_RANK, POSITION_MOVE_DOWN_MENU_ITEM, POSITION_MOVE_UP_MENU_ITEM, TRANSFORM_MENU_ITEM, TRANSFORM_MENU_ITEM_RANK, TRANSFORM_MENU_SECTION, TRANSFORM_MENU_SECTION_RANK, TRANSFORM_CREATE_MENU_SECTION, TRANSFORM_SUGGESTED_MENU_SECTION, TRANSFORM_STRUCTURE_MENU_SECTION, TRANSFORM_HEADINGS_MENU_SECTION, MAIN_BLOCK_MENU_SECTION_RANK, TRANSFORM_SUGGESTED_MENU_SECTION_RANK, TRANSFORM_SUGGESTED_MENU_ITEM } from '@atlaskit/editor-common/block-menu';
|
|
3
3
|
import { ToolbarDropdownItemSection } from '@atlaskit/editor-toolbar';
|
|
4
4
|
import { CopyLinkDropdownItem } from './copy-link';
|
|
5
5
|
import { CopySection } from './copy-section';
|
|
@@ -9,6 +9,7 @@ import { FormatMenuComponent } from './format-menu-nested';
|
|
|
9
9
|
import { FormatMenuSection } from './format-menu-section';
|
|
10
10
|
import { MoveDownDropdownItem } from './move-down';
|
|
11
11
|
import { MoveUpDropdownItem } from './move-up';
|
|
12
|
+
import { SuggestedItemsRenderer } from './suggested-items-renderer';
|
|
12
13
|
const getMoveUpMoveDownMenuComponents = api => {
|
|
13
14
|
return [{
|
|
14
15
|
type: 'block-menu-item',
|
|
@@ -60,14 +61,16 @@ const getTurnIntoMenuComponents = api => {
|
|
|
60
61
|
key: TRANSFORM_MENU_ITEM.key,
|
|
61
62
|
rank: TRANSFORM_MENU_ITEM_RANK[TRANSFORM_SUGGESTED_MENU_SECTION.key]
|
|
62
63
|
},
|
|
63
|
-
component: ({
|
|
64
|
-
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
component: () => /*#__PURE__*/React.createElement(SuggestedItemsRenderer, {
|
|
65
|
+
api: api
|
|
66
|
+
})
|
|
67
|
+
}, {
|
|
68
|
+
type: 'block-menu-item',
|
|
69
|
+
key: TRANSFORM_SUGGESTED_MENU_ITEM.key,
|
|
70
|
+
parent: {
|
|
71
|
+
type: 'block-menu-section',
|
|
72
|
+
key: TRANSFORM_SUGGESTED_MENU_SECTION.key,
|
|
73
|
+
rank: TRANSFORM_SUGGESTED_MENU_SECTION_RANK[TRANSFORM_SUGGESTED_MENU_ITEM.key]
|
|
71
74
|
}
|
|
72
75
|
}, {
|
|
73
76
|
type: 'block-menu-section',
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import React, { useMemo } from 'react';
|
|
2
|
+
import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
|
|
3
|
+
import { ToolbarDropdownItemSection } from '@atlaskit/editor-toolbar';
|
|
4
|
+
import { getSelectedNode } from '../editor-commands/transform-node-utils/utils';
|
|
5
|
+
import { getSortedSuggestedItems } from './utils/suggested-items-rank';
|
|
6
|
+
export const SuggestedItemsRenderer = /*#__PURE__*/React.memo(({
|
|
7
|
+
api
|
|
8
|
+
}) => {
|
|
9
|
+
var _api$blockMenu;
|
|
10
|
+
const {
|
|
11
|
+
preservedSelection
|
|
12
|
+
} = useSharedPluginStateWithSelector(api, ['blockControls'], states => {
|
|
13
|
+
var _states$blockControls;
|
|
14
|
+
return {
|
|
15
|
+
preservedSelection: (_states$blockControls = states.blockControlsState) === null || _states$blockControls === void 0 ? void 0 : _states$blockControls.preservedSelection
|
|
16
|
+
};
|
|
17
|
+
});
|
|
18
|
+
const blockMenuComponents = api === null || api === void 0 ? void 0 : (_api$blockMenu = api.blockMenu) === null || _api$blockMenu === void 0 ? void 0 : _api$blockMenu.actions.getBlockMenuComponents();
|
|
19
|
+
const menuItemsMap = useMemo(() => {
|
|
20
|
+
if (!blockMenuComponents) {
|
|
21
|
+
return new Map();
|
|
22
|
+
}
|
|
23
|
+
return new Map(blockMenuComponents.filter(c => c.type === 'block-menu-item').map(item => [item.key, item]));
|
|
24
|
+
}, [blockMenuComponents]);
|
|
25
|
+
const suggestedItems = useMemo(() => {
|
|
26
|
+
if (!preservedSelection || menuItemsMap.size === 0) {
|
|
27
|
+
return [];
|
|
28
|
+
}
|
|
29
|
+
const selectedNode = getSelectedNode(preservedSelection);
|
|
30
|
+
if (!selectedNode) {
|
|
31
|
+
return [];
|
|
32
|
+
}
|
|
33
|
+
const nodeTypeName = selectedNode.node.type.name;
|
|
34
|
+
const sortedKeys = getSortedSuggestedItems(nodeTypeName);
|
|
35
|
+
return sortedKeys.map(key => menuItemsMap.get(key)).filter(item => item !== undefined);
|
|
36
|
+
}, [menuItemsMap, preservedSelection]);
|
|
37
|
+
if (suggestedItems.length === 0) {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
|
|
41
|
+
title: "Suggested"
|
|
42
|
+
}, suggestedItems.map(item => {
|
|
43
|
+
const ItemComponent = item.component;
|
|
44
|
+
return ItemComponent ? /*#__PURE__*/React.createElement(ItemComponent, {
|
|
45
|
+
key: item.key
|
|
46
|
+
}) : null;
|
|
47
|
+
}));
|
|
48
|
+
});
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Suggested transformations mapping for each block type.
|
|
3
|
+
* Based on the Block Menu Compatibility Matrix:
|
|
4
|
+
* https://hello.atlassian.net/wiki/spaces/egcuc/pages/5868774224/Block+Menu+Compatibility+Matrix#Suggested-for-each-block-type
|
|
5
|
+
*
|
|
6
|
+
* This mapping defines which transform items should appear in the TRANSFORM_SUGGESTED_MENU_SECTION
|
|
7
|
+
* for each block type, ranked by priority (lower rank = higher priority).
|
|
8
|
+
*
|
|
9
|
+
* Structure:
|
|
10
|
+
* {
|
|
11
|
+
* [sourceNodeType]: {
|
|
12
|
+
* [targetMenuItemKey]: rank
|
|
13
|
+
* }
|
|
14
|
+
* }
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { TRANSFORM_STRUCTURE_PANEL_MENU_ITEM, TRANSFORM_STRUCTURE_EXPAND_MENU_ITEM, TRANSFORM_STRUCTURE_LAYOUT_MENU_ITEM, TRANSFORM_STRUCTURE_QUOTE_MENU_ITEM, TRANSFORM_STRUCTURE_CODE_BLOCK_MENU_ITEM, TRANSFORM_STRUCTURE_BULLETED_LIST_MENU_ITEM, TRANSFORM_STRUCTURE_NUMBERED_LIST_MENU_ITEM, TRANSFORM_STRUCTURE_TASK_LIST_MENU_ITEM, TRANSFORM_HEADINGS_H2_MENU_ITEM, TRANSFORM_HEADINGS_H3_MENU_ITEM, TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM } from '@atlaskit/editor-common/block-menu';
|
|
18
|
+
export const BLOCK_MENU_NODE_TYPES = {
|
|
19
|
+
PARAGRAPH: 'paragraph',
|
|
20
|
+
EXPAND: 'expand',
|
|
21
|
+
BLOCKQUOTE: 'blockquote',
|
|
22
|
+
LAYOUT_SECTION: 'layoutSection',
|
|
23
|
+
PANEL: 'panel',
|
|
24
|
+
CODE_BLOCK: 'codeBlock',
|
|
25
|
+
DECISION: 'decisionList',
|
|
26
|
+
BULLET_LIST: 'bulletList',
|
|
27
|
+
ORDERED_LIST: 'orderedList',
|
|
28
|
+
HEADING: 'heading',
|
|
29
|
+
TASK_LIST: 'taskList',
|
|
30
|
+
MEDIA_SINGLE: 'mediaSingle',
|
|
31
|
+
EXTENSION: 'extension',
|
|
32
|
+
BODIED_EXTENSION: 'bodiedExtension',
|
|
33
|
+
BLOCK_CARD: 'blockCard',
|
|
34
|
+
EMBED_CARD: 'embedCard',
|
|
35
|
+
TABLE: 'table'
|
|
36
|
+
};
|
|
37
|
+
export const TRANSFORM_SUGGESTED_ITEMS_RANK = {
|
|
38
|
+
[BLOCK_MENU_NODE_TYPES.PARAGRAPH]: {
|
|
39
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
40
|
+
[TRANSFORM_HEADINGS_H2_MENU_ITEM.key]: 200,
|
|
41
|
+
[TRANSFORM_HEADINGS_H3_MENU_ITEM.key]: 300
|
|
42
|
+
},
|
|
43
|
+
[BLOCK_MENU_NODE_TYPES.EXPAND]: {
|
|
44
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
45
|
+
[TRANSFORM_STRUCTURE_LAYOUT_MENU_ITEM.key]: 200,
|
|
46
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 300
|
|
47
|
+
},
|
|
48
|
+
[BLOCK_MENU_NODE_TYPES.BLOCKQUOTE]: {
|
|
49
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
50
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 200,
|
|
51
|
+
[TRANSFORM_STRUCTURE_LAYOUT_MENU_ITEM.key]: 300
|
|
52
|
+
},
|
|
53
|
+
[BLOCK_MENU_NODE_TYPES.LAYOUT_SECTION]: {
|
|
54
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
55
|
+
[TRANSFORM_STRUCTURE_EXPAND_MENU_ITEM.key]: 200,
|
|
56
|
+
[TRANSFORM_HEADINGS_H2_MENU_ITEM.key]: 300
|
|
57
|
+
},
|
|
58
|
+
[BLOCK_MENU_NODE_TYPES.PANEL]: {
|
|
59
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 100,
|
|
60
|
+
[TRANSFORM_STRUCTURE_QUOTE_MENU_ITEM.key]: 200,
|
|
61
|
+
[TRANSFORM_STRUCTURE_CODE_BLOCK_MENU_ITEM.key]: 300
|
|
62
|
+
},
|
|
63
|
+
[BLOCK_MENU_NODE_TYPES.CODE_BLOCK]: {
|
|
64
|
+
[TRANSFORM_STRUCTURE_EXPAND_MENU_ITEM.key]: 100,
|
|
65
|
+
[TRANSFORM_STRUCTURE_LAYOUT_MENU_ITEM.key]: 200,
|
|
66
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 300
|
|
67
|
+
},
|
|
68
|
+
[BLOCK_MENU_NODE_TYPES.DECISION]: {
|
|
69
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
70
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 200,
|
|
71
|
+
[TRANSFORM_STRUCTURE_TASK_LIST_MENU_ITEM.key]: 300
|
|
72
|
+
},
|
|
73
|
+
[BLOCK_MENU_NODE_TYPES.BULLET_LIST]: {
|
|
74
|
+
[TRANSFORM_STRUCTURE_NUMBERED_LIST_MENU_ITEM.key]: 100,
|
|
75
|
+
[TRANSFORM_STRUCTURE_QUOTE_MENU_ITEM.key]: 200,
|
|
76
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 300
|
|
77
|
+
},
|
|
78
|
+
[BLOCK_MENU_NODE_TYPES.ORDERED_LIST]: {
|
|
79
|
+
[TRANSFORM_STRUCTURE_BULLETED_LIST_MENU_ITEM.key]: 100,
|
|
80
|
+
[TRANSFORM_STRUCTURE_TASK_LIST_MENU_ITEM.key]: 200,
|
|
81
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 300
|
|
82
|
+
},
|
|
83
|
+
[BLOCK_MENU_NODE_TYPES.HEADING]: {
|
|
84
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 100,
|
|
85
|
+
[TRANSFORM_HEADINGS_H2_MENU_ITEM.key]: 200,
|
|
86
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 300
|
|
87
|
+
},
|
|
88
|
+
[BLOCK_MENU_NODE_TYPES.TASK_LIST]: {
|
|
89
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 100,
|
|
90
|
+
[TRANSFORM_STRUCTURE_NUMBERED_LIST_MENU_ITEM.key]: 200,
|
|
91
|
+
[TRANSFORM_STRUCTURE_CODE_BLOCK_MENU_ITEM.key]: 300
|
|
92
|
+
},
|
|
93
|
+
[BLOCK_MENU_NODE_TYPES.MEDIA_SINGLE]: {
|
|
94
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
95
|
+
[TRANSFORM_STRUCTURE_LAYOUT_MENU_ITEM.key]: 200,
|
|
96
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 300
|
|
97
|
+
},
|
|
98
|
+
[BLOCK_MENU_NODE_TYPES.EXTENSION]: {
|
|
99
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
100
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 200,
|
|
101
|
+
[TRANSFORM_STRUCTURE_EXPAND_MENU_ITEM.key]: 300
|
|
102
|
+
},
|
|
103
|
+
[BLOCK_MENU_NODE_TYPES.BODIED_EXTENSION]: {
|
|
104
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
105
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 200,
|
|
106
|
+
[TRANSFORM_STRUCTURE_EXPAND_MENU_ITEM.key]: 300
|
|
107
|
+
},
|
|
108
|
+
[BLOCK_MENU_NODE_TYPES.BLOCK_CARD]: {
|
|
109
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
110
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 200
|
|
111
|
+
},
|
|
112
|
+
[BLOCK_MENU_NODE_TYPES.EMBED_CARD]: {
|
|
113
|
+
[TRANSFORM_STRUCTURE_PANEL_MENU_ITEM.key]: 100,
|
|
114
|
+
[TRANSFORM_STRUCTURE_PARAGRAPH_MENU_ITEM.key]: 200
|
|
115
|
+
},
|
|
116
|
+
[BLOCK_MENU_NODE_TYPES.TABLE]: {
|
|
117
|
+
[TRANSFORM_STRUCTURE_EXPAND_MENU_ITEM.key]: 100,
|
|
118
|
+
[TRANSFORM_STRUCTURE_LAYOUT_MENU_ITEM.key]: 200
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
export const getSuggestedItemsForNodeType = nodeType => {
|
|
122
|
+
return TRANSFORM_SUGGESTED_ITEMS_RANK[nodeType];
|
|
123
|
+
};
|
|
124
|
+
export const getSortedSuggestedItems = nodeType => {
|
|
125
|
+
const suggestions = getSuggestedItemsForNodeType(nodeType);
|
|
126
|
+
if (!suggestions) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
return Object.entries(suggestions).sort(([, rankA], [, rankB]) => rankA - rankB).map(([key]) => key);
|
|
130
|
+
};
|
|
@@ -1,37 +1,32 @@
|
|
|
1
1
|
import { Fragment } from '@atlaskit/editor-prosemirror/model';
|
|
2
|
-
var extractNestedLists = function extractNestedLists(node, listTypes, itemTypes) {
|
|
2
|
+
var extractNestedLists = function extractNestedLists(node, listTypes, itemTypes, schema) {
|
|
3
3
|
var items = [];
|
|
4
|
+
var paragraph = schema.nodes.paragraph;
|
|
4
5
|
var _extract = function extract(currentNode) {
|
|
5
6
|
currentNode.forEach(function (child) {
|
|
6
|
-
// list item -> take content without nested lists, then recurse into nested lists
|
|
7
7
|
if (itemTypes.some(function (type) {
|
|
8
8
|
return child.type === type;
|
|
9
9
|
})) {
|
|
10
|
-
// Filter out nested list nodes from the list item's content
|
|
11
10
|
var contentWithoutNestedLists = [];
|
|
12
11
|
var nestedLists = [];
|
|
13
12
|
child.forEach(function (grandChild) {
|
|
14
13
|
if (listTypes.some(function (type) {
|
|
15
14
|
return grandChild.type === type;
|
|
16
15
|
})) {
|
|
17
|
-
// This is a nested list - collect it for later processing
|
|
18
16
|
nestedLists.push(grandChild);
|
|
19
17
|
} else {
|
|
20
|
-
|
|
21
|
-
|
|
18
|
+
if (grandChild.isText) {
|
|
19
|
+
contentWithoutNestedLists.push(paragraph.createAndFill({}, grandChild));
|
|
20
|
+
} else {
|
|
21
|
+
contentWithoutNestedLists.push(grandChild);
|
|
22
|
+
}
|
|
22
23
|
}
|
|
23
24
|
});
|
|
24
|
-
|
|
25
|
-
// Add the list item with only its non-list content
|
|
26
25
|
items.push(child.copy(Fragment.from(contentWithoutNestedLists)));
|
|
27
|
-
|
|
28
|
-
// Now process nested lists to maintain document order
|
|
29
26
|
nestedLists.forEach(function (nestedList) {
|
|
30
27
|
_extract(nestedList);
|
|
31
28
|
});
|
|
32
|
-
}
|
|
33
|
-
// lists -> keep operating
|
|
34
|
-
else if (listTypes.some(function (type) {
|
|
29
|
+
} else if (listTypes.some(function (type) {
|
|
35
30
|
return child.type === type;
|
|
36
31
|
})) {
|
|
37
32
|
_extract(child);
|
|
@@ -47,24 +42,6 @@ var extractNestedLists = function extractNestedLists(node, listTypes, itemTypes)
|
|
|
47
42
|
* to it's first ancestor list, maintaining document order.
|
|
48
43
|
*
|
|
49
44
|
* @example
|
|
50
|
-
* Input:
|
|
51
|
-
* - bulletList
|
|
52
|
-
* - listItem "A"
|
|
53
|
-
* - listItem "B"
|
|
54
|
-
* - bulletList
|
|
55
|
-
* - listItem "C"
|
|
56
|
-
* - listItem "D"
|
|
57
|
-
* - listItem "E"
|
|
58
|
-
*
|
|
59
|
-
* Output:
|
|
60
|
-
* - bulletList
|
|
61
|
-
* - listItem "A"
|
|
62
|
-
* - listItem "B"
|
|
63
|
-
* - listItem "C"
|
|
64
|
-
* - listItem "D"
|
|
65
|
-
* - listItem "E"
|
|
66
|
-
*
|
|
67
|
-
* @example
|
|
68
45
|
* Input (deeply nested):
|
|
69
46
|
* - bulletList
|
|
70
47
|
* - listItem "1"
|
|
@@ -95,7 +72,7 @@ export var flattenListStep = function flattenListStep(nodes, context) {
|
|
|
95
72
|
if (listTypes.some(function (type) {
|
|
96
73
|
return node.type === type;
|
|
97
74
|
})) {
|
|
98
|
-
return node.copy(Fragment.from(extractNestedLists(node, listTypes, [context.schema.nodes.listItem, context.schema.nodes.taskItem])));
|
|
75
|
+
return node.copy(Fragment.from(extractNestedLists(node, listTypes, [context.schema.nodes.listItem, context.schema.nodes.taskItem], context.schema)));
|
|
99
76
|
}
|
|
100
77
|
return node;
|
|
101
78
|
});
|
|
@@ -8,6 +8,7 @@ import { unwrapExpandStep } from './unwrapExpandStep';
|
|
|
8
8
|
import { unwrapListStep } from './unwrapListStep';
|
|
9
9
|
import { unwrapStep } from './unwrapStep';
|
|
10
10
|
import { wrapIntoLayoutStep } from './wrapIntoLayoutStep';
|
|
11
|
+
import { wrapMixedContentStep } from './wrapMixedContentStep';
|
|
11
12
|
import { wrapStep } from './wrapStep';
|
|
12
13
|
|
|
13
14
|
// Exampled step for overrides:
|
|
@@ -57,15 +58,15 @@ var TRANSFORM_STEPS_OVERRIDE = {
|
|
|
57
58
|
codeBlock: [unwrapStep, flattenStep, wrapStep]
|
|
58
59
|
},
|
|
59
60
|
expand: {
|
|
60
|
-
panel: [unwrapExpandStep,
|
|
61
|
-
blockquote: [unwrapExpandStep,
|
|
61
|
+
panel: [unwrapExpandStep, wrapMixedContentStep],
|
|
62
|
+
blockquote: [unwrapExpandStep, wrapMixedContentStep],
|
|
62
63
|
layoutSection: [unwrapExpandStep, wrapIntoLayoutStep],
|
|
63
64
|
paragraph: [unwrapExpandStep],
|
|
64
65
|
codeBlock: [unwrapExpandStep, flattenStep, wrapStep]
|
|
65
66
|
},
|
|
66
67
|
nestedExpand: {
|
|
67
|
-
panel: [unwrapExpandStep,
|
|
68
|
-
blockquote: [unwrapExpandStep,
|
|
68
|
+
panel: [unwrapExpandStep, wrapMixedContentStep],
|
|
69
|
+
blockquote: [unwrapExpandStep, wrapMixedContentStep],
|
|
69
70
|
paragraph: [unwrapExpandStep],
|
|
70
71
|
codeBlock: [unwrapExpandStep, flattenStep, wrapStep]
|
|
71
72
|
},
|
|
@@ -1,7 +1,22 @@
|
|
|
1
1
|
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
|
|
2
2
|
/**
|
|
3
|
-
* Given an array of nodes,
|
|
3
|
+
* Given an array of nodes, processes each list removing all parent list nodes and
|
|
4
|
+
* just returning their child contents.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* Input:
|
|
8
|
+
* - bulletList
|
|
9
|
+
* - listItem "1"
|
|
10
|
+
* - paragraph "1"
|
|
11
|
+
* - listItem "2"
|
|
12
|
+
* - paragraph "2"
|
|
13
|
+
*
|
|
14
|
+
* Output:
|
|
15
|
+
* - paragraph "1"
|
|
16
|
+
* - paragraph "2"
|
|
17
|
+
*
|
|
4
18
|
* @param nodes
|
|
19
|
+
* @param context
|
|
5
20
|
* @returns
|
|
6
21
|
*/
|
|
7
22
|
export var unwrapListStep = function unwrapListStep(nodes, context) {
|