init component

This commit is contained in:
Robin COuret
2026-02-16 17:28:37 +01:00
parent 460c7a25e0
commit e0e50af706
4557 changed files with 666911 additions and 8 deletions

View File

@@ -0,0 +1,230 @@
import '../sass/form.sass';
import '../../dropdown/dropdown.sass';
import './autocomplete.sass';
import { Eq } from 'fp-ts/lib/Eq';
import { FunctionN, Predicate } from 'fp-ts/lib/function';
import { PropType, VNode } from 'vue';
export interface AutocompleteItem<T> {
id: string;
isSelected: boolean;
isHovered: boolean;
text: string;
value: T;
index: number;
}
export declare const BAutocomplete: import("vue").DefineComponent<{
selectedItems: {
type: PropType<unknown[]>;
required: true;
};
items: {
type: PropType<unknown[]>;
default: import("fp-ts/lib/function").Lazy<never[]>;
};
itemFilter: {
type: PropType<FunctionN<[string], Predicate<unknown>>>;
required: false;
};
itemId: {
type: PropType<(item: unknown) => any>;
default: string;
};
itemText: {
type: PropType<(item: unknown) => any>;
default: string;
};
closeOnSelect: {
type: PropType<boolean>;
default: boolean;
};
clearOnSelect: {
type: PropType<boolean>;
default: boolean;
};
openOnFocus: {
type: PropType<boolean>;
default: boolean;
};
onSelected: {
type: PropType<FunctionN<[unknown], void>>;
required: false;
};
"onUpdate:selectedItems": {
type: PropType<FunctionN<[unknown[]], void>>;
default: import("fp-ts/lib/function").Lazy<FunctionN<[unknown[]], void>>;
};
modelValue: {
type: PropType<string>;
required: false;
};
'onUpdate:modelValue': {
type: PropType<FunctionN<[string], void>>;
default: import("fp-ts/lib/function").Lazy<FunctionN<[string], void>>;
};
themeMap: {
type: PropType<import("../../..").ThemeColorMap>;
required: boolean;
default: import("fp-ts/lib/function").Lazy<import("../../..").ThemeColorMap>;
};
isThemeable: {
type: PropType<boolean>;
required: boolean;
default: boolean;
};
eq: {
type: PropType<Eq<unknown>>;
default: import("fp-ts/lib/function").Lazy<Eq<unknown>>;
};
isFocused: {
type: PropType<boolean>;
default: boolean;
};
onFocus: {
type: PropType<(e?: Event | undefined) => void>;
required: false;
};
onBlur: {
type: PropType<(e?: Event | undefined) => void>;
required: false;
};
focusOnMount: {
type: PropType<boolean>;
default: boolean;
};
isDisabled: {
type: PropType<boolean>;
required: boolean;
default: boolean;
};
isReadonly: {
type: PropType<boolean>;
required: boolean;
default: boolean;
};
disableIfReadonly: {
type: PropType<boolean>;
required: boolean;
default: boolean;
};
useNativeValidation: {
type: PropType<boolean>;
default: boolean;
};
isValid: {
type: PropType<boolean>;
default: boolean;
};
'onUpdate:isValid': {
type: PropType<FunctionN<[boolean], void>>;
default: import("fp-ts/lib/function").Lazy<() => void>;
};
variant: {
type: PropType<import("../../..").ColorVariant>;
default: "";
};
type: {
type: PropType<string>;
};
autocomplete: {
type: PropType<string>;
};
placeholder: {
type: PropType<string>;
};
size: {
type: PropType<import("../../..").SizeVariant>;
default: import("../../..").SizeVariant;
};
isRequired: {
type: BooleanConstructor;
default: boolean;
};
isExpanded: {
type: PropType<boolean>;
default: boolean;
};
isLoading: {
type: PropType<boolean>;
default: boolean;
};
isRounded: {
type: PropType<boolean>;
default: boolean;
};
maxlength: {
type: PropType<string | number>;
};
icon: null;
usePasswordReveal: {
type: PropType<boolean>;
default: undefined;
};
}, () => VNode<import("vue").RendererNode, import("vue").RendererElement, {
[key: string]: any;
}>, unknown, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, Record<string, any>, string, import("vue").VNodeProps & import("vue").AllowedComponentProps & import("vue").ComponentCustomProps, Readonly<{
isThemeable: boolean;
themeMap: import("../../..").ThemeColorMap;
variant: import("../../..").ColorVariant;
size: import("../../..").SizeVariant;
isExpanded: boolean;
isRounded: boolean;
isLoading: boolean;
isFocused: boolean;
isDisabled: boolean;
focusOnMount: boolean;
"onUpdate:modelValue": FunctionN<[string], void>;
isReadonly: boolean;
disableIfReadonly: boolean;
useNativeValidation: boolean;
isValid: boolean;
"onUpdate:isValid": FunctionN<[boolean], void>;
isRequired: boolean;
usePasswordReveal: boolean;
selectedItems: unknown[];
"onUpdate:selectedItems": FunctionN<[unknown[]], void>;
items: unknown[];
itemId: (item: unknown) => any;
itemText: (item: unknown) => any;
closeOnSelect: boolean;
clearOnSelect: boolean;
openOnFocus: boolean;
eq: Eq<unknown>;
} & {
icon?: any;
type?: string | undefined;
onFocus?: ((e?: Event | undefined) => void) | undefined;
onBlur?: ((e?: Event | undefined) => void) | undefined;
modelValue?: string | undefined;
autocomplete?: string | undefined;
placeholder?: string | undefined;
maxlength?: string | number | undefined;
itemFilter?: FunctionN<[string], Predicate<unknown>> | undefined;
onSelected?: FunctionN<[unknown], void> | undefined;
}>, {
isThemeable: boolean;
themeMap: import("../../..").ThemeColorMap;
variant: import("../../..").ColorVariant;
size: import("../../..").SizeVariant;
isExpanded: boolean;
isRounded: boolean;
isLoading: boolean;
isFocused: boolean;
isDisabled: boolean;
focusOnMount: boolean;
"onUpdate:modelValue": FunctionN<[string], void>;
isReadonly: boolean;
disableIfReadonly: boolean;
useNativeValidation: boolean;
isValid: boolean;
"onUpdate:isValid": FunctionN<[boolean], void>;
isRequired: boolean;
usePasswordReveal: boolean;
"onUpdate:selectedItems": FunctionN<[unknown[]], void>;
items: unknown[];
itemId: (item: unknown) => any;
itemText: (item: unknown) => any;
closeOnSelect: boolean;
clearOnSelect: boolean;
openOnFocus: boolean;
eq: Eq<unknown>;
}>;

View File

@@ -0,0 +1,317 @@
import "../../../../src/components/form/sass/form.sass";
import "../../../../src/components/dropdown/dropdown.sass";
import "../../../../src/components/form/autocomplete/autocomplete.sass";
import { StaticUseInputProps } from '../../../composables/input/useInput';
import { getUseModelPropsDefinition } from '../../../composables/model/useModel';
import { useProxy } from '../../../composables/proxy';
import { getEqPropsDefinition } from '../../../composables/shared';
import { useThemePropsDefinition } from '../../../composables/theme';
import { constEmptyArray, extractProp, isFunction, isHTMLElement, isObject, isString, toggleListItem } from '../../../utils/helpers';
import { DropdownThemeMap } from '../../dropdown';
import BDropdown from '../../dropdown/BDropdown';
import { isArrowDownEvent, isArrowUpEvent, isEnterEvent, isEscEvent, isTabEvent } from '../../../utils/eventHelpers';
import { constant, constVoid } from 'fp-ts/lib/function';
import BDropdownDivider from '../../dropdown/BDropdownDivider';
import BDropdownItem from '../../dropdown/BDropdownItem';
import { head, isEmpty, lookup } from 'fp-ts/lib/Array';
import { alt, chain, fold, fromNullable, isSome, map, none, some, toUndefined } from 'fp-ts/lib/Option';
import { pipe } from 'fp-ts/lib/pipeable';
import { defineComponent, shallowRef, computed, onBeforeUpdate, nextTick, toRef, h } from 'vue';
import { BInput } from '../input';
function getActiveDescendentId(selectedItems, itemId) {
return pipe(selectedItems, head, chain(item => {
if (isString(item) && isFunction(itemId)) {
const id = extractProp(itemId, item);
return isString(id) ? some(id) : none;
}
if (isString(item)) {
return some(item);
}
if (isString(itemId) && isObject(item) && Object.hasOwnProperty.call(item, itemId)) {
const id = item[itemId];
return isString(id) ? some(id) : none;
}
const id = extractProp(itemId, item);
return isString(id) ? some(id) : none;
}), toUndefined);
}
function getAutocompleteItems(items, selectedItems, itemId, itemText, eq, hoveredItem) {
return items.map((item, index) => {
const id = extractProp(itemId, item);
const nid = isString(id) ? id : String(id);
const text = extractProp(itemText, item);
return {
id: nid,
isSelected: selectedItems.some(i => eq.equals(i, item)),
isHovered: isSome(hoveredItem) ? hoveredItem.value.id === nid : false,
text: isString(text) ? text : String(text),
value: item,
index
};
});
}
function getSetSelected(props, closeDropdown, inputModel, selectedItemsModel) {
const toggle = toggleListItem(props.eq);
return item => {
const text = extractProp(props.itemText, item.value);
inputModel.value = props.clearOnSelect ? '' : isString(text) ? text : String(text);
selectedItemsModel.value = toggle(item.value, selectedItemsModel.value || []);
if (props.closeOnSelect) {
closeDropdown();
}
};
}
function getSetHovered(hoveredItem, templateItems) {
return item => {
const newItem = fromNullable(item);
if (isSome(newItem)) {
hoveredItem.value = newItem;
pipe(newItem, map(item => item.index), chain(index => lookup(index, templateItems.value)), fold(constant(constVoid), li => () => li.focus && li.focus()))();
}
};
}
function getOnKeydown(autocompleteItems, hoveredItem, closeDropdown, setSelected, setHovered) {
function onArrowPress(isUp) {
pipe(hoveredItem.value, map(item => item.index), alt(() => some(0)), chain(index => lookup(isUp ? Math.max(index - 1, 0) : Math.min(index + 1, autocompleteItems.value.length - 1), autocompleteItems.value)), fold(constant(constVoid), newItem => () => setHovered(newItem)))();
}
return function onKeydown(event) {
if (isEnterEvent(event)) {
event.preventDefault();
if (isSome(hoveredItem.value)) {
setSelected(hoveredItem.value.value);
}
} else if (isTabEvent(event)) {
event.preventDefault();
if (isSome(hoveredItem.value)) {
setSelected(hoveredItem.value.value);
} else {
nextTick(closeDropdown);
}
} else if (isArrowUpEvent(event)) {
event.preventDefault();
onArrowPress(true);
} else if (isArrowDownEvent(event)) {
event.preventDefault();
onArrowPress(false);
} else if (isEscEvent(event)) {
event.preventDefault();
nextTick(closeDropdown);
}
};
}
function getGenerateItem(itemsRef, length, onKeydown, setSelected, setHovered, slots) {
return function generateItem(item, index) {
return h(BDropdownItem, {
key: item.id,
ref: el => {
if (isHTMLElement(el)) {
itemsRef.value[index] = el;
}
},
id: item.id,
isActive: item.isSelected,
tabindex: item.isSelected ? -1 : 0,
'aria-selected': item.isSelected,
'aria-label': `Option ${index + 1} of ${length.value}`,
class: {
'is-hovered': item.isHovered
},
onClick: () => setSelected(item),
onMouseenter: () => setHovered(item),
onKeydown
}, () => slots.default ? slots.default({
option: item,
index
}) : item.text);
};
}
function generateHeaderItem(slots) {
return h('li', {
class: 'dropdown-item',
tabindex: -1
}, slots.header && slots.header());
}
function generateFooterItem(slots) {
return h('li', {
class: 'dropdown-item',
tabindex: -1
}, slots.footer && slots.footer());
}
function generateLoadingItem(slots) {
return h('li', {
tabindex: -1
}, [h(BDropdownItem, {
tag: 'div'
}, () => slots.loading ? slots.loading() : 'Loading results...')]);
}
function generateEmptyItem(modelValue, slots) {
return h(BDropdownItem, {
class: 'is-disabled'
}, () => slots.empty ? slots.empty({
searchValue: modelValue
}) : modelValue ? `No results` : `No results for ${modelValue}`);
}
function defineAutocomplete() {
return defineComponent({
name: 'b-autocomplete',
props: { ...StaticUseInputProps,
...getEqPropsDefinition(),
...useThemePropsDefinition(DropdownThemeMap),
...getUseModelPropsDefinition(),
...getUseModelPropsDefinition('selectedItems', 'onUpdate:selectedItems'),
selectedItems: {
type: Array,
required: true
},
items: {
type: Array,
default: constEmptyArray
},
itemFilter: {
type: Function,
required: false
},
itemId: {
type: [String, Function],
default: 'id'
},
itemText: {
type: [String, Function],
default: 'text'
},
closeOnSelect: {
type: Boolean,
default: true
},
clearOnSelect: {
type: Boolean,
default: true
},
openOnFocus: {
type: Boolean,
default: true
},
onSelected: {
type: Function,
required: false
}
},
setup(props, {
slots
}) {
const {
value: searchValue
} = useProxy(computed(() => props.modelValue ?? ''), toRef(props, 'onUpdate:modelValue'));
const {
value: selectedItems
} = useProxy(toRef(props, 'selectedItems'), toRef(props, 'onUpdate:selectedItems'));
const itemsRef = shallowRef([]);
const filteredItems = computed(() => {
if (props.itemFilter) {
return props.items.filter(props.itemFilter(searchValue.value));
} else {
const sv = searchValue.value.toLowerCase();
const extract = props.itemText;
return props.items.filter(i => extractProp(extract, i).toLowerCase().includes(sv));
}
});
onBeforeUpdate(() => {
itemsRef.value = [];
});
const dropdown = shallowRef(null);
function close() {
dropdown.value && dropdown.value.toggle.setOff();
}
const hoveredItem = shallowRef(none);
const activeDescendentId = computed(() => getActiveDescendentId(props.selectedItems, props.itemId));
const autocompleteItems = computed(() => getAutocompleteItems(filteredItems.value, selectedItems.value, props.itemId, props.itemText, props.eq, hoveredItem.value));
const numberOfItems = computed(() => autocompleteItems.value.length);
const setSelected = getSetSelected(props, close, searchValue, selectedItems);
const setHovered = getSetHovered(hoveredItem, itemsRef);
const onKeydown = getOnKeydown(autocompleteItems, hoveredItem, close, setSelected, setHovered);
const generateItem = getGenerateItem(itemsRef, numberOfItems, onKeydown, setSelected, setHovered, slots);
return () => {
return h(BDropdown, {
ref: dropdown,
isMobileModal: false,
class: ['b-autocomplete', {
'is-expanded': props.isExpanded
}]
}, {
trigger: () => {
return h(BInput, {
modelValue: searchValue.value,
type: 'text',
size: props.size,
isLoading: props.isLoading,
isRounded: props.isRounded,
icon: props.icon,
maxlength: props.maxlength,
autocomplete: 'off',
placeholder: props.placeholder,
role: 'searchbox',
'aria-activedescendant': activeDescendentId.value,
'onUpdate:modelValue': val => {
searchValue.value = val;
},
// onFocus: () => {
// nextTick().then(() => {
// if (props.openOnFocus && toggle.isOff.value) {
// toggle.setOn();
// }
// });
// },
onBlur: props.onBlur,
onKeydown
});
},
default: () => {
let nodes;
if (props.isLoading) {
nodes = [generateLoadingItem(slots)];
} else {
nodes = isEmpty(autocompleteItems.value) ? [generateEmptyItem(searchValue.value, slots)] : autocompleteItems.value.map(generateItem);
if (slots.header) {
nodes.unshift(generateHeaderItem(slots), h(BDropdownDivider));
}
if (slots.footer) {
nodes.push(h(BDropdownDivider), generateFooterItem(slots));
}
}
return nodes;
}
});
};
}
});
}
export const BAutocomplete = defineAutocomplete();
//# sourceMappingURL=BAutocomplete.js.map

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
export * from './BAutocomplete';

View File

@@ -0,0 +1,2 @@
export * from './BAutocomplete';
//# sourceMappingURL=index.js.map

View File

@@ -0,0 +1 @@
{"version":3,"sources":["../../../../src/components/form/autocomplete/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAd","sourcesContent":["export * from './BAutocomplete';\n"],"sourceRoot":"","file":"index.js"}