You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

1366 lines
43 KiB

  1. import { openBlock, createBlock, withKeys, withModifiers, renderSlot, createVNode, withDirectives, Fragment, renderList, toDisplayString, vShow, createCommentVNode, Transition, withCtx, createTextVNode } from 'vue';
  2. function isEmpty (opt) {
  3. if (opt === 0) return false
  4. if (Array.isArray(opt) && opt.length === 0) return true
  5. return !opt
  6. }
  7. function not (fun) {
  8. return (...params) => !fun(...params)
  9. }
  10. function includes (str, query) {
  11. /* istanbul ignore else */
  12. if (str === undefined) str = 'undefined';
  13. if (str === null) str = 'null';
  14. if (str === false) str = 'false';
  15. const text = str.toString().toLowerCase();
  16. return text.indexOf(query.trim()) !== -1
  17. }
  18. function filterOptions (options, search, label, customLabel) {
  19. return search ? options
  20. .filter((option) => includes(customLabel(option, label), search))
  21. .sort((a, b) => customLabel(a, label).length - customLabel(b, label).length) : options
  22. }
  23. function stripGroups (options) {
  24. return options.filter((option) => !option.$isLabel)
  25. }
  26. function flattenOptions (values, label) {
  27. return (options) =>
  28. options.reduce((prev, curr) => {
  29. /* istanbul ignore else */
  30. if (curr[values] && curr[values].length) {
  31. prev.push({
  32. $groupLabel: curr[label],
  33. $isLabel: true
  34. });
  35. return prev.concat(curr[values])
  36. }
  37. return prev
  38. }, [])
  39. }
  40. function filterGroups (search, label, values, groupLabel, customLabel) {
  41. return (groups) =>
  42. groups.map((group) => {
  43. /* istanbul ignore else */
  44. if (!group[values]) {
  45. console.warn(`Options passed to vue-multiselect do not contain groups, despite the config.`);
  46. return []
  47. }
  48. const groupOptions = filterOptions(group[values], search, label, customLabel);
  49. return groupOptions.length
  50. ? {
  51. [groupLabel]: group[groupLabel],
  52. [values]: groupOptions
  53. }
  54. : []
  55. })
  56. }
  57. const flow = (...fns) => (x) => fns.reduce((v, f) => f(v), x);
  58. var multiselectMixin = {
  59. data () {
  60. return {
  61. search: '',
  62. isOpen: false,
  63. preferredOpenDirection: 'below',
  64. optimizedHeight: this.maxHeight
  65. }
  66. },
  67. props: {
  68. /**
  69. * Decide whether to filter the results based on search query.
  70. * Useful for async filtering, where we search through more complex data.
  71. * @type {Boolean}
  72. */
  73. internalSearch: {
  74. type: Boolean,
  75. default: true
  76. },
  77. /**
  78. * Array of available options: Objects, Strings or Integers.
  79. * If array of objects, visible label will default to option.label.
  80. * If `labal` prop is passed, label will equal option['label']
  81. * @type {Array}
  82. */
  83. options: {
  84. type: Array,
  85. required: true
  86. },
  87. /**
  88. * Equivalent to the `multiple` attribute on a `<select>` input.
  89. * @default false
  90. * @type {Boolean}
  91. */
  92. multiple: {
  93. type: Boolean,
  94. default: false
  95. },
  96. /**
  97. * Key to compare objects
  98. * @default 'id'
  99. * @type {String}
  100. */
  101. trackBy: {
  102. type: String
  103. },
  104. /**
  105. * Label to look for in option Object
  106. * @default 'label'
  107. * @type {String}
  108. */
  109. label: {
  110. type: String
  111. },
  112. /**
  113. * Enable/disable search in options
  114. * @default true
  115. * @type {Boolean}
  116. */
  117. searchable: {
  118. type: Boolean,
  119. default: true
  120. },
  121. /**
  122. * Clear the search input after `)
  123. * @default true
  124. * @type {Boolean}
  125. */
  126. clearOnSelect: {
  127. type: Boolean,
  128. default: true
  129. },
  130. /**
  131. * Hide already selected options
  132. * @default false
  133. * @type {Boolean}
  134. */
  135. hideSelected: {
  136. type: Boolean,
  137. default: false
  138. },
  139. /**
  140. * Equivalent to the `placeholder` attribute on a `<select>` input.
  141. * @default 'Select option'
  142. * @type {String}
  143. */
  144. placeholder: {
  145. type: String,
  146. default: 'Select option'
  147. },
  148. /**
  149. * Allow to remove all selected values
  150. * @default true
  151. * @type {Boolean}
  152. */
  153. allowEmpty: {
  154. type: Boolean,
  155. default: true
  156. },
  157. /**
  158. * Reset this.internalValue, this.search after this.internalValue changes.
  159. * Useful if want to create a stateless dropdown.
  160. * @default false
  161. * @type {Boolean}
  162. */
  163. resetAfter: {
  164. type: Boolean,
  165. default: false
  166. },
  167. /**
  168. * Enable/disable closing after selecting an option
  169. * @default true
  170. * @type {Boolean}
  171. */
  172. closeOnSelect: {
  173. type: Boolean,
  174. default: true
  175. },
  176. /**
  177. * Function to interpolate the custom label
  178. * @default false
  179. * @type {Function}
  180. */
  181. customLabel: {
  182. type: Function,
  183. default (option, label) {
  184. if (isEmpty(option)) return ''
  185. return label ? option[label] : option
  186. }
  187. },
  188. /**
  189. * Disable / Enable tagging
  190. * @default false
  191. * @type {Boolean}
  192. */
  193. taggable: {
  194. type: Boolean,
  195. default: false
  196. },
  197. /**
  198. * String to show when highlighting a potential tag
  199. * @default 'Press enter to create a tag'
  200. * @type {String}
  201. */
  202. tagPlaceholder: {
  203. type: String,
  204. default: 'Press enter to create a tag'
  205. },
  206. /**
  207. * By default new tags will appear above the search results.
  208. * Changing to 'bottom' will revert this behaviour
  209. * and will proritize the search results
  210. * @default 'top'
  211. * @type {String}
  212. */
  213. tagPosition: {
  214. type: String,
  215. default: 'top'
  216. },
  217. /**
  218. * Number of allowed selected options. No limit if 0.
  219. * @default 0
  220. * @type {Number}
  221. */
  222. max: {
  223. type: [Number, Boolean],
  224. default: false
  225. },
  226. /**
  227. * Will be passed with all events as second param.
  228. * Useful for identifying events origin.
  229. * @default null
  230. * @type {String|Integer}
  231. */
  232. id: {
  233. default: null
  234. },
  235. /**
  236. * Limits the options displayed in the dropdown
  237. * to the first X options.
  238. * @default 1000
  239. * @type {Integer}
  240. */
  241. optionsLimit: {
  242. type: Number,
  243. default: 1000
  244. },
  245. /**
  246. * Name of the property containing
  247. * the group values
  248. * @default 1000
  249. * @type {String}
  250. */
  251. groupValues: {
  252. type: String
  253. },
  254. /**
  255. * Name of the property containing
  256. * the group label
  257. * @default 1000
  258. * @type {String}
  259. */
  260. groupLabel: {
  261. type: String
  262. },
  263. /**
  264. * Allow to select all group values
  265. * by selecting the group label
  266. * @default false
  267. * @type {Boolean}
  268. */
  269. groupSelect: {
  270. type: Boolean,
  271. default: false
  272. },
  273. /**
  274. * Array of keyboard keys to block
  275. * when selecting
  276. * @default 1000
  277. * @type {String}
  278. */
  279. blockKeys: {
  280. type: Array,
  281. default () {
  282. return []
  283. }
  284. },
  285. /**
  286. * Prevent from wiping up the search value
  287. * @default false
  288. * @type {Boolean}
  289. */
  290. preserveSearch: {
  291. type: Boolean,
  292. default: false
  293. },
  294. /**
  295. * Select 1st options if value is empty
  296. * @default false
  297. * @type {Boolean}
  298. */
  299. preselectFirst: {
  300. type: Boolean,
  301. default: false
  302. },
  303. /**
  304. * Prevent autofocus
  305. * @default false
  306. * @type {Boolean}
  307. */
  308. preventAutofocus: {
  309. type: Boolean,
  310. default: false
  311. }
  312. },
  313. mounted () {
  314. /* istanbul ignore else */
  315. if (!this.multiple && this.max) {
  316. console.warn('[Vue-Multiselect warn]: Max prop should not be used when prop Multiple equals false.');
  317. }
  318. if (
  319. this.preselectFirst &&
  320. !this.internalValue.length &&
  321. this.options.length
  322. ) {
  323. this.select(this.filteredOptions[0]);
  324. }
  325. },
  326. computed: {
  327. internalValue () {
  328. return this.modelValue || this.modelValue === 0
  329. ? Array.isArray(this.modelValue) ? this.modelValue : [this.modelValue]
  330. : []
  331. },
  332. filteredOptions () {
  333. const search = this.search || '';
  334. const normalizedSearch = search.toLowerCase().trim();
  335. let options = this.options.concat();
  336. /* istanbul ignore else */
  337. if (this.internalSearch) {
  338. options = this.groupValues
  339. ? this.filterAndFlat(options, normalizedSearch, this.label)
  340. : filterOptions(options, normalizedSearch, this.label, this.customLabel);
  341. } else {
  342. options = this.groupValues ? flattenOptions(this.groupValues, this.groupLabel)(options) : options;
  343. }
  344. options = this.hideSelected
  345. ? options.filter(not(this.isSelected))
  346. : options;
  347. /* istanbul ignore else */
  348. if (this.taggable && normalizedSearch.length && !this.isExistingOption(normalizedSearch)) {
  349. if (this.tagPosition === 'bottom') {
  350. options.push({isTag: true, label: search});
  351. } else {
  352. options.unshift({isTag: true, label: search});
  353. }
  354. }
  355. return options.slice(0, this.optionsLimit)
  356. },
  357. valueKeys () {
  358. if (this.trackBy) {
  359. return this.internalValue.map((element) => element[this.trackBy])
  360. } else {
  361. return this.internalValue
  362. }
  363. },
  364. optionKeys () {
  365. const options = this.groupValues ? this.flatAndStrip(this.options) : this.options;
  366. return options.map((element) => this.customLabel(element, this.label).toString().toLowerCase())
  367. },
  368. currentOptionLabel () {
  369. return this.multiple
  370. ? this.searchable ? '' : this.placeholder
  371. : this.internalValue.length
  372. ? this.getOptionLabel(this.internalValue[0])
  373. : this.searchable ? '' : this.placeholder
  374. }
  375. },
  376. watch: {
  377. internalValue: {
  378. handler () {
  379. /* istanbul ignore else */
  380. if (this.resetAfter && this.internalValue.length) {
  381. this.search = '';
  382. this.$emit('update:modelValue', this.multiple ? [] : null);
  383. }
  384. },
  385. deep: true
  386. },
  387. search () {
  388. this.$emit('search-change', this.search);
  389. }
  390. },
  391. emits: ['open', 'search-change', 'close', 'select', 'update:modelValue', 'remove', 'tag'],
  392. methods: {
  393. /**
  394. * Returns the internalValue in a way it can be emited to the parent
  395. * @returns {Object||Array||String||Integer}
  396. */
  397. getValue () {
  398. return this.multiple
  399. ? this.internalValue
  400. : this.internalValue.length === 0
  401. ? null
  402. : this.internalValue[0]
  403. },
  404. /**
  405. * Filters and then flattens the options list
  406. * @param {Array}
  407. * @return {Array} returns a filtered and flat options list
  408. */
  409. filterAndFlat (options, search, label) {
  410. return flow(
  411. filterGroups(search, label, this.groupValues, this.groupLabel, this.customLabel),
  412. flattenOptions(this.groupValues, this.groupLabel)
  413. )(options)
  414. },
  415. /**
  416. * Flattens and then strips the group labels from the options list
  417. * @param {Array}
  418. * @return {Array} returns a flat options list without group labels
  419. */
  420. flatAndStrip (options) {
  421. return flow(
  422. flattenOptions(this.groupValues, this.groupLabel),
  423. stripGroups
  424. )(options)
  425. },
  426. /**
  427. * Updates the search value
  428. * @param {String}
  429. */
  430. updateSearch (query) {
  431. this.search = query;
  432. },
  433. /**
  434. * Finds out if the given query is already present
  435. * in the available options
  436. * @param {String}
  437. * @return {Boolean} returns true if element is available
  438. */
  439. isExistingOption (query) {
  440. return !this.options
  441. ? false
  442. : this.optionKeys.indexOf(query) > -1
  443. },
  444. /**
  445. * Finds out if the given element is already present
  446. * in the result value
  447. * @param {Object||String||Integer} option passed element to check
  448. * @returns {Boolean} returns true if element is selected
  449. */
  450. isSelected (option) {
  451. const opt = this.trackBy
  452. ? option[this.trackBy]
  453. : option;
  454. return this.valueKeys.indexOf(opt) > -1
  455. },
  456. /**
  457. * Finds out if the given option is disabled
  458. * @param {Object||String||Integer} option passed element to check
  459. * @returns {Boolean} returns true if element is disabled
  460. */
  461. isOptionDisabled (option) {
  462. return !!option.$isDisabled
  463. },
  464. /**
  465. * Returns empty string when options is null/undefined
  466. * Returns tag query if option is tag.
  467. * Returns the customLabel() results and casts it to string.
  468. *
  469. * @param {Object||String||Integer} Passed option
  470. * @returns {Object||String}
  471. */
  472. getOptionLabel (option) {
  473. if (isEmpty(option)) return ''
  474. /* istanbul ignore else */
  475. if (option.isTag) return option.label
  476. /* istanbul ignore else */
  477. if (option.$isLabel) return option.$groupLabel
  478. const label = this.customLabel(option, this.label);
  479. /* istanbul ignore else */
  480. if (isEmpty(label)) return ''
  481. return label
  482. },
  483. /**
  484. * Add the given option to the list of selected options
  485. * or sets the option as the selected option.
  486. * If option is already selected -> remove it from the results.
  487. *
  488. * @param {Object||String||Integer} option to select/deselect
  489. * @param {Boolean} block removing
  490. */
  491. select (option, key) {
  492. /* istanbul ignore else */
  493. if (option.$isLabel && this.groupSelect) {
  494. this.selectGroup(option);
  495. return
  496. }
  497. if (this.blockKeys.indexOf(key) !== -1 ||
  498. this.disabled ||
  499. option.$isDisabled ||
  500. option.$isLabel
  501. ) return
  502. /* istanbul ignore else */
  503. if (this.max && this.multiple && this.internalValue.length === this.max) return
  504. /* istanbul ignore else */
  505. if (key === 'Tab' && !this.pointerDirty) return
  506. if (option.isTag) {
  507. this.$emit('tag', option.label, this.id);
  508. this.search = '';
  509. if (this.closeOnSelect && !this.multiple) this.deactivate();
  510. } else {
  511. const isSelected = this.isSelected(option);
  512. if (isSelected) {
  513. if (key !== 'Tab') this.removeElement(option);
  514. return
  515. }
  516. if (this.multiple) {
  517. this.$emit('update:modelValue', this.internalValue.concat([option]));
  518. } else {
  519. this.$emit('update:modelValue', option);
  520. }
  521. this.$emit('select', option, this.id);
  522. /* istanbul ignore else */
  523. if (this.clearOnSelect) this.search = '';
  524. }
  525. /* istanbul ignore else */
  526. if (this.closeOnSelect) this.deactivate();
  527. },
  528. /**
  529. * Add the given group options to the list of selected options
  530. * If all group optiona are already selected -> remove it from the results.
  531. *
  532. * @param {Object||String||Integer} group to select/deselect
  533. */
  534. selectGroup (selectedGroup) {
  535. const group = this.options.find((option) => {
  536. return option[this.groupLabel] === selectedGroup.$groupLabel
  537. });
  538. if (!group) return
  539. if (this.wholeGroupSelected(group)) {
  540. this.$emit('remove', group[this.groupValues], this.id);
  541. const groupValues = this.trackBy ? group[this.groupValues].map(val => val[this.trackBy]) : group[this.groupValues];
  542. const newValue = this.internalValue.filter(
  543. option => groupValues.indexOf(this.trackBy ? option[this.trackBy] : option) === -1
  544. );
  545. this.$emit('update:modelValue', newValue);
  546. } else {
  547. let optionsToAdd = group[this.groupValues].filter(
  548. option => !(this.isOptionDisabled(option) || this.isSelected(option))
  549. );
  550. // if max is defined then just select options respecting max
  551. if (this.max) {
  552. optionsToAdd.splice(this.max - this.internalValue.length);
  553. }
  554. this.$emit('select', optionsToAdd, this.id);
  555. this.$emit(
  556. 'update:modelValue',
  557. this.internalValue.concat(optionsToAdd)
  558. );
  559. }
  560. if (this.closeOnSelect) this.deactivate();
  561. },
  562. /**
  563. * Helper to identify if all values in a group are selected
  564. *
  565. * @param {Object} group to validated selected values against
  566. */
  567. wholeGroupSelected (group) {
  568. return group[this.groupValues].every((option) => this.isSelected(option) || this.isOptionDisabled(option)
  569. )
  570. },
  571. /**
  572. * Helper to identify if all values in a group are disabled
  573. *
  574. * @param {Object} group to check for disabled values
  575. */
  576. wholeGroupDisabled (group) {
  577. return group[this.groupValues].every(this.isOptionDisabled)
  578. },
  579. /**
  580. * Removes the given option from the selected options.
  581. * Additionally checks this.allowEmpty prop if option can be removed when
  582. * it is the last selected option.
  583. *
  584. * @param {type} option description
  585. * @return {type} description
  586. */
  587. removeElement (option, shouldClose = true) {
  588. /* istanbul ignore else */
  589. if (this.disabled) return
  590. /* istanbul ignore else */
  591. if (option.$isDisabled) return
  592. /* istanbul ignore else */
  593. if (!this.allowEmpty && this.internalValue.length <= 1) {
  594. this.deactivate();
  595. return
  596. }
  597. const index = typeof option === 'object'
  598. ? this.valueKeys.indexOf(option[this.trackBy])
  599. : this.valueKeys.indexOf(option);
  600. if (this.multiple) {
  601. const newValue = this.internalValue.slice(0, index).concat(this.internalValue.slice(index + 1));
  602. this.$emit('update:modelValue', newValue);
  603. } else {
  604. this.$emit('update:modelValue', null);
  605. }
  606. this.$emit('remove', option, this.id);
  607. /* istanbul ignore else */
  608. if (this.closeOnSelect && shouldClose) this.deactivate();
  609. },
  610. /**
  611. * Calls this.removeElement() with the last element
  612. * from this.internalValue (selected element Array)
  613. *
  614. * @fires this#removeElement
  615. */
  616. removeLastElement () {
  617. /* istanbul ignore else */
  618. if (this.blockKeys.indexOf('Delete') !== -1) return
  619. /* istanbul ignore else */
  620. if (this.search.length === 0 && Array.isArray(this.internalValue) && this.internalValue.length) {
  621. this.removeElement(this.internalValue[this.internalValue.length - 1], false);
  622. }
  623. },
  624. /**
  625. * Opens the multiselect’s dropdown.
  626. * Sets this.isOpen to TRUE
  627. */
  628. activate () {
  629. /* istanbul ignore else */
  630. if (this.isOpen || this.disabled) return
  631. this.adjustPosition();
  632. /* istanbul ignore else */
  633. if (this.groupValues && this.pointer === 0 && this.filteredOptions.length) {
  634. this.pointer = 1;
  635. }
  636. this.isOpen = true;
  637. /* istanbul ignore else */
  638. if (this.searchable) {
  639. if (!this.preserveSearch) this.search = '';
  640. if (!this.preventAutofocus) this.$nextTick(() => this.$refs.search && this.$refs.search.focus());
  641. } else if (!this.preventAutofocus) {
  642. if (typeof this.$el !== 'undefined') this.$el.focus();
  643. }
  644. this.$emit('open', this.id);
  645. },
  646. /**
  647. * Closes the multiselect’s dropdown.
  648. * Sets this.isOpen to FALSE
  649. */
  650. deactivate () {
  651. /* istanbul ignore else */
  652. if (!this.isOpen) return
  653. this.isOpen = false;
  654. /* istanbul ignore else */
  655. if (this.searchable) {
  656. if (this.$refs.search !== null && typeof this.$refs.search !== 'undefined') this.$refs.search.blur();
  657. } else {
  658. if (typeof this.$el !== 'undefined') this.$el.blur();
  659. }
  660. if (!this.preserveSearch) this.search = '';
  661. this.$emit('close', this.getValue(), this.id);
  662. },
  663. /**
  664. * Call this.activate() or this.deactivate()
  665. * depending on this.isOpen value.
  666. *
  667. * @fires this#activate || this#deactivate
  668. * @property {Boolean} isOpen indicates if dropdown is open
  669. */
  670. toggle () {
  671. this.isOpen
  672. ? this.deactivate()
  673. : this.activate();
  674. },
  675. /**
  676. * Updates the hasEnoughSpace variable used for
  677. * detecting where to expand the dropdown
  678. */
  679. adjustPosition () {
  680. if (typeof window === 'undefined') return
  681. const spaceAbove = this.$el.getBoundingClientRect().top;
  682. const spaceBelow = window.innerHeight - this.$el.getBoundingClientRect().bottom;
  683. const hasEnoughSpaceBelow = spaceBelow > this.maxHeight;
  684. if (hasEnoughSpaceBelow || spaceBelow > spaceAbove || this.openDirection === 'below' || this.openDirection === 'bottom') {
  685. this.preferredOpenDirection = 'below';
  686. this.optimizedHeight = Math.min(spaceBelow - 40, this.maxHeight);
  687. } else {
  688. this.preferredOpenDirection = 'above';
  689. this.optimizedHeight = Math.min(spaceAbove - 40, this.maxHeight);
  690. }
  691. }
  692. }
  693. };
  694. var pointerMixin = {
  695. data () {
  696. return {
  697. pointer: 0,
  698. pointerDirty: false
  699. }
  700. },
  701. props: {
  702. /**
  703. * Enable/disable highlighting of the pointed value.
  704. * @type {Boolean}
  705. * @default true
  706. */
  707. showPointer: {
  708. type: Boolean,
  709. default: true
  710. },
  711. optionHeight: {
  712. type: Number,
  713. default: 40
  714. }
  715. },
  716. computed: {
  717. pointerPosition () {
  718. return this.pointer * this.optionHeight
  719. },
  720. visibleElements () {
  721. return this.optimizedHeight / this.optionHeight
  722. }
  723. },
  724. watch: {
  725. filteredOptions () {
  726. this.pointerAdjust();
  727. },
  728. isOpen () {
  729. this.pointerDirty = false;
  730. },
  731. pointer () {
  732. this.$refs.search && this.$refs.search.setAttribute('aria-activedescendant', this.id + '-' + this.pointer.toString());
  733. }
  734. },
  735. methods: {
  736. optionHighlight (index, option) {
  737. return {
  738. 'multiselect__option--highlight': index === this.pointer && this.showPointer,
  739. 'multiselect__option--selected': this.isSelected(option)
  740. }
  741. },
  742. groupHighlight (index, selectedGroup) {
  743. if (!this.groupSelect) {
  744. return [
  745. 'multiselect__option--disabled',
  746. {'multiselect__option--group': selectedGroup.$isLabel}
  747. ]
  748. }
  749. const group = this.options.find((option) => {
  750. return option[this.groupLabel] === selectedGroup.$groupLabel
  751. });
  752. return group && !this.wholeGroupDisabled(group) ? [
  753. 'multiselect__option--group',
  754. {'multiselect__option--highlight': index === this.pointer && this.showPointer},
  755. {'multiselect__option--group-selected': this.wholeGroupSelected(group)}
  756. ] : 'multiselect__option--disabled'
  757. },
  758. addPointerElement ({key} = 'Enter') {
  759. /* istanbul ignore else */
  760. if (this.filteredOptions.length > 0) {
  761. this.select(this.filteredOptions[this.pointer], key);
  762. }
  763. this.pointerReset();
  764. },
  765. pointerForward () {
  766. /* istanbul ignore else */
  767. if (this.pointer < this.filteredOptions.length - 1) {
  768. this.pointer++;
  769. /* istanbul ignore next */
  770. if (this.$refs.list.scrollTop <= this.pointerPosition - (this.visibleElements - 1) * this.optionHeight) {
  771. this.$refs.list.scrollTop = this.pointerPosition - (this.visibleElements - 1) * this.optionHeight;
  772. }
  773. /* istanbul ignore else */
  774. if (
  775. this.filteredOptions[this.pointer] &&
  776. this.filteredOptions[this.pointer].$isLabel &&
  777. !this.groupSelect
  778. ) this.pointerForward();
  779. }
  780. this.pointerDirty = true;
  781. },
  782. pointerBackward () {
  783. if (this.pointer > 0) {
  784. this.pointer--;
  785. /* istanbul ignore else */
  786. if (this.$refs.list.scrollTop >= this.pointerPosition) {
  787. this.$refs.list.scrollTop = this.pointerPosition;
  788. }
  789. /* istanbul ignore else */
  790. if (
  791. this.filteredOptions[this.pointer] &&
  792. this.filteredOptions[this.pointer].$isLabel &&
  793. !this.groupSelect
  794. ) this.pointerBackward();
  795. } else {
  796. /* istanbul ignore else */
  797. if (
  798. this.filteredOptions[this.pointer] &&
  799. this.filteredOptions[0].$isLabel &&
  800. !this.groupSelect
  801. ) this.pointerForward();
  802. }
  803. this.pointerDirty = true;
  804. },
  805. pointerReset () {
  806. /* istanbul ignore else */
  807. if (!this.closeOnSelect) return
  808. this.pointer = 0;
  809. /* istanbul ignore else */
  810. if (this.$refs.list) {
  811. this.$refs.list.scrollTop = 0;
  812. }
  813. },
  814. pointerAdjust () {
  815. /* istanbul ignore else */
  816. if (this.pointer >= this.filteredOptions.length - 1) {
  817. this.pointer = this.filteredOptions.length
  818. ? this.filteredOptions.length - 1
  819. : 0;
  820. }
  821. if (this.filteredOptions.length > 0 &&
  822. this.filteredOptions[this.pointer].$isLabel &&
  823. !this.groupSelect
  824. ) {
  825. this.pointerForward();
  826. }
  827. },
  828. pointerSet (index) {
  829. this.pointer = index;
  830. this.pointerDirty = true;
  831. }
  832. }
  833. };
  834. var script = {
  835. name: 'vue-multiselect',
  836. mixins: [multiselectMixin, pointerMixin],
  837. compatConfig: {
  838. MODE: 3,
  839. ATTR_ENUMERATED_COERCION: false
  840. },
  841. props: {
  842. /**
  843. * name attribute to match optional label element
  844. * @default ''
  845. * @type {String}
  846. */
  847. name: {
  848. type: String,
  849. default: ''
  850. },
  851. /**
  852. * Presets the selected options value.
  853. * @type {Object||Array||String||Integer}
  854. */
  855. modelValue: {
  856. type: null,
  857. default () {
  858. return []
  859. }
  860. },
  861. /**
  862. * String to show when pointing to an option
  863. * @default 'Press enter to select'
  864. * @type {String}
  865. */
  866. selectLabel: {
  867. type: String,
  868. default: 'Press enter to select'
  869. },
  870. /**
  871. * String to show when pointing to an option
  872. * @default 'Press enter to select'
  873. * @type {String}
  874. */
  875. selectGroupLabel: {
  876. type: String,
  877. default: 'Press enter to select group'
  878. },
  879. /**
  880. * String to show next to selected option
  881. * @default 'Selected'
  882. * @type {String}
  883. */
  884. selectedLabel: {
  885. type: String,
  886. default: 'Selected'
  887. },
  888. /**
  889. * String to show when pointing to an already selected option
  890. * @default 'Press enter to remove'
  891. * @type {String}
  892. */
  893. deselectLabel: {
  894. type: String,
  895. default: 'Press enter to remove'
  896. },
  897. /**
  898. * String to show when pointing to an already selected option
  899. * @default 'Press enter to remove'
  900. * @type {String}
  901. */
  902. deselectGroupLabel: {
  903. type: String,
  904. default: 'Press enter to deselect group'
  905. },
  906. /**
  907. * Decide whether to show pointer labels
  908. * @default true
  909. * @type {Boolean}
  910. */
  911. showLabels: {
  912. type: Boolean,
  913. default: true
  914. },
  915. /**
  916. * Limit the display of selected options. The rest will be hidden within the limitText string.
  917. * @default 99999
  918. * @type {Integer}
  919. */
  920. limit: {
  921. type: Number,
  922. default: 99999
  923. },
  924. /**
  925. * Sets maxHeight style value of the dropdown
  926. * @default 300
  927. * @type {Integer}
  928. */
  929. maxHeight: {
  930. type: Number,
  931. default: 300
  932. },
  933. /**
  934. * Function that process the message shown when selected
  935. * elements pass the defined limit.
  936. * @default 'and * more'
  937. * @param {Int} count Number of elements more than limit
  938. * @type {Function}
  939. */
  940. limitText: {
  941. type: Function,
  942. default: (count) => `and ${count} more`
  943. },
  944. /**
  945. * Set true to trigger the loading spinner.
  946. * @default False
  947. * @type {Boolean}
  948. */
  949. loading: {
  950. type: Boolean,
  951. default: false
  952. },
  953. /**
  954. * Disables the multiselect if true.
  955. * @default false
  956. * @type {Boolean}
  957. */
  958. disabled: {
  959. type: Boolean,
  960. default: false
  961. },
  962. /**
  963. * Enables search input's spellcheck if true.
  964. * @default false
  965. * @type {Boolean}
  966. */
  967. spellcheck: {
  968. type: Boolean,
  969. default: false
  970. },
  971. /**
  972. * Fixed opening direction
  973. * @default ''
  974. * @type {String}
  975. */
  976. openDirection: {
  977. type: String,
  978. default: ''
  979. },
  980. /**
  981. * Shows slot with message about empty options
  982. * @default true
  983. * @type {Boolean}
  984. */
  985. showNoOptions: {
  986. type: Boolean,
  987. default: true
  988. },
  989. showNoResults: {
  990. type: Boolean,
  991. default: true
  992. },
  993. tabindex: {
  994. type: Number,
  995. default: 0
  996. },
  997. required: {
  998. type: Boolean,
  999. default: false
  1000. }
  1001. },
  1002. computed: {
  1003. hasOptionGroup () {
  1004. return this.groupValues && this.groupLabel && this.groupSelect
  1005. },
  1006. isSingleLabelVisible () {
  1007. return (
  1008. (this.singleValue || this.singleValue === 0) &&
  1009. (!this.isOpen || !this.searchable) &&
  1010. !this.visibleValues.length
  1011. )
  1012. },
  1013. isPlaceholderVisible () {
  1014. return !this.internalValue.length && (!this.searchable || !this.isOpen)
  1015. },
  1016. visibleValues () {
  1017. return this.multiple ? this.internalValue.slice(0, this.limit) : []
  1018. },
  1019. singleValue () {
  1020. return this.internalValue[0]
  1021. },
  1022. deselectLabelText () {
  1023. return this.showLabels ? this.deselectLabel : ''
  1024. },
  1025. deselectGroupLabelText () {
  1026. return this.showLabels ? this.deselectGroupLabel : ''
  1027. },
  1028. selectLabelText () {
  1029. return this.showLabels ? this.selectLabel : ''
  1030. },
  1031. selectGroupLabelText () {
  1032. return this.showLabels ? this.selectGroupLabel : ''
  1033. },
  1034. selectedLabelText () {
  1035. return this.showLabels ? this.selectedLabel : ''
  1036. },
  1037. inputStyle () {
  1038. if (
  1039. this.searchable ||
  1040. (this.multiple && this.modelValue && this.modelValue.length)
  1041. ) {
  1042. // Hide input by setting the width to 0 allowing it to receive focus
  1043. return this.isOpen
  1044. ? {width: '100%'}
  1045. : {width: '0', position: 'absolute', padding: '0'}
  1046. }
  1047. return ''
  1048. },
  1049. contentStyle () {
  1050. return this.options.length
  1051. ? {display: 'inline-block'}
  1052. : {display: 'block'}
  1053. },
  1054. isAbove () {
  1055. if (this.openDirection === 'above' || this.openDirection === 'top') {
  1056. return true
  1057. } else if (
  1058. this.openDirection === 'below' ||
  1059. this.openDirection === 'bottom'
  1060. ) {
  1061. return false
  1062. } else {
  1063. return this.preferredOpenDirection === 'above'
  1064. }
  1065. },
  1066. showSearchInput () {
  1067. return (
  1068. this.searchable &&
  1069. (this.hasSingleSelectedSlot &&
  1070. (this.visibleSingleValue || this.visibleSingleValue === 0)
  1071. ? this.isOpen
  1072. : true)
  1073. )
  1074. }
  1075. }
  1076. };
  1077. const _hoisted_1 = {
  1078. ref: "tags",
  1079. class: "multiselect__tags"
  1080. };
  1081. const _hoisted_2 = { class: "multiselect__tags-wrap" };
  1082. const _hoisted_3 = { class: "multiselect__spinner" };
  1083. const _hoisted_4 = { key: 0 };
  1084. const _hoisted_5 = { class: "multiselect__option" };
  1085. const _hoisted_6 = { class: "multiselect__option" };
  1086. const _hoisted_7 = /*#__PURE__*/createTextVNode("No elements found. Consider changing the search query.");
  1087. const _hoisted_8 = { class: "multiselect__option" };
  1088. const _hoisted_9 = /*#__PURE__*/createTextVNode("List is empty.");
  1089. function render(_ctx, _cache, $props, $setup, $data, $options) {
  1090. return (openBlock(), createBlock("div", {
  1091. tabindex: _ctx.searchable ? -1 : $props.tabindex,
  1092. class: [{ 'multiselect--active': _ctx.isOpen, 'multiselect--disabled': $props.disabled, 'multiselect--above': $options.isAbove, 'multiselect--has-options-group': $options.hasOptionGroup }, "multiselect"],
  1093. onFocus: _cache[14] || (_cache[14] = $event => (_ctx.activate())),
  1094. onBlur: _cache[15] || (_cache[15] = $event => (_ctx.searchable ? false : _ctx.deactivate())),
  1095. onKeydown: [
  1096. _cache[16] || (_cache[16] = withKeys(withModifiers($event => (_ctx.pointerForward()), ["self","prevent"]), ["down"])),
  1097. _cache[17] || (_cache[17] = withKeys(withModifiers($event => (_ctx.pointerBackward()), ["self","prevent"]), ["up"]))
  1098. ],
  1099. onKeypress: _cache[18] || (_cache[18] = withKeys(withModifiers($event => (_ctx.addPointerElement($event)), ["stop","self"]), ["enter","tab"])),
  1100. onKeyup: _cache[19] || (_cache[19] = withKeys($event => (_ctx.deactivate()), ["esc"])),
  1101. role: "combobox",
  1102. "aria-owns": 'listbox-'+_ctx.id
  1103. }, [
  1104. renderSlot(_ctx.$slots, "caret", { toggle: _ctx.toggle }, () => [
  1105. createVNode("div", {
  1106. onMousedown: _cache[1] || (_cache[1] = withModifiers($event => (_ctx.toggle()), ["prevent","stop"])),
  1107. class: "multiselect__select"
  1108. }, null, 32 /* HYDRATE_EVENTS */)
  1109. ]),
  1110. renderSlot(_ctx.$slots, "clear", { search: _ctx.search }),
  1111. createVNode("div", _hoisted_1, [
  1112. renderSlot(_ctx.$slots, "selection", {
  1113. search: _ctx.search,
  1114. remove: _ctx.removeElement,
  1115. values: $options.visibleValues,
  1116. isOpen: _ctx.isOpen
  1117. }, () => [
  1118. withDirectives(createVNode("div", _hoisted_2, [
  1119. (openBlock(true), createBlock(Fragment, null, renderList($options.visibleValues, (option, index) => {
  1120. return renderSlot(_ctx.$slots, "tag", {
  1121. option: option,
  1122. search: _ctx.search,
  1123. remove: _ctx.removeElement
  1124. }, () => [
  1125. (openBlock(), createBlock("span", {
  1126. class: "multiselect__tag",
  1127. key: index
  1128. }, [
  1129. createVNode("span", {
  1130. textContent: toDisplayString(_ctx.getOptionLabel(option))
  1131. }, null, 8 /* PROPS */, ["textContent"]),
  1132. createVNode("i", {
  1133. tabindex: "1",
  1134. onKeypress: withKeys(withModifiers($event => (_ctx.removeElement(option)), ["prevent"]), ["enter"]),
  1135. onMousedown: withModifiers($event => (_ctx.removeElement(option)), ["prevent"]),
  1136. class: "multiselect__tag-icon"
  1137. }, null, 40 /* PROPS, HYDRATE_EVENTS */, ["onKeypress", "onMousedown"])
  1138. ]))
  1139. ])
  1140. }), 256 /* UNKEYED_FRAGMENT */))
  1141. ], 512 /* NEED_PATCH */), [
  1142. [vShow, $options.visibleValues.length > 0]
  1143. ]),
  1144. (_ctx.internalValue && _ctx.internalValue.length > $props.limit)
  1145. ? renderSlot(_ctx.$slots, "limit", { key: 0 }, () => [
  1146. createVNode("strong", {
  1147. class: "multiselect__strong",
  1148. textContent: toDisplayString($props.limitText(_ctx.internalValue.length - $props.limit))
  1149. }, null, 8 /* PROPS */, ["textContent"])
  1150. ])
  1151. : createCommentVNode("v-if", true)
  1152. ]),
  1153. createVNode(Transition, { name: "multiselect__loading" }, {
  1154. default: withCtx(() => [
  1155. renderSlot(_ctx.$slots, "loading", {}, () => [
  1156. withDirectives(createVNode("div", _hoisted_3, null, 512 /* NEED_PATCH */), [
  1157. [vShow, $props.loading]
  1158. ])
  1159. ])
  1160. ]),
  1161. _: 3 /* FORWARDED */
  1162. }),
  1163. (_ctx.searchable)
  1164. ? (openBlock(), createBlock("input", {
  1165. key: 0,
  1166. ref: "search",
  1167. name: $props.name,
  1168. id: _ctx.id,
  1169. type: "text",
  1170. autocomplete: "off",
  1171. spellcheck: $props.spellcheck,
  1172. placeholder: _ctx.placeholder,
  1173. required: $props.required,
  1174. style: $options.inputStyle,
  1175. value: _ctx.search,
  1176. disabled: $props.disabled,
  1177. tabindex: $props.tabindex,
  1178. onInput: _cache[2] || (_cache[2] = $event => (_ctx.updateSearch($event.target.value))),
  1179. onFocus: _cache[3] || (_cache[3] = withModifiers($event => (_ctx.activate()), ["prevent"])),
  1180. onBlur: _cache[4] || (_cache[4] = withModifiers($event => (_ctx.deactivate()), ["prevent"])),
  1181. onKeyup: _cache[5] || (_cache[5] = withKeys($event => (_ctx.deactivate()), ["esc"])),
  1182. onKeydown: [
  1183. _cache[6] || (_cache[6] = withKeys(withModifiers($event => (_ctx.pointerForward()), ["prevent"]), ["down"])),
  1184. _cache[7] || (_cache[7] = withKeys(withModifiers($event => (_ctx.pointerBackward()), ["prevent"]), ["up"])),
  1185. _cache[9] || (_cache[9] = withKeys(withModifiers($event => (_ctx.removeLastElement()), ["stop"]), ["delete"]))
  1186. ],
  1187. onKeypress: _cache[8] || (_cache[8] = withKeys(withModifiers($event => (_ctx.addPointerElement($event)), ["prevent","stop","self"]), ["enter"])),
  1188. class: "multiselect__input",
  1189. "aria-controls": 'listbox-'+_ctx.id
  1190. }, null, 44 /* STYLE, PROPS, HYDRATE_EVENTS */, ["name", "id", "spellcheck", "placeholder", "required", "value", "disabled", "tabindex", "aria-controls"]))
  1191. : createCommentVNode("v-if", true),
  1192. ($options.isSingleLabelVisible)
  1193. ? (openBlock(), createBlock("span", {
  1194. key: 1,
  1195. class: "multiselect__single",
  1196. onMousedown: _cache[10] || (_cache[10] = withModifiers((...args) => (_ctx.toggle && _ctx.toggle(...args)), ["prevent"]))
  1197. }, [
  1198. renderSlot(_ctx.$slots, "singleLabel", { option: $options.singleValue }, () => [
  1199. createTextVNode(toDisplayString(_ctx.currentOptionLabel), 1 /* TEXT */)
  1200. ])
  1201. ], 32 /* HYDRATE_EVENTS */))
  1202. : createCommentVNode("v-if", true),
  1203. ($options.isPlaceholderVisible)
  1204. ? (openBlock(), createBlock("span", {
  1205. key: 2,
  1206. class: "multiselect__placeholder",
  1207. onMousedown: _cache[11] || (_cache[11] = withModifiers((...args) => (_ctx.toggle && _ctx.toggle(...args)), ["prevent"]))
  1208. }, [
  1209. renderSlot(_ctx.$slots, "placeholder", {}, () => [
  1210. createTextVNode(toDisplayString(_ctx.placeholder), 1 /* TEXT */)
  1211. ])
  1212. ], 32 /* HYDRATE_EVENTS */))
  1213. : createCommentVNode("v-if", true)
  1214. ], 512 /* NEED_PATCH */),
  1215. createVNode(Transition, { name: "multiselect" }, {
  1216. default: withCtx(() => [
  1217. withDirectives(createVNode("div", {
  1218. class: "multiselect__content-wrapper",
  1219. onFocus: _cache[12] || (_cache[12] = (...args) => (_ctx.activate && _ctx.activate(...args))),
  1220. tabindex: "-1",
  1221. onMousedown: _cache[13] || (_cache[13] = withModifiers(() => {}, ["prevent"])),
  1222. style: { maxHeight: _ctx.optimizedHeight + 'px' },
  1223. ref: "list"
  1224. }, [
  1225. createVNode("ul", {
  1226. class: "multiselect__content",
  1227. style: $options.contentStyle,
  1228. role: "listbox",
  1229. id: 'listbox-'+_ctx.id,
  1230. "aria-multiselectable": _ctx.multiple
  1231. }, [
  1232. renderSlot(_ctx.$slots, "beforeList"),
  1233. (_ctx.multiple && _ctx.max === _ctx.internalValue.length)
  1234. ? (openBlock(), createBlock("li", _hoisted_4, [
  1235. createVNode("span", _hoisted_5, [
  1236. renderSlot(_ctx.$slots, "maxElements", {}, () => [
  1237. createTextVNode("Maximum of " + toDisplayString(_ctx.max) + " options selected. First remove a selected option to select another.", 1 /* TEXT */)
  1238. ])
  1239. ])
  1240. ]))
  1241. : createCommentVNode("v-if", true),
  1242. (!_ctx.max || _ctx.internalValue.length < _ctx.max)
  1243. ? (openBlock(true), createBlock(Fragment, { key: 1 }, renderList(_ctx.filteredOptions, (option, index) => {
  1244. return (openBlock(), createBlock("li", {
  1245. class: "multiselect__element",
  1246. key: index,
  1247. "aria-selected": _ctx.isSelected(option),
  1248. id: _ctx.id + '-' + index,
  1249. role: !(option && (option.$isLabel || option.$isDisabled)) ? 'option' : null
  1250. }, [
  1251. (!(option && (option.$isLabel || option.$isDisabled)))
  1252. ? (openBlock(), createBlock("span", {
  1253. key: 0,
  1254. class: [_ctx.optionHighlight(index, option), "multiselect__option"],
  1255. onClick: withModifiers($event => (_ctx.select(option)), ["stop"]),
  1256. onMouseenter: withModifiers($event => (_ctx.pointerSet(index)), ["self"]),
  1257. "data-select": option && option.isTag ? _ctx.tagPlaceholder : $options.selectLabelText,
  1258. "data-selected": $options.selectedLabelText,
  1259. "data-deselect": $options.deselectLabelText
  1260. }, [
  1261. renderSlot(_ctx.$slots, "option", {
  1262. option: option,
  1263. search: _ctx.search,
  1264. index: index
  1265. }, () => [
  1266. createVNode("span", null, toDisplayString(_ctx.getOptionLabel(option)), 1 /* TEXT */)
  1267. ])
  1268. ], 42 /* CLASS, PROPS, HYDRATE_EVENTS */, ["onClick", "onMouseenter", "data-select", "data-selected", "data-deselect"]))
  1269. : createCommentVNode("v-if", true),
  1270. (option && (option.$isLabel || option.$isDisabled))
  1271. ? (openBlock(), createBlock("span", {
  1272. key: 1,
  1273. "data-select": _ctx.groupSelect && $options.selectGroupLabelText,
  1274. "data-deselect": _ctx.groupSelect && $options.deselectGroupLabelText,
  1275. class: [_ctx.groupHighlight(index, option), "multiselect__option"],
  1276. onMouseenter: withModifiers($event => (_ctx.groupSelect && _ctx.pointerSet(index)), ["self"]),
  1277. onMousedown: withModifiers($event => (_ctx.selectGroup(option)), ["prevent"])
  1278. }, [
  1279. renderSlot(_ctx.$slots, "option", {
  1280. option: option,
  1281. search: _ctx.search,
  1282. index: index
  1283. }, () => [
  1284. createVNode("span", null, toDisplayString(_ctx.getOptionLabel(option)), 1 /* TEXT */)
  1285. ])
  1286. ], 42 /* CLASS, PROPS, HYDRATE_EVENTS */, ["data-select", "data-deselect", "onMouseenter", "onMousedown"]))
  1287. : createCommentVNode("v-if", true)
  1288. ], 8 /* PROPS */, ["aria-selected", "id", "role"]))
  1289. }), 128 /* KEYED_FRAGMENT */))
  1290. : createCommentVNode("v-if", true),
  1291. withDirectives(createVNode("li", null, [
  1292. createVNode("span", _hoisted_6, [
  1293. renderSlot(_ctx.$slots, "noResult", { search: _ctx.search }, () => [
  1294. _hoisted_7
  1295. ])
  1296. ])
  1297. ], 512 /* NEED_PATCH */), [
  1298. [vShow, $props.showNoResults && (_ctx.filteredOptions.length === 0 && _ctx.search && !$props.loading)]
  1299. ]),
  1300. withDirectives(createVNode("li", null, [
  1301. createVNode("span", _hoisted_8, [
  1302. renderSlot(_ctx.$slots, "noOptions", {}, () => [
  1303. _hoisted_9
  1304. ])
  1305. ])
  1306. ], 512 /* NEED_PATCH */), [
  1307. [vShow, $props.showNoOptions && ((_ctx.options.length === 0 || ($options.hasOptionGroup === true && _ctx.filteredOptions.length === 0)) && !_ctx.search && !$props.loading)]
  1308. ]),
  1309. renderSlot(_ctx.$slots, "afterList")
  1310. ], 12 /* STYLE, PROPS */, ["id", "aria-multiselectable"])
  1311. ], 36 /* STYLE, HYDRATE_EVENTS */), [
  1312. [vShow, _ctx.isOpen]
  1313. ])
  1314. ]),
  1315. _: 3 /* FORWARDED */
  1316. })
  1317. ], 42 /* CLASS, PROPS, HYDRATE_EVENTS */, ["tabindex", "aria-owns"]))
  1318. }
  1319. script.render = render;
  1320. export default script;
  1321. export { script as Multiselect, multiselectMixin, pointerMixin };