mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-28 12:11:08 -04:00
v5.0.0 update
This commit is contained in:
75
resources/scripts/components/base-select/BaseMultiselect.d.ts
vendored
Normal file
75
resources/scripts/components/base-select/BaseMultiselect.d.ts
vendored
Normal file
@ -0,0 +1,75 @@
|
||||
import Vue, { VNode } from 'vue';
|
||||
|
||||
declare class BaseMultiselect extends Vue {
|
||||
modelValue?: any;
|
||||
value?: any;
|
||||
mode: 'single' | 'multiple' | 'tags';
|
||||
options?: any[];
|
||||
searchable?: boolean;
|
||||
valueProp?: string;
|
||||
trackBy?: string;
|
||||
label?: string;
|
||||
placeholder?: string | null;
|
||||
multipleLabel?: any; // Function
|
||||
disabled?: boolean;
|
||||
max?: number;
|
||||
limit?: number;
|
||||
loading?: boolean;
|
||||
id?: string;
|
||||
caret?: boolean;
|
||||
maxHeight?: string | number;
|
||||
noOptionsText?: string;
|
||||
noResultsText?: string;
|
||||
canDeselect?: boolean;
|
||||
canClear?: boolean;
|
||||
clearOnSearch?: boolean;
|
||||
clearOnSelect?: boolean;
|
||||
delay?: number;
|
||||
filterResults?: boolean;
|
||||
minChars?: number;
|
||||
resolveOnLoad?: boolean;
|
||||
appendNewTag?: boolean;
|
||||
createTag?: boolean;
|
||||
addTagOn?: string[];
|
||||
hideSelected?: boolean;
|
||||
showOptions?: boolean;
|
||||
object?: boolean;
|
||||
required?: boolean;
|
||||
openDirection?: 'top' | 'bottom';
|
||||
nativeSupport?: boolean;
|
||||
classes?: object;
|
||||
strict?: boolean;
|
||||
closeOnSelect?: boolean;
|
||||
autocomplete?: string;
|
||||
groups: boolean;
|
||||
groupLabel: string;
|
||||
groupOptions: string;
|
||||
groupHideEmpty: boolean;
|
||||
groupSelect: boolean;
|
||||
inputType: string;
|
||||
|
||||
$emit(eventName: 'change', e: { originalEvent: Event, value: any }): this;
|
||||
$emit(eventName: 'select', e: { originalEvent: Event, value: any, option: any }): this;
|
||||
$emit(eventName: 'deselect', e: { originalEvent: Event, value: any, option: any }): this;
|
||||
$emit(eventName: 'remove', e: { originalEvent: Event, value: any, option: any }): this;
|
||||
$emit(eventName: 'search-change', e: { originalEvent: Event, query: string }): this;
|
||||
$emit(eventName: 'tag', e: { originalEvent: Event, query: string }): this;
|
||||
$emit(eventName: 'paste', e: { originalEvent: Event }): this;
|
||||
$emit(eventName: 'open'): this;
|
||||
$emit(eventName: 'close'): this;
|
||||
$emit(eventName: 'clear'): this;
|
||||
|
||||
$slots: {
|
||||
placeholder: VNode[];
|
||||
afterlist: VNode[];
|
||||
beforelist: VNode[];
|
||||
list: VNode[];
|
||||
multiplelabel: VNode[];
|
||||
singlelabel: VNode[];
|
||||
option: VNode[];
|
||||
groupLabel: VNode[];
|
||||
tag: VNode[];
|
||||
};
|
||||
}
|
||||
|
||||
export default BaseMultiselect;
|
||||
646
resources/scripts/components/base-select/BaseMultiselect.vue
Executable file
646
resources/scripts/components/base-select/BaseMultiselect.vue
Executable file
@ -0,0 +1,646 @@
|
||||
<template>
|
||||
<BaseContentPlaceholders v-if="contentLoading">
|
||||
<BaseContentPlaceholdersBox
|
||||
:rounded="true"
|
||||
class="w-full"
|
||||
style="height: 40px"
|
||||
/>
|
||||
</BaseContentPlaceholders>
|
||||
<div
|
||||
v-else
|
||||
:id="id"
|
||||
ref="multiselect"
|
||||
:tabindex="tabindex"
|
||||
:class="classList.container"
|
||||
@focusin="activate"
|
||||
@focusout="deactivate"
|
||||
@keydown="handleKeydown"
|
||||
@focus="handleFocus"
|
||||
>
|
||||
<!-- Search -->
|
||||
<template v-if="mode !== 'tags' && searchable && !disabled">
|
||||
<input
|
||||
ref="input"
|
||||
:type="inputType"
|
||||
:modelValue="search"
|
||||
:value="search"
|
||||
:class="classList.search"
|
||||
:autocomplete="autocomplete"
|
||||
@input="handleSearchInput"
|
||||
@paste.stop="handlePaste"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Tags (with search) -->
|
||||
<template v-if="mode == 'tags'">
|
||||
<div :class="classList.tags">
|
||||
<slot
|
||||
v-for="(option, i, key) in iv"
|
||||
name="tag"
|
||||
:option="option"
|
||||
:handleTagRemove="handleTagRemove"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span :key="key" :class="classList.tag">
|
||||
{{ option[label] }}
|
||||
<span
|
||||
v-if="!disabled"
|
||||
:class="classList.tagRemove"
|
||||
@mousedown.stop="handleTagRemove(option, $event)"
|
||||
>
|
||||
<span :class="classList.tagRemoveIcon"></span>
|
||||
</span>
|
||||
</span>
|
||||
</slot>
|
||||
|
||||
<div :class="classList.tagsSearchWrapper">
|
||||
<!-- Used for measuring search width -->
|
||||
<span :class="classList.tagsSearchCopy">{{ search }}</span>
|
||||
|
||||
<!-- Actual search input -->
|
||||
<input
|
||||
v-if="searchable && !disabled"
|
||||
ref="input"
|
||||
:type="inputType"
|
||||
:modelValue="search"
|
||||
:value="search"
|
||||
:class="classList.tagsSearch"
|
||||
:autocomplete="autocomplete"
|
||||
style="box-shadow: none !important"
|
||||
@input="handleSearchInput"
|
||||
@paste.stop="handlePaste"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- Single label -->
|
||||
<template v-if="mode == 'single' && hasSelected && !search && iv">
|
||||
<slot name="singlelabel" :value="iv">
|
||||
<div :class="classList.singleLabel">
|
||||
{{ iv[label] }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<!-- Multiple label -->
|
||||
<template v-if="mode == 'multiple' && hasSelected && !search">
|
||||
<slot name="multiplelabel" :values="iv">
|
||||
<div :class="classList.multipleLabel">
|
||||
{{ multipleLabelText }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<!-- Placeholder -->
|
||||
<template v-if="placeholder && !hasSelected && !search">
|
||||
<slot name="placeholder">
|
||||
<div :class="classList.placeholder">
|
||||
{{ placeholder }}
|
||||
</div>
|
||||
</slot>
|
||||
</template>
|
||||
|
||||
<!-- Spinner -->
|
||||
<slot v-if="busy" name="spinner">
|
||||
<span :class="classList.spinner"></span>
|
||||
</slot>
|
||||
|
||||
<!-- Clear -->
|
||||
<slot
|
||||
v-if="hasSelected && !disabled && canClear && !busy"
|
||||
name="clear"
|
||||
:clear="clear"
|
||||
>
|
||||
<span :class="classList.clear" @mousedown="clear"
|
||||
><span :class="classList.clearIcon"></span
|
||||
></span>
|
||||
</slot>
|
||||
|
||||
<!-- Caret -->
|
||||
<slot v-if="caret" name="caret">
|
||||
<span
|
||||
:class="classList.caret"
|
||||
@mousedown.prevent.stop="handleCaretClick"
|
||||
></span>
|
||||
</slot>
|
||||
|
||||
<!-- Options -->
|
||||
<div :class="classList.dropdown" tabindex="-1">
|
||||
<div class="w-full overflow-y-auto">
|
||||
<slot name="beforelist" :options="fo"></slot>
|
||||
|
||||
<ul :class="classList.options">
|
||||
<template v-if="groups">
|
||||
<li
|
||||
v-for="(group, i, key) in fg"
|
||||
:key="key"
|
||||
:class="classList.group"
|
||||
>
|
||||
<div
|
||||
:class="classList.groupLabel(group)"
|
||||
:data-pointed="isPointed(group)"
|
||||
@mouseenter="setPointer(group)"
|
||||
@click="handleGroupClick(group)"
|
||||
>
|
||||
<slot name="grouplabel" :group="group">
|
||||
<span>{{ group[groupLabel] }}</span>
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
<ul :class="classList.groupOptions">
|
||||
<li
|
||||
v-for="(option, i, key) in group.__VISIBLE__"
|
||||
:key="key"
|
||||
:class="classList.option(option, group)"
|
||||
:data-pointed="isPointed(option)"
|
||||
@mouseenter="setPointer(option)"
|
||||
@click="handleOptionClick(option)"
|
||||
>
|
||||
<slot name="option" :option="option" :search="search">
|
||||
<span>{{ option[label] }}</span>
|
||||
</slot>
|
||||
</li>
|
||||
</ul>
|
||||
</li>
|
||||
</template>
|
||||
<template v-else>
|
||||
<li
|
||||
v-for="(option, i, key) in fo"
|
||||
:key="key"
|
||||
:class="classList.option(option)"
|
||||
:data-pointed="isPointed(option)"
|
||||
@mouseenter="setPointer(option)"
|
||||
@click="handleOptionClick(option)"
|
||||
>
|
||||
<slot name="option" :option="option" :search="search">
|
||||
<span>{{ option[label] }}</span>
|
||||
</slot>
|
||||
</li>
|
||||
</template>
|
||||
</ul>
|
||||
|
||||
<slot v-if="noOptions" name="nooptions">
|
||||
<div :class="classList.noOptions" v-html="noOptionsText"></div>
|
||||
</slot>
|
||||
|
||||
<slot v-if="noResults" name="noresults">
|
||||
<div :class="classList.noResults" v-html="noResultsText"></div>
|
||||
</slot>
|
||||
|
||||
<slot name="afterlist" :options="fo"> </slot>
|
||||
</div>
|
||||
<slot name="action"></slot>
|
||||
</div>
|
||||
|
||||
<!-- Hacky input element to show HTML5 required warning -->
|
||||
<input
|
||||
v-if="required"
|
||||
:class="classList.fakeInput"
|
||||
tabindex="-1"
|
||||
:value="textValue"
|
||||
required
|
||||
/>
|
||||
|
||||
<!-- Native input support -->
|
||||
<template v-if="nativeSupport">
|
||||
<input
|
||||
v-if="mode == 'single'"
|
||||
type="hidden"
|
||||
:name="name"
|
||||
:value="plainValue !== undefined ? plainValue : ''"
|
||||
/>
|
||||
<template v-else>
|
||||
<input
|
||||
v-for="(v, i) in plainValue"
|
||||
:key="i"
|
||||
type="hidden"
|
||||
:name="`${name}[]`"
|
||||
:value="v"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- Create height for empty input -->
|
||||
<div :class="classList.spacer"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import useData from './composables/useData'
|
||||
import useValue from './composables/useValue'
|
||||
import useSearch from './composables/useSearch'
|
||||
import usePointer from './composables/usePointer'
|
||||
import useOptions from './composables/useOptions'
|
||||
import usePointerAction from './composables/usePointerAction'
|
||||
import useDropdown from './composables/useDropdown'
|
||||
import useMultiselect from './composables/useMultiselect'
|
||||
import useKeyboard from './composables/useKeyboard'
|
||||
import useClasses from './composables/useClasses'
|
||||
|
||||
export default {
|
||||
name: 'BaseMultiselect',
|
||||
props: {
|
||||
preserveSearch: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
initialSearch: {
|
||||
type: String,
|
||||
default: null,
|
||||
},
|
||||
contentLoading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
required: false,
|
||||
},
|
||||
modelValue: {
|
||||
required: false,
|
||||
},
|
||||
options: {
|
||||
type: [Array, Object, Function],
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
id: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
},
|
||||
name: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
default: 'multiselect',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'label',
|
||||
},
|
||||
trackBy: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'label',
|
||||
},
|
||||
valueProp: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'value',
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
mode: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'single', // single|multiple|tags
|
||||
},
|
||||
searchable: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
limit: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: -1,
|
||||
},
|
||||
hideSelected: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
createTag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
appendNewTag: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
caret: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
noOptionsText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'The list is empty',
|
||||
},
|
||||
noResultsText: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'No results found',
|
||||
},
|
||||
multipleLabel: {
|
||||
type: Function,
|
||||
required: false,
|
||||
},
|
||||
object: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
delay: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: -1,
|
||||
},
|
||||
minChars: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 0,
|
||||
},
|
||||
resolveOnLoad: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
filterResults: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
clearOnSearch: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
clearOnSelect: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
canDeselect: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
canClear: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
max: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: -1,
|
||||
},
|
||||
showOptions: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
addTagOn: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => ['enter'],
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
openDirection: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'bottom',
|
||||
},
|
||||
nativeSupport: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
invalid: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
classes: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: () => ({
|
||||
container:
|
||||
'p-0 relative mx-auto w-full flex items-center justify-end box-border cursor-pointer border border-gray-200 rounded-md bg-white text-sm leading-snug outline-none max-h-10',
|
||||
containerDisabled:
|
||||
'cursor-default bg-gray-200 bg-opacity-50 !text-gray-400',
|
||||
containerOpen: '',
|
||||
containerOpenTop: '',
|
||||
containerActive: 'ring-1 ring-primary-400 border-primary-400',
|
||||
containerInvalid:
|
||||
'border-red-400 ring-red-400 focus:ring-red-400 focus:border-red-400',
|
||||
containerInvalidActive: 'ring-1 border-red-400 ring-red-400',
|
||||
singleLabel:
|
||||
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5',
|
||||
multipleLabel:
|
||||
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5',
|
||||
search:
|
||||
'w-full absolute inset-0 outline-none appearance-none box-border border-0 text-sm font-sans bg-white rounded-md pl-3.5',
|
||||
tags: 'flex-grow flex-shrink flex flex-wrap mt-1 pl-2',
|
||||
tag: 'bg-primary-500 text-white text-sm font-semibold py-0.5 pl-2 rounded mr-1 mb-1 flex items-center whitespace-nowrap',
|
||||
tagDisabled: 'pr-2 !bg-gray-400 text-white',
|
||||
tagRemove:
|
||||
'flex items-center justify-center p-1 mx-0.5 rounded-sm hover:bg-black hover:bg-opacity-10 group',
|
||||
tagRemoveIcon:
|
||||
'bg-multiselect-remove text-white bg-center bg-no-repeat opacity-30 inline-block w-3 h-3 group-hover:opacity-60',
|
||||
tagsSearchWrapper:
|
||||
'inline-block relative mx-1 mb-1 flex-grow flex-shrink h-full',
|
||||
tagsSearch:
|
||||
'absolute inset-0 border-0 focus:outline-none !shadow-none !focus:shadow-none appearance-none p-0 text-sm font-sans box-border w-full',
|
||||
tagsSearchCopy: 'invisible whitespace-pre-wrap inline-block h-px',
|
||||
placeholder:
|
||||
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 text-gray-400 text-sm',
|
||||
caret:
|
||||
'bg-multiselect-caret bg-center bg-no-repeat w-5 h-5 py-px box-content z-5 relative mr-1 opacity-40 flex-shrink-0 flex-grow-0 transition-transform transform',
|
||||
caretOpen: 'rotate-180 pointer-events-auto',
|
||||
clear:
|
||||
'pr-3.5 relative z-10 opacity-40 transition duration-300 flex-shrink-0 flex-grow-0 flex hover:opacity-80',
|
||||
clearIcon:
|
||||
'bg-multiselect-remove bg-center bg-no-repeat w-2.5 h-4 py-px box-content inline-block',
|
||||
spinner:
|
||||
'bg-multiselect-spinner bg-center bg-no-repeat w-4 h-4 z-10 mr-3.5 animate-spin flex-shrink-0 flex-grow-0',
|
||||
dropdown:
|
||||
'max-h-60 shadow-lg absolute -left-px -right-px -bottom-1 transform translate-y-full border border-gray-300 mt-1 overflow-y-auto z-50 bg-white flex flex-col rounded-md',
|
||||
dropdownTop:
|
||||
'-translate-y-full -top-2 bottom-auto flex-col-reverse rounded-md',
|
||||
dropdownHidden: 'hidden',
|
||||
options: 'flex flex-col p-0 m-0 list-none',
|
||||
optionsTop: 'flex-col-reverse',
|
||||
group: 'p-0 m-0',
|
||||
groupLabel:
|
||||
'flex text-sm box-border items-center justify-start text-left py-1 px-3 font-semibold bg-gray-200 cursor-default leading-normal',
|
||||
groupLabelPointable: 'cursor-pointer',
|
||||
groupLabelPointed: 'bg-gray-300 text-gray-700',
|
||||
groupLabelSelected: 'bg-primary-600 text-white',
|
||||
groupLabelDisabled: 'bg-gray-100 text-gray-300 cursor-not-allowed',
|
||||
groupLabelSelectedPointed: 'bg-primary-600 text-white opacity-90',
|
||||
groupLabelSelectedDisabled:
|
||||
'text-primary-100 bg-primary-600 bg-opacity-50 cursor-not-allowed',
|
||||
groupOptions: 'p-0 m-0',
|
||||
option:
|
||||
'flex items-center justify-start box-border text-left cursor-pointer text-sm leading-snug py-2 px-3',
|
||||
optionPointed: 'text-gray-800 bg-gray-100',
|
||||
optionSelected: 'text-white bg-primary-500',
|
||||
optionDisabled: 'text-gray-300 cursor-not-allowed',
|
||||
optionSelectedPointed: 'text-white bg-primary-500 opacity-90',
|
||||
optionSelectedDisabled:
|
||||
'text-primary-100 bg-primary-500 bg-opacity-50 cursor-not-allowed',
|
||||
noOptions: 'py-2 px-3 text-gray-600 bg-white',
|
||||
noResults: 'py-2 px-3 text-gray-600 bg-white',
|
||||
fakeInput:
|
||||
'bg-transparent absolute left-0 right-0 -bottom-px w-full h-px border-0 p-0 appearance-none outline-none text-transparent',
|
||||
spacer: 'h-9 py-px box-content',
|
||||
}),
|
||||
},
|
||||
strict: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
closeOnSelect: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
autocomplete: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
groups: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
groupLabel: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'label',
|
||||
},
|
||||
groupOptions: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'options',
|
||||
},
|
||||
groupHideEmpty: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
groupSelect: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: true,
|
||||
},
|
||||
inputType: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: 'text',
|
||||
},
|
||||
},
|
||||
emits: [
|
||||
'open',
|
||||
'close',
|
||||
'select',
|
||||
'deselect',
|
||||
'input',
|
||||
'search-change',
|
||||
'tag',
|
||||
'update:modelValue',
|
||||
'change',
|
||||
'clear',
|
||||
],
|
||||
setup(props, context) {
|
||||
const value = useValue(props, context)
|
||||
const pointer = usePointer(props, context)
|
||||
const dropdown = useDropdown(props, context)
|
||||
const search = useSearch(props, context)
|
||||
|
||||
const data = useData(props, context, {
|
||||
iv: value.iv,
|
||||
})
|
||||
|
||||
const multiselect = useMultiselect(props, context, {
|
||||
input: search.input,
|
||||
open: dropdown.open,
|
||||
close: dropdown.close,
|
||||
clearSearch: search.clearSearch,
|
||||
})
|
||||
|
||||
const options = useOptions(props, context, {
|
||||
ev: value.ev,
|
||||
iv: value.iv,
|
||||
search: search.search,
|
||||
clearSearch: search.clearSearch,
|
||||
update: data.update,
|
||||
pointer: pointer.pointer,
|
||||
clearPointer: pointer.clearPointer,
|
||||
blur: multiselect.blur,
|
||||
deactivate: multiselect.deactivate,
|
||||
})
|
||||
|
||||
const pointerAction = usePointerAction(props, context, {
|
||||
fo: options.fo,
|
||||
fg: options.fg,
|
||||
handleOptionClick: options.handleOptionClick,
|
||||
handleGroupClick: options.handleGroupClick,
|
||||
search: search.search,
|
||||
pointer: pointer.pointer,
|
||||
setPointer: pointer.setPointer,
|
||||
clearPointer: pointer.clearPointer,
|
||||
multiselect: multiselect.multiselect,
|
||||
})
|
||||
|
||||
const keyboard = useKeyboard(props, context, {
|
||||
iv: value.iv,
|
||||
update: data.update,
|
||||
search: search.search,
|
||||
setPointer: pointer.setPointer,
|
||||
selectPointer: pointerAction.selectPointer,
|
||||
backwardPointer: pointerAction.backwardPointer,
|
||||
forwardPointer: pointerAction.forwardPointer,
|
||||
blur: multiselect.blur,
|
||||
fo: options.fo,
|
||||
})
|
||||
|
||||
const classes = useClasses(props, context, {
|
||||
isOpen: dropdown.isOpen,
|
||||
isPointed: pointerAction.isPointed,
|
||||
canPointGroups: pointerAction.canPointGroups,
|
||||
isSelected: options.isSelected,
|
||||
isDisabled: options.isDisabled,
|
||||
isActive: multiselect.isActive,
|
||||
resolving: options.resolving,
|
||||
fo: options.fo,
|
||||
})
|
||||
|
||||
return {
|
||||
...value,
|
||||
...dropdown,
|
||||
...multiselect,
|
||||
...pointer,
|
||||
...data,
|
||||
...search,
|
||||
...options,
|
||||
...pointerAction,
|
||||
...keyboard,
|
||||
...classes,
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
@ -0,0 +1,181 @@
|
||||
import { computed, toRefs } from 'vue'
|
||||
|
||||
export default function useClasses(props, context, dependencies) {
|
||||
const refs = toRefs(props)
|
||||
const { disabled, openDirection, showOptions, invalid } = refs
|
||||
|
||||
// ============ DEPENDENCIES ============
|
||||
|
||||
const isOpen = dependencies.isOpen
|
||||
const isPointed = dependencies.isPointed
|
||||
const isSelected = dependencies.isSelected
|
||||
const isDisabled = dependencies.isDisabled
|
||||
const isActive = dependencies.isActive
|
||||
const canPointGroups = dependencies.canPointGroups
|
||||
const resolving = dependencies.resolving
|
||||
const fo = dependencies.fo
|
||||
const isInvalid = invalid
|
||||
|
||||
const classes = {
|
||||
container: 'multiselect',
|
||||
containerDisabled: 'is-disabled',
|
||||
containerOpen: 'is-open',
|
||||
containerOpenTop: 'is-open-top',
|
||||
containerActive: 'is-active',
|
||||
containerInvalid: 'is-invalid',
|
||||
containerInvalidActive: 'is-invalid-active',
|
||||
singleLabel: 'multiselect-single-label',
|
||||
multipleLabel: 'multiselect-multiple-label',
|
||||
search: 'multiselect-search',
|
||||
tags: 'multiselect-tags',
|
||||
tag: 'multiselect-tag',
|
||||
tagDisabled: 'is-disabled',
|
||||
tagRemove: 'multiselect-tag-remove',
|
||||
tagRemoveIcon: 'multiselect-tag-remove-icon',
|
||||
tagsSearchWrapper: 'multiselect-tags-search-wrapper',
|
||||
tagsSearch: 'multiselect-tags-search',
|
||||
tagsSearchCopy: 'multiselect-tags-search-copy',
|
||||
placeholder: 'multiselect-placeholder',
|
||||
caret: 'multiselect-caret',
|
||||
caretOpen: 'is-open',
|
||||
clear: 'multiselect-clear',
|
||||
clearIcon: 'multiselect-clear-icon',
|
||||
spinner: 'multiselect-spinner',
|
||||
dropdown: 'multiselect-dropdown',
|
||||
dropdownTop: 'is-top',
|
||||
dropdownHidden: 'is-hidden',
|
||||
options: 'multiselect-options',
|
||||
optionsTop: 'is-top',
|
||||
group: 'multiselect-group',
|
||||
groupLabel: 'multiselect-group-label',
|
||||
groupLabelPointable: 'is-pointable',
|
||||
groupLabelPointed: 'is-pointed',
|
||||
groupLabelSelected: 'is-selected',
|
||||
groupLabelDisabled: 'is-disabled',
|
||||
groupLabelSelectedPointed: 'is-selected is-pointed',
|
||||
groupLabelSelectedDisabled: 'is-selected is-disabled',
|
||||
groupOptions: 'multiselect-group-options',
|
||||
option: 'multiselect-option',
|
||||
optionPointed: 'is-pointed',
|
||||
optionSelected: 'is-selected',
|
||||
optionDisabled: 'is-disabled',
|
||||
optionSelectedPointed: 'is-selected is-pointed',
|
||||
optionSelectedDisabled: 'is-selected is-disabled',
|
||||
noOptions: 'multiselect-no-options',
|
||||
noResults: 'multiselect-no-results',
|
||||
fakeInput: 'multiselect-fake-input',
|
||||
spacer: 'multiselect-spacer',
|
||||
...refs.classes.value,
|
||||
}
|
||||
|
||||
// ============== COMPUTED ==============
|
||||
|
||||
const showDropdown = computed(() => {
|
||||
return !!(
|
||||
isOpen.value &&
|
||||
showOptions.value &&
|
||||
(!resolving.value || (resolving.value && fo.value.length))
|
||||
)
|
||||
})
|
||||
|
||||
const classList = computed(() => {
|
||||
return {
|
||||
container: [classes.container]
|
||||
.concat(disabled.value ? classes.containerDisabled : [])
|
||||
.concat(
|
||||
showDropdown.value && openDirection.value === 'top'
|
||||
? classes.containerOpenTop
|
||||
: []
|
||||
)
|
||||
.concat(
|
||||
showDropdown.value && openDirection.value !== 'top'
|
||||
? classes.containerOpen
|
||||
: []
|
||||
)
|
||||
.concat(isActive.value ? classes.containerActive : [])
|
||||
.concat(invalid.value ? classes.containerInvalid : []),
|
||||
spacer: classes.spacer,
|
||||
singleLabel: classes.singleLabel,
|
||||
multipleLabel: classes.multipleLabel,
|
||||
search: classes.search,
|
||||
tags: classes.tags,
|
||||
tag: [classes.tag].concat(disabled.value ? classes.tagDisabled : []),
|
||||
tagRemove: classes.tagRemove,
|
||||
tagRemoveIcon: classes.tagRemoveIcon,
|
||||
tagsSearchWrapper: classes.tagsSearchWrapper,
|
||||
tagsSearch: classes.tagsSearch,
|
||||
tagsSearchCopy: classes.tagsSearchCopy,
|
||||
placeholder: classes.placeholder,
|
||||
caret: [classes.caret].concat(isOpen.value ? classes.caretOpen : []),
|
||||
clear: classes.clear,
|
||||
clearIcon: classes.clearIcon,
|
||||
spinner: classes.spinner,
|
||||
dropdown: [classes.dropdown]
|
||||
.concat(openDirection.value === 'top' ? classes.dropdownTop : [])
|
||||
.concat(
|
||||
!isOpen.value || !showOptions.value || !showDropdown.value
|
||||
? classes.dropdownHidden
|
||||
: []
|
||||
),
|
||||
options: [classes.options].concat(
|
||||
openDirection.value === 'top' ? classes.optionsTop : []
|
||||
),
|
||||
group: classes.group,
|
||||
groupLabel: (g) => {
|
||||
let groupLabel = [classes.groupLabel]
|
||||
|
||||
if (isPointed(g)) {
|
||||
groupLabel.push(
|
||||
isSelected(g)
|
||||
? classes.groupLabelSelectedPointed
|
||||
: classes.groupLabelPointed
|
||||
)
|
||||
} else if (isSelected(g) && canPointGroups.value) {
|
||||
groupLabel.push(
|
||||
isDisabled(g)
|
||||
? classes.groupLabelSelectedDisabled
|
||||
: classes.groupLabelSelected
|
||||
)
|
||||
} else if (isDisabled(g)) {
|
||||
groupLabel.push(classes.groupLabelDisabled)
|
||||
}
|
||||
|
||||
if (canPointGroups.value) {
|
||||
groupLabel.push(classes.groupLabelPointable)
|
||||
}
|
||||
|
||||
return groupLabel
|
||||
},
|
||||
groupOptions: classes.groupOptions,
|
||||
option: (o, g) => {
|
||||
let option = [classes.option]
|
||||
|
||||
if (isPointed(o)) {
|
||||
option.push(
|
||||
isSelected(o)
|
||||
? classes.optionSelectedPointed
|
||||
: classes.optionPointed
|
||||
)
|
||||
} else if (isSelected(o)) {
|
||||
option.push(
|
||||
isDisabled(o)
|
||||
? classes.optionSelectedDisabled
|
||||
: classes.optionSelected
|
||||
)
|
||||
} else if (isDisabled(o) || (g && isDisabled(g))) {
|
||||
option.push(classes.optionDisabled)
|
||||
}
|
||||
|
||||
return option
|
||||
},
|
||||
noOptions: classes.noOptions,
|
||||
noResults: classes.noResults,
|
||||
fakeInput: classes.fakeInput,
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
classList,
|
||||
showDropdown,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,56 @@
|
||||
import { toRefs } from 'vue'
|
||||
import isNullish from './../utils/isNullish'
|
||||
|
||||
export default function useData(props, context, dep) {
|
||||
const { object, valueProp, mode } = toRefs(props)
|
||||
|
||||
// ============ DEPENDENCIES ============
|
||||
|
||||
const iv = dep.iv
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
const update = (val) => {
|
||||
// Setting object(s) as internal value
|
||||
iv.value = makeInternal(val)
|
||||
|
||||
// Setting object(s) or plain value as external
|
||||
// value based on `option` setting
|
||||
const externalVal = makeExternal(val)
|
||||
|
||||
context.emit('change', externalVal)
|
||||
context.emit('input', externalVal)
|
||||
context.emit('update:modelValue', externalVal)
|
||||
}
|
||||
|
||||
// no export
|
||||
const makeExternal = (val) => {
|
||||
// If external value should be object
|
||||
// no transformation is required
|
||||
if (object.value) {
|
||||
return val
|
||||
}
|
||||
|
||||
// No need to transform if empty value
|
||||
if (isNullish(val)) {
|
||||
return val
|
||||
}
|
||||
|
||||
// If external should be plain transform
|
||||
// value object to plain values
|
||||
return !Array.isArray(val) ? val[valueProp.value] : val.map(v => v[valueProp.value])
|
||||
}
|
||||
|
||||
// no export
|
||||
const makeInternal = (val) => {
|
||||
if (isNullish(val)) {
|
||||
return mode.value === 'single' ? {} : []
|
||||
}
|
||||
|
||||
return val
|
||||
}
|
||||
|
||||
return {
|
||||
update,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,35 @@
|
||||
import { ref, toRefs } from 'vue'
|
||||
|
||||
export default function useDropdown(props, context, dep) {
|
||||
const { disabled } = toRefs(props)
|
||||
|
||||
// ================ DATA ================
|
||||
|
||||
const isOpen = ref(false)
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
const open = () => {
|
||||
if (isOpen.value || disabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isOpen.value = true
|
||||
context.emit('open')
|
||||
}
|
||||
|
||||
const close = () => {
|
||||
if (!isOpen.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isOpen.value = false
|
||||
context.emit('close')
|
||||
}
|
||||
|
||||
return {
|
||||
isOpen,
|
||||
open,
|
||||
close,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,140 @@
|
||||
import { toRefs } from 'vue'
|
||||
|
||||
export default function useKeyboard(props, context, dep) {
|
||||
const {
|
||||
mode, addTagOn, createTag, openDirection, searchable,
|
||||
showOptions, valueProp, groups: groupped,
|
||||
} = toRefs(props)
|
||||
|
||||
// ============ DEPENDENCIES ============
|
||||
|
||||
const iv = dep.iv
|
||||
const update = dep.update
|
||||
const search = dep.search
|
||||
const setPointer = dep.setPointer
|
||||
const selectPointer = dep.selectPointer
|
||||
const backwardPointer = dep.backwardPointer
|
||||
const forwardPointer = dep.forwardPointer
|
||||
const blur = dep.blur
|
||||
const fo = dep.fo
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
// no export
|
||||
const preparePointer = () => {
|
||||
// When options are hidden and creating tags is allowed
|
||||
// no pointer will be set (because options are hidden).
|
||||
// In such case we need to set the pointer manually to the
|
||||
// first option, which equals to the option created from
|
||||
// the search value.
|
||||
if (mode.value === 'tags' && !showOptions.value && createTag.value && searchable.value && !groupped.value) {
|
||||
setPointer(fo.value[fo.value.map(o => o[valueProp.value]).indexOf(search.value)])
|
||||
}
|
||||
}
|
||||
|
||||
const handleKeydown = (e) => {
|
||||
switch (e.keyCode) {
|
||||
// backspace
|
||||
case 8:
|
||||
if (mode.value === 'single') {
|
||||
return
|
||||
}
|
||||
|
||||
if (searchable.value && [null, ''].indexOf(search.value) === -1) {
|
||||
return
|
||||
}
|
||||
|
||||
if (iv.value.length === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
update([...iv.value].slice(0, -1))
|
||||
break
|
||||
|
||||
// enter
|
||||
case 13:
|
||||
e.preventDefault()
|
||||
|
||||
if (mode.value === 'tags' && addTagOn.value.indexOf('enter') === -1 && createTag.value) {
|
||||
return
|
||||
}
|
||||
|
||||
preparePointer()
|
||||
selectPointer()
|
||||
break
|
||||
|
||||
// space
|
||||
case 32:
|
||||
if (searchable.value && mode.value !== 'tags' && !createTag.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode.value === 'tags' && ((addTagOn.value.indexOf('space') === -1 && createTag.value) || !createTag.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
e.preventDefault()
|
||||
|
||||
preparePointer()
|
||||
selectPointer()
|
||||
break
|
||||
|
||||
// tab
|
||||
// semicolon
|
||||
// comma
|
||||
case 9:
|
||||
case 186:
|
||||
case 188:
|
||||
if (mode.value !== 'tags') {
|
||||
return
|
||||
}
|
||||
|
||||
const charMap = {
|
||||
9: 'tab',
|
||||
186: ';',
|
||||
188: ','
|
||||
}
|
||||
|
||||
if (addTagOn.value.indexOf(charMap[e.keyCode]) === -1 || !createTag.value) {
|
||||
return
|
||||
}
|
||||
|
||||
preparePointer()
|
||||
selectPointer()
|
||||
e.preventDefault()
|
||||
break
|
||||
|
||||
// escape
|
||||
case 27:
|
||||
blur()
|
||||
break
|
||||
|
||||
// up
|
||||
case 38:
|
||||
e.preventDefault()
|
||||
|
||||
if (!showOptions.value) {
|
||||
return
|
||||
}
|
||||
|
||||
openDirection.value === 'top' ? forwardPointer() : backwardPointer()
|
||||
break
|
||||
|
||||
// down
|
||||
case 40:
|
||||
e.preventDefault()
|
||||
|
||||
if (!showOptions.value) {
|
||||
return
|
||||
}
|
||||
|
||||
openDirection.value === 'top' ? backwardPointer() : forwardPointer()
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
handleKeydown,
|
||||
preparePointer,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
import { ref, toRefs, computed } from 'vue'
|
||||
|
||||
export default function useMultiselect(props, context, dep) {
|
||||
const { searchable, disabled } = toRefs(props)
|
||||
|
||||
// ============ DEPENDENCIES ============
|
||||
|
||||
const input = dep.input
|
||||
const open = dep.open
|
||||
const close = dep.close
|
||||
const clearSearch = dep.clearSearch
|
||||
|
||||
// ================ DATA ================
|
||||
|
||||
const multiselect = ref(null)
|
||||
|
||||
const isActive = ref(false)
|
||||
|
||||
// ============== COMPUTED ==============
|
||||
|
||||
const tabindex = computed(() => {
|
||||
return searchable.value || disabled.value ? -1 : 0
|
||||
})
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
const blur = () => {
|
||||
if (searchable.value) {
|
||||
input.value.blur()
|
||||
}
|
||||
|
||||
multiselect.value.blur()
|
||||
}
|
||||
|
||||
const handleFocus = () => {
|
||||
if (searchable.value && !disabled.value) {
|
||||
input.value.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const activate = () => {
|
||||
|
||||
if (disabled.value) {
|
||||
return
|
||||
}
|
||||
|
||||
isActive.value = true
|
||||
|
||||
open()
|
||||
}
|
||||
|
||||
const deactivate = () => {
|
||||
isActive.value = false
|
||||
|
||||
setTimeout(() => {
|
||||
if (!isActive.value) {
|
||||
close()
|
||||
clearSearch()
|
||||
}
|
||||
}, 1)
|
||||
}
|
||||
|
||||
const handleCaretClick = () => {
|
||||
if (isActive.value) {
|
||||
deactivate()
|
||||
blur()
|
||||
} else {
|
||||
activate()
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
multiselect,
|
||||
tabindex,
|
||||
isActive,
|
||||
blur,
|
||||
handleFocus,
|
||||
activate,
|
||||
deactivate,
|
||||
handleCaretClick,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,626 @@
|
||||
import { ref, toRefs, computed, watch, nextTick } from 'vue'
|
||||
import normalize from './../utils/normalize'
|
||||
import isObject from './../utils/isObject'
|
||||
import isNullish from './../utils/isNullish'
|
||||
import arraysEqual from './../utils/arraysEqual'
|
||||
|
||||
export default function useOptions(props, context, dep) {
|
||||
const {
|
||||
options, mode, trackBy, limit, hideSelected, createTag, label,
|
||||
appendNewTag, multipleLabel, object, loading, delay, resolveOnLoad,
|
||||
minChars, filterResults, clearOnSearch, clearOnSelect, valueProp,
|
||||
canDeselect, max, strict, closeOnSelect, groups: groupped, groupLabel,
|
||||
groupOptions, groupHideEmpty, groupSelect,
|
||||
} = toRefs(props)
|
||||
|
||||
// ============ DEPENDENCIES ============
|
||||
|
||||
const iv = dep.iv
|
||||
const ev = dep.ev
|
||||
const search = dep.search
|
||||
const clearSearch = dep.clearSearch
|
||||
const update = dep.update
|
||||
const pointer = dep.pointer
|
||||
const clearPointer = dep.clearPointer
|
||||
const blur = dep.blur
|
||||
const deactivate = dep.deactivate
|
||||
|
||||
// ================ DATA ================
|
||||
|
||||
// no export
|
||||
// appendedOptions
|
||||
const ap = ref([])
|
||||
|
||||
// no export
|
||||
// resolvedOptions
|
||||
const ro = ref([])
|
||||
|
||||
const resolving = ref(false)
|
||||
|
||||
// ============== COMPUTED ==============
|
||||
|
||||
// no export
|
||||
// extendedOptions
|
||||
const eo = computed(() => {
|
||||
if (groupped.value) {
|
||||
let groups = ro.value || /* istanbul ignore next */[]
|
||||
|
||||
let eo = []
|
||||
|
||||
groups.forEach((group) => {
|
||||
optionsToArray(group[groupOptions.value]).forEach((option) => {
|
||||
eo.push(Object.assign({}, option, group.disabled ? { disabled: true } : {}))
|
||||
})
|
||||
})
|
||||
|
||||
return eo
|
||||
} else {
|
||||
let eo = optionsToArray(ro.value || [])
|
||||
|
||||
if (ap.value.length) {
|
||||
eo = eo.concat(ap.value)
|
||||
}
|
||||
|
||||
return eo
|
||||
}
|
||||
})
|
||||
|
||||
const fg = computed(() => {
|
||||
if (!groupped.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return filterGroups((ro.value || /* istanbul ignore next */[]).map((group) => {
|
||||
const arrayOptions = optionsToArray(group[groupOptions.value])
|
||||
|
||||
return {
|
||||
...group,
|
||||
group: true,
|
||||
[groupOptions.value]: filterOptions(arrayOptions, false).map(o => Object.assign({}, o, group.disabled ? { disabled: true } : {})),
|
||||
__VISIBLE__: filterOptions(arrayOptions).map(o => Object.assign({}, o, group.disabled ? { disabled: true } : {})),
|
||||
}
|
||||
// Difference between __VISIBLE__ and {groupOptions}: visible does not contain selected options when hideSelected=true
|
||||
}))
|
||||
})
|
||||
|
||||
// filteredOptions
|
||||
const fo = computed(() => {
|
||||
let options = eo.value
|
||||
|
||||
if (createdTag.value.length) {
|
||||
options = createdTag.value.concat(options)
|
||||
}
|
||||
|
||||
options = filterOptions(options)
|
||||
|
||||
if (limit.value > 0) {
|
||||
options = options.slice(0, limit.value)
|
||||
}
|
||||
|
||||
return options
|
||||
})
|
||||
|
||||
const hasSelected = computed(() => {
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
return !isNullish(iv.value[valueProp.value])
|
||||
|
||||
case 'multiple':
|
||||
case 'tags':
|
||||
return !isNullish(iv.value) && iv.value.length > 0
|
||||
}
|
||||
})
|
||||
|
||||
const multipleLabelText = computed(() => {
|
||||
return multipleLabel !== undefined && multipleLabel.value !== undefined
|
||||
? multipleLabel.value(iv.value)
|
||||
: (iv.value && iv.value.length > 1 ? `${iv.value.length} options selected` : `1 option selected`)
|
||||
})
|
||||
|
||||
const noOptions = computed(() => {
|
||||
return !eo.value.length && !resolving.value && !createdTag.value.length
|
||||
})
|
||||
|
||||
|
||||
const noResults = computed(() => {
|
||||
return eo.value.length > 0 && fo.value.length == 0 && ((search.value && groupped.value) || !groupped.value)
|
||||
})
|
||||
|
||||
// no export
|
||||
const createdTag = computed(() => {
|
||||
if (createTag.value === false || !search.value) {
|
||||
return []
|
||||
}
|
||||
|
||||
return getOptionByTrackBy(search.value) !== -1 ? [] : [{
|
||||
[valueProp.value]: search.value,
|
||||
[label.value]: search.value,
|
||||
[trackBy.value]: search.value,
|
||||
}]
|
||||
})
|
||||
|
||||
// no export
|
||||
const nullValue = computed(() => {
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
return null
|
||||
|
||||
case 'multiple':
|
||||
case 'tags':
|
||||
return []
|
||||
}
|
||||
})
|
||||
|
||||
const busy = computed(() => {
|
||||
return loading.value || resolving.value
|
||||
})
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
/**
|
||||
* @param {array|object|string|number} option
|
||||
*/
|
||||
const select = (option) => {
|
||||
if (typeof option !== 'object') {
|
||||
option = getOption(option)
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
update(option)
|
||||
break
|
||||
|
||||
case 'multiple':
|
||||
case 'tags':
|
||||
update((iv.value).concat(option))
|
||||
break
|
||||
}
|
||||
|
||||
context.emit('select', finalValue(option), option)
|
||||
}
|
||||
|
||||
const deselect = (option) => {
|
||||
if (typeof option !== 'object') {
|
||||
option = getOption(option)
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
clear()
|
||||
break
|
||||
|
||||
case 'tags':
|
||||
case 'multiple':
|
||||
update(Array.isArray(option)
|
||||
? iv.value.filter(v => option.map(o => o[valueProp.value]).indexOf(v[valueProp.value]) === -1)
|
||||
: iv.value.filter(v => v[valueProp.value] != option[valueProp.value]))
|
||||
break
|
||||
}
|
||||
|
||||
context.emit('deselect', finalValue(option), option)
|
||||
}
|
||||
|
||||
// no export
|
||||
const finalValue = (option) => {
|
||||
return object.value ? option : option[valueProp.value]
|
||||
}
|
||||
|
||||
const remove = (option) => {
|
||||
deselect(option)
|
||||
}
|
||||
|
||||
const handleTagRemove = (option, e) => {
|
||||
if (e.button !== 0) {
|
||||
e.preventDefault()
|
||||
return
|
||||
}
|
||||
|
||||
remove(option)
|
||||
}
|
||||
|
||||
const clear = () => {
|
||||
context.emit('clear')
|
||||
update(nullValue.value)
|
||||
}
|
||||
|
||||
const isSelected = (option) => {
|
||||
if (option.group !== undefined) {
|
||||
return mode.value === 'single' ? false : areAllSelected(option[groupOptions.value]) && option[groupOptions.value].length
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
return !isNullish(iv.value) && iv.value[valueProp.value] == option[valueProp.value]
|
||||
|
||||
case 'tags':
|
||||
case 'multiple':
|
||||
return !isNullish(iv.value) && iv.value.map(o => o[valueProp.value]).indexOf(option[valueProp.value]) !== -1
|
||||
}
|
||||
}
|
||||
|
||||
const isDisabled = (option) => {
|
||||
return option.disabled === true
|
||||
}
|
||||
|
||||
const isMax = () => {
|
||||
if (max === undefined || max.value === -1 || (!hasSelected.value && max.value > 0)) {
|
||||
return false
|
||||
}
|
||||
|
||||
return iv.value.length >= max.value
|
||||
}
|
||||
|
||||
const handleOptionClick = (option) => {
|
||||
if (isDisabled(option)) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
if (isSelected(option)) {
|
||||
if (canDeselect.value) {
|
||||
deselect(option)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
blur()
|
||||
select(option)
|
||||
break
|
||||
|
||||
case 'multiple':
|
||||
if (isSelected(option)) {
|
||||
deselect(option)
|
||||
return
|
||||
}
|
||||
|
||||
if (isMax()) {
|
||||
return
|
||||
}
|
||||
|
||||
select(option)
|
||||
|
||||
if (clearOnSelect.value) {
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
if (hideSelected.value) {
|
||||
clearPointer()
|
||||
}
|
||||
|
||||
// If we need to close the dropdown on select we also need
|
||||
// to blur the input, otherwise further searches will not
|
||||
// display any options
|
||||
if (closeOnSelect.value) {
|
||||
blur()
|
||||
}
|
||||
break
|
||||
|
||||
case 'tags':
|
||||
if (isSelected(option)) {
|
||||
deselect(option)
|
||||
return
|
||||
}
|
||||
|
||||
if (isMax()) {
|
||||
return
|
||||
}
|
||||
|
||||
if (getOption(option[valueProp.value]) === undefined && createTag.value) {
|
||||
context.emit('tag', option[valueProp.value])
|
||||
|
||||
if (appendNewTag.value) {
|
||||
appendOption(option)
|
||||
}
|
||||
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
if (clearOnSelect.value) {
|
||||
clearSearch()
|
||||
}
|
||||
|
||||
select(option)
|
||||
|
||||
if (hideSelected.value) {
|
||||
clearPointer()
|
||||
}
|
||||
|
||||
// If we need to close the dropdown on select we also need
|
||||
// to blur the input, otherwise further searches will not
|
||||
// display any options
|
||||
if (closeOnSelect.value) {
|
||||
blur()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (closeOnSelect.value) {
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
const handleGroupClick = (group) => {
|
||||
if (isDisabled(group) || mode.value === 'single' || !groupSelect.value) {
|
||||
return
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'multiple':
|
||||
case 'tags':
|
||||
if (areAllEnabledSelected(group[groupOptions.value])) {
|
||||
deselect(group[groupOptions.value])
|
||||
} else {
|
||||
select(group[groupOptions.value]
|
||||
.filter(o => iv.value.map(v => v[valueProp.value]).indexOf(o[valueProp.value]) === -1)
|
||||
.filter(o => !o.disabled)
|
||||
.filter((o, k) => iv.value.length + 1 + k <= max.value || max.value === -1)
|
||||
)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if (closeOnSelect.value) {
|
||||
deactivate()
|
||||
}
|
||||
}
|
||||
|
||||
// no export
|
||||
const areAllEnabledSelected = (options) => {
|
||||
return options.find(o => !isSelected(o) && !o.disabled) === undefined
|
||||
}
|
||||
|
||||
// no export
|
||||
const areAllSelected = (options) => {
|
||||
return options.find(o => !isSelected(o)) === undefined
|
||||
}
|
||||
|
||||
const getOption = (val) => {
|
||||
return eo.value[eo.value.map(o => String(o[valueProp.value])).indexOf(String(val))]
|
||||
}
|
||||
|
||||
// no export
|
||||
const getOptionByTrackBy = (val, norm = true) => {
|
||||
return eo.value.map(o => o[trackBy.value]).indexOf(val)
|
||||
}
|
||||
|
||||
// no export
|
||||
const shouldHideOption = (option) => {
|
||||
return ['tags', 'multiple'].indexOf(mode.value) !== -1 && hideSelected.value && isSelected(option)
|
||||
}
|
||||
|
||||
// no export
|
||||
const appendOption = (option) => {
|
||||
ap.value.push(option)
|
||||
}
|
||||
|
||||
// no export
|
||||
const filterGroups = (groups) => {
|
||||
// If the search has value we need to filter among
|
||||
// he ones that are visible to the user to avoid
|
||||
// displaying groups which technically have options
|
||||
// based on search but that option is already selected.
|
||||
return groupHideEmpty.value
|
||||
? groups.filter(g => search.value
|
||||
? g.__VISIBLE__.length
|
||||
: g[groupOptions.value].length
|
||||
)
|
||||
: groups.filter(g => search.value ? g.__VISIBLE__.length : true)
|
||||
}
|
||||
|
||||
// no export
|
||||
const filterOptions = (options, excludeHideSelected = true) => {
|
||||
let fo = options
|
||||
|
||||
if (search.value && filterResults.value) {
|
||||
fo = fo.filter((option) => {
|
||||
return normalize(option[trackBy.value], strict.value).indexOf(normalize(search.value, strict.value)) !== -1
|
||||
})
|
||||
}
|
||||
|
||||
if (hideSelected.value && excludeHideSelected) {
|
||||
fo = fo.filter((option) => !shouldHideOption(option))
|
||||
}
|
||||
|
||||
return fo
|
||||
}
|
||||
|
||||
// no export
|
||||
const optionsToArray = (options) => {
|
||||
let uo = options
|
||||
|
||||
// Transforming an object to an array of objects
|
||||
if (isObject(uo)) {
|
||||
uo = Object.keys(uo).map((key) => {
|
||||
let val = uo[key]
|
||||
|
||||
return { [valueProp.value]: key, [trackBy.value]: val, [label.value]: val }
|
||||
})
|
||||
}
|
||||
|
||||
// Transforming an plain arrays to an array of objects
|
||||
uo = uo.map((val) => {
|
||||
return typeof val === 'object' ? val : { [valueProp.value]: val, [trackBy.value]: val, [label.value]: val }
|
||||
})
|
||||
|
||||
return uo
|
||||
}
|
||||
|
||||
// no export
|
||||
const initInternalValue = () => {
|
||||
if (!isNullish(ev.value)) {
|
||||
iv.value = makeInternal(ev.value)
|
||||
}
|
||||
}
|
||||
|
||||
const resolveOptions = (callback) => {
|
||||
resolving.value = true
|
||||
|
||||
options.value(search.value).then((response) => {
|
||||
ro.value = response
|
||||
|
||||
if (typeof callback == 'function') {
|
||||
callback(response)
|
||||
}
|
||||
|
||||
resolving.value = false
|
||||
})
|
||||
}
|
||||
|
||||
// no export
|
||||
const refreshLabels = () => {
|
||||
if (!hasSelected.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (mode.value === 'single') {
|
||||
let newLabel = getOption(iv.value[valueProp.value])[label.value]
|
||||
|
||||
iv.value[label.value] = newLabel
|
||||
|
||||
if (object.value) {
|
||||
ev.value[label.value] = newLabel
|
||||
}
|
||||
} else {
|
||||
iv.value.forEach((val, i) => {
|
||||
let newLabel = getOption(iv.value[i][valueProp.value])[label.value]
|
||||
|
||||
iv.value[i][label.value] = newLabel
|
||||
|
||||
if (object.value) {
|
||||
ev.value[i][label.value] = newLabel
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const refreshOptions = (callback) => {
|
||||
resolveOptions(callback)
|
||||
}
|
||||
|
||||
// no export
|
||||
const makeInternal = (val) => {
|
||||
if (isNullish(val)) {
|
||||
return mode.value === 'single' ? {} : []
|
||||
}
|
||||
|
||||
if (object.value) {
|
||||
return val
|
||||
}
|
||||
|
||||
// If external should be plain transform
|
||||
// value object to plain values
|
||||
return mode.value === 'single' ? getOption(val) || {} : val.filter(v => !!getOption(v)).map(v => getOption(v))
|
||||
}
|
||||
|
||||
// ================ HOOKS ===============
|
||||
|
||||
if (mode.value !== 'single' && !isNullish(ev.value) && !Array.isArray(ev.value)) {
|
||||
throw new Error(`v-model must be an array when using "${mode.value}" mode`)
|
||||
}
|
||||
|
||||
if (options && typeof options.value == 'function') {
|
||||
if (resolveOnLoad.value) {
|
||||
resolveOptions(initInternalValue)
|
||||
} else if (object.value == true) {
|
||||
initInternalValue()
|
||||
}
|
||||
}
|
||||
else {
|
||||
ro.value = options.value
|
||||
|
||||
initInternalValue()
|
||||
}
|
||||
|
||||
// ============== WATCHERS ==============
|
||||
|
||||
if (delay.value > -1) {
|
||||
watch(search, (query) => {
|
||||
if (query.length < minChars.value) {
|
||||
return
|
||||
}
|
||||
|
||||
resolving.value = true
|
||||
|
||||
if (clearOnSearch.value) {
|
||||
ro.value = []
|
||||
}
|
||||
setTimeout(() => {
|
||||
if (query != search.value) {
|
||||
return
|
||||
}
|
||||
|
||||
options.value(search.value).then((response) => {
|
||||
if (query == search.value) {
|
||||
ro.value = response
|
||||
pointer.value = fo.value.filter(o => o.disabled !== true)[0] || null
|
||||
resolving.value = false
|
||||
}
|
||||
})
|
||||
}, delay.value)
|
||||
|
||||
}, { flush: 'sync' })
|
||||
}
|
||||
|
||||
watch(ev, (newValue) => {
|
||||
if (isNullish(newValue)) {
|
||||
iv.value = makeInternal(newValue)
|
||||
return
|
||||
}
|
||||
|
||||
switch (mode.value) {
|
||||
case 'single':
|
||||
if (object.value ? newValue[valueProp.value] != iv.value[valueProp.value] : newValue != iv.value[valueProp.value]) {
|
||||
iv.value = makeInternal(newValue)
|
||||
}
|
||||
break
|
||||
|
||||
case 'multiple':
|
||||
case 'tags':
|
||||
if (!arraysEqual(object.value ? newValue.map(o => o[valueProp.value]) : newValue, iv.value.map(o => o[valueProp.value]))) {
|
||||
iv.value = makeInternal(newValue)
|
||||
}
|
||||
break
|
||||
}
|
||||
}, { deep: true })
|
||||
|
||||
if (typeof props.options !== 'function') {
|
||||
watch(options, (n, o) => {
|
||||
ro.value = props.options
|
||||
|
||||
if (!Object.keys(iv.value).length) {
|
||||
initInternalValue()
|
||||
}
|
||||
|
||||
refreshLabels()
|
||||
})
|
||||
}
|
||||
|
||||
return {
|
||||
fo,
|
||||
filteredOptions: fo,
|
||||
hasSelected,
|
||||
multipleLabelText,
|
||||
eo,
|
||||
extendedOptions: eo,
|
||||
fg,
|
||||
filteredGroups: fg,
|
||||
noOptions,
|
||||
noResults,
|
||||
resolving,
|
||||
busy,
|
||||
select,
|
||||
deselect,
|
||||
remove,
|
||||
clear,
|
||||
isSelected,
|
||||
isDisabled,
|
||||
isMax,
|
||||
getOption,
|
||||
handleOptionClick,
|
||||
handleGroupClick,
|
||||
handleTagRemove,
|
||||
refreshOptions,
|
||||
resolveOptions,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { ref, toRefs } from 'vue'
|
||||
|
||||
export default function usePointer(props, context, dep) {
|
||||
const { groupSelect, mode, groups } = toRefs(props)
|
||||
|
||||
// ================ DATA ================
|
||||
|
||||
const pointer = ref(null)
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
const setPointer = (option) => {
|
||||
if (option === undefined || (option !== null && option.disabled)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (groups.value && option && option.group && (mode.value === 'single' || !groupSelect.value)) {
|
||||
return
|
||||
}
|
||||
|
||||
pointer.value = option
|
||||
}
|
||||
|
||||
const clearPointer = () => {
|
||||
setPointer(null)
|
||||
}
|
||||
|
||||
return {
|
||||
pointer,
|
||||
setPointer,
|
||||
clearPointer,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,241 @@
|
||||
import { toRefs, watch, nextTick, computed } from 'vue'
|
||||
|
||||
export default function usePointer(props, context, dep) {
|
||||
const {
|
||||
valueProp, showOptions, searchable, groupLabel,
|
||||
groups: groupped, mode, groupSelect,
|
||||
} = toRefs(props)
|
||||
|
||||
// ============ DEPENDENCIES ============
|
||||
|
||||
const fo = dep.fo
|
||||
const fg = dep.fg
|
||||
const handleOptionClick = dep.handleOptionClick
|
||||
const handleGroupClick = dep.handleGroupClick
|
||||
const search = dep.search
|
||||
const pointer = dep.pointer
|
||||
const setPointer = dep.setPointer
|
||||
const clearPointer = dep.clearPointer
|
||||
const multiselect = dep.multiselect
|
||||
|
||||
// ============== COMPUTED ==============
|
||||
|
||||
// no export
|
||||
const options = computed(() => {
|
||||
return fo.value.filter(o => !o.disabled)
|
||||
})
|
||||
|
||||
const groups = computed(() => {
|
||||
return fg.value.filter(o => !o.disabled)
|
||||
})
|
||||
|
||||
const canPointGroups = computed(() => {
|
||||
return mode.value !== 'single' && groupSelect.value
|
||||
})
|
||||
|
||||
const isPointerGroup = computed(() => {
|
||||
return pointer.value && pointer.value.group
|
||||
})
|
||||
|
||||
const currentGroup = computed(() => {
|
||||
return getParentGroup(pointer.value)
|
||||
})
|
||||
|
||||
const prevGroup = computed(() => {
|
||||
const group = isPointerGroup.value ? pointer.value : /* istanbul ignore next */ getParentGroup(pointer.value)
|
||||
const groupIndex = groups.value.map(g => g[groupLabel.value]).indexOf(group[groupLabel.value])
|
||||
let prevGroup = groups.value[groupIndex - 1]
|
||||
|
||||
if (prevGroup === undefined) {
|
||||
prevGroup = lastGroup.value
|
||||
}
|
||||
|
||||
return prevGroup
|
||||
})
|
||||
|
||||
const nextGroup = computed(() => {
|
||||
let nextIndex = groups.value.map(g => g.label).indexOf(isPointerGroup.value
|
||||
? pointer.value[groupLabel.value]
|
||||
: getParentGroup(pointer.value)[groupLabel.value]) + 1
|
||||
|
||||
if (groups.value.length <= nextIndex) {
|
||||
nextIndex = 0
|
||||
}
|
||||
|
||||
return groups.value[nextIndex]
|
||||
})
|
||||
|
||||
const lastGroup = computed(() => {
|
||||
return [...groups.value].slice(-1)[0]
|
||||
})
|
||||
|
||||
const currentGroupFirstEnabledOption = computed(() => {
|
||||
return pointer.value.__VISIBLE__.filter(o => !o.disabled)[0]
|
||||
})
|
||||
|
||||
const currentGroupPrevEnabledOption = computed(() => {
|
||||
const options = currentGroup.value.__VISIBLE__.filter(o => !o.disabled)
|
||||
return options[options.map(o => o[valueProp.value]).indexOf(pointer.value[valueProp.value]) - 1]
|
||||
})
|
||||
|
||||
const currentGroupNextEnabledOption = computed(() => {
|
||||
const options = getParentGroup(pointer.value).__VISIBLE__.filter(o => !o.disabled)
|
||||
return options[options.map(o => o[valueProp.value]).indexOf(pointer.value[valueProp.value]) + 1]
|
||||
})
|
||||
|
||||
const prevGroupLastEnabledOption = computed(() => {
|
||||
return [...prevGroup.value.__VISIBLE__.filter(o => !o.disabled)].slice(-1)[0]
|
||||
})
|
||||
|
||||
const lastGroupLastEnabledOption = computed(() => {
|
||||
return [...lastGroup.value.__VISIBLE__.filter(o => !o.disabled)].slice(-1)[0]
|
||||
})
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
const isPointed = (option) => {
|
||||
if (!pointer.value) {
|
||||
return
|
||||
}
|
||||
|
||||
if (option.group) {
|
||||
return pointer.value[groupLabel.value] == option[groupLabel.value]
|
||||
} else {
|
||||
return pointer.value[valueProp.value] == option[valueProp.value]
|
||||
}
|
||||
}
|
||||
|
||||
const setPointerFirst = () => {
|
||||
setPointer(options.value[0] || null)
|
||||
}
|
||||
|
||||
const selectPointer = () => {
|
||||
if (!pointer.value || pointer.value.disabled === true) {
|
||||
return
|
||||
}
|
||||
|
||||
if (isPointerGroup.value) {
|
||||
handleGroupClick(pointer.value)
|
||||
} else {
|
||||
handleOptionClick(pointer.value)
|
||||
}
|
||||
}
|
||||
|
||||
const forwardPointer = () => {
|
||||
if (pointer.value === null) {
|
||||
setPointer((groupped.value && canPointGroups.value ? groups.value[0] : options.value[0]) || null)
|
||||
}
|
||||
else if (groupped.value && canPointGroups.value) {
|
||||
let nextPointer = isPointerGroup.value ? currentGroupFirstEnabledOption.value : currentGroupNextEnabledOption.value
|
||||
|
||||
if (nextPointer === undefined) {
|
||||
nextPointer = nextGroup.value
|
||||
}
|
||||
|
||||
setPointer(nextPointer || /* istanbul ignore next */ null)
|
||||
} else {
|
||||
let next = options.value.map(o => o[valueProp.value]).indexOf(pointer.value[valueProp.value]) + 1
|
||||
|
||||
if (options.value.length <= next) {
|
||||
next = 0
|
||||
}
|
||||
|
||||
setPointer(options.value[next] || null)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
adjustWrapperScrollToPointer()
|
||||
})
|
||||
}
|
||||
|
||||
const backwardPointer = () => {
|
||||
if (pointer.value === null) {
|
||||
let prevPointer = options.value[options.value.length - 1]
|
||||
|
||||
if (groupped.value && canPointGroups.value) {
|
||||
prevPointer = lastGroupLastEnabledOption.value
|
||||
|
||||
if (prevPointer === undefined) {
|
||||
prevPointer = lastGroup.value
|
||||
}
|
||||
}
|
||||
|
||||
setPointer(prevPointer || null)
|
||||
}
|
||||
else if (groupped.value && canPointGroups.value) {
|
||||
let prevPointer = isPointerGroup.value ? prevGroupLastEnabledOption.value : currentGroupPrevEnabledOption.value
|
||||
|
||||
if (prevPointer === undefined) {
|
||||
prevPointer = isPointerGroup.value ? prevGroup.value : currentGroup.value
|
||||
}
|
||||
|
||||
setPointer(prevPointer || /* istanbul ignore next */ null)
|
||||
} else {
|
||||
let prevIndex = options.value.map(o => o[valueProp.value]).indexOf(pointer.value[valueProp.value]) - 1
|
||||
|
||||
if (prevIndex < 0) {
|
||||
prevIndex = options.value.length - 1
|
||||
}
|
||||
|
||||
setPointer(options.value[prevIndex] || null)
|
||||
}
|
||||
|
||||
nextTick(() => {
|
||||
adjustWrapperScrollToPointer()
|
||||
})
|
||||
}
|
||||
|
||||
const getParentGroup = (option) => {
|
||||
return groups.value.find((group) => {
|
||||
return group.__VISIBLE__.map(o => o[valueProp.value]).indexOf(option[valueProp.value]) !== -1
|
||||
})
|
||||
}
|
||||
|
||||
// no export
|
||||
/* istanbul ignore next */
|
||||
const adjustWrapperScrollToPointer = () => {
|
||||
let pointedOption = multiselect.value.querySelector(`[data-pointed]`)
|
||||
|
||||
if (!pointedOption) {
|
||||
return
|
||||
}
|
||||
|
||||
let wrapper = pointedOption.parentElement.parentElement
|
||||
|
||||
if (groupped.value) {
|
||||
wrapper = isPointerGroup.value
|
||||
? pointedOption.parentElement.parentElement.parentElement
|
||||
: pointedOption.parentElement.parentElement.parentElement.parentElement
|
||||
}
|
||||
|
||||
if (pointedOption.offsetTop + pointedOption.offsetHeight > wrapper.clientHeight + wrapper.scrollTop) {
|
||||
wrapper.scrollTop = pointedOption.offsetTop + pointedOption.offsetHeight - wrapper.clientHeight
|
||||
}
|
||||
|
||||
if (pointedOption.offsetTop < wrapper.scrollTop) {
|
||||
wrapper.scrollTop = pointedOption.offsetTop
|
||||
}
|
||||
}
|
||||
|
||||
// ============== WATCHERS ==============
|
||||
|
||||
watch(search, (val) => {
|
||||
if (searchable.value) {
|
||||
if (val.length && showOptions.value) {
|
||||
setPointerFirst()
|
||||
} else {
|
||||
clearPointer()
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
pointer,
|
||||
canPointGroups,
|
||||
isPointed,
|
||||
setPointerFirst,
|
||||
selectPointer,
|
||||
forwardPointer,
|
||||
backwardPointer,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,41 @@
|
||||
import { ref, toRefs, computed, watch } from 'vue'
|
||||
|
||||
export default function useSearch (props, context, dep)
|
||||
{
|
||||
const { preserveSearch } = toRefs(props)
|
||||
|
||||
// ================ DATA ================
|
||||
|
||||
const search = ref(props.initialSearch) || ref(null)
|
||||
|
||||
const input = ref(null)
|
||||
|
||||
|
||||
// =============== METHODS ==============
|
||||
|
||||
const clearSearch = () => {
|
||||
if (!preserveSearch.value) search.value = ''
|
||||
}
|
||||
|
||||
const handleSearchInput = (e) => {
|
||||
search.value = e.target.value
|
||||
}
|
||||
|
||||
const handlePaste = (e) => {
|
||||
context.emit('paste', e)
|
||||
}
|
||||
|
||||
// ============== WATCHERS ==============
|
||||
|
||||
watch(search, (val) => {
|
||||
context.emit('search-change', val)
|
||||
})
|
||||
|
||||
return {
|
||||
search,
|
||||
input,
|
||||
clearSearch,
|
||||
handleSearchInput,
|
||||
handlePaste,
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
import { computed, toRefs, ref } from 'vue'
|
||||
|
||||
export default function useValue(props, context) {
|
||||
const { value, modelValue, mode, valueProp } = toRefs(props)
|
||||
|
||||
// ================ DATA ================
|
||||
|
||||
// internalValue
|
||||
const iv = ref(mode.value !== 'single' ? [] : {})
|
||||
|
||||
// ============== COMPUTED ==============
|
||||
|
||||
/* istanbul ignore next */
|
||||
// externalValue
|
||||
const ev = context.expose !== undefined ? modelValue : value
|
||||
|
||||
const plainValue = computed(() => {
|
||||
return mode.value === 'single' ? iv.value[valueProp.value] : iv.value.map(v => v[valueProp.value])
|
||||
})
|
||||
|
||||
const textValue = computed(() => {
|
||||
return mode.value !== 'single' ? iv.value.map(v => v[valueProp.value]).join(',') : iv.value[valueProp.value]
|
||||
})
|
||||
|
||||
return {
|
||||
iv,
|
||||
internalValue: iv,
|
||||
ev,
|
||||
externalValue: ev,
|
||||
textValue,
|
||||
plainValue,
|
||||
}
|
||||
}
|
||||
1
resources/scripts/components/base-select/index.d.ts
vendored
Normal file
1
resources/scripts/components/base-select/index.d.ts
vendored
Normal file
@ -0,0 +1 @@
|
||||
export * from './BaseMultiselect';
|
||||
@ -0,0 +1,7 @@
|
||||
export default function arraysEqual (array1, array2) {
|
||||
const array2Sorted = array2.slice().sort()
|
||||
|
||||
return array1.length === array2.length && array1.slice().sort().every(function(value, index) {
|
||||
return value === array2Sorted[index];
|
||||
})
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export default function isNullish (val) {
|
||||
return [null, undefined, false].indexOf(val) !== -1
|
||||
}
|
||||
@ -0,0 +1,3 @@
|
||||
export default function isObject (variable) {
|
||||
return Object.prototype.toString.call(variable) === '[object Object]'
|
||||
}
|
||||
@ -0,0 +1,5 @@
|
||||
export default function normalize (str, strict = true) {
|
||||
return strict
|
||||
? String(str).toLowerCase().trim()
|
||||
: String(str).normalize('NFD').replace(/\p{Diacritic}/gu, '').toLowerCase().trim()
|
||||
}
|
||||
Reference in New Issue
Block a user