@mekari/pixel3-autocomplete 0.0.0

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.
@@ -0,0 +1,126 @@
1
+ // src/modules/autocomplete.props.ts
2
+ var autocompleteProps = {
3
+ id: {
4
+ type: String,
5
+ default: ""
6
+ },
7
+ data: {
8
+ type: [Array],
9
+ default: () => []
10
+ },
11
+ modelValue: {
12
+ type: [Number, String, Object, Array]
13
+ },
14
+ defaultValue: [Number, String, Object],
15
+ placeholder: {
16
+ type: String
17
+ },
18
+ labelProp: {
19
+ type: String,
20
+ default: "value"
21
+ },
22
+ valueProp: {
23
+ type: String,
24
+ default: "value"
25
+ },
26
+ isReadOnly: {
27
+ type: [Boolean],
28
+ default: false
29
+ },
30
+ isDisabled: {
31
+ type: [Boolean],
32
+ default: false
33
+ },
34
+ isInvalid: {
35
+ type: [Boolean],
36
+ default: false
37
+ },
38
+ isRequired: {
39
+ type: [Boolean],
40
+ default: false
41
+ },
42
+ isFullWidth: {
43
+ type: [Boolean],
44
+ default: true
45
+ },
46
+ isLoading: {
47
+ type: [Boolean],
48
+ default: false
49
+ },
50
+ isContentLoading: {
51
+ type: [Boolean],
52
+ default: false
53
+ },
54
+ isClearable: {
55
+ type: [Boolean],
56
+ default: false
57
+ },
58
+ isSearchable: {
59
+ type: [Boolean],
60
+ default: false
61
+ },
62
+ contentAttrs: {
63
+ type: Object
64
+ },
65
+ emptyText: {
66
+ type: [String],
67
+ default: "No result found"
68
+ },
69
+ contentLoadingText: {
70
+ type: [String],
71
+ default: "Loading"
72
+ },
73
+ isInfinityScroll: {
74
+ type: [Boolean],
75
+ default: false
76
+ },
77
+ isManualFilter: {
78
+ type: [Boolean],
79
+ default: false
80
+ },
81
+ usePortal: {
82
+ type: Boolean,
83
+ default: false
84
+ },
85
+ isShowButtonAction: {
86
+ type: Boolean,
87
+ default: false
88
+ },
89
+ buttonActionText: {
90
+ type: String,
91
+ default: "Button"
92
+ },
93
+ isGroupSuggestions: {
94
+ type: Boolean,
95
+ default: false
96
+ },
97
+ groupKey: {
98
+ type: String,
99
+ default: "group"
100
+ },
101
+ keyCode: {
102
+ type: Array,
103
+ default: () => ["Enter"]
104
+ },
105
+ isRawValue: {
106
+ type: [Boolean],
107
+ default: false
108
+ },
109
+ isListItemActiveFunction: {
110
+ type: Function
111
+ },
112
+ isKeepAlive: {
113
+ type: Boolean,
114
+ default: true
115
+ },
116
+ isAdaptiveWidth: {
117
+ type: Boolean,
118
+ default: true
119
+ }
120
+ };
121
+ var autocompleteEmit = ["update:modelValue", "input", "change", "enter", "scrollEnd", "buttonAction"];
122
+
123
+ export {
124
+ autocompleteProps,
125
+ autocompleteEmit
126
+ };
@@ -0,0 +1,6 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
3
+
4
+ export {
5
+ __name
6
+ };
@@ -0,0 +1,111 @@
1
+ import {
2
+ useAutocomplete
3
+ } from "./chunk-YGOZYS5H.mjs";
4
+ import {
5
+ autocompleteProps
6
+ } from "./chunk-OX7ZBBEQ.mjs";
7
+
8
+ // src/autocomplete.tsx
9
+ import { createTextVNode as _createTextVNode, createVNode as _createVNode } from "vue";
10
+ import { defineComponent } from "vue";
11
+ import { MpPopover, MpPopoverTrigger, MpPopoverContent, MpPopoverList, MpPopoverListItem } from "@mekari/pixel3-popover";
12
+ import { MpInput, MpInputGroup, MpInputRightAddon, MpInputLeftAddon } from "@mekari/pixel3-input";
13
+ import { MpSpinner } from "@mekari/pixel3-spinner";
14
+ import { MpIcon } from "@mekari/pixel3-icon";
15
+ import { MpText } from "@mekari/pixel3-text";
16
+ import { flex } from "@mekari/pixel3-styled-system/patterns";
17
+ import { css } from "@mekari/pixel3-styled-system/css";
18
+ var MpAutocomplete = defineComponent({
19
+ name: "MpAutocomplete",
20
+ props: autocompleteProps,
21
+ emits: ["update:modelValue", "input", "change", "enter", "scrollEnd", "buttonAction"],
22
+ setup(props, {
23
+ emit,
24
+ slots
25
+ }) {
26
+ const {
27
+ getSuggestionDatas,
28
+ getGroupSuggestions,
29
+ currentSearch,
30
+ scrollEndNode,
31
+ rootAtrrs,
32
+ popoverAtrrs,
33
+ popoverContentAtrrs,
34
+ inputAtrrs,
35
+ buttonActionAttrs,
36
+ emptyTextAttrs,
37
+ contentLoadingAttrs,
38
+ popoverListItemAttrs,
39
+ getLabel
40
+ } = useAutocomplete(props, emit);
41
+ return () => {
42
+ const suggestionsNode = !props.isGroupSuggestions ? getSuggestionDatas.value.map((item, index) => {
43
+ return _createVNode(MpPopoverListItem, popoverListItemAttrs(item, index), {
44
+ default: () => [slots.default ? slots.default({
45
+ item
46
+ }) : getLabel(item)]
47
+ });
48
+ }) : getGroupSuggestions.value.map((group, groupIndex) => {
49
+ return _createVNode("div", {
50
+ "key": groupIndex,
51
+ "data-category": group.key,
52
+ "class": flex({
53
+ direction: "column",
54
+ alignItems: "start",
55
+ width: "full"
56
+ })
57
+ }, [slots.group ? slots.group({
58
+ group
59
+ }) : _createVNode(MpText, {
60
+ "size": "body-small",
61
+ "color": "gray.600",
62
+ "class": css({
63
+ px: 3,
64
+ py: 2
65
+ })
66
+ }, {
67
+ default: () => [group.key]
68
+ }), group.values.map((item) => {
69
+ return _createVNode(MpPopoverListItem, popoverListItemAttrs(item, item.index), {
70
+ default: () => [slots.default ? slots.default({
71
+ item
72
+ }) : getLabel(item)]
73
+ });
74
+ })]);
75
+ });
76
+ const buttonActionNode = props.isShowButtonAction && _createVNode("button", buttonActionAttrs.value, [_createTextVNode(" "), slots.buttonAction ? slots.buttonAction(getSuggestionDatas.value, currentSearch.value) : props.buttonActionText, _createTextVNode(" ")]);
77
+ const emptyTextNode = _createVNode(MpText, emptyTextAttrs, {
78
+ default: () => [_createTextVNode(" "), props.emptyText, _createTextVNode(" ")]
79
+ });
80
+ const contentLoadingNode = props.isContentLoading && _createVNode("div", contentLoadingAttrs, [_createVNode(MpSpinner, {
81
+ "size": "md"
82
+ }, null), _createVNode(MpText, null, {
83
+ default: () => [_createTextVNode(" "), props.contentLoadingText, _createTextVNode(" ")]
84
+ })]);
85
+ return _createVNode("div", rootAtrrs.value, [_createVNode(MpPopover, popoverAtrrs.value, {
86
+ default: () => [_createVNode(MpPopoverTrigger, null, {
87
+ default: () => [_createVNode(MpInputGroup, null, {
88
+ default: () => [slots.leftAddon && _createVNode(MpInputLeftAddon, null, {
89
+ default: () => [_createTextVNode(" "), slots.leftAddon(), _createTextVNode(" ")]
90
+ }), _createVNode(MpInput, inputAtrrs.value, null), _createVNode(MpInputRightAddon, null, {
91
+ default: () => [props.isLoading ? _createVNode(MpSpinner, null, null) : _createVNode(MpIcon, {
92
+ "name": "chevrons-down",
93
+ "size": "sm"
94
+ }, null)]
95
+ })]
96
+ })]
97
+ }), _createVNode(MpPopoverContent, popoverContentAtrrs.value, {
98
+ default: () => [_createVNode(MpPopoverList, null, {
99
+ default: () => [getSuggestionDatas.value.length === 0 ? emptyTextNode : suggestionsNode, contentLoadingNode]
100
+ }), props.isInfinityScroll && _createVNode("div", {
101
+ "ref": scrollEndNode
102
+ }, null), buttonActionNode]
103
+ })]
104
+ })]);
105
+ };
106
+ }
107
+ });
108
+
109
+ export {
110
+ MpAutocomplete
111
+ };
@@ -0,0 +1,394 @@
1
+ import {
2
+ __name
3
+ } from "./chunk-QZ7VFGWC.mjs";
4
+
5
+ // src/modules/autocomplete.hooks.ts
6
+ import { ref, computed, toRefs, nextTick, watch, onMounted } from "vue";
7
+ import { isDef, isUndef, isElementVisible, scrollToTargetElement, isEqual, isObject, getUniqueId, useIntersectionObserver, groupBy } from "@mekari/pixel3-utils";
8
+ import { css } from "@mekari/pixel3-styled-system/css";
9
+ function useAutocomplete(props, emit) {
10
+ const {
11
+ data,
12
+ usePortal,
13
+ isKeepAlive,
14
+ isClearable,
15
+ isSearchable,
16
+ isReadOnly,
17
+ isFullWidth,
18
+ labelProp,
19
+ valueProp,
20
+ isInfinityScroll,
21
+ placeholder,
22
+ isAdaptiveWidth
23
+ } = toRefs(props);
24
+ const scrollEndNode = ref();
25
+ const currentValue = ref();
26
+ const currentLabel = ref("");
27
+ const currentCursor = ref("");
28
+ const currentSearch = ref("");
29
+ const isOutside = ref(true);
30
+ const isPopoverOpen = ref(false);
31
+ const {
32
+ resume: enableIntersectionObserver,
33
+ pause: disableIntersectionObserver
34
+ } = useIntersectionObserver(scrollEndNode, ([{
35
+ isIntersecting
36
+ }]) => {
37
+ if (isIntersecting) {
38
+ emit("scrollEnd");
39
+ }
40
+ });
41
+ const getId = props.id || getUniqueId("", "autocomplete").value;
42
+ const getSuggestionDatas = computed(() => {
43
+ const datas = data.value || [];
44
+ if (props.isManualFilter)
45
+ return datas;
46
+ if (!currentSearch.value)
47
+ return datas;
48
+ if (currentSearch.value === currentLabel.value)
49
+ return datas;
50
+ return datas.filter((item) => {
51
+ const label = getLabel(item).toLocaleLowerCase();
52
+ const search = currentSearch.value.toLocaleLowerCase();
53
+ return label.includes(search);
54
+ });
55
+ });
56
+ const getGroupSuggestions = computed(() => {
57
+ const mapped = getSuggestionDatas.value.map((item, index) => {
58
+ const groupKey = item[props.groupKey];
59
+ const isUncategorized = /* @__PURE__ */ __name((value) => {
60
+ if (isUndef(value) || value === "")
61
+ return true;
62
+ return false;
63
+ }, "isUncategorized");
64
+ return {
65
+ ...item,
66
+ index,
67
+ [props.groupKey]: isUncategorized(groupKey) ? "UNCATEGORIZED" : groupKey
68
+ };
69
+ });
70
+ const grouped = groupBy(mapped, props.groupKey);
71
+ const keys = Object.keys(grouped);
72
+ const results = keys.map((item) => {
73
+ return {
74
+ key: item,
75
+ values: grouped[item]
76
+ };
77
+ });
78
+ return results;
79
+ });
80
+ const getValueOfSuggestions = computed(() => {
81
+ if (props.isRawValue)
82
+ return getSuggestionDatas.value;
83
+ const isArrayOfObject = getSuggestionDatas.value.some((item) => isObject(item));
84
+ if (isArrayOfObject) {
85
+ return getSuggestionDatas.value.map((item) => item[valueProp.value]);
86
+ }
87
+ return getSuggestionDatas.value;
88
+ });
89
+ const getCursorPosition = computed(() => getValueOfSuggestions.value.findIndex((item) => isEqual(getValue(item), currentCursor.value)));
90
+ function onOpenPopover(e) {
91
+ isPopoverOpen.value = true;
92
+ if (isSearchable.value) {
93
+ const target = e.target;
94
+ target.setSelectionRange(0, target.value.length || 0);
95
+ }
96
+ if (props.modelValue) {
97
+ currentCursor.value = getValue(props.modelValue);
98
+ nextTick(() => {
99
+ handleAdjustScrollPosition();
100
+ });
101
+ } else {
102
+ handleSetFirstSuggestion();
103
+ }
104
+ if (isInfinityScroll.value) {
105
+ enableIntersectionObserver();
106
+ }
107
+ }
108
+ __name(onOpenPopover, "onOpenPopover");
109
+ function onBlur() {
110
+ isPopoverOpen.value = false;
111
+ const inputEl = document.getElementById(`${getId}-control`);
112
+ inputEl == null ? void 0 : inputEl.blur();
113
+ if (isInfinityScroll.value) {
114
+ disableIntersectionObserver();
115
+ }
116
+ }
117
+ __name(onBlur, "onBlur");
118
+ function onInputChange(e) {
119
+ const target = e.target;
120
+ currentSearch.value = target.value;
121
+ onSearchChange();
122
+ emit("input", e);
123
+ }
124
+ __name(onInputChange, "onInputChange");
125
+ function onSelectListItem(value) {
126
+ const innerValue = getValue(value);
127
+ emit("update:modelValue", innerValue);
128
+ emit("change", innerValue);
129
+ onBlur();
130
+ }
131
+ __name(onSelectListItem, "onSelectListItem");
132
+ function onSearchChange() {
133
+ nextTick(() => {
134
+ const hasSearchAndResult = currentSearch.value && getValueOfSuggestions.value.length;
135
+ const isEqualSearchAndLabel = currentSearch.value === currentLabel.value;
136
+ const emptySearchButHaveLabel = !currentSearch.value && currentLabel.value;
137
+ if (hasSearchAndResult) {
138
+ if (isEqualSearchAndLabel) {
139
+ currentCursor.value = currentValue.value;
140
+ handleAdjustScrollPosition();
141
+ } else {
142
+ handleSetFirstSuggestion();
143
+ }
144
+ }
145
+ if (emptySearchButHaveLabel) {
146
+ currentCursor.value = currentValue.value;
147
+ handleAdjustScrollPosition();
148
+ }
149
+ });
150
+ }
151
+ __name(onSearchChange, "onSearchChange");
152
+ function onModelValueChange() {
153
+ const data2 = props.data.find((item) => {
154
+ return isEqual(getValue(item), props.modelValue);
155
+ });
156
+ if (data2) {
157
+ currentValue.value = props.modelValue;
158
+ currentLabel.value = getLabel(data2);
159
+ currentSearch.value = getLabel(data2);
160
+ }
161
+ }
162
+ __name(onModelValueChange, "onModelValueChange");
163
+ function handleForceFocusToInput() {
164
+ const inputEl = document.getElementById(`${getId}-control`);
165
+ inputEl == null ? void 0 : inputEl.focus();
166
+ }
167
+ __name(handleForceFocusToInput, "handleForceFocusToInput");
168
+ function onKeydown(e) {
169
+ if (e.code === "Enter") {
170
+ if (getSuggestionDatas.value.length) {
171
+ onSelectListItem(currentCursor.value);
172
+ emit("enter", getSuggestionDatas.value, currentSearch.value);
173
+ } else {
174
+ emit("enter", getSuggestionDatas.value, currentSearch.value);
175
+ onBlur();
176
+ }
177
+ }
178
+ if (e.code === "ArrowDown") {
179
+ e.preventDefault();
180
+ e.stopPropagation();
181
+ const totalData = getSuggestionDatas.value.length - 1;
182
+ const position = getCursorPosition.value;
183
+ const isLastPosition = totalData === position;
184
+ if (!isLastPosition) {
185
+ const nextPosition = position + 1;
186
+ currentCursor.value = getValueOfSuggestions.value[nextPosition];
187
+ handleAdjustScrollPosition();
188
+ }
189
+ }
190
+ if (e.code === "ArrowUp") {
191
+ e.preventDefault();
192
+ e.stopPropagation();
193
+ const position = getCursorPosition.value;
194
+ const isFirstPosition = position === 0;
195
+ if (!isFirstPosition) {
196
+ const nextPosition = position - 1;
197
+ currentCursor.value = getValueOfSuggestions.value[nextPosition];
198
+ handleAdjustScrollPosition();
199
+ }
200
+ }
201
+ }
202
+ __name(onKeydown, "onKeydown");
203
+ async function handleAdjustScrollPosition() {
204
+ const position = getValueOfSuggestions.value.indexOf(currentCursor.value);
205
+ const containerElement = document.getElementById(`popover-content-${getId}`);
206
+ const targetElement = containerElement == null ? void 0 : containerElement.querySelector(`[data-index="${position}"]`);
207
+ if (isDef(targetElement)) {
208
+ const isVisible = await isElementVisible(targetElement);
209
+ const getPosition = /* @__PURE__ */ __name(() => {
210
+ if (getCursorPosition.value === 0)
211
+ return "top";
212
+ if (props.data.length === getCursorPosition.value + 1)
213
+ return "bottom";
214
+ return void 0;
215
+ }, "getPosition");
216
+ if (!isVisible) {
217
+ nextTick(() => {
218
+ scrollToTargetElement(targetElement, containerElement, getPosition());
219
+ });
220
+ }
221
+ }
222
+ }
223
+ __name(handleAdjustScrollPosition, "handleAdjustScrollPosition");
224
+ function handleSetFirstSuggestion() {
225
+ currentCursor.value = getValueOfSuggestions.value[0] || "";
226
+ handleAdjustScrollPosition();
227
+ }
228
+ __name(handleSetFirstSuggestion, "handleSetFirstSuggestion");
229
+ function getValue(value) {
230
+ if (props.isRawValue)
231
+ return value;
232
+ return isObject(value) ? value[valueProp.value] : value;
233
+ }
234
+ __name(getValue, "getValue");
235
+ function getLabel(value) {
236
+ return isObject(value) ? value[labelProp.value] : value;
237
+ }
238
+ __name(getLabel, "getLabel");
239
+ function isListItemActive(item) {
240
+ if (props.isListItemActiveFunction)
241
+ return props.isListItemActiveFunction(props.modelValue, item);
242
+ return isEqual(props.modelValue, getValue(item));
243
+ }
244
+ __name(isListItemActive, "isListItemActive");
245
+ const rootAtrrs = computed(() => {
246
+ return {
247
+ style: {
248
+ width: "100%",
249
+ position: "relative"
250
+ },
251
+ onMouseenter: () => {
252
+ isOutside.value = false;
253
+ },
254
+ onMouseleave: () => {
255
+ isOutside.value = true;
256
+ }
257
+ };
258
+ });
259
+ const popoverAtrrs = computed(() => {
260
+ return {
261
+ id: getId,
262
+ isManual: true,
263
+ isOpen: isPopoverOpen.value,
264
+ usePortal: usePortal.value,
265
+ isKeepAlive: isKeepAlive.value,
266
+ isAdaptiveWidth: isAdaptiveWidth.value
267
+ };
268
+ });
269
+ const popoverContentAtrrs = computed(() => {
270
+ return {
271
+ class: css({
272
+ maxHeight: "300px",
273
+ overflowY: "auto",
274
+ position: "relative"
275
+ }),
276
+ onClick: handleForceFocusToInput,
277
+ ...props.contentAttrs
278
+ };
279
+ });
280
+ const inputAtrrs = computed(() => {
281
+ return {
282
+ // key: currentLabel.value,
283
+ id: getId,
284
+ modelValue: currentLabel.value,
285
+ isClearable: isClearable.value,
286
+ autocomplete: "off",
287
+ isReadOnly: !isSearchable.value || isReadOnly.value,
288
+ isFullWidth: isFullWidth.value,
289
+ placeholder: placeholder == null ? void 0 : placeholder.value,
290
+ onInput: onInputChange,
291
+ onKeydown,
292
+ onFocus: onOpenPopover,
293
+ onBlur: () => {
294
+ if (isOutside.value) {
295
+ onBlur();
296
+ }
297
+ }
298
+ };
299
+ });
300
+ const buttonActionAttrs = computed(() => {
301
+ return {
302
+ class: css({
303
+ cursor: "pointer",
304
+ width: "full",
305
+ textAlign: "center",
306
+ roundedTop: "0",
307
+ borderTopWidth: "1px",
308
+ borderColor: "blue.50",
309
+ color: "blue.400",
310
+ position: "sticky",
311
+ bottom: "0px",
312
+ bg: "white",
313
+ fontSize: "md",
314
+ zIndex: "999",
315
+ py: 2,
316
+ height: "9.5",
317
+ _hover: {
318
+ color: "blue.500"
319
+ }
320
+ }),
321
+ onClick: (e) => {
322
+ e.stopPropagation();
323
+ emit("buttonAction", getSuggestionDatas.value, currentSearch.value);
324
+ isOutside.value = true;
325
+ onBlur();
326
+ }
327
+ };
328
+ });
329
+ const emptyTextAttrs = {
330
+ color: "gray.400",
331
+ class: css({
332
+ px: 3,
333
+ py: 2
334
+ })
335
+ };
336
+ const contentLoadingAttrs = {
337
+ class: css({
338
+ px: 3,
339
+ py: 2,
340
+ display: "flex",
341
+ alignItems: "center",
342
+ gap: 3
343
+ })
344
+ };
345
+ const popoverListItemAttrs = /* @__PURE__ */ __name((item, index) => {
346
+ const innerValue = getValue(item);
347
+ return {
348
+ onClick: (e) => {
349
+ e.stopPropagation();
350
+ onSelectListItem(item);
351
+ },
352
+ onMouseenter: () => currentCursor.value = innerValue,
353
+ isActive: isListItemActive(item),
354
+ "data-highlight": isEqual(innerValue, currentCursor.value) ? true : void 0,
355
+ "data-index": index,
356
+ "aria-selected": isListItemActive(item) ? true : void 0
357
+ };
358
+ }, "popoverListItemAttrs");
359
+ watch(() => props.modelValue, (newValue) => {
360
+ if (newValue)
361
+ onModelValueChange();
362
+ });
363
+ watch(() => props.data, () => {
364
+ if (props.isManualFilter && isPopoverOpen.value) {
365
+ nextTick(() => {
366
+ handleSetFirstSuggestion();
367
+ });
368
+ }
369
+ });
370
+ onMounted(() => {
371
+ if (props.modelValue)
372
+ onModelValueChange();
373
+ });
374
+ return {
375
+ currentSearch,
376
+ scrollEndNode,
377
+ getSuggestionDatas,
378
+ getGroupSuggestions,
379
+ rootAtrrs,
380
+ popoverAtrrs,
381
+ popoverContentAtrrs,
382
+ inputAtrrs,
383
+ buttonActionAttrs,
384
+ emptyTextAttrs,
385
+ contentLoadingAttrs,
386
+ popoverListItemAttrs,
387
+ getLabel
388
+ };
389
+ }
390
+ __name(useAutocomplete, "useAutocomplete");
391
+
392
+ export {
393
+ useAutocomplete
394
+ };
@@ -0,0 +1,4 @@
1
+ export { MpAutocomplete } from './autocomplete.mjs';
2
+ import 'vue';
3
+ import './modules/autocomplete.props.mjs';
4
+ import '@mekari/pixel3-popover';
@@ -0,0 +1,4 @@
1
+ export { MpAutocomplete } from './autocomplete.js';
2
+ import 'vue';
3
+ import './modules/autocomplete.props.js';
4
+ import '@mekari/pixel3-popover';