vue-composable-ui 0.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +1 -1
- package/README.md +5 -1
- package/dist/combobox.vue_vue_type_script_setup_true_lang-BhVMcx67.js +140 -0
- package/dist/comboboxHiddenInput.vue_vue_type_script_setup_true_lang-Ba-UPd-M.js +29 -0
- package/dist/comboboxInput.vue_vue_type_script_setup_true_lang-DzRfJ5ke.js +33 -0
- package/dist/comboboxOption.vue_vue_type_script_setup_true_lang-OyJxB4Jk.js +42 -0
- package/dist/comboboxOptions.vue_vue_type_script_setup_true_lang-Doy6MfEQ.js +20 -0
- package/dist/composable-ui.js +49 -0
- package/dist/composable-ui10.js +4 -0
- package/dist/composable-ui11.js +4 -0
- package/dist/composable-ui12.js +4 -0
- package/dist/composable-ui13.js +4 -0
- package/dist/composable-ui14.js +4 -0
- package/dist/composable-ui15.js +4 -0
- package/dist/composable-ui16.js +4 -0
- package/dist/composable-ui17.js +31 -0
- package/dist/composable-ui18.js +4 -0
- package/dist/composable-ui19.js +4 -0
- package/dist/composable-ui2.js +55 -0
- package/dist/composable-ui20.js +4 -0
- package/dist/composable-ui21.js +100 -0
- package/dist/composable-ui22.js +144 -0
- package/dist/composable-ui23.js +7 -0
- package/dist/composable-ui24.js +7 -0
- package/dist/composable-ui3.js +4 -0
- package/dist/composable-ui4.js +4 -0
- package/dist/composable-ui5.js +4 -0
- package/dist/composable-ui6.js +4 -0
- package/dist/composable-ui7.js +4 -0
- package/dist/composable-ui8.js +4 -0
- package/dist/composable-ui9.js +4 -0
- package/dist/listbox.vue_vue_type_script_setup_true_lang-Bj1ZXiBX.js +69 -0
- package/dist/listboxOption.vue_vue_type_script_setup_true_lang-B_yRptJm.js +37 -0
- package/dist/menu.vue_vue_type_script_setup_true_lang-Ci7q2rOg.js +82 -0
- package/dist/menuItem.vue_vue_type_script_setup_true_lang-mXtMa4gO.js +63 -0
- package/dist/menuItems.vue_vue_type_script_setup_true_lang-p9vftREy.js +49 -0
- package/dist/popover.vue_vue_type_script_setup_true_lang-DhKF7_nY.js +50 -0
- package/dist/popoverDialog.vue_vue_type_script_setup_true_lang-CDXUO7wZ.js +33 -0
- package/dist/tab.vue_vue_type_script_setup_true_lang-Y5OFGqMB.js +29 -0
- package/dist/tabContainer.vue_vue_type_script_setup_true_lang-fPpLno2Z.js +40 -0
- package/dist/tabList.vue_vue_type_script_setup_true_lang-CZyqGCuj.js +48 -0
- package/dist/tabPanel.vue_vue_type_script_setup_true_lang-CGhP6SHR.js +31 -0
- package/dist/tooltip.vue_vue_type_script_setup_true_lang-DVKYJyfL.js +85 -0
- package/dist/types/components/combobox/combobox.vue.d.ts +45 -0
- package/dist/types/components/combobox/comboboxHiddenInput.vue.d.ts +6 -0
- package/dist/types/components/combobox/comboboxInput.vue.d.ts +9 -0
- package/dist/types/components/combobox/comboboxOption.vue.d.ts +20 -0
- package/dist/types/components/combobox/comboboxOptions.vue.d.ts +12 -0
- package/dist/types/components/injectionKeys.d.ts +43 -0
- package/dist/types/components/listbox/listbox.vue.d.ts +26 -0
- package/dist/types/components/listbox/listboxOption.vue.d.ts +20 -0
- package/dist/types/components/menu/menu.vue.d.ts +42 -0
- package/dist/types/components/menu/menuItem.vue.d.ts +24 -0
- package/dist/types/components/menu/menuItems.vue.d.ts +12 -0
- package/dist/types/components/popover/popover.vue.d.ts +40 -0
- package/dist/types/components/popover/popoverDialog.vue.d.ts +12 -0
- package/dist/types/components/tabs/tab.vue.d.ts +17 -0
- package/dist/types/components/tabs/tabContainer.vue.d.ts +19 -0
- package/dist/types/components/tabs/tabList.vue.d.ts +17 -0
- package/dist/types/components/tabs/tabPanel.vue.d.ts +15 -0
- package/dist/types/components/tooltip.vue.d.ts +34 -0
- package/dist/types/composables/formControl.d.ts +48 -0
- package/dist/types/composables/list.d.ts +107 -0
- package/dist/types/composables/listOption.d.ts +34 -0
- package/dist/types/composables/popover.d.ts +68 -0
- package/dist/types/main.d.ts +22 -0
- package/dist/types/utils/element.d.ts +3 -0
- package/package.json +28 -12
- package/.github/workflows/deploy.yml +0 -21
- package/.github/workflows/docs.yml +0 -62
- package/.vscode/extensions.json +0 -7
- package/docs/.vitepress/config.mts +0 -40
- package/docs/.vitepress/theme/index.js +0 -4
- package/docs/.vitepress/theme/style.scss +0 -155
- package/docs/components/combobox.md +0 -174
- package/docs/components/demos/combobox.vue +0 -142
- package/docs/components/demos/combobox_autocomplete.vue +0 -75
- package/docs/components/demos/combobox_tags.vue +0 -127
- package/docs/components/demos/input.vue +0 -35
- package/docs/components/demos/listbox.vue +0 -64
- package/docs/components/demos/menu.vue +0 -93
- package/docs/components/demos/popover.vue +0 -16
- package/docs/components/demos/tabs.vue +0 -59
- package/docs/components/demos/tooltip.vue +0 -27
- package/docs/components/listbox.md +0 -9
- package/docs/components/menu.md +0 -121
- package/docs/components/popover.md +0 -82
- package/docs/components/tabs.md +0 -9
- package/docs/components/tooltip.md +0 -78
- package/docs/composables/form-control.md +0 -130
- package/docs/composables/list-option.md +0 -47
- package/docs/composables/list.md +0 -192
- package/docs/composables/popover.md +0 -90
- package/docs/index.md +0 -24
- package/docs/intro.md +0 -0
- package/src/components/combobox/combobox.vue +0 -229
- package/src/components/combobox/comboboxFormInput.vue +0 -33
- package/src/components/combobox/comboboxOption.vue +0 -52
- package/src/components/combobox/comboboxOptions.vue +0 -17
- package/src/components/injectionKeys.ts +0 -50
- package/src/components/listbox/listbox.vue +0 -91
- package/src/components/listbox/listboxOption.vue +0 -37
- package/src/components/menu/menu.vue +0 -100
- package/src/components/menu/menuItem.vue +0 -86
- package/src/components/menu/menuItems.vue +0 -51
- package/src/components/popover/popover.vue +0 -68
- package/src/components/popover/popoverDialog.vue +0 -35
- package/src/components/tabs/tab.vue +0 -35
- package/src/components/tabs/tabContainer.vue +0 -50
- package/src/components/tabs/tabList.vue +0 -52
- package/src/components/tabs/tabPanel.vue +0 -36
- package/src/components/tooltip.vue +0 -115
- package/src/composables/formControl.ts +0 -144
- package/src/composables/list.ts +0 -326
- package/src/composables/listOption.ts +0 -80
- package/src/composables/popover.ts +0 -306
- package/src/main.ts +0 -65
- package/src/utils/element.ts +0 -19
- package/src/utils/id.ts +0 -5
- package/src/vite-env.d.ts +0 -1
- package/tsconfig.json +0 -27
- package/tsconfig.node.json +0 -10
- package/vite.config.ts +0 -28
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
<template>
|
|
2
|
-
<slot :attrs="activatorAttrs" />
|
|
3
|
-
|
|
4
|
-
<Teleport to="body">
|
|
5
|
-
<div
|
|
6
|
-
ref="popupEl"
|
|
7
|
-
v-if="isOpen"
|
|
8
|
-
:id="tooltipId"
|
|
9
|
-
v-bind="$attrs"
|
|
10
|
-
role="tooltip"
|
|
11
|
-
>
|
|
12
|
-
<slot name="content">{{ text }}</slot>
|
|
13
|
-
</div>
|
|
14
|
-
</Teleport>
|
|
15
|
-
</template>
|
|
16
|
-
|
|
17
|
-
<script setup lang="ts">
|
|
18
|
-
import { onBeforeUnmount, onMounted, ref, useSlots } from "vue";
|
|
19
|
-
import { useSequentialId } from "../utils/id";
|
|
20
|
-
import { usePopover } from "../composables/popover";
|
|
21
|
-
|
|
22
|
-
defineOptions({
|
|
23
|
-
inheritAttrs: false,
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
const props = withDefaults(
|
|
27
|
-
defineProps<{
|
|
28
|
-
id?: string;
|
|
29
|
-
text?: string;
|
|
30
|
-
delay?: string | number;
|
|
31
|
-
direction?: "up" | "down" | "left" | "right";
|
|
32
|
-
}>(),
|
|
33
|
-
{
|
|
34
|
-
direction: "up",
|
|
35
|
-
}
|
|
36
|
-
);
|
|
37
|
-
const slots = useSlots();
|
|
38
|
-
|
|
39
|
-
const popupEl = ref();
|
|
40
|
-
let timer: NodeJS.Timeout;
|
|
41
|
-
|
|
42
|
-
const tooltipId = props.id ?? useSequentialId("tooltip");
|
|
43
|
-
|
|
44
|
-
const { isOpen } = usePopover(`[aria-describedby=${tooltipId}]`, popupEl, {
|
|
45
|
-
direction: props.direction,
|
|
46
|
-
align: "center",
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
function open() {
|
|
50
|
-
timer = setTimeout(
|
|
51
|
-
() => {
|
|
52
|
-
isOpen.value = true;
|
|
53
|
-
},
|
|
54
|
-
typeof props.delay === "string" ? parseInt(props.delay) : props.delay
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function close() {
|
|
59
|
-
isOpen.value = false;
|
|
60
|
-
clearTimeout(timer);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
function onKeyDown(evt: KeyboardEvent) {
|
|
64
|
-
if (evt.key === "Escape") {
|
|
65
|
-
close();
|
|
66
|
-
}
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
const activatorAttrs = {
|
|
70
|
-
"aria-describedby": tooltipId,
|
|
71
|
-
onmouseenter: open,
|
|
72
|
-
onmouseleave: close,
|
|
73
|
-
onfocus: open,
|
|
74
|
-
onblur: close,
|
|
75
|
-
onkeydown: onKeyDown,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const controller = new AbortController();
|
|
79
|
-
|
|
80
|
-
onMounted(() => {
|
|
81
|
-
if (slots.default) return;
|
|
82
|
-
const activator = document.querySelector(
|
|
83
|
-
`[aria-describedby=${tooltipId}]`
|
|
84
|
-
) as HTMLElement;
|
|
85
|
-
if (!activator) {
|
|
86
|
-
throw new Error(`Tooltip activator not found: ${tooltipId}`);
|
|
87
|
-
}
|
|
88
|
-
activator.addEventListener("mouseenter", open, {
|
|
89
|
-
signal: controller.signal,
|
|
90
|
-
passive: true,
|
|
91
|
-
});
|
|
92
|
-
activator.addEventListener("mouseleave", close, {
|
|
93
|
-
signal: controller.signal,
|
|
94
|
-
passive: true,
|
|
95
|
-
});
|
|
96
|
-
activator.addEventListener("focus", open, {
|
|
97
|
-
signal: controller.signal,
|
|
98
|
-
passive: true,
|
|
99
|
-
});
|
|
100
|
-
activator.addEventListener("blur", close, {
|
|
101
|
-
signal: controller.signal,
|
|
102
|
-
passive: true,
|
|
103
|
-
});
|
|
104
|
-
activator.addEventListener("keydown", onKeyDown, {
|
|
105
|
-
signal: controller.signal,
|
|
106
|
-
passive: true,
|
|
107
|
-
});
|
|
108
|
-
});
|
|
109
|
-
|
|
110
|
-
onBeforeUnmount(() => {
|
|
111
|
-
controller.abort();
|
|
112
|
-
});
|
|
113
|
-
</script>
|
|
114
|
-
|
|
115
|
-
<style scoped></style>
|
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
import type { MaybeRef, Ref } from "vue";
|
|
2
|
-
import { ref, unref, onBeforeUnmount, onMounted, readonly } from "vue";
|
|
3
|
-
|
|
4
|
-
import type { TemplateRefOrSelector } from "../utils/element";
|
|
5
|
-
import { useHTMLElement } from "../utils/element";
|
|
6
|
-
|
|
7
|
-
type FormElement = HTMLInputElement | HTMLTextAreaElement | HTMLSelectElement;
|
|
8
|
-
|
|
9
|
-
/**
|
|
10
|
-
* A validating function must return `true` or a string describing the error
|
|
11
|
-
*/
|
|
12
|
-
export type FormValidator = (value: string) => boolean | string;
|
|
13
|
-
|
|
14
|
-
export interface useFormControlReturn {
|
|
15
|
-
/**
|
|
16
|
-
* Focus the form control
|
|
17
|
-
*/
|
|
18
|
-
focus: () => void;
|
|
19
|
-
/**
|
|
20
|
-
* Validate the form control
|
|
21
|
-
*
|
|
22
|
-
* @returns Validation result
|
|
23
|
-
*/
|
|
24
|
-
validate: () => boolean;
|
|
25
|
-
/**
|
|
26
|
-
* Whether the form control is valid. A shorthand for `validity.valid`, but can be manually overriden.
|
|
27
|
-
*/
|
|
28
|
-
isValid: Ref<boolean>;
|
|
29
|
-
/**
|
|
30
|
-
* The validity state of the form control
|
|
31
|
-
*
|
|
32
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/ValidityState}
|
|
33
|
-
*/
|
|
34
|
-
validity: Readonly<Ref<ValidityState | undefined>>;
|
|
35
|
-
/**
|
|
36
|
-
* A string representing a localized message that describes the validation constraints that the control does not satisfy (if any)
|
|
37
|
-
*/
|
|
38
|
-
validationMessage: Readonly<Ref<string>>;
|
|
39
|
-
/**
|
|
40
|
-
* Whether the value has been changed since the form control was mounted
|
|
41
|
-
*/
|
|
42
|
-
isChanged: Ref<boolean>;
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Utilizes Constraint Validation API to enable custom validation constrains for a form control
|
|
47
|
-
*
|
|
48
|
-
* @param el A template ref or a CSS selector
|
|
49
|
-
* @param validators An array of validating functions
|
|
50
|
-
* @param showValidationError Whether to show the default browser validation error (true by default)
|
|
51
|
-
*
|
|
52
|
-
* @see {@link https://developer.mozilla.org/en-US/docs/Web/HTML/Constraint_validation}
|
|
53
|
-
*/
|
|
54
|
-
export function useFormControl(
|
|
55
|
-
el: TemplateRefOrSelector<FormElement>,
|
|
56
|
-
validators: MaybeRef<FormValidator[]> = [],
|
|
57
|
-
showValidationError: MaybeRef<boolean> = true
|
|
58
|
-
): useFormControlReturn {
|
|
59
|
-
const isChanged = ref(false);
|
|
60
|
-
const isValid = ref(true);
|
|
61
|
-
const validity = ref<ValidityState>();
|
|
62
|
-
const validationMessage = ref("");
|
|
63
|
-
|
|
64
|
-
let formElement: FormElement | undefined;
|
|
65
|
-
let initValue: string;
|
|
66
|
-
|
|
67
|
-
function focus() {
|
|
68
|
-
formElement?.focus();
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function updateValidationState() {
|
|
72
|
-
isValid.value = formElement!.validity.valid;
|
|
73
|
-
validity.value = formElement!.validity;
|
|
74
|
-
validationMessage.value = formElement!.validationMessage;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
function validate() {
|
|
78
|
-
if (!formElement) throw new Error("Form element is undefined");
|
|
79
|
-
formElement.setCustomValidity("");
|
|
80
|
-
|
|
81
|
-
if (formElement.validity.valid) {
|
|
82
|
-
const value = formElement.value;
|
|
83
|
-
for (const validator of unref(validators)) {
|
|
84
|
-
const check = validator(value);
|
|
85
|
-
if (check !== true) {
|
|
86
|
-
formElement.setCustomValidity(check as string);
|
|
87
|
-
break;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
updateValidationState();
|
|
93
|
-
return isValid.value;
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
function onInvalid(event: Event) {
|
|
97
|
-
updateValidationState();
|
|
98
|
-
if (!unref(showValidationError)) {
|
|
99
|
-
event.preventDefault();
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
function onChange() {
|
|
104
|
-
validate();
|
|
105
|
-
isChanged.value = initValue !== formElement!.value;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
let abortController = new AbortController();
|
|
109
|
-
|
|
110
|
-
onMounted(() => {
|
|
111
|
-
formElement = useHTMLElement(el).value;
|
|
112
|
-
if (!formElement) throw new Error("Form element is undefined");
|
|
113
|
-
|
|
114
|
-
initValue = formElement.value;
|
|
115
|
-
|
|
116
|
-
formElement.addEventListener("change", onChange, {
|
|
117
|
-
signal: abortController.signal,
|
|
118
|
-
passive: true,
|
|
119
|
-
});
|
|
120
|
-
formElement.addEventListener("invalid", onInvalid, {
|
|
121
|
-
signal: abortController.signal,
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// Fix autofocus?
|
|
125
|
-
if (formElement?.autofocus) {
|
|
126
|
-
focus();
|
|
127
|
-
}
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
onBeforeUnmount(() => {
|
|
131
|
-
abortController.abort();
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
return {
|
|
135
|
-
focus,
|
|
136
|
-
validate,
|
|
137
|
-
|
|
138
|
-
isValid,
|
|
139
|
-
validity: readonly(validity),
|
|
140
|
-
validationMessage: readonly(validationMessage),
|
|
141
|
-
|
|
142
|
-
isChanged,
|
|
143
|
-
};
|
|
144
|
-
}
|
package/src/composables/list.ts
DELETED
|
@@ -1,326 +0,0 @@
|
|
|
1
|
-
import type { ComputedRef, MaybeRef, Ref, ShallowRef, InjectionKey } from "vue";
|
|
2
|
-
import {
|
|
3
|
-
ref,
|
|
4
|
-
shallowRef,
|
|
5
|
-
unref,
|
|
6
|
-
provide,
|
|
7
|
-
computed,
|
|
8
|
-
readonly,
|
|
9
|
-
shallowReadonly,
|
|
10
|
-
triggerRef,
|
|
11
|
-
} from "vue";
|
|
12
|
-
import { useHTMLElement, TemplateRefOrSelector } from "../utils/element";
|
|
13
|
-
|
|
14
|
-
export type ItemValue = unknown;
|
|
15
|
-
export interface Item {
|
|
16
|
-
id: number;
|
|
17
|
-
element: Ref<HTMLElement | undefined>;
|
|
18
|
-
value: ItemValue;
|
|
19
|
-
disabled: MaybeRef<boolean>;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ActivationOptions {
|
|
23
|
-
/**
|
|
24
|
-
* Focus the corresponding HTML element
|
|
25
|
-
*
|
|
26
|
-
* @defaultValue `false`
|
|
27
|
-
*/
|
|
28
|
-
focus?: boolean;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
interface NavigationOptions {
|
|
32
|
-
/**
|
|
33
|
-
* Infinitely cycle throught the items
|
|
34
|
-
*
|
|
35
|
-
* @defaultValue `false`
|
|
36
|
-
*/
|
|
37
|
-
loop?: boolean;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
export interface UseListReturn {
|
|
41
|
-
/**
|
|
42
|
-
* Array of items
|
|
43
|
-
*/
|
|
44
|
-
items: Readonly<ShallowRef<Item[]>>;
|
|
45
|
-
/**
|
|
46
|
-
* Multiselection flag
|
|
47
|
-
*/
|
|
48
|
-
isMultiselectable: Readonly<Ref<boolean>>;
|
|
49
|
-
/**
|
|
50
|
-
* Currently active item
|
|
51
|
-
*/
|
|
52
|
-
activeItem: ComputedRef<Item | undefined>;
|
|
53
|
-
/**
|
|
54
|
-
* Select the currently active item
|
|
55
|
-
*/
|
|
56
|
-
selectActiveItem: () => void;
|
|
57
|
-
/**
|
|
58
|
-
* Activate the currently selected item
|
|
59
|
-
*/
|
|
60
|
-
activateSelectedItem: (options?: ActivationOptions) => void;
|
|
61
|
-
/**
|
|
62
|
-
* Activate the first item
|
|
63
|
-
*/
|
|
64
|
-
activateFirstItem: (options?: ActivationOptions) => void;
|
|
65
|
-
/**
|
|
66
|
-
* Activate the last item
|
|
67
|
-
*/
|
|
68
|
-
activateLastItem: (options?: ActivationOptions) => void;
|
|
69
|
-
/**
|
|
70
|
-
* Activate next item
|
|
71
|
-
*/
|
|
72
|
-
activateNextItem: (options?: ActivationOptions & NavigationOptions) => void;
|
|
73
|
-
/**
|
|
74
|
-
* Activate previous item
|
|
75
|
-
*/
|
|
76
|
-
activatePrevItem: (options?: ActivationOptions & NavigationOptions) => void;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
interface UseListInjectionKey {
|
|
80
|
-
/**
|
|
81
|
-
* The list value
|
|
82
|
-
*/
|
|
83
|
-
value: Readonly<Ref<unknown>>;
|
|
84
|
-
/**
|
|
85
|
-
* Select an item by ID
|
|
86
|
-
*/
|
|
87
|
-
selectItem: (itemId: number) => void;
|
|
88
|
-
/**
|
|
89
|
-
* Activate an item by ID
|
|
90
|
-
*/
|
|
91
|
-
activateItem: (itemId: number, options?: ActivationOptions) => void;
|
|
92
|
-
/**
|
|
93
|
-
* Check if an item is selected
|
|
94
|
-
*/
|
|
95
|
-
isSelected: (itemId: number) => boolean;
|
|
96
|
-
/**
|
|
97
|
-
* Check if an item is active
|
|
98
|
-
*/
|
|
99
|
-
isActive: (itemId: number) => boolean;
|
|
100
|
-
/**
|
|
101
|
-
* Add an item to the list
|
|
102
|
-
*
|
|
103
|
-
* @returns New item ID
|
|
104
|
-
*/
|
|
105
|
-
addItem: (
|
|
106
|
-
itemEl: TemplateRefOrSelector,
|
|
107
|
-
itemVal: ItemValue,
|
|
108
|
-
disabled?: MaybeRef<boolean>
|
|
109
|
-
) => number;
|
|
110
|
-
/**
|
|
111
|
-
* Remove an item from the list by ID
|
|
112
|
-
*/
|
|
113
|
-
removeItem: (itemId: number) => void;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export const injectList: InjectionKey<UseListInjectionKey> = Symbol();
|
|
117
|
-
|
|
118
|
-
function isValuesEqual(v1: ItemValue, v2: ItemValue) {
|
|
119
|
-
const [_v1, _v2] = [unref(v1), unref(v2)];
|
|
120
|
-
if (typeof _v1 === "object") {
|
|
121
|
-
if (typeof _v2 === "object") {
|
|
122
|
-
return JSON.stringify(_v1) === JSON.stringify(_v2);
|
|
123
|
-
}
|
|
124
|
-
return false;
|
|
125
|
-
}
|
|
126
|
-
return _v1 === _v2;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Creates a reactive list of HTML elements with assigned values.
|
|
131
|
-
*
|
|
132
|
-
* @param value A ref to store the list value. Set to an array to enable multiselection.
|
|
133
|
-
*/
|
|
134
|
-
export function useList(value: Ref<unknown>): UseListReturn {
|
|
135
|
-
let lastId = 0;
|
|
136
|
-
|
|
137
|
-
const items: ShallowRef<Item[]> = shallowRef([]);
|
|
138
|
-
const activeItemId: Ref<number | undefined> = ref();
|
|
139
|
-
|
|
140
|
-
const isMultiselectable = computed(() => Array.isArray(value.value));
|
|
141
|
-
|
|
142
|
-
function getItem(itemId: number) {
|
|
143
|
-
const item = items.value.find((_item) => {
|
|
144
|
-
return _item.id === itemId;
|
|
145
|
-
});
|
|
146
|
-
if (!item) throw new Error(`Item not found: (${itemId})`);
|
|
147
|
-
return item;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
function focusItem(item: Item) {
|
|
151
|
-
const el = unref(item.element);
|
|
152
|
-
if (!el) return;
|
|
153
|
-
el.focus();
|
|
154
|
-
return el;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function activateItem(
|
|
158
|
-
item?: Item,
|
|
159
|
-
{ focus = false }: ActivationOptions = {}
|
|
160
|
-
) {
|
|
161
|
-
activeItemId.value = item?.id ?? undefined;
|
|
162
|
-
if (item !== undefined && focus) focusItem(item);
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function activateItemById(
|
|
166
|
-
itemId: number,
|
|
167
|
-
{ focus = false }: ActivationOptions = {}
|
|
168
|
-
) {
|
|
169
|
-
const item = getItem(itemId);
|
|
170
|
-
return activateItem(item, { focus });
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function isActive(itemId: number) {
|
|
174
|
-
return activeItemId.value === itemId;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function addItem(
|
|
178
|
-
itemEl: TemplateRefOrSelector,
|
|
179
|
-
itemVal: ItemValue,
|
|
180
|
-
disabled: MaybeRef<boolean> = false
|
|
181
|
-
) {
|
|
182
|
-
const el = useHTMLElement(itemEl);
|
|
183
|
-
const id = lastId++;
|
|
184
|
-
items.value.push({
|
|
185
|
-
id,
|
|
186
|
-
value: itemVal,
|
|
187
|
-
element: el,
|
|
188
|
-
disabled,
|
|
189
|
-
});
|
|
190
|
-
triggerRef(items);
|
|
191
|
-
return id;
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
function removeItem(itemId: number) {
|
|
195
|
-
const item = getItem(itemId);
|
|
196
|
-
const idx = items.value.indexOf(item);
|
|
197
|
-
items.value.splice(idx, 1);
|
|
198
|
-
triggerRef(items);
|
|
199
|
-
if (isActive(itemId)) {
|
|
200
|
-
activateItem();
|
|
201
|
-
}
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function selectItem(itemId: number) {
|
|
205
|
-
const item = getItem(itemId);
|
|
206
|
-
if (unref(item.disabled)) return;
|
|
207
|
-
const v = unref(item.value);
|
|
208
|
-
if (v === undefined) return;
|
|
209
|
-
if (unref(isMultiselectable)) {
|
|
210
|
-
const idx = (value.value as ItemValue[]).findIndex((iv) =>
|
|
211
|
-
isValuesEqual(v, iv)
|
|
212
|
-
);
|
|
213
|
-
if (idx > -1) {
|
|
214
|
-
(value.value as ItemValue[]).splice(idx, 1);
|
|
215
|
-
} else {
|
|
216
|
-
value.value = [...(value.value as ItemValue[]), v];
|
|
217
|
-
}
|
|
218
|
-
} else {
|
|
219
|
-
value.value = v;
|
|
220
|
-
}
|
|
221
|
-
activateItem(item);
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
function isSelected(item: Item) {
|
|
225
|
-
return unref(isMultiselectable)
|
|
226
|
-
? !!(value.value as ItemValue[]).find((iv) =>
|
|
227
|
-
isValuesEqual(item.value, iv)
|
|
228
|
-
)
|
|
229
|
-
: isValuesEqual(value, item.value);
|
|
230
|
-
}
|
|
231
|
-
|
|
232
|
-
function isSelectedById(itemId: number) {
|
|
233
|
-
try {
|
|
234
|
-
const item = getItem(itemId);
|
|
235
|
-
return isSelected(item);
|
|
236
|
-
} catch {
|
|
237
|
-
return false;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function activateSelectedItem({ focus = false }: ActivationOptions = {}) {
|
|
242
|
-
for (let item of items.value) {
|
|
243
|
-
if (isSelected(item)) {
|
|
244
|
-
return activateItem(item, { focus });
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function activateFirstItem({ focus = false }: ActivationOptions = {}) {
|
|
250
|
-
if (!items.value.length) return;
|
|
251
|
-
return activateItem(items.value[0], { focus });
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
function activateLastItem({ focus = false }: ActivationOptions = {}) {
|
|
255
|
-
if (!items.value.length) return;
|
|
256
|
-
return activateItem(items.value[items.value.length - 1], { focus });
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function activateNextItem({
|
|
260
|
-
focus = false,
|
|
261
|
-
loop = false,
|
|
262
|
-
}: ActivationOptions & NavigationOptions = {}) {
|
|
263
|
-
if (activeItemId.value === undefined) return activateFirstItem({ focus });
|
|
264
|
-
const item = getItem(activeItemId.value);
|
|
265
|
-
const idx = items.value.indexOf(item);
|
|
266
|
-
let nextItem: Item;
|
|
267
|
-
if (idx + 1 < items.value.length) {
|
|
268
|
-
nextItem = items.value[idx + 1];
|
|
269
|
-
} else {
|
|
270
|
-
if (loop) {
|
|
271
|
-
nextItem = items.value[0];
|
|
272
|
-
} else {
|
|
273
|
-
return;
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
return activateItem(nextItem, { focus });
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
function activatePrevItem({
|
|
280
|
-
focus = false,
|
|
281
|
-
loop = false,
|
|
282
|
-
}: ActivationOptions & NavigationOptions = {}) {
|
|
283
|
-
if (activeItemId.value === undefined) return activateLastItem({ focus });
|
|
284
|
-
const item = getItem(activeItemId.value);
|
|
285
|
-
const idx = items.value.indexOf(item);
|
|
286
|
-
let prevItem: Item;
|
|
287
|
-
if (idx - 1 > -1) {
|
|
288
|
-
prevItem = items.value[idx - 1];
|
|
289
|
-
} else {
|
|
290
|
-
if (loop) {
|
|
291
|
-
prevItem = items.value[items.value.length - 1];
|
|
292
|
-
} else {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
return activateItem(prevItem, { focus });
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
provide(injectList, {
|
|
300
|
-
value: readonly(value),
|
|
301
|
-
selectItem,
|
|
302
|
-
activateItem: activateItemById,
|
|
303
|
-
isSelected: isSelectedById,
|
|
304
|
-
isActive,
|
|
305
|
-
addItem,
|
|
306
|
-
removeItem,
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
return {
|
|
310
|
-
items: shallowReadonly(items),
|
|
311
|
-
isMultiselectable: readonly(isMultiselectable),
|
|
312
|
-
activeItem: computed(() =>
|
|
313
|
-
activeItemId.value !== undefined ? getItem(activeItemId.value) : undefined
|
|
314
|
-
),
|
|
315
|
-
selectActiveItem: () => {
|
|
316
|
-
if (activeItemId.value !== undefined) {
|
|
317
|
-
selectItem(activeItemId.value);
|
|
318
|
-
}
|
|
319
|
-
},
|
|
320
|
-
activateSelectedItem,
|
|
321
|
-
activateFirstItem,
|
|
322
|
-
activateLastItem,
|
|
323
|
-
activateNextItem,
|
|
324
|
-
activatePrevItem,
|
|
325
|
-
};
|
|
326
|
-
}
|
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import type { ComputedRef, MaybeRef } from "vue";
|
|
2
|
-
import {
|
|
3
|
-
ref,
|
|
4
|
-
unref,
|
|
5
|
-
inject,
|
|
6
|
-
computed,
|
|
7
|
-
onBeforeUnmount,
|
|
8
|
-
onBeforeMount,
|
|
9
|
-
} from "vue";
|
|
10
|
-
|
|
11
|
-
import type { TemplateRefOrSelector } from "../utils/element";
|
|
12
|
-
|
|
13
|
-
import type { ItemValue, ActivationOptions } from "./list";
|
|
14
|
-
import { injectList } from "./list";
|
|
15
|
-
|
|
16
|
-
interface UseListOptionReturn {
|
|
17
|
-
/**
|
|
18
|
-
* Whether the item is selected
|
|
19
|
-
*/
|
|
20
|
-
isSelected: ComputedRef<boolean>;
|
|
21
|
-
/**
|
|
22
|
-
* Whether the item is active
|
|
23
|
-
*/
|
|
24
|
-
isActive: ComputedRef<boolean>;
|
|
25
|
-
/**
|
|
26
|
-
* Select the item
|
|
27
|
-
*/
|
|
28
|
-
select: () => void;
|
|
29
|
-
/**
|
|
30
|
-
* Activate the item
|
|
31
|
-
*/
|
|
32
|
-
activate: (options?: ActivationOptions) => void;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Registers an HTML element and an associated value in the list created in the parent component
|
|
37
|
-
*
|
|
38
|
-
* @param el A template ref or a CSS selector
|
|
39
|
-
* @param value A value of any type
|
|
40
|
-
* @param disabled Whether the item is disabled (false by default)
|
|
41
|
-
*/
|
|
42
|
-
export function useListOption(
|
|
43
|
-
el: TemplateRefOrSelector,
|
|
44
|
-
value: ItemValue,
|
|
45
|
-
disabled?: MaybeRef<boolean>
|
|
46
|
-
): UseListOptionReturn {
|
|
47
|
-
const list = inject(injectList);
|
|
48
|
-
if (!list) {
|
|
49
|
-
throw new Error("List injectable context is not provided");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
const itemId = ref(-1);
|
|
53
|
-
|
|
54
|
-
const _isSelected = computed(() => list.isSelected(itemId.value));
|
|
55
|
-
const _isActive = computed(() => list.isActive(itemId.value));
|
|
56
|
-
|
|
57
|
-
function select() {
|
|
58
|
-
list!.selectItem(itemId.value);
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function activate({ focus = true } = {}) {
|
|
62
|
-
if (unref(disabled)) return;
|
|
63
|
-
list!.activateItem(itemId.value, { focus });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
onBeforeMount(() => {
|
|
67
|
-
itemId.value = list.addItem(el, value, disabled);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
onBeforeUnmount(() => {
|
|
71
|
-
list.removeItem(itemId.value);
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
isSelected: _isSelected,
|
|
76
|
-
isActive: _isActive,
|
|
77
|
-
select,
|
|
78
|
-
activate,
|
|
79
|
-
};
|
|
80
|
-
}
|