b2315268e51a2b219ec1f54e1d37d87fa0cad027bc4790d46a99b09205c7a611d4bb50130118ce0c649d331888b699444336f341caf89b77471e74d2c9cbb3-exec 55 KB

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