quasar-ui-sellmate-ui-kit 3.14.0 → 3.14.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "quasar-ui-sellmate-ui-kit",
3
- "version": "3.14.0",
3
+ "version": "3.14.2",
4
4
  "author": "Sellmate Dev Team <dev@sellmate.co.kr>",
5
5
  "description": "Sellmate UI Kit",
6
6
  "license": "MIT",
@@ -9,18 +9,13 @@
9
9
  multiple
10
10
  emit-value
11
11
  map-options
12
+ autocomplete="country"
12
13
  color="positive"
13
- :popup-content-class="
14
- [
15
- 's-select-checkbox-opts',
16
- idClass,
17
- isScrolled && 's-select-opts--scroll',
18
- popupContentClass,
19
- ].join(' ')
20
- "
14
+ :popup-content-class="['s-select-checkbox-opts', popupContentClass].join(' ')"
21
15
  class="s-select-checkbox"
22
16
  :option-label="optionLabel"
23
17
  :option-value="optionValue"
18
+ :option-group="optionGroup"
24
19
  @add="onAddChecked"
25
20
  @remove="onRemoveChecked"
26
21
  :menu-offset="[0, 4]"
@@ -29,22 +24,24 @@
29
24
  no-error-icon
30
25
  hide-bottom-space
31
26
  ref="sSelectRef"
32
- @popup-show="handleScroll"
33
- @popup-hide="removeScroll"
34
27
  >
28
+ <!-- TODO: 아무것도 선택되지 않았을 때 props 값으로 표기 해줘야함 기본 값은 "전체" -->
35
29
  <template #before-options>
36
30
  <div class="search-input-form-container">
37
- <input-with-search-icon
38
- v-model="search"
39
- autofocus
40
- class="select-search-input"
41
- :placeholder="searchPlaceholder"
42
- @keydown.prevent.enter="e => onSearch(e.target.value)"
43
- @keydown.prevent.arrow-down="handleKey"
44
- @keydown.prevent.arrow-up="handleKey"
45
- @on-input="handleInput"
46
- @on-delete="handleDelete"
47
- />
31
+ <form class="select-search-input-form">
32
+ <q-icon :name="searchIcon" size="20px" />
33
+ <input
34
+ v-model="search"
35
+ autofocus
36
+ class="select-search-input"
37
+ :placeholder="searchPlaceholder"
38
+ @input="
39
+ data => {
40
+ onSearch(data.target.value);
41
+ }
42
+ "
43
+ />
44
+ </form>
48
45
  </div>
49
46
  </template>
50
47
  <template #option="{ itemProps, opt, selected, toggleOption }">
@@ -52,59 +49,70 @@
52
49
  <q-item-section side v-if="!opt.disable">
53
50
  <s-checkbox :modelValue="selected" @update:modelValue="toggleOption(opt)" />
54
51
  </q-item-section>
52
+ <q-item-section v-if="opt[optionGroup]" class="text-Grey_Darken-4">
53
+ {{ opt[optionGroup] }}
54
+ </q-item-section>
55
55
  <q-item-section avatar v-if="opt.logo">
56
- <q-img class="q-pa-none bg-grey-11" :src="opt.logo" width="60px" height="22px" />
56
+ <q-img class="q-pa-none bg-Grey_Lighten-5" :src="opt.logo" width="60px" height="22px" />
57
57
  </q-item-section>
58
58
  <q-item-section>
59
59
  {{ opt[optionLabel] }}
60
60
  </q-item-section>
61
61
  </q-item>
62
62
  </template>
63
- <template #selected v-if="useAll && !!options.length && model.length === options.length">
64
- <div>
65
- {{ selectPlaceholder }}
63
+ <template
64
+ v-if="
65
+ (options.length && useAll && (model[0] === '' || model.includes(''))) ||
66
+ (noSelected && !options.length)
67
+ "
68
+ #selected
69
+ >
70
+ <div v-if="noSelected && !options.length">{{ noSelected }}</div>
71
+ <div v-else-if="options.length && useAll && (model[0] === '' || model.includes(''))">
72
+ {{ placeholder }}
66
73
  </div>
67
74
  </template>
68
-
69
75
  <template #no-option>
70
76
  <div class="search-input-form-container">
71
- <input-with-search-icon
72
- v-model="search"
73
- autofocus
74
- class="select-search-input"
75
- :placeholder="searchPlaceholder"
76
- @keydown.prevent.enter="e => onSearch(e.target.value)"
77
- @keydown.prevent.arrow-down="handleKey"
78
- @keydown.prevent.arrow-up="handleKey"
79
- @on-input="handleInput"
80
- @on-delete="handleDelete"
81
- />
77
+ <form class="select-search-input-form">
78
+ <q-icon :name="searchIcon" size="20px" />
79
+ <input
80
+ v-model="search"
81
+ autofocus
82
+ class="select-search-input"
83
+ :placeholder="searchPlaceholder"
84
+ @input="
85
+ data => {
86
+ onSearch(data.target.value);
87
+ }
88
+ "
89
+ />
90
+ </form>
82
91
  </div>
83
92
  <q-item class="s-select-no-option">
84
- <q-item-section class="text-grey">{{ noOptionLabel }}</q-item-section>
93
+ <q-item-section class="text-grey">{{ noData }}</q-item-section>
85
94
  </q-item>
86
95
  </template>
87
96
  </q-select>
88
97
  </template>
89
98
 
90
99
  <script>
91
- import { defineComponent, onBeforeMount, ref, nextTick, watch } from 'vue';
92
- import { QSelect, QItem, QItemSection, QImg, useId, debounce } from 'quasar';
93
- import { selectDownArrowIcon } from '../assets/icons';
94
- import InputWithSearchIcon from './InputWithSearchIcon.vue';
100
+ import { QIcon, QImg, QItem, QItemSection, QSelect, debounce } from 'quasar';
101
+ import { defineComponent, nextTick, onBeforeMount, ref, watch } from 'vue';
102
+ import { searchIcon, selectDownArrowIcon } from '../assets/icons';
95
103
 
96
104
  export default defineComponent({
97
- name: 'SSelectCheckbox',
98
- emits: ['update:modelValue', 'onSearch'],
105
+ name: 'SSelectSearchCheckbox',
106
+ emits: ['update:modelValue'],
99
107
  components: {
100
108
  QSelect,
101
109
  QItem,
102
110
  QItemSection,
103
111
  QImg,
104
- InputWithSearchIcon,
112
+ QIcon,
105
113
  },
106
114
  props: {
107
- searchPlaceholder: { type: String, default: '검색어 입력 후 Enter' },
115
+ searchPlaceholder: { type: String, default: '검색' },
108
116
  options: {
109
117
  type: Array,
110
118
  required: true,
@@ -121,209 +129,144 @@
121
129
  type: String,
122
130
  default: 'value',
123
131
  },
132
+ optionGroup: {
133
+ type: String,
134
+ default: 'group',
135
+ },
124
136
  useAll: {
125
137
  type: Boolean,
126
138
  default: false,
127
139
  },
128
- selectPlaceholder: {
140
+ placeholder: {
129
141
  type: String,
130
142
  default: '전체',
131
143
  },
144
+ checkboxSearch: {
145
+ type: String,
146
+ default: '',
147
+ },
148
+ noData: {
149
+ type: String,
150
+ default: '데이터 없음',
151
+ },
132
152
  popupContentClass: {
133
- default: () => '',
134
153
  type: String,
135
154
  },
136
- noOptionLabel: {
155
+ noSelected: {
137
156
  type: String,
138
- default: '선택지가 없습니다.',
157
+ default: '선택',
139
158
  },
140
159
  },
141
160
  setup(props, { emit }) {
142
- const NO_VALUE = ['', null];
143
- const filteredOptions = ref(props.options);
144
- const checkNoValue = value => NO_VALUE.includes(value);
145
161
  const model = ref(props.modelValue);
162
+ const filteredOptions = ref(props.options);
163
+ const searchInput = ref(null);
164
+ const search = ref('');
165
+ const isDisable = props.options.find(v => Boolean(v.disable));
146
166
 
167
+ /**
168
+ * @param {{ index: number; value: string | number }} detail
169
+ */
147
170
  function onRemoveChecked(detail) {
148
- if (checkNoValue(detail.value)) {
171
+ const idx = model.value.findIndex(v => v === '');
172
+ if (detail.value === '') {
149
173
  nextTick(() => {
150
174
  model.value = [];
151
175
  });
152
- } else if (detail.index > 0) {
176
+ } else if (idx >= 0) {
153
177
  nextTick(() => {
154
- model.value = model.value.filter(v => v !== detail.value && !checkNoValue(v));
178
+ model.value.splice(idx, 1);
155
179
  });
156
180
  }
157
181
  }
158
182
 
159
- const getOnlyValueArray = () => filteredOptions.value.map(v => v[props.optionValue]);
183
+ /**
184
+ * @param {{ index: number; value: string | number }} detail
185
+ */
160
186
  function onAddChecked(detail) {
161
- if (checkNoValue(detail.value)) {
187
+ if (detail.value === '') {
162
188
  nextTick(() => {
163
- model.value = getOnlyValueArray();
189
+ model.value = props.options
190
+ .filter(v => !v.category)
191
+ .map(option => option[props.optionValue]);
164
192
  });
165
193
  } else {
166
194
  nextTick(() => {
167
- if (props.useAll && model.value.length === filteredOptions.value.length - 1) {
168
- model.value = getOnlyValueArray();
195
+ if (model.value.length === props.options.length - 1) {
196
+ model.value = props.options
197
+ .filter(v => !v.category)
198
+ .map(option => option[props.optionValue]);
169
199
  }
170
200
  });
171
201
  }
172
202
  }
173
203
 
174
- function checkEmptyValue(opt) {
175
- if (typeof opt === 'object' && opt !== null) {
176
- return opt[props.optionValue] !== 0 && !opt[props.optionValue];
204
+ const onSearch = debounce(val => {
205
+ if (val === '') {
206
+ filteredOptions.value = props.options;
207
+ } else {
208
+ filteredOptions.value = props.options.filter(
209
+ v => (v[props.optionLabel] || v).toLowerCase().indexOf(val.toLowerCase()) > -1,
210
+ );
177
211
  }
178
-
179
- return opt !== 0 && !opt;
180
- }
212
+ }, 200);
181
213
 
182
214
  watch(
183
215
  () => props.modelValue,
184
- newModelValue => {
185
- if (props.useAll && newModelValue.length === 1 && checkEmptyValue(newModelValue[0])) {
186
- model.value = getOnlyValueArray();
187
- return;
188
- }
189
- model.value = newModelValue;
190
- },
191
- );
216
+ val => {
217
+ model.value = val;
192
218
 
193
- watch(
194
- () => props.options,
195
- newOptionValue => {
196
- filteredOptions.value = newOptionValue;
197
- if (props.useAll && !props.modelValue[0]) {
198
- model.value = getOnlyValueArray();
219
+ if (
220
+ props.options.filter(opt => opt[props.optionValue] && !opt.category).length ===
221
+ model.value.length &&
222
+ model.value.length
223
+ ) {
224
+ model.value = props.options
225
+ .filter(v => !v.category)
226
+ .map(option => option[props.optionValue]);
199
227
  }
200
228
  },
201
229
  );
202
230
 
203
- watch(model, newValue => {
204
- emit('update:modelValue', newValue);
205
- });
206
-
207
- onBeforeMount(() => {
208
- if (props.useAll && checkEmptyValue(props.modelValue[0])) {
209
- model.value = getOnlyValueArray();
210
- }
231
+ watch(model, val => {
232
+ emit('update:modelValue', val);
211
233
  });
212
234
 
213
- const search = ref('');
214
- const sSelectRef = ref(null);
215
-
216
- function handleKey(e) {
217
- if (sSelectRef.value) {
218
- sSelectRef.value.moveOptionSelection(e.key === 'ArrowDown' ? 1 : -1, true);
219
- }
220
- }
221
-
222
- function handleInput(val) {
223
- search.value = val;
224
- sSelectRef.value.setOptionIndex(-1);
225
-
226
- if (!val) {
227
- filteredOptions.value = props.options;
228
- sSelectRef.value.setOptionIndex(-1);
229
- }
230
- }
231
-
232
- function handleDelete() {
233
- sSelectRef.value.setOptionIndex(-1);
234
- search.value = '';
235
- emit('onSearch', '');
236
- }
237
-
238
- const onSearch = debounce(val => {
239
- if (!val) {
240
- filteredOptions.value = props.options;
241
- return;
242
- }
243
-
244
- // 옵션을 선택 했을 때
245
- if (sSelectRef.value.getOptionIndex() !== -1) {
246
- const selectedValue =
247
- filteredOptions.value[sSelectRef.value.getOptionIndex()][props.optionValue];
248
- if (model.value.includes(selectedValue)) {
249
- model.value = model.value.filter(v => !checkNoValue(v) && v !== selectedValue);
250
- } else {
251
- console.log('sdfsdf')
252
- model.value.push(selectedValue);
253
- }
254
- handleDelete();
255
- sSelectRef.value.setOptionIndex(-1);
256
- sSelectRef.value.hidePopup();
257
- return;
258
- }
259
-
260
- const filtered = props.options.filter(
261
- v => (v[props.optionLabel] || v).toLowerCase().indexOf(val.toLowerCase()) > -1,
262
- );
263
-
264
- if (!filtered.length) {
265
- emit('onSearch', val);
266
- sSelectRef.value.setOptionIndex(-1);
267
- return;
235
+ const allSelectedModel = () => {
236
+ if (props.useAll && model.value.filter(v => v).length < 1) {
237
+ model.value = props.options
238
+ .filter(v => !v[props.optionGroup])
239
+ .map(option => option[props.optionValue]);
268
240
  }
269
-
270
- filteredOptions.value = filtered;
271
- }, 200);
272
-
273
- const isScrolled = ref(false);
274
- const id = useId();
275
- const idClass = `s-select-checkbox-opt-${id.value}`;
276
- let scrollTimeout = null;
277
- const handleScroll = () => {
278
- const selectOptsEl = document.getElementsByClassName(idClass);
279
- nextTick(() => {
280
- if (selectOptsEl[0]) {
281
- selectOptsEl[0].addEventListener('scroll', () => {
282
- isScrolled.value = true;
283
-
284
- if (scrollTimeout) {
285
- clearTimeout(scrollTimeout);
286
- }
287
- scrollTimeout = setTimeout(() => {
288
- isScrolled.value = false;
289
- }, 100); // 100ms 후 스크롤 멈춤으로 간주
290
- });
291
- }
292
- });
293
241
  };
294
242
 
295
- const removeScroll = () => {
296
- const selectOptsEl = document.getElementsByClassName(`s-select-opt-${id.value}`);
297
- nextTick(() => {
298
- if (selectOptsEl[0]) {
299
- selectOptsEl[0].removeEventListener('scroll', () => {
300
- if (scrollTimeout) {
301
- clearTimeout(scrollTimeout);
302
- }
243
+ watch(
244
+ () => props.options,
245
+ val => {
246
+ model.value = [];
247
+ filteredOptions.value = val;
248
+ nextTick(() => {
249
+ allSelectedModel();
250
+ });
251
+ },
252
+ );
303
253
 
304
- isScrolled.value = false;
305
- });
306
- }
307
- });
308
- };
254
+ onBeforeMount(() => {
255
+ allSelectedModel();
256
+ });
309
257
 
310
258
  return {
311
259
  model,
312
260
  selectDownArrowIcon,
261
+ onRemoveChecked,
262
+ onAddChecked,
313
263
  onSearch,
264
+ isDisable,
314
265
  filteredOptions,
315
266
  search,
316
- sSelectRef,
317
- isScrolled,
318
- id,
319
- idClass,
320
- onRemoveChecked,
321
- onAddChecked,
322
- handleKey,
323
- handleInput,
324
- handleDelete,
325
- handleScroll,
326
- removeScroll,
267
+ searchInput,
268
+ searchIcon,
269
+ sSelectRef: ref(null),
327
270
  };
328
271
  },
329
272
  });
@@ -335,6 +278,7 @@
335
278
 
336
279
  .s-select-checkbox {
337
280
  @extend %select;
281
+
338
282
  .q-field__native {
339
283
  display: block;
340
284
  min-height: 0;
@@ -345,9 +289,11 @@
345
289
  white-space: nowrap;
346
290
  overflow: hidden;
347
291
  text-overflow: ellipsis;
292
+
348
293
  > span {
349
294
  max-height: $default-height;
350
295
  }
296
+
351
297
  > .s-checkbox {
352
298
  margin-right: $default-icon-margin;
353
299
  }
@@ -356,21 +302,59 @@
356
302
 
357
303
  .s-select-checkbox-opts {
358
304
  @extend %select-menu-list;
359
- > .q-virtual-scroll__content {
360
- > .q-item.disabled {
361
- border: none;
362
- color: $Grey_Darken-4 !important;
363
- opacity: 1 !important;
364
- background: $Grey_Lighten-5 !important;
365
- border-top: 1px solid $Grey_Lighten-3 !important;
366
- &:not(.q-manual-focusable):first-of-type {
367
- border-top: none !important;
368
- }
369
- }
370
- }
371
305
  }
372
306
 
373
307
  .custom-select-options {
374
308
  height: $default-height;
375
309
  }
310
+
311
+ .disabled.s-select-checkbox-option {
312
+ border: none;
313
+ background: none !important;
314
+ color: $Grey_Default !important;
315
+ }
316
+
317
+ .select-search-input-form {
318
+ height: 28px;
319
+ display: flex;
320
+ align-items: center;
321
+ padding-left: 8px;
322
+ position: relative;
323
+ border-radius: 2px;
324
+ border: 1px solid $Grey_Lighten-1;
325
+ background-color: white;
326
+ position: sticky;
327
+ top: 0;
328
+ z-index: 1;
329
+ margin: 4px;
330
+
331
+ &::after {
332
+ content: '';
333
+ position: absolute;
334
+ top: 0;
335
+ right: 0;
336
+ bottom: 0;
337
+ left: 0;
338
+ pointer-events: none;
339
+ border: 1px solid transparent;
340
+ border-radius: inherit;
341
+ }
342
+
343
+ &:hover,
344
+ &:focus-within {
345
+ &::after {
346
+ border-color: #0075ff;
347
+ box-shadow: 0 0 4px #0075ff;
348
+ transition: border-color 0.36s cubic-bezier(0.4, 0, 0.2, 1);
349
+ }
350
+ }
351
+
352
+ .select-search-input {
353
+ font-size: 12px;
354
+ margin-left: 8px;
355
+ flex-grow: 1;
356
+ border: none;
357
+ outline: none;
358
+ }
359
+ }
376
360
  </style>
@@ -29,14 +29,17 @@
29
29
  "
30
30
  >
31
31
  <template #no-data="props">
32
- <slot name="noData" v-bind="props">
32
+ <slot name="no-data" v-bind="props">
33
33
  <div class="full-width text-center">
34
34
  {{ noDataLabel }}
35
35
  </div>
36
36
  </slot>
37
37
  </template>
38
+
38
39
  <template #loading>
39
- <q-inner-loading showing color="positive" size="72px" />
40
+ <slot name="loading">
41
+ <q-inner-loading showing color="positive" size="72px" />
42
+ </slot>
40
43
  </template>
41
44
 
42
45
  <template v-for="(column, index) in columns" :key="index" #[`body-cell-${column.name}`]="props">
@@ -100,8 +103,9 @@
100
103
  </slot>
101
104
  </q-td>
102
105
  </template>
106
+
103
107
  <template v-for="(_, slotName, index) in $slots" :key="index" #[slotName]="data">
104
- <slot :name="slotName" v-bind="data"> </slot>
108
+ <slot :name="slotName" :key="`slot-${index}`" v-bind="data"> </slot>
105
109
  </template>
106
110
  </q-table>
107
111
  <s-pagination