evui 3.4.24 → 3.4.26

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.
@@ -50,7 +50,14 @@
50
50
 
51
51
  <script>
52
52
  import { clickoutside } from '@/directives/clickoutside';
53
- import { computed, inject, nextTick, onBeforeMount, reactive, ref, watch } from 'vue';
53
+ import {
54
+ computed,
55
+ nextTick,
56
+ onBeforeMount,
57
+ reactive,
58
+ ref,
59
+ watch,
60
+ } from 'vue';
54
61
 
55
62
  export default {
56
63
  name: 'EVGridColumnSetting',
@@ -70,9 +77,22 @@ export default {
70
77
  type: String,
71
78
  default: '',
72
79
  },
80
+ position: {
81
+ type: Object,
82
+ default: () => ({
83
+ top: 0,
84
+ left: 0,
85
+ columnListMenuWidth: 0,
86
+ }),
87
+ },
88
+ isShowMenuOnClick: {
89
+ type: Boolean,
90
+ default: false,
91
+ },
73
92
  },
74
93
  emits: {
75
- 'update:isShow': null,
94
+ 'update:isShow': [Boolean],
95
+ 'update:isShowMenuOnClick': [Boolean],
76
96
  'apply-column': null,
77
97
  },
78
98
  setup(props, { emit }) {
@@ -93,12 +113,14 @@ export default {
93
113
  const isDisabled = computed(() => !columnList.value.length);
94
114
  let timer = null;
95
115
  let lastCheckedColumn = null;
96
-
97
- const toolbarWrapperDiv = inject('toolbarWrapper');
98
116
  const columnSettingStyle = reactive({
99
117
  top: null,
100
118
  left: null,
101
119
  });
120
+ const computedIsShowMenuOnClick = computed({
121
+ get: () => props.isShowMenuOnClick,
122
+ set: val => emit('update:isShowMenuOnClick', val),
123
+ });
102
124
 
103
125
  const onCheckColumn = (columns) => {
104
126
  if (columns?.length === 1) {
@@ -155,8 +177,8 @@ export default {
155
177
  const checkedColumns = applyColumnList.value.map(col => col.text);
156
178
 
157
179
  emit('apply-column', checkedColumns);
158
- initSearchValue();
159
180
  isShowColumnSetting.value = false;
181
+ computedIsShowMenuOnClick.value = false;
160
182
  };
161
183
 
162
184
  const setColumns = () => {
@@ -175,7 +197,7 @@ export default {
175
197
 
176
198
  const hideColumnSetting = () => {
177
199
  isShowColumnSetting.value = false;
178
- initValue();
200
+ computedIsShowMenuOnClick.value = false;
179
201
  };
180
202
 
181
203
  const initWrapperDiv = () => {
@@ -188,6 +210,29 @@ export default {
188
210
  }
189
211
  };
190
212
 
213
+ const setPosition = async () => {
214
+ await nextTick();
215
+
216
+ const docWidth = document.documentElement.clientWidth;
217
+ const columnSettingWrapperRect = columnSettingWrapper.value?.getBoundingClientRect();
218
+ const columnSettingWidth = columnSettingWrapperRect?.width;
219
+ const { top, left, columnListMenuWidth } = props.position;
220
+ let columnSettingLeft;
221
+
222
+ if (columnListMenuWidth) { // 컨텍스트 메뉴일 때
223
+ columnSettingLeft = left;
224
+
225
+ if (docWidth < left + columnSettingWidth) {
226
+ columnSettingLeft = left - columnSettingWidth - columnListMenuWidth;
227
+ }
228
+ } else {
229
+ columnSettingLeft = left - columnSettingWidth;
230
+ }
231
+
232
+ columnSettingStyle.top = `${top + document.documentElement.scrollTop}px`;
233
+ columnSettingStyle.left = `${columnSettingLeft + document.documentElement.scrollLeft}px`;
234
+ };
235
+
191
236
  onBeforeMount(() => initWrapperDiv());
192
237
 
193
238
  watch(() => props.columns, () => {
@@ -195,21 +240,11 @@ export default {
195
240
  }, { immediate: true, deep: true });
196
241
 
197
242
  watch(() => isShowColumnSetting.value, async () => {
198
- if (!isShowColumnSetting.value) {
199
- return;
200
- }
201
- await nextTick();
202
-
203
- const columnSettingWrapperRect = columnSettingWrapper.value?.getBoundingClientRect();
204
- const toolbarWrapperDivRect = toolbarWrapperDiv.value?.getBoundingClientRect();
205
-
206
- const columnSettingWidth = columnSettingWrapperRect?.width;
207
- const toolbarHeight = toolbarWrapperDivRect?.height;
208
- const columnSettingTop = toolbarWrapperDivRect?.top + document.documentElement.scrollTop;
209
- const columnSettingRight = toolbarWrapperDivRect?.right + document.documentElement.scrollLeft;
243
+ initValue();
210
244
 
211
- columnSettingStyle.top = `${columnSettingTop + toolbarHeight}px`;
212
- columnSettingStyle.left = `${columnSettingRight - columnSettingWidth}px`;
245
+ if (isShowColumnSetting.value) {
246
+ await setPosition();
247
+ }
213
248
  });
214
249
 
215
250
  watch(() => props.hiddenColumn, (value) => {
@@ -24,7 +24,7 @@ export default {
24
24
  .gridToolbar > .ev-button {
25
25
  margin: 0 2px 0 2px;
26
26
  }
27
- .gridToolbar > .column-setting__icon {
27
+ .gridToolbar > .grid-setting__icon {
28
28
  float: right;
29
29
  margin: 10px 5px;
30
30
  cursor: pointer;
@@ -64,6 +64,17 @@
64
64
  //&:nth-last-child(1) {
65
65
  // border-right: 0;
66
66
  //}
67
+ &.checkbox-all {
68
+ padding: 0;
69
+ .ev-checkbox {
70
+ display: inline-flex;
71
+ width: 100%;
72
+ height: 100%;
73
+ justify-content: center;
74
+ align-items: center;
75
+ }
76
+ }
77
+
67
78
  .column-sort__icon {
68
79
  position: absolute;
69
80
  top: 50%;
@@ -165,6 +176,11 @@
165
176
  }
166
177
  }
167
178
  }
179
+ .row-contextmenu {
180
+ display: inline-flex;
181
+ justify-content: center;
182
+ align-items: center;
183
+ }
168
184
 
169
185
  .cell {
170
186
  display: inline-block;
@@ -183,9 +199,31 @@
183
199
  text-overflow: ellipsis;
184
200
  }
185
201
  &.row-checkbox {
186
- display: inline-flex;
187
- justify-content: center;
188
- align-items: center;
202
+ padding: 0;
203
+ .ev-checkbox {
204
+ display: inline-flex;
205
+ width: 100%;
206
+ height: 100%;
207
+ justify-content: center;
208
+ align-items: center;
209
+ }
210
+ }
211
+ &.row-detail-toggle {
212
+ padding: 0;
213
+ .row-detail-toggle-icon {
214
+ display: inline-flex;
215
+ width: 100%;
216
+ height: 100%;
217
+ justify-content: center;
218
+ align-items: center;
219
+ cursor: pointer;
220
+ transition: transform $animate-fast;
221
+ }
222
+ &--expanded {
223
+ .row-detail-toggle-icon {
224
+ transform: rotate(90deg);
225
+ }
226
+ }
189
227
  }
190
228
  &.render {
191
229
  overflow: initial;
@@ -6,6 +6,7 @@ const ROW_INDEX = 0;
6
6
  const ROW_CHECK_INDEX = 1;
7
7
  const ROW_DATA_INDEX = 2;
8
8
  const ROW_SELECT_INDEX = 3;
9
+ const ROW_EXPAND_INDEX = 4;
9
10
 
10
11
  export const commonFunctions = () => {
11
12
  const { props } = getCurrentInstance();
@@ -79,11 +80,12 @@ export const scrollEvent = (params) => {
79
80
  summaryScroll,
80
81
  getPagingData,
81
82
  updatePagingInfo,
83
+ expandedInfo,
82
84
  } = params;
83
85
  /**
84
86
  * 수직 스크롤의 위치 계산 후 적용한다.
85
87
  */
86
- const updateVScroll = (isScroll) => {
88
+ const updateVScrollBase = (isScroll) => {
87
89
  const bodyEl = elementInfo.body;
88
90
  const rowHeight = resizeInfo.rowHeight;
89
91
  if (bodyEl) {
@@ -118,6 +120,23 @@ export const scrollEvent = (params) => {
118
120
  }
119
121
  }
120
122
  };
123
+
124
+ /**
125
+ * rowDetail slot 시에는 가상 스크롤을 적용하지 않는다.
126
+ */
127
+ const updateVScroll = (isScroll) => {
128
+ if (expandedInfo.useRowDetail) {
129
+ let store = stores.store;
130
+ if (pageInfo.isClientPaging) {
131
+ store = getPagingData();
132
+ }
133
+ stores.viewStore = store;
134
+ scrollInfo.vScrollTopHeight = 0;
135
+ scrollInfo.vScrollBottomHeight = 0;
136
+ } else {
137
+ updateVScrollBase(isScroll);
138
+ }
139
+ };
121
140
  /**
122
141
  * 수평 스크롤의 위치 계산 후 적용한다.
123
142
  */
@@ -162,6 +181,7 @@ export const resizeEvent = (params) => {
162
181
  resizeInfo,
163
182
  elementInfo,
164
183
  checkInfo,
184
+ expandedInfo,
165
185
  stores,
166
186
  isRenderer,
167
187
  updateVScroll,
@@ -202,6 +222,10 @@ export const resizeEvent = (params) => {
202
222
  elWidth -= resizeInfo.minWidth;
203
223
  }
204
224
 
225
+ if (expandedInfo.useRowDetail) {
226
+ elWidth -= resizeInfo.minWidth;
227
+ }
228
+
205
229
  columnWidth = elWidth - result.totalWidth;
206
230
  if (columnWidth > 0) {
207
231
  remainWidth = columnWidth
@@ -447,7 +471,7 @@ export const clickEvent = (params) => {
447
471
 
448
472
  export const checkEvent = (params) => {
449
473
  const { checkInfo, stores, pageInfo, getPagingData } = params;
450
- const { emit } = getCurrentInstance();
474
+ const { props, emit } = getCurrentInstance();
451
475
  /**
452
476
  * row에 대한 체크 상태를 해제한다.
453
477
  *
@@ -483,7 +507,10 @@ export const checkEvent = (params) => {
483
507
  if (pageInfo.isClientPaging) {
484
508
  store = getPagingData();
485
509
  }
486
- const isAllChecked = store.every(d => d[ROW_CHECK_INDEX]);
510
+
511
+ const isAllChecked = store
512
+ .filter(rowData => !props.uncheckable.includes(rowData[ROW_DATA_INDEX]))
513
+ .every(d => d[ROW_CHECK_INDEX]);
487
514
  if (store.length && isAllChecked) {
488
515
  checkInfo.isHeaderChecked = true;
489
516
  }
@@ -511,14 +538,18 @@ export const checkEvent = (params) => {
511
538
  store = getPagingData();
512
539
  }
513
540
  store.forEach((row) => {
541
+ const uncheckable = props.uncheckable.includes(row[ROW_DATA_INDEX]);
514
542
  if (isHeaderChecked) {
515
- if (!checkInfo.checkedRows.includes(row[ROW_DATA_INDEX])) {
543
+ if (!checkInfo.checkedRows.includes(row[ROW_DATA_INDEX]) && !uncheckable) {
516
544
  checkInfo.checkedRows.push(row[ROW_DATA_INDEX]);
517
545
  }
518
546
  } else {
519
547
  checkInfo.checkedRows.splice(checkInfo.checkedRows.indexOf(row[ROW_DATA_INDEX]), 1);
520
548
  }
521
- row[ROW_CHECK_INDEX] = isHeaderChecked;
549
+
550
+ if (!uncheckable) {
551
+ row[ROW_CHECK_INDEX] = isHeaderChecked;
552
+ }
522
553
  });
523
554
  emit('update:checked', checkInfo.checkedRows);
524
555
  emit('check-all', event, checkInfo.checkedRows);
@@ -526,6 +557,34 @@ export const checkEvent = (params) => {
526
557
  return { onCheck, onCheckAll };
527
558
  };
528
559
 
560
+ export const expandEvent = (params) => {
561
+ const { expandedInfo } = params;
562
+ const { emit } = getCurrentInstance();
563
+
564
+ /**
565
+ * expand click 이벤트를 처리한다.
566
+ *
567
+ * @param {object} event - 이벤트 객체
568
+ * @param {array} row - row 데이터
569
+ */
570
+ const onExpanded = (event, row) => {
571
+ const data = row[ROW_DATA_INDEX];
572
+ const index = expandedInfo.expandedRows.indexOf(data);
573
+ if (index === -1) {
574
+ expandedInfo.expandedRows.push(data);
575
+ } else {
576
+ expandedInfo.expandedRows.splice(index, 1);
577
+ }
578
+ row[ROW_EXPAND_INDEX] = !row[ROW_EXPAND_INDEX];
579
+ emit('update:expanded', expandedInfo.expandedRows);
580
+ emit('expand-row', event, row[ROW_DATA_INDEX], row[ROW_EXPAND_INDEX], row[ROW_INDEX]);
581
+ };
582
+
583
+ return {
584
+ onExpanded,
585
+ };
586
+ };
587
+
529
588
  export const sortEvent = (params) => {
530
589
  const { sortInfo, stores, updatePagingInfo } = params;
531
590
  const { emit } = getCurrentInstance();
@@ -899,9 +958,10 @@ export const contextMenuEvent = (params) => {
899
958
  stores,
900
959
  selectInfo,
901
960
  onSort,
902
- setColumnHidden,
903
- useColumnSetting,
904
961
  filterInfo,
962
+ useGridSetting,
963
+ columnSettingInfo,
964
+ setColumnHidden,
905
965
  } = params;
906
966
  /**
907
967
  * 컨텍스트 메뉴를 설정한다.
@@ -1006,16 +1066,43 @@ export const contextMenuEvent = (params) => {
1006
1066
  {
1007
1067
  text: 'Hide',
1008
1068
  iconClass: 'ev-icon-visibility-off',
1009
- disabled: !useColumnSetting.value || stores.orderedColumns.length === 1,
1069
+ disabled: !useGridSetting.value || stores.orderedColumns.length === 1,
1010
1070
  click: () => setColumnHidden(column.field),
1011
1071
  },
1012
1072
  ];
1013
1073
  }
1014
1074
  };
1075
+ /**
1076
+ * 상단 우측의 Grid 옵션에 대한 Contextmenu 를 생성한다.
1077
+ *
1078
+ * @param {object} event - 이벤트 객체
1079
+ */
1080
+ const onGridSettingContextMenu = (e) => {
1081
+ const columnListMenu = {
1082
+ text: 'Column List',
1083
+ isShowMenu: true,
1084
+ click: () => {
1085
+ columnSettingInfo.isShowColumnSetting = true;
1086
+ contextInfo.isShowMenuOnClick = true;
1087
+ },
1088
+ };
1089
+
1090
+ if (contextInfo.customGridSettingContextMenu.length) {
1091
+ contextInfo.gridSettingContextMenuItems = [
1092
+ ...contextInfo.customGridSettingContextMenu,
1093
+ columnListMenu,
1094
+ ];
1095
+ } else {
1096
+ contextInfo.gridSettingContextMenuItems = [columnListMenu];
1097
+ }
1098
+ contextInfo.gridSettingMenu.show(e);
1099
+ };
1100
+
1015
1101
  return {
1016
1102
  setContextMenu,
1017
1103
  onContextMenu,
1018
1104
  onColumnContextMenu,
1105
+ onGridSettingContextMenu,
1019
1106
  };
1020
1107
  };
1021
1108
 
@@ -1028,6 +1115,7 @@ export const storeEvent = (params) => {
1028
1115
  sortInfo,
1029
1116
  elementInfo,
1030
1117
  filterInfo,
1118
+ expandedInfo,
1031
1119
  setSort,
1032
1120
  updateVScroll,
1033
1121
  setFilter,
@@ -1044,16 +1132,22 @@ export const storeEvent = (params) => {
1044
1132
  let hasUnChecked = false;
1045
1133
  rows.forEach((row, idx) => {
1046
1134
  const checked = props.checked.includes(row);
1135
+ const uncheckable = props.uncheckable.includes(row);
1047
1136
  let selected = false;
1048
1137
  if (selectInfo.useSelect) {
1049
1138
  selected = props.selected.includes(row);
1050
1139
  }
1051
- if (!checked) {
1140
+ if (!checked && !uncheckable) {
1052
1141
  hasUnChecked = true;
1053
1142
  }
1054
- store.push([idx, checked, row, selected]);
1143
+ let expanded = false;
1144
+ if (expandedInfo.useRowDetail) {
1145
+ expanded = props.expanded.includes(row);
1146
+ }
1147
+ store.push([idx, checked, row, selected, expanded, uncheckable]);
1055
1148
  });
1056
1149
  checkInfo.isHeaderChecked = rows.length > 0 ? !hasUnChecked : false;
1150
+ checkInfo.isHeaderUncheckable = rows.every(row => props.uncheckable.includes(row));
1057
1151
  stores.originStore = store;
1058
1152
  }
1059
1153
  if (filterInfo.isFiltering) {
@@ -1134,12 +1228,36 @@ export const columnSettingEvent = (params) => {
1134
1228
  const {
1135
1229
  stores,
1136
1230
  columnSettingInfo,
1231
+ contextInfo,
1137
1232
  onSearch,
1138
1233
  onResize,
1139
1234
  } = params;
1140
- const setColumnSetting = () => {
1141
- columnSettingInfo.isShowColumnSetting = true;
1235
+
1236
+ const setPositionColumnSetting = (toolbarRef) => {
1237
+ if (!columnSettingInfo.isShowColumnSetting) {
1238
+ return;
1239
+ }
1240
+ columnSettingInfo.columnSettingPosition.columnListMenuWidth = 0;
1241
+
1242
+ if (contextInfo.gridSettingContextMenuItems.length) {
1243
+ // 컨텍스트 메뉴 형태인 경우
1244
+ const columnListMenu = contextInfo.gridSettingContextMenuItems.length - 1;
1245
+ const columnListMenuRect = contextInfo.gridSettingMenu?.rootMenuList?.$el?.children[0]
1246
+ .children[columnListMenu].getBoundingClientRect();
1247
+
1248
+ columnSettingInfo.columnSettingPosition.columnListMenuWidth = columnListMenuRect.width;
1249
+ columnSettingInfo.columnSettingPosition.top = columnListMenuRect.top;
1250
+ columnSettingInfo.columnSettingPosition.left = columnListMenuRect.right;
1251
+ } else {
1252
+ // 컬럼 리스트만 있는 경우
1253
+ const toolbarRefDivRect = toolbarRef?.getBoundingClientRect();
1254
+ const toolbarHeight = toolbarRefDivRect?.height;
1255
+
1256
+ columnSettingInfo.columnSettingPosition.top = toolbarRefDivRect?.top + toolbarHeight;
1257
+ columnSettingInfo.columnSettingPosition.left = toolbarRefDivRect?.right;
1258
+ }
1142
1259
  };
1260
+
1143
1261
  const initColumnSettingInfo = () => {
1144
1262
  stores.filteredColumns.length = 0;
1145
1263
  columnSettingInfo.isShowColumnSetting = false;
@@ -1195,7 +1313,12 @@ export const columnSettingEvent = (params) => {
1195
1313
  setFilteringColumn();
1196
1314
  };
1197
1315
 
1198
- return { setColumnSetting, initColumnSettingInfo, onApplyColumn, setColumnHidden };
1316
+ return {
1317
+ setPositionColumnSetting,
1318
+ initColumnSettingInfo,
1319
+ onApplyColumn,
1320
+ setColumnHidden,
1321
+ };
1199
1322
  };
1200
1323
 
1201
1324
  export const dragEvent = ({ stores }) => {
@@ -14,7 +14,7 @@
14
14
  @show-context-menu="getContextMenuFlag"
15
15
  @contextmenu.prevent="showContextMenu"
16
16
  />
17
- <div v-if="!treeNodeData.length">No data</div>
17
+ <div v-if="!treeNodeData.length">{{ emptyText }}</div>
18
18
  <ev-context-menu
19
19
  v-if="contextMenuItems.length"
20
20
  ref="contextMenu"
@@ -24,7 +24,7 @@
24
24
  </template>
25
25
 
26
26
  <script>
27
- import { ref, reactive, watch, onMounted, onBeforeUnmount } from 'vue';
27
+ import { ref, watch, onMounted, onBeforeUnmount } from 'vue';
28
28
  import TreeNode from './TreeNode';
29
29
 
30
30
  export default {
@@ -70,7 +70,7 @@ export default {
70
70
  check: Array,
71
71
  },
72
72
  setup(props, { emit }) {
73
- let treeNodeData = reactive(props.data);
73
+ const treeNodeData = ref(props.data);
74
74
  let allNodeInfo = [];
75
75
  const contextMenu = ref(null);
76
76
  let contextMenuFlag = false; // flag for showing contextMenu or not
@@ -150,8 +150,8 @@ export default {
150
150
  }
151
151
  }
152
152
 
153
- if (treeNodeData.length) {
154
- flattenChildren(treeNodeData[0]);
153
+ if (treeNodeData.value.length) {
154
+ flattenChildren(treeNodeData.value[0]);
155
155
  }
156
156
  return flatTree;
157
157
  }
@@ -185,7 +185,7 @@ export default {
185
185
  node.indeterminate = false;
186
186
  updateTreeUp(nodeKey); // propagate up
187
187
  updateTreeDown(node, { checked: isChecked, indeterminate: false }); // reset `indeterminate`
188
- const checkedNodes = allNodeInfo.filter(obj => obj.node.checked).map(obj => obj.node);
188
+ const checkedNodes = getCheckedNodes();
189
189
  emit('check', checkedNodes);
190
190
  rebuildTree();
191
191
  }
@@ -278,9 +278,11 @@ export default {
278
278
  });
279
279
  }
280
280
 
281
- watch(props.data, (newData) => {
282
- treeNodeData = newData;
281
+ watch(() => props.data, (newData) => {
282
+ treeNodeData.value = newData;
283
283
  allNodeInfo = getAllNodeInfo();
284
+ }, {
285
+ deep: true,
284
286
  });
285
287
 
286
288