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.
 
 
 

1372 lines
43 KiB

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