@atlaskit/editor-plugin-paste-options-toolbar 11.2.2 → 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.
- package/CHANGELOG.md +18 -0
- package/dist/cjs/pasteOptionsToolbarPlugin.js +51 -13
- package/dist/cjs/ui/utils/paste-menu-rules/hasMixedNodes.js +76 -0
- package/dist/cjs/ui/utils/paste-menu-rules/isNotSingleLink.js +117 -0
- package/dist/cjs/ui/utils/paste-menu-rules/rules.js +82 -1
- package/dist/es2019/pasteOptionsToolbarPlugin.js +49 -13
- package/dist/es2019/ui/utils/paste-menu-rules/hasMixedNodes.js +70 -0
- package/dist/es2019/ui/utils/paste-menu-rules/isNotSingleLink.js +111 -0
- package/dist/es2019/ui/utils/paste-menu-rules/rules.js +72 -1
- package/dist/esm/pasteOptionsToolbarPlugin.js +51 -13
- package/dist/esm/ui/utils/paste-menu-rules/hasMixedNodes.js +70 -0
- package/dist/esm/ui/utils/paste-menu-rules/isNotSingleLink.js +111 -0
- package/dist/esm/ui/utils/paste-menu-rules/rules.js +82 -1
- package/dist/types/pasteOptionsToolbarPluginType.d.ts +17 -0
- package/dist/types/ui/utils/paste-menu-rules/hasMixedNodes.d.ts +10 -0
- package/dist/types/ui/utils/paste-menu-rules/isNotSingleLink.d.ts +17 -0
- package/dist/types/ui/utils/paste-menu-rules/types.d.ts +39 -0
- package/dist/types-ts4.5/pasteOptionsToolbarPluginType.d.ts +17 -0
- package/dist/types-ts4.5/ui/utils/paste-menu-rules/hasMixedNodes.d.ts +10 -0
- package/dist/types-ts4.5/ui/utils/paste-menu-rules/isNotSingleLink.d.ts +17 -0
- package/dist/types-ts4.5/ui/utils/paste-menu-rules/types.d.ts +39 -0
- package/package.json +8 -6
|
@@ -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
|
-
|
|
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$
|
|
31
|
-
var pasteOptsState = api === null || api === void 0 || (_api$
|
|
32
|
-
var pasteState = api === null || api === void 0 || (_api$
|
|
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$
|
|
36
|
-
return (_pasteOptsState$
|
|
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$
|
|
40
|
-
return (_pasteOptsState$
|
|
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$
|
|
44
|
-
return (_pasteState$
|
|
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$
|
|
48
|
-
return pasteState === null || pasteState === void 0 || (_pasteState$
|
|
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$
|
|
55
|
-
return pasteState === null || pasteState === void 0 || (_pasteState$
|
|
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
|
-
|
|
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;
|