ninegrid2 6.315.0 → 6.317.0

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.
@@ -11,7 +11,6 @@ import { ngDataManager } from "./ngDataManager.js";
11
11
  import { ngView } from "./ngView.js";
12
12
  import { ngTableWrapper } from "./ngTableWrapper.js";
13
13
  import { ngColumns } from "./ngColumns.js";
14
- import { ngFiltering } from "./ngFiltering.js";
15
14
  import { ngSorting } from "./ngSorting.js";
16
15
  import { ngMatrixManager } from "./ngMatrixManager.js";
17
16
  import { ngColResizer } from "./ngColResizer.js";
@@ -19,6 +18,8 @@ import { ngColMover } from "./ngColMover.js";
19
18
  import { ngRowMover } from "./ngRowDrag.js";
20
19
  import { ngCustomEvent } from "./ngCustomEvent.js";
21
20
 
21
+ import { ngFiltering } from "../utils/ngFiltering.js";
22
+
22
23
  class ninegridContainer extends HTMLElement
23
24
  {
24
25
  #colResizer;
package/dist/index.js CHANGED
@@ -54,7 +54,6 @@ import "./etc/ngDataManager.js";
54
54
  import "./etc/ngExpandIcon.js";
55
55
  import "./etc/ngExport.js";
56
56
  import "./etc/ngFields.js";
57
- import "./etc/ngFiltering.js";
58
57
  import "./etc/ngFoot.js";
59
58
  import "./etc/ngHead.js";
60
59
  import "./etc/ngIcon.js";
@@ -99,6 +98,9 @@ import "./etc/nxTest.js";
99
98
  import "./etc/nxTopMenu.js";
100
99
  //import "./etc/object-observe.js";
101
100
 
101
+ import "./utils/ngFiltering.js";
102
+
103
+
102
104
  import "./ai/aiSettings.js";
103
105
  import "./ai/aiMessage.js";
104
106
 
@@ -333,6 +333,18 @@ export class ninegrid {
333
333
  static nvl = (v, v2) => {
334
334
  return ninegrid.isNvl(v) ? v2 : v;
335
335
  };
336
+
337
+ static isEllipsis = (element) => {
338
+ const clone = element.cloneNode(true);
339
+ clone.style.width = "auto";
340
+ clone.style.visibility = "hidden";
341
+ document.body.appendChild(clone);
342
+
343
+ const isTruncated = clone.scrollWidth > element.clientWidth;
344
+ document.body.removeChild(clone);
345
+
346
+ return isTruncated;
347
+ }
336
348
 
337
349
  static oppositeColor = (bgColor, lightColor, darkColor) => {
338
350
  var color = (bgColor.charAt(0) === '#') ? bgColor.substring(1, 7) : bgColor;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "ninegrid2",
3
3
  "type": "module",
4
- "version": "6.315.0",
4
+ "version": "6.317.0",
5
5
  "main": "dist/index.js",
6
6
  "exports": {
7
7
  "import": "./dist/index.js",
@@ -19,9 +19,9 @@ class aiMessage extends HTMLElement
19
19
  </style>
20
20
 
21
21
  <div class="chat-message">
22
- <div title="${this.#message}" class="message">
22
+ <span title="${this.#message}" class="message">
23
23
  ${this.#message}
24
- </div>
24
+ </span>
25
25
  <span class="more" style="position:relative;"><a href="#">more</a></span>
26
26
  </div>
27
27
 
@@ -45,6 +45,8 @@ class aiMessage extends HTMLElement
45
45
  console.log(v);;
46
46
  }
47
47
 
48
+
49
+
48
50
  #init = () => {
49
51
 
50
52
  const html = `
@@ -81,27 +83,7 @@ class aiMessage extends HTMLElement
81
83
  alert(this.#message);
82
84
  });
83
85
 
84
- setTimeout(() => {
85
- const elMessage = this.shadowRoot.querySelector(".message");
86
- const elMore = this.shadowRoot.querySelector(".more");
87
-
88
- console.log(elMessage.scrollWidth, elMessage.clientWidth);
89
-
90
- elMore.style.display = "flex";
91
- //elMore.style.position = "absolute";
92
-
93
- console.log(elMessage.scrollWidth, elMessage.clientWidth);
94
-
95
- //elMore.style.display = (elMessage.scrollWidth > elMessage.clientWidth) ? "flex" : "none";
96
- if (elMessage.scrollWidth > elMessage.clientWidth) {
97
- //elMore.style.display = "flex";
98
- //elMore.style.position = "absolute";
99
- }
100
- else {
101
- //elMore.style.display = "none";
102
- }
103
- }, 100);
104
-
86
+ this.shadowRoot.querySelector(".more").style.display = (ninegrid.isEllipsis(this.shadowRoot.querySelector(".message"))) ? "flex" : "none";
105
87
  };
106
88
  }
107
89
 
@@ -11,7 +11,6 @@ import { ngDataManager } from "./ngDataManager.js";
11
11
  import { ngView } from "./ngView.js";
12
12
  import { ngTableWrapper } from "./ngTableWrapper.js";
13
13
  import { ngColumns } from "./ngColumns.js";
14
- import { ngFiltering } from "./ngFiltering.js";
15
14
  import { ngSorting } from "./ngSorting.js";
16
15
  import { ngMatrixManager } from "./ngMatrixManager.js";
17
16
  import { ngColResizer } from "./ngColResizer.js";
@@ -19,6 +18,8 @@ import { ngColMover } from "./ngColMover.js";
19
18
  import { ngRowMover } from "./ngRowDrag.js";
20
19
  import { ngCustomEvent } from "./ngCustomEvent.js";
21
20
 
21
+ import { ngFiltering } from "../utils/ngFiltering.js";
22
+
22
23
  class ninegridContainer extends HTMLElement
23
24
  {
24
25
  #colResizer;
package/src/index.js CHANGED
@@ -54,7 +54,6 @@ import "./etc/ngDataManager.js";
54
54
  import "./etc/ngExpandIcon.js";
55
55
  import "./etc/ngExport.js";
56
56
  import "./etc/ngFields.js";
57
- import "./etc/ngFiltering.js";
58
57
  import "./etc/ngFoot.js";
59
58
  import "./etc/ngHead.js";
60
59
  import "./etc/ngIcon.js";
@@ -99,6 +98,9 @@ import "./etc/nxTest.js";
99
98
  import "./etc/nxTopMenu.js";
100
99
  //import "./etc/object-observe.js";
101
100
 
101
+ import "./utils/ngFiltering.js";
102
+
103
+
102
104
  import "./ai/aiSettings.js";
103
105
  import "./ai/aiMessage.js";
104
106
 
@@ -0,0 +1,392 @@
1
+ import ninegrid from "../index.js";
2
+
3
+ /**
4
+ * button.filterOptions = [{
5
+ * colnm: "userId",
6
+ * data: [],
7
+ * }]
8
+ * button.
9
+ */
10
+ export class ngFiltering
11
+ {
12
+ #owner;
13
+ #isFiltering;
14
+
15
+ constructor(owner) {
16
+ this.#owner = owner;
17
+ this.#isFiltering = false;
18
+
19
+ const filterPanel = document.createElement("ng-filter-panel"); // ✅ 필터 패널 생성
20
+ filterPanel.style.display = "none"; // ✅ 숨김 처리
21
+ this.#owner.shadowRoot.appendChild(filterPanel); // ✅ Shadow DOM 내부에 추가
22
+ }
23
+
24
+ initialize = () => {
25
+ /**
26
+ * 1. grid attr == filter : on();
27
+ */
28
+ this.#isFiltering = false;
29
+ this.#owner.data.clearFilter();
30
+ };
31
+
32
+ isFiltering = () => {
33
+ return this.#isFiltering;
34
+ };
35
+
36
+ on = (v) => {
37
+
38
+ this.#isFiltering = true;
39
+ const lastRowIndex = this.#getLastRowIndex();
40
+
41
+ this.#owner.body.querySelectorAll(".ng-table thead th, .ng-table thead td").forEach((td) => {
42
+ const rowIndex = td.closest("tr")?.sectionRowIndex;
43
+ if (rowIndex !== undefined && rowIndex + td.rowSpan - 1 === lastRowIndex) {
44
+ const options = Array.from(this.#owner.activeTmpl.querySelectorAll(`[data-col="${td.dataset.col}"]`))
45
+ .map(el => el.dataset.bind ? { colnm: el.dataset.bind, data: [] } : null)
46
+ .filter(Boolean); // ✅ `null` 제거
47
+
48
+ if (options.length > 0) {
49
+ td.querySelector("ng-filter-button")?.remove();
50
+
51
+ const filterButton = document.createElement("ng-filter-button");
52
+ td.appendChild(filterButton);
53
+ filterButton.filterOptions = options;
54
+ }
55
+ }
56
+ });
57
+ };
58
+
59
+ off = () => {
60
+ this.#isFiltering = false;
61
+
62
+ this.#owner.body.querySelectorAll(".ng-table ng-filter-button").forEach((el) => {
63
+ el.remove(); // ✅ 요소 삭제
64
+ });
65
+ };
66
+
67
+ /**
68
+ clear = () => {
69
+ console.log(this.#owner.dataManager.rawRecords);
70
+ this.#owner.dataManager.rawRecords.map(item => { item.__ng.filtered = false; });
71
+
72
+
73
+ this.#records = this.#owner.dataManager.rawRecords.filter(item => { return !item.__ng.deleted && !item.__ng.filtered; } );
74
+ return this.#records;
75
+ }
76
+
77
+ refresh = () => {
78
+ this.#records = this.#owner.dataManager.rawRecords.filter(item => { return !item.__ng.deleted && !item.__ng.filtered; } );
79
+ //this.#records.forEach((o,i) => { o.__ng.rowidx = i; });
80
+ //this.getValidData().forEach((o,i) => { o.__ng._[ninegrid.ROW.ORDER] = i + 1; });
81
+ this.#resetOrder();
82
+ return this.#records;
83
+ } */
84
+
85
+
86
+ /**
87
+ * oFilter : {
88
+ * col1 : [],
89
+ * col2 : []
90
+ * }
91
+ */
92
+ set = (oFilter) => {
93
+
94
+ this.on();
95
+
96
+ // ✅ JSON 변환 (배열 → 객체)
97
+ let jsonFilter = Array.isArray(oFilter)
98
+ ? Object.fromEntries(Object.keys(oFilter[0]).map(key => [key, [...new Set(oFilter.map(item => item[key]))]]))
99
+ : oFilter;
100
+
101
+ this.#owner.data.clearFilter();
102
+
103
+ Object.entries(jsonFilter).forEach(([key, arr]) => {
104
+ const idx = this.#owner.fields.indexOf(key);
105
+
106
+ // ✅ 숫자 판별 및 변환
107
+ if (this.#owner.data.getValidData().some(o => typeof o.v[idx] === "number")) {
108
+ arr = arr.map(Number);
109
+ }
110
+
111
+ // ✅ 필터 적용
112
+ this.#owner.data.getValidData()
113
+ .filter(m => arr.nineBinarySearch(m.v[idx] || '') < 0)
114
+ .forEach(m => { m.__ng.filtered = true; });
115
+ });
116
+
117
+
118
+ this.#owner.data.refreshFilter();
119
+
120
+ // ✅ 필터 버튼 초기화
121
+ this.#owner.shadowRoot.querySelectorAll("ng-filter-button").forEach(el => {
122
+ el.filterOptions.forEach(opt => { opt.data = []; });
123
+ });
124
+
125
+ Object.entries(jsonFilter).forEach(([key, arr]) => {
126
+ this.#owner.shadowRoot.querySelectorAll("ng-filter-button").forEach(el => {
127
+
128
+ let options = el.filterOptions;
129
+
130
+ options.forEach(opt => {
131
+ if (opt.colnm === key) opt.data = arr;
132
+ });
133
+
134
+ el.filterOptions = options;
135
+ });
136
+ });
137
+
138
+
139
+ this.#owner.scrollTo_V1(0);
140
+
141
+ this.#owner.paging.reset();
142
+ };
143
+
144
+ #getLastRowIndex = (v = "thead") =>
145
+ [...this.#owner.body.querySelectorAll(`.ng-table ${v}`)]
146
+ .reduce((maxIndex, el) => Math.max(el.rows.length - 1, maxIndex), 0);
147
+
148
+ }
149
+
150
+ class ngFilterButton extends HTMLElement
151
+ {
152
+ #owner;
153
+ #filterOptions;
154
+
155
+ constructor () {
156
+ super();
157
+ }
158
+
159
+ connectedCallback() {
160
+ this.#owner = this.getRootNode().host;//this.closest("nine-grid");
161
+
162
+ this.removeEventListener("click", this.#onClick);
163
+ this.addEventListener("click", this.#onClick);
164
+ };
165
+
166
+ get filterOptions() {
167
+ return this.#filterOptions;
168
+ };
169
+ set filterOptions(v) {
170
+ this.#filterOptions = v;
171
+ this.classList.toggle('filtered', this.#filterOptions.some(item => item.data.length > 0));
172
+ }
173
+
174
+ #onClick = (e) => {
175
+ e.preventDefault();
176
+ e.stopPropagation();
177
+
178
+ const panel = this.#owner.shadowRoot.querySelector("ng-filter-panel");
179
+ const col = this.closest("th,td")?.dataset.col; // ✅ `?.` 활용하여 안전한 접근
180
+
181
+ panel.col === col ? panel.close() : panel.open(this);
182
+ };
183
+ }
184
+
185
+ class ngFilterPanel extends HTMLElement
186
+ {
187
+ #owner;
188
+ #button;
189
+ #timer;
190
+
191
+ constructor () {
192
+ super();
193
+ this.attachShadow({ mode: 'open' });
194
+ }
195
+
196
+ connectedCallback() {
197
+
198
+ this.#owner = this.getRootNode().host;
199
+
200
+ const cssPath = this.getRootNode().host.closest("nine-grid").getAttribute("css-path") || "";
201
+
202
+ this.shadowRoot.innerHTML = `
203
+ <style>
204
+ @import "https://cdn.jsdelivr.net/npm/ninegrid@${ninegrid.version}/dist/css/ngFilterPanel.css";
205
+ ${ninegrid.getCustomPath(this,"ngFilterPanel.css")}
206
+ </style>
207
+
208
+ <div class="head">
209
+ <input type="text">
210
+ </div>
211
+ <nine-grid class="simple filter" css-path="${cssPath}" select-type="row" auto-fit-col="true" col-indicator-type="collapse">
212
+ <table style="display: none;">
213
+ <colgroup>
214
+ <col width="180" />
215
+ <col width="30" />
216
+ </colgroup>
217
+ <tbody>
218
+ <tr style="height: 24px;">
219
+ <td data-bind="LVL" data-expr="data.DATA2">
220
+ <ng-tree-item />
221
+ </td>
222
+ <td data-bind="CHK">
223
+ <ng-checkbox update-row-state="false" visible="data.LVL!=1" selected-border-color="white" />
224
+ </td>
225
+ </tr>
226
+ </tbody>
227
+ </table>
228
+ </nine-grid>
229
+
230
+ <div>
231
+ <label><input type="checkbox" checked>Select All</label>
232
+ <button id="btnOk">OK</button>
233
+ <button id="btnCancel">Cancel</button>
234
+ </div>
235
+ `;
236
+
237
+ this.shadowRoot.querySelector("input[type=text]").addEventListener("input", this.#onInput);
238
+ this.shadowRoot.querySelector("input[type=checkbox]").addEventListener("change", this.#onSelectAll);
239
+ this.shadowRoot.querySelector("#btnOk").addEventListener("click", this.#onOk);
240
+ this.shadowRoot.querySelector("#btnCancel").addEventListener("click", this.#onCancel);
241
+ };
242
+
243
+ #onOk = (e) => {
244
+
245
+ const grd = this.shadowRoot.querySelector("nine-grid");
246
+
247
+ this.classList.add("loading");
248
+
249
+ setTimeout(() => {
250
+ const [LVL_IDX, CHK_IDX, COLNM_IDX, DATA_IDX] = ["LVL", "CHK", "COLNM", "DATA"].map(k => grd.fields.indexOf(k));
251
+ const checked = grd.data.getValidDataNF().filter(m => m.v[LVL_IDX] === 2 && m.v[CHK_IDX] === "Y");
252
+ const filterOptions = checked.length > 0 ? [...new Set(checked.map(m => m.v[COLNM_IDX]))].map(v => ({
253
+ colnm: v,
254
+ data: [...new Set(grd.data.getValidData().filter(m => m.v[CHK_IDX] === "Y" && m.v[COLNM_IDX] === v).map(m => m.v[DATA_IDX] || ''))].sort()
255
+ })) : [];
256
+
257
+
258
+ this.#button.filterOptions = filterOptions;
259
+
260
+ const oParam = Object.fromEntries(
261
+ [...this.#owner.body.querySelectorAll("ng-filter-button")]
262
+ .flatMap(el => el.filterOptions)
263
+ .filter(opt => opt.data.length > 0)
264
+ .map(opt => [opt.colnm, opt.data])
265
+ );
266
+
267
+ this.#owner.filtering.set(oParam);
268
+
269
+ this.classList.remove("loading");
270
+ this.style.display = 'none';
271
+ });
272
+ };
273
+
274
+
275
+
276
+ #onCancel = (e) => {
277
+ this.style.display = 'none';
278
+ };
279
+
280
+ #onSelectAll = (e) => {
281
+ const grd = this.shadowRoot.querySelector("nine-grid");
282
+ const idx = grd.fields.indexOf("CHK");
283
+ const isChecked = e.target.checked; // ✅ jQuery 없이 `checked` 값 가져오기
284
+
285
+ grd.data.getValidData().forEach(m => {
286
+ m.v[idx] = isChecked ? "Y" : "N"; // ✅ `Y` 또는 `N` 값 설정
287
+ });
288
+
289
+ grd.refreshData();
290
+ };
291
+
292
+
293
+
294
+ #onInput = (e) => {
295
+ const grd = this.shadowRoot.querySelector("nine-grid");
296
+
297
+ grd.classList.add("loading");
298
+
299
+ const data = grd.dataManager.rawRecords;
300
+ data.forEach(m => { m.__ng.filtered = false; });
301
+
302
+ const v = e.target.value.toLowerCase(); // ✅ jQuery 없이 값 가져오기
303
+ const [LVL_IDX, DATA_IDX] = ["LVL", "DATA"].map(field => grd.fields.indexOf(field));
304
+
305
+ data.forEach(m => {
306
+ m.__ng.filtered = m.v[LVL_IDX] === 2 && !String(m.v[DATA_IDX] || "").toLowerCase().includes(v);
307
+ });
308
+
309
+ grd.data.resetRecords();
310
+
311
+ clearTimeout(this.#timer);
312
+ this.#timer = setTimeout(() => {
313
+ grd.dataManager.viewRecords.reset();
314
+ grd.dataManager.viewRecords.touch();
315
+ }, 200)
316
+ };
317
+
318
+ close = () => {
319
+ this.col = null;
320
+ this.style.display = 'none';
321
+ };
322
+
323
+ open = (filterButton) => {
324
+
325
+ /** 위치 */
326
+ const cell = filterButton.closest("th,td");
327
+
328
+ const { left: btnLeft } = filterButton.getBoundingClientRect();
329
+ const { left: ownerLeft, width: ownerWidth, top: ownerTop } = this.#owner.getBoundingClientRect();
330
+ const { top: cellTop, height: cellHeight } = cell.getBoundingClientRect();
331
+ const { width: targetWidth } = this.getBoundingClientRect();
332
+
333
+ let l = Math.max(0, btnLeft - ownerLeft);
334
+ l = Math.min(l, ownerWidth - targetWidth - 5);
335
+
336
+ const t = cellTop + cellHeight - ownerTop;
337
+
338
+ Object.assign(this.style, { left: `${l}px`, top: `${t}px`, display: "flex" });
339
+ this.classList.add("loading");
340
+
341
+ this.shadowRoot.querySelector("input[type=text]").value = "";
342
+
343
+ setTimeout(() => {
344
+ const data = this.#owner.data.getValidDataNF();
345
+ const col = cell.dataset.col;
346
+ this.col = col;
347
+ this.#button = filterButton;
348
+
349
+ const ds = filterButton.filterOptions.map((opt, i) => {
350
+ const groupLabel = `<span class="group">${cell.textContent}${filterButton.filterOptions.length > 1 ? ` #${i + 1} (${opt.colnm})` : ""}</span>`;
351
+
352
+ const cellEl = this.#owner.activeTmpl.querySelector(`[data-col="${col}"][data-bind="${opt.colnm}"]`);
353
+ const expr = cellEl?.getAttribute("data-expr");
354
+ const exprFunc = expr ? this.#owner.exprFunction(expr) : null;
355
+
356
+ const data2 = data.map(rowData => {
357
+ const idx = this.#owner.fields.indexOf(opt.colnm);
358
+ return expr ? { v: rowData.v[idx], v2: exprFunc(this.#owner.data.conv(rowData), rowData.__ng.rowidx, this.#owner.data) } : { v: rowData.v[idx], v2: rowData.v[idx] };
359
+ });
360
+
361
+ return [
362
+ { LVL: 1, CHK: "N", DATA2: groupLabel },
363
+ ...[...new Set(data2.map(JSON.stringify))]
364
+ .map(JSON.parse)
365
+ .sort((a, b) => (a.v2 || "") > (b.v2 || "") ? 1 : (a.v2 || "") < (b.v2 || "") ? -1 : 0)
366
+ .map(o => ({
367
+ LVL: 2,
368
+ DATA: o.v,
369
+ DATA2: o.v2 || ` <span class="empty">(empty)</span> ${o.v}`,
370
+ COLNM: opt.colnm,
371
+ CHK: opt.data.length === 0 || opt.data.nineBinarySearch(o.v || "") >= 0 ? "Y" : "N",
372
+ }))
373
+ ];
374
+ }).flat();
375
+
376
+ const grd = this.shadowRoot.querySelector("nine-grid");
377
+ grd.fields.add(["DATA","DATA2","COLNM"]);
378
+ grd.data.set(ds);
379
+
380
+ // ✅ 데이터 필터링 및 체크 상태 결정
381
+ const checkbox = this.shadowRoot.querySelector("input[type=checkbox]");
382
+ checkbox.checked = grd.data.getValidData().every(item => !(item.LVL === 2 && item.CHK !== "Y"));
383
+
384
+ this.shadowRoot.querySelector("input").focus();
385
+ this.classList.remove("loading");
386
+ });
387
+ };
388
+ }
389
+
390
+
391
+ customElements.define("ng-filter-button", ngFilterButton);
392
+ customElements.define("ng-filter-panel", ngFilterPanel);
@@ -333,6 +333,18 @@ export class ninegrid {
333
333
  static nvl = (v, v2) => {
334
334
  return ninegrid.isNvl(v) ? v2 : v;
335
335
  };
336
+
337
+ static isEllipsis = (element) => {
338
+ const clone = element.cloneNode(true);
339
+ clone.style.width = "auto";
340
+ clone.style.visibility = "hidden";
341
+ document.body.appendChild(clone);
342
+
343
+ const isTruncated = clone.scrollWidth > element.clientWidth;
344
+ document.body.removeChild(clone);
345
+
346
+ return isTruncated;
347
+ }
336
348
 
337
349
  static oppositeColor = (bgColor, lightColor, darkColor) => {
338
350
  var color = (bgColor.charAt(0) === '#') ? bgColor.substring(1, 7) : bgColor;