quasar-ui-sellmate-ui-kit 3.14.2 → 3.14.5

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.
@@ -1,320 +1,328 @@
1
- <template>
2
- <q-select
3
- outlined
4
- dense
5
- options-dense
6
- :dropdown-icon="selectDownArrowIcon"
7
- v-model="model"
8
- :options="filteredOptions"
9
- emit-value
10
- map-options
11
- color="positive"
12
- class="s-select-search"
13
- :popup-content-class="
14
- [
15
- 's-select-opts',
16
- `s-select-opt-${id}`,
17
- isScrolled && 's-select-opts--scroll',
18
- popupContentClass,
19
- ].join(' ')
20
- "
21
- :option-label="optionLabel"
22
- :option-value="optionValue"
23
- :menu-offset="[0, 4]"
24
- menu-self="top left"
25
- menu-anchor="bottom start"
26
- no-error-icon
27
- hide-bottom-space
28
- ref="sSelectRef"
29
- @popup-show="handleScroll"
30
- @popup-hide="removeScroll"
31
- >
32
- <template #before-options>
33
- <div class="search-input-form-container">
34
- <input-with-search-icon
35
- v-model="search"
36
- autofocus
37
- class="select-search-input"
38
- :placeholder="searchPlaceholder"
39
- @keydown.prevent.enter="e => onSearch(e.target.value)"
40
- @keydown.prevent.arrow-down="handleKey"
41
- @keydown.prevent.arrow-up="handleKey"
42
- @on-input="handleInput"
43
- @on-delete="handleDelete"
44
- />
45
- </div>
46
- </template>
47
- <template #option="{ itemProps, opt }">
48
- <q-item v-bind="itemProps" class="custom-select-options">
49
- <q-item-section>
50
- {{ opt[optionLabel] }}
51
- </q-item-section>
52
- </q-item>
53
- </template>
54
- <template v-if="(options.length && !model) || (noSelected && !options.length)" #selected>
55
- <div v-if="noSelected && !options.length">{{ noSelected }}</div>
56
- <div v-else>
57
- {{ placeholder }}
58
- </div>
59
- </template>
60
- <template #no-option>
61
- <div class="search-input-form-container">
62
- <input-with-search-icon
63
- v-model="search"
64
- autofocus
65
- class="select-search-input"
66
- :placeholder="searchPlaceholder"
67
- @keydown.prevent.enter="e => onSearch(e.target.value)"
68
- @keydown.prevent.arrow-down="handleKey"
69
- @keydown.prevent.arrow-up="handleKey"
70
- @on-input="handleInput"
71
- @on-delete="handleDelete"
72
- />
73
- </div>
74
- <q-item class="s-select-no-option">
75
- <q-item-section class="text-grey">{{ noData }}</q-item-section>
76
- </q-item>
77
- </template>
78
- </q-select>
79
- </template>
80
-
81
- <script>
82
- import { QImg, QItem, QItemSection, QSelect, debounce, useId } from 'quasar';
83
- import { defineComponent, nextTick, ref, watch } from 'vue';
84
- import { selectDownArrowIcon } from '../assets/icons';
85
- import InputWithSearchIcon from './InputWithSearchIcon.vue';
86
-
87
- export default defineComponent({
88
- name: 'SSelectSearch',
89
- emits: ['update:modelValue', 'onSearch'],
90
- components: {
91
- QSelect,
92
- QItem,
93
- QItemSection,
94
- InputWithSearchIcon,
95
- },
96
- props: {
97
- searchPlaceholder: { type: String, default: '검색어 입력 후 Enter' },
98
- options: {
99
- type: Array,
100
- required: true,
101
- },
102
- modelValue: {
103
- type: [String, Number, null],
104
- required: true,
105
- },
106
- optionLabel: {
107
- type: String,
108
- default: 'label',
109
- },
110
- optionValue: {
111
- type: String,
112
- default: 'value',
113
- },
114
- placeholder: {
115
- type: String,
116
- default: '전체',
117
- },
118
- noData: {
119
- type: String,
120
- default: '데이터 없음',
121
- },
122
- popupContentClass: {
123
- type: String,
124
- },
125
- noSelected: {
126
- type: String,
127
- default: '선택',
128
- },
129
- },
130
- setup(props, { emit }) {
131
- const id = useId();
132
- const model = ref(props.modelValue);
133
- const filteredOptions = ref(props.options);
134
- const findLabel = () =>
135
- props.options.find(opt => opt[props.optionValue] === props.modelValue)[props.optionLabel] ||
136
- '';
137
-
138
- const search = ref('');
139
- const sSelectRef = ref(null);
140
-
141
- function handleKey(e) {
142
- if (sSelectRef.value) {
143
- sSelectRef.value.moveOptionSelection(e.key === 'ArrowDown' ? 1 : -1, true);
144
- }
145
- }
146
-
147
- function handleInput(val) {
148
- search.value = val;
149
- sSelectRef.value.setOptionIndex(-1);
150
-
151
- if (!val) {
152
- filteredOptions.value = props.options;
153
- sSelectRef.value.setOptionIndex(-1);
154
- }
155
- }
156
-
157
- function handleDelete() {
158
- sSelectRef.value.setOptionIndex(-1);
159
- search.value = '';
160
- emit('onSearch', '');
161
- }
162
-
163
- const onSearch = debounce(val => {
164
- if (!val) {
165
- filteredOptions.value = props.options;
166
- return;
167
- }
168
-
169
- // 옵션을 선택 했을
170
- if (sSelectRef.value.getOptionIndex() !== -1) {
171
- model.value = filteredOptions.value[sSelectRef.value.getOptionIndex()][props.optionValue];
172
- handleDelete();
173
- sSelectRef.value.setOptionIndex(-1);
174
- sSelectRef.value.hidePopup();
175
- return;
176
- }
177
-
178
- const filtered = props.options.filter(
179
- v => (v[props.optionLabel] || v).toLowerCase().indexOf(val.toLowerCase()) > -1,
180
- );
181
-
182
- if (!filtered.length) {
183
- emit('onSearch', val);
184
- sSelectRef.value.setOptionIndex(-1);
185
- return;
186
- }
187
-
188
- filteredOptions.value = filtered;
189
- }, 200);
190
-
191
- watch(
192
- () => props.modelValue,
193
- val => {
194
- model.value = val;
195
- sSelectRef.value.setOptionIndex(-1);
196
- },
197
- );
198
-
199
- watch(model, val => {
200
- emit('update:modelValue', val);
201
- });
202
-
203
- watch(
204
- () => props.options,
205
- val => {
206
- // const existingValues = new Set(filteredOptions.value.map((option) => option.value));
207
- // const nonDuplicateOptions = val.filter((option) => !existingValues.has(option.value));
208
- // filteredOptions.value.push(...nonDuplicateOptions);
209
- filteredOptions.value = val;
210
- sSelectRef.value.setOptionIndex(-1);
211
- },
212
- { deep: true },
213
- );
214
-
215
- const isScrolled = ref(false);
216
-
217
- let scrollTimeout = null;
218
- const handleScroll = () => {
219
- const selectOptsEl = document.getElementsByClassName(`s-select-opt-${id.value}`);
220
- nextTick(() => {
221
- if (selectOptsEl[0]) {
222
- selectOptsEl[0].addEventListener('scroll', () => {
223
- isScrolled.value = true;
224
-
225
- if (scrollTimeout) {
226
- clearTimeout(scrollTimeout);
227
- }
228
- scrollTimeout = setTimeout(() => {
229
- isScrolled.value = false;
230
- }, 100); // 100ms 후 스크롤 멈춤으로 간주
231
- });
232
- }
233
- });
234
- };
235
-
236
- const removeScroll = () => {
237
- const selectOptsEl = document.getElementsByClassName(`s-select-opt-${id.value}`);
238
- nextTick(() => {
239
- if (selectOptsEl[0]) {
240
- selectOptsEl[0].removeEventListener('scroll', () => {
241
- if (scrollTimeout) {
242
- clearTimeout(scrollTimeout);
243
- }
244
-
245
- isScrolled.value = false;
246
- });
247
- }
248
- });
249
- };
250
-
251
- return {
252
- model,
253
- selectDownArrowIcon,
254
- onSearch,
255
- filteredOptions,
256
- search,
257
- sSelectRef,
258
- isScrolled,
259
- id,
260
- handleKey,
261
- handleInput,
262
- handleDelete,
263
- handleScroll,
264
- removeScroll,
265
- };
266
- },
267
- });
268
- </script>
269
-
270
- <style lang="scss">
271
- @import '../css/quasar.variables.scss';
272
- @import '../css/extends.scss';
273
-
274
- .s-select-search {
275
- @extend %select;
276
-
277
- .q-field__native {
278
- display: block;
279
- min-height: 0;
280
- height: $default-height;
281
- width: 100%;
282
- padding: $select-padding !important;
283
- color: $Grey_Darken-4;
284
- white-space: nowrap;
285
- overflow: hidden;
286
- text-overflow: ellipsis;
287
-
288
- > span {
289
- max-height: $default-height;
290
- }
291
- }
292
- }
293
-
294
- .s-select-opts {
295
- @extend %select-menu-list;
296
- max-height: 252px;
297
- .search-input-form-container {
298
- position: sticky;
299
- top: 0px;
300
- padding: 4px;
301
- z-index: 1;
302
- background-color: white;
303
- }
304
- &--scroll {
305
- .search-input-form-container {
306
- box-shadow: 2px 2px 8px 2px #00000033;
307
- }
308
- }
309
- }
310
-
311
- .custom-select-options {
312
- height: $default-height;
313
- }
314
-
315
- .disabled.s-select-option {
316
- border: none;
317
- background: none !important;
318
- color: $Grey_Default !important;
319
- }
320
- </style>
1
+ <template>
2
+ <q-select
3
+ outlined
4
+ dense
5
+ options-dense
6
+ :dropdown-icon="selectDownArrowIcon"
7
+ v-model="model"
8
+ :options="filteredOptions"
9
+ emit-value
10
+ map-options
11
+ color="positive"
12
+ class="s-select-search"
13
+ :popup-content-class="
14
+ [
15
+ 's-select-opts',
16
+ `s-select-opt-${id}`,
17
+ isScrolled && 's-select-opts--scroll',
18
+ popupContentClass,
19
+ ].join(' ')
20
+ "
21
+ :option-label="optionLabel"
22
+ :option-value="optionValue"
23
+ :menu-offset="[0, 4]"
24
+ menu-self="top left"
25
+ menu-anchor="bottom start"
26
+ no-error-icon
27
+ hide-bottom-space
28
+ ref="sSelectRef"
29
+ @popup-show="handleScroll"
30
+ @popup-hide="removeScroll"
31
+ >
32
+ <template #before-options>
33
+ <div class="search-input-form-container">
34
+ <input-with-search-icon
35
+ v-model="search"
36
+ autofocus
37
+ class="select-search-input"
38
+ :placeholder="searchPlaceholder"
39
+ @keydown.prevent.enter="e => onSearch(e.target.value)"
40
+ @keydown.prevent.arrow-down="handleKey"
41
+ @keydown.prevent.arrow-up="handleKey"
42
+ @on-input="handleInput"
43
+ @on-delete="handleDelete"
44
+ />
45
+ </div>
46
+ </template>
47
+ <template #option="{ itemProps, opt }">
48
+ <q-item v-bind="itemProps" class="custom-select-options">
49
+ <q-item-section>
50
+ {{ opt[optionLabel] }}
51
+ </q-item-section>
52
+ </q-item>
53
+ </template>
54
+ <template
55
+ v-if="(options && options.length && !model) || (noSelected && options && !options.length)"
56
+ #selected
57
+ >
58
+ <div v-if="noSelected && !options.length">{{ noSelected }}</div>
59
+ <div v-else>
60
+ {{ placeholder }}
61
+ </div>
62
+ </template>
63
+ <template #no-option>
64
+ <div class="search-input-form-container">
65
+ <input-with-search-icon
66
+ v-model="search"
67
+ autofocus
68
+ class="select-search-input"
69
+ :placeholder="searchPlaceholder"
70
+ @keydown.prevent.enter="e => onSearch(e.target.value)"
71
+ @keydown.prevent.arrow-down="handleKey"
72
+ @keydown.prevent.arrow-up="handleKey"
73
+ @on-input="handleInput"
74
+ @on-delete="handleDelete"
75
+ />
76
+ </div>
77
+ <q-item class="s-select-no-option">
78
+ <q-item-section class="text-grey">{{ noData }}</q-item-section>
79
+ </q-item>
80
+ </template>
81
+ </q-select>
82
+ </template>
83
+
84
+ <script>
85
+ import { QImg, QItem, QItemSection, QSelect, debounce, useId } from 'quasar';
86
+ import { defineComponent, nextTick, ref, watch } from 'vue';
87
+ import { selectDownArrowIcon } from '../assets/icons';
88
+ import InputWithSearchIcon from './InputWithSearchIcon.vue';
89
+
90
+ export default defineComponent({
91
+ name: 'SSelectSearch',
92
+ emits: ['update:modelValue', 'onSearch'],
93
+ components: {
94
+ QSelect,
95
+ QItem,
96
+ QItemSection,
97
+ InputWithSearchIcon,
98
+ },
99
+ props: {
100
+ searchPlaceholder: { type: String, default: '검색어 입력 후 Enter' },
101
+ options: {
102
+ type: Array,
103
+ required: true,
104
+ },
105
+ modelValue: {
106
+ type: [String, Number, null],
107
+ required: true,
108
+ },
109
+ optionLabel: {
110
+ type: String,
111
+ default: 'label',
112
+ },
113
+ optionValue: {
114
+ type: String,
115
+ default: 'value',
116
+ },
117
+ placeholder: {
118
+ type: String,
119
+ default: '전체',
120
+ },
121
+ noData: {
122
+ type: String,
123
+ default: '데이터 없음',
124
+ },
125
+ popupContentClass: {
126
+ type: String,
127
+ },
128
+ noSelected: {
129
+ type: String,
130
+ default: '선택',
131
+ },
132
+ },
133
+ setup(props, { emit }) {
134
+ const id = useId();
135
+ const model = ref(props.modelValue);
136
+ const filteredOptions = ref(props.options || []);
137
+ const findLabel = () =>
138
+ props.options.find(opt => opt[props.optionValue] === props.modelValue)[props.optionLabel] ||
139
+ '';
140
+
141
+ const search = ref('');
142
+ const sSelectRef = ref(null);
143
+
144
+ function handleKey(e) {
145
+ if (sSelectRef.value) {
146
+ sSelectRef.value.moveOptionSelection(e.key === 'ArrowDown' ? 1 : -1, true);
147
+ }
148
+ }
149
+
150
+ function handleInput(val) {
151
+ if (!sSelectRef.value) return;
152
+
153
+ search.value = val;
154
+ sSelectRef.value.setOptionIndex(-1);
155
+
156
+ if (!val && props.options) {
157
+ filteredOptions.value = props.options;
158
+ sSelectRef.value.setOptionIndex(-1);
159
+ }
160
+ }
161
+
162
+ function handleDelete() {
163
+ if (!sSelectRef.value) return;
164
+ sSelectRef.value.setOptionIndex(-1);
165
+ search.value = '';
166
+ emit('onSearch', '');
167
+ }
168
+
169
+ const onSearch = debounce(val => {
170
+ if (!val && props.options) {
171
+ filteredOptions.value = props.options;
172
+ return;
173
+ }
174
+
175
+ // 옵션을 선택 했을 때
176
+ if (sSelectRef.value && sSelectRef.value.getOptionIndex() !== -1) {
177
+ model.value = filteredOptions.value[sSelectRef.value.getOptionIndex()][props.optionValue];
178
+ handleDelete();
179
+ sSelectRef.value.setOptionIndex(-1);
180
+ sSelectRef.value.hidePopup();
181
+ return;
182
+ }
183
+
184
+ const filtered = props.options.filter(
185
+ v => (v[props.optionLabel] || v).toLowerCase().indexOf(val.toLowerCase()) > -1,
186
+ );
187
+
188
+ if (!filtered.length && sSelectRef.value) {
189
+ emit('onSearch', val);
190
+ sSelectRef.value.setOptionIndex(-1);
191
+ return;
192
+ }
193
+
194
+ filteredOptions.value = filtered;
195
+ }, 200);
196
+
197
+ watch(
198
+ () => props.modelValue,
199
+ val => {
200
+ model.value = val;
201
+ if (sSelectRef.value) sSelectRef.value.setOptionIndex(-1);
202
+ },
203
+ );
204
+
205
+ watch(model, val => {
206
+ emit('update:modelValue', val);
207
+ });
208
+
209
+ watch(
210
+ () => props.options,
211
+ val => {
212
+ // const existingValues = new Set(filteredOptions.value.map((option) => option.value));
213
+ // const nonDuplicateOptions = val.filter((option) => !existingValues.has(option.value));
214
+ // filteredOptions.value.push(...nonDuplicateOptions);
215
+ if (val) {
216
+ filteredOptions.value = val;
217
+ if (sSelectRef.value) sSelectRef.value.setOptionIndex(-1);
218
+ }
219
+ },
220
+ { deep: true },
221
+ );
222
+
223
+ const isScrolled = ref(false);
224
+
225
+ let scrollTimeout = null;
226
+ const handleScroll = () => {
227
+ const selectOptsEl = document.getElementsByClassName(`s-select-opt-${id.value}`);
228
+ nextTick(() => {
229
+ if (selectOptsEl[0]) {
230
+ selectOptsEl[0].addEventListener('scroll', () => {
231
+ isScrolled.value = true;
232
+
233
+ if (scrollTimeout) {
234
+ clearTimeout(scrollTimeout);
235
+ }
236
+ scrollTimeout = setTimeout(() => {
237
+ isScrolled.value = false;
238
+ }, 100); // 100ms 후 스크롤 멈춤으로 간주
239
+ });
240
+ }
241
+ });
242
+ };
243
+
244
+ const removeScroll = () => {
245
+ const selectOptsEl = document.getElementsByClassName(`s-select-opt-${id.value}`);
246
+ nextTick(() => {
247
+ if (selectOptsEl[0]) {
248
+ selectOptsEl[0].removeEventListener('scroll', () => {
249
+ if (scrollTimeout) {
250
+ clearTimeout(scrollTimeout);
251
+ }
252
+
253
+ isScrolled.value = false;
254
+ });
255
+ }
256
+ });
257
+ };
258
+
259
+ return {
260
+ model,
261
+ selectDownArrowIcon,
262
+ onSearch,
263
+ filteredOptions,
264
+ search,
265
+ sSelectRef,
266
+ isScrolled,
267
+ id,
268
+ handleKey,
269
+ handleInput,
270
+ handleDelete,
271
+ handleScroll,
272
+ removeScroll,
273
+ };
274
+ },
275
+ });
276
+ </script>
277
+
278
+ <style lang="scss">
279
+ @import '../css/quasar.variables.scss';
280
+ @import '../css/extends.scss';
281
+
282
+ .s-select-search {
283
+ @extend %select;
284
+
285
+ .q-field__native {
286
+ display: block;
287
+ min-height: 0;
288
+ height: $default-height;
289
+ width: 100%;
290
+ padding: $select-padding !important;
291
+ color: $Grey_Darken-4;
292
+ white-space: nowrap;
293
+ overflow: hidden;
294
+ text-overflow: ellipsis;
295
+
296
+ > span {
297
+ max-height: $default-height;
298
+ }
299
+ }
300
+ }
301
+
302
+ .s-select-opts {
303
+ @extend %select-menu-list;
304
+ max-height: 252px;
305
+ .search-input-form-container {
306
+ position: sticky;
307
+ top: 0px;
308
+ padding: 4px;
309
+ z-index: 1;
310
+ background-color: white;
311
+ }
312
+ &--scroll {
313
+ .search-input-form-container {
314
+ box-shadow: 2px 2px 8px 2px #00000033;
315
+ }
316
+ }
317
+ }
318
+
319
+ .custom-select-options {
320
+ height: $default-height;
321
+ }
322
+
323
+ .disabled.s-select-option {
324
+ border: none;
325
+ background: none !important;
326
+ color: $Grey_Default !important;
327
+ }
328
+ </style>