@atlaskit/editor-plugin-block-menu 5.2.1 → 5.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/dist/cjs/editor-commands/transform-node-utils/{flattenListStep.js → steps/flattenListStep.js} +3 -5
  3. package/dist/cjs/editor-commands/transform-node-utils/steps/listToListStep.js +232 -0
  4. package/dist/cjs/editor-commands/transform-node-utils/transform.js +4 -3
  5. package/dist/cjs/ui/copy-link.js +35 -18
  6. package/dist/cjs/ui/utils/copyLink.js +30 -38
  7. package/dist/es2019/editor-commands/transform-node-utils/{flattenListStep.js → steps/flattenListStep.js} +3 -5
  8. package/dist/es2019/editor-commands/transform-node-utils/steps/listToListStep.js +225 -0
  9. package/dist/es2019/editor-commands/transform-node-utils/transform.js +4 -3
  10. package/dist/es2019/ui/copy-link.js +35 -15
  11. package/dist/es2019/ui/utils/copyLink.js +26 -25
  12. package/dist/esm/editor-commands/transform-node-utils/{flattenListStep.js → steps/flattenListStep.js} +3 -5
  13. package/dist/esm/editor-commands/transform-node-utils/steps/listToListStep.js +226 -0
  14. package/dist/esm/editor-commands/transform-node-utils/transform.js +4 -3
  15. package/dist/esm/ui/copy-link.js +36 -19
  16. package/dist/esm/ui/utils/copyLink.js +31 -39
  17. package/dist/{types-ts4.5/editor-commands/transform-node-utils → types/editor-commands/transform-node-utils/steps}/flattenListStep.d.ts +1 -1
  18. package/dist/types/editor-commands/transform-node-utils/steps/listToListStep.d.ts +65 -0
  19. package/dist/{types-ts4.5/editor-commands/transform-node-utils → types/editor-commands/transform-node-utils/steps}/unwrapListStep.d.ts +1 -1
  20. package/dist/types/ui/utils/copyLink.d.ts +10 -3
  21. package/dist/{types/editor-commands/transform-node-utils → types-ts4.5/editor-commands/transform-node-utils/steps}/flattenListStep.d.ts +1 -1
  22. package/dist/types-ts4.5/editor-commands/transform-node-utils/steps/listToListStep.d.ts +65 -0
  23. package/dist/{types/editor-commands/transform-node-utils → types-ts4.5/editor-commands/transform-node-utils/steps}/unwrapListStep.d.ts +1 -1
  24. package/dist/types-ts4.5/ui/utils/copyLink.d.ts +10 -3
  25. package/package.json +2 -2
  26. /package/dist/cjs/editor-commands/transform-node-utils/{unwrapListStep.js → steps/unwrapListStep.js} +0 -0
  27. /package/dist/es2019/editor-commands/transform-node-utils/{unwrapListStep.js → steps/unwrapListStep.js} +0 -0
  28. /package/dist/esm/editor-commands/transform-node-utils/{unwrapListStep.js → steps/unwrapListStep.js} +0 -0
@@ -0,0 +1,225 @@
1
+ import { Fragment } from '@atlaskit/editor-prosemirror/model';
2
+ const isListType = (node, schema) => {
3
+ const lists = [schema.nodes.taskList, schema.nodes.bulletList, schema.nodes.orderedList];
4
+ return lists.some(list => list === node.type);
5
+ };
6
+
7
+ /**
8
+ * Converts FROM taskList structure TO bulletList/orderedList structure.
9
+ */
10
+ const convertFromTaskListStructure = (node, targetListType, targetItemType) => {
11
+ const schema = node.type.schema;
12
+ const targetListNodeType = schema.nodes[targetListType];
13
+ const convertedItems = [];
14
+ node.content.forEach(child => {
15
+ if (isListType(child, schema)) {
16
+ // This is a nested list - it should become a child of the previous item
17
+ if (convertedItems.length > 0) {
18
+ const previousItem = convertedItems[convertedItems.length - 1];
19
+ // Convert the nested list and add it to the previous item's content
20
+ const convertedNestedList = transformList(child, targetListType, targetItemType);
21
+ const newContent = previousItem.content.append(Fragment.from([convertedNestedList]));
22
+ const updatedItem = previousItem.type.create(previousItem.attrs, newContent);
23
+ convertedItems[convertedItems.length - 1] = updatedItem;
24
+ }
25
+ // If there's no previous item, skip this nested list (orphaned)
26
+ } else {
27
+ const convertedItem = transformListItem(child, targetItemType, targetListType);
28
+ if (convertedItem) {
29
+ convertedItems.push(convertedItem);
30
+ }
31
+ }
32
+ });
33
+ return targetListNodeType.create(node.attrs, Fragment.from(convertedItems));
34
+ };
35
+
36
+ /**
37
+ * Converts FROM bulletList/orderedList structure TO taskList structure.
38
+ */
39
+ const convertToTaskListStructure = (node, targetListType, targetItemType) => {
40
+ const schema = node.type.schema;
41
+ const targetListNodeType = schema.nodes[targetListType];
42
+ const transformedContent = [];
43
+ node.content.forEach(itemNode => {
44
+ const transformedItem = transformListItem(itemNode, targetItemType, targetListType, true);
45
+ if (transformedItem) {
46
+ transformedContent.push(transformedItem);
47
+ }
48
+ itemNode.content.forEach(child => {
49
+ if (isListType(child, schema)) {
50
+ const transformedNestedList = transformList(child, targetListType, targetItemType);
51
+ transformedContent.push(transformedNestedList);
52
+ }
53
+ });
54
+ });
55
+ return targetListNodeType.create(node.attrs, Fragment.from(transformedContent));
56
+ };
57
+
58
+ /**
59
+ * Converts a single list item (listItem or taskItem) to the target item type.
60
+ * Handles content transformation based on the target type's requirements.
61
+ * @param itemNode - The list item node to convert
62
+ * @param targetItemType - The target item type (listItem or taskItem)
63
+ * @param targetListType - The target list type (bulletList, orderedList, or taskList)
64
+ * @param excludeNestedLists - When true, nested lists are excluded from the item's content
65
+ * (used when converting to taskList where nested lists become siblings)
66
+ */
67
+ const transformListItem = (itemNode, targetItemType, targetListType, excludeNestedLists = false) => {
68
+ const schema = itemNode.type.schema;
69
+ const targetItemNodeType = schema.nodes[targetItemType];
70
+ const isTargetTaskItem = targetItemType === 'taskItem';
71
+ const isSourceTaskItem = itemNode.type.name === 'taskItem';
72
+ const paragraphType = schema.nodes.paragraph;
73
+ if (!targetItemNodeType) {
74
+ return null;
75
+ }
76
+ if (isTargetTaskItem) {
77
+ const inlineContent = [];
78
+ itemNode.content.forEach(child => {
79
+ if (child.type === paragraphType) {
80
+ child.content.forEach(inline => {
81
+ inlineContent.push(inline);
82
+ });
83
+ }
84
+ if (child.isText) {
85
+ inlineContent.push(child);
86
+ }
87
+ // TODO: EDITOR-3887 - Skip mediaSingle, codeBlock, and nested lists
88
+ // Nested lists will be extracted and placed as siblings in the taskList
89
+ });
90
+ return targetItemNodeType.create({}, Fragment.from(inlineContent));
91
+ } else {
92
+ const newContent = [];
93
+ if (isSourceTaskItem) {
94
+ newContent.push(paragraphType.create(null, itemNode.content));
95
+ } else {
96
+ itemNode.content.forEach(child => {
97
+ if (isListType(child, schema)) {
98
+ if (excludeNestedLists) {
99
+ // Skip nested lists - they will be handled separately as siblings
100
+ return;
101
+ }
102
+ newContent.push(transformList(child, targetListType, targetItemType));
103
+ } else {
104
+ newContent.push(child);
105
+ }
106
+ });
107
+ }
108
+ if (newContent.length === 0) {
109
+ newContent.push(paragraphType.create());
110
+ }
111
+ return targetItemNodeType.create({}, Fragment.from(newContent));
112
+ }
113
+ };
114
+
115
+ /**
116
+ * Recursively converts nested lists to the target list type.
117
+ * This function handles the conversion of both the list container and its items,
118
+ * including any nested lists within those items.
119
+ *
120
+ * Important: taskList has a different nesting structure than bulletList/orderedList:
121
+ * - taskList: nested taskLists are SIBLINGS of taskItems in the parent taskList
122
+ * - bulletList/orderedList: nested lists are CHILDREN of listItems
123
+ */
124
+ const transformList = (node, targetListType, targetItemType) => {
125
+ const schema = node.type.schema;
126
+ const targetListNodeType = schema.nodes[targetListType];
127
+ const targetItemNodeType = schema.nodes[targetItemType];
128
+ const taskListType = schema.nodes.taskList;
129
+ if (!targetListNodeType || !targetItemNodeType) {
130
+ return node;
131
+ }
132
+ const isSourceTaskList = node.type === taskListType;
133
+ const isTargetTaskList = targetListType === 'taskList';
134
+ if (isSourceTaskList && !isTargetTaskList) {
135
+ return convertFromTaskListStructure(node, targetListType, targetItemType);
136
+ } else if (!isSourceTaskList && isTargetTaskList) {
137
+ return convertToTaskListStructure(node, targetListType, targetItemType);
138
+ } else {
139
+ const transformedItems = [];
140
+ node.content.forEach(childNode => {
141
+ const transformedItem = isListType(childNode, schema) ? transformList(childNode, targetListType, targetItemType) : transformListItem(childNode, targetItemType, targetListType);
142
+ if (transformedItem) {
143
+ transformedItems.push(transformedItem);
144
+ }
145
+ });
146
+ return targetListNodeType.create(node.attrs, Fragment.from(transformedItems));
147
+ }
148
+ };
149
+
150
+ /**
151
+ * Transform step that converts between bulletList, orderedList, and taskList types.
152
+ * This step maintains the order and indentation of the list by recursively
153
+ * converting all nested lists while preserving the structure. It also handles
154
+ * conversion between listItem and taskItem types.
155
+ *
156
+ * When converting to taskList/taskItem, unsupported content (images, codeBlocks) is filtered out.
157
+ *
158
+ * @example
159
+ * Input (bulletList with nested bulletList):
160
+ * - bulletList
161
+ * - listItem "1"
162
+ * - bulletList
163
+ * - listItem "1.1"
164
+ * - bulletList
165
+ * - listItem "1.1.1"
166
+ * - listItem "1.2"
167
+ * - listItem "2"
168
+ *
169
+ * Output (orderedList with nested orderedList):
170
+ * 1. orderedList
171
+ * 1. listItem "1"
172
+ * 1. orderedList
173
+ * 1. listItem "1.1"
174
+ * 1. orderedList
175
+ * 1. listItem "1.1.1"
176
+ * 2. listItem "1.2"
177
+ * 2. listItem "2"
178
+ *
179
+ * @example
180
+ * Input (bulletList with nested taskList):
181
+ * - bulletList
182
+ * - listItem "Regular item"
183
+ * - taskList
184
+ * - taskItem "Task 1" (checked)
185
+ * - taskItem "Task 2" (unchecked)
186
+ *
187
+ * Output (orderedList with nested orderedList, taskItems converted to listItems):
188
+ * 1. orderedList
189
+ * 1. listItem "Regular item"
190
+ * 1. orderedList
191
+ * 1. listItem "Task 1"
192
+ * 2. listItem "Task 2"
193
+ *
194
+ * @example
195
+ * Input (bulletList to taskList, with paragraph extraction):
196
+ * - bulletList
197
+ * - listItem
198
+ * - paragraph "Text content"
199
+ * - listItem
200
+ * - paragraph "Text"
201
+ * - codeBlock "code"
202
+ * - mediaSingle (image)
203
+ *
204
+ * Output (taskList with text extracted from paragraphs, unsupported content filtered):
205
+ * - taskList
206
+ * - taskItem "Text content" (text extracted from paragraph)
207
+ * - taskItem "Text" (text extracted, codeBlock and image filtered out)
208
+ *
209
+ * @param nodes - The nodes to transform
210
+ * @param context - The transformation context containing schema and target node type
211
+ * @returns The transformed nodes
212
+ */
213
+ export const listToListStep = (nodes, context) => {
214
+ const {
215
+ schema,
216
+ targetNodeTypeName
217
+ } = context;
218
+ return nodes.map(node => {
219
+ if (isListType(node, schema)) {
220
+ const targetItemType = targetNodeTypeName === 'taskList' ? 'taskItem' : 'listItem';
221
+ return transformList(node, targetNodeTypeName, targetItemType);
222
+ }
223
+ return node;
224
+ });
225
+ };
@@ -1,14 +1,15 @@
1
1
  import { getTargetNodeTypeNameInContext } from '../transform-node-utils/utils';
2
- import { flattenListStep } from './flattenListStep';
3
2
  import { flattenStep } from './flattenStep';
4
3
  import { convertBulletListToTextStep } from './steps/convertBulletListToTextStep';
5
4
  import { convertOrderedListToTextStep } from './steps/convertOrderedListToTextStep';
6
5
  import { convertTaskListToTextStep } from './steps/convertTaskListToTextStep';
6
+ import { flattenListStep } from './steps/flattenListStep';
7
+ import { listToListStep } from './steps/listToListStep';
7
8
  import { unwrapLayoutStep } from './steps/unwrapLayoutStep';
9
+ import { unwrapListStep } from './steps/unwrapListStep';
8
10
  import { stubStep } from './stubStep';
9
11
  import { NODE_CATEGORY_BY_TYPE, toNodeTypeValue } from './types';
10
12
  import { unwrapExpandStep } from './unwrapExpandStep';
11
- import { unwrapListStep } from './unwrapListStep';
12
13
  import { unwrapStep } from './unwrapStep';
13
14
  import { wrapIntoLayoutStep } from './wrapIntoLayoutStep';
14
15
  import { wrapMixedContentStep } from './wrapMixedContentStep';
@@ -39,7 +40,7 @@ const TRANSFORM_STEPS = {
39
40
  list: {
40
41
  atomic: undefined,
41
42
  container: [wrapStep],
42
- list: [stubStep],
43
+ list: [listToListStep],
43
44
  text: [flattenListStep, unwrapListStep]
44
45
  },
45
46
  text: {
@@ -1,6 +1,7 @@
1
1
  import React, { useCallback } from 'react';
2
- import { useIntl, injectIntl } from 'react-intl-next';
2
+ import { injectIntl, useIntl } from 'react-intl-next';
3
3
  import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
4
+ import { useSharedPluginStateWithSelector } from '@atlaskit/editor-common/hooks';
4
5
  import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages';
5
6
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
6
7
  import LinkIcon from '@atlaskit/icon/core/link';
@@ -15,15 +16,38 @@ const CopyLinkDropdownItemContent = ({
15
16
  api,
16
17
  config
17
18
  }) => {
18
- var _api$selection, _api$selection$shared, _api$selection$shared2;
19
19
  const {
20
20
  formatMessage
21
21
  } = useIntl();
22
22
  const {
23
23
  onDropdownOpenChanged
24
24
  } = useBlockMenu();
25
- const selection = api === null || api === void 0 ? void 0 : (_api$selection = api.selection) === null || _api$selection === void 0 ? void 0 : (_api$selection$shared = _api$selection.sharedState) === null || _api$selection$shared === void 0 ? void 0 : (_api$selection$shared2 = _api$selection$shared.currentState()) === null || _api$selection$shared2 === void 0 ? void 0 : _api$selection$shared2.selection;
25
+ const {
26
+ getLinkPath,
27
+ blockLinkHashPrefix
28
+ } = config || {};
29
+ const {
30
+ preservedSelection,
31
+ defaultSelection,
32
+ menuTriggerBy,
33
+ schema
34
+ } = useSharedPluginStateWithSelector(api, ['blockControls', 'selection', 'core'], ({
35
+ blockControlsState,
36
+ selectionState,
37
+ coreState
38
+ }) => {
39
+ return {
40
+ menuTriggerBy: blockControlsState === null || blockControlsState === void 0 ? void 0 : blockControlsState.menuTriggerBy,
41
+ preservedSelection: blockControlsState === null || blockControlsState === void 0 ? void 0 : blockControlsState.preservedSelection,
42
+ defaultSelection: selectionState === null || selectionState === void 0 ? void 0 : selectionState.selection,
43
+ schema: coreState === null || coreState === void 0 ? void 0 : coreState.schema
44
+ };
45
+ });
46
+ const selection = preservedSelection || defaultSelection;
26
47
  const handleClick = useCallback(() => {
48
+ if (!selection || !schema) {
49
+ return;
50
+ }
27
51
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
28
52
  tr
29
53
  }) => {
@@ -45,7 +69,12 @@ const CopyLinkDropdownItemContent = ({
45
69
  return tr;
46
70
  });
47
71
  onDropdownOpenChanged(false);
48
- copyLink(config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockLinkHashPrefix, api).then(success => {
72
+ copyLink({
73
+ getLinkPath,
74
+ blockLinkHashPrefix,
75
+ selection,
76
+ schema
77
+ }).then(success => {
49
78
  if (success) {
50
79
  api === null || api === void 0 ? void 0 : api.core.actions.execute(({
51
80
  tr
@@ -57,19 +86,10 @@ const CopyLinkDropdownItemContent = ({
57
86
  });
58
87
  }
59
88
  });
60
- }, [config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockLinkHashPrefix, api, onDropdownOpenChanged]);
61
- const checkIsNestedNode = useCallback(() => {
62
- var _api$selection2, _api$selection2$share, _api$selection2$share2, _api$blockControls2, _api$blockControls2$s, _api$blockControls2$s2;
63
- const selection = api === null || api === void 0 ? void 0 : (_api$selection2 = api.selection) === null || _api$selection2 === void 0 ? void 0 : (_api$selection2$share = _api$selection2.sharedState) === null || _api$selection2$share === void 0 ? void 0 : (_api$selection2$share2 = _api$selection2$share.currentState()) === null || _api$selection2$share2 === void 0 ? void 0 : _api$selection2$share2.selection;
64
- const menuTriggerBy = api === null || api === void 0 ? void 0 : (_api$blockControls2 = api.blockControls) === null || _api$blockControls2 === void 0 ? void 0 : (_api$blockControls2$s = _api$blockControls2.sharedState) === null || _api$blockControls2$s === void 0 ? void 0 : (_api$blockControls2$s2 = _api$blockControls2$s.currentState()) === null || _api$blockControls2$s2 === void 0 ? void 0 : _api$blockControls2$s2.menuTriggerBy;
65
- if (!selection || !menuTriggerBy) {
66
- return false;
67
- }
68
- return isNestedNode(selection, menuTriggerBy);
69
- }, [api]);
89
+ }, [api, blockLinkHashPrefix, getLinkPath, onDropdownOpenChanged, schema, selection]);
70
90
 
71
91
  // Hide copy link when `platform_editor_adf_with_localid` feature flag is off or when the node is nested or on empty line
72
- if (!fg('platform_editor_adf_with_localid') || checkIsNestedNode() || !!(selection !== null && selection !== void 0 && selection.empty)) {
92
+ if (!fg('platform_editor_adf_with_localid') || !!menuTriggerBy && isNestedNode(selection, menuTriggerBy) || selection !== null && selection !== void 0 && selection.empty) {
73
93
  return null;
74
94
  }
75
95
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
@@ -1,33 +1,34 @@
1
- import { DEFAULT_BLOCK_LINK_HASH_PREFIX } from '@atlaskit/editor-common/block-menu';
1
+ import { createBlockLinkHashValue, DEFAULT_BLOCK_LINK_HASH_PREFIX } from '@atlaskit/editor-common/block-menu';
2
2
  import { copyToClipboard } from '@atlaskit/editor-common/clipboard';
3
- import { NodeSelection, TextSelection } from '@atlaskit/editor-prosemirror/state';
4
- import { CellSelection } from '@atlaskit/editor-tables';
5
- export const copyLink = async (getLinkPath, blockLinkHashPrefix = DEFAULT_BLOCK_LINK_HASH_PREFIX, api) => {
3
+ import { logException } from '@atlaskit/editor-common/monitoring';
4
+ import { expandSelectionToBlockRange } from '../../editor-commands/transform-node-utils/utils';
5
+ export const copyLink = async ({
6
+ getLinkPath,
7
+ blockLinkHashPrefix = DEFAULT_BLOCK_LINK_HASH_PREFIX,
8
+ selection,
9
+ schema
10
+ }) => {
11
+ const blockRange = expandSelectionToBlockRange(selection, schema);
12
+ if (!blockRange) {
13
+ return false;
14
+ }
15
+
16
+ // get the link to the first node in the selection
17
+ const node = blockRange.$from.nodeAfter;
18
+ if (!node || !node.attrs || !node.attrs.localId) {
19
+ return false;
20
+ }
21
+ const path = (getLinkPath === null || getLinkPath === void 0 ? void 0 : getLinkPath()) || location.pathname;
6
22
  try {
7
- var _api$selection, _api$selection$shared;
8
- let node;
9
- const selection = api === null || api === void 0 ? void 0 : (_api$selection = api.selection) === null || _api$selection === void 0 ? void 0 : (_api$selection$shared = _api$selection.sharedState.currentState()) === null || _api$selection$shared === void 0 ? void 0 : _api$selection$shared.selection;
10
- if (selection instanceof NodeSelection && selection.node) {
11
- node = selection.node;
12
- } else if (selection instanceof TextSelection) {
13
- node = selection.$from.node();
14
- } else if (selection instanceof CellSelection) {
15
- node = selection.$anchorCell.node(-1);
16
- }
17
- if (!node || !node.attrs || !node.attrs.localId) {
18
- return false;
19
- }
20
- const path = (getLinkPath === null || getLinkPath === void 0 ? void 0 : getLinkPath()) || location.pathname;
21
- if (!path) {
22
- return false;
23
- }
24
23
  const url = new URL(location.origin + path);
25
- // append the localId as a hash fragment in the form #block-{localId}
26
- url.hash = `${blockLinkHashPrefix}${node.attrs.localId}`;
24
+ url.hash = createBlockLinkHashValue(node.attrs.localId, blockLinkHashPrefix);
27
25
  const href = url.toString();
28
26
  await copyToClipboard(href);
29
- return true;
30
- } catch (e) {
27
+ } catch (error) {
28
+ logException(error, {
29
+ location: 'editor-plugin-block-menu'
30
+ });
31
31
  return false;
32
32
  }
33
+ return true;
33
34
  };
@@ -14,12 +14,10 @@ var extractNestedLists = function extractNestedLists(node, listTypes, itemTypes,
14
14
  return grandChild.type === type;
15
15
  })) {
16
16
  nestedLists.push(grandChild);
17
+ } else if (grandChild.isText) {
18
+ contentWithoutNestedLists.push(paragraph.createAndFill({}, grandChild));
17
19
  } else {
18
- if (grandChild.isText) {
19
- contentWithoutNestedLists.push(paragraph.createAndFill({}, grandChild));
20
- } else {
21
- contentWithoutNestedLists.push(grandChild);
22
- }
20
+ contentWithoutNestedLists.push(grandChild);
23
21
  }
24
22
  });
25
23
  items.push(child.copy(Fragment.from(contentWithoutNestedLists)));
@@ -0,0 +1,226 @@
1
+ import { Fragment } from '@atlaskit/editor-prosemirror/model';
2
+ var isListType = function isListType(node, schema) {
3
+ var lists = [schema.nodes.taskList, schema.nodes.bulletList, schema.nodes.orderedList];
4
+ return lists.some(function (list) {
5
+ return list === node.type;
6
+ });
7
+ };
8
+
9
+ /**
10
+ * Converts FROM taskList structure TO bulletList/orderedList structure.
11
+ */
12
+ var convertFromTaskListStructure = function convertFromTaskListStructure(node, targetListType, targetItemType) {
13
+ var schema = node.type.schema;
14
+ var targetListNodeType = schema.nodes[targetListType];
15
+ var convertedItems = [];
16
+ node.content.forEach(function (child) {
17
+ if (isListType(child, schema)) {
18
+ // This is a nested list - it should become a child of the previous item
19
+ if (convertedItems.length > 0) {
20
+ var previousItem = convertedItems[convertedItems.length - 1];
21
+ // Convert the nested list and add it to the previous item's content
22
+ var convertedNestedList = _transformList(child, targetListType, targetItemType);
23
+ var newContent = previousItem.content.append(Fragment.from([convertedNestedList]));
24
+ var updatedItem = previousItem.type.create(previousItem.attrs, newContent);
25
+ convertedItems[convertedItems.length - 1] = updatedItem;
26
+ }
27
+ // If there's no previous item, skip this nested list (orphaned)
28
+ } else {
29
+ var convertedItem = transformListItem(child, targetItemType, targetListType);
30
+ if (convertedItem) {
31
+ convertedItems.push(convertedItem);
32
+ }
33
+ }
34
+ });
35
+ return targetListNodeType.create(node.attrs, Fragment.from(convertedItems));
36
+ };
37
+
38
+ /**
39
+ * Converts FROM bulletList/orderedList structure TO taskList structure.
40
+ */
41
+ var convertToTaskListStructure = function convertToTaskListStructure(node, targetListType, targetItemType) {
42
+ var schema = node.type.schema;
43
+ var targetListNodeType = schema.nodes[targetListType];
44
+ var transformedContent = [];
45
+ node.content.forEach(function (itemNode) {
46
+ var transformedItem = transformListItem(itemNode, targetItemType, targetListType, true);
47
+ if (transformedItem) {
48
+ transformedContent.push(transformedItem);
49
+ }
50
+ itemNode.content.forEach(function (child) {
51
+ if (isListType(child, schema)) {
52
+ var transformedNestedList = _transformList(child, targetListType, targetItemType);
53
+ transformedContent.push(transformedNestedList);
54
+ }
55
+ });
56
+ });
57
+ return targetListNodeType.create(node.attrs, Fragment.from(transformedContent));
58
+ };
59
+
60
+ /**
61
+ * Converts a single list item (listItem or taskItem) to the target item type.
62
+ * Handles content transformation based on the target type's requirements.
63
+ * @param itemNode - The list item node to convert
64
+ * @param targetItemType - The target item type (listItem or taskItem)
65
+ * @param targetListType - The target list type (bulletList, orderedList, or taskList)
66
+ * @param excludeNestedLists - When true, nested lists are excluded from the item's content
67
+ * (used when converting to taskList where nested lists become siblings)
68
+ */
69
+ var transformListItem = function transformListItem(itemNode, targetItemType, targetListType) {
70
+ var excludeNestedLists = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : false;
71
+ var schema = itemNode.type.schema;
72
+ var targetItemNodeType = schema.nodes[targetItemType];
73
+ var isTargetTaskItem = targetItemType === 'taskItem';
74
+ var isSourceTaskItem = itemNode.type.name === 'taskItem';
75
+ var paragraphType = schema.nodes.paragraph;
76
+ if (!targetItemNodeType) {
77
+ return null;
78
+ }
79
+ if (isTargetTaskItem) {
80
+ var inlineContent = [];
81
+ itemNode.content.forEach(function (child) {
82
+ if (child.type === paragraphType) {
83
+ child.content.forEach(function (inline) {
84
+ inlineContent.push(inline);
85
+ });
86
+ }
87
+ if (child.isText) {
88
+ inlineContent.push(child);
89
+ }
90
+ // TODO: EDITOR-3887 - Skip mediaSingle, codeBlock, and nested lists
91
+ // Nested lists will be extracted and placed as siblings in the taskList
92
+ });
93
+ return targetItemNodeType.create({}, Fragment.from(inlineContent));
94
+ } else {
95
+ var newContent = [];
96
+ if (isSourceTaskItem) {
97
+ newContent.push(paragraphType.create(null, itemNode.content));
98
+ } else {
99
+ itemNode.content.forEach(function (child) {
100
+ if (isListType(child, schema)) {
101
+ if (excludeNestedLists) {
102
+ // Skip nested lists - they will be handled separately as siblings
103
+ return;
104
+ }
105
+ newContent.push(_transformList(child, targetListType, targetItemType));
106
+ } else {
107
+ newContent.push(child);
108
+ }
109
+ });
110
+ }
111
+ if (newContent.length === 0) {
112
+ newContent.push(paragraphType.create());
113
+ }
114
+ return targetItemNodeType.create({}, Fragment.from(newContent));
115
+ }
116
+ };
117
+
118
+ /**
119
+ * Recursively converts nested lists to the target list type.
120
+ * This function handles the conversion of both the list container and its items,
121
+ * including any nested lists within those items.
122
+ *
123
+ * Important: taskList has a different nesting structure than bulletList/orderedList:
124
+ * - taskList: nested taskLists are SIBLINGS of taskItems in the parent taskList
125
+ * - bulletList/orderedList: nested lists are CHILDREN of listItems
126
+ */
127
+ var _transformList = function transformList(node, targetListType, targetItemType) {
128
+ var schema = node.type.schema;
129
+ var targetListNodeType = schema.nodes[targetListType];
130
+ var targetItemNodeType = schema.nodes[targetItemType];
131
+ var taskListType = schema.nodes.taskList;
132
+ if (!targetListNodeType || !targetItemNodeType) {
133
+ return node;
134
+ }
135
+ var isSourceTaskList = node.type === taskListType;
136
+ var isTargetTaskList = targetListType === 'taskList';
137
+ if (isSourceTaskList && !isTargetTaskList) {
138
+ return convertFromTaskListStructure(node, targetListType, targetItemType);
139
+ } else if (!isSourceTaskList && isTargetTaskList) {
140
+ return convertToTaskListStructure(node, targetListType, targetItemType);
141
+ } else {
142
+ var transformedItems = [];
143
+ node.content.forEach(function (childNode) {
144
+ var transformedItem = isListType(childNode, schema) ? _transformList(childNode, targetListType, targetItemType) : transformListItem(childNode, targetItemType, targetListType);
145
+ if (transformedItem) {
146
+ transformedItems.push(transformedItem);
147
+ }
148
+ });
149
+ return targetListNodeType.create(node.attrs, Fragment.from(transformedItems));
150
+ }
151
+ };
152
+
153
+ /**
154
+ * Transform step that converts between bulletList, orderedList, and taskList types.
155
+ * This step maintains the order and indentation of the list by recursively
156
+ * converting all nested lists while preserving the structure. It also handles
157
+ * conversion between listItem and taskItem types.
158
+ *
159
+ * When converting to taskList/taskItem, unsupported content (images, codeBlocks) is filtered out.
160
+ *
161
+ * @example
162
+ * Input (bulletList with nested bulletList):
163
+ * - bulletList
164
+ * - listItem "1"
165
+ * - bulletList
166
+ * - listItem "1.1"
167
+ * - bulletList
168
+ * - listItem "1.1.1"
169
+ * - listItem "1.2"
170
+ * - listItem "2"
171
+ *
172
+ * Output (orderedList with nested orderedList):
173
+ * 1. orderedList
174
+ * 1. listItem "1"
175
+ * 1. orderedList
176
+ * 1. listItem "1.1"
177
+ * 1. orderedList
178
+ * 1. listItem "1.1.1"
179
+ * 2. listItem "1.2"
180
+ * 2. listItem "2"
181
+ *
182
+ * @example
183
+ * Input (bulletList with nested taskList):
184
+ * - bulletList
185
+ * - listItem "Regular item"
186
+ * - taskList
187
+ * - taskItem "Task 1" (checked)
188
+ * - taskItem "Task 2" (unchecked)
189
+ *
190
+ * Output (orderedList with nested orderedList, taskItems converted to listItems):
191
+ * 1. orderedList
192
+ * 1. listItem "Regular item"
193
+ * 1. orderedList
194
+ * 1. listItem "Task 1"
195
+ * 2. listItem "Task 2"
196
+ *
197
+ * @example
198
+ * Input (bulletList to taskList, with paragraph extraction):
199
+ * - bulletList
200
+ * - listItem
201
+ * - paragraph "Text content"
202
+ * - listItem
203
+ * - paragraph "Text"
204
+ * - codeBlock "code"
205
+ * - mediaSingle (image)
206
+ *
207
+ * Output (taskList with text extracted from paragraphs, unsupported content filtered):
208
+ * - taskList
209
+ * - taskItem "Text content" (text extracted from paragraph)
210
+ * - taskItem "Text" (text extracted, codeBlock and image filtered out)
211
+ *
212
+ * @param nodes - The nodes to transform
213
+ * @param context - The transformation context containing schema and target node type
214
+ * @returns The transformed nodes
215
+ */
216
+ export var listToListStep = function listToListStep(nodes, context) {
217
+ var schema = context.schema,
218
+ targetNodeTypeName = context.targetNodeTypeName;
219
+ return nodes.map(function (node) {
220
+ if (isListType(node, schema)) {
221
+ var targetItemType = targetNodeTypeName === 'taskList' ? 'taskItem' : 'listItem';
222
+ return _transformList(node, targetNodeTypeName, targetItemType);
223
+ }
224
+ return node;
225
+ });
226
+ };
@@ -1,14 +1,15 @@
1
1
  import { getTargetNodeTypeNameInContext } from '../transform-node-utils/utils';
2
- import { flattenListStep } from './flattenListStep';
3
2
  import { flattenStep } from './flattenStep';
4
3
  import { convertBulletListToTextStep } from './steps/convertBulletListToTextStep';
5
4
  import { convertOrderedListToTextStep } from './steps/convertOrderedListToTextStep';
6
5
  import { convertTaskListToTextStep } from './steps/convertTaskListToTextStep';
6
+ import { flattenListStep } from './steps/flattenListStep';
7
+ import { listToListStep } from './steps/listToListStep';
7
8
  import { unwrapLayoutStep } from './steps/unwrapLayoutStep';
9
+ import { unwrapListStep } from './steps/unwrapListStep';
8
10
  import { stubStep } from './stubStep';
9
11
  import { NODE_CATEGORY_BY_TYPE, toNodeTypeValue } from './types';
10
12
  import { unwrapExpandStep } from './unwrapExpandStep';
11
- import { unwrapListStep } from './unwrapListStep';
12
13
  import { unwrapStep } from './unwrapStep';
13
14
  import { wrapIntoLayoutStep } from './wrapIntoLayoutStep';
14
15
  import { wrapMixedContentStep } from './wrapMixedContentStep';
@@ -39,7 +40,7 @@ var TRANSFORM_STEPS = {
39
40
  list: {
40
41
  atomic: undefined,
41
42
  container: [wrapStep],
42
- list: [stubStep],
43
+ list: [listToListStep],
43
44
  text: [flattenListStep, unwrapListStep]
44
45
  },
45
46
  text: {