gnui 1.2.17 → 1.2.19

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 (78) hide show
  1. package/dist/js/gnui.esm.js +736 -96
  2. package/dist/js/gnui.js +736 -96
  3. package/dist/js/gnui.min.js +6 -6
  4. package/dist/styles/default.css +1019 -108
  5. package/dist/styles/gpi.css +1019 -108
  6. package/dist/styles/green24.css +1230 -289
  7. package/dist/styles/insights.css +1019 -108
  8. package/dist/styles/nac.css +969 -58
  9. package/dist/styles/ztnac.css +1206 -265
  10. package/package.json +1 -1
  11. package/styleguide/assets/components.js +216 -9
  12. package/styleguide/assets/js/gnui.js +736 -96
  13. package/styleguide/assets/js/gnui.min.js +6 -6
  14. package/styleguide/assets/styles/default.css +1019 -108
  15. package/styleguide/assets/styles/gpi.css +1019 -108
  16. package/styleguide/assets/styles/green24.css +1230 -289
  17. package/styleguide/assets/styles/insights.css +1019 -108
  18. package/styleguide/assets/styles/nac.css +969 -58
  19. package/styleguide/assets/styles/ztnac.css +1206 -265
  20. package/styleguide/category/COLOR/index.html +2 -2
  21. package/styleguide/category/COMPONENT/Alert(js)/index.html +2 -2
  22. package/styleguide/category/COMPONENT/Bignumber/index.html +2 -2
  23. package/styleguide/category/COMPONENT/Breadcrumb/index.html +2 -2
  24. package/styleguide/category/COMPONENT/Calendar(js)/index.html +2 -2
  25. package/styleguide/category/COMPONENT/Card/index.html +2 -2
  26. package/styleguide/category/COMPONENT/Chart(js)/index.html +2 -2
  27. package/styleguide/category/COMPONENT/Datagrid(js)/index.html +136 -9
  28. package/styleguide/category/COMPONENT/Datalist(js)/index.html +2 -2
  29. package/styleguide/category/COMPONENT/Growl(js)/index.html +2 -2
  30. package/styleguide/category/COMPONENT/JsonView(js)/index.html +2 -2
  31. package/styleguide/category/COMPONENT/Loader(js)/index.html +21 -4
  32. package/styleguide/category/COMPONENT/MenuButton(js)/index.html +74 -6
  33. package/styleguide/category/COMPONENT/Message(js)/index.html +2 -2
  34. package/styleguide/category/COMPONENT/Modal(js)/index.html +2 -2
  35. package/styleguide/category/COMPONENT/Pagination(js)/index.html +2 -2
  36. package/styleguide/category/COMPONENT/Panel/index.html +2 -2
  37. package/styleguide/category/COMPONENT/Progressbar(js)/index.html +2 -2
  38. package/styleguide/category/COMPONENT/Tab(js)/index.html +2 -2
  39. package/styleguide/category/COMPONENT/Tagcloud(js)/index.html +2 -2
  40. package/styleguide/category/COMPONENT/Tooltip(js)/index.html +2 -2
  41. package/styleguide/category/COMPONENT/Tree(js)/index.html +2 -2
  42. package/styleguide/category/CONTROLS/Button(js)/index.html +2 -2
  43. package/styleguide/category/CONTROLS/Checkbox/index.html +2 -2
  44. package/styleguide/category/CONTROLS/Colorpicker(js)/index.html +2 -2
  45. package/styleguide/category/CONTROLS/Datepicker(js)/index.html +2 -2
  46. package/styleguide/category/CONTROLS/Dropdown(js)/index.html +2 -2
  47. package/styleguide/category/CONTROLS/File/index.html +2 -2
  48. package/styleguide/category/CONTROLS/Form/Control/index.html +2 -2
  49. package/styleguide/category/CONTROLS/Form/Field/index.html +2 -2
  50. package/styleguide/category/CONTROLS/Form/Plain/index.html +2 -2
  51. package/styleguide/category/CONTROLS/Input/index.html +2 -2
  52. package/styleguide/category/CONTROLS/MultiText(js)/index.html +2 -2
  53. package/styleguide/category/CONTROLS/Picklist(js)/index.html +28 -18
  54. package/styleguide/category/CONTROLS/Radio/index.html +2 -2
  55. package/styleguide/category/CONTROLS/Select/index.html +2 -2
  56. package/styleguide/category/CONTROLS/SelectButton(js)/index.html +2 -2
  57. package/styleguide/category/CONTROLS/Slider/index.html +2 -2
  58. package/styleguide/category/CONTROLS/SortableList(js)/index.html +487 -0
  59. package/styleguide/category/CONTROLS/Switch(js)/index.html +2 -2
  60. package/styleguide/category/CONTROLS/SyntaxInput(js)/index.html +2 -2
  61. package/styleguide/category/CONTROLS/Textarea/index.html +2 -2
  62. package/styleguide/category/CONTROLS/Time(js)/index.html +2 -2
  63. package/styleguide/category/ELEMENTS/Box/index.html +2 -2
  64. package/styleguide/category/ELEMENTS/Icon/index.html +2 -2
  65. package/styleguide/category/ELEMENTS/Image/index.html +2 -2
  66. package/styleguide/category/ELEMENTS/List/index.html +2 -2
  67. package/styleguide/category/ELEMENTS/Table/index.html +2 -2
  68. package/styleguide/category/ELEMENTS/Tag/index.html +2 -2
  69. package/styleguide/category/ELEMENTS/Title/index.html +2 -2
  70. package/styleguide/category/LAYOUT/Container/index.html +2 -2
  71. package/styleguide/category/LAYOUT/Grid/index.html +2 -2
  72. package/styleguide/category/LAYOUT/Splitter(js)/index.html +2 -2
  73. package/styleguide/category/UTILITY/index.html +2 -2
  74. package/styleguide/category/Utils/index.html +2 -2
  75. package/styleguide/color.html +2 -2
  76. package/styleguide/index.html +2 -2
  77. package/styleguide/tag/javascript/index.html +608 -31
  78. package/styleguide/tag/v.0.1.0/index.html +608 -31
@@ -16930,7 +16930,10 @@
16930
16930
  // 이벤트 삭제
16931
16931
  delete this._eventMap[uid];
16932
16932
  }
16933
- dispatch(uid, name, params) {
16933
+ /**
16934
+ * lifeCycle 핸들러 실행
16935
+ */
16936
+ cyclepatch(uid, name, params) {
16934
16937
  const _events = this._getEvent(uid, name);
16935
16938
  if (_events.length) {
16936
16939
  _events.forEach((_event) => {
@@ -16939,6 +16942,28 @@
16939
16942
  });
16940
16943
  }
16941
16944
  }
16945
+ /**
16946
+ * 등록된 이벤트 핸들러 실행
16947
+ *
16948
+ * - sync/async 핸들러를 모두 지원한다.
16949
+ * - 모든 핸들러를 순차 실행한 뒤, 하나라도 `false` 를 반환한 경우 `true`(cancelled)를 반환한다.
16950
+ * (첫 false 에서 중단하지 않고, 나머지 핸들러도 계속 실행한다)
16951
+ */
16952
+ async dispatch(uid, name, params) {
16953
+ const _events = this._getEvent(uid, name);
16954
+ let cancelled = false;
16955
+ if (_events.length) {
16956
+ for (const _event of _events) {
16957
+ const _target = _event.target || this;
16958
+ // sync/async 둘 다 지원: Promise.resolve 로 감싸서 await
16959
+ const result = await Promise.resolve(params ? _event.handler.call(_target, ...params) : _event.handler.call(_target));
16960
+ if (result === false) {
16961
+ cancelled = true;
16962
+ }
16963
+ }
16964
+ }
16965
+ return cancelled;
16966
+ }
16942
16967
  _getEvent(uid, name) {
16943
16968
  // parameters에 해당하는 이벤트 반환
16944
16969
  return this._eventMap[uid]
@@ -17023,7 +17048,7 @@
17023
17048
  });
17024
17049
  return _findComponent;
17025
17050
  }
17026
- // 등록된 컴포넌트 제거
17051
+ // 등록된 컴포넌트 제거 (selector 기반)
17027
17052
  _removeComponent(selector) {
17028
17053
  if (!selector) {
17029
17054
  return;
@@ -17032,10 +17057,30 @@
17032
17057
  Object.values(this._componentMap).forEach(n => {
17033
17058
  // 동일한 selector 인지 비교해서 동일한 component의 selector이면 제거 처리
17034
17059
  if (isEquals(n.selector, _selector)) {
17035
- delete this._componentMap[_selector._uid];
17060
+ // componentMap에서 완전히 제거
17061
+ delete this._componentMap[n.uid];
17062
+ // 컴포넌트 내부 참조도 제거 (메모리 누수 방지)
17063
+ if (n.component) {
17064
+ n.component = null;
17065
+ }
17066
+ n.selector = null;
17036
17067
  }
17037
17068
  });
17038
17069
  }
17070
+ // 등록된 컴포넌트 제거 (uid 기반 - 더 효율적)
17071
+ _removeComponentByUid(uid) {
17072
+ if (!uid || !this._componentMap[uid]) {
17073
+ return;
17074
+ }
17075
+ // 컴포넌트 내부 참조 제거 (메모리 누수 방지)
17076
+ const componentInfo = this._componentMap[uid];
17077
+ if (componentInfo.component) {
17078
+ componentInfo.component = null;
17079
+ }
17080
+ componentInfo.selector = null;
17081
+ // componentMap에서 완전히 제거
17082
+ delete this._componentMap[uid];
17083
+ }
17039
17084
  // 컴포넌트 life cycle에 따른 eventManager dispatch
17040
17085
  _detectedCycle(uid, name) {
17041
17086
  // component 마지막 status 업데이트
@@ -17045,7 +17090,7 @@
17045
17090
  // event manager를 이용해 해당 uid 이벤트 dispatch
17046
17091
  const eventManager = GNCoreEventManager.getInstance();
17047
17092
  // 호출 후
17048
- eventManager.dispatch(uid, name, '');
17093
+ eventManager.cyclepatch(uid, name, '');
17049
17094
  // 이벤트 해제 - life cycle 은 컴포넌트 별로 한번씩만 존재하므로..
17050
17095
  eventManager.remove(uid, name);
17051
17096
  }
@@ -17059,6 +17104,7 @@
17059
17104
  function _removedNode(removed) {
17060
17105
  Array.prototype.forEach.call(removed, (rm) => {
17061
17106
  // 삭제노드 연관 컴포넌트 (ex. tooltip) 삭제
17107
+ var _a;
17062
17108
  const dependents = findAll('[data-gnui]', rm);
17063
17109
  each(dependents, (dependent) => {
17064
17110
  if (isElement$2(dependent)) {
@@ -17071,12 +17117,19 @@
17071
17117
  if (isElement$2(rm) && attr(rm, 'data-gnui')) {
17072
17118
  remove($('#' + attr(rm, 'data-gnui')));
17073
17119
  }
17120
+ const childComponents = $$('[data-gn-uid]', rm);
17121
+ each(childComponents, (childComponent) => {
17122
+ if (isElement$2(childComponent)) {
17123
+ const findComponent = closerThis._getComponent($(childComponent));
17124
+ if (findComponent && findComponent._uid && findComponent.$el && findComponent.$name !== 'modal') {
17125
+ // $destroy 내부에서 removeAll과 _removeComponentByUid를 처리하므로 직접 호출만
17126
+ findComponent.$destroy(findComponent, false);
17127
+ }
17128
+ }
17129
+ });
17074
17130
  const findComponent = closerThis._getComponent($(rm));
17075
- if (findComponent && findComponent._uid && !findComponent.$el.parentNode && findComponent.$name !== 'modal') {
17076
- // state manager 에서 component 삭제
17077
- closerThis._removeComponent(rm);
17078
- // event manager 에서 unbind
17079
- GNCoreEventManager.getInstance().removeAll(findComponent._uid);
17131
+ if (findComponent && findComponent._uid && !((_a = findComponent.$el) === null || _a === void 0 ? void 0 : _a.parentNode) && findComponent.$name !== 'modal') {
17132
+ findComponent.$destroy(findComponent, false);
17080
17133
  }
17081
17134
  });
17082
17135
  }
@@ -17232,6 +17285,7 @@
17232
17285
  if (tmpRole) {
17233
17286
  attr(this.$el, 'role', tmpRole);
17234
17287
  }
17288
+ attr(this.$el, 'data-gn-uid', this._uid);
17235
17289
  // inherit selector class
17236
17290
  selector.className && addClass(this.$el, selector.className);
17237
17291
  }
@@ -17271,25 +17325,48 @@
17271
17325
  }
17272
17326
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
17273
17327
  $update(element = this.$el, e) { }
17274
- $event(component, name, ...params) {
17328
+ /**
17329
+ * 컴포넌트 이벤트 디스패치 헬퍼
17330
+ *
17331
+ * - sync/async 핸들러 모두 지원한다.
17332
+ * - 하나 이상의 핸들러에서 `false`를 반환하면 `true`(cancelled) 를 반환한다.
17333
+ * - 단순 알림용(fire-and-forget) 이벤트는 반환값/await 없이 호출해도 된다.
17334
+ */
17335
+ async $event(component, name, ...params) {
17275
17336
  const eventManager = GNCoreEventManager.getInstance();
17276
- eventManager.dispatch(component._uid, name, params);
17337
+ return eventManager.dispatch(component._uid, name, params);
17277
17338
  }
17278
17339
  $destroy(component = this, removeEl = true) {
17340
+ var _a;
17279
17341
  const stateManager = GNUIState.getInstance();
17280
17342
  // state manager 를 통해 destroy 상태 dispatch
17281
17343
  stateManager._detectedCycle(component._uid, 'destroy');
17282
- // remove Component in state manager
17283
- stateManager._removeComponent(component.$selector);
17284
17344
  // remove DOM (by removeEl)
17285
17345
  if (removeEl) {
17286
17346
  style(component.$el, 'display', 'none');
17287
17347
  remove(component.$el);
17288
17348
  }
17349
+ if (((_a = component.$options) === null || _a === void 0 ? void 0 : _a._destroy) && isFunction(component.$options._destroy)) {
17350
+ component.$options._destroy();
17351
+ }
17289
17352
  // state manager 를 통해 destroy 상태 dispatch
17290
17353
  stateManager._detectedCycle(component._uid, 'destroyed');
17291
- // event manager 에서 등록 해제가 가장 마지막..
17354
+ // event manager 에서 등록 해제
17292
17355
  GNCoreEventManager.getInstance().removeAll(component._uid);
17356
+ // state manager에서 component 제거 (uid 기반으로 효율적 제거)
17357
+ stateManager._removeComponentByUid(component._uid);
17358
+ // 메모리 누수 방지: component의 모든 hasOwnProperty 제거
17359
+ // config, events, methods, _hidden 등 동적으로 추가된 속성 포함
17360
+ Object.keys(component).forEach((key) => {
17361
+ try {
17362
+ component[key] = null;
17363
+ delete component[key];
17364
+ }
17365
+ catch (e) {
17366
+ // readonly 속성 등 삭제 불가능한 경우 무시
17367
+ }
17368
+ });
17369
+ component = null;
17293
17370
  }
17294
17371
  }
17295
17372
 
@@ -33490,8 +33567,19 @@
33490
33567
  });
33491
33568
  if (this.$options.value) {
33492
33569
  if (this.$options.multiple) {
33493
- const values = this.$options.value.split(',');
33494
- this.$options.value = this.$options.flatData.filter((opt) => values.includes(opt.value) && opt.text);
33570
+ // multiple 모드에서 다양한 타입의 value를 문자열 배열로 변환
33571
+ // 지원 타입: 문자열(쉼표 구분), 객체 배열, 단일 객체, 문자열 배열
33572
+ const values = typeof this.$options.value === 'string'
33573
+ ? this.$options.value.split(',') // 케이스 1: 'item1,item2,item3'
33574
+ : Array.isArray(this.$options.value)
33575
+ ? this.$options.value.map((v) => (typeof v === 'object' && v !== null && 'value' in v ? String(v.value) : String(v))) // 케이스 3: [{value:'item1', text:'항목1'}, ...] 또는 ['item1', 'item2']
33576
+ : typeof this.$options.value === 'object' && this.$options.value !== null && 'value' in this.$options.value
33577
+ ? [String(this.$options.value.value)] // 케이스 2: {value:'item1', text:'항목1'}
33578
+ : [String(this.$options.value)]; // 기타: 숫자 등
33579
+ this.$options.value = this.$options.flatData.filter((opt) => {
33580
+ const optValue = typeof opt.value === 'string' ? opt.value : String(opt.value);
33581
+ return values.includes(optValue) && opt.text;
33582
+ });
33495
33583
  }
33496
33584
  else {
33497
33585
  this.$options.value = this.$options.flatData.find((opt) => opt.value + '' === this.$options.value + '' && opt.text);
@@ -37006,26 +37094,8 @@
37006
37094
  }
37007
37095
  this.$event(this, 'onSort', column);
37008
37096
  },
37009
- renderHeader: (columns) => {
37010
- this.$options.hasOrder &&
37011
- !this.$options.readonly &&
37012
- columns.push({
37013
- label: this.$options.textSets.orderLabel,
37014
- key: 'btnOrder',
37015
- style: {
37016
- width: '50px'
37017
- }
37018
- });
37019
- this.$options.hasDelete &&
37020
- !this.$options.readonly &&
37021
- columns.push({
37022
- label: this.$options.textSets.deleteLabel,
37023
- key: 'btnDelete',
37024
- style: {
37025
- width: '30px'
37026
- }
37027
- });
37028
- this._setColumnsTemplate();
37097
+ renderHeader: (columns, isReset = false) => {
37098
+ this._setColumnsTemplate(isReset);
37029
37099
  return (createElement$1("div", { className: "gn-datagrid-header-row", style: {
37030
37100
  'grid-template-columns': this._columnsTemplate.join(' ')
37031
37101
  } },
@@ -37064,6 +37134,10 @@
37064
37134
  column.draggable && (this.$options.headers ? idx < this.$options.headers.length - 1 : true) && createElement$1("span", { className: "is-handle", "data-index": idx })));
37065
37135
  },
37066
37136
  renderBody: (data, columns) => {
37137
+ // 헤더가 숨겨진 경우에도 body 렌더 전에 템플릿 폭을 준비한다
37138
+ if (!this._columnsTemplate || !this._columnsTemplate.length) {
37139
+ this._setColumnsTemplate();
37140
+ }
37067
37141
  rowIdx$1 = 0;
37068
37142
  return (createElement$1("div", { className: "gn-datagrid-body", style: {
37069
37143
  maxHeight: this.$options.bodyHeight ? this.$options.bodyHeight : 'auto'
@@ -37077,7 +37151,6 @@
37077
37151
  });
37078
37152
  },
37079
37153
  renderRow: (row, columns, depth = 0, hasChild, isOpened, isCheck = false) => {
37080
- row._depth = depth;
37081
37154
  const _index = rowIdx$1++;
37082
37155
  if (row.isChecked) {
37083
37156
  isCheck = true;
@@ -37211,8 +37284,9 @@
37211
37284
  e.stopPropagation();
37212
37285
  toggler = parents(e.currentTarget, '.gn-datagrid-body-row');
37213
37286
  }
37214
- const children = nextUntil(toggler, '.gn-datagrid-body-row[data-depth="' + row._depth + '"]').filter((x) => {
37215
- return x.dataset.depth > row._depth;
37287
+ const rowDepth = Number(attr(toggler, 'data-depth')) || 0;
37288
+ const children = nextUntil(toggler, '.gn-datagrid-body-row[data-depth="' + rowDepth + '"]').filter((x) => {
37289
+ return Number(attr(x, 'data-depth')) > rowDepth;
37216
37290
  });
37217
37291
  type = type ? type : hasClass(toggler, 'is-collapsed') ? 'expand' : 'collapse';
37218
37292
  if (type === 'collapse') {
@@ -37228,7 +37302,7 @@
37228
37302
  //show childs
37229
37303
  removeClass(toggler, 'is-collapsed');
37230
37304
  removeClass(children.filter((x) => {
37231
- return x.dataset.depth == row._depth + 1;
37305
+ return Number(attr(x, 'data-depth')) == rowDepth + 1;
37232
37306
  }), 'is-hidden');
37233
37307
  this.$event(this, 'onToggle', 'expanded', row, index$1(toggler));
37234
37308
  }
@@ -37286,13 +37360,15 @@
37286
37360
  e.stopPropagation();
37287
37361
  const checker = parents(e.currentTarget, '.gn-datagrid-body-row');
37288
37362
  const checkerState = e.target.checked;
37289
- find('.is-allChecker', this.$el).checked = false;
37363
+ const allChecker = find('.is-allChecker', this.$el);
37364
+ allChecker && (allChecker.checked = false);
37365
+ const rowDepth = Number(attr(checker, 'data-depth')) || 0;
37290
37366
  // 1. row에 자식노드가 있는지 확인한다.
37291
37367
  if (this.$options.checkCapturing && row[this.$options.childField] && row[this.$options.childField].length) {
37292
37368
  // 2. 자식노드가 있는경우 자식 체크박스도 함께 토글한다.
37293
- nextUntil(checker, '.gn-datagrid-body-row[data-depth="' + row._depth + '"]')
37369
+ nextUntil(checker, '.gn-datagrid-body-row[data-depth="' + rowDepth + '"]')
37294
37370
  .filter((x) => {
37295
- return x.dataset.depth > row._depth;
37371
+ return Number(attr(x, 'data-depth')) > rowDepth;
37296
37372
  })
37297
37373
  .forEach((x) => {
37298
37374
  const _checker = find('.is-rowChecker', x);
@@ -37302,17 +37378,17 @@
37302
37378
  });
37303
37379
  }
37304
37380
  // 3. 체크 해제인 경우만 부모노드가 있는지 확인한다.
37305
- if (this.$options.checkCapturing && row._depth > 0 && !checkerState) {
37381
+ if (this.$options.checkCapturing && rowDepth > 0 && !checkerState) {
37306
37382
  // 4. 부모노드가 체크되어 있는지 확인한다
37307
37383
  const exeDepth = [];
37308
37384
  prevUntil(checker, '.gn-datagrid-body-row[data-depth="0"]')
37309
37385
  .filter((x) => {
37310
- const _thisDepth = x.dataset.depth;
37386
+ const _thisDepth = attr(x, 'data-depth');
37311
37387
  if (exeDepth.includes(_thisDepth)) {
37312
37388
  return false;
37313
37389
  }
37314
37390
  exeDepth.push(_thisDepth);
37315
- return _thisDepth < row._depth;
37391
+ return Number(_thisDepth) < rowDepth;
37316
37392
  })
37317
37393
  .forEach((x) => {
37318
37394
  const _checker = find('.is-rowChecker', x);
@@ -37328,9 +37404,21 @@
37328
37404
  if (hasCheck === undefined) {
37329
37405
  hasCheck = this.$options.hasCheck;
37330
37406
  }
37331
- this.$options.headers = headers;
37407
+ const prevHeaders = [...(this.$options.headers || [])];
37408
+ // _prepareHeaders가 배열을 변경하므로 비교용(prevHeaders)과 가공용(baseHeaders)을 분리한다
37409
+ const baseHeaders = headers ? [...headers] : [...prevHeaders];
37410
+ const preparedHeaders = this._prepareHeaders(baseHeaders);
37411
+ // 헤더 배열의 내용(길이, key)이 바뀐 경우에만 DOM 기반 폭 계산을 리셋한다
37412
+ const isReset = prevHeaders.length !== preparedHeaders.length || prevHeaders.some((header, idx) => { var _a; return header.key !== ((_a = preparedHeaders[idx]) === null || _a === void 0 ? void 0 : _a.key); });
37332
37413
  this.$options.hasCheck = hasCheck;
37333
- this.$template.reRender(find('.gn-datagrid-header-row', this.$el), this._hidden.renderHeader(this.$options.headers));
37414
+ const headerRow = find('.gn-datagrid-header-row', this.$el);
37415
+ // 헤더가 없을 때도 컬럼 수/폭 변경을 반영하기 위해 템플릿을 갱신한다
37416
+ if (this.$options.hasHeader && headerRow) {
37417
+ this.$template.reRender(headerRow, this._hidden.renderHeader(preparedHeaders, isReset));
37418
+ }
37419
+ else {
37420
+ this._setColumnsTemplate(isReset);
37421
+ }
37334
37422
  this._hidden.resetData(data ? arrClone(data) : this.$options.data);
37335
37423
  this.$render(this.$options);
37336
37424
  isFunction(resolve) && resolve();
@@ -37345,6 +37433,10 @@
37345
37433
  });
37346
37434
  },
37347
37435
  awaitData: (data) => {
37436
+ // asyncData 콜백 실행 중 컴포넌트가 destroy된 경우 안전하게 종료
37437
+ if (!this.$options || !this._hidden) {
37438
+ return;
37439
+ }
37348
37440
  if (this.$options.asyncData && this.$options.paginator && !this._paginator) {
37349
37441
  this._paginator = new Pagination('pagination', find('.gn-datagrid-footer', this.$el), {
37350
37442
  total: this.$options.paginator.total || 0,
@@ -37369,7 +37461,8 @@
37369
37461
  this._fixCellStyleOnDraggable();
37370
37462
  // 체크박스가 있는경우 전체 체크항목을 해제해준다
37371
37463
  if (this.$options.hasCheck) {
37372
- find('.is-allChecker', this.$el).checked = false;
37464
+ const allChecker = find('.is-allChecker', this.$el);
37465
+ allChecker && (allChecker.checked = false);
37373
37466
  }
37374
37467
  isFunction(resolve) && resolve();
37375
37468
  });
@@ -37404,8 +37497,18 @@
37404
37497
  stopRowSelectEvent: (e) => {
37405
37498
  e.stopPropagation();
37406
37499
  },
37407
- deleteRow: (index) => {
37408
- this.$options.data = this.$options.data.filter((_data, idx) => index !== idx);
37500
+ deleteRow: async (index) => {
37501
+ var _a;
37502
+ const confirmMessage = (_a = this.$options.textSets) === null || _a === void 0 ? void 0 : _a.deleteConfirmMessage;
37503
+ if (confirmMessage && !window.confirm(confirmMessage)) {
37504
+ return;
37505
+ }
37506
+ const removedData = this._hidden.findData(index);
37507
+ const cancelled = await this.$event(this, 'onDelete', removedData, index);
37508
+ if (cancelled) {
37509
+ return;
37510
+ }
37511
+ this._hidden.deleteData(index);
37409
37512
  this._hidden.resetData(this.$options.data);
37410
37513
  this.$event(this, 'onChange', this.$options.data);
37411
37514
  },
@@ -37440,22 +37543,38 @@
37440
37543
  col.offHover && col.offHover.call(this, row, col, index, e);
37441
37544
  },
37442
37545
  findData: (index) => {
37443
- let deter = 0, indexData = null;
37444
- const findIndex = (datas, index) => {
37445
- return datas.some((data) => {
37446
- if (index === deter) {
37447
- indexData = data;
37546
+ return this._hidden.walkByIndex(index, 'find');
37547
+ },
37548
+ deleteData: (index) => {
37549
+ return this._hidden.walkByIndex(index, 'delete');
37550
+ },
37551
+ walkByIndex: (index, action) => {
37552
+ let deter = 0;
37553
+ let result = null;
37554
+ const findIndex = (datas) => {
37555
+ for (let i = 0; i < datas.length; i++) {
37556
+ if (deter === index) {
37557
+ switch (action) {
37558
+ case 'find':
37559
+ result = datas[i];
37560
+ break;
37561
+ case 'delete':
37562
+ result = datas.splice(i, 1)[0];
37563
+ break;
37564
+ }
37448
37565
  return true;
37449
37566
  }
37450
- ++deter;
37451
- if (isArray$1(data[this.$options.childField]) && data[this.$options.childField].length) {
37452
- return findIndex(data[this.$options.childField], index);
37567
+ deter++;
37568
+ const children = datas[i][this.$options.childField];
37569
+ if (Array.isArray(children) && children.length) {
37570
+ if (findIndex(children))
37571
+ return true;
37453
37572
  }
37454
- return false;
37455
- });
37573
+ }
37574
+ return false;
37456
37575
  };
37457
- findIndex(this.$options.data, index);
37458
- return indexData;
37576
+ findIndex(this.$options.data);
37577
+ return result;
37459
37578
  },
37460
37579
  getChecked: () => {
37461
37580
  return findAll('.is-rowChecker', this.$el)
@@ -37542,11 +37661,13 @@
37542
37661
  hasOrder: false,
37543
37662
  hasDelete: false,
37544
37663
  isEllipsis: false,
37664
+ hasHeader: true,
37545
37665
  data: [],
37546
37666
  textSets: {
37547
37667
  noData: 'No records available.',
37548
37668
  orderLabel: '',
37549
- deleteLabel: ''
37669
+ deleteLabel: '',
37670
+ deleteConfirmMessage: ''
37550
37671
  },
37551
37672
  childField: 'child',
37552
37673
  checkCapturing: true,
@@ -37564,7 +37685,8 @@
37564
37685
  onCheck: true,
37565
37686
  onDoubleClick: true,
37566
37687
  onChange: true,
37567
- onDragEnd: true
37688
+ onDragEnd: true,
37689
+ onDelete: true
37568
37690
  };
37569
37691
  this.methods = {
37570
37692
  reRender(options) {
@@ -37624,18 +37746,21 @@
37624
37746
  this.$selector = this.$selector;
37625
37747
  this.$init(this, options);
37626
37748
  }
37627
- _setColumnsTemplate() {
37749
+ _setColumnsTemplate(isReset = false) {
37628
37750
  // header cell의 각 넓이를 배열로 가져온다
37629
37751
  // ! 모든 컬럼의 넓이가 지정된 경우 이동(btnOrder), 삭제(btnDelete) 컬럼을 제외한 마지막 컬럼은 1fr로 고정
37630
37752
  const _isfixedAllWidth = this.$options.headers.every((header) => { var _a; return ((_a = header.style) === null || _a === void 0 ? void 0 : _a.width) !== undefined; });
37631
37753
  const _fixedTemplateColumn = this.$options.headers.findLast((header) => !this._isSystemAddedColumn(header.key) && !header.isHidden);
37754
+ // 헤더 DOM이 없거나 모든 일반 컬럼이 숨겨진 경우에도 안전하게 비교할 수 있도록 key만 옵셔널하게 캐싱한다
37755
+ const _fixedTemplateKey = _fixedTemplateColumn === null || _fixedTemplateColumn === void 0 ? void 0 : _fixedTemplateColumn.key;
37632
37756
  const columns = findAll('.gn-datagrid-header-cell', this.$el);
37633
- if (this.$el && columns.length) {
37757
+ // isReset이면 기존 DOM 폭을 재사용하지 않고 헤더 정의로 재계산
37758
+ if (this.$el && columns.length && !isReset) {
37634
37759
  this._columnsTemplate = findAll('.gn-datagrid-header-cell', this.$el).map((header, idx) => {
37635
37760
  if (this.$options.headers[idx].isHidden) {
37636
37761
  return '';
37637
37762
  }
37638
- else if (_isfixedAllWidth && this.$options.headers[idx].key === _fixedTemplateColumn.key) {
37763
+ else if (_isfixedAllWidth && _fixedTemplateKey && this.$options.headers[idx].key === _fixedTemplateKey) {
37639
37764
  return '1fr';
37640
37765
  }
37641
37766
  else {
@@ -37649,7 +37774,7 @@
37649
37774
  if (header.isHidden) {
37650
37775
  return '';
37651
37776
  }
37652
- else if (_isfixedAllWidth && header.key === _fixedTemplateColumn.key) {
37777
+ else if (_isfixedAllWidth && _fixedTemplateKey && header.key === _fixedTemplateKey) {
37653
37778
  return '1fr';
37654
37779
  }
37655
37780
  else {
@@ -37670,18 +37795,45 @@
37670
37795
  _isSystemAddedColumn(key) {
37671
37796
  return ['btnOrder', 'btnDelete'].includes(key);
37672
37797
  }
37798
+ // 옵션에 따른 추가 해더 구성
37799
+ _prepareHeaders(headers = []) {
37800
+ const hasOrderColumn = headers.some((header) => header.key === 'btnOrder');
37801
+ const hasDeleteColumn = headers.some((header) => header.key === 'btnDelete');
37802
+ if (this.$options.hasOrder && !this.$options.readonly && !hasOrderColumn) {
37803
+ headers.push({
37804
+ label: this.$options.textSets.orderLabel,
37805
+ key: 'btnOrder',
37806
+ style: {
37807
+ width: '50px'
37808
+ }
37809
+ });
37810
+ }
37811
+ if (this.$options.hasDelete && !this.$options.readonly && !hasDeleteColumn) {
37812
+ headers.push({
37813
+ label: this.$options.textSets.deleteLabel,
37814
+ key: 'btnDelete',
37815
+ style: {
37816
+ width: '30px'
37817
+ }
37818
+ });
37819
+ }
37820
+ this.$options.headers = headers;
37821
+ return headers;
37822
+ }
37673
37823
  template(config) {
37674
37824
  const styles = {};
37825
+ const headers = this._prepareHeaders(config.headers);
37675
37826
  return (createElement$1("div", { id: this._uid, className: 'gn-datagrid' +
37676
37827
  (config.style ? ' is-' + config.style : '') +
37677
37828
  (config.isEllipsis ? ' is-ellipsis' : '') +
37678
37829
  (config.bodyHeight ? ' has-fixed-body' : '') +
37679
37830
  (config.fixHeader ? ' has-fixed-header' : '') +
37680
37831
  (config.fixFooter ? ' has-fixed-footer' : '') +
37832
+ (!config.hasHeader ? ' is-headless' : '') +
37681
37833
  (config.data.some((d) => isArray$1(d[this.$options.childField])) ? ' has-left-padding' : '') +
37682
37834
  (config.disabled ? ' is-disabled' : ''), style: styles },
37683
- createElement$1("div", { className: "gn-datagrid-header" }, this._hidden.renderHeader(config.headers)),
37684
- createElement$1("div", { className: "gn-datagrid-contents", style: { marginTop: this.$options.bodyTopMargin ? this.$options.bodyTopMargin : '0', marginBottom: this.$options.bodyBottomMargin ? this.$options.bodyBottomMargin : '0' } }, this._hidden.renderBody(arrClone(config.data), config.headers)),
37835
+ config.hasHeader && createElement$1("div", { className: "gn-datagrid-header" }, this._hidden.renderHeader(headers)),
37836
+ createElement$1("div", { className: "gn-datagrid-contents", style: { marginTop: this.$options.bodyTopMargin ? this.$options.bodyTopMargin : '0', marginBottom: this.$options.bodyBottomMargin ? this.$options.bodyBottomMargin : '0' } }, this._hidden.renderBody(arrClone(config.data), headers)),
37685
37837
  config.paginator /* 페이지네이터 옵션 확인 */ && createElement$1("div", { className: "gn-datagrid-footer" })));
37686
37838
  }
37687
37839
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -37698,18 +37850,20 @@
37698
37850
  }
37699
37851
  }
37700
37852
  completed() {
37701
- if (this.$options.fixHeader) {
37853
+ if (this.$options.fixHeader && this.$options.hasHeader) {
37702
37854
  const body = find('.gn-datagrid-contents', this.$el);
37703
37855
  const header = find('.gn-datagrid-header', this.$el);
37704
- const _offset = offset(header);
37705
- this.$options.bodyTopMargin = _offset.height ? _offset.height - 1 + 'px' : '2.4rem';
37706
- css$1(body, 'margin-top', this.$options.bodyTopMargin);
37856
+ if (header) {
37857
+ const _offset = offset(header);
37858
+ this.$options.bodyTopMargin = _offset.height ? _offset.height - 1 + 'px' : '2.4rem';
37859
+ css$1(body, 'margin-top', this.$options.bodyTopMargin);
37860
+ }
37707
37861
  if (this.$options.paginator) {
37708
37862
  this.$options.bodyBottomMargin = '2.4rem';
37709
37863
  css$1(body, 'margin-bottom', this.$options.bodyBottomMargin);
37710
37864
  }
37711
37865
  }
37712
- if (this.$options.fixHeader || this.$options.bodyHeight) {
37866
+ if ((this.$options.fixHeader && this.$options.hasHeader) || this.$options.bodyHeight) {
37713
37867
  this._hidden.setBlankHeader();
37714
37868
  on(window, 'resize', this._hidden.setBlankHeader);
37715
37869
  }
@@ -39054,7 +39208,9 @@
39054
39208
  super(name, selector, options);
39055
39209
  this._hidden = {
39056
39210
  open: () => {
39057
- addClass(this.$el, 'is-open');
39211
+ if (!this.$options.disabled) {
39212
+ addClass(this.$el, 'is-open');
39213
+ }
39058
39214
  },
39059
39215
  close: () => {
39060
39216
  removeClass(this.$el, 'is-open');
@@ -39066,6 +39222,62 @@
39066
39222
  changeText: (buttonText) => {
39067
39223
  this.$options.textSets.buttonText = buttonText;
39068
39224
  html(find('.menuButton-text', this.$el), buttonText);
39225
+ },
39226
+ disable: () => {
39227
+ this.$options.disabled = true;
39228
+ const buttonEl = find('button', this.$el);
39229
+ if (buttonEl) {
39230
+ attr(buttonEl, 'disabled', true);
39231
+ }
39232
+ addClass(this.$el, 'is-disabled');
39233
+ this._hidden.close();
39234
+ },
39235
+ enable: () => {
39236
+ this.$options.disabled = false;
39237
+ const buttonEl = find('button', this.$el);
39238
+ if (buttonEl) {
39239
+ removeAttr(buttonEl, 'disabled');
39240
+ }
39241
+ removeClass(this.$el, 'is-disabled');
39242
+ },
39243
+ renderMenus: (menus, depth = 0, parentPath = '') => {
39244
+ return (createElement$1("ul", { className: depth > 0 ? 'menuButton-submenu' : '' }, menus.map((menu, index) => {
39245
+ const hasChild = menu.child && isArray$1(menu.child) && menu.child.length > 0;
39246
+ const hasHtml = !!menu.html;
39247
+ // html이 있으면 innerHTML이 모든 자식 요소를 덮어쓰므로 서브메뉴를 렌더링하지 않음
39248
+ const canRenderChild = hasChild && depth < 2 && !hasHtml; // 최대 2단계까지만 허용
39249
+ const isDisabled = menu.disabled === true;
39250
+ const isActived = menu.actived === true;
39251
+ // 부모 경로를 포함한 고유한 ID 생성
39252
+ const currentPath = parentPath ? `${parentPath}-${index}` : `${index}`;
39253
+ const uniqueId = `${this._uid}-${currentPath}`;
39254
+ return (createElement$1("li", { id: uniqueId, className: 'menuButton-menu' +
39255
+ (this.$options.align ? ' has-text-' + this.$options.align : '') +
39256
+ (canRenderChild ? ' has-submenu' : '') +
39257
+ (depth > 0 ? ' is-submenu-item' : '') +
39258
+ (isDisabled ? ' is-disabled' : '') +
39259
+ (isActived ? ' is-actived' : ''), "on-click": (e) => {
39260
+ // disabled 상태이거나 자식 메뉴가 있는 경우 클릭 이벤트 처리하지 않음
39261
+ if (isDisabled) {
39262
+ e.stopPropagation();
39263
+ e.preventDefault();
39264
+ return;
39265
+ }
39266
+ // 자식 메뉴가 없는 경우에만 select 이벤트 발생
39267
+ if (!canRenderChild) {
39268
+ e.stopPropagation();
39269
+ this._hidden.select.call(this, menu, e);
39270
+ }
39271
+ }, innerHTML: hasHtml ? menu.html : '' },
39272
+ hasHtml ? ('') : (createElement$1("span", { className: "menuButton-menu-content" },
39273
+ createElement$1("span", { className: "menuButton-menu-text" }, menu.text),
39274
+ canRenderChild && (createElement$1("span", { className: "menuButton-menu-arrow" },
39275
+ createElement$1("i", { className: "fas fa-caret-right" }))))),
39276
+ canRenderChild && this._hidden.renderMenus.call(this, menu.child, depth + 1, currentPath)));
39277
+ })));
39278
+ },
39279
+ renderSub: (data) => {
39280
+ return createElement$1("div", null, isArray$1(data) && data.length && isArray$1(data[0]) ? data.map((menus) => this._hidden.renderMenus.call(this, menus)) : this._hidden.renderMenus.call(this, data));
39069
39281
  }
39070
39282
  };
39071
39283
  this.config = {
@@ -39085,6 +39297,19 @@
39085
39297
  },
39086
39298
  buttonText(text) {
39087
39299
  this._hidden.changeText(text);
39300
+ },
39301
+ reRender(data) {
39302
+ this.$options.data = data;
39303
+ const menuMenusEl = find('.menuButton-menus > div', this.$el);
39304
+ if (menuMenusEl && this.$template) {
39305
+ this.$template.reRender(menuMenusEl, this._hidden.renderSub.call(this, data));
39306
+ }
39307
+ },
39308
+ disabled() {
39309
+ this._hidden.disable();
39310
+ },
39311
+ enabled() {
39312
+ this._hidden.enable();
39088
39313
  }
39089
39314
  };
39090
39315
  this.$selector = this.$selector;
@@ -39095,23 +39320,19 @@
39095
39320
  if (config.width) {
39096
39321
  styles.width = getUnit('width', config.width);
39097
39322
  }
39098
- const renderMenus = (menus) => {
39099
- return (createElement$1("ul", null, menus.map((menu, index) => (createElement$1("li", { id: this._uid + '-' + index, className: 'menuButton-menu' + (config.align ? ' has-text-' + config.align : ''), "on-click": (e) => {
39100
- this._hidden.select.call(this, menu, e);
39101
- }, innerHTML: menu.html ? menu.html : '' }, menu.html ? '' : menu.text)))));
39102
- };
39103
- const renderSub = (data) => {
39104
- return createElement$1("div", null, isArray$1(data) && data.length && isArray$1(data[0]) ? data.map((menus) => renderMenus(menus)) : renderMenus(data));
39105
- };
39106
- return (createElement$1("div", { id: this._uid, className: 'gn-menuButton' + (config.color ? ' is-' + config.color : '') + (config.style ? ' is-' + config.style : '') + (config.size ? ' is-' + config.size : ''), style: styles },
39107
- createElement$1("button", { type: "button", className: config.align ? 'has-text-' + config.align : '', "on-click": this._hidden.open },
39323
+ return (createElement$1("div", { id: this._uid, className: 'gn-menuButton' +
39324
+ (config.color ? ' is-' + config.color : '') +
39325
+ (config.style ? ' is-' + config.style : '') +
39326
+ (config.size ? ' is-' + config.size : '') +
39327
+ (config.disabled ? ' is-disabled' : ''), style: styles },
39328
+ createElement$1("button", { type: "button", className: config.align ? 'has-text-' + config.align : '', disabled: config.disabled, "on-click": this._hidden.open },
39108
39329
  config.icon && (createElement$1("span", { className: 'gn-icon is-' + (config.size === 'large' ? 'medium' : config.size === 'medium' ? 'normal' : 'small') },
39109
39330
  createElement$1("i", { className: 'fas fa-' + config.icon }),
39110
39331
  ' ')),
39111
39332
  createElement$1("span", { className: "gn-icon is-small menuButton-icon" },
39112
39333
  createElement$1("i", { className: "fas fa-caret-down" })),
39113
39334
  createElement$1("span", { className: "menuButton-text" }, config.textSets.buttonText)),
39114
- createElement$1("div", { className: "menuButton-menus" }, renderSub(config.data))));
39335
+ createElement$1("div", { className: "menuButton-menus" }, this._hidden.renderSub.call(this, config.data))));
39115
39336
  }
39116
39337
  completed() {
39117
39338
  // 해당 컴포넌트 외 클릭 시 menu panel 숨김
@@ -39322,6 +39543,7 @@
39322
39543
  }
39323
39544
  }
39324
39545
 
39546
+ library$1.add(icons$1, icons);
39325
39547
  class Picklist extends GNCoreInstance {
39326
39548
  constructor(name, selector, options = {}) {
39327
39549
  super(name, selector, options);
@@ -39463,17 +39685,26 @@
39463
39685
  },
39464
39686
  renderSub: (item) => {
39465
39687
  const items = this.$options.data[item] || [];
39466
- return (createElement$1("ul", null, items.map((option, index) => (createElement$1("li", { id: this._uid + '_opt_' + index, className: 'dropdown-item' + (option.selected ? ' is-active' : ''), "data-value": option.value, "on-click": this._hidden.toggle.bind(this), "on-dblclick": this._hidden.move.bind(this, item === 'source' ? 'add' : 'remove', [
39467
- {
39468
- value: option.value,
39469
- text: option.text
39470
- }
39471
- ]) },
39472
- createElement$1("span", { className: "dropdown-text" }, option.text))))));
39688
+ return (createElement$1("ul", null, items.map((option, index) => {
39689
+ var _a;
39690
+ return (createElement$1("li", { id: this._uid + '_opt_' + index, className: 'dropdown-item' + (option.selected ? ' is-active' : ''), "data-value": option.value, "on-click": !option.text ? null : this._hidden.toggle.bind(this), "on-dblclick": !option.text
39691
+ ? null
39692
+ : this._hidden.move.bind(this, item === 'source' ? 'add' : 'remove', [
39693
+ {
39694
+ value: option.value,
39695
+ text: option.text,
39696
+ html: (_a = option.html) !== null && _a !== void 0 ? _a : null
39697
+ }
39698
+ ]) },
39699
+ createElement$1("span", { className: "dropdown-text", innerHTML: option.html ? option.html : '' }, option.html ? ('') : option.icon ? (createElement$1("span", null,
39700
+ createElement$1("span", { className: 'gn-icon' + (this.$options.size ? ' is-' + this.$options.size : '') },
39701
+ createElement$1("i", { className: (this.isBrandIcon(option.icon) ? 'fab' : 'fa') + ` fa-${option.icon}` })),
39702
+ escapeEntity(option.text))) : (escapeEntity(option.text)))));
39703
+ })));
39473
39704
  },
39474
39705
  getSelection: (target) => {
39475
39706
  return findAll('.is-active', this.$options.delegates[target]).map((select) => {
39476
- return { value: attr(select, 'data-value'), text: text$1(select) };
39707
+ return this.$options.data[target].find((option) => option.value === attr(select, 'data-value'));
39477
39708
  });
39478
39709
  },
39479
39710
  disable: () => {
@@ -39633,6 +39864,11 @@
39633
39864
  style(find('.picklist-target .dropdown-items', this.$el), 'height', getUnit('height', getNumber(this.$options.height) - getNumber(style(find('.picklist-target .picklist-caption', this.$el), 'height'))));
39634
39865
  }
39635
39866
  }
39867
+ isBrandIcon(iconName) {
39868
+ const iconLookup = { prefix: 'fab', iconName: iconName };
39869
+ const iconDefinition = findIconDefinition$1(iconLookup);
39870
+ return iconDefinition !== undefined;
39871
+ }
39636
39872
  }
39637
39873
 
39638
39874
  class Progressbar extends GNCoreInstance {
@@ -39791,6 +40027,409 @@
39791
40027
  }
39792
40028
  }
39793
40029
 
40030
+ const CLASS_DRAGGING = 'is-dragging';
40031
+ const CLASS_GROUP_DRAGGING = 'is-group-dragging';
40032
+ const CLASS_DRAG_OVER_TOP = 'is-drag-over-top';
40033
+ const CLASS_DRAG_OVER_BOTTOM = 'is-drag-over-bottom';
40034
+ const DEFAULT_NO_DATA = 'No records available.';
40035
+ const DRAG_STATE_CLASSES = [CLASS_DRAGGING, CLASS_GROUP_DRAGGING, CLASS_DRAG_OVER_TOP, CLASS_DRAG_OVER_BOTTOM];
40036
+ class SortableList extends GNCoreInstance {
40037
+ constructor(name, selector, options = {}) {
40038
+ super(name, selector, options);
40039
+ this._dragging = null;
40040
+ this._selection = new Set();
40041
+ this.DRAG_IMAGE_OFFSET_X = 0;
40042
+ this.DRAG_IMAGE_OFFSET_Y = 0;
40043
+ this.DRAG_OVER_THRESHOLD_RATIO = 0.5;
40044
+ this._hidden = {
40045
+ toggle: (e) => {
40046
+ if (this.$options.disabled) {
40047
+ return;
40048
+ }
40049
+ const value = attr(e.currentTarget, 'data-value');
40050
+ const hasItem = this.$options.data.some((option) => option.value === value);
40051
+ if (!value || !hasItem) {
40052
+ return;
40053
+ }
40054
+ if (this._selection.has(value)) {
40055
+ this._selection.delete(value);
40056
+ removeClass(e.currentTarget, 'is-active');
40057
+ }
40058
+ else {
40059
+ this._selection.add(value);
40060
+ addClass(e.currentTarget, 'is-active');
40061
+ }
40062
+ },
40063
+ sort: (dir) => {
40064
+ const items = this.$options.data || [];
40065
+ const selected = this._hidden.getSelection();
40066
+ if (!selected.length || selected.length === items.length) {
40067
+ return;
40068
+ }
40069
+ const selectedValues = new Set(selected.map((item) => item.value));
40070
+ // dir: up/down → 이동, up-all/down-all → 맨 위/맨 아래로 보내기
40071
+ if (dir.indexOf('all') > -1) {
40072
+ this.$options.data = items.slice().sort((a, b) => {
40073
+ const _sort = dir === 'up-all' ? -1 : 1;
40074
+ const aSel = selectedValues.has(a.value);
40075
+ const bSel = selectedValues.has(b.value);
40076
+ if (aSel && bSel) {
40077
+ return 0;
40078
+ }
40079
+ else if (aSel) {
40080
+ return _sort;
40081
+ }
40082
+ else if (bSel) {
40083
+ return _sort * -1;
40084
+ }
40085
+ return 0;
40086
+ });
40087
+ }
40088
+ else {
40089
+ if (dir === 'up') {
40090
+ const reordered = items.slice();
40091
+ // 한 번에 한 칸씩만 올려 상대 순서를 유지한다
40092
+ for (let i = 1; i < reordered.length; i++) {
40093
+ if (!selectedValues.has(reordered[i].value) || selectedValues.has(reordered[i - 1].value)) {
40094
+ continue;
40095
+ }
40096
+ const temp = reordered[i - 1];
40097
+ reordered[i - 1] = reordered[i];
40098
+ reordered[i] = temp;
40099
+ }
40100
+ this.$options.data = reordered;
40101
+ }
40102
+ else {
40103
+ let reordered = [];
40104
+ let itemsToMoveDown = [];
40105
+ items.forEach((option) => {
40106
+ // reordered: 최종 순서를 쌓는 버퍼, itemsToMoveDown: 아래로 밀어야 할 선택 항목 임시 저장
40107
+ // 비선택 항목은 흐름대로 push, 선택 항목은 dir에 따라 위/아래로 밀어 넣음
40108
+ if (!selectedValues.has(option.value)) {
40109
+ reordered.push(option);
40110
+ if (itemsToMoveDown.length) {
40111
+ reordered = reordered.concat(itemsToMoveDown);
40112
+ itemsToMoveDown = [];
40113
+ }
40114
+ }
40115
+ else if (dir === 'down') {
40116
+ itemsToMoveDown.push(option);
40117
+ }
40118
+ });
40119
+ if (itemsToMoveDown.length) {
40120
+ reordered = reordered.concat(itemsToMoveDown);
40121
+ itemsToMoveDown = [];
40122
+ }
40123
+ this.$options.data = reordered.slice();
40124
+ }
40125
+ }
40126
+ this._hidden.reRender();
40127
+ this._hidden.updateControls();
40128
+ this.$event(this, 'onChange', this.$options.data);
40129
+ },
40130
+ renderSub: () => {
40131
+ var _a, _b;
40132
+ const items = this.$options.data || [];
40133
+ const hasCols = items.some((item) => isArray$1(item.cols) && item.cols.length);
40134
+ if (!items.length) {
40135
+ return (createElement$1("ul", { className: "sortablelist-rows" },
40136
+ createElement$1("li", { className: "dropdown-item is-empty" }, (_b = (_a = this.$options.textSets) === null || _a === void 0 ? void 0 : _a.noData) !== null && _b !== void 0 ? _b : DEFAULT_NO_DATA)));
40137
+ }
40138
+ return (createElement$1("ul", { className: 'sortablelist-rows' + (hasCols ? ' is-cols' : '') }, items.map((option, index) => {
40139
+ var _a;
40140
+ return (createElement$1("li", { id: this._uid + '_opt_' + index, className: 'dropdown-item' + (this._selection.has(option.value) ? ' is-active' : ''), "data-value": option.value, draggable: this.$options.draggable && !this.$options.disabled ? true : null, "on-click": this._hidden.toggle.bind(this), "on-dragstart": this.$options.draggable ? this._hidden.dragStart.bind(this) : null, "on-dragover": this.$options.draggable ? this._hidden.dragOver.bind(this) : null, "on-dragleave": this.$options.draggable ? this._hidden.dragLeave.bind(this) : null, "on-drop": this.$options.draggable ? this._hidden.drop.bind(this) : null, "on-dragend": this.$options.draggable ? this._hidden.dragEnd.bind(this) : null }, hasCols && isArray$1(option.cols) && option.cols.length ? (createElement$1("div", { className: "sortablelist-cols" }, option.cols.map((col, colIndex) => (createElement$1("span", { className: "sortablelist-col", "data-col": colIndex }, escapeEntity(col)))))) : (createElement$1("span", { className: "dropdown-text" }, escapeEntity((_a = option.text) !== null && _a !== void 0 ? _a : option.value)))));
40141
+ })));
40142
+ },
40143
+ getSelection: () => {
40144
+ return this.$options.data.filter((option) => this._selection.has(option.value));
40145
+ },
40146
+ reRender: () => {
40147
+ const listContainer = find('ul', this.$options.delegates.list);
40148
+ listContainer && this.$template.reRender(listContainer, this._hidden.renderSub());
40149
+ },
40150
+ syncSelection: () => {
40151
+ const values = new Set(this.$options.data.map((item) => item.value));
40152
+ this._selection.forEach(val => {
40153
+ if (!values.has(val)) {
40154
+ this._selection.delete(val);
40155
+ }
40156
+ });
40157
+ },
40158
+ hydrateSelection: (items) => {
40159
+ this._selection.clear();
40160
+ items.forEach((item) => {
40161
+ if (item.selected) {
40162
+ this._selection.add(item.value);
40163
+ }
40164
+ });
40165
+ },
40166
+ validateData: (items) => {
40167
+ if (!isArray$1(items)) {
40168
+ throw new TypeError('Invalid SortableList data: data must be an array');
40169
+ }
40170
+ const seen = new Set();
40171
+ items.forEach((item) => {
40172
+ if (!item || typeof item.value !== 'string') {
40173
+ throw new TypeError('Invalid SortableList data: value must be string');
40174
+ }
40175
+ if (item.value === '') {
40176
+ throw new TypeError('Invalid SortableList data: value cannot be empty');
40177
+ }
40178
+ if (seen.has(item.value)) {
40179
+ throw new TypeError(`Invalid SortableList data: duplicate value '${item.value}'`);
40180
+ }
40181
+ seen.add(item.value);
40182
+ });
40183
+ },
40184
+ updateControls: () => {
40185
+ const hasData = (this.$options.data || []).length > 0;
40186
+ const disableButtons = this.$options.disabled || !hasData;
40187
+ if (disableButtons) {
40188
+ attr(findAll('button', this.$el), 'disabled', true);
40189
+ }
40190
+ else {
40191
+ removeAttr(findAll('button', this.$el), 'disabled');
40192
+ }
40193
+ if (!hasData) {
40194
+ addClass(this.$el, 'is-empty');
40195
+ }
40196
+ else {
40197
+ removeClass(this.$el, 'is-empty');
40198
+ }
40199
+ },
40200
+ clearDragState: () => {
40201
+ var _a;
40202
+ DRAG_STATE_CLASSES.forEach(className => {
40203
+ removeClass(findAll(`.${className}`, this.$el), className);
40204
+ });
40205
+ if ((_a = this._dragging) === null || _a === void 0 ? void 0 : _a.preview) {
40206
+ this._dragging.preview.remove();
40207
+ }
40208
+ this._dragging = null;
40209
+ },
40210
+ dragStart: (e) => {
40211
+ if (this.$options.disabled || !this.$options.draggable) {
40212
+ return;
40213
+ }
40214
+ // 기존 드래그 상태 정리 (이전 드래그가 중단된 경우 대비)
40215
+ this._hidden.clearDragState();
40216
+ const value = attr(e.currentTarget, 'data-value');
40217
+ if (!value) {
40218
+ return;
40219
+ }
40220
+ const selected = this._hidden.getSelection().map((item) => item.value);
40221
+ const isGroup = selected.length > 1 && selected.includes(value);
40222
+ const dragValues = isGroup ? selected : [value];
40223
+ this._dragging = { values: dragValues, isGroup: isGroup, preview: null };
40224
+ if (e.dataTransfer) {
40225
+ e.dataTransfer.setData('text/plain', value);
40226
+ e.dataTransfer.effectAllowed = 'move';
40227
+ if (isGroup) {
40228
+ const preview = document.createElement('div');
40229
+ preview.className = 'sortablelist-drag-preview';
40230
+ preview.setAttribute('data-count', String(dragValues.length));
40231
+ const list = document.createElement('div');
40232
+ list.className = 'sortablelist-drag-preview-list';
40233
+ dragValues.forEach((dragValue) => {
40234
+ const itemEl = find(`[data-value="${CSS.escape(dragValue)}"]`, this.$options.delegates.list);
40235
+ if (!(itemEl instanceof HTMLElement)) {
40236
+ return;
40237
+ }
40238
+ const clone = itemEl.cloneNode(true);
40239
+ removeClass(clone, CLASS_DRAGGING);
40240
+ removeClass(clone, CLASS_GROUP_DRAGGING);
40241
+ removeClass(clone, CLASS_DRAG_OVER_TOP);
40242
+ removeClass(clone, CLASS_DRAG_OVER_BOTTOM);
40243
+ addClass(clone, 'is-ghost');
40244
+ list.appendChild(clone);
40245
+ });
40246
+ preview.appendChild(list);
40247
+ document.body.appendChild(preview);
40248
+ e.dataTransfer.setDragImage(preview, this.DRAG_IMAGE_OFFSET_X, this.DRAG_IMAGE_OFFSET_Y);
40249
+ this._dragging.preview = preview;
40250
+ }
40251
+ }
40252
+ findAll('.dropdown-item', this.$options.delegates.list).forEach((node) => {
40253
+ const itemValue = attr(node, 'data-value');
40254
+ if (dragValues.includes(itemValue)) {
40255
+ addClass(node, CLASS_DRAGGING);
40256
+ if (isGroup) {
40257
+ addClass(node, CLASS_GROUP_DRAGGING);
40258
+ }
40259
+ }
40260
+ });
40261
+ },
40262
+ dragOver: (e) => {
40263
+ if (!this._dragging || this.$options.disabled || !this.$options.draggable) {
40264
+ return;
40265
+ }
40266
+ e.preventDefault();
40267
+ if (!(e.currentTarget instanceof HTMLElement)) {
40268
+ return;
40269
+ }
40270
+ const target = e.currentTarget;
40271
+ const value = attr(target, 'data-value');
40272
+ if (this._dragging.values.includes(value)) {
40273
+ return;
40274
+ }
40275
+ removeClass(target, CLASS_DRAG_OVER_TOP);
40276
+ removeClass(target, CLASS_DRAG_OVER_BOTTOM);
40277
+ const rect = target.getBoundingClientRect();
40278
+ const midpoint = rect.top + rect.height * this.DRAG_OVER_THRESHOLD_RATIO;
40279
+ if (e.clientY > midpoint) {
40280
+ addClass(target, CLASS_DRAG_OVER_BOTTOM);
40281
+ }
40282
+ else {
40283
+ addClass(target, CLASS_DRAG_OVER_TOP);
40284
+ }
40285
+ },
40286
+ dragLeave: (e) => {
40287
+ if (e.currentTarget instanceof HTMLElement) {
40288
+ removeClass(e.currentTarget, 'is-drag-over-top');
40289
+ removeClass(e.currentTarget, 'is-drag-over-bottom');
40290
+ }
40291
+ },
40292
+ drop: (e) => {
40293
+ if (!this._dragging || this.$options.disabled || !this.$options.draggable) {
40294
+ return;
40295
+ }
40296
+ e.preventDefault();
40297
+ if (!(e.currentTarget instanceof HTMLElement)) {
40298
+ return;
40299
+ }
40300
+ const targetEl = e.currentTarget;
40301
+ const targetValue = attr(targetEl, 'data-value');
40302
+ const dragValues = this._dragging.values;
40303
+ if (!targetValue || dragValues.includes(targetValue)) {
40304
+ this._hidden.clearDragState();
40305
+ return;
40306
+ }
40307
+ const items = this.$options.data;
40308
+ const dragItems = items.filter((item) => dragValues.includes(item.value));
40309
+ const remain = items.filter((item) => !dragValues.includes(item.value));
40310
+ const targetIndex = remain.findIndex((item) => item.value === targetValue);
40311
+ const rect = targetEl.getBoundingClientRect();
40312
+ const midpoint = rect.top + rect.height * this.DRAG_OVER_THRESHOLD_RATIO;
40313
+ const insertAfter = e.clientY > midpoint;
40314
+ const insertIndex = targetIndex === -1 ? remain.length : insertAfter ? targetIndex + 1 : targetIndex;
40315
+ remain.splice(insertIndex, 0, ...dragItems);
40316
+ this.$options.data = remain;
40317
+ this._hidden.reRender();
40318
+ this._hidden.updateControls();
40319
+ this.$event(this, 'onChange', this.$options.data);
40320
+ this._hidden.clearDragState();
40321
+ },
40322
+ dragEnd: () => {
40323
+ this._hidden.clearDragState();
40324
+ },
40325
+ setData: (data) => {
40326
+ // 드래그 중일 경우 상태 정리 (DOM 요소가 제거되면 dragend 이벤트가 발생하지 않을 수 있음)
40327
+ this._hidden.clearDragState();
40328
+ this._hidden.validateData(data);
40329
+ this.$options.data = data;
40330
+ this._hidden.hydrateSelection(data);
40331
+ this._hidden.syncSelection();
40332
+ this._hidden.reRender();
40333
+ this._hidden.updateControls();
40334
+ this.$event(this, 'onChange', this.$options.data);
40335
+ },
40336
+ disable: () => {
40337
+ // 드래그 중일 경우 상태 정리
40338
+ this._hidden.clearDragState();
40339
+ this.$options.disabled = true;
40340
+ addClass(this.$el, 'is-disabled');
40341
+ this._hidden.reRender();
40342
+ this._hidden.updateControls();
40343
+ },
40344
+ enable: () => {
40345
+ // 드래그 중일 경우 상태 정리
40346
+ this._hidden.clearDragState();
40347
+ this.$options.disabled = false;
40348
+ removeClass(this.$el, 'is-disabled');
40349
+ this._hidden.reRender();
40350
+ this._hidden.updateControls();
40351
+ }
40352
+ };
40353
+ this.config = {
40354
+ name: this.$selector.name || this._uid,
40355
+ data: [],
40356
+ delegates: {
40357
+ list: '.sortablelist-items'
40358
+ },
40359
+ buttonPosition: 'left',
40360
+ draggable: false,
40361
+ textSets: {
40362
+ noData: DEFAULT_NO_DATA
40363
+ },
40364
+ height: 150
40365
+ };
40366
+ this.events = {
40367
+ onChange: true
40368
+ };
40369
+ this.methods = {
40370
+ getData() {
40371
+ return this.$options.data;
40372
+ },
40373
+ setData(data) {
40374
+ const next = isArray$1(data) ? data : data && 'data' in data ? data.data : null;
40375
+ if (!isArray$1(next)) {
40376
+ throw new TypeError('Invalid SortableList data: data must be an array');
40377
+ }
40378
+ this._hidden.setData(next);
40379
+ },
40380
+ disable() {
40381
+ this._hidden.disable();
40382
+ },
40383
+ enable() {
40384
+ this._hidden.enable();
40385
+ }
40386
+ };
40387
+ this.$selector = this.$selector;
40388
+ this.$init(this, options);
40389
+ }
40390
+ template(config) {
40391
+ const styles = {};
40392
+ if (config.width) {
40393
+ styles.width = getUnit('width', config.width);
40394
+ }
40395
+ const listStyles = {};
40396
+ if (config.height) {
40397
+ const heightValue = getUnit('height', config.height);
40398
+ styles.height = heightValue;
40399
+ listStyles.height = heightValue;
40400
+ listStyles.maxHeight = heightValue;
40401
+ }
40402
+ const controlClass = 'sortablelist-controls gn-control is-small has-arrange is-vertical is-center' + (config.buttonPosition === 'right' ? ' is-right' : '');
40403
+ const buttonDisabled = config.disabled || (config.data || []).length === 0;
40404
+ return (createElement$1("div", { id: this._uid, className: 'gn-sortablelist' + (config.disabled ? ' is-disabled' : ''), style: styles },
40405
+ createElement$1("div", { className: controlClass },
40406
+ createElement$1("button", { type: "button", className: "gn-button is-outline", "on-click": this._hidden.sort.bind(this, 'up-all'), disabled: buttonDisabled },
40407
+ createElement$1("span", { className: "gn-icon" },
40408
+ createElement$1("i", { className: "fa fa-light fa-angle-double-up" }))),
40409
+ createElement$1("button", { type: "button", className: "gn-button is-outline", "on-click": this._hidden.sort.bind(this, 'up'), disabled: buttonDisabled },
40410
+ createElement$1("span", { className: "gn-icon" },
40411
+ createElement$1("i", { className: "fa fa-light fa-angle-up" }))),
40412
+ createElement$1("button", { type: "button", className: "gn-button is-outline", "on-click": this._hidden.sort.bind(this, 'down'), disabled: buttonDisabled },
40413
+ createElement$1("span", { className: "gn-icon" },
40414
+ createElement$1("i", { className: "fa fa-light fa-angle-down" }))),
40415
+ createElement$1("button", { type: "button", className: "gn-button is-outline", "on-click": this._hidden.sort.bind(this, 'down-all'), disabled: buttonDisabled },
40416
+ createElement$1("span", { className: "gn-icon" },
40417
+ createElement$1("i", { className: "fa fa-light fa-angle-double-down" })))),
40418
+ createElement$1("div", { className: "gn-dropdown is-opened sortablelist-items" },
40419
+ createElement$1("div", { className: "dropdown-items", style: listStyles }, this._hidden.renderSub()))));
40420
+ }
40421
+ beforeMount() {
40422
+ this._hidden.validateData(this.$options.data);
40423
+ this._hidden.hydrateSelection(this.$options.data);
40424
+ }
40425
+ completed() {
40426
+ this._hidden.updateControls();
40427
+ }
40428
+ destroyed() {
40429
+ this._hidden.clearDragState();
40430
+ }
40431
+ }
40432
+
39794
40433
  class Splitter extends GNCoreInstance {
39795
40434
  constructor(name, selector, options = {}) {
39796
40435
  super(name, selector, options);
@@ -40438,6 +41077,7 @@
40438
41077
  picklist: Picklist,
40439
41078
  progressbar: Progressbar,
40440
41079
  selectbutton: SelectButton,
41080
+ sortablelist: SortableList,
40441
41081
  splitter: Splitter,
40442
41082
  switch: Switch,
40443
41083
  syntaxinput: SyntaxInput,