Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.
 
 
 

1375 řádky
46 KiB

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