@atlaskit/editor-plugin-block-menu 3.2.0 → 3.2.2

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 (57) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/cjs/blockMenuPlugin.js +5 -2
  3. package/dist/cjs/editor-commands/formatNode.js +48 -1
  4. package/dist/cjs/editor-commands/transforms/container-transforms.js +15 -4
  5. package/dist/cjs/editor-commands/transforms/layout-transforms.js +29 -18
  6. package/dist/cjs/editor-commands/transforms/utils.js +20 -1
  7. package/dist/cjs/ui/block-menu-components.js +5 -14
  8. package/dist/cjs/ui/block-menu-provider.js +40 -0
  9. package/dist/cjs/ui/block-menu.js +14 -4
  10. package/dist/cjs/ui/copy-block.js +11 -3
  11. package/dist/cjs/ui/copy-section.js +7 -0
  12. package/dist/cjs/ui/delete-section.js +23 -0
  13. package/dist/cjs/ui/format-menu-nested.js +28 -0
  14. package/dist/cjs/ui/utils/checkIsFormatMenuHidden.js +35 -1
  15. package/dist/es2019/blockMenuPlugin.js +5 -2
  16. package/dist/es2019/editor-commands/formatNode.js +53 -2
  17. package/dist/es2019/editor-commands/transforms/container-transforms.js +18 -5
  18. package/dist/es2019/editor-commands/transforms/layout-transforms.js +22 -11
  19. package/dist/es2019/editor-commands/transforms/utils.js +13 -0
  20. package/dist/es2019/ui/block-menu-components.js +6 -15
  21. package/dist/es2019/ui/block-menu-provider.js +31 -0
  22. package/dist/es2019/ui/block-menu.js +15 -4
  23. package/dist/es2019/ui/copy-block.js +9 -3
  24. package/dist/es2019/ui/copy-section.js +7 -0
  25. package/dist/es2019/ui/delete-section.js +17 -0
  26. package/dist/es2019/ui/format-menu-nested.js +23 -0
  27. package/dist/es2019/ui/utils/checkIsFormatMenuHidden.js +35 -1
  28. package/dist/esm/blockMenuPlugin.js +5 -2
  29. package/dist/esm/editor-commands/formatNode.js +49 -2
  30. package/dist/esm/editor-commands/transforms/container-transforms.js +16 -5
  31. package/dist/esm/editor-commands/transforms/layout-transforms.js +28 -17
  32. package/dist/esm/editor-commands/transforms/utils.js +18 -0
  33. package/dist/esm/ui/block-menu-components.js +6 -15
  34. package/dist/esm/ui/block-menu-provider.js +32 -0
  35. package/dist/esm/ui/block-menu.js +14 -4
  36. package/dist/esm/ui/copy-block.js +11 -3
  37. package/dist/esm/ui/copy-section.js +7 -0
  38. package/dist/esm/ui/delete-section.js +16 -0
  39. package/dist/esm/ui/format-menu-nested.js +21 -0
  40. package/dist/esm/ui/utils/checkIsFormatMenuHidden.js +35 -1
  41. package/dist/types/editor-commands/transforms/layout-transforms.d.ts +3 -0
  42. package/dist/types/editor-commands/transforms/utils.d.ts +2 -1
  43. package/dist/types/ui/block-menu-components.d.ts +1 -2
  44. package/dist/types/ui/block-menu-provider.d.ts +18 -0
  45. package/dist/types/ui/block-menu.d.ts +1 -1
  46. package/dist/types/ui/copy-section.d.ts +1 -1
  47. package/dist/types/ui/delete-section.d.ts +7 -0
  48. package/dist/types/ui/format-menu-nested.d.ts +4 -0
  49. package/dist/types-ts4.5/editor-commands/transforms/layout-transforms.d.ts +3 -0
  50. package/dist/types-ts4.5/editor-commands/transforms/utils.d.ts +2 -1
  51. package/dist/types-ts4.5/ui/block-menu-components.d.ts +1 -2
  52. package/dist/types-ts4.5/ui/block-menu-provider.d.ts +18 -0
  53. package/dist/types-ts4.5/ui/block-menu.d.ts +1 -1
  54. package/dist/types-ts4.5/ui/copy-section.d.ts +1 -1
  55. package/dist/types-ts4.5/ui/delete-section.d.ts +7 -0
  56. package/dist/types-ts4.5/ui/format-menu-nested.d.ts +4 -0
  57. package/package.json +4 -4
@@ -1,5 +1,50 @@
1
- import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
1
+ import { findParentNodeOfType, findSelectedNodeOfType, safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils';
2
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
+ import { createDefaultLayoutSection } from './transforms/layout-transforms';
2
4
  import { transformNodeToTargetType } from './transforms/transformNodeToTargetType';
5
+ /**
6
+ * Handles formatting when selection is empty by inserting a new target node
7
+ */
8
+ const formatNodeWhenSelectionEmpty = (tr, targetType, nodePos, schema) => {
9
+ var _pmSafeInsert;
10
+ const {
11
+ nodes
12
+ } = schema;
13
+ const {
14
+ paragraph
15
+ } = nodes;
16
+ // if not using the ' ' here, the safeInsert from editor-common will fail to insert the heading
17
+ // and the pmSafeInsert introduce an issue that after inserting heading, and click on the handle, selection will go to top of the doc
18
+ // as an workaround, use the spaceTextNode here
19
+ const spaceTextNode = schema.text(' ');
20
+ let targetNode;
21
+ if (targetType.startsWith('heading')) {
22
+ const levelString = targetType.slice(-1);
23
+ const level = parseInt(levelString, 10);
24
+ if (isNaN(level) || level < 1 || level > 6) {
25
+ return null;
26
+ }
27
+ targetNode = nodes.heading.createAndFill({
28
+ level
29
+ }, spaceTextNode);
30
+ } else if (targetType === 'paragraph') {
31
+ targetNode = nodes.paragraph.createAndFill({}, spaceTextNode);
32
+ } else if (targetType === 'layoutSection') {
33
+ const contentAsParagraph = paragraph.createAndFill({}, spaceTextNode);
34
+ if (contentAsParagraph) {
35
+ targetNode = createDefaultLayoutSection(schema, contentAsParagraph);
36
+ }
37
+ } else {
38
+ const targetNodeType = nodes[targetType];
39
+ targetNode = targetNodeType.createAndFill();
40
+ }
41
+ if (!targetNode) {
42
+ return tr;
43
+ }
44
+ tr = (_pmSafeInsert = pmSafeInsert(targetNode, nodePos)(tr)) !== null && _pmSafeInsert !== void 0 ? _pmSafeInsert : tr;
45
+ return tr;
46
+ };
47
+
3
48
  /**
4
49
  * Formats the current node or selection to the specified target type
5
50
  * @param targetType - The target node type to convert to
@@ -11,14 +56,20 @@ export const formatNode = targetType => {
11
56
  const {
12
57
  selection
13
58
  } = tr;
59
+ const schema = tr.doc.type.schema;
14
60
  const {
15
61
  nodes
16
- } = tr.doc.type.schema;
62
+ } = schema;
17
63
 
18
64
  // Find the node to format from the current selection
19
65
  let nodeToFormat;
20
66
  let nodePos = selection.from;
21
67
 
68
+ // when selection is empty, we insert a empty target node
69
+ if (selection.empty && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true)) {
70
+ return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
71
+ }
72
+
22
73
  // Try to find the current node from selection
23
74
  const selectedNode = findSelectedNodeOfType([nodes.paragraph, nodes.heading, nodes.blockquote, nodes.panel, nodes.expand, nodes.codeBlock, nodes.bulletList, nodes.orderedList, nodes.taskList, nodes.layoutSection])(selection);
24
75
  if (selectedNode) {
@@ -1,7 +1,7 @@
1
1
  import { Fragment, Slice } from '@atlaskit/editor-prosemirror/model';
2
2
  import { findChildrenByType } from '@atlaskit/editor-prosemirror/utils';
3
3
  import { getInlineNodeTextContent } from './inline-node-transforms';
4
- import { isBlockNodeType, isListNodeType, isContainerNodeType, isBlockNodeForExtraction, convertNodeToInlineContent, getContentSupportChecker, convertCodeBlockContentToParagraphs, filterMarksForTargetNodeType } from './utils';
4
+ import { isBlockNodeType, isListNodeType, isContainerNodeType, isBlockNodeForExtraction, convertNodeToInlineContent, getContentSupportChecker, convertCodeBlockContentToParagraphs, filterMarksForTargetNodeType, getMarksWithBreakout } from './utils';
5
5
  const convertInvalidNodeToValidNodeType = (sourceContent, sourceNodeType, validNodeType, withMarks) => {
6
6
  const validTransformedContent = [];
7
7
  // Headings are not valid inside headings so convert heading nodes to paragraphs
@@ -28,9 +28,11 @@ export const transformToContainer = ({
28
28
  const schema = tr.doc.type.schema;
29
29
  const content = selection.content().content;
30
30
  let transformedContent = content;
31
+ let marks = [];
31
32
  if (sourceNode.type === schema.nodes.codeBlock) {
32
33
  const paragraphNodes = convertCodeBlockContentToParagraphs(sourceNode, schema);
33
34
  transformedContent = Fragment.fromArray(paragraphNodes);
35
+ marks = getMarksWithBreakout(sourceNode, targetNodeType);
34
36
  }
35
37
  if (targetNodeType === schema.nodes.blockquote) {
36
38
  transformedContent = convertInvalidNodeToValidNodeType(transformedContent, schema.nodes.heading, schema.nodes.paragraph, true);
@@ -42,7 +44,7 @@ export const transformToContainer = ({
42
44
  if (sourceNode.type === schema.nodes.paragraph || sourceNode.type === schema.nodes.heading) {
43
45
  transformedContent = filterMarksForTargetNodeType(transformedContent, targetNodeType);
44
46
  }
45
- const newNode = targetNodeType.createAndFill(targetAttrs, transformedContent);
47
+ const newNode = targetNodeType.createAndFill(targetAttrs, transformedContent, marks);
46
48
  if (!newNode) {
47
49
  return null;
48
50
  }
@@ -172,7 +174,8 @@ export const unwrapAndConvertToBlockType = context => {
172
174
  // if target node is code block, do unwrap and convert to code block
173
175
  if (targetNodeType === codeBlock) {
174
176
  const codeBlockContent = sourceChildren.map(node => node.content.textBetween(0, node.content.size, '\n')).join('\n');
175
- transformedContent = [codeBlock.createChecked({}, schema.text(codeBlockContent))];
177
+ const marks = getMarksWithBreakout(sourceNode, targetNodeType);
178
+ transformedContent = [codeBlock.createChecked({}, schema.text(codeBlockContent), marks)];
176
179
  }
177
180
  const slice = new Slice(Fragment.fromArray(transformedContent), 0, 0);
178
181
  tr.replaceRange(rangeStart, rangeStart + sourceNode.nodeSize, slice);
@@ -273,6 +276,14 @@ export const transformBetweenContainerTypes = context => {
273
276
  // Special handling for codeBlock target
274
277
  if (targetNodeType.name === 'codeBlock') {
275
278
  const contentSplits = splitContentForCodeBlock(sourceNode, targetNodeType, targetAttrs, tr.doc.type.schema);
279
+ if (contentSplits.length === 0) {
280
+ const {
281
+ schema
282
+ } = tr.doc.type;
283
+ const marks = getMarksWithBreakout(sourceNode, targetNodeType);
284
+ const codeBlock = schema.nodes.codeBlock.create(targetAttrs, null, marks);
285
+ return tr.replaceWith(sourcePos, sourcePos + sourceNode.nodeSize, codeBlock);
286
+ }
276
287
  return applySplitsToTransaction(tr, sourcePos, sourceNode.nodeSize, contentSplits);
277
288
  }
278
289
 
@@ -320,7 +331,8 @@ const splitContentForCodeBlock = (sourceNode, targetNodeType, targetAttrs, schem
320
331
  const flushCurrentCodeBlock = () => {
321
332
  if (currentTextContent.length > 0) {
322
333
  const codeText = currentTextContent.join('\n');
323
- const codeBlockNode = targetNodeType.create(targetAttrs, schema.text(codeText));
334
+ const marks = getMarksWithBreakout(sourceNode, targetNodeType);
335
+ const codeBlockNode = targetNodeType.create(targetAttrs, schema.text(codeText), marks);
324
336
  splits.push(codeBlockNode);
325
337
  currentTextContent = [];
326
338
  }
@@ -391,7 +403,8 @@ const splitContentAroundUnsupportedBlocks = (sourceNode, isContentSupported, tar
391
403
  }
392
404
  const flushCurrentContainer = () => {
393
405
  if (currentContainerContent.length > 0) {
394
- const containerNode = targetNodeType.create(targetAttrs, Fragment.fromArray(currentContainerContent));
406
+ const marks = getMarksWithBreakout(sourceNode, targetNodeType);
407
+ const containerNode = targetNodeType.create(targetAttrs, Fragment.fromArray(currentContainerContent), marks);
395
408
  splits.push(containerNode);
396
409
  currentContainerContent = [];
397
410
  }
@@ -1,27 +1,38 @@
1
1
  import { DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH } from '@atlaskit/editor-common/styles';
2
2
  import { Fragment } from '@atlaskit/editor-prosemirror/model';
3
3
  import { convertUnwrappedLayoutContent, unwrapLayoutNodesToTextNodes } from './layout/utils';
4
- export const convertToLayout = context => {
5
- const {
6
- tr,
7
- sourceNode,
8
- sourcePos
9
- } = context;
4
+ import { getMarksWithBreakout, isHeadingOrParagraphNode } from './utils';
5
+ export const createDefaultLayoutSection = (schema, content, marks) => {
10
6
  const {
11
7
  layoutSection,
12
8
  layoutColumn,
13
9
  paragraph
14
- } = tr.doc.type.schema.nodes || {};
15
- const content = sourceNode.mark(sourceNode.marks.filter(mark => mark.type.name !== 'breakout'));
10
+ } = schema.nodes;
16
11
  const layoutContent = Fragment.fromArray([layoutColumn.createChecked({
17
12
  width: DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH
18
13
  }, content), layoutColumn.create({
19
14
  width: DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH
20
15
  }, paragraph.createAndFill())]);
21
- const layoutSectionNode = layoutSection.createChecked(undefined, layoutContent);
16
+ return layoutSection.createChecked(undefined, layoutContent, marks);
17
+ };
18
+ export const convertToLayout = context => {
19
+ const {
20
+ tr,
21
+ sourceNode,
22
+ sourcePos
23
+ } = context;
24
+ const content = sourceNode.mark(sourceNode.marks.filter(mark => mark.type.name !== 'breakout'));
22
25
 
23
- // Replace the original node with the new layout node
24
- tr.replaceRangeWith(sourcePos, sourcePos + sourceNode.nodeSize, layoutSectionNode);
26
+ // Layout supports breakout mark that can have width attribute
27
+ // When other nodes with breakout (codeBlock and expand) are converted to a layout, the layout should get width of original node
28
+ const marks = getMarksWithBreakout(sourceNode, tr.doc.type.schema.nodes.layoutSection);
29
+ const layoutSectionNode = createDefaultLayoutSection(tr.doc.type.schema, content, marks);
30
+ if (isHeadingOrParagraphNode(sourceNode)) {
31
+ // -1 to fix when sourceNode is the last node in the document, unable to convert to layout
32
+ tr.replaceRangeWith(sourcePos > 0 ? sourcePos - 1 : sourcePos, sourcePos + sourceNode.nodeSize - 1, layoutSectionNode);
33
+ } else {
34
+ tr.replaceRangeWith(sourcePos, sourcePos + sourceNode.nodeSize, layoutSectionNode);
35
+ }
25
36
  return tr;
26
37
  };
27
38
  export const transformLayoutNode = context => {
@@ -196,4 +196,17 @@ export const convertCodeBlockContentToParagraphs = (codeBlockNode, schema) => {
196
196
  paragraphNodes.push(paragraphNode);
197
197
  });
198
198
  return paragraphNodes;
199
+ };
200
+ const isBreakoutMarkSupported = nodeType => {
201
+ return ['codeBlock', 'expand', 'layoutSection'].includes(nodeType.name);
202
+ };
203
+ export const getMarksWithBreakout = (sourceNode, targetNodeType) => {
204
+ const allowedMarks = targetNodeType.allowedMarks(sourceNode.marks);
205
+ const sourceBreakoutMark = sourceNode.marks.find(mark => mark.type.name === 'breakout');
206
+ if (sourceBreakoutMark && isBreakoutMarkSupported(targetNodeType)) {
207
+ // Check if breakout mark is already in allowedMarks to avoid duplicates
208
+ const hasBreakoutMark = allowedMarks.some(mark => mark.type.name === 'breakout');
209
+ return hasBreakoutMark ? allowedMarks : [...allowedMarks, sourceBreakoutMark];
210
+ }
211
+ return allowedMarks;
199
212
  };
@@ -1,12 +1,12 @@
1
1
  import React from 'react';
2
2
  import { MOVE_UP_MENU_ITEM, MOVE_UP_DOWN_MENU_SECTION, MOVE_DOWN_MENU_ITEM, MOVE_BLOCK_SECTION_RANK, PRIMARY_MENU_SECTION, BLOCK_MENU_SECTION_RANK, COPY_MENU_SECTION, COPY_BLOCK_MENU_ITEM, COPY_MENU_SECTION_RANK, COPY_LINK_MENU_ITEM, DELETE_MENU_SECTION, DELETE_MENU_ITEM, DELETE_SECTION_RANK, NESTED_FORMAT_MENU_SECTION, NESTED_FORMAT_MENU } from '@atlaskit/editor-common/block-menu';
3
- import { ToolbarDropdownItemSection, ToolbarNestedDropdownMenu } from '@atlaskit/editor-toolbar';
4
- import ChangesIcon from '@atlaskit/icon/core/changes';
5
- import ChevronRightIcon from '@atlaskit/icon/core/chevron-right';
3
+ import { ToolbarDropdownItemSection } from '@atlaskit/editor-toolbar';
6
4
  import CopyBlockMenuItem from './copy-block';
7
5
  import { CopyLinkDropdownItem } from './copy-link';
8
6
  import { CopySection } from './copy-section';
9
7
  import { DeleteDropdownItem } from './delete-button';
8
+ import { DeleteSection } from './delete-section';
9
+ import { FormatMenuComponent } from './format-menu-nested';
10
10
  import { FormatMenuSection } from './format-menu-section';
11
11
  import { MoveDownDropdownItem } from './move-down';
12
12
  import { MoveUpDropdownItem } from './move-up';
@@ -49,16 +49,7 @@ const getFormatMenuComponents = api => {
49
49
  } = {
50
50
  children: null
51
51
  }) => {
52
- return /*#__PURE__*/React.createElement(ToolbarNestedDropdownMenu, {
53
- text: "Format",
54
- elemBefore: /*#__PURE__*/React.createElement(ChangesIcon, {
55
- label: ""
56
- }),
57
- elemAfter: /*#__PURE__*/React.createElement(ChevronRightIcon, {
58
- label: 'example nested menu'
59
- }),
60
- enableMaxHeight: true
61
- }, children);
52
+ return /*#__PURE__*/React.createElement(FormatMenuComponent, null, children);
62
53
  }
63
54
  }, {
64
55
  type: 'block-menu-section',
@@ -144,8 +135,8 @@ export const getBlockMenuComponents = ({
144
135
  component: ({
145
136
  children
146
137
  }) => {
147
- return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
148
- hasSeparator: true
138
+ return /*#__PURE__*/React.createElement(DeleteSection, {
139
+ api: api
149
140
  }, children);
150
141
  }
151
142
  }, ...getMoveUpMoveDownMenuComponents(api), {
@@ -0,0 +1,31 @@
1
+ import React, { useCallback, createContext, useContext } from 'react';
2
+ const BlockMenuContext = /*#__PURE__*/createContext({
3
+ onDropdownOpenChanged: () => {}
4
+ });
5
+ export const useBlockMenu = () => {
6
+ const context = useContext(BlockMenuContext);
7
+ if (!context) {
8
+ throw new Error('useBlockMenu must be used within BlockMenuProvider');
9
+ }
10
+ return context;
11
+ };
12
+ export const BlockMenuProvider = ({
13
+ children,
14
+ api
15
+ }) => {
16
+ const onDropdownOpenChanged = useCallback(isOpen => {
17
+ if (!isOpen) {
18
+ // On Dropdown closed, return focus to editor
19
+ setTimeout(() => requestAnimationFrame(() => {
20
+ api === null || api === void 0 ? void 0 : api.core.actions.focus({
21
+ scrollIntoView: false
22
+ });
23
+ }), 1);
24
+ }
25
+ }, [api]);
26
+ return /*#__PURE__*/React.createElement(BlockMenuContext.Provider, {
27
+ value: {
28
+ onDropdownOpenChanged
29
+ }
30
+ }, children);
31
+ };
@@ -1,4 +1,4 @@
1
- /* block-menu.tsx generated by @compiled/babel-plugin v0.36.1 */
1
+ /* block-menu.tsx generated by @compiled/babel-plugin v0.38.1 */
2
2
  import "./block-menu.compiled.css";
3
3
  import { ax, ix } from "@compiled/react/runtime";
4
4
  import React, { useContext, useEffect } from 'react';
@@ -11,6 +11,8 @@ import { OutsideClickTargetRefContext, withReactEditorViewOuterListeners } from
11
11
  import { akEditorFloatingOverlapPanelZIndex } from '@atlaskit/editor-shared-styles';
12
12
  import { ToolbarDropdownItem, ToolbarDropdownItemSection, ToolbarNestedDropdownMenu } from '@atlaskit/editor-toolbar';
13
13
  import { Box } from '@atlaskit/primitives/compiled';
14
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
15
+ import { useBlockMenu } from './block-menu-provider';
14
16
  import { BlockMenuRenderer } from './block-menu-renderer';
15
17
  const styles = {
16
18
  base: "_2rko12b0 _bfhk1bhr _16qs1cd0"
@@ -62,15 +64,23 @@ const BlockMenu = ({
62
64
  currentUserIntent: (_states$userIntentSta = states.userIntentState) === null || _states$userIntentSta === void 0 ? void 0 : _states$userIntentSta.currentUserIntent
63
65
  };
64
66
  });
67
+ const {
68
+ onDropdownOpenChanged
69
+ } = useBlockMenu();
65
70
  const hasFocus = (_editorView$hasFocus = editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) !== null && _editorView$hasFocus !== void 0 ? _editorView$hasFocus : false;
66
71
  const hasSelection = !!editorView && !editorView.state.selection.empty;
72
+ const emptyLineEnabled = expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
73
+
74
+ // hasSelection true, always show block menu
75
+ // hasSelection false, only show block menu when empty line experiment is enabled
76
+ const shouldShowBlockMenuForEmptyLine = hasSelection || emptyLineEnabled && !hasSelection;
67
77
  useEffect(() => {
68
78
  var _api$userIntent;
69
- if (!isMenuOpen || !menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !hasSelection || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
79
+ if (!isMenuOpen || !menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
70
80
  return;
71
81
  }
72
82
  api === null || api === void 0 ? void 0 : api.core.actions.execute(api === null || api === void 0 ? void 0 : (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('blockMenuOpen'));
73
- }, [api, isMenuOpen, menuTriggerBy, isSelectedViaDragHandle, hasFocus, hasSelection, currentUserIntent]);
83
+ }, [api, isMenuOpen, menuTriggerBy, isSelectedViaDragHandle, hasFocus, shouldShowBlockMenuForEmptyLine, currentUserIntent]);
74
84
  if (!isMenuOpen) {
75
85
  return null;
76
86
  }
@@ -84,13 +94,14 @@ const BlockMenu = ({
84
94
  })({
85
95
  tr
86
96
  });
97
+ onDropdownOpenChanged(false);
87
98
  api === null || api === void 0 ? void 0 : (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 ? void 0 : _api$userIntent2.commands.setCurrentUserIntent(currentUserIntent === 'blockMenuOpen' ? 'default' : currentUserIntent || 'default')({
88
99
  tr
89
100
  });
90
101
  return tr;
91
102
  });
92
103
  };
93
- if (!menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !hasSelection || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
104
+ if (!menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
94
105
  closeMenu();
95
106
  return null;
96
107
  }
@@ -75,10 +75,16 @@ const CopyBlockMenuItem = ({
75
75
  // When nodeType.inlineContent is true, it will be treated as an inline node in the copyDomNode function,
76
76
  // but we want to treat it as a block node when copying, hence setting it to false here
77
77
  if (selection.node.type.name === 'codeBlock') {
78
- nodeType.inlineContent = false;
78
+ const codeBlockNodeType = {
79
+ ...nodeType,
80
+ inlineContent: false
81
+ };
82
+ const domNode = toDOM(selection.node, schema);
83
+ copyDomNode(domNode, codeBlockNodeType, selection);
84
+ } else {
85
+ const domNode = toDOM(selection.node, schema);
86
+ copyDomNode(domNode, nodeType, selection);
79
87
  }
80
- const domNode = toDOM(selection.node, schema);
81
- copyDomNode(domNode, nodeType, selection);
82
88
  }
83
89
 
84
90
  // close the block menu after copying
@@ -1,13 +1,20 @@
1
1
  import React, { useCallback } from 'react';
2
2
  import { ToolbarDropdownItemSection } from '@atlaskit/editor-toolbar';
3
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
4
  import { checkIsFormatMenuHidden } from './utils/checkIsFormatMenuHidden';
4
5
  export const CopySection = ({
5
6
  api,
6
7
  children
7
8
  }) => {
9
+ var _api$selection, _api$selection$shared, _api$selection$shared2;
8
10
  const isFormatMenuHidden = useCallback(() => {
9
11
  return checkIsFormatMenuHidden(api);
10
12
  }, [api]);
13
+ 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;
14
+ const isEmptyLineSelected = !!(selection !== null && selection !== void 0 && selection.empty) && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
15
+ if (isEmptyLineSelected) {
16
+ return null;
17
+ }
11
18
  return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
12
19
  hasSeparator: !isFormatMenuHidden()
13
20
  }, children);
@@ -0,0 +1,17 @@
1
+ import React from 'react';
2
+ import { ToolbarDropdownItemSection } from '@atlaskit/editor-toolbar';
3
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
4
+ export const DeleteSection = ({
5
+ api,
6
+ children
7
+ }) => {
8
+ var _api$selection, _api$selection$shared, _api$selection$shared2;
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) === 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;
10
+ const isEmptyLineSelected = !!(selection !== null && selection !== void 0 && selection.empty) && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
11
+ if (isEmptyLineSelected) {
12
+ return null;
13
+ }
14
+ return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
15
+ hasSeparator: true
16
+ }, children);
17
+ };
@@ -0,0 +1,23 @@
1
+ import React from 'react';
2
+ import { useIntl } from 'react-intl-next';
3
+ import { messages } from '@atlaskit/editor-common/block-menu';
4
+ import { ToolbarNestedDropdownMenu } from '@atlaskit/editor-toolbar';
5
+ import ChangesIcon from '@atlaskit/icon/core/changes';
6
+ import ChevronRightIcon from '@atlaskit/icon/core/chevron-right';
7
+ export const FormatMenuComponent = ({
8
+ children
9
+ }) => {
10
+ const {
11
+ formatMessage
12
+ } = useIntl();
13
+ return /*#__PURE__*/React.createElement(ToolbarNestedDropdownMenu, {
14
+ text: formatMessage(messages.turnInto),
15
+ elemBefore: /*#__PURE__*/React.createElement(ChangesIcon, {
16
+ label: ""
17
+ }),
18
+ elemAfter: /*#__PURE__*/React.createElement(ChevronRightIcon, {
19
+ label: ""
20
+ }),
21
+ enableMaxHeight: true
22
+ }, children);
23
+ };
@@ -1,5 +1,6 @@
1
1
  import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
2
2
  import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
3
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
4
  import { isNestedNode } from './isNestedNode';
4
5
  const getIsFormatMenuHidden = (selection, schema, menuTriggerBy) => {
5
6
  const nodes = schema.nodes;
@@ -26,6 +27,39 @@ const getIsFormatMenuHidden = (selection, schema, menuTriggerBy) => {
26
27
  const isNested = isNestedNode(selection, menuTriggerBy);
27
28
  return !content || isNested;
28
29
  };
30
+ const getIsFormatMenuHiddenEmptyLine = (selection, schema, menuTriggerBy) => {
31
+ const nodes = schema.nodes;
32
+ if (!nodes) {
33
+ return false;
34
+ }
35
+ const isNested = isNestedNode(selection, menuTriggerBy);
36
+ if (selection.empty || selection.content().size === 0) {
37
+ // if empty selection, show format menu
38
+ return false;
39
+ } else if (isNested) {
40
+ // if nested, always hide format menu
41
+ return true;
42
+ } else {
43
+ let content;
44
+ const allowedNodes = [nodes.paragraph, nodes.heading, nodes.blockquote, nodes.panel, nodes.codeBlock, nodes.bulletList, nodes.orderedList, nodes.taskList];
45
+ if (expValEquals('platform_editor_block_menu_layout_format', 'isEnabled', true)) {
46
+ allowedNodes.push(nodes.layoutSection);
47
+ }
48
+ if (expValEquals('platform_editor_block_menu_expand_format', 'isEnabled', true)) {
49
+ allowedNodes.push(nodes.expand);
50
+ }
51
+ const selectedNode = findSelectedNodeOfType(allowedNodes)(selection);
52
+ if (selectedNode) {
53
+ content = selectedNode.node;
54
+ } else {
55
+ const listTypeOrBlockQuoteNode = findParentNodeOfType([nodes.paragraph, nodes.heading, nodes.blockquote, nodes.listItem, nodes.taskItem])(selection);
56
+ if (listTypeOrBlockQuoteNode) {
57
+ content = listTypeOrBlockQuoteNode.node;
58
+ }
59
+ }
60
+ return !content;
61
+ }
62
+ };
29
63
  export const checkIsFormatMenuHidden = api => {
30
64
  var _api$selection, _api$selection$shared, _api$selection$shared2, _api$core$sharedState, _api$blockControls, _api$blockControls$sh;
31
65
  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,5 +68,5 @@ export const checkIsFormatMenuHidden = api => {
34
68
  if (!selection || !schema || !menuTriggerBy) {
35
69
  return false;
36
70
  }
37
- return getIsFormatMenuHidden(selection, schema, menuTriggerBy);
71
+ return expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true) ? getIsFormatMenuHiddenEmptyLine(selection, schema, menuTriggerBy) : getIsFormatMenuHidden(selection, schema, menuTriggerBy);
38
72
  };
@@ -4,6 +4,7 @@ import { formatNode as _formatNode } from './editor-commands/formatNode';
4
4
  import { createPlugin } from './pm-plugins/main';
5
5
  import BlockMenu from './ui/block-menu';
6
6
  import { getBlockMenuComponents } from './ui/block-menu-components';
7
+ import { BlockMenuProvider } from './ui/block-menu-provider';
7
8
  export var blockMenuPlugin = function blockMenuPlugin(_ref) {
8
9
  var api = _ref.api,
9
10
  config = _ref.config;
@@ -52,13 +53,15 @@ export var blockMenuPlugin = function blockMenuPlugin(_ref) {
52
53
  popupsMountPoint = _ref2.popupsMountPoint,
53
54
  popupsBoundariesElement = _ref2.popupsBoundariesElement,
54
55
  popupsScrollableElement = _ref2.popupsScrollableElement;
55
- return /*#__PURE__*/React.createElement(BlockMenu, {
56
+ return /*#__PURE__*/React.createElement(BlockMenuProvider, {
57
+ api: api
58
+ }, /*#__PURE__*/React.createElement(BlockMenu, {
56
59
  editorView: editorView,
57
60
  api: api,
58
61
  mountTo: popupsMountPoint,
59
62
  boundariesElement: popupsBoundariesElement,
60
63
  scrollableElement: popupsScrollableElement
61
- });
64
+ }));
62
65
  }
63
66
  };
64
67
  };
@@ -1,5 +1,46 @@
1
- import { findParentNodeOfType, findSelectedNodeOfType } from '@atlaskit/editor-prosemirror/utils';
1
+ import { findParentNodeOfType, findSelectedNodeOfType, safeInsert as pmSafeInsert } from '@atlaskit/editor-prosemirror/utils';
2
+ import { expValEqualsNoExposure } from '@atlaskit/tmp-editor-statsig/exp-val-equals-no-exposure';
3
+ import { createDefaultLayoutSection } from './transforms/layout-transforms';
2
4
  import { transformNodeToTargetType } from './transforms/transformNodeToTargetType';
5
+ /**
6
+ * Handles formatting when selection is empty by inserting a new target node
7
+ */
8
+ var formatNodeWhenSelectionEmpty = function formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema) {
9
+ var _pmSafeInsert;
10
+ var nodes = schema.nodes;
11
+ var paragraph = nodes.paragraph;
12
+ // if not using the ' ' here, the safeInsert from editor-common will fail to insert the heading
13
+ // and the pmSafeInsert introduce an issue that after inserting heading, and click on the handle, selection will go to top of the doc
14
+ // as an workaround, use the spaceTextNode here
15
+ var spaceTextNode = schema.text(' ');
16
+ var targetNode;
17
+ if (targetType.startsWith('heading')) {
18
+ var levelString = targetType.slice(-1);
19
+ var level = parseInt(levelString, 10);
20
+ if (isNaN(level) || level < 1 || level > 6) {
21
+ return null;
22
+ }
23
+ targetNode = nodes.heading.createAndFill({
24
+ level: level
25
+ }, spaceTextNode);
26
+ } else if (targetType === 'paragraph') {
27
+ targetNode = nodes.paragraph.createAndFill({}, spaceTextNode);
28
+ } else if (targetType === 'layoutSection') {
29
+ var contentAsParagraph = paragraph.createAndFill({}, spaceTextNode);
30
+ if (contentAsParagraph) {
31
+ targetNode = createDefaultLayoutSection(schema, contentAsParagraph);
32
+ }
33
+ } else {
34
+ var targetNodeType = nodes[targetType];
35
+ targetNode = targetNodeType.createAndFill();
36
+ }
37
+ if (!targetNode) {
38
+ return tr;
39
+ }
40
+ tr = (_pmSafeInsert = pmSafeInsert(targetNode, nodePos)(tr)) !== null && _pmSafeInsert !== void 0 ? _pmSafeInsert : tr;
41
+ return tr;
42
+ };
43
+
3
44
  /**
4
45
  * Formats the current node or selection to the specified target type
5
46
  * @param targetType - The target node type to convert to
@@ -8,12 +49,18 @@ export var formatNode = function formatNode(targetType) {
8
49
  return function (_ref) {
9
50
  var tr = _ref.tr;
10
51
  var selection = tr.selection;
11
- var nodes = tr.doc.type.schema.nodes;
52
+ var schema = tr.doc.type.schema;
53
+ var nodes = schema.nodes;
12
54
 
13
55
  // Find the node to format from the current selection
14
56
  var nodeToFormat;
15
57
  var nodePos = selection.from;
16
58
 
59
+ // when selection is empty, we insert a empty target node
60
+ if (selection.empty && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true)) {
61
+ return formatNodeWhenSelectionEmpty(tr, targetType, nodePos, schema);
62
+ }
63
+
17
64
  // Try to find the current node from selection
18
65
  var selectedNode = findSelectedNodeOfType([nodes.paragraph, nodes.heading, nodes.blockquote, nodes.panel, nodes.expand, nodes.codeBlock, nodes.bulletList, nodes.orderedList, nodes.taskList, nodes.layoutSection])(selection);
19
66
  if (selectedNode) {