@atlaskit/editor-plugin-paste-options-toolbar 11.2.3 → 11.3.0

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.
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Returns true if the given text node's sole mark is a `link` mark whose href
3
+ * matches the node's text content exactly (i.e. the link label IS the URL).
4
+ */
5
+ const isUrlOnlyLinkTextNode = node => {
6
+ var _node$marks$0$attrs$h, _node$marks$0$attrs;
7
+ if (!node.isText) {
8
+ return false;
9
+ }
10
+ if (node.marks.length !== 1 || node.marks[0].type.name !== 'link') {
11
+ return false;
12
+ }
13
+ const href = (_node$marks$0$attrs$h = (_node$marks$0$attrs = node.marks[0].attrs) === null || _node$marks$0$attrs === void 0 ? void 0 : _node$marks$0$attrs.href) !== null && _node$marks$0$attrs$h !== void 0 ? _node$marks$0$attrs$h : '';
14
+ return node.text === href;
15
+ };
16
+
17
+ /**
18
+ * Returns true if the slice represents a single inline card (smartlink) node.
19
+ * Handles two shapes:
20
+ * - paragraph > inlineCard (smartlink from editor/renderer, wrapped in a paragraph)
21
+ * - inlineCard (top-level inlineCard with no paragraph wrapper)
22
+ */
23
+ const isSingleInlineCard = slice => {
24
+ if (slice.content.childCount !== 1) {
25
+ return false;
26
+ }
27
+ const topNode = slice.content.child(0);
28
+
29
+ // Top-level inlineCard (no paragraph wrapper)
30
+ if (topNode.type.name === 'inlineCard') {
31
+ return true;
32
+ }
33
+
34
+ // paragraph > inlineCard
35
+ if (topNode.type.name !== 'paragraph') {
36
+ return false;
37
+ }
38
+ if (topNode.childCount !== 1) {
39
+ return false;
40
+ }
41
+ return topNode.child(0).type.name === 'inlineCard';
42
+ };
43
+
44
+ /**
45
+ * Returns the children of a Fragment, filtering out whitespace-only text nodes.
46
+ * This handles trailing/leading spaces that browsers sometimes include when
47
+ * copying a link (e.g. `<a href="...">URL</a> `).
48
+ */
49
+ const significantChildren = fragment => {
50
+ const children = [];
51
+ fragment.forEach(child => {
52
+ var _child$text;
53
+ if (child.isText && ((_child$text = child.text) === null || _child$text === void 0 ? void 0 : _child$text.trim()) === '') {
54
+ return;
55
+ }
56
+ children.push(child);
57
+ });
58
+ return children;
59
+ };
60
+
61
+ /**
62
+ * Returns true if the slice represents a single bare link with no label.
63
+ * Handles two shapes:
64
+ * - paragraph > text(link mark) (standard rich-text paste, wrapped in a paragraph)
65
+ * - text(link mark) (top-level text node with no paragraph wrapper)
66
+ * Whitespace-only sibling text nodes are ignored in both cases.
67
+ */
68
+ const isSingleBareLink = slice => {
69
+ // Top-level text node with a link mark (no paragraph wrapper)
70
+ const significantTopChildren = significantChildren(slice.content);
71
+ if (significantTopChildren.length === 1 && isUrlOnlyLinkTextNode(significantTopChildren[0])) {
72
+ return true;
73
+ }
74
+
75
+ // paragraph > text(link mark)
76
+ if (slice.content.childCount !== 1) {
77
+ return false;
78
+ }
79
+ const topNode = slice.content.child(0);
80
+ if (topNode.type.name !== 'paragraph') {
81
+ return false;
82
+ }
83
+ const children = significantChildren(topNode.content);
84
+ if (children.length !== 1) {
85
+ return false;
86
+ }
87
+ return isUrlOnlyLinkTextNode(children[0]);
88
+ };
89
+
90
+ /**
91
+ * Returns `true` when the pasted content is NOT a single standalone link.
92
+ *
93
+ * A paste is considered a "single link" (returns `false`) when:
94
+ * - The slice contains exactly one paragraph with one text node whose text
95
+ * equals its `link` mark href (bare URL link, no custom label), OR
96
+ * - The slice contains exactly one paragraph with a single `inlineCard` node
97
+ * (smartlink from the renderer or editor).
98
+ *
99
+ * Returns `true` (not a single link) when:
100
+ * - The pasted link has a custom label (text ≠ href)
101
+ * - There are multiple paragraphs or sibling nodes
102
+ * - There is additional text alongside the link in the same paragraph
103
+ * - The slice is absent (plain-text paste)
104
+ */
105
+ export const isNotSingleLink = slice => {
106
+ if (!slice || !slice.content.size) {
107
+ // No rich-text slice → plain text paste, not a single link in the relevant sense
108
+ return true;
109
+ }
110
+ return !isSingleBareLink(slice) && !isSingleInlineCard(slice);
111
+ };
@@ -1,11 +1,19 @@
1
+ import { hasMixedNodes } from './hasMixedNodes';
1
2
  import { hasTableNode } from './hasTableNode';
2
3
  import { isNotProse } from './isNotProse';
4
+ import { isNotSingleLink } from './isNotSingleLink';
3
5
  /**
4
6
  * Returns a rule that hides the paste-menu button when the pasted plain-text
5
7
  * is shorter than `minChars` characters.
6
8
  */
7
9
  const minCharsRule = minChars => context => context.getPlaintextLength() < minChars;
8
10
 
11
+ /**
12
+ * Returns a rule that hides the paste-menu button when the pasted plain-text
13
+ * is longer than `maxChars` characters.
14
+ */
15
+ const maxCharsRule = maxChars => context => context.getPlaintextLength() > maxChars;
16
+
9
17
  /**
10
18
  * A rule that hides the paste-menu button when the pasted content is plain
11
19
  * text (i.e. not rich-text / prose).
@@ -29,6 +37,63 @@ const containsTableRule = context => hasTableNode(context.getPastedSlice());
29
37
  */
30
38
  const excludedAncestorRule = excludedNames => context => context.getAncestorNodeNames().some(name => excludedNames.includes(name));
31
39
 
40
+ /**
41
+ * Returns a rule that hides the paste-menu button when the pasted content contains
42
+ * more than one node type (e.g. a mix of paragraphs and headings, or a table
43
+ * with multiple cell types).
44
+ */
45
+ const hideIfMixedNodesRule = context => {
46
+ const slice = context.getPastedSlice();
47
+ if (!slice) {
48
+ return false;
49
+ }
50
+ return hasMixedNodes(slice);
51
+ };
52
+
53
+ /**
54
+ * Returns a rule that hides the paste-menu button when the pasted content contains
55
+ * only a single node type (e.g. a single paragraph, or a table with only one cell type).
56
+ * This is the inverse of `hideIfMixedNodesRule` and can be used in combination with it
57
+ * to target pastes that contain either exactly one node type or more than one node type.
58
+ */
59
+ const hideIfSingleNodeRule = context => {
60
+ const slice = context.getPastedSlice();
61
+ if (!slice) {
62
+ return false;
63
+ }
64
+ return !hasMixedNodes(slice);
65
+ };
66
+
67
+ /**
68
+ * A rule that hides the paste-menu button when the paste source is NOT an
69
+ * external application (i.e. the content was pasted from within the Fabric
70
+ * editor or renderer rather than from a third-party source).
71
+ */
72
+ const notExternalPasteRule = context => {
73
+ const source = context.getPasteSource();
74
+ return source === 'fabric-editor' || source === 'fabric-renderer';
75
+ };
76
+
77
+ /**
78
+ * A rule that hides the paste-menu button when the pasted content is a single
79
+ * standalone link — i.e. a bare URL link (text equals href) alone in a
80
+ * paragraph, or a single inline card (smartlink) alone in a paragraph.
81
+ *
82
+ * Returns `true` (hidden) when the paste is NOT a single link — e.g. a link
83
+ * with a custom label, a link accompanied by other text or sibling nodes, or
84
+ * multiple paragraphs.
85
+ */
86
+ const notSingleLinkRule = context => isNotSingleLink(context.getPastedSlice());
87
+
88
+ /**
89
+ * A rule that hides the paste-menu button when the pasted content IS a single
90
+ * standalone link (the inverse of `notSingleLinkRule`).
91
+ *
92
+ * Use this to suppress buttons (e.g. Improve Writing, Fix Spelling) that
93
+ * should not appear for single-link pastes.
94
+ */
95
+ const isSingleLinkRule = context => !isNotSingleLink(context.getPastedSlice());
96
+
32
97
  /**
33
98
  * Combines multiple context-aware rules with short-circuit evaluation.
34
99
  * Returns `true` (hidden) as soon as the first rule returns `true`; returns
@@ -61,6 +126,12 @@ export const createPasteMenuRuleFactories = getContext => ({
61
126
  allRules: (...rules) => bind(getContext, allRules(...rules)),
62
127
  containsTableRule: bind(getContext, containsTableRule),
63
128
  excludedAncestorRule: excludedNames => bind(getContext, excludedAncestorRule(excludedNames)),
129
+ hideIfMixedNodesRule: bind(getContext, hideIfMixedNodesRule),
130
+ hideIfSingleNodeRule: bind(getContext, hideIfSingleNodeRule),
131
+ isSingleLinkRule: bind(getContext, isSingleLinkRule),
132
+ maxCharsRule: maxChars => bind(getContext, maxCharsRule(maxChars)),
64
133
  minCharsRule: minChars => bind(getContext, minCharsRule(minChars)),
65
- notProseRule: bind(getContext, notProseRule)
134
+ notExternalPasteRule: bind(getContext, notExternalPasteRule),
135
+ notProseRule: bind(getContext, notProseRule),
136
+ notSingleLinkRule: bind(getContext, notSingleLinkRule)
66
137
  });
@@ -22,37 +22,75 @@ export var pasteOptionsToolbarPlugin = function pasteOptionsToolbarPlugin(_ref)
22
22
  api: api
23
23
  }));
24
24
  }
25
+ if (config !== null && config !== void 0 && config.pasteMenuButtonsFactory && config !== null && config !== void 0 && config.usePopupBasedPasteActionsMenu) {
26
+ var _api$uiControlRegistr2;
27
+ var rules = function () {
28
+ var getContext = function getContext() {
29
+ var _api$pasteOptionsTool, _api$paste;
30
+ var pasteOptsState = api === null || api === void 0 || (_api$pasteOptionsTool = api.pasteOptionsToolbarPlugin) === null || _api$pasteOptionsTool === void 0 ? void 0 : _api$pasteOptionsTool.sharedState.currentState();
31
+ var pasteState = api === null || api === void 0 || (_api$paste = api.paste) === null || _api$paste === void 0 ? void 0 : _api$paste.sharedState.currentState();
32
+ return {
33
+ getPlaintextLength: function getPlaintextLength() {
34
+ var _pasteOptsState$plain;
35
+ return (_pasteOptsState$plain = pasteOptsState === null || pasteOptsState === void 0 ? void 0 : pasteOptsState.plaintextLength) !== null && _pasteOptsState$plain !== void 0 ? _pasteOptsState$plain : 0;
36
+ },
37
+ getAncestorNodeNames: function getAncestorNodeNames() {
38
+ var _pasteOptsState$paste;
39
+ return (_pasteOptsState$paste = pasteOptsState === null || pasteOptsState === void 0 ? void 0 : pasteOptsState.pasteAncestorNodeNames) !== null && _pasteOptsState$paste !== void 0 ? _pasteOptsState$paste : [];
40
+ },
41
+ getPastedText: function getPastedText() {
42
+ var _pasteState$lastConte, _pasteState$lastConte2;
43
+ return (_pasteState$lastConte = pasteState === null || pasteState === void 0 || (_pasteState$lastConte2 = pasteState.lastContentPasted) === null || _pasteState$lastConte2 === void 0 ? void 0 : _pasteState$lastConte2.text) !== null && _pasteState$lastConte !== void 0 ? _pasteState$lastConte : '';
44
+ },
45
+ getPastedSlice: function getPastedSlice() {
46
+ var _pasteState$lastConte3;
47
+ return pasteState === null || pasteState === void 0 || (_pasteState$lastConte3 = pasteState.lastContentPasted) === null || _pasteState$lastConte3 === void 0 ? void 0 : _pasteState$lastConte3.pastedSlice;
48
+ },
49
+ getNodeTypes: function getNodeTypes() {
50
+ return [];
51
+ },
52
+ getPasteSource: function getPasteSource() {
53
+ var _pasteState$lastConte4;
54
+ return pasteState === null || pasteState === void 0 || (_pasteState$lastConte4 = pasteState.lastContentPasted) === null || _pasteState$lastConte4 === void 0 ? void 0 : _pasteState$lastConte4.pasteSource;
55
+ }
56
+ };
57
+ };
58
+ return createPasteMenuRuleFactories(getContext);
59
+ }();
60
+ var productButtons = config.pasteMenuButtonsFactory(rules);
61
+ api === null || api === void 0 || (_api$uiControlRegistr2 = api.uiControlRegistry) === null || _api$uiControlRegistr2 === void 0 || _api$uiControlRegistr2.actions.register(productButtons);
62
+ }
25
63
  return {
26
64
  name: 'pasteOptionsToolbarPlugin',
27
65
  actions: {
28
66
  getPasteMenuRules: function getPasteMenuRules() {
29
67
  var getContext = function getContext() {
30
- var _api$pasteOptionsTool, _api$paste;
31
- var pasteOptsState = api === null || api === void 0 || (_api$pasteOptionsTool = api.pasteOptionsToolbarPlugin) === null || _api$pasteOptionsTool === void 0 ? void 0 : _api$pasteOptionsTool.sharedState.currentState();
32
- var pasteState = api === null || api === void 0 || (_api$paste = api.paste) === null || _api$paste === void 0 ? void 0 : _api$paste.sharedState.currentState();
68
+ var _api$pasteOptionsTool2, _api$paste2;
69
+ var pasteOptsState = api === null || api === void 0 || (_api$pasteOptionsTool2 = api.pasteOptionsToolbarPlugin) === null || _api$pasteOptionsTool2 === void 0 ? void 0 : _api$pasteOptionsTool2.sharedState.currentState();
70
+ var pasteState = api === null || api === void 0 || (_api$paste2 = api.paste) === null || _api$paste2 === void 0 ? void 0 : _api$paste2.sharedState.currentState();
33
71
  return {
34
72
  getPlaintextLength: function getPlaintextLength() {
35
- var _pasteOptsState$plain;
36
- return (_pasteOptsState$plain = pasteOptsState === null || pasteOptsState === void 0 ? void 0 : pasteOptsState.plaintextLength) !== null && _pasteOptsState$plain !== void 0 ? _pasteOptsState$plain : 0;
73
+ var _pasteOptsState$plain2;
74
+ return (_pasteOptsState$plain2 = pasteOptsState === null || pasteOptsState === void 0 ? void 0 : pasteOptsState.plaintextLength) !== null && _pasteOptsState$plain2 !== void 0 ? _pasteOptsState$plain2 : 0;
37
75
  },
38
76
  getAncestorNodeNames: function getAncestorNodeNames() {
39
- var _pasteOptsState$paste;
40
- return (_pasteOptsState$paste = pasteOptsState === null || pasteOptsState === void 0 ? void 0 : pasteOptsState.pasteAncestorNodeNames) !== null && _pasteOptsState$paste !== void 0 ? _pasteOptsState$paste : [];
77
+ var _pasteOptsState$paste2;
78
+ return (_pasteOptsState$paste2 = pasteOptsState === null || pasteOptsState === void 0 ? void 0 : pasteOptsState.pasteAncestorNodeNames) !== null && _pasteOptsState$paste2 !== void 0 ? _pasteOptsState$paste2 : [];
41
79
  },
42
80
  getPastedText: function getPastedText() {
43
- var _pasteState$lastConte, _pasteState$lastConte2;
44
- return (_pasteState$lastConte = pasteState === null || pasteState === void 0 || (_pasteState$lastConte2 = pasteState.lastContentPasted) === null || _pasteState$lastConte2 === void 0 ? void 0 : _pasteState$lastConte2.text) !== null && _pasteState$lastConte !== void 0 ? _pasteState$lastConte : '';
81
+ var _pasteState$lastConte5, _pasteState$lastConte6;
82
+ return (_pasteState$lastConte5 = pasteState === null || pasteState === void 0 || (_pasteState$lastConte6 = pasteState.lastContentPasted) === null || _pasteState$lastConte6 === void 0 ? void 0 : _pasteState$lastConte6.text) !== null && _pasteState$lastConte5 !== void 0 ? _pasteState$lastConte5 : '';
45
83
  },
46
84
  getPastedSlice: function getPastedSlice() {
47
- var _pasteState$lastConte3;
48
- return pasteState === null || pasteState === void 0 || (_pasteState$lastConte3 = pasteState.lastContentPasted) === null || _pasteState$lastConte3 === void 0 ? void 0 : _pasteState$lastConte3.pastedSlice;
85
+ var _pasteState$lastConte7;
86
+ return pasteState === null || pasteState === void 0 || (_pasteState$lastConte7 = pasteState.lastContentPasted) === null || _pasteState$lastConte7 === void 0 ? void 0 : _pasteState$lastConte7.pastedSlice;
49
87
  },
50
88
  getNodeTypes: function getNodeTypes() {
51
89
  return [];
52
90
  },
53
91
  getPasteSource: function getPasteSource() {
54
- var _pasteState$lastConte4;
55
- return pasteState === null || pasteState === void 0 || (_pasteState$lastConte4 = pasteState.lastContentPasted) === null || _pasteState$lastConte4 === void 0 ? void 0 : _pasteState$lastConte4.pasteSource;
92
+ var _pasteState$lastConte8;
93
+ return pasteState === null || pasteState === void 0 || (_pasteState$lastConte8 = pasteState.lastContentPasted) === null || _pasteState$lastConte8 === void 0 ? void 0 : _pasteState$lastConte8.pasteSource;
56
94
  }
57
95
  };
58
96
  };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Structural container nodes are nodes whose children are determined by
3
+ * document structure (e.g. list items inside a list, rows inside a table)
4
+ * rather than by content choice. Having different child types inside these
5
+ * nodes does NOT indicate mixed content.
6
+ *
7
+ * All other container nodes (listItem, blockTaskItem, blockquote, panel,
8
+ * expand, layoutColumn, tableCell, tableHeader, bodiedExtension, etc.) hold
9
+ * rich block content, so mixed block children inside them IS mixing.
10
+ */
11
+ var STRUCTURAL_CONTAINERS = new Set(['bulletList', 'orderedList', 'listItem', 'taskList', 'decisionList', 'table', 'tableRow']);
12
+
13
+ /**
14
+ * Returns true if a node's direct block-level children include more than one
15
+ * distinct node type — i.e. the children are "mixed".
16
+ */
17
+ var hasBlockSiblings = function hasBlockSiblings(node) {
18
+ var types = new Set();
19
+ node.forEach(function (child) {
20
+ if (child.isBlock) {
21
+ types.add(child.type.name);
22
+ }
23
+ });
24
+ return types.size > 1;
25
+ };
26
+
27
+ /**
28
+ * Returns true if the slice contains sibling block nodes of different types
29
+ * at the same level anywhere in the document tree.
30
+ *
31
+ * Structural container nodes (bulletList, orderedList, taskList, decisionList,
32
+ * table, tableRow) are excluded from the sibling check because their children
33
+ * are typed by structure, not content. All other container nodes are checked.
34
+ */
35
+ export var hasMixedNodes = function hasMixedNodes(slice) {
36
+ var _slice$content;
37
+ if (!(slice !== null && slice !== void 0 && (_slice$content = slice.content) !== null && _slice$content !== void 0 && _slice$content.size)) {
38
+ return false;
39
+ }
40
+
41
+ // Check the top-level children of the slice (Fragment has no parent Node,
42
+ // so we construct a synthetic check by iterating slice.content directly).
43
+ var topLevelTypes = new Set();
44
+ slice.content.forEach(function (node) {
45
+ if (node.isBlock) {
46
+ topLevelTypes.add(node.type.name);
47
+ }
48
+ });
49
+ if (topLevelTypes.size > 1) {
50
+ return true;
51
+ }
52
+
53
+ // Walk every descendant node and check its direct block children for mixing,
54
+ // skipping structural containers (their children are structural, not content).
55
+ var mixed = false;
56
+ slice.content.descendants(function (node) {
57
+ if (mixed) {
58
+ return false; // short-circuit once found
59
+ }
60
+ if (STRUCTURAL_CONTAINERS.has(node.type.name)) {
61
+ return true; // recurse into but don't check children for mixing
62
+ }
63
+ if (hasBlockSiblings(node)) {
64
+ mixed = true;
65
+ return false;
66
+ }
67
+ return true;
68
+ });
69
+ return mixed;
70
+ };
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Returns true if the given text node's sole mark is a `link` mark whose href
3
+ * matches the node's text content exactly (i.e. the link label IS the URL).
4
+ */
5
+ var isUrlOnlyLinkTextNode = function isUrlOnlyLinkTextNode(node) {
6
+ var _node$marks$0$attrs$h, _node$marks$0$attrs;
7
+ if (!node.isText) {
8
+ return false;
9
+ }
10
+ if (node.marks.length !== 1 || node.marks[0].type.name !== 'link') {
11
+ return false;
12
+ }
13
+ var href = (_node$marks$0$attrs$h = (_node$marks$0$attrs = node.marks[0].attrs) === null || _node$marks$0$attrs === void 0 ? void 0 : _node$marks$0$attrs.href) !== null && _node$marks$0$attrs$h !== void 0 ? _node$marks$0$attrs$h : '';
14
+ return node.text === href;
15
+ };
16
+
17
+ /**
18
+ * Returns true if the slice represents a single inline card (smartlink) node.
19
+ * Handles two shapes:
20
+ * - paragraph > inlineCard (smartlink from editor/renderer, wrapped in a paragraph)
21
+ * - inlineCard (top-level inlineCard with no paragraph wrapper)
22
+ */
23
+ var isSingleInlineCard = function isSingleInlineCard(slice) {
24
+ if (slice.content.childCount !== 1) {
25
+ return false;
26
+ }
27
+ var topNode = slice.content.child(0);
28
+
29
+ // Top-level inlineCard (no paragraph wrapper)
30
+ if (topNode.type.name === 'inlineCard') {
31
+ return true;
32
+ }
33
+
34
+ // paragraph > inlineCard
35
+ if (topNode.type.name !== 'paragraph') {
36
+ return false;
37
+ }
38
+ if (topNode.childCount !== 1) {
39
+ return false;
40
+ }
41
+ return topNode.child(0).type.name === 'inlineCard';
42
+ };
43
+
44
+ /**
45
+ * Returns the children of a Fragment, filtering out whitespace-only text nodes.
46
+ * This handles trailing/leading spaces that browsers sometimes include when
47
+ * copying a link (e.g. `<a href="...">URL</a> `).
48
+ */
49
+ var significantChildren = function significantChildren(fragment) {
50
+ var children = [];
51
+ fragment.forEach(function (child) {
52
+ var _child$text;
53
+ if (child.isText && ((_child$text = child.text) === null || _child$text === void 0 ? void 0 : _child$text.trim()) === '') {
54
+ return;
55
+ }
56
+ children.push(child);
57
+ });
58
+ return children;
59
+ };
60
+
61
+ /**
62
+ * Returns true if the slice represents a single bare link with no label.
63
+ * Handles two shapes:
64
+ * - paragraph > text(link mark) (standard rich-text paste, wrapped in a paragraph)
65
+ * - text(link mark) (top-level text node with no paragraph wrapper)
66
+ * Whitespace-only sibling text nodes are ignored in both cases.
67
+ */
68
+ var isSingleBareLink = function isSingleBareLink(slice) {
69
+ // Top-level text node with a link mark (no paragraph wrapper)
70
+ var significantTopChildren = significantChildren(slice.content);
71
+ if (significantTopChildren.length === 1 && isUrlOnlyLinkTextNode(significantTopChildren[0])) {
72
+ return true;
73
+ }
74
+
75
+ // paragraph > text(link mark)
76
+ if (slice.content.childCount !== 1) {
77
+ return false;
78
+ }
79
+ var topNode = slice.content.child(0);
80
+ if (topNode.type.name !== 'paragraph') {
81
+ return false;
82
+ }
83
+ var children = significantChildren(topNode.content);
84
+ if (children.length !== 1) {
85
+ return false;
86
+ }
87
+ return isUrlOnlyLinkTextNode(children[0]);
88
+ };
89
+
90
+ /**
91
+ * Returns `true` when the pasted content is NOT a single standalone link.
92
+ *
93
+ * A paste is considered a "single link" (returns `false`) when:
94
+ * - The slice contains exactly one paragraph with one text node whose text
95
+ * equals its `link` mark href (bare URL link, no custom label), OR
96
+ * - The slice contains exactly one paragraph with a single `inlineCard` node
97
+ * (smartlink from the renderer or editor).
98
+ *
99
+ * Returns `true` (not a single link) when:
100
+ * - The pasted link has a custom label (text ≠ href)
101
+ * - There are multiple paragraphs or sibling nodes
102
+ * - There is additional text alongside the link in the same paragraph
103
+ * - The slice is absent (plain-text paste)
104
+ */
105
+ export var isNotSingleLink = function isNotSingleLink(slice) {
106
+ if (!slice || !slice.content.size) {
107
+ // No rich-text slice → plain text paste, not a single link in the relevant sense
108
+ return true;
109
+ }
110
+ return !isSingleBareLink(slice) && !isSingleInlineCard(slice);
111
+ };
@@ -1,5 +1,7 @@
1
+ import { hasMixedNodes } from './hasMixedNodes';
1
2
  import { hasTableNode } from './hasTableNode';
2
3
  import { isNotProse } from './isNotProse';
4
+ import { isNotSingleLink } from './isNotSingleLink';
3
5
  /**
4
6
  * Returns a rule that hides the paste-menu button when the pasted plain-text
5
7
  * is shorter than `minChars` characters.
@@ -10,6 +12,16 @@ var _minCharsRule = function minCharsRule(minChars) {
10
12
  };
11
13
  };
12
14
 
15
+ /**
16
+ * Returns a rule that hides the paste-menu button when the pasted plain-text
17
+ * is longer than `maxChars` characters.
18
+ */
19
+ var _maxCharsRule = function maxCharsRule(maxChars) {
20
+ return function (context) {
21
+ return context.getPlaintextLength() > maxChars;
22
+ };
23
+ };
24
+
13
25
  /**
14
26
  * A rule that hides the paste-menu button when the pasted content is plain
15
27
  * text (i.e. not rich-text / prose).
@@ -41,6 +53,67 @@ var _excludedAncestorRule = function excludedAncestorRule(excludedNames) {
41
53
  };
42
54
  };
43
55
 
56
+ /**
57
+ * Returns a rule that hides the paste-menu button when the pasted content contains
58
+ * more than one node type (e.g. a mix of paragraphs and headings, or a table
59
+ * with multiple cell types).
60
+ */
61
+ var hideIfMixedNodesRule = function hideIfMixedNodesRule(context) {
62
+ var slice = context.getPastedSlice();
63
+ if (!slice) {
64
+ return false;
65
+ }
66
+ return hasMixedNodes(slice);
67
+ };
68
+
69
+ /**
70
+ * Returns a rule that hides the paste-menu button when the pasted content contains
71
+ * only a single node type (e.g. a single paragraph, or a table with only one cell type).
72
+ * This is the inverse of `hideIfMixedNodesRule` and can be used in combination with it
73
+ * to target pastes that contain either exactly one node type or more than one node type.
74
+ */
75
+ var hideIfSingleNodeRule = function hideIfSingleNodeRule(context) {
76
+ var slice = context.getPastedSlice();
77
+ if (!slice) {
78
+ return false;
79
+ }
80
+ return !hasMixedNodes(slice);
81
+ };
82
+
83
+ /**
84
+ * A rule that hides the paste-menu button when the paste source is NOT an
85
+ * external application (i.e. the content was pasted from within the Fabric
86
+ * editor or renderer rather than from a third-party source).
87
+ */
88
+ var notExternalPasteRule = function notExternalPasteRule(context) {
89
+ var source = context.getPasteSource();
90
+ return source === 'fabric-editor' || source === 'fabric-renderer';
91
+ };
92
+
93
+ /**
94
+ * A rule that hides the paste-menu button when the pasted content is a single
95
+ * standalone link — i.e. a bare URL link (text equals href) alone in a
96
+ * paragraph, or a single inline card (smartlink) alone in a paragraph.
97
+ *
98
+ * Returns `true` (hidden) when the paste is NOT a single link — e.g. a link
99
+ * with a custom label, a link accompanied by other text or sibling nodes, or
100
+ * multiple paragraphs.
101
+ */
102
+ var notSingleLinkRule = function notSingleLinkRule(context) {
103
+ return isNotSingleLink(context.getPastedSlice());
104
+ };
105
+
106
+ /**
107
+ * A rule that hides the paste-menu button when the pasted content IS a single
108
+ * standalone link (the inverse of `notSingleLinkRule`).
109
+ *
110
+ * Use this to suppress buttons (e.g. Improve Writing, Fix Spelling) that
111
+ * should not appear for single-link pastes.
112
+ */
113
+ var isSingleLinkRule = function isSingleLinkRule(context) {
114
+ return !isNotSingleLink(context.getPastedSlice());
115
+ };
116
+
44
117
  /**
45
118
  * Combines multiple context-aware rules with short-circuit evaluation.
46
119
  * Returns `true` (hidden) as soon as the first rule returns `true`; returns
@@ -88,9 +161,17 @@ export var createPasteMenuRuleFactories = function createPasteMenuRuleFactories(
88
161
  excludedAncestorRule: function excludedAncestorRule(excludedNames) {
89
162
  return bind(getContext, _excludedAncestorRule(excludedNames));
90
163
  },
164
+ hideIfMixedNodesRule: bind(getContext, hideIfMixedNodesRule),
165
+ hideIfSingleNodeRule: bind(getContext, hideIfSingleNodeRule),
166
+ isSingleLinkRule: bind(getContext, isSingleLinkRule),
167
+ maxCharsRule: function maxCharsRule(maxChars) {
168
+ return bind(getContext, _maxCharsRule(maxChars));
169
+ },
91
170
  minCharsRule: function minCharsRule(minChars) {
92
171
  return bind(getContext, _minCharsRule(minChars));
93
172
  },
94
- notProseRule: bind(getContext, notProseRule)
173
+ notExternalPasteRule: bind(getContext, notExternalPasteRule),
174
+ notProseRule: bind(getContext, notProseRule),
175
+ notSingleLinkRule: bind(getContext, notSingleLinkRule)
95
176
  };
96
177
  };
@@ -2,6 +2,7 @@ import type { NextEditorPlugin, OptionalPlugin } from '@atlaskit/editor-common/t
2
2
  import type { AnalyticsPlugin } from '@atlaskit/editor-plugin-analytics';
3
3
  import type { PastePlugin } from '@atlaskit/editor-plugin-paste';
4
4
  import type { UiControlRegistryPlugin } from '@atlaskit/editor-plugin-ui-control-registry';
5
+ import type { RegisterComponent } from '@atlaskit/editor-ui-control-model';
5
6
  import type { ToolbarDropdownOption } from './types/types';
6
7
  import type { PasteMenuRuleFactories } from './ui/utils/paste-menu-rules/types';
7
8
  export type PasteOptionsToolbarPluginDependencies = [
@@ -35,6 +36,22 @@ export type PasteOptionsToolbarPlugin = NextEditorPlugin<'pasteOptionsToolbarPlu
35
36
  };
36
37
  dependencies: PasteOptionsToolbarPluginDependencies;
37
38
  pluginConfiguration?: {
39
+ /**
40
+ * Optional factory for composing product-specific paste menu buttons.
41
+ * Called with the pre-bound rule factories so products can compose
42
+ * `isHidden` callbacks before plugin setup.
43
+ *
44
+ * @example
45
+ * pasteMenuButtonsFactory: (rules) => [
46
+ * {
47
+ * type: 'menu-item',
48
+ * key: 'my-product-button',
49
+ * isHidden: rules.allRules(rules.notProseRule, rules.minCharsRule(100)),
50
+ * component: () => <MyProductButton />,
51
+ * },
52
+ * ]
53
+ */
54
+ pasteMenuButtonsFactory?: (rules: PasteMenuRuleFactories) => RegisterComponent[];
38
55
  usePopupBasedPasteActionsMenu?: boolean;
39
56
  };
40
57
  sharedState: PasteOptionsToolbarSharedState;
@@ -0,0 +1,10 @@
1
+ import type { Slice } from '@atlaskit/editor-prosemirror/model';
2
+ /**
3
+ * Returns true if the slice contains sibling block nodes of different types
4
+ * at the same level anywhere in the document tree.
5
+ *
6
+ * Structural container nodes (bulletList, orderedList, taskList, decisionList,
7
+ * table, tableRow) are excluded from the sibling check because their children
8
+ * are typed by structure, not content. All other container nodes are checked.
9
+ */
10
+ export declare const hasMixedNodes: (slice: Slice | undefined) => boolean;
@@ -0,0 +1,17 @@
1
+ import type { Slice } from '@atlaskit/editor-prosemirror/model';
2
+ /**
3
+ * Returns `true` when the pasted content is NOT a single standalone link.
4
+ *
5
+ * A paste is considered a "single link" (returns `false`) when:
6
+ * - The slice contains exactly one paragraph with one text node whose text
7
+ * equals its `link` mark href (bare URL link, no custom label), OR
8
+ * - The slice contains exactly one paragraph with a single `inlineCard` node
9
+ * (smartlink from the renderer or editor).
10
+ *
11
+ * Returns `true` (not a single link) when:
12
+ * - The pasted link has a custom label (text ≠ href)
13
+ * - There are multiple paragraphs or sibling nodes
14
+ * - There is additional text alongside the link in the same paragraph
15
+ * - The slice is absent (plain-text paste)
16
+ */
17
+ export declare const isNotSingleLink: (slice: Slice | undefined) => boolean;