@atlaskit/editor-plugin-block-menu 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,26 @@
1
1
  # @atlaskit/editor-plugin-block-menu
2
2
 
3
+ ## 1.0.11
4
+
5
+ ### Patch Changes
6
+
7
+ - [`fcef7ff2e1083`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/fcef7ff2e1083) -
8
+ Split unsupported content when converting to codeblock
9
+ - [`1754f5027f568`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/1754f5027f568) -
10
+ Fix missing copy link on table node
11
+ - Updated dependencies
12
+
13
+ ## 1.0.10
14
+
15
+ ### Patch Changes
16
+
17
+ - [`74c42a764926a`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/74c42a764926a) -
18
+ Hide copy link option when platform_editor_adf_with_localid FG is off or when selection is a
19
+ nested node
20
+ - [`614ef1a575e84`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/614ef1a575e84) -
21
+ [ux] ED-29183: Fixed p and headings with alignment not able to convert to panel, expand, block
22
+ quote
23
+
3
24
  ## 1.0.9
4
25
 
5
26
  ### Patch Changes
@@ -20,6 +20,19 @@ var convertInvalidNodeToValidNodeType = function convertInvalidNodeToValidNodeTy
20
20
  });
21
21
  return _model.Fragment.from(validTransformedContent);
22
22
  };
23
+ var filterMarksForTargetNodeType = function filterMarksForTargetNodeType(content, targetNodeType) {
24
+ var withValidMarks = [];
25
+ content.forEach(function (node) {
26
+ if (node.marks.length > 0) {
27
+ var allowedMarks = targetNodeType.allowedMarks(node.marks);
28
+ var updatedNode = node.mark(allowedMarks);
29
+ withValidMarks.push(updatedNode);
30
+ } else {
31
+ withValidMarks.push(node);
32
+ }
33
+ });
34
+ return _model.Fragment.from(withValidMarks);
35
+ };
23
36
 
24
37
  /**
25
38
  * Transform selection to container type
@@ -39,6 +52,13 @@ var transformToContainer = exports.transformToContainer = function transformToCo
39
52
  if (targetNodeType === schema.nodes.blockquote) {
40
53
  transformedContent = convertInvalidNodeToValidNodeType(transformedContent, schema.nodes.heading, schema.nodes.paragraph, true);
41
54
  }
55
+
56
+ // Preserve marks that are allowed in the target node type
57
+ // e.g. blocks (heading/ paragraph) with alignment need to remove alignment
58
+ // as panel/ blockQuote/ expands does not support alignment
59
+ if (sourceNode.type === schema.nodes.paragraph || sourceNode.type === schema.nodes.heading) {
60
+ transformedContent = filterMarksForTargetNodeType(transformedContent, targetNodeType);
61
+ }
42
62
  var newNode = targetNodeType.createAndFill(targetAttrs, transformedContent);
43
63
  if (!newNode) {
44
64
  return null;
@@ -62,6 +82,16 @@ var transformContainerNode = exports.transformContainerNode = function transform
62
82
 
63
83
  // Transform container to block type - unwrap and convert content
64
84
  if ((0, _utils.isBlockNodeType)(targetNodeType)) {
85
+ // special case container to codeblock
86
+ if (targetNodeType.name === 'codeBlock') {
87
+ return transformBetweenContainerTypes({
88
+ tr: tr,
89
+ sourceNode: sourceNode,
90
+ sourcePos: sourcePos,
91
+ targetNodeType: targetNodeType,
92
+ targetAttrs: targetAttrs
93
+ });
94
+ }
65
95
  return unwrapAndConvertToBlockType({
66
96
  tr: tr,
67
97
  sourceNode: sourceNode,
@@ -254,18 +284,29 @@ var transformBetweenContainerTypes = exports.transformBetweenContainerTypes = fu
254
284
  targetNodeType = context.targetNodeType,
255
285
  targetAttrs = context.targetAttrs;
256
286
 
287
+ // Special handling for codeBlock target
288
+ if (targetNodeType.name === 'codeBlock') {
289
+ var _contentSplits = splitContentForCodeBlock(sourceNode, targetNodeType, targetAttrs, tr.doc.type.schema);
290
+ return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, _contentSplits);
291
+ }
292
+
257
293
  // Get content validation for target container type
258
294
  var isContentSupported = (0, _utils.getContentSupportChecker)(targetNodeType);
259
295
 
260
296
  // Process content and collect splits
261
297
  var contentSplits = splitContentAroundUnsupportedBlocks(sourceNode, isContentSupported, targetNodeType, targetAttrs, tr.doc.type.schema);
298
+ return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, contentSplits);
299
+ };
262
300
 
263
- // Replace the original node with the first split
301
+ /**
302
+ * Apply content splits to transaction - shared utility for replacing and inserting splits
303
+ */
304
+ var applySplitsToTransaction = function applySplitsToTransaction(tr, sourcePos, sourceNodeSize, contentSplits) {
264
305
  var insertPos = sourcePos;
265
306
  contentSplits.forEach(function (splitNode, index) {
266
307
  if (index === 0) {
267
308
  // Replace the original node with the first split
268
- tr.replaceWith(sourcePos, sourcePos + sourceNode.nodeSize, splitNode);
309
+ tr.replaceWith(sourcePos, sourcePos + sourceNodeSize, splitNode);
269
310
  insertPos = sourcePos + splitNode.nodeSize;
270
311
  } else {
271
312
  // Insert additional splits after
@@ -276,18 +317,75 @@ var transformBetweenContainerTypes = exports.transformBetweenContainerTypes = fu
276
317
  return tr;
277
318
  };
278
319
 
320
+ /**
321
+ * Split content for codeBlock transformation, creating codeBlocks for text content
322
+ * and preserving unsupported blocks (like tables) separately
323
+ */
324
+ var splitContentForCodeBlock = function splitContentForCodeBlock(sourceNode, targetNodeType, targetAttrs, schema) {
325
+ var _sourceNode$attrs3;
326
+ var splits = [];
327
+ var children = sourceNode.content.content;
328
+ var currentTextContent = [];
329
+
330
+ // Handle expand title - add as first text if source is expand with title
331
+ if (sourceNode.type.name === 'expand' && (_sourceNode$attrs3 = sourceNode.attrs) !== null && _sourceNode$attrs3 !== void 0 && _sourceNode$attrs3.title) {
332
+ currentTextContent.push(sourceNode.attrs.title);
333
+ }
334
+ var flushCurrentCodeBlock = function flushCurrentCodeBlock() {
335
+ if (currentTextContent.length > 0) {
336
+ var codeText = currentTextContent.join('\n');
337
+ var codeBlockNode = targetNodeType.create(targetAttrs, schema.text(codeText));
338
+ splits.push(codeBlockNode);
339
+ currentTextContent = [];
340
+ }
341
+ };
342
+ var isCodeBlockCompatible = function isCodeBlockCompatible(node) {
343
+ // Only text blocks (paragraph, heading) can be converted to codeBlock text
344
+ return node.isTextblock || node.type.name === 'codeBlock';
345
+ };
346
+ children.forEach(function (childNode) {
347
+ if (isCodeBlockCompatible(childNode)) {
348
+ // Extract text content from compatible nodes
349
+ if (childNode.type.name === 'codeBlock') {
350
+ // If it's already a codeBlock, extract its text
351
+ currentTextContent.push(childNode.textContent);
352
+ } else if (childNode.isTextblock) {
353
+ // Extract text from text blocks (paragraphs, headings, etc.)
354
+ var text = childNode.textContent;
355
+ if (text.trim()) {
356
+ currentTextContent.push(text);
357
+ }
358
+ }
359
+ } else if ((0, _utils.isBlockNodeForExtraction)(childNode)) {
360
+ // Unsupported block node (table, etc.) - flush current codeBlock, add block, continue
361
+ flushCurrentCodeBlock();
362
+ splits.push(childNode);
363
+ } else {
364
+ // Other unsupported content - try to extract text if possible
365
+ var _text = childNode.textContent;
366
+ if (_text && _text.trim()) {
367
+ currentTextContent.push(_text);
368
+ }
369
+ }
370
+ });
371
+
372
+ // Flush any remaining text content as a codeBlock
373
+ flushCurrentCodeBlock();
374
+ return splits;
375
+ };
376
+
279
377
  /**
280
378
  * Split content around unsupported block nodes, creating separate containers
281
379
  * for content before and after each unsupported block
282
380
  */
283
381
  var splitContentAroundUnsupportedBlocks = function splitContentAroundUnsupportedBlocks(sourceNode, isContentSupported, targetNodeType, targetAttrs, schema) {
284
- var _sourceNode$attrs3;
382
+ var _sourceNode$attrs4;
285
383
  var splits = [];
286
384
  var children = sourceNode.content.content;
287
385
  var currentContainerContent = [];
288
386
 
289
387
  // Handle expand title - add as first paragraph if source is expand with title
290
- if (sourceNode.type.name === 'expand' && (_sourceNode$attrs3 = sourceNode.attrs) !== null && _sourceNode$attrs3 !== void 0 && _sourceNode$attrs3.title) {
388
+ if (sourceNode.type.name === 'expand' && (_sourceNode$attrs4 = sourceNode.attrs) !== null && _sourceNode$attrs4 !== void 0 && _sourceNode$attrs4.title) {
291
389
  var titleParagraph = schema.nodes.paragraph.create({}, schema.text(sourceNode.attrs.title));
292
390
  currentContainerContent.push(titleParagraph);
293
391
  }
@@ -11,7 +11,9 @@ var _reactIntlNext = require("react-intl-next");
11
11
  var _messages = require("@atlaskit/editor-common/messages");
12
12
  var _editorToolbar = require("@atlaskit/editor-toolbar");
13
13
  var _link = _interopRequireDefault(require("@atlaskit/icon/core/link"));
14
+ var _platformFeatureFlags = require("@atlaskit/platform-feature-flags");
14
15
  var _copyLink = require("./utils/copyLink");
16
+ var _isNestedNode = require("./utils/isNestedNode");
15
17
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function _interopRequireWildcard(e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != _typeof(e) && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (var _t in e) "default" !== _t && {}.hasOwnProperty.call(e, _t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, _t)) && (i.get || i.set) ? o(f, _t, i) : f[_t] = e[_t]); return f; })(e, t); }
16
18
  var CopyLinkDropdownItemContent = function CopyLinkDropdownItemContent(_ref) {
17
19
  var api = _ref.api,
@@ -32,6 +34,16 @@ var CopyLinkDropdownItemContent = function CopyLinkDropdownItemContent(_ref) {
32
34
  api === null || api === void 0 || api.core.actions.focus();
33
35
  return (0, _copyLink.copyLink)(config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockQueryParam, api);
34
36
  }, [config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockQueryParam, api]);
37
+ var checkIsNestedNode = (0, _react.useCallback)(function () {
38
+ var _api$selection;
39
+ var selection = api === null || api === void 0 || (_api$selection = api.selection) === null || _api$selection === void 0 || (_api$selection = _api$selection.sharedState) === null || _api$selection === void 0 || (_api$selection = _api$selection.currentState()) === null || _api$selection === void 0 ? void 0 : _api$selection.selection;
40
+ return (0, _isNestedNode.isNestedNode)(selection);
41
+ }, [api]);
42
+
43
+ // Hide copy link when `platform_editor_adf_with_localid` feature flag is off or when the node is nested
44
+ if (!(0, _platformFeatureFlags.fg)('platform_editor_adf_with_localid') || checkIsNestedNode()) {
45
+ return null;
46
+ }
35
47
  return /*#__PURE__*/_react.default.createElement(_editorToolbar.ToolbarDropdownItem, {
36
48
  onClick: handleClick,
37
49
  elemBefore: /*#__PURE__*/_react.default.createElement(_link.default, {
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.isNestedNode = void 0;
7
+ var _state = require("@atlaskit/editor-prosemirror/state");
8
+ var _editorTables = require("@atlaskit/editor-tables");
9
+ /**
10
+ * Determines if a node is nested (not at top-level) based on its depth and context.
11
+ *
12
+ * Simple rules:
13
+ * - Depth 0-1: Always top-level (not nested)
14
+ * - Depth 2: Top-level for blockquotes and task lists
15
+ * - Depth 3: Top-level for list items only
16
+ * - Depth 4+: Always nested
17
+ *
18
+ * @param selection - The current ProseMirror selection
19
+ * @returns true if nested, false if top-level
20
+ */
21
+ var isNestedNode = exports.isNestedNode = function isNestedNode(selection) {
22
+ if (!selection) {
23
+ return false;
24
+ }
25
+ var $from = selection.$from;
26
+ var depth = $from.depth;
27
+ if ($from.depth > 0 && selection instanceof _state.NodeSelection) {
28
+ return true;
29
+ }
30
+
31
+ // Depth 0-1: Always top-level
32
+ if (depth <= 1) {
33
+ return false;
34
+ }
35
+
36
+ // Depth 4+: Always nested
37
+ if (depth > 3) {
38
+ return true;
39
+ }
40
+
41
+ // Special case for table selection
42
+ if (selection instanceof _editorTables.CellSelection) {
43
+ return depth > 3;
44
+ }
45
+
46
+ // Check parent node type for depth 2-3
47
+ var parentNode = $from.node(depth - 1);
48
+ if (!parentNode) {
49
+ return true;
50
+ }
51
+ var parentType = parentNode.type.name;
52
+
53
+ // Special cases where content is still top-level
54
+ if (parentType === 'listItem' && depth === 3 || parentType === 'blockquote' && depth === 2 || parentType === 'taskList' && depth === 2) {
55
+ return false;
56
+ }
57
+
58
+ // Everything else at depth 2-3 is nested
59
+ return true;
60
+ };
@@ -12,6 +12,19 @@ const convertInvalidNodeToValidNodeType = (sourceContent, sourceNodeType, validN
12
12
  });
13
13
  return Fragment.from(validTransformedContent);
14
14
  };
15
+ const filterMarksForTargetNodeType = (content, targetNodeType) => {
16
+ const withValidMarks = [];
17
+ content.forEach(node => {
18
+ if (node.marks.length > 0) {
19
+ const allowedMarks = targetNodeType.allowedMarks(node.marks);
20
+ const updatedNode = node.mark(allowedMarks);
21
+ withValidMarks.push(updatedNode);
22
+ } else {
23
+ withValidMarks.push(node);
24
+ }
25
+ });
26
+ return Fragment.from(withValidMarks);
27
+ };
15
28
 
16
29
  /**
17
30
  * Transform selection to container type
@@ -32,6 +45,13 @@ export const transformToContainer = ({
32
45
  if (targetNodeType === schema.nodes.blockquote) {
33
46
  transformedContent = convertInvalidNodeToValidNodeType(transformedContent, schema.nodes.heading, schema.nodes.paragraph, true);
34
47
  }
48
+
49
+ // Preserve marks that are allowed in the target node type
50
+ // e.g. blocks (heading/ paragraph) with alignment need to remove alignment
51
+ // as panel/ blockQuote/ expands does not support alignment
52
+ if (sourceNode.type === schema.nodes.paragraph || sourceNode.type === schema.nodes.heading) {
53
+ transformedContent = filterMarksForTargetNodeType(transformedContent, targetNodeType);
54
+ }
35
55
  const newNode = targetNodeType.createAndFill(targetAttrs, transformedContent);
36
56
  if (!newNode) {
37
57
  return null;
@@ -56,6 +76,16 @@ export const transformContainerNode = ({
56
76
 
57
77
  // Transform container to block type - unwrap and convert content
58
78
  if (isBlockNodeType(targetNodeType)) {
79
+ // special case container to codeblock
80
+ if (targetNodeType.name === 'codeBlock') {
81
+ return transformBetweenContainerTypes({
82
+ tr,
83
+ sourceNode,
84
+ sourcePos,
85
+ targetNodeType,
86
+ targetAttrs
87
+ });
88
+ }
59
89
  return unwrapAndConvertToBlockType({
60
90
  tr,
61
91
  sourceNode,
@@ -253,18 +283,29 @@ export const transformBetweenContainerTypes = context => {
253
283
  targetAttrs
254
284
  } = context;
255
285
 
286
+ // Special handling for codeBlock target
287
+ if (targetNodeType.name === 'codeBlock') {
288
+ const contentSplits = splitContentForCodeBlock(sourceNode, targetNodeType, targetAttrs, tr.doc.type.schema);
289
+ return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, contentSplits);
290
+ }
291
+
256
292
  // Get content validation for target container type
257
293
  const isContentSupported = getContentSupportChecker(targetNodeType);
258
294
 
259
295
  // Process content and collect splits
260
296
  const contentSplits = splitContentAroundUnsupportedBlocks(sourceNode, isContentSupported, targetNodeType, targetAttrs, tr.doc.type.schema);
297
+ return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, contentSplits);
298
+ };
261
299
 
262
- // Replace the original node with the first split
300
+ /**
301
+ * Apply content splits to transaction - shared utility for replacing and inserting splits
302
+ */
303
+ const applySplitsToTransaction = (tr, sourcePos, sourceNodeSize, contentSplits) => {
263
304
  let insertPos = sourcePos;
264
305
  contentSplits.forEach((splitNode, index) => {
265
306
  if (index === 0) {
266
307
  // Replace the original node with the first split
267
- tr.replaceWith(sourcePos, sourcePos + sourceNode.nodeSize, splitNode);
308
+ tr.replaceWith(sourcePos, sourcePos + sourceNodeSize, splitNode);
268
309
  insertPos = sourcePos + splitNode.nodeSize;
269
310
  } else {
270
311
  // Insert additional splits after
@@ -275,18 +316,75 @@ export const transformBetweenContainerTypes = context => {
275
316
  return tr;
276
317
  };
277
318
 
319
+ /**
320
+ * Split content for codeBlock transformation, creating codeBlocks for text content
321
+ * and preserving unsupported blocks (like tables) separately
322
+ */
323
+ const splitContentForCodeBlock = (sourceNode, targetNodeType, targetAttrs, schema) => {
324
+ var _sourceNode$attrs3;
325
+ const splits = [];
326
+ const children = sourceNode.content.content;
327
+ let currentTextContent = [];
328
+
329
+ // Handle expand title - add as first text if source is expand with title
330
+ if (sourceNode.type.name === 'expand' && (_sourceNode$attrs3 = sourceNode.attrs) !== null && _sourceNode$attrs3 !== void 0 && _sourceNode$attrs3.title) {
331
+ currentTextContent.push(sourceNode.attrs.title);
332
+ }
333
+ const flushCurrentCodeBlock = () => {
334
+ if (currentTextContent.length > 0) {
335
+ const codeText = currentTextContent.join('\n');
336
+ const codeBlockNode = targetNodeType.create(targetAttrs, schema.text(codeText));
337
+ splits.push(codeBlockNode);
338
+ currentTextContent = [];
339
+ }
340
+ };
341
+ const isCodeBlockCompatible = node => {
342
+ // Only text blocks (paragraph, heading) can be converted to codeBlock text
343
+ return node.isTextblock || node.type.name === 'codeBlock';
344
+ };
345
+ children.forEach(childNode => {
346
+ if (isCodeBlockCompatible(childNode)) {
347
+ // Extract text content from compatible nodes
348
+ if (childNode.type.name === 'codeBlock') {
349
+ // If it's already a codeBlock, extract its text
350
+ currentTextContent.push(childNode.textContent);
351
+ } else if (childNode.isTextblock) {
352
+ // Extract text from text blocks (paragraphs, headings, etc.)
353
+ const text = childNode.textContent;
354
+ if (text.trim()) {
355
+ currentTextContent.push(text);
356
+ }
357
+ }
358
+ } else if (isBlockNodeForExtraction(childNode)) {
359
+ // Unsupported block node (table, etc.) - flush current codeBlock, add block, continue
360
+ flushCurrentCodeBlock();
361
+ splits.push(childNode);
362
+ } else {
363
+ // Other unsupported content - try to extract text if possible
364
+ const text = childNode.textContent;
365
+ if (text && text.trim()) {
366
+ currentTextContent.push(text);
367
+ }
368
+ }
369
+ });
370
+
371
+ // Flush any remaining text content as a codeBlock
372
+ flushCurrentCodeBlock();
373
+ return splits;
374
+ };
375
+
278
376
  /**
279
377
  * Split content around unsupported block nodes, creating separate containers
280
378
  * for content before and after each unsupported block
281
379
  */
282
380
  const splitContentAroundUnsupportedBlocks = (sourceNode, isContentSupported, targetNodeType, targetAttrs, schema) => {
283
- var _sourceNode$attrs3;
381
+ var _sourceNode$attrs4;
284
382
  const splits = [];
285
383
  const children = sourceNode.content.content;
286
384
  let currentContainerContent = [];
287
385
 
288
386
  // Handle expand title - add as first paragraph if source is expand with title
289
- if (sourceNode.type.name === 'expand' && (_sourceNode$attrs3 = sourceNode.attrs) !== null && _sourceNode$attrs3 !== void 0 && _sourceNode$attrs3.title) {
387
+ if (sourceNode.type.name === 'expand' && (_sourceNode$attrs4 = sourceNode.attrs) !== null && _sourceNode$attrs4 !== void 0 && _sourceNode$attrs4.title) {
290
388
  const titleParagraph = schema.nodes.paragraph.create({}, schema.text(sourceNode.attrs.title));
291
389
  currentContainerContent.push(titleParagraph);
292
390
  }
@@ -3,7 +3,9 @@ import { useIntl, injectIntl } from 'react-intl-next';
3
3
  import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages';
4
4
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
5
  import LinkIcon from '@atlaskit/icon/core/link';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
6
7
  import { copyLink } from './utils/copyLink';
8
+ import { isNestedNode } from './utils/isNestedNode';
7
9
  const CopyLinkDropdownItemContent = ({
8
10
  api,
9
11
  config
@@ -26,6 +28,16 @@ const CopyLinkDropdownItemContent = ({
26
28
  api === null || api === void 0 ? void 0 : api.core.actions.focus();
27
29
  return copyLink(config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockQueryParam, api);
28
30
  }, [config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockQueryParam, api]);
31
+ const checkIsNestedNode = useCallback(() => {
32
+ var _api$selection, _api$selection$shared, _api$selection$shared2;
33
+ 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;
34
+ return isNestedNode(selection);
35
+ }, [api]);
36
+
37
+ // Hide copy link when `platform_editor_adf_with_localid` feature flag is off or when the node is nested
38
+ if (!fg('platform_editor_adf_with_localid') || checkIsNestedNode()) {
39
+ return null;
40
+ }
29
41
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
30
42
  onClick: handleClick,
31
43
  elemBefore: /*#__PURE__*/React.createElement(LinkIcon, {
@@ -0,0 +1,57 @@
1
+ import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
2
+ import { CellSelection } from '@atlaskit/editor-tables';
3
+
4
+ /**
5
+ * Determines if a node is nested (not at top-level) based on its depth and context.
6
+ *
7
+ * Simple rules:
8
+ * - Depth 0-1: Always top-level (not nested)
9
+ * - Depth 2: Top-level for blockquotes and task lists
10
+ * - Depth 3: Top-level for list items only
11
+ * - Depth 4+: Always nested
12
+ *
13
+ * @param selection - The current ProseMirror selection
14
+ * @returns true if nested, false if top-level
15
+ */
16
+ export const isNestedNode = selection => {
17
+ if (!selection) {
18
+ return false;
19
+ }
20
+ const {
21
+ $from
22
+ } = selection;
23
+ const depth = $from.depth;
24
+ if ($from.depth > 0 && selection instanceof NodeSelection) {
25
+ return true;
26
+ }
27
+
28
+ // Depth 0-1: Always top-level
29
+ if (depth <= 1) {
30
+ return false;
31
+ }
32
+
33
+ // Depth 4+: Always nested
34
+ if (depth > 3) {
35
+ return true;
36
+ }
37
+
38
+ // Special case for table selection
39
+ if (selection instanceof CellSelection) {
40
+ return depth > 3;
41
+ }
42
+
43
+ // Check parent node type for depth 2-3
44
+ const parentNode = $from.node(depth - 1);
45
+ if (!parentNode) {
46
+ return true;
47
+ }
48
+ const parentType = parentNode.type.name;
49
+
50
+ // Special cases where content is still top-level
51
+ if (parentType === 'listItem' && depth === 3 || parentType === 'blockquote' && depth === 2 || parentType === 'taskList' && depth === 2) {
52
+ return false;
53
+ }
54
+
55
+ // Everything else at depth 2-3 is nested
56
+ return true;
57
+ };
@@ -13,6 +13,19 @@ var convertInvalidNodeToValidNodeType = function convertInvalidNodeToValidNodeTy
13
13
  });
14
14
  return Fragment.from(validTransformedContent);
15
15
  };
16
+ var filterMarksForTargetNodeType = function filterMarksForTargetNodeType(content, targetNodeType) {
17
+ var withValidMarks = [];
18
+ content.forEach(function (node) {
19
+ if (node.marks.length > 0) {
20
+ var allowedMarks = targetNodeType.allowedMarks(node.marks);
21
+ var updatedNode = node.mark(allowedMarks);
22
+ withValidMarks.push(updatedNode);
23
+ } else {
24
+ withValidMarks.push(node);
25
+ }
26
+ });
27
+ return Fragment.from(withValidMarks);
28
+ };
16
29
 
17
30
  /**
18
31
  * Transform selection to container type
@@ -32,6 +45,13 @@ export var transformToContainer = function transformToContainer(_ref) {
32
45
  if (targetNodeType === schema.nodes.blockquote) {
33
46
  transformedContent = convertInvalidNodeToValidNodeType(transformedContent, schema.nodes.heading, schema.nodes.paragraph, true);
34
47
  }
48
+
49
+ // Preserve marks that are allowed in the target node type
50
+ // e.g. blocks (heading/ paragraph) with alignment need to remove alignment
51
+ // as panel/ blockQuote/ expands does not support alignment
52
+ if (sourceNode.type === schema.nodes.paragraph || sourceNode.type === schema.nodes.heading) {
53
+ transformedContent = filterMarksForTargetNodeType(transformedContent, targetNodeType);
54
+ }
35
55
  var newNode = targetNodeType.createAndFill(targetAttrs, transformedContent);
36
56
  if (!newNode) {
37
57
  return null;
@@ -55,6 +75,16 @@ export var transformContainerNode = function transformContainerNode(_ref2) {
55
75
 
56
76
  // Transform container to block type - unwrap and convert content
57
77
  if (isBlockNodeType(targetNodeType)) {
78
+ // special case container to codeblock
79
+ if (targetNodeType.name === 'codeBlock') {
80
+ return transformBetweenContainerTypes({
81
+ tr: tr,
82
+ sourceNode: sourceNode,
83
+ sourcePos: sourcePos,
84
+ targetNodeType: targetNodeType,
85
+ targetAttrs: targetAttrs
86
+ });
87
+ }
58
88
  return unwrapAndConvertToBlockType({
59
89
  tr: tr,
60
90
  sourceNode: sourceNode,
@@ -247,18 +277,29 @@ export var transformBetweenContainerTypes = function transformBetweenContainerTy
247
277
  targetNodeType = context.targetNodeType,
248
278
  targetAttrs = context.targetAttrs;
249
279
 
280
+ // Special handling for codeBlock target
281
+ if (targetNodeType.name === 'codeBlock') {
282
+ var _contentSplits = splitContentForCodeBlock(sourceNode, targetNodeType, targetAttrs, tr.doc.type.schema);
283
+ return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, _contentSplits);
284
+ }
285
+
250
286
  // Get content validation for target container type
251
287
  var isContentSupported = getContentSupportChecker(targetNodeType);
252
288
 
253
289
  // Process content and collect splits
254
290
  var contentSplits = splitContentAroundUnsupportedBlocks(sourceNode, isContentSupported, targetNodeType, targetAttrs, tr.doc.type.schema);
291
+ return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, contentSplits);
292
+ };
255
293
 
256
- // Replace the original node with the first split
294
+ /**
295
+ * Apply content splits to transaction - shared utility for replacing and inserting splits
296
+ */
297
+ var applySplitsToTransaction = function applySplitsToTransaction(tr, sourcePos, sourceNodeSize, contentSplits) {
257
298
  var insertPos = sourcePos;
258
299
  contentSplits.forEach(function (splitNode, index) {
259
300
  if (index === 0) {
260
301
  // Replace the original node with the first split
261
- tr.replaceWith(sourcePos, sourcePos + sourceNode.nodeSize, splitNode);
302
+ tr.replaceWith(sourcePos, sourcePos + sourceNodeSize, splitNode);
262
303
  insertPos = sourcePos + splitNode.nodeSize;
263
304
  } else {
264
305
  // Insert additional splits after
@@ -269,18 +310,75 @@ export var transformBetweenContainerTypes = function transformBetweenContainerTy
269
310
  return tr;
270
311
  };
271
312
 
313
+ /**
314
+ * Split content for codeBlock transformation, creating codeBlocks for text content
315
+ * and preserving unsupported blocks (like tables) separately
316
+ */
317
+ var splitContentForCodeBlock = function splitContentForCodeBlock(sourceNode, targetNodeType, targetAttrs, schema) {
318
+ var _sourceNode$attrs3;
319
+ var splits = [];
320
+ var children = sourceNode.content.content;
321
+ var currentTextContent = [];
322
+
323
+ // Handle expand title - add as first text if source is expand with title
324
+ if (sourceNode.type.name === 'expand' && (_sourceNode$attrs3 = sourceNode.attrs) !== null && _sourceNode$attrs3 !== void 0 && _sourceNode$attrs3.title) {
325
+ currentTextContent.push(sourceNode.attrs.title);
326
+ }
327
+ var flushCurrentCodeBlock = function flushCurrentCodeBlock() {
328
+ if (currentTextContent.length > 0) {
329
+ var codeText = currentTextContent.join('\n');
330
+ var codeBlockNode = targetNodeType.create(targetAttrs, schema.text(codeText));
331
+ splits.push(codeBlockNode);
332
+ currentTextContent = [];
333
+ }
334
+ };
335
+ var isCodeBlockCompatible = function isCodeBlockCompatible(node) {
336
+ // Only text blocks (paragraph, heading) can be converted to codeBlock text
337
+ return node.isTextblock || node.type.name === 'codeBlock';
338
+ };
339
+ children.forEach(function (childNode) {
340
+ if (isCodeBlockCompatible(childNode)) {
341
+ // Extract text content from compatible nodes
342
+ if (childNode.type.name === 'codeBlock') {
343
+ // If it's already a codeBlock, extract its text
344
+ currentTextContent.push(childNode.textContent);
345
+ } else if (childNode.isTextblock) {
346
+ // Extract text from text blocks (paragraphs, headings, etc.)
347
+ var text = childNode.textContent;
348
+ if (text.trim()) {
349
+ currentTextContent.push(text);
350
+ }
351
+ }
352
+ } else if (isBlockNodeForExtraction(childNode)) {
353
+ // Unsupported block node (table, etc.) - flush current codeBlock, add block, continue
354
+ flushCurrentCodeBlock();
355
+ splits.push(childNode);
356
+ } else {
357
+ // Other unsupported content - try to extract text if possible
358
+ var _text = childNode.textContent;
359
+ if (_text && _text.trim()) {
360
+ currentTextContent.push(_text);
361
+ }
362
+ }
363
+ });
364
+
365
+ // Flush any remaining text content as a codeBlock
366
+ flushCurrentCodeBlock();
367
+ return splits;
368
+ };
369
+
272
370
  /**
273
371
  * Split content around unsupported block nodes, creating separate containers
274
372
  * for content before and after each unsupported block
275
373
  */
276
374
  var splitContentAroundUnsupportedBlocks = function splitContentAroundUnsupportedBlocks(sourceNode, isContentSupported, targetNodeType, targetAttrs, schema) {
277
- var _sourceNode$attrs3;
375
+ var _sourceNode$attrs4;
278
376
  var splits = [];
279
377
  var children = sourceNode.content.content;
280
378
  var currentContainerContent = [];
281
379
 
282
380
  // Handle expand title - add as first paragraph if source is expand with title
283
- if (sourceNode.type.name === 'expand' && (_sourceNode$attrs3 = sourceNode.attrs) !== null && _sourceNode$attrs3 !== void 0 && _sourceNode$attrs3.title) {
381
+ if (sourceNode.type.name === 'expand' && (_sourceNode$attrs4 = sourceNode.attrs) !== null && _sourceNode$attrs4 !== void 0 && _sourceNode$attrs4.title) {
284
382
  var titleParagraph = schema.nodes.paragraph.create({}, schema.text(sourceNode.attrs.title));
285
383
  currentContainerContent.push(titleParagraph);
286
384
  }
@@ -3,7 +3,9 @@ import { useIntl, injectIntl } from 'react-intl-next';
3
3
  import { blockMenuMessages as messages } from '@atlaskit/editor-common/messages';
4
4
  import { ToolbarDropdownItem } from '@atlaskit/editor-toolbar';
5
5
  import LinkIcon from '@atlaskit/icon/core/link';
6
+ import { fg } from '@atlaskit/platform-feature-flags';
6
7
  import { copyLink } from './utils/copyLink';
8
+ import { isNestedNode } from './utils/isNestedNode';
7
9
  var CopyLinkDropdownItemContent = function CopyLinkDropdownItemContent(_ref) {
8
10
  var api = _ref.api,
9
11
  config = _ref.config;
@@ -23,6 +25,16 @@ var CopyLinkDropdownItemContent = function CopyLinkDropdownItemContent(_ref) {
23
25
  api === null || api === void 0 || api.core.actions.focus();
24
26
  return copyLink(config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockQueryParam, api);
25
27
  }, [config === null || config === void 0 ? void 0 : config.getLinkPath, config === null || config === void 0 ? void 0 : config.blockQueryParam, api]);
28
+ var checkIsNestedNode = useCallback(function () {
29
+ var _api$selection;
30
+ var selection = api === null || api === void 0 || (_api$selection = api.selection) === null || _api$selection === void 0 || (_api$selection = _api$selection.sharedState) === null || _api$selection === void 0 || (_api$selection = _api$selection.currentState()) === null || _api$selection === void 0 ? void 0 : _api$selection.selection;
31
+ return isNestedNode(selection);
32
+ }, [api]);
33
+
34
+ // Hide copy link when `platform_editor_adf_with_localid` feature flag is off or when the node is nested
35
+ if (!fg('platform_editor_adf_with_localid') || checkIsNestedNode()) {
36
+ return null;
37
+ }
26
38
  return /*#__PURE__*/React.createElement(ToolbarDropdownItem, {
27
39
  onClick: handleClick,
28
40
  elemBefore: /*#__PURE__*/React.createElement(LinkIcon, {
@@ -0,0 +1,55 @@
1
+ import { NodeSelection } from '@atlaskit/editor-prosemirror/state';
2
+ import { CellSelection } from '@atlaskit/editor-tables';
3
+
4
+ /**
5
+ * Determines if a node is nested (not at top-level) based on its depth and context.
6
+ *
7
+ * Simple rules:
8
+ * - Depth 0-1: Always top-level (not nested)
9
+ * - Depth 2: Top-level for blockquotes and task lists
10
+ * - Depth 3: Top-level for list items only
11
+ * - Depth 4+: Always nested
12
+ *
13
+ * @param selection - The current ProseMirror selection
14
+ * @returns true if nested, false if top-level
15
+ */
16
+ export var isNestedNode = function isNestedNode(selection) {
17
+ if (!selection) {
18
+ return false;
19
+ }
20
+ var $from = selection.$from;
21
+ var depth = $from.depth;
22
+ if ($from.depth > 0 && selection instanceof NodeSelection) {
23
+ return true;
24
+ }
25
+
26
+ // Depth 0-1: Always top-level
27
+ if (depth <= 1) {
28
+ return false;
29
+ }
30
+
31
+ // Depth 4+: Always nested
32
+ if (depth > 3) {
33
+ return true;
34
+ }
35
+
36
+ // Special case for table selection
37
+ if (selection instanceof CellSelection) {
38
+ return depth > 3;
39
+ }
40
+
41
+ // Check parent node type for depth 2-3
42
+ var parentNode = $from.node(depth - 1);
43
+ if (!parentNode) {
44
+ return true;
45
+ }
46
+ var parentType = parentNode.type.name;
47
+
48
+ // Special cases where content is still top-level
49
+ if (parentType === 'listItem' && depth === 3 || parentType === 'blockquote' && depth === 2 || parentType === 'taskList' && depth === 2) {
50
+ return false;
51
+ }
52
+
53
+ // Everything else at depth 2-3 is nested
54
+ return true;
55
+ };
@@ -0,0 +1,14 @@
1
+ import { type Selection } from '@atlaskit/editor-prosemirror/state';
2
+ /**
3
+ * Determines if a node is nested (not at top-level) based on its depth and context.
4
+ *
5
+ * Simple rules:
6
+ * - Depth 0-1: Always top-level (not nested)
7
+ * - Depth 2: Top-level for blockquotes and task lists
8
+ * - Depth 3: Top-level for list items only
9
+ * - Depth 4+: Always nested
10
+ *
11
+ * @param selection - The current ProseMirror selection
12
+ * @returns true if nested, false if top-level
13
+ */
14
+ export declare const isNestedNode: (selection: Selection | undefined) => boolean;
@@ -0,0 +1,14 @@
1
+ import { type Selection } from '@atlaskit/editor-prosemirror/state';
2
+ /**
3
+ * Determines if a node is nested (not at top-level) based on its depth and context.
4
+ *
5
+ * Simple rules:
6
+ * - Depth 0-1: Always top-level (not nested)
7
+ * - Depth 2: Top-level for blockquotes and task lists
8
+ * - Depth 3: Top-level for list items only
9
+ * - Depth 4+: Always nested
10
+ *
11
+ * @param selection - The current ProseMirror selection
12
+ * @returns true if nested, false if top-level
13
+ */
14
+ export declare const isNestedNode: (selection: Selection | undefined) => boolean;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atlaskit/editor-plugin-block-menu",
3
- "version": "1.0.9",
3
+ "version": "1.0.11",
4
4
  "description": "BlockMenu plugin for @atlaskit/editor-core",
5
5
  "author": "Atlassian Pty Ltd",
6
6
  "license": "Apache-2.0",
@@ -46,7 +46,7 @@
46
46
  "@babel/runtime": "^7.0.0"
47
47
  },
48
48
  "peerDependencies": {
49
- "@atlaskit/editor-common": "^108.5.0",
49
+ "@atlaskit/editor-common": "^108.6.0",
50
50
  "react": "^18.2.0",
51
51
  "react-intl-next": "npm:react-intl@^5.18.1"
52
52
  },
@@ -85,5 +85,10 @@
85
85
  "import-no-extraneous-disable-for-examples-and-docs"
86
86
  ]
87
87
  }
88
+ },
89
+ "platform-feature-flags": {
90
+ "platform_editor_adf_with_localid": {
91
+ "type": "boolean"
92
+ }
88
93
  }
89
94
  }