@atlaskit/editor-plugin-block-menu 3.1.6 → 3.2.1

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 (46) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/dist/cjs/blockMenuPlugin.js +19 -2
  3. package/dist/cjs/editor-commands/formatNode.js +48 -1
  4. package/dist/cjs/editor-commands/transforms/layout-transforms.js +26 -19
  5. package/dist/cjs/ui/block-menu-components.js +3 -2
  6. package/dist/cjs/ui/block-menu-provider.js +40 -0
  7. package/dist/cjs/ui/block-menu.js +13 -3
  8. package/dist/cjs/ui/copy-block.js +11 -3
  9. package/dist/cjs/ui/copy-section.js +7 -0
  10. package/dist/cjs/ui/delete-section.js +23 -0
  11. package/dist/cjs/ui/utils/checkIsFormatMenuHidden.js +35 -1
  12. package/dist/es2019/blockMenuPlugin.js +19 -2
  13. package/dist/es2019/editor-commands/formatNode.js +53 -2
  14. package/dist/es2019/editor-commands/transforms/layout-transforms.js +19 -12
  15. package/dist/es2019/ui/block-menu-components.js +3 -2
  16. package/dist/es2019/ui/block-menu-provider.js +31 -0
  17. package/dist/es2019/ui/block-menu.js +14 -3
  18. package/dist/es2019/ui/copy-block.js +9 -3
  19. package/dist/es2019/ui/copy-section.js +7 -0
  20. package/dist/es2019/ui/delete-section.js +17 -0
  21. package/dist/es2019/ui/utils/checkIsFormatMenuHidden.js +35 -1
  22. package/dist/esm/blockMenuPlugin.js +19 -2
  23. package/dist/esm/editor-commands/formatNode.js +49 -2
  24. package/dist/esm/editor-commands/transforms/layout-transforms.js +25 -18
  25. package/dist/esm/ui/block-menu-components.js +3 -2
  26. package/dist/esm/ui/block-menu-provider.js +32 -0
  27. package/dist/esm/ui/block-menu.js +13 -3
  28. package/dist/esm/ui/copy-block.js +11 -3
  29. package/dist/esm/ui/copy-section.js +7 -0
  30. package/dist/esm/ui/delete-section.js +16 -0
  31. package/dist/esm/ui/utils/checkIsFormatMenuHidden.js +35 -1
  32. package/dist/types/blockMenuPluginType.d.ts +8 -0
  33. package/dist/types/editor-commands/transforms/layout-transforms.d.ts +3 -0
  34. package/dist/types/index.d.ts +1 -1
  35. package/dist/types/ui/block-menu-provider.d.ts +18 -0
  36. package/dist/types/ui/block-menu.d.ts +1 -1
  37. package/dist/types/ui/copy-section.d.ts +1 -1
  38. package/dist/types/ui/delete-section.d.ts +7 -0
  39. package/dist/types-ts4.5/blockMenuPluginType.d.ts +8 -0
  40. package/dist/types-ts4.5/editor-commands/transforms/layout-transforms.d.ts +3 -0
  41. package/dist/types-ts4.5/index.d.ts +1 -1
  42. package/dist/types-ts4.5/ui/block-menu-provider.d.ts +18 -0
  43. package/dist/types-ts4.5/ui/block-menu.d.ts +1 -1
  44. package/dist/types-ts4.5/ui/copy-section.d.ts +1 -1
  45. package/dist/types-ts4.5/ui/delete-section.d.ts +7 -0
  46. package/package.json +4 -4
@@ -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
+ };
@@ -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
+ };
@@ -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;
@@ -33,18 +34,34 @@ export var blockMenuPlugin = function blockMenuPlugin(_ref) {
33
34
  return _formatNode(targetType);
34
35
  }
35
36
  },
37
+ getSharedState: function getSharedState(editorState) {
38
+ var _api$blockControls;
39
+ if (!editorState) {
40
+ return {
41
+ currentSelectedNodeName: undefined
42
+ };
43
+ }
44
+
45
+ // Get the menuTriggerBy from blockControls plugin if available
46
+ var currentSelectedNodeName = api === null || api === void 0 || (_api$blockControls = api.blockControls) === null || _api$blockControls === void 0 || (_api$blockControls = _api$blockControls.sharedState.currentState()) === null || _api$blockControls === void 0 ? void 0 : _api$blockControls.menuTriggerBy;
47
+ return {
48
+ currentSelectedNodeName: currentSelectedNodeName
49
+ };
50
+ },
36
51
  contentComponent: function contentComponent(_ref2) {
37
52
  var editorView = _ref2.editorView,
38
53
  popupsMountPoint = _ref2.popupsMountPoint,
39
54
  popupsBoundariesElement = _ref2.popupsBoundariesElement,
40
55
  popupsScrollableElement = _ref2.popupsScrollableElement;
41
- return /*#__PURE__*/React.createElement(BlockMenu, {
56
+ return /*#__PURE__*/React.createElement(BlockMenuProvider, {
57
+ api: api
58
+ }, /*#__PURE__*/React.createElement(BlockMenu, {
42
59
  editorView: editorView,
43
60
  api: api,
44
61
  mountTo: popupsMountPoint,
45
62
  boundariesElement: popupsBoundariesElement,
46
63
  scrollableElement: popupsScrollableElement
47
- });
64
+ }));
48
65
  }
49
66
  };
50
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) {
@@ -2,26 +2,33 @@ import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
2
2
  import { DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH } from '@atlaskit/editor-common/styles';
3
3
  import { Fragment } from '@atlaskit/editor-prosemirror/model';
4
4
  import { convertUnwrappedLayoutContent, unwrapLayoutNodesToTextNodes } from './layout/utils';
5
+ import { isHeadingOrParagraphNode } from './utils';
6
+ export var createDefaultLayoutSection = function createDefaultLayoutSection(schema, content) {
7
+ var _schema$nodes = schema.nodes,
8
+ layoutSection = _schema$nodes.layoutSection,
9
+ layoutColumn = _schema$nodes.layoutColumn,
10
+ paragraph = _schema$nodes.paragraph;
11
+ var layoutContent = Fragment.fromArray([layoutColumn.createChecked({
12
+ width: DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH
13
+ }, content), layoutColumn.create({
14
+ width: DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH
15
+ }, paragraph.createAndFill())]);
16
+ return layoutSection.createChecked(undefined, layoutContent);
17
+ };
5
18
  export var convertToLayout = function convertToLayout(context) {
6
19
  var tr = context.tr,
7
20
  sourceNode = context.sourceNode,
8
21
  sourcePos = context.sourcePos;
9
- var _ref = tr.doc.type.schema.nodes || {},
10
- layoutSection = _ref.layoutSection,
11
- layoutColumn = _ref.layoutColumn,
12
- paragraph = _ref.paragraph;
13
22
  var content = sourceNode.mark(sourceNode.marks.filter(function (mark) {
14
23
  return mark.type.name !== 'breakout';
15
24
  }));
16
- var layoutContent = Fragment.fromArray([layoutColumn.createChecked({
17
- width: DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH
18
- }, content), layoutColumn.create({
19
- width: DEFAULT_TWO_COLUMN_LAYOUT_COLUMN_WIDTH
20
- }, paragraph.createAndFill())]);
21
- var layoutSectionNode = layoutSection.createChecked(undefined, layoutContent);
22
-
23
- // Replace the original node with the new layout node
24
- tr.replaceRangeWith(sourcePos, sourcePos + sourceNode.nodeSize, layoutSectionNode);
25
+ var layoutSectionNode = createDefaultLayoutSection(tr.doc.type.schema, content);
26
+ if (isHeadingOrParagraphNode(sourceNode)) {
27
+ // -1 to fix when sourceNode is the last node in the document, unable to convert to layout
28
+ tr.replaceRangeWith(sourcePos > 0 ? sourcePos - 1 : sourcePos, sourcePos + sourceNode.nodeSize - 1, layoutSectionNode);
29
+ } else {
30
+ tr.replaceRangeWith(sourcePos, sourcePos + sourceNode.nodeSize, layoutSectionNode);
31
+ }
25
32
  return tr;
26
33
  };
27
34
  export var transformLayoutNode = function transformLayoutNode(context) {
@@ -31,11 +38,11 @@ export var transformLayoutNode = function transformLayoutNode(context) {
31
38
  sourcePos = context.sourcePos,
32
39
  targetAttrs = context.targetAttrs;
33
40
  var schema = tr.doc.type.schema || {};
34
- var _ref2 = schema.nodes || {},
35
- layoutSection = _ref2.layoutSection,
36
- layoutColumn = _ref2.layoutColumn,
37
- paragraph = _ref2.paragraph,
38
- heading = _ref2.heading;
41
+ var _ref = schema.nodes || {},
42
+ layoutSection = _ref.layoutSection,
43
+ layoutColumn = _ref.layoutColumn,
44
+ paragraph = _ref.paragraph,
45
+ heading = _ref.heading;
39
46
  var layoutColumnNodes = [];
40
47
  var targetTextNodeType = targetNodeType === heading ? heading : paragraph;
41
48
  sourceNode.children.forEach(function (child) {
@@ -8,6 +8,7 @@ import CopyBlockMenuItem from './copy-block';
8
8
  import { CopyLinkDropdownItem } from './copy-link';
9
9
  import { CopySection } from './copy-section';
10
10
  import { DeleteDropdownItem } from './delete-button';
11
+ import { DeleteSection } from './delete-section';
11
12
  import { FormatMenuSection } from './format-menu-section';
12
13
  import { MoveDownDropdownItem } from './move-down';
13
14
  import { MoveUpDropdownItem } from './move-up';
@@ -148,8 +149,8 @@ export var getBlockMenuComponents = function getBlockMenuComponents(_ref4) {
148
149
  rank: BLOCK_MENU_SECTION_RANK[DELETE_MENU_SECTION.key],
149
150
  component: function component(_ref7) {
150
151
  var children = _ref7.children;
151
- return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
152
- hasSeparator: true
152
+ return /*#__PURE__*/React.createElement(DeleteSection, {
153
+ api: api
153
154
  }, children);
154
155
  }
155
156
  }], _toConsumableArray(getMoveUpMoveDownMenuComponents(api)), [{
@@ -0,0 +1,32 @@
1
+ import React, { useCallback, createContext, useContext } from 'react';
2
+ var BlockMenuContext = /*#__PURE__*/createContext({
3
+ onDropdownOpenChanged: function onDropdownOpenChanged() {}
4
+ });
5
+ export var useBlockMenu = function useBlockMenu() {
6
+ var context = useContext(BlockMenuContext);
7
+ if (!context) {
8
+ throw new Error('useBlockMenu must be used within BlockMenuProvider');
9
+ }
10
+ return context;
11
+ };
12
+ export var BlockMenuProvider = function BlockMenuProvider(_ref) {
13
+ var children = _ref.children,
14
+ api = _ref.api;
15
+ var onDropdownOpenChanged = useCallback(function (isOpen) {
16
+ if (!isOpen) {
17
+ // On Dropdown closed, return focus to editor
18
+ setTimeout(function () {
19
+ return requestAnimationFrame(function () {
20
+ api === null || api === void 0 || api.core.actions.focus({
21
+ scrollIntoView: false
22
+ });
23
+ });
24
+ }, 1);
25
+ }
26
+ }, [api]);
27
+ return /*#__PURE__*/React.createElement(BlockMenuContext.Provider, {
28
+ value: {
29
+ onDropdownOpenChanged: onDropdownOpenChanged
30
+ }
31
+ }, children);
32
+ };
@@ -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
  var styles = {
16
18
  base: "_2rko12b0 _bfhk1bhr _16qs1cd0"
@@ -65,15 +67,22 @@ var BlockMenu = function BlockMenu(_ref2) {
65
67
  isSelectedViaDragHandle = _useSharedPluginState.isSelectedViaDragHandle,
66
68
  isMenuOpen = _useSharedPluginState.isMenuOpen,
67
69
  currentUserIntent = _useSharedPluginState.currentUserIntent;
70
+ var _useBlockMenu = useBlockMenu(),
71
+ onDropdownOpenChanged = _useBlockMenu.onDropdownOpenChanged;
68
72
  var hasFocus = (_editorView$hasFocus = editorView === null || editorView === void 0 ? void 0 : editorView.hasFocus()) !== null && _editorView$hasFocus !== void 0 ? _editorView$hasFocus : false;
69
73
  var hasSelection = !!editorView && !editorView.state.selection.empty;
74
+ var emptyLineEnabled = expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
75
+
76
+ // hasSelection true, always show block menu
77
+ // hasSelection false, only show block menu when empty line experiment is enabled
78
+ var shouldShowBlockMenuForEmptyLine = hasSelection || emptyLineEnabled && !hasSelection;
70
79
  useEffect(function () {
71
80
  var _api$userIntent;
72
- if (!isMenuOpen || !menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !hasSelection || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
81
+ if (!isMenuOpen || !menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
73
82
  return;
74
83
  }
75
84
  api === null || api === void 0 || api.core.actions.execute(api === null || api === void 0 || (_api$userIntent = api.userIntent) === null || _api$userIntent === void 0 ? void 0 : _api$userIntent.commands.setCurrentUserIntent('blockMenuOpen'));
76
- }, [api, isMenuOpen, menuTriggerBy, isSelectedViaDragHandle, hasFocus, hasSelection, currentUserIntent]);
85
+ }, [api, isMenuOpen, menuTriggerBy, isSelectedViaDragHandle, hasFocus, shouldShowBlockMenuForEmptyLine, currentUserIntent]);
77
86
  if (!isMenuOpen) {
78
87
  return null;
79
88
  }
@@ -86,13 +95,14 @@ var BlockMenu = function BlockMenu(_ref2) {
86
95
  })({
87
96
  tr: tr
88
97
  });
98
+ onDropdownOpenChanged(false);
89
99
  api === null || api === void 0 || (_api$userIntent2 = api.userIntent) === null || _api$userIntent2 === void 0 || _api$userIntent2.commands.setCurrentUserIntent(currentUserIntent === 'blockMenuOpen' ? 'default' : currentUserIntent || 'default')({
90
100
  tr: tr
91
101
  });
92
102
  return tr;
93
103
  });
94
104
  };
95
- if (!menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !hasSelection || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
105
+ if (!menuTriggerBy || !isSelectedViaDragHandle || !hasFocus || !shouldShowBlockMenuForEmptyLine || ['resizing', 'dragging'].includes(currentUserIntent || '')) {
96
106
  closeMenu();
97
107
  return null;
98
108
  }
@@ -1,3 +1,6 @@
1
+ import _defineProperty from "@babel/runtime/helpers/defineProperty";
2
+ function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
3
+ function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
1
4
  import React from 'react';
2
5
  import { injectIntl, useIntl } from 'react-intl-next';
3
6
  import { messages } from '@atlaskit/editor-common/block-menu';
@@ -73,10 +76,15 @@ var CopyBlockMenuItem = function CopyBlockMenuItem(_ref) {
73
76
  // When nodeType.inlineContent is true, it will be treated as an inline node in the copyDomNode function,
74
77
  // but we want to treat it as a block node when copying, hence setting it to false here
75
78
  if (selection.node.type.name === 'codeBlock') {
76
- _nodeType.inlineContent = false;
79
+ var codeBlockNodeType = _objectSpread(_objectSpread({}, _nodeType), {}, {
80
+ inlineContent: false
81
+ });
82
+ var _domNode2 = toDOM(selection.node, schema);
83
+ copyDomNode(_domNode2, codeBlockNodeType, selection);
84
+ } else {
85
+ var _domNode3 = toDOM(selection.node, schema);
86
+ copyDomNode(_domNode3, _nodeType, selection);
77
87
  }
78
- var _domNode2 = toDOM(selection.node, schema);
79
- copyDomNode(_domNode2, _nodeType, selection);
80
88
  }
81
89
 
82
90
  // close the block menu after copying
@@ -1,12 +1,19 @@
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 var CopySection = function CopySection(_ref) {
6
+ var _api$selection;
5
7
  var api = _ref.api,
6
8
  children = _ref.children;
7
9
  var isFormatMenuHidden = useCallback(function () {
8
10
  return checkIsFormatMenuHidden(api);
9
11
  }, [api]);
12
+ 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;
13
+ var isEmptyLineSelected = !!(selection !== null && selection !== void 0 && selection.empty) && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
14
+ if (isEmptyLineSelected) {
15
+ return null;
16
+ }
10
17
  return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
11
18
  hasSeparator: !isFormatMenuHidden()
12
19
  }, children);
@@ -0,0 +1,16 @@
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 var DeleteSection = function DeleteSection(_ref) {
5
+ var _api$selection;
6
+ var api = _ref.api,
7
+ children = _ref.children;
8
+ 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;
9
+ var isEmptyLineSelected = !!(selection !== null && selection !== void 0 && selection.empty) && expValEqualsNoExposure('platform_editor_block_menu_empty_line', 'isEnabled', true);
10
+ if (isEmptyLineSelected) {
11
+ return null;
12
+ }
13
+ return /*#__PURE__*/React.createElement(ToolbarDropdownItemSection, {
14
+ hasSeparator: true
15
+ }, children);
16
+ };
@@ -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
  var getIsFormatMenuHidden = function getIsFormatMenuHidden(selection, schema, menuTriggerBy) {
5
6
  var nodes = schema.nodes;
@@ -26,6 +27,39 @@ var getIsFormatMenuHidden = function getIsFormatMenuHidden(selection, schema, me
26
27
  var isNested = isNestedNode(selection, menuTriggerBy);
27
28
  return !content || isNested;
28
29
  };
30
+ var getIsFormatMenuHiddenEmptyLine = function getIsFormatMenuHiddenEmptyLine(selection, schema, menuTriggerBy) {
31
+ var nodes = schema.nodes;
32
+ if (!nodes) {
33
+ return false;
34
+ }
35
+ var 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
+ var content;
44
+ var 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
+ var selectedNode = findSelectedNodeOfType(allowedNodes)(selection);
52
+ if (selectedNode) {
53
+ content = selectedNode.node;
54
+ } else {
55
+ var 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 var checkIsFormatMenuHidden = function checkIsFormatMenuHidden(api) {
30
64
  var _api$selection, _api$core$sharedState, _api$blockControls;
31
65
  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;
@@ -34,5 +68,5 @@ export var checkIsFormatMenuHidden = function 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
  };
@@ -19,6 +19,7 @@ export type BlockMenuPlugin = NextEditorPlugin<'blockMenu', {
19
19
  OptionalPlugin<DecorationsPlugin>
20
20
  ];
21
21
  pluginConfiguration?: BlockMenuPluginOptions;
22
+ sharedState: BlockMenuSharedState;
22
23
  }>;
23
24
  export type BlockMenuPluginOptions = {
24
25
  /**
@@ -31,6 +32,13 @@ export type BlockMenuPluginOptions = {
31
32
  */
32
33
  getLinkPath?: () => string | null;
33
34
  };
35
+ export type BlockMenuSharedState = {
36
+ /**
37
+ * The name of the currently selected node type that triggered the block menu
38
+ * This exposes the menuTriggerBy value from blockControls plugin
39
+ */
40
+ currentSelectedNodeName: string | undefined;
41
+ } | undefined;
34
42
  type WithRank<T> = T & {
35
43
  rank: number;
36
44
  };
@@ -1,3 +1,6 @@
1
1
  import type { TransformContext } from '@atlaskit/editor-common/transforms';
2
+ import type { Node as PMNode } from '@atlaskit/editor-prosemirror/model';
3
+ import { type Schema } from '@atlaskit/editor-prosemirror/model';
4
+ export declare const createDefaultLayoutSection: (schema: Schema, content: PMNode) => PMNode;
2
5
  export declare const convertToLayout: (context: TransformContext) => import("prosemirror-state").Transaction;
3
6
  export declare const transformLayoutNode: (context: TransformContext) => import("prosemirror-state").Transaction;
@@ -1,2 +1,2 @@
1
1
  export { blockMenuPlugin } from './blockMenuPlugin';
2
- export type { BlockMenuPlugin, RegisterBlockMenuComponent, Parent, BlockMenuPluginOptions, } from './blockMenuPluginType';
2
+ export type { BlockMenuPlugin, RegisterBlockMenuComponent, Parent, BlockMenuPluginOptions, BlockMenuSharedState, } from './blockMenuPluginType';