evui 3.3.36 → 3.3.39

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.
Files changed (141) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +40 -40
  3. package/dist/evui.common.js +1907 -1832
  4. package/dist/evui.common.js.map +1 -1
  5. package/dist/evui.umd.js +1907 -1832
  6. package/dist/evui.umd.js.map +1 -1
  7. package/dist/evui.umd.min.js +1 -1
  8. package/dist/evui.umd.min.js.map +1 -1
  9. package/dist/img/{EVUI.7f3588fb.svg → EVUI.b82ee81a.svg} +292 -292
  10. package/dist/img/{icon_mysql.7ea26d5d.svg → icon_mysql.1085fdc9.svg} +78 -78
  11. package/dist/img/{icon_oracle.9009b108.svg → icon_oracle.0572d3ee.svg} +13 -13
  12. package/dist/img/{icon_postgresql.f8fffba9.svg → icon_postgresql.ee12bde8.svg} +58 -58
  13. package/package.json +61 -61
  14. package/src/common/emitter.js +20 -20
  15. package/src/common/utils.debounce.js +223 -223
  16. package/src/common/utils.js +134 -134
  17. package/src/common/utils.table.js +78 -78
  18. package/src/common/utils.throttle.js +83 -83
  19. package/src/common/utils.tree.js +18 -18
  20. package/src/components/button/Button.vue +198 -198
  21. package/src/components/button/index.js +7 -7
  22. package/src/components/buttonGroup/ButtonGroup.vue +11 -11
  23. package/src/components/buttonGroup/index.js +7 -7
  24. package/src/components/calendar/Calendar.vue +661 -661
  25. package/src/components/calendar/index.js +7 -7
  26. package/src/components/calendar/uses.js +1272 -1272
  27. package/src/components/chart/Chart.vue +189 -192
  28. package/src/components/chart/chart.core.js +870 -870
  29. package/src/components/chart/element/element.bar.js +524 -524
  30. package/src/components/chart/element/element.bar.time.js +156 -156
  31. package/src/components/chart/element/element.heatmap.js +533 -533
  32. package/src/components/chart/element/element.line.js +339 -339
  33. package/src/components/chart/element/element.pie.js +197 -197
  34. package/src/components/chart/element/element.scatter.js +184 -184
  35. package/src/components/chart/element/element.tip.js +550 -542
  36. package/src/components/chart/helpers/helpers.canvas.js +265 -265
  37. package/src/components/chart/helpers/helpers.constant.js +206 -206
  38. package/src/components/chart/helpers/helpers.util.js +346 -338
  39. package/src/components/chart/index.js +9 -9
  40. package/src/components/chart/model/index.js +4 -4
  41. package/src/components/chart/model/model.series.js +93 -93
  42. package/src/components/chart/model/model.store.js +977 -967
  43. package/src/components/chart/plugins/plugins.interaction.js +769 -769
  44. package/src/components/chart/plugins/plugins.legend.gradient.js +602 -602
  45. package/src/components/chart/plugins/plugins.legend.js +1155 -1151
  46. package/src/components/chart/plugins/plugins.pie.js +254 -254
  47. package/src/components/chart/plugins/plugins.title.js +56 -56
  48. package/src/components/chart/plugins/plugins.tooltip.js +692 -692
  49. package/src/components/chart/scale/scale.js +848 -848
  50. package/src/components/chart/scale/scale.linear.js +38 -38
  51. package/src/components/chart/scale/scale.logarithmic.js +128 -128
  52. package/src/components/chart/scale/scale.step.js +336 -336
  53. package/src/components/chart/scale/scale.time.category.js +277 -277
  54. package/src/components/chart/scale/scale.time.js +48 -48
  55. package/src/components/chart/style/chart.scss +312 -312
  56. package/src/components/chart/uses.js +264 -252
  57. package/src/components/checkbox/Checkbox.vue +200 -200
  58. package/src/components/checkbox/index.js +7 -7
  59. package/src/components/checkboxGroup/CheckboxGroup.vue +44 -44
  60. package/src/components/checkboxGroup/index.js +7 -7
  61. package/src/components/contextMenu/ContextMenu.vue +80 -80
  62. package/src/components/contextMenu/MenuList.vue +149 -149
  63. package/src/components/contextMenu/index.js +7 -7
  64. package/src/components/contextMenu/uses.js +203 -203
  65. package/src/components/datePicker/DatePicker.vue +437 -437
  66. package/src/components/datePicker/index.js +7 -7
  67. package/src/components/datePicker/uses.js +419 -419
  68. package/src/components/grid/Grid.vue +827 -827
  69. package/src/components/grid/grid.filter.window.vue +493 -493
  70. package/src/components/grid/grid.pagination.vue +75 -75
  71. package/src/components/grid/grid.summary.vue +265 -265
  72. package/src/components/grid/grid.toolbar.vue +26 -26
  73. package/src/components/grid/index.js +11 -11
  74. package/src/components/grid/style/grid.scss +263 -263
  75. package/src/components/grid/uses.js +1002 -1007
  76. package/src/components/icon/Icon.vue +49 -49
  77. package/src/components/icon/index.js +8 -8
  78. package/src/components/inputNumber/InputNumber.vue +212 -212
  79. package/src/components/inputNumber/index.js +7 -7
  80. package/src/components/inputNumber/uses.js +217 -217
  81. package/src/components/loading/Loading.vue +125 -125
  82. package/src/components/loading/index.js +7 -7
  83. package/src/components/menu/Menu.vue +68 -68
  84. package/src/components/menu/MenuItem.vue +187 -187
  85. package/src/components/menu/index.js +7 -7
  86. package/src/components/message/Message.vue +223 -223
  87. package/src/components/message/index.js +31 -31
  88. package/src/components/messageBox/MessageBox.vue +358 -358
  89. package/src/components/messageBox/index.js +22 -22
  90. package/src/components/notification/Notification.vue +316 -316
  91. package/src/components/notification/index.js +49 -49
  92. package/src/components/pagination/Pagination.vue +271 -271
  93. package/src/components/pagination/index.js +7 -7
  94. package/src/components/pagination/pageButton.vue +30 -30
  95. package/src/components/progress/Progress.vue +139 -139
  96. package/src/components/progress/index.js +7 -7
  97. package/src/components/radio/Radio.vue +159 -159
  98. package/src/components/radio/index.js +7 -7
  99. package/src/components/radioGroup/RadioGroup.vue +41 -41
  100. package/src/components/radioGroup/index.js +7 -7
  101. package/src/components/scheduler/Scheduler.vue +149 -149
  102. package/src/components/scheduler/index.js +7 -7
  103. package/src/components/scheduler/uses.js +183 -183
  104. package/src/components/select/Select.vue +440 -440
  105. package/src/components/select/index.js +7 -7
  106. package/src/components/select/uses.js +270 -270
  107. package/src/components/slider/Slider.vue +505 -505
  108. package/src/components/slider/index.js +7 -7
  109. package/src/components/slider/uses.js +390 -390
  110. package/src/components/tabPanel/TabPanel.vue +74 -74
  111. package/src/components/tabPanel/index.js +7 -7
  112. package/src/components/tabs/Tabs.vue +517 -517
  113. package/src/components/tabs/index.js +7 -7
  114. package/src/components/textField/TextField.vue +375 -375
  115. package/src/components/textField/index.js +7 -7
  116. package/src/components/timePicker/TimePicker.vue +352 -352
  117. package/src/components/timePicker/index.js +7 -7
  118. package/src/components/toggle/Toggle.vue +115 -115
  119. package/src/components/toggle/index.js +7 -7
  120. package/src/components/tree/Tree.vue +313 -313
  121. package/src/components/tree/TreeNode.vue +293 -293
  122. package/src/components/tree/index.js +7 -7
  123. package/src/components/treeGrid/TreeGrid.vue +758 -758
  124. package/src/components/treeGrid/TreeGridNode.vue +275 -275
  125. package/src/components/treeGrid/index.js +9 -9
  126. package/src/components/treeGrid/style/treeGrid.scss +261 -261
  127. package/src/components/treeGrid/treeGrid.toolbar.vue +26 -26
  128. package/src/components/treeGrid/uses.js +867 -867
  129. package/src/components/window/Window.vue +329 -329
  130. package/src/components/window/index.js +7 -7
  131. package/src/components/window/uses.js +899 -899
  132. package/src/directives/clickoutside.js +90 -90
  133. package/src/main.js +116 -116
  134. package/src/style/components/input.scss +108 -108
  135. package/src/style/functions.scss +3 -3
  136. package/src/style/index.scss +6 -6
  137. package/src/style/lib/fonts/EVUI.svg +292 -292
  138. package/src/style/lib/icon.css +888 -888
  139. package/src/style/mixins.scss +94 -94
  140. package/src/style/themes.scss +67 -67
  141. package/src/style/variables.scss +22 -22
@@ -1,517 +1,517 @@
1
- <template>
2
- <section
3
- v-resize="onResize"
4
- v-observe-visibility="{
5
- callback: onResize,
6
- once: true,
7
- }"
8
- class="ev-tabs"
9
- :class="{
10
- closable,
11
- stretch,
12
- }"
13
- >
14
- <div class="ev-tabs-header">
15
- <div
16
- class="ev-tabs-nav-wrapper"
17
- :class="{
18
- 'has-scroll': hasScroll,
19
- }"
20
- >
21
- <template v-if="hasScroll">
22
- <span
23
- class="ev-tabs-arrow prev"
24
- @click="scrollTab('prev')"
25
- >
26
- <i class="ev-icon-s-arrow-left" />
27
- </span>
28
- <span
29
- class="ev-tabs-arrow next"
30
- @click="scrollTab('next')"
31
- >
32
- <i class="ev-icon-s-arrow-right" />
33
- </span>
34
- </template>
35
- <div
36
- ref="listWrapperRef"
37
- class="ev-tabs-list-wrapper"
38
- >
39
- <ul
40
- ref="listRef"
41
- class="ev-tabs-list"
42
- :style="listRefStyle"
43
- >
44
- <li
45
- v-for="(item, idx) in computedTabList"
46
- :key="`${item.value}_${idx}`"
47
- class="ev-tabs-title"
48
- v-bind="{ draggable }"
49
- :class="{
50
- active: item.value === mv,
51
- 'has-icon': item.iconClass,
52
- 'drag-select': dragSelectCls(item.value),
53
- 'select-idx': selectIdxCls(idx),
54
- }"
55
- @click="clickTab(item.value)"
56
- @dragstart.stop="dragstartTab(item, idx)"
57
- @dragover.prevent="dragoverTab(item.value)"
58
- @dragend.prevent="dragendTab"
59
- >
60
- <i
61
- v-if="item.iconClass"
62
- class="ev-tabs-icon"
63
- :class="item.iconClass"
64
- />
65
- <span
66
- class="text"
67
- :title="item.text"
68
- >
69
- {{ item.text }}
70
- </span>
71
- <span
72
- v-if="closable"
73
- class="close-icon"
74
- @click.stop="removeTab(item.value)"
75
- >
76
- <i class="ev-icon-s-close" />
77
- </span>
78
- </li>
79
- </ul>
80
- </div>
81
- </div>
82
- </div>
83
- <div class="ev-tabs-body">
84
- <slot />
85
- </div>
86
- </section>
87
- </template>
88
-
89
- <script>
90
- import {
91
- ref, reactive, computed,
92
- provide, triggerRef,
93
- onBeforeUpdate, nextTick,
94
- } from 'vue';
95
-
96
- export default {
97
- name: 'EvTabs',
98
- props: {
99
- modelValue: {
100
- type: [String, Number],
101
- default: null,
102
- },
103
- panels: {
104
- type: Array,
105
- default: () => [],
106
- validator: (list) => {
107
- const valueList = list.map(v => v.value);
108
- const setList = [...new Set(valueList)];
109
- if (list.length !== setList.length) {
110
- console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is duplicate values.');
111
- return false;
112
- }
113
- if (!list.every(v => Object.hasOwnProperty.call(v, 'value'))) {
114
- console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is essential.');
115
- return false;
116
- }
117
- return true;
118
- },
119
- },
120
- closable: {
121
- type: Boolean,
122
- default: false,
123
- },
124
- stretch: {
125
- type: Boolean,
126
- default: false,
127
- },
128
- draggable: {
129
- type: Boolean,
130
- default: false,
131
- },
132
- },
133
- emits: {
134
- 'update:modelValue': [String, Number],
135
- 'update:panels': [Array],
136
- change: [String, Number],
137
- },
138
- setup(props, { emit }) {
139
- // 드래그 상태 여부 (dragstart ~ dragend)
140
- const isDragState = ref(false);
141
-
142
- const tabCloneList = ref([]);
143
-
144
- const mv = computed({
145
- get: () => props.modelValue,
146
- set: (val) => {
147
- emit('update:modelValue', val);
148
- emit('change', val);
149
- },
150
- });
151
-
152
- provide('evTabs', mv);
153
-
154
- const tabList = computed({
155
- get: () => props.panels,
156
- set: val => emit('update:panels', val),
157
- });
158
- const computedTabList = computed(() => {
159
- if (!props.draggable) {
160
- return tabList.value;
161
- }
162
- if (!isDragState.value) {
163
- return tabList.value;
164
- }
165
- return tabCloneList.value;
166
- });
167
- const tabElValueList = tabList.value.map(v => v.value);
168
-
169
- const listWrapperRef = ref(null);
170
- const listRef = ref(null);
171
- const hasScroll = ref(false);
172
-
173
- const translateScroll = reactive({
174
- x: 0,
175
- });
176
- const listRefStyle = computed(() => ({
177
- transform: `translateX(${translateScroll.x}px)`,
178
- }));
179
-
180
- /**
181
- * 상단 탭 nav의 element 길이를 감시 및 계산하여 스크롤 여부 확인
182
- * UL의 길이가 긴 경우 양쪽에 버튼 노출
183
- */
184
- const observeListEl = () => {
185
- const listWrapperWidth = listWrapperRef.value.offsetWidth;
186
- const listWidth = listRef.value.offsetWidth;
187
- hasScroll.value = listWrapperWidth < listWidth;
188
-
189
- if (hasScroll.value) {
190
- const widthLimit = listWrapperWidth - listWidth;
191
- if (widthLimit > translateScroll.x) {
192
- translateScroll.x = widthLimit;
193
- }
194
- } else {
195
- translateScroll.x = 0;
196
- }
197
- };
198
-
199
- onBeforeUpdate(() => {
200
- // 삭제된 탭이 선택된 경우 탭선택 인덱스를 변경하는 로직
201
- if (tabElValueList.length === tabList.value.length + 1) {
202
- let longList;
203
- let shortList;
204
- if (tabElValueList.length > tabList.value.length) {
205
- longList = tabElValueList;
206
- shortList = tabList.value.map(v => v.value);
207
- } else {
208
- longList = tabList.value.map(v => v.value);
209
- shortList = tabElValueList;
210
- }
211
- const removeValue = longList.filter(v => !shortList.includes(v))[0];
212
- if (mv.value === removeValue) {
213
- const selectedIdx = tabElValueList.findIndex(v => v === removeValue);
214
- if (selectedIdx === 0) {
215
- mv.value = tabList.value[0].value;
216
- } else {
217
- mv.value = tabList.value[selectedIdx - 1].value;
218
- }
219
- }
220
- }
221
- });
222
-
223
- /**
224
- * 탭 클릭 로직
225
- */
226
- const clickTab = (val) => {
227
- mv.value = val;
228
- };
229
-
230
- /**
231
- * 탭 삭제 로직
232
- */
233
- const removeTab = (val) => {
234
- if (tabList.value.length < 2) {
235
- return;
236
- }
237
- const selectedIdx = tabList.value.findIndex(v => v.value === val);
238
- if (selectedIdx < 0) {
239
- mv.value = tabList.value[0].value;
240
- return;
241
- }
242
- if (val === mv.value) {
243
- if (selectedIdx === 0) {
244
- mv.value = tabList.value[1].value;
245
- } else {
246
- mv.value = tabList.value[selectedIdx - 1].value;
247
- }
248
- }
249
- tabList.value.splice(selectedIdx, 1);
250
- nextTick(() => {
251
- tabElValueList.splice(selectedIdx, 1);
252
- });
253
- triggerRef(tabList);
254
- };
255
-
256
- /**
257
- * tab nav위에서 마우스 휠 동작
258
- * @param type - {'next'|'prev'}
259
- * @param movingWidth
260
- */
261
- const scrollTab = (type, movingWidth = 100) => {
262
- const listWrapperWidth = listWrapperRef.value.offsetWidth;
263
- const listWidth = listRef.value.offsetWidth;
264
- const widthLimit = listWrapperWidth - listWidth;
265
- if (type === 'next' && translateScroll.x !== widthLimit) {
266
- if (widthLimit >= translateScroll.x - movingWidth) {
267
- translateScroll.x = widthLimit;
268
- } else {
269
- translateScroll.x -= movingWidth;
270
- }
271
- } else if (type === 'prev' && translateScroll.x !== 0) {
272
- if (movingWidth * -1 <= translateScroll.x) {
273
- translateScroll.x = 0;
274
- } else {
275
- translateScroll.x += movingWidth;
276
- }
277
- }
278
- };
279
-
280
- // draggable 모드에서 drag되는 아이템
281
- const dragObj = reactive({
282
- item: {},
283
- idx: null,
284
- });
285
-
286
- /**
287
- * 드래그된 LI의 클래스
288
- * @param val
289
- * @returns {boolean|boolean}
290
- */
291
- const dragSelectCls = val => props.draggable && dragObj.item?.value === val;
292
-
293
- /**
294
- * 드래그하기위해 선택한 li의 idx 여부 클래스
295
- */
296
- const selectIdxCls = idx => props.draggable && dragObj.idx === idx;
297
-
298
- /**
299
- * 탭 드래그 시작 메소드, isDragState모드 시작
300
- * @param item - 선택한 아이템
301
- */
302
- const dragstartTab = (item, idx) => {
303
- if (!props.draggable) {
304
- return;
305
- }
306
- tabCloneList.value = [...tabList.value];
307
- dragObj.item = item;
308
- dragObj.idx = idx;
309
- isDragState.value = true;
310
- };
311
-
312
- /**
313
- * 탭 드래그오버 메소드
314
- * @param val - 오버 중인 아이템의 value
315
- */
316
- const dragoverTab = (val) => {
317
- if (!props.draggable || dragObj.item?.value === val) {
318
- return;
319
- }
320
- const dragValueIdx = tabCloneList.value.findIndex(v => v.value === dragObj.item?.value);
321
- const targetValueIdx = tabCloneList.value.findIndex(v => v.value === val);
322
- tabCloneList.value.splice(dragValueIdx, 1);
323
- tabCloneList.value.splice(targetValueIdx, 0, dragObj.item);
324
- };
325
-
326
- /**
327
- * 탭 드래그 종료 메소드, 원래 tabList에 값을 넣고 isDragState모드를 종료
328
- */
329
- const dragendTab = () => {
330
- if (!props.draggable) {
331
- return;
332
- }
333
- tabList.value = [...tabCloneList.value];
334
- dragObj.item = {};
335
- dragObj.idx = null;
336
- isDragState.value = false;
337
- tabCloneList.value.splice(0);
338
- };
339
-
340
- const onResize = () => {
341
- observeListEl();
342
- };
343
-
344
- return {
345
- mv,
346
- computedTabList,
347
- clickTab,
348
- removeTab,
349
-
350
- listWrapperRef,
351
- listRef,
352
- hasScroll,
353
- listRefStyle,
354
- scrollTab,
355
-
356
- dragstartTab,
357
- dragoverTab,
358
- dragendTab,
359
- dragSelectCls,
360
- selectIdxCls,
361
-
362
- onResize,
363
- };
364
- },
365
- };
366
- </script>
367
-
368
- <style lang="scss">
369
- @import '../../style/index.scss';
370
-
371
- .ev-tabs {
372
- ul, li {
373
- list-style: none;
374
- }
375
-
376
- @include state('closable') {
377
- .ev-tabs-title {
378
- &:hover {
379
- .text {
380
- transform: translateX(-5px);
381
- }
382
- .close-icon {
383
- opacity: 1;
384
- }
385
- }
386
- }
387
- }
388
- @include state('stretch') {
389
- .ev-tabs-title {
390
- width: 100%;
391
- }
392
- }
393
- }
394
-
395
- .ev-tabs-header {
396
- $tab-header-height: $input-default-height;
397
- position: relative;
398
-
399
- @include evThemify() {
400
- border-bottom: 1px solid evThemed('border-base');
401
- }
402
-
403
- .ev-tabs-list-wrapper {
404
- user-select: none;
405
- overflow: hidden;
406
- }
407
- .ev-tabs-list {
408
- display: flex;
409
- float: left;
410
- border-radius: 4px 4px 0 0;
411
- border-bottom: none !important;
412
- text-align: center;
413
- transition: transform .3s;
414
- user-select: none;
415
-
416
- @include evThemify() {
417
- border: 1px solid evThemed('border-base');
418
- }
419
- }
420
- .ev-tabs-title {
421
- position: relative;
422
- width: 100px;
423
- height: $tab-header-height;
424
- padding: 0 17px;
425
- line-height: $tab-header-height;
426
- cursor: pointer;
427
-
428
- @include evThemify() {
429
- background-color: evThemed('background-lighten');
430
- }
431
- &:not(:first-child) {
432
- @include evThemify() {
433
- border-left: 1px solid evThemed('border-base');
434
- }
435
- }
436
- &:not(.select-idx):hover {
437
- @include evThemify() {
438
- color: evThemed('primary');
439
- }
440
- }
441
- &.active {
442
- background-color: transparent;
443
-
444
- @include evThemify() {
445
- border-bottom: 1px solid evThemed('background-base');
446
- color: evThemed('primary');
447
- }
448
- }
449
- &.has-icon {
450
- padding-left: 32px;
451
- }
452
- &.drag-select {
453
- @include evThemify() {
454
- background-color: rgba(evThemed('background-base'), 0.3);
455
- }
456
- }
457
-
458
- .text {
459
- transition: transform $animate-base;
460
-
461
- @include shortening();
462
- }
463
- .close-icon {
464
- position: absolute;
465
- top: 50%;
466
- right: 7px;
467
- transform: translateY(-50%);
468
- font-size: $font-size-small;
469
- opacity: 0;
470
- transition: opacity $animate-base;
471
- }
472
- }
473
- .ev-tabs-icon {
474
- position: absolute;
475
- left: 10px;
476
- }
477
- }
478
-
479
- .ev-tabs-nav-wrapper {
480
- $tab-header-height: $input-default-height;
481
- box-sizing: border-box;
482
- margin-bottom: -1px;
483
- user-select: none;
484
-
485
- &.has-scroll {
486
- $arrow-width: 17px;
487
- padding: 0 20px;
488
-
489
- .ev-tabs-arrow {
490
- position: absolute;
491
- top: 0;
492
- width: $arrow-width;
493
- height: $tab-header-height;
494
- line-height: $tab-header-height;
495
- font-size: $font-size-base;
496
- text-align: center;
497
- cursor: pointer;
498
-
499
- @include evThemify() {
500
- background-color: evThemed('background-base');
501
- }
502
-
503
- &:hover {
504
- @include evThemify() {
505
- color: evThemed('primary');
506
- }
507
- }
508
- &.prev {
509
- left: 0;
510
- }
511
- &.next {
512
- right: 0;
513
- }
514
- }
515
- }
516
- }
517
- </style>
1
+ <template>
2
+ <section
3
+ v-resize="onResize"
4
+ v-observe-visibility="{
5
+ callback: onResize,
6
+ once: true,
7
+ }"
8
+ class="ev-tabs"
9
+ :class="{
10
+ closable,
11
+ stretch,
12
+ }"
13
+ >
14
+ <div class="ev-tabs-header">
15
+ <div
16
+ class="ev-tabs-nav-wrapper"
17
+ :class="{
18
+ 'has-scroll': hasScroll,
19
+ }"
20
+ >
21
+ <template v-if="hasScroll">
22
+ <span
23
+ class="ev-tabs-arrow prev"
24
+ @click="scrollTab('prev')"
25
+ >
26
+ <i class="ev-icon-s-arrow-left" />
27
+ </span>
28
+ <span
29
+ class="ev-tabs-arrow next"
30
+ @click="scrollTab('next')"
31
+ >
32
+ <i class="ev-icon-s-arrow-right" />
33
+ </span>
34
+ </template>
35
+ <div
36
+ ref="listWrapperRef"
37
+ class="ev-tabs-list-wrapper"
38
+ >
39
+ <ul
40
+ ref="listRef"
41
+ class="ev-tabs-list"
42
+ :style="listRefStyle"
43
+ >
44
+ <li
45
+ v-for="(item, idx) in computedTabList"
46
+ :key="`${item.value}_${idx}`"
47
+ class="ev-tabs-title"
48
+ v-bind="{ draggable }"
49
+ :class="{
50
+ active: item.value === mv,
51
+ 'has-icon': item.iconClass,
52
+ 'drag-select': dragSelectCls(item.value),
53
+ 'select-idx': selectIdxCls(idx),
54
+ }"
55
+ @click="clickTab(item.value)"
56
+ @dragstart.stop="dragstartTab(item, idx)"
57
+ @dragover.prevent="dragoverTab(item.value)"
58
+ @dragend.prevent="dragendTab"
59
+ >
60
+ <i
61
+ v-if="item.iconClass"
62
+ class="ev-tabs-icon"
63
+ :class="item.iconClass"
64
+ />
65
+ <span
66
+ class="text"
67
+ :title="item.text"
68
+ >
69
+ {{ item.text }}
70
+ </span>
71
+ <span
72
+ v-if="closable"
73
+ class="close-icon"
74
+ @click.stop="removeTab(item.value)"
75
+ >
76
+ <i class="ev-icon-s-close" />
77
+ </span>
78
+ </li>
79
+ </ul>
80
+ </div>
81
+ </div>
82
+ </div>
83
+ <div class="ev-tabs-body">
84
+ <slot />
85
+ </div>
86
+ </section>
87
+ </template>
88
+
89
+ <script>
90
+ import {
91
+ ref, reactive, computed,
92
+ provide, triggerRef,
93
+ onBeforeUpdate, nextTick,
94
+ } from 'vue';
95
+
96
+ export default {
97
+ name: 'EvTabs',
98
+ props: {
99
+ modelValue: {
100
+ type: [String, Number],
101
+ default: null,
102
+ },
103
+ panels: {
104
+ type: Array,
105
+ default: () => [],
106
+ validator: (list) => {
107
+ const valueList = list.map(v => v.value);
108
+ const setList = [...new Set(valueList)];
109
+ if (list.length !== setList.length) {
110
+ console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is duplicate values.');
111
+ return false;
112
+ }
113
+ if (!list.every(v => Object.hasOwnProperty.call(v, 'value'))) {
114
+ console.warn('[EVUI][Tabs] TabPanel \'value\' attribute is essential.');
115
+ return false;
116
+ }
117
+ return true;
118
+ },
119
+ },
120
+ closable: {
121
+ type: Boolean,
122
+ default: false,
123
+ },
124
+ stretch: {
125
+ type: Boolean,
126
+ default: false,
127
+ },
128
+ draggable: {
129
+ type: Boolean,
130
+ default: false,
131
+ },
132
+ },
133
+ emits: {
134
+ 'update:modelValue': [String, Number],
135
+ 'update:panels': [Array],
136
+ change: [String, Number],
137
+ },
138
+ setup(props, { emit }) {
139
+ // 드래그 상태 여부 (dragstart ~ dragend)
140
+ const isDragState = ref(false);
141
+
142
+ const tabCloneList = ref([]);
143
+
144
+ const mv = computed({
145
+ get: () => props.modelValue,
146
+ set: (val) => {
147
+ emit('update:modelValue', val);
148
+ emit('change', val);
149
+ },
150
+ });
151
+
152
+ provide('evTabs', mv);
153
+
154
+ const tabList = computed({
155
+ get: () => props.panels,
156
+ set: val => emit('update:panels', val),
157
+ });
158
+ const computedTabList = computed(() => {
159
+ if (!props.draggable) {
160
+ return tabList.value;
161
+ }
162
+ if (!isDragState.value) {
163
+ return tabList.value;
164
+ }
165
+ return tabCloneList.value;
166
+ });
167
+ const tabElValueList = tabList.value.map(v => v.value);
168
+
169
+ const listWrapperRef = ref(null);
170
+ const listRef = ref(null);
171
+ const hasScroll = ref(false);
172
+
173
+ const translateScroll = reactive({
174
+ x: 0,
175
+ });
176
+ const listRefStyle = computed(() => ({
177
+ transform: `translateX(${translateScroll.x}px)`,
178
+ }));
179
+
180
+ /**
181
+ * 상단 탭 nav의 element 길이를 감시 및 계산하여 스크롤 여부 확인
182
+ * UL의 길이가 긴 경우 양쪽에 버튼 노출
183
+ */
184
+ const observeListEl = () => {
185
+ const listWrapperWidth = listWrapperRef.value.offsetWidth;
186
+ const listWidth = listRef.value.offsetWidth;
187
+ hasScroll.value = listWrapperWidth < listWidth;
188
+
189
+ if (hasScroll.value) {
190
+ const widthLimit = listWrapperWidth - listWidth;
191
+ if (widthLimit > translateScroll.x) {
192
+ translateScroll.x = widthLimit;
193
+ }
194
+ } else {
195
+ translateScroll.x = 0;
196
+ }
197
+ };
198
+
199
+ onBeforeUpdate(() => {
200
+ // 삭제된 탭이 선택된 경우 탭선택 인덱스를 변경하는 로직
201
+ if (tabElValueList.length === tabList.value.length + 1) {
202
+ let longList;
203
+ let shortList;
204
+ if (tabElValueList.length > tabList.value.length) {
205
+ longList = tabElValueList;
206
+ shortList = tabList.value.map(v => v.value);
207
+ } else {
208
+ longList = tabList.value.map(v => v.value);
209
+ shortList = tabElValueList;
210
+ }
211
+ const removeValue = longList.filter(v => !shortList.includes(v))[0];
212
+ if (mv.value === removeValue) {
213
+ const selectedIdx = tabElValueList.findIndex(v => v === removeValue);
214
+ if (selectedIdx === 0) {
215
+ mv.value = tabList.value[0].value;
216
+ } else {
217
+ mv.value = tabList.value[selectedIdx - 1].value;
218
+ }
219
+ }
220
+ }
221
+ });
222
+
223
+ /**
224
+ * 탭 클릭 로직
225
+ */
226
+ const clickTab = (val) => {
227
+ mv.value = val;
228
+ };
229
+
230
+ /**
231
+ * 탭 삭제 로직
232
+ */
233
+ const removeTab = (val) => {
234
+ if (tabList.value.length < 2) {
235
+ return;
236
+ }
237
+ const selectedIdx = tabList.value.findIndex(v => v.value === val);
238
+ if (selectedIdx < 0) {
239
+ mv.value = tabList.value[0].value;
240
+ return;
241
+ }
242
+ if (val === mv.value) {
243
+ if (selectedIdx === 0) {
244
+ mv.value = tabList.value[1].value;
245
+ } else {
246
+ mv.value = tabList.value[selectedIdx - 1].value;
247
+ }
248
+ }
249
+ tabList.value.splice(selectedIdx, 1);
250
+ nextTick(() => {
251
+ tabElValueList.splice(selectedIdx, 1);
252
+ });
253
+ triggerRef(tabList);
254
+ };
255
+
256
+ /**
257
+ * tab nav위에서 마우스 휠 동작
258
+ * @param type - {'next'|'prev'}
259
+ * @param movingWidth
260
+ */
261
+ const scrollTab = (type, movingWidth = 100) => {
262
+ const listWrapperWidth = listWrapperRef.value.offsetWidth;
263
+ const listWidth = listRef.value.offsetWidth;
264
+ const widthLimit = listWrapperWidth - listWidth;
265
+ if (type === 'next' && translateScroll.x !== widthLimit) {
266
+ if (widthLimit >= translateScroll.x - movingWidth) {
267
+ translateScroll.x = widthLimit;
268
+ } else {
269
+ translateScroll.x -= movingWidth;
270
+ }
271
+ } else if (type === 'prev' && translateScroll.x !== 0) {
272
+ if (movingWidth * -1 <= translateScroll.x) {
273
+ translateScroll.x = 0;
274
+ } else {
275
+ translateScroll.x += movingWidth;
276
+ }
277
+ }
278
+ };
279
+
280
+ // draggable 모드에서 drag되는 아이템
281
+ const dragObj = reactive({
282
+ item: {},
283
+ idx: null,
284
+ });
285
+
286
+ /**
287
+ * 드래그된 LI의 클래스
288
+ * @param val
289
+ * @returns {boolean|boolean}
290
+ */
291
+ const dragSelectCls = val => props.draggable && dragObj.item?.value === val;
292
+
293
+ /**
294
+ * 드래그하기위해 선택한 li의 idx 여부 클래스
295
+ */
296
+ const selectIdxCls = idx => props.draggable && dragObj.idx === idx;
297
+
298
+ /**
299
+ * 탭 드래그 시작 메소드, isDragState모드 시작
300
+ * @param item - 선택한 아이템
301
+ */
302
+ const dragstartTab = (item, idx) => {
303
+ if (!props.draggable) {
304
+ return;
305
+ }
306
+ tabCloneList.value = [...tabList.value];
307
+ dragObj.item = item;
308
+ dragObj.idx = idx;
309
+ isDragState.value = true;
310
+ };
311
+
312
+ /**
313
+ * 탭 드래그오버 메소드
314
+ * @param val - 오버 중인 아이템의 value
315
+ */
316
+ const dragoverTab = (val) => {
317
+ if (!props.draggable || dragObj.item?.value === val) {
318
+ return;
319
+ }
320
+ const dragValueIdx = tabCloneList.value.findIndex(v => v.value === dragObj.item?.value);
321
+ const targetValueIdx = tabCloneList.value.findIndex(v => v.value === val);
322
+ tabCloneList.value.splice(dragValueIdx, 1);
323
+ tabCloneList.value.splice(targetValueIdx, 0, dragObj.item);
324
+ };
325
+
326
+ /**
327
+ * 탭 드래그 종료 메소드, 원래 tabList에 값을 넣고 isDragState모드를 종료
328
+ */
329
+ const dragendTab = () => {
330
+ if (!props.draggable) {
331
+ return;
332
+ }
333
+ tabList.value = [...tabCloneList.value];
334
+ dragObj.item = {};
335
+ dragObj.idx = null;
336
+ isDragState.value = false;
337
+ tabCloneList.value.splice(0);
338
+ };
339
+
340
+ const onResize = () => {
341
+ observeListEl();
342
+ };
343
+
344
+ return {
345
+ mv,
346
+ computedTabList,
347
+ clickTab,
348
+ removeTab,
349
+
350
+ listWrapperRef,
351
+ listRef,
352
+ hasScroll,
353
+ listRefStyle,
354
+ scrollTab,
355
+
356
+ dragstartTab,
357
+ dragoverTab,
358
+ dragendTab,
359
+ dragSelectCls,
360
+ selectIdxCls,
361
+
362
+ onResize,
363
+ };
364
+ },
365
+ };
366
+ </script>
367
+
368
+ <style lang="scss">
369
+ @import '../../style/index.scss';
370
+
371
+ .ev-tabs {
372
+ ul, li {
373
+ list-style: none;
374
+ }
375
+
376
+ @include state('closable') {
377
+ .ev-tabs-title {
378
+ &:hover {
379
+ .text {
380
+ transform: translateX(-5px);
381
+ }
382
+ .close-icon {
383
+ opacity: 1;
384
+ }
385
+ }
386
+ }
387
+ }
388
+ @include state('stretch') {
389
+ .ev-tabs-title {
390
+ width: 100%;
391
+ }
392
+ }
393
+ }
394
+
395
+ .ev-tabs-header {
396
+ $tab-header-height: $input-default-height;
397
+ position: relative;
398
+
399
+ @include evThemify() {
400
+ border-bottom: 1px solid evThemed('border-base');
401
+ }
402
+
403
+ .ev-tabs-list-wrapper {
404
+ user-select: none;
405
+ overflow: hidden;
406
+ }
407
+ .ev-tabs-list {
408
+ display: flex;
409
+ float: left;
410
+ border-radius: 4px 4px 0 0;
411
+ border-bottom: none !important;
412
+ text-align: center;
413
+ transition: transform .3s;
414
+ user-select: none;
415
+
416
+ @include evThemify() {
417
+ border: 1px solid evThemed('border-base');
418
+ }
419
+ }
420
+ .ev-tabs-title {
421
+ position: relative;
422
+ width: 100px;
423
+ height: $tab-header-height;
424
+ padding: 0 17px;
425
+ line-height: $tab-header-height;
426
+ cursor: pointer;
427
+
428
+ @include evThemify() {
429
+ background-color: evThemed('background-lighten');
430
+ }
431
+ &:not(:first-child) {
432
+ @include evThemify() {
433
+ border-left: 1px solid evThemed('border-base');
434
+ }
435
+ }
436
+ &:not(.select-idx):hover {
437
+ @include evThemify() {
438
+ color: evThemed('primary');
439
+ }
440
+ }
441
+ &.active {
442
+ background-color: transparent;
443
+
444
+ @include evThemify() {
445
+ border-bottom: 1px solid evThemed('background-base');
446
+ color: evThemed('primary');
447
+ }
448
+ }
449
+ &.has-icon {
450
+ padding-left: 32px;
451
+ }
452
+ &.drag-select {
453
+ @include evThemify() {
454
+ background-color: rgba(evThemed('background-base'), 0.3);
455
+ }
456
+ }
457
+
458
+ .text {
459
+ transition: transform $animate-base;
460
+
461
+ @include shortening();
462
+ }
463
+ .close-icon {
464
+ position: absolute;
465
+ top: 50%;
466
+ right: 7px;
467
+ transform: translateY(-50%);
468
+ font-size: $font-size-small;
469
+ opacity: 0;
470
+ transition: opacity $animate-base;
471
+ }
472
+ }
473
+ .ev-tabs-icon {
474
+ position: absolute;
475
+ left: 10px;
476
+ }
477
+ }
478
+
479
+ .ev-tabs-nav-wrapper {
480
+ $tab-header-height: $input-default-height;
481
+ box-sizing: border-box;
482
+ margin-bottom: -1px;
483
+ user-select: none;
484
+
485
+ &.has-scroll {
486
+ $arrow-width: 17px;
487
+ padding: 0 20px;
488
+
489
+ .ev-tabs-arrow {
490
+ position: absolute;
491
+ top: 0;
492
+ width: $arrow-width;
493
+ height: $tab-header-height;
494
+ line-height: $tab-header-height;
495
+ font-size: $font-size-base;
496
+ text-align: center;
497
+ cursor: pointer;
498
+
499
+ @include evThemify() {
500
+ background-color: evThemed('background-base');
501
+ }
502
+
503
+ &:hover {
504
+ @include evThemify() {
505
+ color: evThemed('primary');
506
+ }
507
+ }
508
+ &.prev {
509
+ left: 0;
510
+ }
511
+ &.next {
512
+ right: 0;
513
+ }
514
+ }
515
+ }
516
+ }
517
+ </style>