mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 12:11:08 -04:00
369 lines
11 KiB
Vue
Executable File
369 lines
11 KiB
Vue
Executable File
<template>
|
|
<div
|
|
:tabindex="searchable ? -1 : tabindex"
|
|
:class="{'multiselect--active': isOpen, 'multiselect--disabled': disabled, 'multiselect--above': isAbove }"
|
|
:aria-owns="'listbox-'+id"
|
|
class="base-select multiselect"
|
|
role="combobox"
|
|
@focus="activate()"
|
|
@blur="searchable ? false : deactivate()"
|
|
@keydown.self.down.prevent="pointerForward()"
|
|
@keydown.self.up.prevent="pointerBackward()"
|
|
@keypress.enter.tab.stop.self="addPointerElement($event)"
|
|
@keyup.esc="deactivate()"
|
|
>
|
|
<slot :toggle="toggle" name="caret">
|
|
<div class="multiselect__select" @mousedown.prevent.stop="toggle()" />
|
|
</slot>
|
|
<!-- <slot name="clear" :search="search"></slot> -->
|
|
<div ref="tags" :class="{'in-valid': invalid}" class="multiselect__tags">
|
|
<slot
|
|
:search="search"
|
|
:remove="removeElement"
|
|
:values="visibleValues"
|
|
:is-open="isOpen"
|
|
name="selection"
|
|
>
|
|
<div v-show="visibleValues.length > 0" class="multiselect__tags-wrap">
|
|
<template v-for="(option, index) of visibleValues" @mousedown.prevent>
|
|
<slot :option="option" :search="search" :remove="removeElement" name="tag">
|
|
<span :key="index" class="multiselect__tag">
|
|
<span v-text="getOptionLabel(option)"/>
|
|
<i class="multiselect__tag-icon" tabindex="1" @keypress.enter.prevent="removeElement(option)" @mousedown.prevent="removeElement(option)"/>
|
|
</span>
|
|
</slot>
|
|
</template>
|
|
</div>
|
|
<template v-if="internalValue && internalValue.length > limit">
|
|
<slot name="limit">
|
|
<strong class="multiselect__strong" v-text="limitText(internalValue.length - limit)"/>
|
|
</slot>
|
|
</template>
|
|
</slot>
|
|
<transition name="multiselect__loading">
|
|
<slot name="loading">
|
|
<div v-show="loading" class="multiselect__spinner"/>
|
|
</slot>
|
|
</transition>
|
|
<input
|
|
ref="search"
|
|
:name="name"
|
|
:id="id"
|
|
:placeholder="placeholder"
|
|
:style="inputStyle"
|
|
:value="search"
|
|
:disabled="disabled"
|
|
:tabindex="tabindex"
|
|
:aria-controls="'listbox-'+id"
|
|
:class="['multiselect__input']"
|
|
type="text"
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
@input="updateSearch($event.target.value)"
|
|
@focus.prevent="activate()"
|
|
@blur.prevent="deactivate()"
|
|
@keyup.esc="deactivate()"
|
|
@keydown.down.prevent="pointerForward()"
|
|
@keydown.up.prevent="pointerBackward()"
|
|
@keypress.enter.prevent.stop.self="addPointerElement($event)"
|
|
@keydown.delete.stop="removeLastElement()"
|
|
>
|
|
<span
|
|
v-if="isSingleLabelVisible"
|
|
class="multiselect__single"
|
|
@mousedown.prevent="toggle"
|
|
>
|
|
<slot :option="singleValue" name="singleLabel">
|
|
<template>{{ currentOptionLabel }}</template>
|
|
</slot>
|
|
</span>
|
|
</div>
|
|
<transition name="multiselect">
|
|
<div
|
|
v-show="isOpen"
|
|
ref="list"
|
|
:style="{ maxHeight: optimizedHeight + 'px' }"
|
|
class="multiselect__content-wrapper"
|
|
tabindex="-1"
|
|
@focus="activate"
|
|
@mousedown.prevent
|
|
>
|
|
<ul :style="contentStyle" :id="'listbox-'+id" class="multiselect__content" role="listbox">
|
|
<slot name="beforeList"/>
|
|
<li v-if="multiple && max === internalValue.length">
|
|
<span class="multiselect__option">
|
|
<slot name="maxElements"> {{ $t('validation.maximum_options_error', { max: max }) }} </slot>
|
|
</span>
|
|
</li>
|
|
<template v-if="!max || internalValue.length < max">
|
|
<li
|
|
v-for="(option, index) of filteredOptions"
|
|
:key="index"
|
|
:id="id + '-' + index"
|
|
:role="!(option && (option.$isLabel || option.$isDisabled)) ? 'option' : null"
|
|
class="multiselect__element"
|
|
>
|
|
<span
|
|
v-if="!(option && (option.$isLabel || option.$isDisabled))"
|
|
:class="optionHighlight(index, option)"
|
|
:data-select="option && option.isTag ? tagPlaceholder : selectLabelText"
|
|
:data-selected="selectedLabelText"
|
|
:data-deselect="deselectLabelText"
|
|
class="multiselect__option"
|
|
@click.stop="select(option)"
|
|
@mouseenter.self="pointerSet(index)"
|
|
>
|
|
<slot :option="option" :search="search" name="option">
|
|
<span>{{ getOptionLabel(option) }}</span>
|
|
</slot>
|
|
</span>
|
|
<span
|
|
v-if="option && (option.$isLabel || option.$isDisabled)"
|
|
:data-select="groupSelect && selectGroupLabelText"
|
|
:data-deselect="groupSelect && deselectGroupLabelText"
|
|
:class="groupHighlight(index, option)"
|
|
class="multiselect__option"
|
|
@mouseenter.self="groupSelect && pointerSet(index)"
|
|
@mousedown.prevent="selectGroup(option)"
|
|
>
|
|
<slot :option="option" :search="search" name="option">
|
|
<span>{{ getOptionLabel(option) }}</span>
|
|
</slot>
|
|
</span>
|
|
</li>
|
|
</template>
|
|
<li v-if="showNoOptions && (options.length === 0 && !search && !loading)">
|
|
<span class="multiselect__option">
|
|
<slot name="noOptions">{{ $t('general.list_is_empty') }}</slot>
|
|
</span>
|
|
</li>
|
|
</ul>
|
|
<slot name="afterList"/>
|
|
</div>
|
|
</transition>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import multiselectMixin from './multiselectMixin'
|
|
import pointerMixin from './pointerMixin'
|
|
|
|
export default {
|
|
name: 'vue-multiselect',
|
|
mixins: [multiselectMixin, pointerMixin],
|
|
props: {
|
|
/**
|
|
* name attribute to match optional label element
|
|
* @default ''
|
|
* @type {String}
|
|
*/
|
|
name: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/**
|
|
* String to show when pointing to an option
|
|
* @default 'Press enter to select'
|
|
* @type {String}
|
|
*/
|
|
selectLabel: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/**
|
|
* String to show when pointing to an option
|
|
* @default 'Press enter to select'
|
|
* @type {String}
|
|
*/
|
|
selectGroupLabel: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/**
|
|
* String to show next to selected option
|
|
* @default 'Selected'
|
|
* @type {String}
|
|
*/
|
|
selectedLabel: {
|
|
type: String,
|
|
default: 'Selected'
|
|
},
|
|
/**
|
|
* String to show when pointing to an already selected option
|
|
* @default 'Press enter to remove'
|
|
* @type {String}
|
|
*/
|
|
deselectLabel: {
|
|
type: String,
|
|
default: 'Press enter to remove'
|
|
},
|
|
/**
|
|
* String to show when pointing to an already selected option
|
|
* @default 'Press enter to remove'
|
|
* @type {String}
|
|
*/
|
|
deselectGroupLabel: {
|
|
type: String,
|
|
default: 'Press enter to deselect group'
|
|
},
|
|
/**
|
|
* Decide whether to show pointer labels
|
|
* @default true
|
|
* @type {Boolean}
|
|
*/
|
|
showLabels: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
/**
|
|
* Limit the display of selected options. The rest will be hidden within the limitText string.
|
|
* @default 99999
|
|
* @type {Integer}
|
|
*/
|
|
limit: {
|
|
type: Number,
|
|
default: 99999
|
|
},
|
|
/**
|
|
* Sets maxHeight style value of the dropdown
|
|
* @default 300
|
|
* @type {Integer}
|
|
*/
|
|
maxHeight: {
|
|
type: Number,
|
|
default: 300
|
|
},
|
|
/**
|
|
* Function that process the message shown when selected
|
|
* elements pass the defined limit.
|
|
* @default 'and * more'
|
|
* @param {Int} count Number of elements more than limit
|
|
* @type {Function}
|
|
*/
|
|
limitText: {
|
|
type: Function,
|
|
default: count => `and ${count} more`
|
|
},
|
|
/**
|
|
* Set true to trigger the loading spinner.
|
|
* @default False
|
|
* @type {Boolean}
|
|
*/
|
|
loading: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
/**
|
|
* Disables the multiselect if true.
|
|
* @default false
|
|
* @type {Boolean}
|
|
*/
|
|
disabled: {
|
|
type: Boolean,
|
|
default: false
|
|
},
|
|
/**
|
|
* Fixed opening direction
|
|
* @default ''
|
|
* @type {String}
|
|
*/
|
|
openDirection: {
|
|
type: String,
|
|
default: ''
|
|
},
|
|
/**
|
|
* Shows slot with message about empty options
|
|
* @default true
|
|
* @type {Boolean}
|
|
*/
|
|
showNoOptions: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
showNoResults: {
|
|
type: Boolean,
|
|
default: true
|
|
},
|
|
tabindex: {
|
|
type: Number,
|
|
default: 0
|
|
},
|
|
invalid: {
|
|
type: Boolean,
|
|
default: false
|
|
}
|
|
},
|
|
computed: {
|
|
isSingleLabelVisible () {
|
|
return (
|
|
(this.singleValue || this.singleValue === 0) &&
|
|
(!this.isOpen || !this.searchable) &&
|
|
!this.visibleValues.length
|
|
)
|
|
},
|
|
isPlaceholderVisible () {
|
|
return !this.internalValue.length && (!this.searchable || !this.isOpen)
|
|
},
|
|
visibleValues () {
|
|
return this.multiple ? this.internalValue.slice(0, this.limit) : []
|
|
},
|
|
singleValue () {
|
|
return this.internalValue[0]
|
|
},
|
|
deselectLabelText () {
|
|
return this.showLabels ? this.deselectLabel : ''
|
|
},
|
|
deselectGroupLabelText () {
|
|
return this.showLabels ? this.deselectGroupLabel : ''
|
|
},
|
|
selectLabelText () {
|
|
return this.showLabels ? this.selectLabel : ''
|
|
},
|
|
selectGroupLabelText () {
|
|
return this.showLabels ? this.selectGroupLabel : ''
|
|
},
|
|
selectedLabelText () {
|
|
return this.showLabels ? this.selectedLabel : ''
|
|
},
|
|
inputStyle () {
|
|
if (
|
|
this.searchable ||
|
|
(this.multiple && this.value && this.value.length)
|
|
) {
|
|
// Hide input by setting the width to 0 allowing it to receive focus
|
|
|
|
return this.isOpen
|
|
? { width: '100%' }
|
|
: ((this.value) ? { width: '0', position: 'absolute', padding: '0' } : '')
|
|
}
|
|
},
|
|
contentStyle () {
|
|
return this.options.length
|
|
? { display: 'inline-block' }
|
|
: { display: 'block' }
|
|
},
|
|
isAbove () {
|
|
if (this.openDirection === 'above' || this.openDirection === 'top') {
|
|
return true
|
|
} else if (
|
|
this.openDirection === 'below' ||
|
|
this.openDirection === 'bottom'
|
|
) {
|
|
return false
|
|
} else {
|
|
return this.preferredOpenDirection === 'above'
|
|
}
|
|
},
|
|
showSearchInput () {
|
|
return (
|
|
this.searchable &&
|
|
(this.hasSingleSelectedSlot &&
|
|
(this.visibleSingleValue || this.visibleSingleValue === 0)
|
|
? this.isOpen
|
|
: true)
|
|
)
|
|
}
|
|
}
|
|
}
|
|
</script>
|