@atlaskit/editor-plugin-autocomplete 4.1.2 → 4.2.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 +19 -0
- package/dist/cjs/pm-plugins/autocomplete-plugin.js +42 -16
- package/dist/cjs/pm-plugins/ghost-text-decoration.js +4 -1
- package/dist/es2019/pm-plugins/autocomplete-plugin.js +42 -16
- package/dist/es2019/pm-plugins/ghost-text-decoration.js +4 -1
- package/dist/esm/pm-plugins/autocomplete-plugin.js +42 -16
- package/dist/esm/pm-plugins/ghost-text-decoration.js +4 -1
- package/package.json +2 -2
- package/src/pm-plugins/autocomplete-plugin.ts +52 -19
- package/src/pm-plugins/ghost-text-decoration.ts +4 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @atlaskit/editor-plugin-autocomplete
|
|
2
2
|
|
|
3
|
+
## 4.2.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [`268f447fd26f4`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/268f447fd26f4) -
|
|
8
|
+
Fix autocomplete click interactions in the editor
|
|
9
|
+
|
|
10
|
+
### Patch Changes
|
|
11
|
+
|
|
12
|
+
- Updated dependencies
|
|
13
|
+
|
|
14
|
+
## 4.1.3
|
|
15
|
+
|
|
16
|
+
### Patch Changes
|
|
17
|
+
|
|
18
|
+
- [`5cd6c10a9394f`](https://bitbucket.org/atlassian/atlassian-frontend-monorepo/commits/5cd6c10a9394f) -
|
|
19
|
+
Fix autocomplete context extraction when inline formatting inserts leaf nodes before the cursor,
|
|
20
|
+
preventing misaligned ghost-text suggestions that can surface as stray spaces.
|
|
21
|
+
|
|
3
22
|
## 4.1.2
|
|
4
23
|
|
|
5
24
|
### Patch Changes
|
|
@@ -38,6 +38,7 @@ var MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
38
38
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
39
39
|
// on every word boundary for the plugin's lifetime.
|
|
40
40
|
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
41
|
+
var INLINE_LEAF_TEXT = ' ';
|
|
41
42
|
|
|
42
43
|
// eslint-disable-next-line require-unicode-regexp
|
|
43
44
|
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
@@ -63,7 +64,8 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
|
|
|
63
64
|
var maxChars = 200;
|
|
64
65
|
var blockNode = $from.parent;
|
|
65
66
|
var offsetInBlock = $from.parentOffset;
|
|
66
|
-
|
|
67
|
+
// PM offsets count inline leaf nodes; textContent indices do not.
|
|
68
|
+
var blockText = blockNode.textBetween(0, offsetInBlock, undefined, INLINE_LEAF_TEXT);
|
|
67
69
|
if (blockText.length >= maxChars) {
|
|
68
70
|
return blockText.slice(-maxChars);
|
|
69
71
|
}
|
|
@@ -76,7 +78,7 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
|
|
|
76
78
|
var indexInParent = $from.index(depth);
|
|
77
79
|
for (var i = indexInParent - 1; i >= 0 && fullText.length < maxChars; i--) {
|
|
78
80
|
var sibling = parentNode.child(i);
|
|
79
|
-
var siblingText = sibling.
|
|
81
|
+
var siblingText = sibling.textBetween(0, sibling.content.size, '\n', INLINE_LEAF_TEXT);
|
|
80
82
|
fullText = siblingText + '\n' + fullText;
|
|
81
83
|
}
|
|
82
84
|
depth--;
|
|
@@ -167,6 +169,16 @@ var acceptGhostText = function acceptGhostText(state, dispatch) {
|
|
|
167
169
|
}
|
|
168
170
|
return true;
|
|
169
171
|
};
|
|
172
|
+
var acceptGhostTextWithAnalytics = function acceptGhostTextWithAnalytics(state, dispatch, onAccepted) {
|
|
173
|
+
var _pluginState$ghostTex;
|
|
174
|
+
var pluginState = autocompletePluginKey.getState(state);
|
|
175
|
+
var ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
|
|
176
|
+
var accepted = acceptGhostText(state, dispatch);
|
|
177
|
+
if (accepted) {
|
|
178
|
+
onAccepted(ghostText);
|
|
179
|
+
}
|
|
180
|
+
return accepted;
|
|
181
|
+
};
|
|
170
182
|
|
|
171
183
|
/**
|
|
172
184
|
* Context provided to the autocomplete plugin on first editor focus.
|
|
@@ -607,28 +619,18 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
607
619
|
},
|
|
608
620
|
handleKeyDown: (0, _keymap.keydownHandler)({
|
|
609
621
|
Tab: function Tab(state, dispatch) {
|
|
610
|
-
|
|
611
|
-
var pluginState = autocompletePluginKey.getState(state);
|
|
612
|
-
var ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
|
|
613
|
-
var accepted = acceptGhostText(state, dispatch);
|
|
614
|
-
if (accepted) {
|
|
622
|
+
return acceptGhostTextWithAnalytics(state, dispatch, function (ghostText) {
|
|
615
623
|
justAccepted = true;
|
|
616
624
|
lastShownGhostText = '';
|
|
617
625
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
618
|
-
}
|
|
619
|
-
return accepted;
|
|
626
|
+
});
|
|
620
627
|
},
|
|
621
628
|
ArrowRight: function ArrowRight(state, dispatch) {
|
|
622
|
-
|
|
623
|
-
var pluginState = autocompletePluginKey.getState(state);
|
|
624
|
-
var ghostText = (_pluginState$ghostTex2 = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex2 !== void 0 ? _pluginState$ghostTex2 : '';
|
|
625
|
-
var accepted = acceptGhostText(state, dispatch);
|
|
626
|
-
if (accepted) {
|
|
629
|
+
return acceptGhostTextWithAnalytics(state, dispatch, function (ghostText) {
|
|
627
630
|
justAccepted = true;
|
|
628
631
|
lastShownGhostText = '';
|
|
629
632
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
630
|
-
}
|
|
631
|
-
return accepted;
|
|
633
|
+
});
|
|
632
634
|
},
|
|
633
635
|
Escape: function Escape(state, dispatch) {
|
|
634
636
|
var didClear = clearGhostText(state, dispatch);
|
|
@@ -651,6 +653,30 @@ var createAutocompletePlugin = exports.createAutocompletePlugin = function creat
|
|
|
651
653
|
}
|
|
652
654
|
return false;
|
|
653
655
|
},
|
|
656
|
+
mousedown: function mousedown(view, event) {
|
|
657
|
+
if (!isAutocompleteEnabled || !(event.target instanceof Element)) {
|
|
658
|
+
return false;
|
|
659
|
+
}
|
|
660
|
+
if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
|
|
661
|
+
var didClear = clearGhostText(view.state, view.dispatch);
|
|
662
|
+
if (didClear) {
|
|
663
|
+
dismissedContext = getTextBeforeCursor(view.state);
|
|
664
|
+
lastShownGhostText = '';
|
|
665
|
+
fireSuggestionDismissedAnalytics('click');
|
|
666
|
+
}
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
event.preventDefault();
|
|
670
|
+
var accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, function (ghostText) {
|
|
671
|
+
justAccepted = true;
|
|
672
|
+
lastShownGhostText = '';
|
|
673
|
+
fireSuggestionInsertedAnalytics(ghostText);
|
|
674
|
+
});
|
|
675
|
+
if (accepted) {
|
|
676
|
+
view.focus();
|
|
677
|
+
}
|
|
678
|
+
return accepted;
|
|
679
|
+
},
|
|
654
680
|
focus: function focus() {
|
|
655
681
|
var _options$useLocalMode, _options$useLocalMode2;
|
|
656
682
|
if (!isAutocompleteEnabled) {
|
|
@@ -19,10 +19,13 @@ var createGhostTextDecorationSet = exports.createGhostTextDecorationSet = functi
|
|
|
19
19
|
var container = document.createElement('span');
|
|
20
20
|
container.className = GHOST_TEXT_CLASS;
|
|
21
21
|
container.setAttribute('data-autocomplete-ghost', 'true');
|
|
22
|
+
container.setAttribute('contenteditable', 'false');
|
|
23
|
+
container.setAttribute('aria-hidden', 'true');
|
|
22
24
|
container.style.color = '#999';
|
|
23
25
|
container.style.opacity = '0.6';
|
|
24
|
-
container.style.pointerEvents = '
|
|
26
|
+
container.style.pointerEvents = 'auto';
|
|
25
27
|
container.style.userSelect = 'none';
|
|
28
|
+
container.style.cursor = 'pointer';
|
|
26
29
|
container.style.fontStyle = 'italic';
|
|
27
30
|
// U+200B (Zero Width Space) gives the browser a line-break opportunity
|
|
28
31
|
// immediately before the ghost text. This ensures the typed text before
|
|
@@ -24,6 +24,7 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
24
24
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
25
25
|
// on every word boundary for the plugin's lifetime.
|
|
26
26
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
27
|
+
const INLINE_LEAF_TEXT = ' ';
|
|
27
28
|
|
|
28
29
|
// eslint-disable-next-line require-unicode-regexp
|
|
29
30
|
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
@@ -47,7 +48,8 @@ const getTextBeforeCursor = state => {
|
|
|
47
48
|
const maxChars = 200;
|
|
48
49
|
const blockNode = $from.parent;
|
|
49
50
|
const offsetInBlock = $from.parentOffset;
|
|
50
|
-
|
|
51
|
+
// PM offsets count inline leaf nodes; textContent indices do not.
|
|
52
|
+
const blockText = blockNode.textBetween(0, offsetInBlock, undefined, INLINE_LEAF_TEXT);
|
|
51
53
|
if (blockText.length >= maxChars) {
|
|
52
54
|
return blockText.slice(-maxChars);
|
|
53
55
|
}
|
|
@@ -60,7 +62,7 @@ const getTextBeforeCursor = state => {
|
|
|
60
62
|
const indexInParent = $from.index(depth);
|
|
61
63
|
for (let i = indexInParent - 1; i >= 0 && fullText.length < maxChars; i--) {
|
|
62
64
|
const sibling = parentNode.child(i);
|
|
63
|
-
const siblingText = sibling.
|
|
65
|
+
const siblingText = sibling.textBetween(0, sibling.content.size, '\n', INLINE_LEAF_TEXT);
|
|
64
66
|
fullText = siblingText + '\n' + fullText;
|
|
65
67
|
}
|
|
66
68
|
depth--;
|
|
@@ -155,6 +157,16 @@ const acceptGhostText = (state, dispatch) => {
|
|
|
155
157
|
}
|
|
156
158
|
return true;
|
|
157
159
|
};
|
|
160
|
+
const acceptGhostTextWithAnalytics = (state, dispatch, onAccepted) => {
|
|
161
|
+
var _pluginState$ghostTex;
|
|
162
|
+
const pluginState = autocompletePluginKey.getState(state);
|
|
163
|
+
const ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
|
|
164
|
+
const accepted = acceptGhostText(state, dispatch);
|
|
165
|
+
if (accepted) {
|
|
166
|
+
onAccepted(ghostText);
|
|
167
|
+
}
|
|
168
|
+
return accepted;
|
|
169
|
+
};
|
|
158
170
|
|
|
159
171
|
/**
|
|
160
172
|
* Context provided to the autocomplete plugin on first editor focus.
|
|
@@ -594,28 +606,18 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
594
606
|
},
|
|
595
607
|
handleKeyDown: keydownHandler({
|
|
596
608
|
Tab: (state, dispatch) => {
|
|
597
|
-
|
|
598
|
-
const pluginState = autocompletePluginKey.getState(state);
|
|
599
|
-
const ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
|
|
600
|
-
const accepted = acceptGhostText(state, dispatch);
|
|
601
|
-
if (accepted) {
|
|
609
|
+
return acceptGhostTextWithAnalytics(state, dispatch, ghostText => {
|
|
602
610
|
justAccepted = true;
|
|
603
611
|
lastShownGhostText = '';
|
|
604
612
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
605
|
-
}
|
|
606
|
-
return accepted;
|
|
613
|
+
});
|
|
607
614
|
},
|
|
608
615
|
ArrowRight: (state, dispatch) => {
|
|
609
|
-
|
|
610
|
-
const pluginState = autocompletePluginKey.getState(state);
|
|
611
|
-
const ghostText = (_pluginState$ghostTex2 = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex2 !== void 0 ? _pluginState$ghostTex2 : '';
|
|
612
|
-
const accepted = acceptGhostText(state, dispatch);
|
|
613
|
-
if (accepted) {
|
|
616
|
+
return acceptGhostTextWithAnalytics(state, dispatch, ghostText => {
|
|
614
617
|
justAccepted = true;
|
|
615
618
|
lastShownGhostText = '';
|
|
616
619
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
617
|
-
}
|
|
618
|
-
return accepted;
|
|
620
|
+
});
|
|
619
621
|
},
|
|
620
622
|
Escape: (state, dispatch) => {
|
|
621
623
|
const didClear = clearGhostText(state, dispatch);
|
|
@@ -638,6 +640,30 @@ export const createAutocompletePlugin = (options, api) => {
|
|
|
638
640
|
}
|
|
639
641
|
return false;
|
|
640
642
|
},
|
|
643
|
+
mousedown: (view, event) => {
|
|
644
|
+
if (!isAutocompleteEnabled || !(event.target instanceof Element)) {
|
|
645
|
+
return false;
|
|
646
|
+
}
|
|
647
|
+
if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
|
|
648
|
+
const didClear = clearGhostText(view.state, view.dispatch);
|
|
649
|
+
if (didClear) {
|
|
650
|
+
dismissedContext = getTextBeforeCursor(view.state);
|
|
651
|
+
lastShownGhostText = '';
|
|
652
|
+
fireSuggestionDismissedAnalytics('click');
|
|
653
|
+
}
|
|
654
|
+
return false;
|
|
655
|
+
}
|
|
656
|
+
event.preventDefault();
|
|
657
|
+
const accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, ghostText => {
|
|
658
|
+
justAccepted = true;
|
|
659
|
+
lastShownGhostText = '';
|
|
660
|
+
fireSuggestionInsertedAnalytics(ghostText);
|
|
661
|
+
});
|
|
662
|
+
if (accepted) {
|
|
663
|
+
view.focus();
|
|
664
|
+
}
|
|
665
|
+
return accepted;
|
|
666
|
+
},
|
|
641
667
|
focus: () => {
|
|
642
668
|
var _options$useLocalMode, _options$useLocalMode2;
|
|
643
669
|
if (!isAutocompleteEnabled) {
|
|
@@ -13,10 +13,13 @@ export const createGhostTextDecorationSet = (state, position, text) => {
|
|
|
13
13
|
const container = document.createElement('span');
|
|
14
14
|
container.className = GHOST_TEXT_CLASS;
|
|
15
15
|
container.setAttribute('data-autocomplete-ghost', 'true');
|
|
16
|
+
container.setAttribute('contenteditable', 'false');
|
|
17
|
+
container.setAttribute('aria-hidden', 'true');
|
|
16
18
|
container.style.color = '#999';
|
|
17
19
|
container.style.opacity = '0.6';
|
|
18
|
-
container.style.pointerEvents = '
|
|
20
|
+
container.style.pointerEvents = 'auto';
|
|
19
21
|
container.style.userSelect = 'none';
|
|
22
|
+
container.style.cursor = 'pointer';
|
|
20
23
|
container.style.fontStyle = 'italic';
|
|
21
24
|
// U+200B (Zero Width Space) gives the browser a line-break opportunity
|
|
22
25
|
// immediately before the ghost text. This ensures the typed text before
|
|
@@ -31,6 +31,7 @@ var MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
31
31
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
32
32
|
// on every word boundary for the plugin's lifetime.
|
|
33
33
|
var MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
34
|
+
var INLINE_LEAF_TEXT = ' ';
|
|
34
35
|
|
|
35
36
|
// eslint-disable-next-line require-unicode-regexp
|
|
36
37
|
var TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
@@ -56,7 +57,8 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
|
|
|
56
57
|
var maxChars = 200;
|
|
57
58
|
var blockNode = $from.parent;
|
|
58
59
|
var offsetInBlock = $from.parentOffset;
|
|
59
|
-
|
|
60
|
+
// PM offsets count inline leaf nodes; textContent indices do not.
|
|
61
|
+
var blockText = blockNode.textBetween(0, offsetInBlock, undefined, INLINE_LEAF_TEXT);
|
|
60
62
|
if (blockText.length >= maxChars) {
|
|
61
63
|
return blockText.slice(-maxChars);
|
|
62
64
|
}
|
|
@@ -69,7 +71,7 @@ var getTextBeforeCursor = function getTextBeforeCursor(state) {
|
|
|
69
71
|
var indexInParent = $from.index(depth);
|
|
70
72
|
for (var i = indexInParent - 1; i >= 0 && fullText.length < maxChars; i--) {
|
|
71
73
|
var sibling = parentNode.child(i);
|
|
72
|
-
var siblingText = sibling.
|
|
74
|
+
var siblingText = sibling.textBetween(0, sibling.content.size, '\n', INLINE_LEAF_TEXT);
|
|
73
75
|
fullText = siblingText + '\n' + fullText;
|
|
74
76
|
}
|
|
75
77
|
depth--;
|
|
@@ -160,6 +162,16 @@ var acceptGhostText = function acceptGhostText(state, dispatch) {
|
|
|
160
162
|
}
|
|
161
163
|
return true;
|
|
162
164
|
};
|
|
165
|
+
var acceptGhostTextWithAnalytics = function acceptGhostTextWithAnalytics(state, dispatch, onAccepted) {
|
|
166
|
+
var _pluginState$ghostTex;
|
|
167
|
+
var pluginState = autocompletePluginKey.getState(state);
|
|
168
|
+
var ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
|
|
169
|
+
var accepted = acceptGhostText(state, dispatch);
|
|
170
|
+
if (accepted) {
|
|
171
|
+
onAccepted(ghostText);
|
|
172
|
+
}
|
|
173
|
+
return accepted;
|
|
174
|
+
};
|
|
163
175
|
|
|
164
176
|
/**
|
|
165
177
|
* Context provided to the autocomplete plugin on first editor focus.
|
|
@@ -600,28 +612,18 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
600
612
|
},
|
|
601
613
|
handleKeyDown: keydownHandler({
|
|
602
614
|
Tab: function Tab(state, dispatch) {
|
|
603
|
-
|
|
604
|
-
var pluginState = autocompletePluginKey.getState(state);
|
|
605
|
-
var ghostText = (_pluginState$ghostTex = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex !== void 0 ? _pluginState$ghostTex : '';
|
|
606
|
-
var accepted = acceptGhostText(state, dispatch);
|
|
607
|
-
if (accepted) {
|
|
615
|
+
return acceptGhostTextWithAnalytics(state, dispatch, function (ghostText) {
|
|
608
616
|
justAccepted = true;
|
|
609
617
|
lastShownGhostText = '';
|
|
610
618
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
611
|
-
}
|
|
612
|
-
return accepted;
|
|
619
|
+
});
|
|
613
620
|
},
|
|
614
621
|
ArrowRight: function ArrowRight(state, dispatch) {
|
|
615
|
-
|
|
616
|
-
var pluginState = autocompletePluginKey.getState(state);
|
|
617
|
-
var ghostText = (_pluginState$ghostTex2 = pluginState === null || pluginState === void 0 ? void 0 : pluginState.ghostText) !== null && _pluginState$ghostTex2 !== void 0 ? _pluginState$ghostTex2 : '';
|
|
618
|
-
var accepted = acceptGhostText(state, dispatch);
|
|
619
|
-
if (accepted) {
|
|
622
|
+
return acceptGhostTextWithAnalytics(state, dispatch, function (ghostText) {
|
|
620
623
|
justAccepted = true;
|
|
621
624
|
lastShownGhostText = '';
|
|
622
625
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
623
|
-
}
|
|
624
|
-
return accepted;
|
|
626
|
+
});
|
|
625
627
|
},
|
|
626
628
|
Escape: function Escape(state, dispatch) {
|
|
627
629
|
var didClear = clearGhostText(state, dispatch);
|
|
@@ -644,6 +646,30 @@ export var createAutocompletePlugin = function createAutocompletePlugin(options,
|
|
|
644
646
|
}
|
|
645
647
|
return false;
|
|
646
648
|
},
|
|
649
|
+
mousedown: function mousedown(view, event) {
|
|
650
|
+
if (!isAutocompleteEnabled || !(event.target instanceof Element)) {
|
|
651
|
+
return false;
|
|
652
|
+
}
|
|
653
|
+
if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
|
|
654
|
+
var didClear = clearGhostText(view.state, view.dispatch);
|
|
655
|
+
if (didClear) {
|
|
656
|
+
dismissedContext = getTextBeforeCursor(view.state);
|
|
657
|
+
lastShownGhostText = '';
|
|
658
|
+
fireSuggestionDismissedAnalytics('click');
|
|
659
|
+
}
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
event.preventDefault();
|
|
663
|
+
var accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, function (ghostText) {
|
|
664
|
+
justAccepted = true;
|
|
665
|
+
lastShownGhostText = '';
|
|
666
|
+
fireSuggestionInsertedAnalytics(ghostText);
|
|
667
|
+
});
|
|
668
|
+
if (accepted) {
|
|
669
|
+
view.focus();
|
|
670
|
+
}
|
|
671
|
+
return accepted;
|
|
672
|
+
},
|
|
647
673
|
focus: function focus() {
|
|
648
674
|
var _options$useLocalMode, _options$useLocalMode2;
|
|
649
675
|
if (!isAutocompleteEnabled) {
|
|
@@ -13,10 +13,13 @@ export var createGhostTextDecorationSet = function createGhostTextDecorationSet(
|
|
|
13
13
|
var container = document.createElement('span');
|
|
14
14
|
container.className = GHOST_TEXT_CLASS;
|
|
15
15
|
container.setAttribute('data-autocomplete-ghost', 'true');
|
|
16
|
+
container.setAttribute('contenteditable', 'false');
|
|
17
|
+
container.setAttribute('aria-hidden', 'true');
|
|
16
18
|
container.style.color = '#999';
|
|
17
19
|
container.style.opacity = '0.6';
|
|
18
|
-
container.style.pointerEvents = '
|
|
20
|
+
container.style.pointerEvents = 'auto';
|
|
19
21
|
container.style.userSelect = 'none';
|
|
22
|
+
container.style.cursor = 'pointer';
|
|
20
23
|
container.style.fontStyle = 'italic';
|
|
21
24
|
// U+200B (Zero Width Space) gives the browser a line-break opportunity
|
|
22
25
|
// immediately before the ghost text. This ensures the typed text before
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atlaskit/editor-plugin-autocomplete",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.2.0",
|
|
4
4
|
"description": "Client-side text autocomplete plugin for @atlaskit/editor-core",
|
|
5
5
|
"author": "Atlassian Pty Ltd",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
"wink-nlp": "^2.4.0"
|
|
31
31
|
},
|
|
32
32
|
"peerDependencies": {
|
|
33
|
-
"@atlaskit/editor-common": "^116.
|
|
33
|
+
"@atlaskit/editor-common": "^116.40.0",
|
|
34
34
|
"@atlaskit/editor-plugin-analytics": "^12.0.0",
|
|
35
35
|
"react": "^18.2.0"
|
|
36
36
|
},
|
|
@@ -47,6 +47,7 @@ const MAX_INGESTED_CONTEXT_TEXTS = 50;
|
|
|
47
47
|
// non-comment editors (where parentCommentContent never arrives) don't refetch
|
|
48
48
|
// on every word boundary for the plugin's lifetime.
|
|
49
49
|
const MAX_CONTEXT_REFRESH_ATTEMPTS = 5;
|
|
50
|
+
const INLINE_LEAF_TEXT = ' ';
|
|
50
51
|
|
|
51
52
|
// eslint-disable-next-line require-unicode-regexp
|
|
52
53
|
const TRAILING_NON_BOUNDARY_TOKEN_REGEX = /([^\s.,;:!?]*)$/;
|
|
@@ -82,7 +83,8 @@ const getTextBeforeCursor = (state: EditorState): string => {
|
|
|
82
83
|
|
|
83
84
|
const blockNode = $from.parent;
|
|
84
85
|
const offsetInBlock = $from.parentOffset;
|
|
85
|
-
|
|
86
|
+
// PM offsets count inline leaf nodes; textContent indices do not.
|
|
87
|
+
const blockText = blockNode.textBetween(0, offsetInBlock, undefined, INLINE_LEAF_TEXT);
|
|
86
88
|
|
|
87
89
|
if (blockText.length >= maxChars) {
|
|
88
90
|
return blockText.slice(-maxChars);
|
|
@@ -99,7 +101,7 @@ const getTextBeforeCursor = (state: EditorState): string => {
|
|
|
99
101
|
|
|
100
102
|
for (let i = indexInParent - 1; i >= 0 && fullText.length < maxChars; i--) {
|
|
101
103
|
const sibling = parentNode.child(i);
|
|
102
|
-
const siblingText = sibling.
|
|
104
|
+
const siblingText = sibling.textBetween(0, sibling.content.size, '\n', INLINE_LEAF_TEXT);
|
|
103
105
|
fullText = siblingText + '\n' + fullText;
|
|
104
106
|
}
|
|
105
107
|
depth--;
|
|
@@ -190,6 +192,20 @@ const acceptGhostText = (state: EditorState, dispatch?: (tr: Transaction) => voi
|
|
|
190
192
|
return true;
|
|
191
193
|
};
|
|
192
194
|
|
|
195
|
+
const acceptGhostTextWithAnalytics = (
|
|
196
|
+
state: EditorState,
|
|
197
|
+
dispatch: ((tr: Transaction) => void) | undefined,
|
|
198
|
+
onAccepted: (ghostText: string) => void,
|
|
199
|
+
): boolean => {
|
|
200
|
+
const pluginState = autocompletePluginKey.getState(state) as AutocompletePluginState | undefined;
|
|
201
|
+
const ghostText = pluginState?.ghostText ?? '';
|
|
202
|
+
const accepted = acceptGhostText(state, dispatch);
|
|
203
|
+
if (accepted) {
|
|
204
|
+
onAccepted(ghostText);
|
|
205
|
+
}
|
|
206
|
+
return accepted;
|
|
207
|
+
};
|
|
208
|
+
|
|
193
209
|
/**
|
|
194
210
|
* Context provided to the autocomplete plugin on first editor focus.
|
|
195
211
|
* Text fields are selectively ingested to boost word-frequency scoring for
|
|
@@ -376,7 +392,7 @@ export const createAutocompletePlugin = (
|
|
|
376
392
|
return options?.useLocalModel ? 'localLlm' : 'server';
|
|
377
393
|
};
|
|
378
394
|
|
|
379
|
-
const fireSuggestionDismissedAnalytics = (reason: 'escape' | 'blur'): void => {
|
|
395
|
+
const fireSuggestionDismissedAnalytics = (reason: 'escape' | 'blur' | 'click'): void => {
|
|
380
396
|
api?.analytics?.actions.fireAnalyticsEvent({
|
|
381
397
|
action: ACTION.SUGGESTION_DISMISSED,
|
|
382
398
|
actionSubject: ACTION_SUBJECT.CONTEXTUAL_TYPEAHEAD,
|
|
@@ -734,30 +750,18 @@ export const createAutocompletePlugin = (
|
|
|
734
750
|
|
|
735
751
|
handleKeyDown: keydownHandler({
|
|
736
752
|
Tab: (state: EditorState, dispatch?: (tr: Transaction) => void) => {
|
|
737
|
-
|
|
738
|
-
| AutocompletePluginState
|
|
739
|
-
| undefined;
|
|
740
|
-
const ghostText = pluginState?.ghostText ?? '';
|
|
741
|
-
const accepted = acceptGhostText(state, dispatch);
|
|
742
|
-
if (accepted) {
|
|
753
|
+
return acceptGhostTextWithAnalytics(state, dispatch, (ghostText) => {
|
|
743
754
|
justAccepted = true;
|
|
744
755
|
lastShownGhostText = '';
|
|
745
756
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
746
|
-
}
|
|
747
|
-
return accepted;
|
|
757
|
+
});
|
|
748
758
|
},
|
|
749
759
|
ArrowRight: (state: EditorState, dispatch?: (tr: Transaction) => void) => {
|
|
750
|
-
|
|
751
|
-
| AutocompletePluginState
|
|
752
|
-
| undefined;
|
|
753
|
-
const ghostText = pluginState?.ghostText ?? '';
|
|
754
|
-
const accepted = acceptGhostText(state, dispatch);
|
|
755
|
-
if (accepted) {
|
|
760
|
+
return acceptGhostTextWithAnalytics(state, dispatch, (ghostText) => {
|
|
756
761
|
justAccepted = true;
|
|
757
762
|
lastShownGhostText = '';
|
|
758
763
|
fireSuggestionInsertedAnalytics(ghostText);
|
|
759
|
-
}
|
|
760
|
-
return accepted;
|
|
764
|
+
});
|
|
761
765
|
},
|
|
762
766
|
Escape: (state: EditorState, dispatch?: (tr: Transaction) => void) => {
|
|
763
767
|
const didClear = clearGhostText(state, dispatch);
|
|
@@ -784,6 +788,35 @@ export const createAutocompletePlugin = (
|
|
|
784
788
|
}
|
|
785
789
|
return false;
|
|
786
790
|
},
|
|
791
|
+
mousedown: (view: EditorView, event: MouseEvent) => {
|
|
792
|
+
if (!isAutocompleteEnabled || !(event.target instanceof Element)) {
|
|
793
|
+
return false;
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
if (!event.target.closest('[data-autocomplete-ghost="true"]')) {
|
|
797
|
+
const didClear = clearGhostText(view.state, view.dispatch);
|
|
798
|
+
if (didClear) {
|
|
799
|
+
dismissedContext = getTextBeforeCursor(view.state);
|
|
800
|
+
lastShownGhostText = '';
|
|
801
|
+
fireSuggestionDismissedAnalytics('click');
|
|
802
|
+
}
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
event.preventDefault();
|
|
807
|
+
|
|
808
|
+
const accepted = acceptGhostTextWithAnalytics(view.state, view.dispatch, (ghostText) => {
|
|
809
|
+
justAccepted = true;
|
|
810
|
+
lastShownGhostText = '';
|
|
811
|
+
fireSuggestionInsertedAnalytics(ghostText);
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
if (accepted) {
|
|
815
|
+
view.focus();
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return accepted;
|
|
819
|
+
},
|
|
787
820
|
focus: () => {
|
|
788
821
|
if (!isAutocompleteEnabled) {
|
|
789
822
|
return false;
|
|
@@ -22,10 +22,13 @@ export const createGhostTextDecorationSet = (
|
|
|
22
22
|
const container = document.createElement('span');
|
|
23
23
|
container.className = GHOST_TEXT_CLASS;
|
|
24
24
|
container.setAttribute('data-autocomplete-ghost', 'true');
|
|
25
|
+
container.setAttribute('contenteditable', 'false');
|
|
26
|
+
container.setAttribute('aria-hidden', 'true');
|
|
25
27
|
container.style.color = '#999';
|
|
26
28
|
container.style.opacity = '0.6';
|
|
27
|
-
container.style.pointerEvents = '
|
|
29
|
+
container.style.pointerEvents = 'auto';
|
|
28
30
|
container.style.userSelect = 'none';
|
|
31
|
+
container.style.cursor = 'pointer';
|
|
29
32
|
container.style.fontStyle = 'italic';
|
|
30
33
|
// U+200B (Zero Width Space) gives the browser a line-break opportunity
|
|
31
34
|
// immediately before the ghost text. This ensures the typed text before
|