@@ -5,6 +5,7 @@ import ItemSelect from "@/components/ItemSelect.vue";
5
5
6
6
let suggestionApp = null ;
7
7
let suggestionEl = null ;
8
+ let cleanupListeners = null ;
8
9
9
10
export const CrossReferenceInputRule = Extension . create ( {
10
11
name : "crossReferenceInputRule" ,
@@ -53,23 +54,41 @@ function createSuggestionPlugin(options, editor) {
53
54
54
55
state : {
55
56
init ( ) {
56
- return { active : false , query : null , range : null } ;
57
+ return { active : false , range : null , query : null } ;
57
58
} ,
58
-
59
59
apply ( tr , prev , oldState , newState ) {
60
60
const { selection } = newState ;
61
61
const { empty, from } = selection ;
62
- if ( ! empty ) return { ...prev , active : false } ;
62
+ if ( ! empty ) return { active : false , range : null , query : null } ;
63
+
63
64
const $pos = selection . $from ;
64
65
const textBefore = $pos . parent . textContent . slice ( 0 , $pos . parentOffset ) ;
65
66
const match = textBefore . match ( / @ ( \w * ) $ / ) ;
67
+
66
68
if ( ! match ) {
67
69
hideSuggestions ( ) ;
68
- return { ... prev , active : false } ;
70
+ return { active : false , range : null , query : null } ;
69
71
}
72
+
70
73
const query = match [ 1 ] ;
71
74
const range = { from : from - match [ 0 ] . length , to : from } ;
72
- return { active : true , query, range } ;
75
+ return { active : true , range, query } ;
76
+ } ,
77
+ } ,
78
+
79
+ props : {
80
+ handleKeyDown ( view , event ) {
81
+ const state = pluginKey . getState ( view . state ) ;
82
+ if ( ! state ?. active ) return false ;
83
+
84
+ if ( event . key === "Backspace" && state . query === "" && state . range ) {
85
+ const tr = view . state . tr . deleteRange ( state . range ) ;
86
+ view . dispatch ( tr ) ;
87
+ hideSuggestions ( ) ;
88
+ return true ;
89
+ }
90
+
91
+ return false ;
73
92
} ,
74
93
} ,
75
94
@@ -95,6 +114,10 @@ function showSuggestions(view, state, options, editor) {
95
114
if ( ! suggestionEl ) {
96
115
suggestionEl = document . createElement ( "div" ) ;
97
116
suggestionEl . className = "dropdown-menu show p-2 tiptap-suggestions" ;
117
+ suggestionEl . style . position = "fixed" ;
118
+ suggestionEl . style . minWidth = "350px" ;
119
+ suggestionEl . style . maxWidth = "600px" ;
120
+ suggestionEl . style . zIndex = 2000 ;
98
121
document . body . appendChild ( suggestionEl ) ;
99
122
}
100
123
@@ -104,32 +127,33 @@ function showSuggestions(view, state, options, editor) {
104
127
placeholder : "Search items..." ,
105
128
typesToQuery : [ "samples" , "cells" , "starting_materials" ] ,
106
129
"onUpdate:modelValue" : ( item ) => {
107
- if ( ! item ) return ;
130
+ const state = options . pluginKey . getState ( editor . state ) ;
131
+ if ( ! item || ! state ?. range ) return ;
132
+
108
133
options . command ( { editor, range : state . range , props : item } ) ;
109
134
hideSuggestions ( ) ;
110
135
} ,
111
136
} ) ;
112
137
suggestionApp . mount ( suggestionEl ) ;
113
- window . addEventListener ( "scroll" , ( ) => hideSuggestions ( ) , true ) ;
114
- window . addEventListener ( "resize" , ( ) => hideSuggestions ( ) , true ) ;
138
+ cleanupListeners = setupGlobalListeners ( ) ;
115
139
}
116
140
117
- reposition ( view , state ) ;
141
+ reposition ( view , state . range ) ;
118
142
suggestionEl . style . display = "block" ;
119
- suggestionEl . querySelector ( "input" ) ?. focus ( ) ;
143
+
144
+ const input = suggestionEl . querySelector ( "input" ) ;
145
+ if ( input && document . activeElement !== input ) {
146
+ input . focus ( ) ;
147
+ }
120
148
}
121
149
122
- function reposition ( view , state ) {
123
- if ( ! suggestionEl ) return ;
124
- const coords = view . coordsAtPos ( state . range . from ) ;
150
+ function reposition ( view , range ) {
151
+ if ( ! suggestionEl || ! range ) return ;
125
152
126
- suggestionEl . style . position = "absolute" ;
127
- suggestionEl . style . left = `${ coords . left } px` ;
128
- suggestionEl . style . top = `${ coords . bottom - 20 } px` ;
129
- suggestionEl . style . zIndex = 1050 ;
153
+ const coords = view . coordsAtPos ( range . from ) ;
130
154
131
- suggestionEl . style . minWidth = "350px" ;
132
- suggestionEl . style . maxWidth = "600px" ;
155
+ suggestionEl . style . left = ` ${ coords . left } px` ;
156
+ suggestionEl . style . top = ` ${ coords . bottom } px` ;
133
157
}
134
158
135
159
function hideSuggestions ( destroy = false ) {
@@ -139,6 +163,32 @@ function hideSuggestions(destroy = false) {
139
163
suggestionEl . remove ( ) ;
140
164
suggestionEl = null ;
141
165
suggestionApp = null ;
166
+ if ( cleanupListeners ) {
167
+ cleanupListeners ( ) ;
168
+ cleanupListeners = null ;
169
+ }
142
170
}
143
171
}
144
172
}
173
+
174
+ function setupGlobalListeners ( ) {
175
+ const onClickOutside = ( event ) => {
176
+ if ( suggestionEl && ! suggestionEl . contains ( event . target ) ) {
177
+ hideSuggestions ( ) ;
178
+ }
179
+ } ;
180
+
181
+ const onScrollOrResize = ( ) => {
182
+ if ( suggestionEl ) hideSuggestions ( ) ;
183
+ } ;
184
+
185
+ window . addEventListener ( "mousedown" , onClickOutside ) ;
186
+ window . addEventListener ( "scroll" , onScrollOrResize , true ) ;
187
+ window . addEventListener ( "resize" , onScrollOrResize , true ) ;
188
+
189
+ return ( ) => {
190
+ window . removeEventListener ( "mousedown" , onClickOutside ) ;
191
+ window . removeEventListener ( "scroll" , onScrollOrResize , true ) ;
192
+ window . removeEventListener ( "resize" , onScrollOrResize , true ) ;
193
+ } ;
194
+ }
0 commit comments