@reviewpush/rp-treeselect 0.0.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/README.md +172 -0
- package/dist/rp-treeselect.cjs.js +3656 -0
- package/dist/rp-treeselect.cjs.js.map +1 -0
- package/dist/rp-treeselect.cjs.min.js +2 -0
- package/dist/rp-treeselect.cjs.min.js.map +1 -0
- package/dist/rp-treeselect.css +947 -0
- package/dist/rp-treeselect.css.map +1 -0
- package/dist/rp-treeselect.min.css +1 -0
- package/dist/rp-treeselect.umd.js +4837 -0
- package/dist/rp-treeselect.umd.js.map +1 -0
- package/dist/rp-treeselect.umd.min.js +2 -0
- package/dist/rp-treeselect.umd.min.js.map +1 -0
- package/package.json +140 -0
- package/src/assets/checkbox-checked-disabled.png +0 -0
- package/src/assets/checkbox-checked-disabled@2x.png +0 -0
- package/src/assets/checkbox-checked-disabled@3x.png +0 -0
- package/src/assets/checkbox-checked.png +0 -0
- package/src/assets/checkbox-checked@2x.png +0 -0
- package/src/assets/checkbox-checked@3x.png +0 -0
- package/src/assets/checkbox-indeterminate-disabled.png +0 -0
- package/src/assets/checkbox-indeterminate-disabled@2x.png +0 -0
- package/src/assets/checkbox-indeterminate-disabled@3x.png +0 -0
- package/src/assets/checkbox-indeterminate.png +0 -0
- package/src/assets/checkbox-indeterminate@2x.png +0 -0
- package/src/assets/checkbox-indeterminate@3x.png +0 -0
- package/src/components/Control.vue +153 -0
- package/src/components/HiddenFields.vue +37 -0
- package/src/components/Input.vue +295 -0
- package/src/components/Menu.vue +313 -0
- package/src/components/MenuPortal.vue +179 -0
- package/src/components/MultiValue.vue +56 -0
- package/src/components/MultiValueItem.vue +45 -0
- package/src/components/Option.vue +300 -0
- package/src/components/Placeholder.vue +21 -0
- package/src/components/SingleValue.vue +34 -0
- package/src/components/Tip.vue +32 -0
- package/src/components/Treeselect.vue +42 -0
- package/src/components/icons/Arrow.vue +11 -0
- package/src/components/icons/Delete.vue +11 -0
- package/src/constants.js +50 -0
- package/src/index.js +14 -0
- package/src/mixins/treeselectMixin.js +1949 -0
- package/src/style.less +1147 -0
- package/src/utils/.eslintrc.js +6 -0
- package/src/utils/constant.js +1 -0
- package/src/utils/createMap.js +1 -0
- package/src/utils/debounce.js +1 -0
- package/src/utils/deepExtend.js +25 -0
- package/src/utils/find.js +6 -0
- package/src/utils/identity.js +1 -0
- package/src/utils/includes.js +3 -0
- package/src/utils/index.js +38 -0
- package/src/utils/isNaN.js +3 -0
- package/src/utils/isPromise.js +1 -0
- package/src/utils/last.js +1 -0
- package/src/utils/noop.js +1 -0
- package/src/utils/onLeftClick.js +7 -0
- package/src/utils/once.js +1 -0
- package/src/utils/quickDiff.js +9 -0
- package/src/utils/removeFromArray.js +4 -0
- package/src/utils/scrollIntoView.js +15 -0
- package/src/utils/setupResizeAndScrollEventListeners.js +34 -0
- package/src/utils/warning.js +11 -0
- package/src/utils/watchSize.js +67 -0
|
@@ -0,0 +1,1949 @@
|
|
|
1
|
+
import fuzzysearch from 'fuzzysearch'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
warning,
|
|
5
|
+
onLeftClick, scrollIntoView,
|
|
6
|
+
isNaN, isPromise, once,
|
|
7
|
+
identity, constant, createMap,
|
|
8
|
+
quickDiff, last as getLast, includes, find, removeFromArray,
|
|
9
|
+
} from '../utils'
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
NO_PARENT_NODE,
|
|
13
|
+
UNCHECKED, INDETERMINATE, CHECKED,
|
|
14
|
+
LOAD_ROOT_OPTIONS, LOAD_CHILDREN_OPTIONS, ASYNC_SEARCH,
|
|
15
|
+
ALL, BRANCH_PRIORITY, LEAF_PRIORITY, ALL_WITH_INDETERMINATE,
|
|
16
|
+
ALL_CHILDREN, ALL_DESCENDANTS, LEAF_CHILDREN, LEAF_DESCENDANTS,
|
|
17
|
+
ORDER_SELECTED, LEVEL, INDEX,
|
|
18
|
+
} from '../constants'
|
|
19
|
+
|
|
20
|
+
function sortValueByIndex(a, b) {
|
|
21
|
+
let i = 0
|
|
22
|
+
do {
|
|
23
|
+
if (a.level < i) return -1
|
|
24
|
+
if (b.level < i) return 1
|
|
25
|
+
if (a.index[i] !== b.index[i]) return a.index[i] - b.index[i]
|
|
26
|
+
i++
|
|
27
|
+
} while (true)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sortValueByLevel(a, b) {
|
|
31
|
+
return a.level === b.level
|
|
32
|
+
? sortValueByIndex(a, b)
|
|
33
|
+
: a.level - b.level
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function createAsyncOptionsStates() {
|
|
37
|
+
return {
|
|
38
|
+
isLoaded: false,
|
|
39
|
+
isLoading: false,
|
|
40
|
+
loadingError: '',
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function stringifyOptionPropValue(value) {
|
|
45
|
+
if (typeof value === 'string') return value
|
|
46
|
+
if (typeof value === 'number' && !isNaN(value)) return value + ''
|
|
47
|
+
// istanbul ignore next
|
|
48
|
+
return ''
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function match(enableFuzzyMatch, needle, haystack) {
|
|
52
|
+
return enableFuzzyMatch
|
|
53
|
+
? fuzzysearch(needle, haystack)
|
|
54
|
+
: includes(haystack, needle)
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function getErrorMessage(err) {
|
|
58
|
+
return err.message || /* istanbul ignore next */String(err)
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
let instanceId = 0
|
|
62
|
+
|
|
63
|
+
export default {
|
|
64
|
+
provide() {
|
|
65
|
+
return {
|
|
66
|
+
// Enable access to the instance of root component of rp-treeselect
|
|
67
|
+
// across hierarchy.
|
|
68
|
+
instance: this,
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
props: {
|
|
73
|
+
/**
|
|
74
|
+
* Whether to allow resetting value even if there are disabled selected nodes.
|
|
75
|
+
*/
|
|
76
|
+
allowClearingDisabled: {
|
|
77
|
+
type: Boolean,
|
|
78
|
+
default: false,
|
|
79
|
+
},
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* When an ancestor node is selected/deselected, whether its disabled descendants should be selected/deselected.
|
|
83
|
+
* You may want to use this in conjunction with `allowClearingDisabled` prop.
|
|
84
|
+
*/
|
|
85
|
+
allowSelectingDisabledDescendants: {
|
|
86
|
+
type: Boolean,
|
|
87
|
+
default: false,
|
|
88
|
+
},
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Whether the menu should be always open.
|
|
92
|
+
*/
|
|
93
|
+
alwaysOpen: {
|
|
94
|
+
type: Boolean,
|
|
95
|
+
default: false,
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Append the menu to <body />?
|
|
100
|
+
*/
|
|
101
|
+
appendToBody: {
|
|
102
|
+
type: Boolean,
|
|
103
|
+
default: false,
|
|
104
|
+
},
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Whether to enable async search mode.
|
|
108
|
+
*/
|
|
109
|
+
async: {
|
|
110
|
+
type: Boolean,
|
|
111
|
+
default: false,
|
|
112
|
+
},
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Automatically focus the component on mount?
|
|
116
|
+
*/
|
|
117
|
+
autoFocus: {
|
|
118
|
+
type: Boolean,
|
|
119
|
+
default: false,
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Automatically load root options on mount. When set to `false`, root options will be loaded when the menu is opened.
|
|
124
|
+
*/
|
|
125
|
+
autoLoadRootOptions: {
|
|
126
|
+
type: Boolean,
|
|
127
|
+
default: true,
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* When user deselects a node, automatically deselect its ancestors. Applies to flat mode only.
|
|
132
|
+
*/
|
|
133
|
+
autoDeselectAncestors: {
|
|
134
|
+
type: Boolean,
|
|
135
|
+
default: false,
|
|
136
|
+
},
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* When user deselects a node, automatically deselect its descendants. Applies to flat mode only.
|
|
140
|
+
*/
|
|
141
|
+
autoDeselectDescendants: {
|
|
142
|
+
type: Boolean,
|
|
143
|
+
default: false,
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* When user selects a node, automatically select its ancestors. Applies to flat mode only.
|
|
148
|
+
*/
|
|
149
|
+
autoSelectAncestors: {
|
|
150
|
+
type: Boolean,
|
|
151
|
+
default: false,
|
|
152
|
+
},
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* When user selects a node, automatically select its descendants. Applies to flat mode only.
|
|
156
|
+
*/
|
|
157
|
+
autoSelectDescendants: {
|
|
158
|
+
type: Boolean,
|
|
159
|
+
default: false,
|
|
160
|
+
},
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Whether pressing backspace key removes the last item if there is no text input.
|
|
164
|
+
*/
|
|
165
|
+
backspaceRemoves: {
|
|
166
|
+
type: Boolean,
|
|
167
|
+
default: true,
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Function that processes before clearing all input fields.
|
|
172
|
+
* Return `false` to prevent value from being cleared.
|
|
173
|
+
* @type {function(): (boolean|Promise<boolean>)}
|
|
174
|
+
*/
|
|
175
|
+
beforeClearAll: {
|
|
176
|
+
type: Function,
|
|
177
|
+
default: constant(true),
|
|
178
|
+
},
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Show branch nodes before leaf nodes?
|
|
182
|
+
*/
|
|
183
|
+
branchNodesFirst: {
|
|
184
|
+
type: Boolean,
|
|
185
|
+
default: false,
|
|
186
|
+
},
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Should cache results of every search request?
|
|
190
|
+
*/
|
|
191
|
+
cacheOptions: {
|
|
192
|
+
type: Boolean,
|
|
193
|
+
default: true,
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
/**
|
|
197
|
+
* Show an "×" button that resets value?
|
|
198
|
+
*/
|
|
199
|
+
clearable: {
|
|
200
|
+
type: Boolean,
|
|
201
|
+
default: true,
|
|
202
|
+
},
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Title for the "×" button when `multiple: true`.
|
|
206
|
+
*/
|
|
207
|
+
clearAllText: {
|
|
208
|
+
type: String,
|
|
209
|
+
default: 'Clear all',
|
|
210
|
+
},
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Whether to clear the search input after selecting.
|
|
214
|
+
* Use only when `multiple` is `true`.
|
|
215
|
+
* For single-select mode, it **always** clears the input after selecting an option regardless of the prop value.
|
|
216
|
+
*/
|
|
217
|
+
clearOnSelect: {
|
|
218
|
+
type: Boolean,
|
|
219
|
+
default: false,
|
|
220
|
+
},
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Title for the "×" button.
|
|
224
|
+
*/
|
|
225
|
+
clearValueText: {
|
|
226
|
+
type: String,
|
|
227
|
+
default: 'Clear value',
|
|
228
|
+
},
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Whether to close the menu after selecting an option?
|
|
232
|
+
* Use only when `multiple` is `true`.
|
|
233
|
+
*/
|
|
234
|
+
closeOnSelect: {
|
|
235
|
+
type: Boolean,
|
|
236
|
+
default: true,
|
|
237
|
+
},
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* How many levels of branch nodes should be automatically expanded when loaded.
|
|
241
|
+
* Set `Infinity` to make all branch nodes expanded by default.
|
|
242
|
+
*/
|
|
243
|
+
defaultExpandLevel: {
|
|
244
|
+
type: Number,
|
|
245
|
+
default: 0,
|
|
246
|
+
},
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* The default set of options to show before the user starts searching. Used for async search mode.
|
|
250
|
+
* When set to `true`, the results for search query as a empty string will be autoloaded.
|
|
251
|
+
* @type {boolean|node[]}
|
|
252
|
+
*/
|
|
253
|
+
defaultOptions: {
|
|
254
|
+
default: false,
|
|
255
|
+
},
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Whether pressing delete key removes the last item if there is no text input.
|
|
259
|
+
*/
|
|
260
|
+
deleteRemoves: {
|
|
261
|
+
type: Boolean,
|
|
262
|
+
default: true,
|
|
263
|
+
},
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Delimiter to use to join multiple values for the hidden field value.
|
|
267
|
+
*/
|
|
268
|
+
delimiter: {
|
|
269
|
+
type: String,
|
|
270
|
+
default: ',',
|
|
271
|
+
},
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Only show the nodes that match the search value directly, excluding its ancestors.
|
|
275
|
+
*
|
|
276
|
+
* @type {Object}
|
|
277
|
+
*/
|
|
278
|
+
flattenSearchResults: {
|
|
279
|
+
type: Boolean,
|
|
280
|
+
default: false,
|
|
281
|
+
},
|
|
282
|
+
|
|
283
|
+
/**
|
|
284
|
+
* Prevent branch nodes from being selected?
|
|
285
|
+
*/
|
|
286
|
+
disableBranchNodes: {
|
|
287
|
+
type: Boolean,
|
|
288
|
+
default: false,
|
|
289
|
+
},
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Disable the control?
|
|
293
|
+
*/
|
|
294
|
+
disabled: {
|
|
295
|
+
type: Boolean,
|
|
296
|
+
default: false,
|
|
297
|
+
},
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Disable the fuzzy matching functionality?
|
|
301
|
+
*/
|
|
302
|
+
disableFuzzyMatching: {
|
|
303
|
+
type: Boolean,
|
|
304
|
+
default: false,
|
|
305
|
+
},
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Whether to enable flat mode or not. Non-flat mode (default) means:
|
|
309
|
+
* - Whenever a branch node gets checked, all its children will be checked too
|
|
310
|
+
* - Whenever a branch node has all children checked, the branch node itself will be checked too
|
|
311
|
+
* Set `true` to disable this mechanism
|
|
312
|
+
*/
|
|
313
|
+
flat: {
|
|
314
|
+
type: Boolean,
|
|
315
|
+
default: false,
|
|
316
|
+
},
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Will be passed with all events as the last param.
|
|
320
|
+
* Useful for identifying events origin.
|
|
321
|
+
*/
|
|
322
|
+
instanceId: {
|
|
323
|
+
// Add two trailing "$" to distinguish from explictly specified ids.
|
|
324
|
+
default: () => `${instanceId++}$$`,
|
|
325
|
+
type: [ String, Number ],
|
|
326
|
+
},
|
|
327
|
+
|
|
328
|
+
/**
|
|
329
|
+
* Joins multiple values into a single form field with the `delimiter` (legacy mode).
|
|
330
|
+
*/
|
|
331
|
+
joinValues: {
|
|
332
|
+
type: Boolean,
|
|
333
|
+
default: false,
|
|
334
|
+
},
|
|
335
|
+
|
|
336
|
+
/**
|
|
337
|
+
* Limit the display of selected options.
|
|
338
|
+
* The rest will be hidden within the limitText string.
|
|
339
|
+
*/
|
|
340
|
+
limit: {
|
|
341
|
+
type: Number,
|
|
342
|
+
default: Infinity,
|
|
343
|
+
},
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Function that processes the message shown when selected elements pass the defined limit.
|
|
347
|
+
* @type {function(number): string}
|
|
348
|
+
*/
|
|
349
|
+
limitText: {
|
|
350
|
+
type: Function,
|
|
351
|
+
default: function limitTextDefault(count) { // eslint-disable-line func-name-matching
|
|
352
|
+
return `and ${count} more`
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Text displayed when loading options.
|
|
358
|
+
*/
|
|
359
|
+
loadingText: {
|
|
360
|
+
type: String,
|
|
361
|
+
default: 'Loading...',
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Used for dynamically loading options.
|
|
366
|
+
* @type {function({action: string, callback: (function((Error|string)=): void), parentNode: node=, instanceId}): void}
|
|
367
|
+
*/
|
|
368
|
+
loadOptions: {
|
|
369
|
+
type: Function,
|
|
370
|
+
},
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Which node properties to filter on.
|
|
374
|
+
*/
|
|
375
|
+
matchKeys: {
|
|
376
|
+
type: Array,
|
|
377
|
+
default: constant([ 'label' ]),
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* Sets `maxHeight` style value of the menu.
|
|
382
|
+
*/
|
|
383
|
+
maxHeight: {
|
|
384
|
+
type: Number,
|
|
385
|
+
default: 300,
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Set `true` to allow selecting multiple options (a.k.a., multi-select mode).
|
|
390
|
+
*/
|
|
391
|
+
multiple: {
|
|
392
|
+
type: Boolean,
|
|
393
|
+
default: false,
|
|
394
|
+
},
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* Generates a hidden <input /> tag with this field name for html forms.
|
|
398
|
+
*/
|
|
399
|
+
name: {
|
|
400
|
+
type: String,
|
|
401
|
+
},
|
|
402
|
+
|
|
403
|
+
/**
|
|
404
|
+
* Text displayed when a branch node has no children.
|
|
405
|
+
*/
|
|
406
|
+
noChildrenText: {
|
|
407
|
+
type: String,
|
|
408
|
+
default: 'No sub-options.',
|
|
409
|
+
},
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Text displayed when there are no available options.
|
|
413
|
+
*/
|
|
414
|
+
noOptionsText: {
|
|
415
|
+
type: String,
|
|
416
|
+
default: 'No options available.',
|
|
417
|
+
},
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Text displayed when there are no matching search results.
|
|
421
|
+
*/
|
|
422
|
+
noResultsText: {
|
|
423
|
+
type: String,
|
|
424
|
+
default: 'No results found...',
|
|
425
|
+
},
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Used for normalizing source data.
|
|
429
|
+
* @type {function(node, instanceId): node}
|
|
430
|
+
*/
|
|
431
|
+
normalizer: {
|
|
432
|
+
type: Function,
|
|
433
|
+
default: identity,
|
|
434
|
+
},
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* By default (`auto`), the menu will open below the control. If there is not
|
|
438
|
+
* enough space, rp-treeselect will automatically flip the menu.
|
|
439
|
+
* You can use one of other four options to force the menu to be always opened
|
|
440
|
+
* to specified direction.
|
|
441
|
+
* Acceptable values:
|
|
442
|
+
* - `"auto"`
|
|
443
|
+
* - `"below"`
|
|
444
|
+
* - `"bottom"`
|
|
445
|
+
* - `"above"`
|
|
446
|
+
* - `"top"`
|
|
447
|
+
*/
|
|
448
|
+
openDirection: {
|
|
449
|
+
type: String,
|
|
450
|
+
default: 'auto',
|
|
451
|
+
validator(value) {
|
|
452
|
+
const acceptableValues = [ 'auto', 'top', 'bottom', 'above', 'below' ]
|
|
453
|
+
return includes(acceptableValues, value)
|
|
454
|
+
},
|
|
455
|
+
},
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Whether to automatically open the menu when the control is clicked.
|
|
459
|
+
*/
|
|
460
|
+
openOnClick: {
|
|
461
|
+
type: Boolean,
|
|
462
|
+
default: true,
|
|
463
|
+
},
|
|
464
|
+
|
|
465
|
+
/**
|
|
466
|
+
* Whether to automatically open the menu when the control is focused.
|
|
467
|
+
*/
|
|
468
|
+
openOnFocus: {
|
|
469
|
+
type: Boolean,
|
|
470
|
+
default: false,
|
|
471
|
+
},
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Array of available options.
|
|
475
|
+
* @type {node[]}
|
|
476
|
+
*/
|
|
477
|
+
options: {
|
|
478
|
+
type: Array,
|
|
479
|
+
},
|
|
480
|
+
|
|
481
|
+
/**
|
|
482
|
+
* Field placeholder, displayed when there's no value.
|
|
483
|
+
*/
|
|
484
|
+
placeholder: {
|
|
485
|
+
type: String,
|
|
486
|
+
default: 'Select...',
|
|
487
|
+
},
|
|
488
|
+
|
|
489
|
+
/**
|
|
490
|
+
* Applies HTML5 required attribute when needed.
|
|
491
|
+
*/
|
|
492
|
+
required: {
|
|
493
|
+
type: Boolean,
|
|
494
|
+
default: false,
|
|
495
|
+
},
|
|
496
|
+
|
|
497
|
+
/**
|
|
498
|
+
* Text displayed asking user whether to retry loading children options.
|
|
499
|
+
*/
|
|
500
|
+
retryText: {
|
|
501
|
+
type: String,
|
|
502
|
+
default: 'Retry?',
|
|
503
|
+
},
|
|
504
|
+
|
|
505
|
+
/**
|
|
506
|
+
* Title for the retry button.
|
|
507
|
+
*/
|
|
508
|
+
retryTitle: {
|
|
509
|
+
type: String,
|
|
510
|
+
default: 'Click to retry',
|
|
511
|
+
},
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Enable searching feature?
|
|
515
|
+
*/
|
|
516
|
+
searchable: {
|
|
517
|
+
type: Boolean,
|
|
518
|
+
default: true,
|
|
519
|
+
},
|
|
520
|
+
|
|
521
|
+
/**
|
|
522
|
+
* Search in ancestor nodes too.
|
|
523
|
+
*/
|
|
524
|
+
searchNested: {
|
|
525
|
+
type: Boolean,
|
|
526
|
+
default: false,
|
|
527
|
+
},
|
|
528
|
+
|
|
529
|
+
/**
|
|
530
|
+
* Text tip to prompt for async search.
|
|
531
|
+
*/
|
|
532
|
+
searchPromptText: {
|
|
533
|
+
type: String,
|
|
534
|
+
default: 'Type to search...',
|
|
535
|
+
},
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Whether to show a children count next to the label of each branch node.
|
|
539
|
+
*/
|
|
540
|
+
showCount: {
|
|
541
|
+
type: Boolean,
|
|
542
|
+
default: false,
|
|
543
|
+
},
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Used in conjunction with `showCount` to specify which type of count number should be displayed.
|
|
547
|
+
* Acceptable values:
|
|
548
|
+
* - "ALL_CHILDREN"
|
|
549
|
+
* - "ALL_DESCENDANTS"
|
|
550
|
+
* - "LEAF_CHILDREN"
|
|
551
|
+
* - "LEAF_DESCENDANTS"
|
|
552
|
+
*/
|
|
553
|
+
showCountOf: {
|
|
554
|
+
type: String,
|
|
555
|
+
default: ALL_CHILDREN,
|
|
556
|
+
validator(value) {
|
|
557
|
+
const acceptableValues = [ ALL_CHILDREN, ALL_DESCENDANTS, LEAF_CHILDREN, LEAF_DESCENDANTS ]
|
|
558
|
+
return includes(acceptableValues, value)
|
|
559
|
+
},
|
|
560
|
+
},
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Whether to show children count when searching.
|
|
564
|
+
* Fallbacks to the value of `showCount` when not specified.
|
|
565
|
+
* @type {boolean}
|
|
566
|
+
*/
|
|
567
|
+
showCountOnSearch: null,
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* In which order the selected options should be displayed in trigger & sorted in `value` array.
|
|
571
|
+
* Used for multi-select mode only.
|
|
572
|
+
* Acceptable values:
|
|
573
|
+
* - "ORDER_SELECTED"
|
|
574
|
+
* - "LEVEL"
|
|
575
|
+
* - "INDEX"
|
|
576
|
+
*/
|
|
577
|
+
sortValueBy: {
|
|
578
|
+
type: String,
|
|
579
|
+
default: ORDER_SELECTED,
|
|
580
|
+
validator(value) {
|
|
581
|
+
const acceptableValues = [ ORDER_SELECTED, LEVEL, INDEX ]
|
|
582
|
+
return includes(acceptableValues, value)
|
|
583
|
+
},
|
|
584
|
+
},
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* Tab index of the control.
|
|
588
|
+
*/
|
|
589
|
+
tabIndex: {
|
|
590
|
+
type: Number,
|
|
591
|
+
default: 0,
|
|
592
|
+
},
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The value of the control.
|
|
596
|
+
* Should be `id` or `node` object for single-select mode, or an array of `id` or `node` object for multi-select mode.
|
|
597
|
+
* Its format depends on the `valueFormat` prop.
|
|
598
|
+
* For most cases, just use `v-model` instead.
|
|
599
|
+
* @type {?Array}
|
|
600
|
+
*/
|
|
601
|
+
value: null,
|
|
602
|
+
|
|
603
|
+
/**
|
|
604
|
+
* Which kind of nodes should be included in the `value` array in multi-select mode.
|
|
605
|
+
* Acceptable values:
|
|
606
|
+
* - "ALL" - Any node that is checked will be included in the `value` array
|
|
607
|
+
* - "BRANCH_PRIORITY" (default) - If a branch node is checked, all its descendants will be excluded in the `value` array
|
|
608
|
+
* - "LEAF_PRIORITY" - If a branch node is checked, this node itself and its branch descendants will be excluded from the `value` array but its leaf descendants will be included
|
|
609
|
+
* - "ALL_WITH_INDETERMINATE" - Any node that is checked will be included in the `value` array, plus indeterminate nodes
|
|
610
|
+
*/
|
|
611
|
+
valueConsistsOf: {
|
|
612
|
+
type: String,
|
|
613
|
+
default: BRANCH_PRIORITY,
|
|
614
|
+
validator(value) {
|
|
615
|
+
const acceptableValues = [ ALL, BRANCH_PRIORITY, LEAF_PRIORITY, ALL_WITH_INDETERMINATE ]
|
|
616
|
+
return includes(acceptableValues, value)
|
|
617
|
+
},
|
|
618
|
+
},
|
|
619
|
+
|
|
620
|
+
/**
|
|
621
|
+
* Format of `value` prop.
|
|
622
|
+
* Note that, when set to `"object"`, only `id` & `label` properties are required in each `node` object in `value` prop.
|
|
623
|
+
* Acceptable values:
|
|
624
|
+
* - "id"
|
|
625
|
+
* - "object"
|
|
626
|
+
*/
|
|
627
|
+
valueFormat: {
|
|
628
|
+
type: String,
|
|
629
|
+
default: 'id',
|
|
630
|
+
},
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* z-index of the menu.
|
|
634
|
+
*/
|
|
635
|
+
zIndex: {
|
|
636
|
+
type: [ Number, String ],
|
|
637
|
+
default: 999,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
|
|
641
|
+
data() {
|
|
642
|
+
return {
|
|
643
|
+
trigger: {
|
|
644
|
+
// Is the control focused?
|
|
645
|
+
isFocused: false,
|
|
646
|
+
// User entered search query - value of the input.
|
|
647
|
+
searchQuery: '',
|
|
648
|
+
},
|
|
649
|
+
|
|
650
|
+
menu: {
|
|
651
|
+
// Is the menu opened?
|
|
652
|
+
isOpen: false,
|
|
653
|
+
// Id of current highlighted option.
|
|
654
|
+
current: null,
|
|
655
|
+
// The scroll position before last menu closing.
|
|
656
|
+
lastScrollPosition: 0,
|
|
657
|
+
// Which direction to open the menu.
|
|
658
|
+
placement: 'bottom',
|
|
659
|
+
},
|
|
660
|
+
|
|
661
|
+
forest: {
|
|
662
|
+
// Normalized options.
|
|
663
|
+
normalizedOptions: [],
|
|
664
|
+
// <id, node> map for quick look-up.
|
|
665
|
+
nodeMap: createMap(),
|
|
666
|
+
// <id, checkedState> map, used for multi-select mode.
|
|
667
|
+
checkedStateMap: createMap(),
|
|
668
|
+
// Id list of all selected options.
|
|
669
|
+
selectedNodeIds: this.extractCheckedNodeIdsFromValue(),
|
|
670
|
+
// <id, true> map for fast checking:
|
|
671
|
+
// if (forest.selectedNodeIds.indexOf(id) !== -1) forest.selectedNodeMap[id] === true
|
|
672
|
+
selectedNodeMap: createMap(),
|
|
673
|
+
},
|
|
674
|
+
|
|
675
|
+
// States of root options.
|
|
676
|
+
rootOptionsStates: createAsyncOptionsStates(),
|
|
677
|
+
|
|
678
|
+
localSearch: {
|
|
679
|
+
// Has user entered any query to search local options?
|
|
680
|
+
active: false,
|
|
681
|
+
// Has any options matched the search query?
|
|
682
|
+
noResults: true,
|
|
683
|
+
// <id, countObject> map for counting matched children/descendants.
|
|
684
|
+
countMap: createMap(),
|
|
685
|
+
},
|
|
686
|
+
|
|
687
|
+
// <searchQuery, remoteSearchEntry> map.
|
|
688
|
+
remoteSearch: createMap(),
|
|
689
|
+
}
|
|
690
|
+
},
|
|
691
|
+
|
|
692
|
+
computed: {
|
|
693
|
+
/* eslint-disable valid-jsdoc */
|
|
694
|
+
/**
|
|
695
|
+
* Normalized nodes that have been selected.
|
|
696
|
+
* @type {node[]}
|
|
697
|
+
*/
|
|
698
|
+
selectedNodes() {
|
|
699
|
+
return this.forest.selectedNodeIds.map(this.getNode)
|
|
700
|
+
},
|
|
701
|
+
/**
|
|
702
|
+
* Id list of selected nodes with `sortValueBy` prop applied.
|
|
703
|
+
* @type {nodeId[]}
|
|
704
|
+
*/
|
|
705
|
+
internalValue() {
|
|
706
|
+
let internalValue
|
|
707
|
+
|
|
708
|
+
// istanbul ignore else
|
|
709
|
+
if (this.single || this.flat || this.disableBranchNodes || this.valueConsistsOf === ALL) {
|
|
710
|
+
internalValue = this.forest.selectedNodeIds.slice()
|
|
711
|
+
} else if (this.valueConsistsOf === BRANCH_PRIORITY) {
|
|
712
|
+
internalValue = this.forest.selectedNodeIds.filter(id => {
|
|
713
|
+
const node = this.getNode(id)
|
|
714
|
+
if (node.isRootNode) return true
|
|
715
|
+
return !this.isSelected(node.parentNode)
|
|
716
|
+
})
|
|
717
|
+
} else if (this.valueConsistsOf === LEAF_PRIORITY) {
|
|
718
|
+
internalValue = this.forest.selectedNodeIds.filter(id => {
|
|
719
|
+
const node = this.getNode(id)
|
|
720
|
+
if (node.isLeaf) return true
|
|
721
|
+
return node.children.length === 0
|
|
722
|
+
})
|
|
723
|
+
} else if (this.valueConsistsOf === ALL_WITH_INDETERMINATE) {
|
|
724
|
+
const indeterminateNodeIds = []
|
|
725
|
+
internalValue = this.forest.selectedNodeIds.slice()
|
|
726
|
+
this.selectedNodes.forEach(selectedNode => {
|
|
727
|
+
selectedNode.ancestors.forEach(ancestor => {
|
|
728
|
+
if (includes(indeterminateNodeIds, ancestor.id)) return
|
|
729
|
+
if (includes(internalValue, ancestor.id)) return
|
|
730
|
+
indeterminateNodeIds.push(ancestor.id)
|
|
731
|
+
})
|
|
732
|
+
})
|
|
733
|
+
internalValue.push(...indeterminateNodeIds)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
if (this.sortValueBy === LEVEL) {
|
|
737
|
+
internalValue.sort((a, b) => sortValueByLevel(this.getNode(a), this.getNode(b)))
|
|
738
|
+
} else if (this.sortValueBy === INDEX) {
|
|
739
|
+
internalValue.sort((a, b) => sortValueByIndex(this.getNode(a), this.getNode(b)))
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
return internalValue
|
|
743
|
+
},
|
|
744
|
+
/**
|
|
745
|
+
* Has any option been selected?
|
|
746
|
+
* @type {boolean}
|
|
747
|
+
*/
|
|
748
|
+
hasValue() {
|
|
749
|
+
return this.internalValue.length > 0
|
|
750
|
+
},
|
|
751
|
+
/**
|
|
752
|
+
* Single-select mode?
|
|
753
|
+
* @type {boolean}
|
|
754
|
+
*/
|
|
755
|
+
single() {
|
|
756
|
+
return !this.multiple
|
|
757
|
+
},
|
|
758
|
+
/**
|
|
759
|
+
* Id list of nodes displayed in the menu. Nodes that are considered NOT visible:
|
|
760
|
+
* - descendants of a collapsed branch node
|
|
761
|
+
* - in local search mode, nodes that are not matched, unless
|
|
762
|
+
* - it's a branch node and has matched descendants
|
|
763
|
+
* - it's a leaf node and its parent node is explicitly set to show all children
|
|
764
|
+
* @type {id[]}
|
|
765
|
+
*/
|
|
766
|
+
visibleOptionIds() {
|
|
767
|
+
const visibleOptionIds = []
|
|
768
|
+
|
|
769
|
+
this.traverseAllNodesByIndex(node => {
|
|
770
|
+
if (!this.localSearch.active || this.shouldOptionBeIncludedInSearchResult(node)) {
|
|
771
|
+
visibleOptionIds.push(node.id)
|
|
772
|
+
}
|
|
773
|
+
// Skip the traversal of descendants of a branch node if it's not expanded.
|
|
774
|
+
if (node.isBranch && !this.shouldExpand(node)) {
|
|
775
|
+
return false
|
|
776
|
+
}
|
|
777
|
+
})
|
|
778
|
+
|
|
779
|
+
return visibleOptionIds
|
|
780
|
+
},
|
|
781
|
+
/**
|
|
782
|
+
* Has any option should be displayed in the menu?
|
|
783
|
+
* @type {boolean}
|
|
784
|
+
*/
|
|
785
|
+
hasVisibleOptions() {
|
|
786
|
+
return this.visibleOptionIds.length !== 0
|
|
787
|
+
},
|
|
788
|
+
/**
|
|
789
|
+
* Should show children count when searching?
|
|
790
|
+
* @type {boolean}
|
|
791
|
+
*/
|
|
792
|
+
showCountOnSearchComputed() {
|
|
793
|
+
// Vue doesn't allow setting default prop value based on another prop value.
|
|
794
|
+
// So use computed property as a workaround.
|
|
795
|
+
// https://github.com/vuejs/vue/issues/6358
|
|
796
|
+
return typeof this.showCountOnSearch === 'boolean'
|
|
797
|
+
? this.showCountOnSearch
|
|
798
|
+
: this.showCount
|
|
799
|
+
},
|
|
800
|
+
/**
|
|
801
|
+
* Is there any branch node?
|
|
802
|
+
* @type {boolean}
|
|
803
|
+
*/
|
|
804
|
+
hasBranchNodes() {
|
|
805
|
+
return this.forest.normalizedOptions.some(rootNode => rootNode.isBranch)
|
|
806
|
+
},
|
|
807
|
+
shouldFlattenOptions() {
|
|
808
|
+
return this.localSearch.active && this.flattenSearchResults
|
|
809
|
+
},
|
|
810
|
+
/* eslint-enable valid-jsdoc */
|
|
811
|
+
},
|
|
812
|
+
|
|
813
|
+
watch: {
|
|
814
|
+
alwaysOpen(newValue) {
|
|
815
|
+
if (newValue) this.openMenu()
|
|
816
|
+
else this.closeMenu()
|
|
817
|
+
},
|
|
818
|
+
|
|
819
|
+
branchNodesFirst() {
|
|
820
|
+
this.initialize()
|
|
821
|
+
},
|
|
822
|
+
|
|
823
|
+
disabled(newValue) {
|
|
824
|
+
// force close the menu after disabling the control
|
|
825
|
+
if (newValue && this.menu.isOpen) this.closeMenu()
|
|
826
|
+
else if (!newValue && !this.menu.isOpen && this.alwaysOpen) this.openMenu()
|
|
827
|
+
},
|
|
828
|
+
|
|
829
|
+
flat() {
|
|
830
|
+
this.initialize()
|
|
831
|
+
},
|
|
832
|
+
|
|
833
|
+
internalValue(newValue, oldValue) {
|
|
834
|
+
const hasChanged = quickDiff(newValue, oldValue)
|
|
835
|
+
// #122
|
|
836
|
+
// Vue would trigger this watcher when `newValue` and `oldValue` are shallow-equal.
|
|
837
|
+
// We emit the `input` event only when the value actually changes.
|
|
838
|
+
if (hasChanged) this.$emit('input', this.getValue(), this.getInstanceId())
|
|
839
|
+
},
|
|
840
|
+
|
|
841
|
+
matchKeys() {
|
|
842
|
+
this.initialize()
|
|
843
|
+
},
|
|
844
|
+
|
|
845
|
+
multiple(newValue) {
|
|
846
|
+
// We need to rebuild the state when switching from single-select mode
|
|
847
|
+
// to multi-select mode.
|
|
848
|
+
// istanbul ignore else
|
|
849
|
+
if (newValue) this.buildForestState()
|
|
850
|
+
},
|
|
851
|
+
|
|
852
|
+
options: {
|
|
853
|
+
handler() {
|
|
854
|
+
if (this.async) return
|
|
855
|
+
// Re-initialize options when the `options` prop has changed.
|
|
856
|
+
this.initialize()
|
|
857
|
+
this.rootOptionsStates.isLoaded = Array.isArray(this.options)
|
|
858
|
+
},
|
|
859
|
+
deep: true,
|
|
860
|
+
immediate: true,
|
|
861
|
+
},
|
|
862
|
+
|
|
863
|
+
'trigger.searchQuery'() {
|
|
864
|
+
if (this.async) {
|
|
865
|
+
this.handleRemoteSearch()
|
|
866
|
+
} else {
|
|
867
|
+
this.handleLocalSearch()
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
this.$emit('search-change', this.trigger.searchQuery, this.getInstanceId())
|
|
871
|
+
},
|
|
872
|
+
|
|
873
|
+
value() {
|
|
874
|
+
const nodeIdsFromValue = this.extractCheckedNodeIdsFromValue()
|
|
875
|
+
const hasChanged = quickDiff(nodeIdsFromValue, this.internalValue)
|
|
876
|
+
if (hasChanged) this.fixSelectedNodeIds(nodeIdsFromValue)
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
|
|
880
|
+
methods: {
|
|
881
|
+
verifyProps() {
|
|
882
|
+
warning(
|
|
883
|
+
() => this.async ? this.searchable : true,
|
|
884
|
+
() => 'For async search mode, the value of "searchable" prop must be true.',
|
|
885
|
+
)
|
|
886
|
+
|
|
887
|
+
if (this.options == null && !this.loadOptions) {
|
|
888
|
+
warning(
|
|
889
|
+
() => false,
|
|
890
|
+
() => 'Are you meant to dynamically load options? You need to use "loadOptions" prop.',
|
|
891
|
+
)
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
if (this.flat) {
|
|
895
|
+
warning(
|
|
896
|
+
() => this.multiple,
|
|
897
|
+
() => 'You are using flat mode. But you forgot to add "multiple=true"?',
|
|
898
|
+
)
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
if (!this.flat) {
|
|
902
|
+
const propNames = [
|
|
903
|
+
'autoSelectAncestors',
|
|
904
|
+
'autoSelectDescendants',
|
|
905
|
+
'autoDeselectAncestors',
|
|
906
|
+
'autoDeselectDescendants',
|
|
907
|
+
]
|
|
908
|
+
|
|
909
|
+
propNames.forEach(propName => {
|
|
910
|
+
warning(
|
|
911
|
+
() => !this[propName],
|
|
912
|
+
() => `"${propName}" only applies to flat mode.`,
|
|
913
|
+
)
|
|
914
|
+
})
|
|
915
|
+
}
|
|
916
|
+
},
|
|
917
|
+
|
|
918
|
+
resetFlags() {
|
|
919
|
+
this._blurOnSelect = false
|
|
920
|
+
},
|
|
921
|
+
|
|
922
|
+
initialize() {
|
|
923
|
+
const options = this.async
|
|
924
|
+
? this.getRemoteSearchEntry().options
|
|
925
|
+
: this.options
|
|
926
|
+
|
|
927
|
+
if (Array.isArray(options)) {
|
|
928
|
+
// In case we are re-initializing options, keep the old state tree temporarily.
|
|
929
|
+
const prevNodeMap = this.forest.nodeMap
|
|
930
|
+
this.forest.nodeMap = createMap()
|
|
931
|
+
this.keepDataOfSelectedNodes(prevNodeMap)
|
|
932
|
+
this.forest.normalizedOptions = this.normalize(NO_PARENT_NODE, options, prevNodeMap)
|
|
933
|
+
// Cases that need fixing `selectedNodeIds`:
|
|
934
|
+
// 1) Children options of a checked node have been delayed loaded,
|
|
935
|
+
// we should also mark these children as checked. (multi-select mode)
|
|
936
|
+
// 2) Root options have been delayed loaded, we need to initialize states
|
|
937
|
+
// of these nodes. (multi-select mode)
|
|
938
|
+
// 3) Async search mode.
|
|
939
|
+
this.fixSelectedNodeIds(this.internalValue)
|
|
940
|
+
} else {
|
|
941
|
+
this.forest.normalizedOptions = []
|
|
942
|
+
}
|
|
943
|
+
},
|
|
944
|
+
|
|
945
|
+
getInstanceId() {
|
|
946
|
+
return this.instanceId == null ? this.id : this.instanceId
|
|
947
|
+
},
|
|
948
|
+
|
|
949
|
+
getValue() {
|
|
950
|
+
if (this.valueFormat === 'id') {
|
|
951
|
+
return this.multiple
|
|
952
|
+
? this.internalValue.slice()
|
|
953
|
+
: this.internalValue[0]
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
const rawNodes = this.internalValue.map(id => this.getNode(id).raw)
|
|
957
|
+
return this.multiple ? rawNodes : rawNodes[0]
|
|
958
|
+
},
|
|
959
|
+
|
|
960
|
+
getNode(nodeId) {
|
|
961
|
+
warning(
|
|
962
|
+
() => nodeId != null,
|
|
963
|
+
() => `Invalid node id: ${nodeId}`,
|
|
964
|
+
)
|
|
965
|
+
|
|
966
|
+
if (nodeId == null) return null
|
|
967
|
+
|
|
968
|
+
return nodeId in this.forest.nodeMap
|
|
969
|
+
? this.forest.nodeMap[nodeId]
|
|
970
|
+
: this.createFallbackNode(nodeId)
|
|
971
|
+
},
|
|
972
|
+
|
|
973
|
+
createFallbackNode(id) {
|
|
974
|
+
// In case there is a default selected node that is not loaded into the tree yet,
|
|
975
|
+
// we create a fallback node to keep the component working.
|
|
976
|
+
// When the real data is loaded, we'll override this fake node.
|
|
977
|
+
|
|
978
|
+
const raw = this.extractNodeFromValue(id)
|
|
979
|
+
const label = this.enhancedNormalizer(raw).label || `${id} (unknown)`
|
|
980
|
+
const fallbackNode = {
|
|
981
|
+
id,
|
|
982
|
+
label,
|
|
983
|
+
ancestors: [],
|
|
984
|
+
parentNode: NO_PARENT_NODE,
|
|
985
|
+
isFallbackNode: true,
|
|
986
|
+
isRootNode: true,
|
|
987
|
+
isLeaf: true,
|
|
988
|
+
isBranch: false,
|
|
989
|
+
isDisabled: false,
|
|
990
|
+
isNew: false,
|
|
991
|
+
index: [ -1 ],
|
|
992
|
+
level: 0,
|
|
993
|
+
raw,
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
return this.$set(this.forest.nodeMap, id, fallbackNode)
|
|
997
|
+
},
|
|
998
|
+
|
|
999
|
+
extractCheckedNodeIdsFromValue() {
|
|
1000
|
+
if (this.value == null) return []
|
|
1001
|
+
|
|
1002
|
+
if (this.valueFormat === 'id') {
|
|
1003
|
+
return this.multiple
|
|
1004
|
+
? this.value.slice()
|
|
1005
|
+
: [ this.value ]
|
|
1006
|
+
}
|
|
1007
|
+
|
|
1008
|
+
return (this.multiple ? this.value : [ this.value ])
|
|
1009
|
+
.map(node => this.enhancedNormalizer(node))
|
|
1010
|
+
.map(node => node.id)
|
|
1011
|
+
},
|
|
1012
|
+
|
|
1013
|
+
extractNodeFromValue(id) {
|
|
1014
|
+
const defaultNode = { id }
|
|
1015
|
+
|
|
1016
|
+
if (this.valueFormat === 'id') {
|
|
1017
|
+
return defaultNode
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
const valueArray = this.multiple
|
|
1021
|
+
? Array.isArray(this.value) ? this.value : []
|
|
1022
|
+
: this.value ? [ this.value ] : []
|
|
1023
|
+
const matched = find(
|
|
1024
|
+
valueArray,
|
|
1025
|
+
node => node && this.enhancedNormalizer(node).id === id,
|
|
1026
|
+
)
|
|
1027
|
+
|
|
1028
|
+
return matched || defaultNode
|
|
1029
|
+
},
|
|
1030
|
+
|
|
1031
|
+
fixSelectedNodeIds(nodeIdListOfPrevValue) {
|
|
1032
|
+
let nextSelectedNodeIds = []
|
|
1033
|
+
|
|
1034
|
+
// istanbul ignore else
|
|
1035
|
+
if (this.single || this.flat || this.disableBranchNodes || this.valueConsistsOf === ALL) {
|
|
1036
|
+
nextSelectedNodeIds = nodeIdListOfPrevValue
|
|
1037
|
+
} else if (this.valueConsistsOf === BRANCH_PRIORITY) {
|
|
1038
|
+
nodeIdListOfPrevValue.forEach(nodeId => {
|
|
1039
|
+
nextSelectedNodeIds.push(nodeId)
|
|
1040
|
+
const node = this.getNode(nodeId)
|
|
1041
|
+
if (node.isBranch) this.traverseDescendantsBFS(node, descendant => {
|
|
1042
|
+
nextSelectedNodeIds.push(descendant.id)
|
|
1043
|
+
})
|
|
1044
|
+
})
|
|
1045
|
+
} else if (this.valueConsistsOf === LEAF_PRIORITY) {
|
|
1046
|
+
const map = createMap()
|
|
1047
|
+
const queue = nodeIdListOfPrevValue.slice()
|
|
1048
|
+
while (queue.length) {
|
|
1049
|
+
const nodeId = queue.shift()
|
|
1050
|
+
const node = this.getNode(nodeId)
|
|
1051
|
+
nextSelectedNodeIds.push(nodeId)
|
|
1052
|
+
if (node.isRootNode) continue
|
|
1053
|
+
if (!(node.parentNode.id in map)) map[node.parentNode.id] = node.parentNode.children.length
|
|
1054
|
+
if (--map[node.parentNode.id] === 0) queue.push(node.parentNode.id)
|
|
1055
|
+
}
|
|
1056
|
+
} else if (this.valueConsistsOf === ALL_WITH_INDETERMINATE) {
|
|
1057
|
+
const map = createMap()
|
|
1058
|
+
const queue = nodeIdListOfPrevValue.filter(nodeId => {
|
|
1059
|
+
const node = this.getNode(nodeId)
|
|
1060
|
+
return node.isLeaf || node.children.length === 0
|
|
1061
|
+
})
|
|
1062
|
+
while (queue.length) {
|
|
1063
|
+
const nodeId = queue.shift()
|
|
1064
|
+
const node = this.getNode(nodeId)
|
|
1065
|
+
nextSelectedNodeIds.push(nodeId)
|
|
1066
|
+
if (node.isRootNode) continue
|
|
1067
|
+
if (!(node.parentNode.id in map)) map[node.parentNode.id] = node.parentNode.children.length
|
|
1068
|
+
if (--map[node.parentNode.id] === 0) queue.push(node.parentNode.id)
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
const hasChanged = quickDiff(this.forest.selectedNodeIds, nextSelectedNodeIds)
|
|
1073
|
+
// If `nextSelectedNodeIds` doesn't actually differ from old `selectedNodeIds`,
|
|
1074
|
+
// we don't make the assignment to avoid triggering its watchers which may cause
|
|
1075
|
+
// unnecessary calculations.
|
|
1076
|
+
if (hasChanged) this.forest.selectedNodeIds = nextSelectedNodeIds
|
|
1077
|
+
|
|
1078
|
+
this.buildForestState()
|
|
1079
|
+
},
|
|
1080
|
+
|
|
1081
|
+
keepDataOfSelectedNodes(prevNodeMap) {
|
|
1082
|
+
// In case there is any selected node that is not present in the new `options` array.
|
|
1083
|
+
// It could be useful for async search mode.
|
|
1084
|
+
this.forest.selectedNodeIds.forEach(id => {
|
|
1085
|
+
if (!prevNodeMap[id]) return
|
|
1086
|
+
const node = {
|
|
1087
|
+
...prevNodeMap[id],
|
|
1088
|
+
isFallbackNode: true,
|
|
1089
|
+
}
|
|
1090
|
+
this.$set(this.forest.nodeMap, id, node)
|
|
1091
|
+
})
|
|
1092
|
+
},
|
|
1093
|
+
|
|
1094
|
+
isSelected(node) {
|
|
1095
|
+
// whether a node is selected (single-select mode) or fully-checked (multi-select mode)
|
|
1096
|
+
return this.forest.selectedNodeMap[node.id] === true
|
|
1097
|
+
},
|
|
1098
|
+
|
|
1099
|
+
traverseDescendantsBFS(parentNode, callback) {
|
|
1100
|
+
// istanbul ignore if
|
|
1101
|
+
if (!parentNode.isBranch) return
|
|
1102
|
+
const queue = parentNode.children.slice()
|
|
1103
|
+
while (queue.length) {
|
|
1104
|
+
const currNode = queue[0]
|
|
1105
|
+
if (currNode.isBranch) queue.push(...currNode.children)
|
|
1106
|
+
callback(currNode)
|
|
1107
|
+
queue.shift()
|
|
1108
|
+
}
|
|
1109
|
+
},
|
|
1110
|
+
|
|
1111
|
+
traverseDescendantsDFS(parentNode, callback) {
|
|
1112
|
+
if (!parentNode.isBranch) return
|
|
1113
|
+
parentNode.children.forEach(child => {
|
|
1114
|
+
// deep-level node first
|
|
1115
|
+
this.traverseDescendantsDFS(child, callback)
|
|
1116
|
+
callback(child)
|
|
1117
|
+
})
|
|
1118
|
+
},
|
|
1119
|
+
|
|
1120
|
+
traverseAllNodesDFS(callback) {
|
|
1121
|
+
this.forest.normalizedOptions.forEach(rootNode => {
|
|
1122
|
+
// deep-level node first
|
|
1123
|
+
this.traverseDescendantsDFS(rootNode, callback)
|
|
1124
|
+
callback(rootNode)
|
|
1125
|
+
})
|
|
1126
|
+
},
|
|
1127
|
+
|
|
1128
|
+
traverseAllNodesByIndex(callback) {
|
|
1129
|
+
const walk = parentNode => {
|
|
1130
|
+
parentNode.children.forEach(child => {
|
|
1131
|
+
if (callback(child) !== false && child.isBranch) {
|
|
1132
|
+
walk(child)
|
|
1133
|
+
}
|
|
1134
|
+
})
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
// To simplify the code logic of traversal,
|
|
1138
|
+
// we create a fake root node that holds all the root options.
|
|
1139
|
+
walk({ children: this.forest.normalizedOptions })
|
|
1140
|
+
},
|
|
1141
|
+
|
|
1142
|
+
toggleClickOutsideEvent(enabled) {
|
|
1143
|
+
if (enabled) {
|
|
1144
|
+
document.addEventListener('mousedown', this.handleClickOutside, false)
|
|
1145
|
+
} else {
|
|
1146
|
+
document.removeEventListener('mousedown', this.handleClickOutside, false)
|
|
1147
|
+
}
|
|
1148
|
+
},
|
|
1149
|
+
|
|
1150
|
+
getValueContainer() {
|
|
1151
|
+
return this.$refs.control.$refs['value-container']
|
|
1152
|
+
},
|
|
1153
|
+
|
|
1154
|
+
getInput() {
|
|
1155
|
+
return this.getValueContainer().$refs.input
|
|
1156
|
+
},
|
|
1157
|
+
|
|
1158
|
+
focusInput() {
|
|
1159
|
+
this.getInput().focus()
|
|
1160
|
+
},
|
|
1161
|
+
|
|
1162
|
+
blurInput() {
|
|
1163
|
+
this.getInput().blur()
|
|
1164
|
+
},
|
|
1165
|
+
|
|
1166
|
+
handleMouseDown: onLeftClick(function handleMouseDown(evt) {
|
|
1167
|
+
evt.preventDefault()
|
|
1168
|
+
evt.stopPropagation()
|
|
1169
|
+
|
|
1170
|
+
if (this.disabled) return
|
|
1171
|
+
|
|
1172
|
+
const isClickedOnValueContainer = this.getValueContainer().$el.contains(evt.target)
|
|
1173
|
+
if (isClickedOnValueContainer && !this.menu.isOpen && (this.openOnClick || this.trigger.isFocused)) {
|
|
1174
|
+
this.openMenu()
|
|
1175
|
+
}
|
|
1176
|
+
|
|
1177
|
+
if (this._blurOnSelect) {
|
|
1178
|
+
this.blurInput()
|
|
1179
|
+
} else {
|
|
1180
|
+
// Focus the input or prevent blurring.
|
|
1181
|
+
this.focusInput()
|
|
1182
|
+
}
|
|
1183
|
+
|
|
1184
|
+
this.resetFlags()
|
|
1185
|
+
}),
|
|
1186
|
+
|
|
1187
|
+
handleClickOutside(evt) {
|
|
1188
|
+
// istanbul ignore else
|
|
1189
|
+
if (this.$refs.wrapper && !this.$refs.wrapper.contains(evt.target)) {
|
|
1190
|
+
this.blurInput()
|
|
1191
|
+
this.closeMenu()
|
|
1192
|
+
}
|
|
1193
|
+
},
|
|
1194
|
+
|
|
1195
|
+
handleLocalSearch() {
|
|
1196
|
+
const { searchQuery } = this.trigger
|
|
1197
|
+
const done = () => this.resetHighlightedOptionWhenNecessary(true)
|
|
1198
|
+
|
|
1199
|
+
if (!searchQuery) {
|
|
1200
|
+
// Exit sync search mode.
|
|
1201
|
+
this.localSearch.active = false
|
|
1202
|
+
return done()
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
// Enter sync search mode.
|
|
1206
|
+
this.localSearch.active = true
|
|
1207
|
+
|
|
1208
|
+
// Reset states.
|
|
1209
|
+
this.localSearch.noResults = true
|
|
1210
|
+
this.traverseAllNodesDFS(node => {
|
|
1211
|
+
if (node.isBranch) {
|
|
1212
|
+
node.isExpandedOnSearch = false
|
|
1213
|
+
node.showAllChildrenOnSearch = false
|
|
1214
|
+
node.isMatched = false
|
|
1215
|
+
node.hasMatchedDescendants = false
|
|
1216
|
+
this.$set(this.localSearch.countMap, node.id, {
|
|
1217
|
+
[ALL_CHILDREN]: 0,
|
|
1218
|
+
[ALL_DESCENDANTS]: 0,
|
|
1219
|
+
[LEAF_CHILDREN]: 0,
|
|
1220
|
+
[LEAF_DESCENDANTS]: 0,
|
|
1221
|
+
})
|
|
1222
|
+
}
|
|
1223
|
+
})
|
|
1224
|
+
|
|
1225
|
+
const lowerCasedSearchQuery = searchQuery.trim().toLocaleLowerCase()
|
|
1226
|
+
const splitSearchQuery = lowerCasedSearchQuery.replace(/\s+/g, ' ').split(' ')
|
|
1227
|
+
this.traverseAllNodesDFS(node => {
|
|
1228
|
+
if (this.searchNested && splitSearchQuery.length > 1) {
|
|
1229
|
+
node.isMatched = splitSearchQuery.every(filterValue =>
|
|
1230
|
+
match(false, filterValue, node.nestedSearchLabel),
|
|
1231
|
+
)
|
|
1232
|
+
} else {
|
|
1233
|
+
node.isMatched = this.matchKeys.some(matchKey =>
|
|
1234
|
+
match(!this.disableFuzzyMatching, lowerCasedSearchQuery, node.lowerCased[matchKey]),
|
|
1235
|
+
)
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
if (node.isMatched) {
|
|
1239
|
+
this.localSearch.noResults = false
|
|
1240
|
+
node.ancestors.forEach(ancestor => this.localSearch.countMap[ancestor.id][ALL_DESCENDANTS]++)
|
|
1241
|
+
if (node.isLeaf) node.ancestors.forEach(ancestor => this.localSearch.countMap[ancestor.id][LEAF_DESCENDANTS]++)
|
|
1242
|
+
if (node.parentNode !== NO_PARENT_NODE) {
|
|
1243
|
+
this.localSearch.countMap[node.parentNode.id][ALL_CHILDREN] += 1
|
|
1244
|
+
// istanbul ignore else
|
|
1245
|
+
if (node.isLeaf) this.localSearch.countMap[node.parentNode.id][LEAF_CHILDREN] += 1
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
|
|
1249
|
+
if (
|
|
1250
|
+
(node.isMatched || (node.isBranch && node.isExpandedOnSearch)) &&
|
|
1251
|
+
node.parentNode !== NO_PARENT_NODE
|
|
1252
|
+
) {
|
|
1253
|
+
node.parentNode.isExpandedOnSearch = true
|
|
1254
|
+
node.parentNode.hasMatchedDescendants = true
|
|
1255
|
+
}
|
|
1256
|
+
})
|
|
1257
|
+
|
|
1258
|
+
done()
|
|
1259
|
+
},
|
|
1260
|
+
|
|
1261
|
+
handleRemoteSearch() {
|
|
1262
|
+
const { searchQuery } = this.trigger
|
|
1263
|
+
const entry = this.getRemoteSearchEntry()
|
|
1264
|
+
const done = () => {
|
|
1265
|
+
this.initialize()
|
|
1266
|
+
this.resetHighlightedOptionWhenNecessary(true)
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
if ((searchQuery === '' || this.cacheOptions) && entry.isLoaded) {
|
|
1270
|
+
return done()
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
this.callLoadOptionsProp({
|
|
1274
|
+
action: ASYNC_SEARCH,
|
|
1275
|
+
args: { searchQuery },
|
|
1276
|
+
isPending() {
|
|
1277
|
+
return entry.isLoading
|
|
1278
|
+
},
|
|
1279
|
+
start: () => {
|
|
1280
|
+
entry.isLoading = true
|
|
1281
|
+
entry.isLoaded = false
|
|
1282
|
+
entry.loadingError = ''
|
|
1283
|
+
},
|
|
1284
|
+
succeed: options => {
|
|
1285
|
+
entry.isLoaded = true
|
|
1286
|
+
entry.options = options
|
|
1287
|
+
// When the request completes, the search query may have changed.
|
|
1288
|
+
// We only show these options if they are for the current search query.
|
|
1289
|
+
if (this.trigger.searchQuery === searchQuery) done()
|
|
1290
|
+
},
|
|
1291
|
+
fail: err => {
|
|
1292
|
+
entry.loadingError = getErrorMessage(err)
|
|
1293
|
+
},
|
|
1294
|
+
end: () => {
|
|
1295
|
+
entry.isLoading = false
|
|
1296
|
+
},
|
|
1297
|
+
})
|
|
1298
|
+
},
|
|
1299
|
+
|
|
1300
|
+
getRemoteSearchEntry() {
|
|
1301
|
+
const { searchQuery } = this.trigger
|
|
1302
|
+
const entry = this.remoteSearch[searchQuery] || {
|
|
1303
|
+
...createAsyncOptionsStates(),
|
|
1304
|
+
options: [],
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
// Vue doesn't support directly watching on objects.
|
|
1308
|
+
this.$watch(
|
|
1309
|
+
() => entry.options,
|
|
1310
|
+
() => {
|
|
1311
|
+
// TODO: potential redundant re-initialization.
|
|
1312
|
+
if (this.trigger.searchQuery === searchQuery) this.initialize()
|
|
1313
|
+
},
|
|
1314
|
+
{ deep: true },
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
if (searchQuery === '') {
|
|
1318
|
+
if (Array.isArray(this.defaultOptions)) {
|
|
1319
|
+
entry.options = this.defaultOptions
|
|
1320
|
+
entry.isLoaded = true
|
|
1321
|
+
return entry
|
|
1322
|
+
} else if (this.defaultOptions !== true) {
|
|
1323
|
+
entry.isLoaded = true
|
|
1324
|
+
return entry
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
|
|
1328
|
+
if (!this.remoteSearch[searchQuery]) {
|
|
1329
|
+
this.$set(this.remoteSearch, searchQuery, entry)
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
return entry
|
|
1333
|
+
},
|
|
1334
|
+
|
|
1335
|
+
shouldExpand(node) {
|
|
1336
|
+
return this.localSearch.active ? node.isExpandedOnSearch : node.isExpanded
|
|
1337
|
+
},
|
|
1338
|
+
|
|
1339
|
+
shouldOptionBeIncludedInSearchResult(node) {
|
|
1340
|
+
// 1) This option is matched.
|
|
1341
|
+
if (node.isMatched) return true
|
|
1342
|
+
// 2) This option is not matched, but has matched descendant(s).
|
|
1343
|
+
if (node.isBranch && node.hasMatchedDescendants && !this.flattenSearchResults) return true
|
|
1344
|
+
// 3) This option's parent has no matched descendants,
|
|
1345
|
+
// but after being expanded, all its children should be shown.
|
|
1346
|
+
if (!node.isRootNode && node.parentNode.showAllChildrenOnSearch) return true
|
|
1347
|
+
// 4) The default case.
|
|
1348
|
+
return false
|
|
1349
|
+
},
|
|
1350
|
+
|
|
1351
|
+
shouldShowOptionInMenu(node) {
|
|
1352
|
+
if (this.localSearch.active && !this.shouldOptionBeIncludedInSearchResult(node)) {
|
|
1353
|
+
return false
|
|
1354
|
+
}
|
|
1355
|
+
return true
|
|
1356
|
+
},
|
|
1357
|
+
|
|
1358
|
+
getControl() {
|
|
1359
|
+
return this.$refs.control.$el
|
|
1360
|
+
},
|
|
1361
|
+
|
|
1362
|
+
getMenu() {
|
|
1363
|
+
const ref = this.appendToBody ? this.$refs.portal.portalTarget : this
|
|
1364
|
+
const $menu = ref.$refs.menu.$refs.menu
|
|
1365
|
+
return $menu && $menu.nodeName !== '#comment' ? $menu : null
|
|
1366
|
+
},
|
|
1367
|
+
|
|
1368
|
+
setCurrentHighlightedOption(node, scroll = true) {
|
|
1369
|
+
const prev = this.menu.current
|
|
1370
|
+
if (prev != null && prev in this.forest.nodeMap) {
|
|
1371
|
+
this.forest.nodeMap[prev].isHighlighted = false
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
this.menu.current = node.id
|
|
1375
|
+
node.isHighlighted = true
|
|
1376
|
+
|
|
1377
|
+
if (this.menu.isOpen && scroll) {
|
|
1378
|
+
const scrollToOption = () => {
|
|
1379
|
+
const $menu = this.getMenu()
|
|
1380
|
+
const $option = $menu.querySelector(`.rp-treeselect__option[data-id="${node.id}"]`)
|
|
1381
|
+
if ($option) scrollIntoView($menu, $option)
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
// In case `openMenu()` is just called and the menu is not rendered yet.
|
|
1385
|
+
if (this.getMenu()) {
|
|
1386
|
+
scrollToOption()
|
|
1387
|
+
} else {
|
|
1388
|
+
// istanbul ignore next
|
|
1389
|
+
this.$nextTick(scrollToOption)
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
},
|
|
1393
|
+
|
|
1394
|
+
resetHighlightedOptionWhenNecessary(forceReset = false) {
|
|
1395
|
+
const { current } = this.menu
|
|
1396
|
+
|
|
1397
|
+
if (
|
|
1398
|
+
forceReset || current == null ||
|
|
1399
|
+
!(current in this.forest.nodeMap) ||
|
|
1400
|
+
!this.shouldShowOptionInMenu(this.getNode(current))
|
|
1401
|
+
) {
|
|
1402
|
+
this.highlightFirstOption()
|
|
1403
|
+
}
|
|
1404
|
+
},
|
|
1405
|
+
|
|
1406
|
+
highlightFirstOption() {
|
|
1407
|
+
if (!this.hasVisibleOptions) return
|
|
1408
|
+
|
|
1409
|
+
const first = this.visibleOptionIds[0]
|
|
1410
|
+
this.setCurrentHighlightedOption(this.getNode(first))
|
|
1411
|
+
},
|
|
1412
|
+
|
|
1413
|
+
highlightPrevOption() {
|
|
1414
|
+
if (!this.hasVisibleOptions) return
|
|
1415
|
+
|
|
1416
|
+
const prev = this.visibleOptionIds.indexOf(this.menu.current) - 1
|
|
1417
|
+
if (prev === -1) return this.highlightLastOption()
|
|
1418
|
+
this.setCurrentHighlightedOption(this.getNode(this.visibleOptionIds[prev]))
|
|
1419
|
+
},
|
|
1420
|
+
|
|
1421
|
+
highlightNextOption() {
|
|
1422
|
+
if (!this.hasVisibleOptions) return
|
|
1423
|
+
|
|
1424
|
+
const next = this.visibleOptionIds.indexOf(this.menu.current) + 1
|
|
1425
|
+
if (next === this.visibleOptionIds.length) return this.highlightFirstOption()
|
|
1426
|
+
this.setCurrentHighlightedOption(this.getNode(this.visibleOptionIds[next]))
|
|
1427
|
+
},
|
|
1428
|
+
|
|
1429
|
+
highlightLastOption() {
|
|
1430
|
+
if (!this.hasVisibleOptions) return
|
|
1431
|
+
|
|
1432
|
+
const last = getLast(this.visibleOptionIds)
|
|
1433
|
+
this.setCurrentHighlightedOption(this.getNode(last))
|
|
1434
|
+
},
|
|
1435
|
+
|
|
1436
|
+
resetSearchQuery() {
|
|
1437
|
+
this.trigger.searchQuery = ''
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1440
|
+
closeMenu() {
|
|
1441
|
+
if (!this.menu.isOpen || (!this.disabled && this.alwaysOpen)) return
|
|
1442
|
+
this.saveMenuScrollPosition()
|
|
1443
|
+
this.menu.isOpen = false
|
|
1444
|
+
this.toggleClickOutsideEvent(false)
|
|
1445
|
+
this.resetSearchQuery()
|
|
1446
|
+
this.$emit('close', this.getValue(), this.getInstanceId())
|
|
1447
|
+
},
|
|
1448
|
+
|
|
1449
|
+
openMenu() {
|
|
1450
|
+
if (this.disabled || this.menu.isOpen) return
|
|
1451
|
+
this.menu.isOpen = true
|
|
1452
|
+
this.$nextTick(this.resetHighlightedOptionWhenNecessary)
|
|
1453
|
+
this.$nextTick(this.restoreMenuScrollPosition)
|
|
1454
|
+
if (!this.options && !this.async) this.loadRootOptions()
|
|
1455
|
+
this.toggleClickOutsideEvent(true)
|
|
1456
|
+
this.$emit('open', this.getInstanceId())
|
|
1457
|
+
},
|
|
1458
|
+
|
|
1459
|
+
toggleMenu() {
|
|
1460
|
+
if (this.menu.isOpen) {
|
|
1461
|
+
this.closeMenu()
|
|
1462
|
+
} else {
|
|
1463
|
+
this.openMenu()
|
|
1464
|
+
}
|
|
1465
|
+
},
|
|
1466
|
+
|
|
1467
|
+
toggleExpanded(node) {
|
|
1468
|
+
let nextState
|
|
1469
|
+
|
|
1470
|
+
if (this.localSearch.active) {
|
|
1471
|
+
nextState = node.isExpandedOnSearch = !node.isExpandedOnSearch
|
|
1472
|
+
if (nextState) node.showAllChildrenOnSearch = true
|
|
1473
|
+
} else {
|
|
1474
|
+
nextState = node.isExpanded = !node.isExpanded
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (nextState && !node.childrenStates.isLoaded) {
|
|
1478
|
+
this.loadChildrenOptions(node)
|
|
1479
|
+
}
|
|
1480
|
+
},
|
|
1481
|
+
|
|
1482
|
+
buildForestState() {
|
|
1483
|
+
const selectedNodeMap = createMap()
|
|
1484
|
+
this.forest.selectedNodeIds.forEach(selectedNodeId => {
|
|
1485
|
+
selectedNodeMap[selectedNodeId] = true
|
|
1486
|
+
})
|
|
1487
|
+
this.forest.selectedNodeMap = selectedNodeMap
|
|
1488
|
+
|
|
1489
|
+
const checkedStateMap = createMap()
|
|
1490
|
+
if (this.multiple) {
|
|
1491
|
+
this.traverseAllNodesByIndex(node => {
|
|
1492
|
+
checkedStateMap[node.id] = UNCHECKED
|
|
1493
|
+
})
|
|
1494
|
+
|
|
1495
|
+
this.selectedNodes.forEach(selectedNode => {
|
|
1496
|
+
checkedStateMap[selectedNode.id] = CHECKED
|
|
1497
|
+
|
|
1498
|
+
if (!this.flat && !this.disableBranchNodes) {
|
|
1499
|
+
selectedNode.ancestors.forEach(ancestorNode => {
|
|
1500
|
+
if (!this.isSelected(ancestorNode)) {
|
|
1501
|
+
checkedStateMap[ancestorNode.id] = INDETERMINATE
|
|
1502
|
+
}
|
|
1503
|
+
})
|
|
1504
|
+
}
|
|
1505
|
+
})
|
|
1506
|
+
}
|
|
1507
|
+
this.forest.checkedStateMap = checkedStateMap
|
|
1508
|
+
},
|
|
1509
|
+
|
|
1510
|
+
enhancedNormalizer(raw) {
|
|
1511
|
+
return {
|
|
1512
|
+
...raw,
|
|
1513
|
+
...this.normalizer(raw, this.getInstanceId()),
|
|
1514
|
+
}
|
|
1515
|
+
},
|
|
1516
|
+
|
|
1517
|
+
normalize(parentNode, nodes, prevNodeMap) {
|
|
1518
|
+
let normalizedOptions = nodes
|
|
1519
|
+
.map(node => [ this.enhancedNormalizer(node), node ])
|
|
1520
|
+
.map(([ node, raw ], index) => {
|
|
1521
|
+
this.checkDuplication(node)
|
|
1522
|
+
this.verifyNodeShape(node)
|
|
1523
|
+
|
|
1524
|
+
const { id, label, children, isDefaultExpanded } = node
|
|
1525
|
+
const isRootNode = parentNode === NO_PARENT_NODE
|
|
1526
|
+
const level = isRootNode ? 0 : parentNode.level + 1
|
|
1527
|
+
const isBranch = Array.isArray(children) || children === null
|
|
1528
|
+
const isLeaf = !isBranch
|
|
1529
|
+
const isDisabled = !!node.isDisabled || (!this.flat && !isRootNode && parentNode.isDisabled)
|
|
1530
|
+
const isNew = !!node.isNew
|
|
1531
|
+
const lowerCased = this.matchKeys.reduce((prev, key) => ({
|
|
1532
|
+
...prev,
|
|
1533
|
+
[key]: stringifyOptionPropValue(node[key]).toLocaleLowerCase(),
|
|
1534
|
+
}), {})
|
|
1535
|
+
const nestedSearchLabel = isRootNode
|
|
1536
|
+
? lowerCased.label
|
|
1537
|
+
: parentNode.nestedSearchLabel + ' ' + lowerCased.label
|
|
1538
|
+
|
|
1539
|
+
const normalized = this.$set(this.forest.nodeMap, id, createMap())
|
|
1540
|
+
this.$set(normalized, 'id', id)
|
|
1541
|
+
this.$set(normalized, 'label', label)
|
|
1542
|
+
this.$set(normalized, 'level', level)
|
|
1543
|
+
this.$set(normalized, 'ancestors', isRootNode ? [] : [ parentNode ].concat(parentNode.ancestors))
|
|
1544
|
+
this.$set(normalized, 'index', (isRootNode ? [] : parentNode.index).concat(index))
|
|
1545
|
+
this.$set(normalized, 'parentNode', parentNode)
|
|
1546
|
+
this.$set(normalized, 'lowerCased', lowerCased)
|
|
1547
|
+
this.$set(normalized, 'nestedSearchLabel', nestedSearchLabel)
|
|
1548
|
+
this.$set(normalized, 'isDisabled', isDisabled)
|
|
1549
|
+
this.$set(normalized, 'isNew', isNew)
|
|
1550
|
+
this.$set(normalized, 'isMatched', false)
|
|
1551
|
+
this.$set(normalized, 'isHighlighted', false)
|
|
1552
|
+
this.$set(normalized, 'isBranch', isBranch)
|
|
1553
|
+
this.$set(normalized, 'isLeaf', isLeaf)
|
|
1554
|
+
this.$set(normalized, 'isRootNode', isRootNode)
|
|
1555
|
+
this.$set(normalized, 'raw', raw)
|
|
1556
|
+
|
|
1557
|
+
if (isBranch) {
|
|
1558
|
+
const isLoaded = Array.isArray(children)
|
|
1559
|
+
|
|
1560
|
+
this.$set(normalized, 'childrenStates', {
|
|
1561
|
+
...createAsyncOptionsStates(),
|
|
1562
|
+
isLoaded,
|
|
1563
|
+
})
|
|
1564
|
+
this.$set(normalized, 'isExpanded', typeof isDefaultExpanded === 'boolean'
|
|
1565
|
+
? isDefaultExpanded
|
|
1566
|
+
: level < this.defaultExpandLevel)
|
|
1567
|
+
this.$set(normalized, 'hasMatchedDescendants', false)
|
|
1568
|
+
this.$set(normalized, 'hasDisabledDescendants', false)
|
|
1569
|
+
this.$set(normalized, 'isExpandedOnSearch', false)
|
|
1570
|
+
this.$set(normalized, 'showAllChildrenOnSearch', false)
|
|
1571
|
+
this.$set(normalized, 'count', {
|
|
1572
|
+
[ALL_CHILDREN]: 0,
|
|
1573
|
+
[ALL_DESCENDANTS]: 0,
|
|
1574
|
+
[LEAF_CHILDREN]: 0,
|
|
1575
|
+
[LEAF_DESCENDANTS]: 0,
|
|
1576
|
+
})
|
|
1577
|
+
this.$set(normalized, 'children', isLoaded
|
|
1578
|
+
? this.normalize(normalized, children, prevNodeMap)
|
|
1579
|
+
: [])
|
|
1580
|
+
|
|
1581
|
+
if (isDefaultExpanded === true) normalized.ancestors.forEach(ancestor => {
|
|
1582
|
+
ancestor.isExpanded = true
|
|
1583
|
+
})
|
|
1584
|
+
|
|
1585
|
+
if (!isLoaded && typeof this.loadOptions !== 'function') {
|
|
1586
|
+
warning(
|
|
1587
|
+
() => false,
|
|
1588
|
+
() => 'Unloaded branch node detected. "loadOptions" prop is required to load its children.',
|
|
1589
|
+
)
|
|
1590
|
+
} else if (!isLoaded && normalized.isExpanded) {
|
|
1591
|
+
this.loadChildrenOptions(normalized)
|
|
1592
|
+
}
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
normalized.ancestors.forEach(ancestor => ancestor.count[ALL_DESCENDANTS]++)
|
|
1596
|
+
if (isLeaf) normalized.ancestors.forEach(ancestor => ancestor.count[LEAF_DESCENDANTS]++)
|
|
1597
|
+
if (!isRootNode) {
|
|
1598
|
+
parentNode.count[ALL_CHILDREN] += 1
|
|
1599
|
+
if (isLeaf) parentNode.count[LEAF_CHILDREN] += 1
|
|
1600
|
+
if (isDisabled) parentNode.hasDisabledDescendants = true
|
|
1601
|
+
}
|
|
1602
|
+
|
|
1603
|
+
// Preserve previous states.
|
|
1604
|
+
if (prevNodeMap && prevNodeMap[id]) {
|
|
1605
|
+
const prev = prevNodeMap[id]
|
|
1606
|
+
|
|
1607
|
+
normalized.isMatched = prev.isMatched
|
|
1608
|
+
normalized.showAllChildrenOnSearch = prev.showAllChildrenOnSearch
|
|
1609
|
+
normalized.isHighlighted = prev.isHighlighted
|
|
1610
|
+
|
|
1611
|
+
if (prev.isBranch && normalized.isBranch) {
|
|
1612
|
+
normalized.isExpanded = prev.isExpanded
|
|
1613
|
+
normalized.isExpandedOnSearch = prev.isExpandedOnSearch
|
|
1614
|
+
// #97
|
|
1615
|
+
// If `isLoaded` was true, but IS NOT now, we consider this branch node
|
|
1616
|
+
// to be reset to unloaded state by the user of this component.
|
|
1617
|
+
if (prev.childrenStates.isLoaded && !normalized.childrenStates.isLoaded) {
|
|
1618
|
+
// Make sure the node is collapsed, then the user can load its
|
|
1619
|
+
// children again (by expanding).
|
|
1620
|
+
normalized.isExpanded = false
|
|
1621
|
+
// We have reset `childrenStates` and don't want to preserve states here.
|
|
1622
|
+
} else {
|
|
1623
|
+
normalized.childrenStates = { ...prev.childrenStates }
|
|
1624
|
+
}
|
|
1625
|
+
}
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
return normalized
|
|
1629
|
+
})
|
|
1630
|
+
|
|
1631
|
+
if (this.branchNodesFirst) {
|
|
1632
|
+
const branchNodes = normalizedOptions.filter(option => option.isBranch)
|
|
1633
|
+
const leafNodes = normalizedOptions.filter(option => option.isLeaf)
|
|
1634
|
+
normalizedOptions = branchNodes.concat(leafNodes)
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
return normalizedOptions
|
|
1638
|
+
},
|
|
1639
|
+
|
|
1640
|
+
loadRootOptions() {
|
|
1641
|
+
this.callLoadOptionsProp({
|
|
1642
|
+
action: LOAD_ROOT_OPTIONS,
|
|
1643
|
+
isPending: () => {
|
|
1644
|
+
return this.rootOptionsStates.isLoading
|
|
1645
|
+
},
|
|
1646
|
+
start: () => {
|
|
1647
|
+
this.rootOptionsStates.isLoading = true
|
|
1648
|
+
this.rootOptionsStates.loadingError = ''
|
|
1649
|
+
},
|
|
1650
|
+
succeed: () => {
|
|
1651
|
+
this.rootOptionsStates.isLoaded = true
|
|
1652
|
+
// Wait for `options` being re-initialized.
|
|
1653
|
+
this.$nextTick(() => {
|
|
1654
|
+
this.resetHighlightedOptionWhenNecessary(true)
|
|
1655
|
+
})
|
|
1656
|
+
},
|
|
1657
|
+
fail: err => {
|
|
1658
|
+
this.rootOptionsStates.loadingError = getErrorMessage(err)
|
|
1659
|
+
},
|
|
1660
|
+
end: () => {
|
|
1661
|
+
this.rootOptionsStates.isLoading = false
|
|
1662
|
+
},
|
|
1663
|
+
})
|
|
1664
|
+
},
|
|
1665
|
+
|
|
1666
|
+
loadChildrenOptions(parentNode) {
|
|
1667
|
+
// The options may be re-initialized anytime during the loading process.
|
|
1668
|
+
// So `parentNode` can be stale and we use `getNode()` to avoid that.
|
|
1669
|
+
|
|
1670
|
+
const { id, raw } = parentNode
|
|
1671
|
+
|
|
1672
|
+
this.callLoadOptionsProp({
|
|
1673
|
+
action: LOAD_CHILDREN_OPTIONS,
|
|
1674
|
+
args: {
|
|
1675
|
+
// We always pass the raw node instead of the normalized node to any
|
|
1676
|
+
// callback provided by the user of this component.
|
|
1677
|
+
// Because the shape of the raw node is more likely to be closing to
|
|
1678
|
+
// what the back-end API service of the application needs.
|
|
1679
|
+
parentNode: raw,
|
|
1680
|
+
},
|
|
1681
|
+
isPending: () => {
|
|
1682
|
+
return this.getNode(id).childrenStates.isLoading
|
|
1683
|
+
},
|
|
1684
|
+
start: () => {
|
|
1685
|
+
this.getNode(id).childrenStates.isLoading = true
|
|
1686
|
+
this.getNode(id).childrenStates.loadingError = ''
|
|
1687
|
+
},
|
|
1688
|
+
succeed: () => {
|
|
1689
|
+
this.getNode(id).childrenStates.isLoaded = true
|
|
1690
|
+
},
|
|
1691
|
+
fail: err => {
|
|
1692
|
+
this.getNode(id).childrenStates.loadingError = getErrorMessage(err)
|
|
1693
|
+
},
|
|
1694
|
+
end: () => {
|
|
1695
|
+
this.getNode(id).childrenStates.isLoading = false
|
|
1696
|
+
},
|
|
1697
|
+
})
|
|
1698
|
+
},
|
|
1699
|
+
|
|
1700
|
+
callLoadOptionsProp({ action, args, isPending, start, succeed, fail, end }) {
|
|
1701
|
+
if (!this.loadOptions || isPending()) {
|
|
1702
|
+
return
|
|
1703
|
+
}
|
|
1704
|
+
|
|
1705
|
+
start()
|
|
1706
|
+
|
|
1707
|
+
const callback = once((err, result) => {
|
|
1708
|
+
if (err) {
|
|
1709
|
+
fail(err)
|
|
1710
|
+
} else {
|
|
1711
|
+
succeed(result)
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1714
|
+
end()
|
|
1715
|
+
})
|
|
1716
|
+
const result = this.loadOptions({
|
|
1717
|
+
id: this.getInstanceId(),
|
|
1718
|
+
instanceId: this.getInstanceId(),
|
|
1719
|
+
action,
|
|
1720
|
+
...args,
|
|
1721
|
+
callback,
|
|
1722
|
+
})
|
|
1723
|
+
|
|
1724
|
+
if (isPromise(result)) {
|
|
1725
|
+
result.then(() => {
|
|
1726
|
+
callback()
|
|
1727
|
+
}, err => {
|
|
1728
|
+
callback(err)
|
|
1729
|
+
}).catch(err => {
|
|
1730
|
+
// istanbul ignore next
|
|
1731
|
+
console.error(err)
|
|
1732
|
+
})
|
|
1733
|
+
}
|
|
1734
|
+
},
|
|
1735
|
+
|
|
1736
|
+
checkDuplication(node) {
|
|
1737
|
+
warning(
|
|
1738
|
+
() => !((node.id in this.forest.nodeMap) && !this.forest.nodeMap[node.id].isFallbackNode),
|
|
1739
|
+
() => `Detected duplicate presence of node id ${JSON.stringify(node.id)}. ` +
|
|
1740
|
+
`Their labels are "${this.forest.nodeMap[node.id].label}" and "${node.label}" respectively.`,
|
|
1741
|
+
)
|
|
1742
|
+
},
|
|
1743
|
+
|
|
1744
|
+
verifyNodeShape(node) {
|
|
1745
|
+
warning(
|
|
1746
|
+
() => !(node.children === undefined && node.isBranch === true),
|
|
1747
|
+
() => 'Are you meant to declare an unloaded branch node? ' +
|
|
1748
|
+
'`isBranch: true` is no longer supported, please use `children: null` instead.',
|
|
1749
|
+
)
|
|
1750
|
+
},
|
|
1751
|
+
|
|
1752
|
+
select(node) {
|
|
1753
|
+
if (this.disabled || node.isDisabled) {
|
|
1754
|
+
return
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (this.single) {
|
|
1758
|
+
this.clear()
|
|
1759
|
+
}
|
|
1760
|
+
|
|
1761
|
+
const nextState = this.multiple && !this.flat
|
|
1762
|
+
? this.forest.checkedStateMap[node.id] === UNCHECKED
|
|
1763
|
+
: !this.isSelected(node)
|
|
1764
|
+
|
|
1765
|
+
if (nextState) {
|
|
1766
|
+
this._selectNode(node)
|
|
1767
|
+
} else {
|
|
1768
|
+
this._deselectNode(node)
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
this.buildForestState()
|
|
1772
|
+
|
|
1773
|
+
if (nextState) {
|
|
1774
|
+
this.$emit('select', node.raw, this.getInstanceId())
|
|
1775
|
+
} else {
|
|
1776
|
+
this.$emit('deselect', node.raw, this.getInstanceId())
|
|
1777
|
+
}
|
|
1778
|
+
|
|
1779
|
+
if (this.localSearch.active && nextState && (this.single || this.clearOnSelect)) {
|
|
1780
|
+
this.resetSearchQuery()
|
|
1781
|
+
}
|
|
1782
|
+
|
|
1783
|
+
if (this.single && this.closeOnSelect) {
|
|
1784
|
+
this.closeMenu()
|
|
1785
|
+
|
|
1786
|
+
// istanbul ignore else
|
|
1787
|
+
if (this.searchable) {
|
|
1788
|
+
this._blurOnSelect = true
|
|
1789
|
+
}
|
|
1790
|
+
}
|
|
1791
|
+
},
|
|
1792
|
+
|
|
1793
|
+
clear() {
|
|
1794
|
+
if (this.hasValue) {
|
|
1795
|
+
if (this.single || this.allowClearingDisabled) {
|
|
1796
|
+
this.forest.selectedNodeIds = []
|
|
1797
|
+
} else /* if (this.multiple && !this.allowClearingDisabled) */ {
|
|
1798
|
+
this.forest.selectedNodeIds = this.forest.selectedNodeIds.filter(nodeId =>
|
|
1799
|
+
this.getNode(nodeId).isDisabled,
|
|
1800
|
+
)
|
|
1801
|
+
}
|
|
1802
|
+
|
|
1803
|
+
this.buildForestState()
|
|
1804
|
+
}
|
|
1805
|
+
},
|
|
1806
|
+
|
|
1807
|
+
// This is meant to be called only by `select()`.
|
|
1808
|
+
_selectNode(node) {
|
|
1809
|
+
if (this.single || this.disableBranchNodes) {
|
|
1810
|
+
return this.addValue(node)
|
|
1811
|
+
}
|
|
1812
|
+
|
|
1813
|
+
if (this.flat) {
|
|
1814
|
+
this.addValue(node)
|
|
1815
|
+
|
|
1816
|
+
if (this.autoSelectAncestors) {
|
|
1817
|
+
node.ancestors.forEach(ancestor => {
|
|
1818
|
+
if (!this.isSelected(ancestor) && !ancestor.isDisabled) this.addValue(ancestor)
|
|
1819
|
+
})
|
|
1820
|
+
} else if (this.autoSelectDescendants) {
|
|
1821
|
+
this.traverseDescendantsBFS(node, descendant => {
|
|
1822
|
+
if (!this.isSelected(descendant) && !descendant.isDisabled) this.addValue(descendant)
|
|
1823
|
+
})
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
return
|
|
1827
|
+
}
|
|
1828
|
+
|
|
1829
|
+
const isFullyChecked = (
|
|
1830
|
+
node.isLeaf ||
|
|
1831
|
+
(/* node.isBranch && */!node.hasDisabledDescendants) ||
|
|
1832
|
+
(/* node.isBranch && */this.allowSelectingDisabledDescendants)
|
|
1833
|
+
)
|
|
1834
|
+
if (isFullyChecked) {
|
|
1835
|
+
this.addValue(node)
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
if (node.isBranch) {
|
|
1839
|
+
this.traverseDescendantsBFS(node, descendant => {
|
|
1840
|
+
if (!descendant.isDisabled || this.allowSelectingDisabledDescendants) {
|
|
1841
|
+
this.addValue(descendant)
|
|
1842
|
+
}
|
|
1843
|
+
})
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1846
|
+
if (isFullyChecked) {
|
|
1847
|
+
let curr = node
|
|
1848
|
+
while ((curr = curr.parentNode) !== NO_PARENT_NODE) {
|
|
1849
|
+
if (curr.children.every(this.isSelected)) this.addValue(curr)
|
|
1850
|
+
else break
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
},
|
|
1854
|
+
|
|
1855
|
+
// This is meant to be called only by `select()`.
|
|
1856
|
+
_deselectNode(node) {
|
|
1857
|
+
if (this.disableBranchNodes) {
|
|
1858
|
+
return this.removeValue(node)
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
if (this.flat) {
|
|
1862
|
+
this.removeValue(node)
|
|
1863
|
+
|
|
1864
|
+
if (this.autoDeselectAncestors) {
|
|
1865
|
+
node.ancestors.forEach(ancestor => {
|
|
1866
|
+
if (this.isSelected(ancestor) && !ancestor.isDisabled) this.removeValue(ancestor)
|
|
1867
|
+
})
|
|
1868
|
+
} else if (this.autoDeselectDescendants) {
|
|
1869
|
+
this.traverseDescendantsBFS(node, descendant => {
|
|
1870
|
+
if (this.isSelected(descendant) && !descendant.isDisabled) this.removeValue(descendant)
|
|
1871
|
+
})
|
|
1872
|
+
}
|
|
1873
|
+
|
|
1874
|
+
return
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
let hasUncheckedSomeDescendants = false
|
|
1878
|
+
if (node.isBranch) {
|
|
1879
|
+
this.traverseDescendantsDFS(node, descendant => {
|
|
1880
|
+
if (!descendant.isDisabled || this.allowSelectingDisabledDescendants) {
|
|
1881
|
+
this.removeValue(descendant)
|
|
1882
|
+
hasUncheckedSomeDescendants = true
|
|
1883
|
+
}
|
|
1884
|
+
})
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
if (
|
|
1888
|
+
node.isLeaf ||
|
|
1889
|
+
/* node.isBranch && */hasUncheckedSomeDescendants ||
|
|
1890
|
+
/* node.isBranch && */node.children.length === 0
|
|
1891
|
+
) {
|
|
1892
|
+
this.removeValue(node)
|
|
1893
|
+
|
|
1894
|
+
let curr = node
|
|
1895
|
+
while ((curr = curr.parentNode) !== NO_PARENT_NODE) {
|
|
1896
|
+
if (this.isSelected(curr)) this.removeValue(curr)
|
|
1897
|
+
else break
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
},
|
|
1901
|
+
|
|
1902
|
+
addValue(node) {
|
|
1903
|
+
this.forest.selectedNodeIds.push(node.id)
|
|
1904
|
+
this.forest.selectedNodeMap[node.id] = true
|
|
1905
|
+
},
|
|
1906
|
+
|
|
1907
|
+
removeValue(node) {
|
|
1908
|
+
removeFromArray(this.forest.selectedNodeIds, node.id)
|
|
1909
|
+
delete this.forest.selectedNodeMap[node.id]
|
|
1910
|
+
},
|
|
1911
|
+
|
|
1912
|
+
removeLastValue() {
|
|
1913
|
+
if (!this.hasValue) return
|
|
1914
|
+
if (this.single) return this.clear()
|
|
1915
|
+
const lastValue = getLast(this.internalValue)
|
|
1916
|
+
const lastSelectedNode = this.getNode(lastValue)
|
|
1917
|
+
this.select(lastSelectedNode) // deselect
|
|
1918
|
+
},
|
|
1919
|
+
|
|
1920
|
+
saveMenuScrollPosition() {
|
|
1921
|
+
const $menu = this.getMenu()
|
|
1922
|
+
// istanbul ignore else
|
|
1923
|
+
if ($menu) this.menu.lastScrollPosition = $menu.scrollTop
|
|
1924
|
+
},
|
|
1925
|
+
|
|
1926
|
+
restoreMenuScrollPosition() {
|
|
1927
|
+
const $menu = this.getMenu()
|
|
1928
|
+
// istanbul ignore else
|
|
1929
|
+
if ($menu) $menu.scrollTop = this.menu.lastScrollPosition
|
|
1930
|
+
},
|
|
1931
|
+
},
|
|
1932
|
+
|
|
1933
|
+
created() {
|
|
1934
|
+
this.verifyProps()
|
|
1935
|
+
this.resetFlags()
|
|
1936
|
+
},
|
|
1937
|
+
|
|
1938
|
+
mounted() {
|
|
1939
|
+
if (this.autoFocus) this.focusInput()
|
|
1940
|
+
if (!this.options && !this.async && this.autoLoadRootOptions) this.loadRootOptions()
|
|
1941
|
+
if (this.alwaysOpen) this.openMenu()
|
|
1942
|
+
if (this.async && this.defaultOptions) this.handleRemoteSearch()
|
|
1943
|
+
},
|
|
1944
|
+
|
|
1945
|
+
destroyed() {
|
|
1946
|
+
// istanbul ignore next
|
|
1947
|
+
this.toggleClickOutsideEvent(false)
|
|
1948
|
+
},
|
|
1949
|
+
}
|