mirror of
https://github.com/crater-invoice/crater.git
synced 2025-10-27 11:41:09 -04:00
* add dark mode for label * fix dark issue in customer view page * fix remaining label for dark mode --------- Co-authored-by: yashkanakiya <yashkanakiya281297@gmail.com>
650 lines
18 KiB
Vue
Executable File
650 lines
18 KiB
Vue
Executable File
<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 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-sm leading-snug outline-none max-h-10',
|
|
containerDisabled:
|
|
'bg-gray-200 !text-gray-800 dark:!text-gray-800 !dark:text-gray-500 !cursor-not-allowed dark:opacity-50',
|
|
containerOpen: '',
|
|
containerOpenTop: '',
|
|
containerActive: 'ring-1 ring-primary-400 border-primary-400',
|
|
containerInvalid:
|
|
'border-red-500 ring-red-500 focus:ring-red-500 focus:border-red-500 dark:border-red-500 dark:ring-red-500 dark:focus:ring-red-500 dark:focus:border-red-500',
|
|
containerInvalidActive:
|
|
'ring-1 border-red-500 ring-red-500 dark:ring-1 dark:border-red-500 dark:ring-red-500',
|
|
singleLabel:
|
|
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 dark:text-white',
|
|
multipleLabel:
|
|
'flex items-center h-full absolute left-0 top-0 pointer-events-none bg-transparent leading-snug pl-3.5 dark:text-white',
|
|
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 border-transparent focus:border-transparent focus:ring-0 dark:bg-gray-700 dark:text-white',
|
|
tags: 'grow 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 grow shrink h-full',
|
|
tagsSearch:
|
|
'absolute inset-0 border-0 focus:outline-none !shadow-none !focus:shadow-none appearance-none p-0 sm:text-sm font-sans box-border w-full dark:bg-gray-700',
|
|
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 sm:text-sm dark:text-gray-500',
|
|
caret:
|
|
'bg-multiselect-caret-black dark:bg-multiselect-caret-white bg-center bg-no-repeat w-5 h-5 py-px box-content z-5 relative mr-1 opacity-40 shrink-0 grow-0 transition-transform dark:text-white',
|
|
caretOpen: 'rotate-180 pointer-events-auto',
|
|
clear:
|
|
'pr-3.5 relative z-10 opacity-40 transition duration-300 shrink-0 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 shrink-0 grow-0',
|
|
dropdown:
|
|
'max-h-60 shadow-lg absolute -left-px -right-px -bottom-1 translate-y-full border border-gray-300 mt-1 overflow-y-auto z-50 bg-white dark:border-gray-600 flex flex-col rounded-md dark:bg-gray-800 dark:shadow-glass',
|
|
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 dark:bg-gray-700 dark:text-gray-400 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 dark:text-gray-200',
|
|
optionPointed:
|
|
'text-gray-800 bg-gray-100 dark:text-white dark:bg-gray-700/30',
|
|
optionSelected: 'text-white bg-primary-500',
|
|
optionDisabled: 'text-gray-300 cursor-not-allowed dark:text-gray-400',
|
|
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 dark:bg-gray-700 dark:text-gray-200',
|
|
noResults:
|
|
'py-2 px-3 text-gray-600 bg-white dark:bg-gray-700 dark:text-gray-200',
|
|
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>
|