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,597 +1,597 @@
1
- <template>
2
- <q-table
3
- flat
4
- bordered
5
- hide-pagination
6
- hide-selected-banner
7
- v-bind="$attrs"
8
- :columns="columns"
9
- :rows="rows"
10
- v-model:pagination="tablePagination"
11
- :no-data-label="noDataLabel"
12
- class="s-table"
13
- :class="{
14
- 's-select-table': $attrs.selection,
15
- 'resizable-table': resizable,
16
- 'sticky-resizable-table': stickyResizable,
17
- 'sticky-header': stickyHeader,
18
- 'before-search': !rows.length,
19
- 'horizontally-scrolled': isTableScrolled,
20
- 's-table__hover': !noHover,
21
- [`sticky-${stickyColumn.direction}-table`]: stickyColumn,
22
- }"
23
- @selection="updateSelected"
24
- ref="sTableRef"
25
- @request="
26
- props => {
27
- $emit('request', props);
28
- }
29
- "
30
- >
31
- <template #no-data="props">
32
- <slot name="no-data" v-bind="props">
33
- <div class="full-width text-center">
34
- {{ noDataLabel }}
35
- </div>
36
- </slot>
37
- </template>
38
-
39
- <template #loading>
40
- <slot name="loading">
41
- <q-inner-loading showing color="positive" size="72px" />
42
- </slot>
43
- </template>
44
-
45
- <template v-for="(column, index) in columns" :key="index" #[`body-cell-${column.name}`]="props">
46
- <q-td
47
- v-if="navigator"
48
- :class="{
49
- focused: isFocused(props),
50
- 'text-center': props.col.align === 'center',
51
- 'text-right': props.col.align === 'right',
52
- [typeof props.col.classes === 'function'
53
- ? props.col.classes(props.row)
54
- : props.col.classes]: true,
55
- [typeof props.row.class === 'function'
56
- ? props.row.class(props.row)
57
- : typeof props.row.class === 'string'
58
- ? props.row.class
59
- : '']: true,
60
- }"
61
- :style="props.col.style"
62
- @click.stop="focusCell(props)"
63
- >
64
- <slot :name="`body-cell-${column.name}-content`" v-bind="props">
65
- <s-input
66
- ref="inputRef"
67
- v-model="inputData"
68
- v-if="props.col.editable && editing && isFocused(props)"
69
- @blur="closeInput"
70
- v-bind="inputOptions"
71
- />
72
- <span v-else class="s-table-edited-td"
73
- >{{ props.value }}
74
- <q-icon
75
- size="16px"
76
- :name="editIcon"
77
- color="Blue_B_Lighten-1"
78
- class="q-ml-xs"
79
- v-if="props.col.editable"
80
- />
81
- </span>
82
- </slot>
83
- </q-td>
84
- <q-td
85
- v-else
86
- v-bind="props"
87
- :class="{
88
- 'text-center': props.col.align === 'center',
89
- 'text-right': props.col.align === 'right',
90
- [typeof props.col.classes === 'function'
91
- ? props.col.classes(props.row)
92
- : props.col.classes]: true,
93
- [typeof props.row.class === 'function'
94
- ? props.row.class(props.row)
95
- : typeof props.row.class === 'string'
96
- ? props.row.class
97
- : '']: true,
98
- }"
99
- :style="props.col.style"
100
- >
101
- <slot :name="`body-cell-${column.name}-content`" v-bind="props">
102
- {{ props.value }}
103
- </slot>
104
- </q-td>
105
- </template>
106
-
107
- <template v-for="(_, slotName, index) in $slots" :key="index" #[slotName]="data">
108
- <slot :name="slotName" :key="`slot-${index}`" v-bind="data"> </slot>
109
- </template>
110
- </q-table>
111
- <s-pagination
112
- v-if="paginationModel.rowsPerPage !== 0 && ((!hideBottom && pagesNumber > 1) || showBottom)"
113
- v-model="paginationModel.page"
114
- :lastPage="pagesNumber"
115
- class="bg-Grey_Lighten-6 s-border-radius-sm s-border-top-none s-border-Grey_Lighten-3"
116
- @update:modelValue="updatePagination"
117
- />
118
- </template>
119
-
120
- <script>
121
- import { QTable, QInnerLoading, QTd, QIcon, scroll } from 'quasar';
122
- import { defineComponent, onMounted, ref, computed, watch, nextTick } from 'vue';
123
- import { detectStickyWidth, useResizable } from '../composables/table/use-resizable';
124
- import { useNavigator } from '../composables/table/use-navigator';
125
-
126
- export default defineComponent({
127
- name: 'STable',
128
- emits: ['update:page', 'update:focused', 'field-updated', 'request'],
129
- components: {
130
- QTable,
131
- QInnerLoading,
132
- QTd,
133
- QIcon,
134
- },
135
- props: {
136
- pagination: {
137
- type: Object,
138
- default: () => ({
139
- page: 1,
140
- rowsPerPage: 50,
141
- }),
142
- },
143
- hideBottom: {
144
- type: Boolean,
145
- default: false,
146
- },
147
- showBottom: {
148
- type: Boolean,
149
- default: false,
150
- },
151
- noDataLabel: {
152
- type: String,
153
- default: '데이터 조회가 필요합니다',
154
- },
155
- columns: {
156
- type: Array,
157
- default: () => [],
158
- },
159
- rows: {
160
- type: Array,
161
- default: () => [],
162
- },
163
- resizable: {
164
- type: Boolean,
165
- default: false,
166
- },
167
- stickyHeader: {
168
- type: Boolean,
169
- default: false,
170
- },
171
- stickyResizable: {
172
- type: [Array, Number],
173
- required: false,
174
- },
175
- useSticky: {
176
- type: Boolean,
177
- default: false,
178
- },
179
- stickyColumn: {
180
- type: Object,
181
- default: () => ({
182
- count: 1, // sticky하고자하는 column 개수
183
- direction: 'left', // 'left' | 'right'
184
- }),
185
- },
186
- navigator: {
187
- type: Boolean,
188
- default: false,
189
- },
190
- inputOptions: {
191
- type: Object,
192
- default: () => ({ type: 'number' }),
193
- },
194
- focused: {
195
- type: Object,
196
- },
197
- noHover: {
198
- type: Boolean,
199
- default: false,
200
- },
201
- },
202
- setup(props, { emit, attrs }) {
203
- const tablePagination = ref(props.pagination);
204
- const paginationModel = ref(props.pagination);
205
- watch(
206
- () => props.pagination,
207
- newValue => {
208
- paginationModel.value = { ...newValue };
209
- tablePagination.value = { ...newValue };
210
- if (props.pagination.lastPage) tablePagination.value.page = 1;
211
- },
212
- { deep: true },
213
- );
214
-
215
- function updatePagination(val) {
216
- emit('update:page', val);
217
- if (!props.pagination.lastPage) {
218
- tablePagination.value.page = val;
219
- }
220
- }
221
-
222
- const sTableRef = ref(null);
223
- const { focusCell, isFocused, editing, inputRef, inputData, closeInput, editIcon } =
224
- useNavigator(props, attrs, emit);
225
- function updateSelected(details) {
226
- if (details.added && details.evt && details.evt.shiftKey) {
227
- const idx = props.rows.findIndex(r => r === details.rows[0]);
228
- const lastIdx = attrs.selected
229
- .map(x => props.rows.findIndex(y => y === x))
230
- .filter(v => v < idx)
231
- .reduce((a, b) => (Math.abs(b - idx) < Math.abs(a - idx) ? b : a));
232
- for (let i = lastIdx + 1; i < idx; i++) {
233
- attrs.selected.push(props.rows[i]);
234
- }
235
- }
236
- }
237
-
238
- // 스크롤 그림자 로직
239
- const isTableScrolled = ref(false);
240
- function detectHorizontalScroll(tableScrollArea) {
241
- const { getScrollTarget, getHorizontalScrollPosition } = scroll;
242
- const scrollTarget = getScrollTarget(tableScrollArea);
243
- const isStickyLeft = props.stickyColumn.direction === 'left';
244
-
245
- tableScrollArea.addEventListener('scroll', () => {
246
- const scrollPosition = getHorizontalScrollPosition(scrollTarget);
247
-
248
- if (isStickyLeft) {
249
- isTableScrolled.value = scrollPosition > 10; // 오른쪽 고정일 때에 스크롤 위치 계산하는 로직 수정 필요
250
- } else {
251
- isTableScrolled.value =
252
- tableScrollArea.scrollWidth - tableScrollArea.clientWidth - scrollPosition > 10;
253
- }
254
- });
255
- }
256
-
257
- function detectWindowResize(tableElement, tableScrollArea) {
258
- window.addEventListener('resize', () => {
259
- const table = tableElement.getElementsByClassName('q-table')[0];
260
-
261
- if (table.offsetWidth > tableScrollArea.offsetWidth) {
262
- isTableScrolled.value = true;
263
- } else {
264
- isTableScrolled.value = false;
265
- }
266
- });
267
- }
268
-
269
- function handleColumns() {
270
- const { addResizable, addStickyResizable, addSticky } = useResizable();
271
- const tableElement = () => sTableRef.value.$el;
272
- const isStickyLeft = props.stickyColumn.direction === 'left';
273
- const { count, direction: stickyDirection } = props.stickyColumn;
274
-
275
- if (props.useSticky) {
276
- addSticky(tableElement(), isStickyLeft, count);
277
- }
278
- if (props.resizable) {
279
- const stickyCount = props.useSticky && !isStickyLeft ? count : 0;
280
- addResizable(tableElement(), isStickyLeft, stickyCount, props.useSticky);
281
- }
282
- if (props.stickyResizable) {
283
- addStickyResizable(tableElement(), props.stickyResizable);
284
- }
285
-
286
- const tableScrollArea = tableElement().getElementsByClassName('q-table__middle')[0];
287
- const isScrollbarAppear = tableScrollArea.scrollWidth > tableScrollArea.clientWidth;
288
-
289
- if (!isStickyLeft && isScrollbarAppear) {
290
- isTableScrolled.value = true;
291
- }
292
-
293
- detectHorizontalScroll(tableScrollArea);
294
- detectWindowResize(tableElement(), tableScrollArea);
295
- }
296
-
297
- onMounted(() => {
298
- handleColumns();
299
- });
300
-
301
- watch(
302
- () => props.columns,
303
- () => {
304
- nextTick(() => {
305
- handleColumns();
306
- });
307
- },
308
- );
309
-
310
- return {
311
- sTableRef,
312
- focusCell,
313
- isFocused,
314
- editing,
315
- inputRef,
316
- inputData,
317
- closeInput,
318
- editIcon,
319
- updateSelected,
320
- isTableScrolled,
321
- tablePagination,
322
- paginationModel,
323
- pagesNumber: computed(
324
- () =>
325
- props.pagination.lastPage ||
326
- Math.ceil(props.rows.length / paginationModel.value.rowsPerPage),
327
- ),
328
- updatePagination,
329
- };
330
- },
331
- });
332
- </script>
333
-
334
- <style lang="scss">
335
- @import '../css/quasar.variables.scss';
336
- @import '../css/extends.scss';
337
-
338
- .s-table {
339
- border-radius: 8px !important;
340
- border: 1px solid $Grey_Lighten-3;
341
- &__hover {
342
- .q-table__middle {
343
- .q-table {
344
- tr {
345
- &:hover {
346
- td {
347
- background-color: $Grey_Lighten-6 !important;
348
- }
349
- }
350
- }
351
- }
352
- }
353
- }
354
- .q-table__middle {
355
- .q-table {
356
- overflow: auto;
357
- tr {
358
- th,
359
- td {
360
- &.sticky-cell {
361
- overflow: visible;
362
- &::after {
363
- content: '';
364
- background: none;
365
- height: 100%;
366
- position: absolute;
367
- top: 0;
368
- width: 20px;
369
- z-index: 100;
370
- transition: opacity 0.4s;
371
- opacity: 0;
372
- pointer-events: none;
373
- }
374
- }
375
- }
376
- }
377
- thead {
378
- background: $th-bg;
379
- min-height: 0;
380
- tr {
381
- height: 36px;
382
- color: $Grey_Darken-5;
383
- th {
384
- padding: $table-th-padding;
385
- font-size: $default-font;
386
- font-weight: $font-weight-md;
387
- word-break: keep-all;
388
- white-space: nowrap;
389
- &.sticky-cell {
390
- position: sticky;
391
- z-index: 101;
392
- background: $th-bg;
393
- }
394
-
395
- .sort-icon {
396
- color: $Grey_Lighten-1;
397
- font-size: 16px;
398
- opacity: 1;
399
- &.desc-sort {
400
- transform: rotate(180deg);
401
- }
402
- transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
403
- }
404
-
405
- // NOTE: 해당 sortable은 퀘이사에서 기본적으로 사용하는 기능
406
- // 차후 slot 방식으로 사용할경우 해당 방식으로 사용 X
407
- // slot 방식으로 사용할 경우 .sort-icon으로만 사용 예정
408
- &.sortable {
409
- > .q-table__sort-icon {
410
- //display: none;
411
- color: $Grey_Lighten-1;
412
- font-size: 16px;
413
- opacity: 1;
414
- }
415
- }
416
- }
417
- }
418
- }
419
- tbody {
420
- tr {
421
- min-height: 0px;
422
- color: $Grey_Darken-5;
423
- td {
424
- padding: $table-th-padding;
425
- font-size: $default-font;
426
- word-break: keep-all;
427
- white-space: nowrap;
428
- text-overflow: ellipsis;
429
- overflow: hidden;
430
-
431
- &:before {
432
- background: none;
433
- }
434
- &.sticky-cell {
435
- position: sticky;
436
- z-index: 1;
437
- background-color: white;
438
- }
439
- .s-table-edited-td {
440
- display: inline-flex;
441
- align-items: center;
442
- }
443
- }
444
- .focused {
445
- border: 1px solid $positive !important;
446
- }
447
- &:last-child > td {
448
- border-bottom-width: 1px;
449
- }
450
- }
451
- }
452
- }
453
- }
454
- .q-table__bottom {
455
- min-height: 0;
456
- padding: 0;
457
- border: none;
458
- &--nodata {
459
- height: 240px;
460
- color: $Grey_Default;
461
- font-size: $default-font;
462
- line-height: $default-line-height;
463
- display: block;
464
- padding-top: 80px;
465
- }
466
- }
467
- }
468
- .horizontally-scrolled {
469
- .sticky-cell::after {
470
- opacity: 1 !important;
471
- }
472
- }
473
- .s-select-table {
474
- .q-table__middle {
475
- .q-table {
476
- thead {
477
- tr {
478
- th {
479
- &:first-of-type {
480
- padding: 0 8px 0 24px;
481
- line-height: 100%;
482
- > .q-checkbox:not(.s-checkbox) {
483
- @extend %checkbox;
484
- }
485
- }
486
- }
487
- }
488
- }
489
- tbody {
490
- tr {
491
- td {
492
- &:first-of-type {
493
- padding: 0 8px 0 24px;
494
- line-height: 100%;
495
- > .q-checkbox:not(.s-checkbox) {
496
- @extend %checkbox;
497
- }
498
- }
499
- &:after {
500
- background: none;
501
- }
502
- }
503
- }
504
- }
505
- }
506
- }
507
- }
508
- .resizable-table {
509
- .q-table__middle {
510
- .q-table {
511
- thead {
512
- tr {
513
- th:last-of-type {
514
- .resiable-right {
515
- display: none;
516
- }
517
- }
518
- }
519
- }
520
- }
521
- }
522
- }
523
- .s-select-table.resizable-table {
524
- .q-table__middle {
525
- .q-table {
526
- thead {
527
- tr {
528
- th:first-of-type {
529
- > .resiable-right {
530
- display: none;
531
- }
532
- }
533
- }
534
- }
535
- }
536
- }
537
- }
538
- .sticky-resizable-table {
539
- .q-table__middle {
540
- .q-table {
541
- thead {
542
- th:first-of-type {
543
- > .resiable-left {
544
- display: none;
545
- }
546
- }
547
- }
548
- }
549
- }
550
- }
551
- .sticky-header {
552
- .q-table__middle {
553
- .q-table {
554
- thead {
555
- tr {
556
- th {
557
- background: $th-bg;
558
- position: sticky;
559
- top: 0;
560
- z-index: 100;
561
- }
562
- }
563
- }
564
- }
565
- }
566
- }
567
- .before-search {
568
- .q-table__middle {
569
- .q-table {
570
- thead {
571
- opacity: 0.4;
572
- }
573
- }
574
- }
575
- }
576
- .sticky-left-table {
577
- .sticky-cell {
578
- &::after {
579
- box-shadow: inset 12px 0 20px -25px;
580
- right: -20px;
581
- left: auto;
582
- }
583
- }
584
- }
585
- .sticky-right-table {
586
- .sticky-cell {
587
- &:not(.sticky-cell__right--first)::after {
588
- display: none;
589
- }
590
- &::after {
591
- box-shadow: inset -12px 0 20px -25px;
592
- right: auto;
593
- left: -20px;
594
- }
595
- }
596
- }
597
- </style>
1
+ <template>
2
+ <q-table
3
+ flat
4
+ bordered
5
+ hide-pagination
6
+ hide-selected-banner
7
+ v-bind="$attrs"
8
+ :columns="columns"
9
+ :rows="rows"
10
+ v-model:pagination="tablePagination"
11
+ :no-data-label="noDataLabel"
12
+ class="s-table"
13
+ :class="{
14
+ 's-select-table': $attrs.selection,
15
+ 'resizable-table': resizable,
16
+ 'sticky-resizable-table': stickyResizable,
17
+ 'sticky-header': stickyHeader,
18
+ 'before-search': !rows.length,
19
+ 'horizontally-scrolled': isTableScrolled,
20
+ 's-table__hover': !noHover,
21
+ [`sticky-${stickyColumn.direction}-table`]: stickyColumn,
22
+ }"
23
+ @selection="updateSelected"
24
+ ref="sTableRef"
25
+ @request="
26
+ props => {
27
+ $emit('request', props);
28
+ }
29
+ "
30
+ >
31
+ <template #no-data="props">
32
+ <slot name="no-data" v-bind="props">
33
+ <div class="full-width text-center">
34
+ {{ noDataLabel }}
35
+ </div>
36
+ </slot>
37
+ </template>
38
+
39
+ <template #loading>
40
+ <slot name="loading">
41
+ <q-inner-loading showing color="positive" size="72px" />
42
+ </slot>
43
+ </template>
44
+
45
+ <template v-for="(column, index) in columns" :key="index" #[`body-cell-${column.name}`]="props">
46
+ <q-td
47
+ v-if="navigator"
48
+ :class="{
49
+ focused: isFocused(props),
50
+ 'text-center': props.col.align === 'center',
51
+ 'text-right': props.col.align === 'right',
52
+ [typeof props.col.classes === 'function'
53
+ ? props.col.classes(props.row)
54
+ : props.col.classes]: true,
55
+ [typeof props.row.class === 'function'
56
+ ? props.row.class(props.row)
57
+ : typeof props.row.class === 'string'
58
+ ? props.row.class
59
+ : '']: true,
60
+ }"
61
+ :style="props.col.style"
62
+ @click.stop="focusCell(props)"
63
+ >
64
+ <slot :name="`body-cell-${column.name}-content`" v-bind="props">
65
+ <s-input
66
+ ref="inputRef"
67
+ v-model="inputData"
68
+ v-if="props.col.editable && editing && isFocused(props)"
69
+ @blur="closeInput"
70
+ v-bind="inputOptions"
71
+ />
72
+ <span v-else class="s-table-edited-td"
73
+ >{{ props.value }}
74
+ <q-icon
75
+ size="16px"
76
+ :name="editIcon"
77
+ color="Blue_B_Lighten-1"
78
+ class="q-ml-xs"
79
+ v-if="props.col.editable"
80
+ />
81
+ </span>
82
+ </slot>
83
+ </q-td>
84
+ <q-td
85
+ v-else
86
+ v-bind="props"
87
+ :class="{
88
+ 'text-center': props.col.align === 'center',
89
+ 'text-right': props.col.align === 'right',
90
+ [typeof props.col.classes === 'function'
91
+ ? props.col.classes(props.row)
92
+ : props.col.classes]: true,
93
+ [typeof props.row.class === 'function'
94
+ ? props.row.class(props.row)
95
+ : typeof props.row.class === 'string'
96
+ ? props.row.class
97
+ : '']: true,
98
+ }"
99
+ :style="props.col.style"
100
+ >
101
+ <slot :name="`body-cell-${column.name}-content`" v-bind="props">
102
+ {{ props.value }}
103
+ </slot>
104
+ </q-td>
105
+ </template>
106
+
107
+ <template v-for="(_, slotName, index) in $slots" :key="index" #[slotName]="data">
108
+ <slot :name="slotName" :key="`slot-${index}`" v-bind="data"> </slot>
109
+ </template>
110
+ </q-table>
111
+ <s-pagination
112
+ v-if="paginationModel.rowsPerPage !== 0 && ((!hideBottom && pagesNumber > 1) || showBottom)"
113
+ v-model="paginationModel.page"
114
+ :lastPage="pagesNumber"
115
+ class="bg-Grey_Lighten-6 s-border-radius-sm s-border-top-none s-border-Grey_Lighten-3"
116
+ @update:modelValue="updatePagination"
117
+ />
118
+ </template>
119
+
120
+ <script>
121
+ import { QTable, QInnerLoading, QTd, QIcon, scroll } from 'quasar';
122
+ import { defineComponent, onMounted, ref, computed, watch, nextTick } from 'vue';
123
+ import { detectStickyWidth, useResizable } from '../composables/table/use-resizable';
124
+ import { useNavigator } from '../composables/table/use-navigator';
125
+
126
+ export default defineComponent({
127
+ name: 'STable',
128
+ emits: ['update:page', 'update:focused', 'field-updated', 'request'],
129
+ components: {
130
+ QTable,
131
+ QInnerLoading,
132
+ QTd,
133
+ QIcon,
134
+ },
135
+ props: {
136
+ pagination: {
137
+ type: Object,
138
+ default: () => ({
139
+ page: 1,
140
+ rowsPerPage: 50,
141
+ }),
142
+ },
143
+ hideBottom: {
144
+ type: Boolean,
145
+ default: false,
146
+ },
147
+ showBottom: {
148
+ type: Boolean,
149
+ default: false,
150
+ },
151
+ noDataLabel: {
152
+ type: String,
153
+ default: '데이터 조회가 필요합니다',
154
+ },
155
+ columns: {
156
+ type: Array,
157
+ default: () => [],
158
+ },
159
+ rows: {
160
+ type: Array,
161
+ default: () => [],
162
+ },
163
+ resizable: {
164
+ type: Boolean,
165
+ default: false,
166
+ },
167
+ stickyHeader: {
168
+ type: Boolean,
169
+ default: false,
170
+ },
171
+ stickyResizable: {
172
+ type: [Array, Number],
173
+ required: false,
174
+ },
175
+ useSticky: {
176
+ type: Boolean,
177
+ default: false,
178
+ },
179
+ stickyColumn: {
180
+ type: Object,
181
+ default: () => ({
182
+ count: 1, // sticky하고자하는 column 개수
183
+ direction: 'left', // 'left' | 'right'
184
+ }),
185
+ },
186
+ navigator: {
187
+ type: Boolean,
188
+ default: false,
189
+ },
190
+ inputOptions: {
191
+ type: Object,
192
+ default: () => ({ type: 'number' }),
193
+ },
194
+ focused: {
195
+ type: Object,
196
+ },
197
+ noHover: {
198
+ type: Boolean,
199
+ default: false,
200
+ },
201
+ },
202
+ setup(props, { emit, attrs }) {
203
+ const tablePagination = ref(props.pagination);
204
+ const paginationModel = ref(props.pagination);
205
+ watch(
206
+ () => props.pagination,
207
+ newValue => {
208
+ paginationModel.value = { ...newValue };
209
+ tablePagination.value = { ...newValue };
210
+ if (props.pagination.lastPage) tablePagination.value.page = 1;
211
+ },
212
+ { deep: true },
213
+ );
214
+
215
+ function updatePagination(val) {
216
+ emit('update:page', val);
217
+ if (!props.pagination.lastPage) {
218
+ tablePagination.value.page = val;
219
+ }
220
+ }
221
+
222
+ const sTableRef = ref(null);
223
+ const { focusCell, isFocused, editing, inputRef, inputData, closeInput, editIcon } =
224
+ useNavigator(props, attrs, emit);
225
+ function updateSelected(details) {
226
+ if (details.added && details.evt && details.evt.shiftKey) {
227
+ const idx = props.rows.findIndex(r => r === details.rows[0]);
228
+ const lastIdx = attrs.selected
229
+ .map(x => props.rows.findIndex(y => y === x))
230
+ .filter(v => v < idx)
231
+ .reduce((a, b) => (Math.abs(b - idx) < Math.abs(a - idx) ? b : a));
232
+ for (let i = lastIdx + 1; i < idx; i++) {
233
+ attrs.selected.push(props.rows[i]);
234
+ }
235
+ }
236
+ }
237
+
238
+ // 스크롤 그림자 로직
239
+ const isTableScrolled = ref(false);
240
+ function detectHorizontalScroll(tableScrollArea) {
241
+ const { getScrollTarget, getHorizontalScrollPosition } = scroll;
242
+ const scrollTarget = getScrollTarget(tableScrollArea);
243
+ const isStickyLeft = props.stickyColumn.direction === 'left';
244
+
245
+ tableScrollArea.addEventListener('scroll', () => {
246
+ const scrollPosition = getHorizontalScrollPosition(scrollTarget);
247
+
248
+ if (isStickyLeft) {
249
+ isTableScrolled.value = scrollPosition > 10; // 오른쪽 고정일 때에 스크롤 위치 계산하는 로직 수정 필요
250
+ } else {
251
+ isTableScrolled.value =
252
+ tableScrollArea.scrollWidth - tableScrollArea.clientWidth - scrollPosition > 10;
253
+ }
254
+ });
255
+ }
256
+
257
+ function detectWindowResize(tableElement, tableScrollArea) {
258
+ window.addEventListener('resize', () => {
259
+ const table = tableElement.getElementsByClassName('q-table')[0];
260
+
261
+ if (table.offsetWidth > tableScrollArea.offsetWidth) {
262
+ isTableScrolled.value = true;
263
+ } else {
264
+ isTableScrolled.value = false;
265
+ }
266
+ });
267
+ }
268
+
269
+ function handleColumns() {
270
+ const { addResizable, addStickyResizable, addSticky } = useResizable();
271
+ const tableElement = () => sTableRef.value.$el;
272
+ const isStickyLeft = props.stickyColumn.direction === 'left';
273
+ const { count, direction: stickyDirection } = props.stickyColumn;
274
+
275
+ if (props.useSticky) {
276
+ addSticky(tableElement(), isStickyLeft, count);
277
+ }
278
+ if (props.resizable) {
279
+ const stickyCount = props.useSticky && !isStickyLeft ? count : 0;
280
+ addResizable(tableElement(), isStickyLeft, stickyCount, props.useSticky);
281
+ }
282
+ if (props.stickyResizable) {
283
+ addStickyResizable(tableElement(), props.stickyResizable);
284
+ }
285
+
286
+ const tableScrollArea = tableElement().getElementsByClassName('q-table__middle')[0];
287
+ const isScrollbarAppear = tableScrollArea.scrollWidth > tableScrollArea.clientWidth;
288
+
289
+ if (!isStickyLeft && isScrollbarAppear) {
290
+ isTableScrolled.value = true;
291
+ }
292
+
293
+ detectHorizontalScroll(tableScrollArea);
294
+ detectWindowResize(tableElement(), tableScrollArea);
295
+ }
296
+
297
+ onMounted(() => {
298
+ handleColumns();
299
+ });
300
+
301
+ watch(
302
+ () => props.columns,
303
+ () => {
304
+ nextTick(() => {
305
+ handleColumns();
306
+ });
307
+ },
308
+ );
309
+
310
+ return {
311
+ sTableRef,
312
+ focusCell,
313
+ isFocused,
314
+ editing,
315
+ inputRef,
316
+ inputData,
317
+ closeInput,
318
+ editIcon,
319
+ updateSelected,
320
+ isTableScrolled,
321
+ tablePagination,
322
+ paginationModel,
323
+ pagesNumber: computed(
324
+ () =>
325
+ props.pagination.lastPage ||
326
+ Math.ceil(props.rows.length / paginationModel.value.rowsPerPage),
327
+ ),
328
+ updatePagination,
329
+ };
330
+ },
331
+ });
332
+ </script>
333
+
334
+ <style lang="scss">
335
+ @import '../css/quasar.variables.scss';
336
+ @import '../css/extends.scss';
337
+
338
+ .s-table {
339
+ border-radius: 8px !important;
340
+ border: 1px solid $Grey_Lighten-3;
341
+ &__hover {
342
+ .q-table__middle {
343
+ .q-table {
344
+ tr {
345
+ &:hover {
346
+ td {
347
+ background-color: $Grey_Lighten-6 !important;
348
+ }
349
+ }
350
+ }
351
+ }
352
+ }
353
+ }
354
+ .q-table__middle {
355
+ .q-table {
356
+ overflow: auto;
357
+ tr {
358
+ th,
359
+ td {
360
+ &.sticky-cell {
361
+ overflow: visible;
362
+ &::after {
363
+ content: '';
364
+ background: none;
365
+ height: 100%;
366
+ position: absolute;
367
+ top: 0;
368
+ width: 20px;
369
+ z-index: 100;
370
+ transition: opacity 0.4s;
371
+ opacity: 0;
372
+ pointer-events: none;
373
+ }
374
+ }
375
+ }
376
+ }
377
+ thead {
378
+ background: $th-bg;
379
+ min-height: 0;
380
+ tr {
381
+ height: 36px;
382
+ color: $Grey_Darken-5;
383
+ th {
384
+ padding: $table-th-padding;
385
+ font-size: $default-font;
386
+ font-weight: $font-weight-md;
387
+ word-break: keep-all;
388
+ white-space: nowrap;
389
+ &.sticky-cell {
390
+ position: sticky;
391
+ z-index: 101;
392
+ background: $th-bg;
393
+ }
394
+
395
+ .sort-icon {
396
+ color: $Grey_Lighten-1;
397
+ font-size: 16px;
398
+ opacity: 1;
399
+ &.desc-sort {
400
+ transform: rotate(180deg);
401
+ }
402
+ transition: transform 0.3s cubic-bezier(0.25, 0.8, 0.5, 1);
403
+ }
404
+
405
+ // NOTE: 해당 sortable은 퀘이사에서 기본적으로 사용하는 기능
406
+ // 차후 slot 방식으로 사용할경우 해당 방식으로 사용 X
407
+ // slot 방식으로 사용할 경우 .sort-icon으로만 사용 예정
408
+ &.sortable {
409
+ > .q-table__sort-icon {
410
+ //display: none;
411
+ color: $Grey_Lighten-1;
412
+ font-size: 16px;
413
+ opacity: 1;
414
+ }
415
+ }
416
+ }
417
+ }
418
+ }
419
+ tbody {
420
+ tr {
421
+ min-height: 0px;
422
+ color: $Grey_Darken-5;
423
+ td {
424
+ padding: $table-th-padding;
425
+ font-size: $default-font;
426
+ word-break: keep-all;
427
+ white-space: nowrap;
428
+ text-overflow: ellipsis;
429
+ overflow: hidden;
430
+
431
+ &:before {
432
+ background: none;
433
+ }
434
+ &.sticky-cell {
435
+ position: sticky;
436
+ z-index: 1;
437
+ background-color: white;
438
+ }
439
+ .s-table-edited-td {
440
+ display: inline-flex;
441
+ align-items: center;
442
+ }
443
+ }
444
+ .focused {
445
+ border: 1px solid $positive !important;
446
+ }
447
+ &:last-child > td {
448
+ border-bottom-width: 1px;
449
+ }
450
+ }
451
+ }
452
+ }
453
+ }
454
+ .q-table__bottom {
455
+ min-height: 0;
456
+ padding: 0;
457
+ border: none;
458
+ &--nodata {
459
+ height: 240px;
460
+ color: $Grey_Default;
461
+ font-size: $default-font;
462
+ line-height: $default-line-height;
463
+ display: block;
464
+ padding-top: 80px;
465
+ }
466
+ }
467
+ }
468
+ .horizontally-scrolled {
469
+ .sticky-cell::after {
470
+ opacity: 1 !important;
471
+ }
472
+ }
473
+ .s-select-table {
474
+ .q-table__middle {
475
+ .q-table {
476
+ thead {
477
+ tr {
478
+ th {
479
+ &:first-of-type {
480
+ padding: 0 8px 0 24px;
481
+ line-height: 100%;
482
+ > .q-checkbox:not(.s-checkbox) {
483
+ @extend %checkbox;
484
+ }
485
+ }
486
+ }
487
+ }
488
+ }
489
+ tbody {
490
+ tr {
491
+ td {
492
+ &:first-of-type {
493
+ padding: 0 8px 0 24px;
494
+ line-height: 100%;
495
+ > .q-checkbox:not(.s-checkbox) {
496
+ @extend %checkbox;
497
+ }
498
+ }
499
+ &:after {
500
+ background: none;
501
+ }
502
+ }
503
+ }
504
+ }
505
+ }
506
+ }
507
+ }
508
+ .resizable-table {
509
+ .q-table__middle {
510
+ .q-table {
511
+ thead {
512
+ tr {
513
+ th:last-of-type {
514
+ .resiable-right {
515
+ display: none;
516
+ }
517
+ }
518
+ }
519
+ }
520
+ }
521
+ }
522
+ }
523
+ .s-select-table.resizable-table {
524
+ .q-table__middle {
525
+ .q-table {
526
+ thead {
527
+ tr {
528
+ th:first-of-type {
529
+ > .resiable-right {
530
+ display: none;
531
+ }
532
+ }
533
+ }
534
+ }
535
+ }
536
+ }
537
+ }
538
+ .sticky-resizable-table {
539
+ .q-table__middle {
540
+ .q-table {
541
+ thead {
542
+ th:first-of-type {
543
+ > .resiable-left {
544
+ display: none;
545
+ }
546
+ }
547
+ }
548
+ }
549
+ }
550
+ }
551
+ .sticky-header {
552
+ .q-table__middle {
553
+ .q-table {
554
+ thead {
555
+ tr {
556
+ th {
557
+ background: $th-bg;
558
+ position: sticky;
559
+ top: 0;
560
+ z-index: 100;
561
+ }
562
+ }
563
+ }
564
+ }
565
+ }
566
+ }
567
+ .before-search {
568
+ .q-table__middle {
569
+ .q-table {
570
+ thead {
571
+ opacity: 0.4;
572
+ }
573
+ }
574
+ }
575
+ }
576
+ .sticky-left-table {
577
+ .sticky-cell {
578
+ &::after {
579
+ box-shadow: inset 12px 0 20px -25px;
580
+ right: -20px;
581
+ left: auto;
582
+ }
583
+ }
584
+ }
585
+ .sticky-right-table {
586
+ .sticky-cell {
587
+ &:not(.sticky-cell__right--first)::after {
588
+ display: none;
589
+ }
590
+ &::after {
591
+ box-shadow: inset -12px 0 20px -25px;
592
+ right: auto;
593
+ left: -20px;
594
+ }
595
+ }
596
+ }
597
+ </style>