@gridnexa/angular 0.0.1 → 0.0.3

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.
package/README.md CHANGED
@@ -1,10 +1,33 @@
1
1
  # @gridnexa/angular
2
2
 
3
- Angular package for GridNexa.
3
+ Enterprise Angular data grid for modern UI products that need spreadsheet-grade power without giving up product polish.
4
+
5
+ GridNexa is built for React, Angular, Vue, and JavaScript teams. The Angular package gives you a native Angular grid with typed columns, row selection, row numbers, sorting, filtering, advanced filters, formulas, inline editing, grouped headers, grouping, pivoting, tree data, master/detail, CSV export, Excel export, and theme-ready UI.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ npm install @gridnexa/angular
11
+ ```
12
+
13
+ ```bash
14
+ pnpm add @gridnexa/angular
15
+ ```
16
+
17
+ ## Usage
4
18
 
5
19
  ```ts
6
20
  import { Component } from "@angular/core";
7
- import { GridNexaAngularComponent } from "@gridnexa/angular";
21
+ import { GridNexaAngularComponent, type Column } from "@gridnexa/angular";
22
+
23
+ interface Employee {
24
+ id: number;
25
+ name: string;
26
+ department: string;
27
+ city: string;
28
+ score: number;
29
+ adjustedScore: string;
30
+ }
8
31
 
9
32
  @Component({
10
33
  selector: "app-root",
@@ -16,16 +39,52 @@ import { GridNexaAngularComponent } from "@gridnexa/angular";
16
39
  [rows]="rows"
17
40
  [rowNumbers]="true"
18
41
  [checkboxSelection]="true"
42
+ [enableFillHandle]="true"
43
+ [enableUndoRedo]="true"
44
+ [pageSize]="20"
45
+ [getRowId]="getRowId"
19
46
  (rowSelectionChange)="onSelection($event)"
47
+ (cellValueChange)="onCellValueChange($event)"
20
48
  />
21
49
  `,
22
50
  })
23
51
  export class AppComponent {
24
- columns = columns;
25
- rows = rows;
52
+ columns: Column<Employee>[] = [
53
+ { id: "name", field: "name", headerName: "Name", sortable: true, filter: "text", editable: true },
54
+ { id: "department", field: "department", headerName: "Department", filter: "set" },
55
+ { id: "score", field: "score", headerName: "Score", filter: "number", editable: true },
56
+ { id: "adjustedScore", field: "adjustedScore", headerName: "Adjusted" },
57
+ ];
26
58
 
27
- onSelection(rows: unknown[]) {
59
+ rows: Employee[] = [
60
+ { id: 1, name: "John Carter", department: "Operations", city: "London", score: 92, adjustedScore: "=score * 1.05" },
61
+ { id: 2, name: "Alice Moreau", department: "Product", city: "Paris", score: 87, adjustedScore: "=score * 1.05" },
62
+ ];
63
+
64
+ getRowId = (row: Employee) => row.id;
65
+
66
+ onSelection(rows: Employee[]) {
28
67
  console.log(rows);
29
68
  }
69
+
70
+ onCellValueChange(event: unknown) {
71
+ console.log(event);
72
+ }
30
73
  }
31
74
  ```
75
+
76
+ ## Features
77
+
78
+ - Native Angular data grid with TypeScript column models
79
+ - Excel-style features: formulas, fill, copy/paste, undo/redo, find, CSV export, and Excel export
80
+ - Filtering: column filters, quick filter, external filter, and advanced filter model
81
+ - Data modeling: grouping, pivoting, tree data indentation, master/detail, and transactions
82
+ - Grid UX: row selection, row numbers, pagination, status output, and tools panel
83
+ - Columns: merged headers, column visibility tools, column reorder, and row reorder
84
+ - Events: row selection, cell value changes, pivot model changes, advanced filter changes, and server-side operation events
85
+
86
+ ## Links
87
+
88
+ - Website: https://www.gridnexa.in/
89
+ - Help: https://www.gridnexa.in/help
90
+ - Repository: https://github.com/mhalungekar9/SmartGrid
package/dist/index.cjs CHANGED
@@ -135,6 +135,12 @@ function buildPivot(rows, columns, groupBy, pivotBy, pivotValueColumns, pivotAgg
135
135
  });
136
136
  return { columns: pivotColumns, rows: pivotRows, active: true };
137
137
  }
138
+ function buildGroupSummary(rows, columns, groupBy) {
139
+ return columns.filter((column) => column.field !== groupBy).map((column) => {
140
+ const values = rows.map((row) => Number(value(row, column, columns))).filter(Number.isFinite);
141
+ return values.length ? `${column.headerName}: ${values.reduce((sum, entry) => sum + entry, 0).toLocaleString()}` : "";
142
+ }).filter(Boolean).slice(0, 3).join(" | ");
143
+ }
138
144
  function cell(text, tag = "td") {
139
145
  const element = document.createElement(tag);
140
146
  element.textContent = text;
@@ -176,9 +182,17 @@ var GridNexaAngularComponent = class {
176
182
  sortState = null;
177
183
  hiddenColumnIds = /* @__PURE__ */ new Set();
178
184
  activeCell = null;
185
+ rangeAnchor = null;
186
+ rangeEnd = null;
187
+ contextMenu = null;
188
+ expandedDetailIds = /* @__PURE__ */ new Set();
189
+ collapsedGroups = /* @__PURE__ */ new Set();
190
+ collapsedTreeKeys = /* @__PURE__ */ new Set();
179
191
  findText = "";
180
192
  toolsOpen = false;
181
193
  draggedColumnId = null;
194
+ draggedRowIndex = null;
195
+ columnWidths = /* @__PURE__ */ new Map();
182
196
  undoStack = [];
183
197
  redoStack = [];
184
198
  ngAfterViewInit() {
@@ -186,6 +200,9 @@ var GridNexaAngularComponent = class {
186
200
  }
187
201
  ngOnChanges() {
188
202
  this.hiddenColumnIds = new Set(this.columns.filter((column) => column.hidden).map((column) => column.id));
203
+ this.columns.forEach((column) => {
204
+ if (column.width && !this.columnWidths.has(column.id)) this.columnWidths.set(column.id, column.width);
205
+ });
189
206
  this.applyTransaction();
190
207
  this.render();
191
208
  }
@@ -257,15 +274,36 @@ var GridNexaAngularComponent = class {
257
274
  }
258
275
  fillDown() {
259
276
  if (!this.activeCell || !this.enableFillHandle) return;
277
+ const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
278
+ if (this.rangeAnchor && this.rangeEnd && column) {
279
+ const minRow = Math.min(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex);
280
+ const maxRow = Math.max(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex);
281
+ const sourceRow2 = this.rows[minRow];
282
+ if (!sourceRow2 || column.editable === false) return;
283
+ const sourceValue = rawValue(sourceRow2, column);
284
+ for (let rowIndex = minRow + 1; rowIndex <= maxRow; rowIndex += 1) {
285
+ const row = this.rows[rowIndex];
286
+ if (row) this.setCellValue(row, rowIndex, column, sourceValue);
287
+ }
288
+ this.render();
289
+ return;
290
+ }
260
291
  const sourceRow = this.rows[this.activeCell.rowIndex];
261
292
  const targetRow = this.rows[this.activeCell.rowIndex + 1];
262
- const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
263
293
  if (!sourceRow || !targetRow || !column || column.editable === false) return;
264
294
  this.setCellValue(targetRow, this.activeCell.rowIndex + 1, column, rawValue(sourceRow, column));
265
295
  this.render();
266
296
  }
267
297
  async copyActiveCell() {
268
298
  if (!this.activeCell || typeof navigator === "undefined") return;
299
+ if (this.rangeAnchor && this.rangeEnd) {
300
+ const columns = this.columns.filter((column2) => !this.hiddenColumnIds.has(column2.id) && !column2.hidden);
301
+ const anchorColumn = columns.findIndex((entry) => entry.id === this.rangeAnchor?.columnId);
302
+ const endColumn = columns.findIndex((entry) => entry.id === this.rangeEnd?.columnId);
303
+ const text = this.rows.slice(Math.min(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex), Math.max(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex) + 1).map((row2) => columns.slice(Math.min(anchorColumn, endColumn), Math.max(anchorColumn, endColumn) + 1).map((column2) => format(row2, column2, this.columns)).join(" ")).join("\n");
304
+ await navigator.clipboard?.writeText(text);
305
+ return;
306
+ }
269
307
  const row = this.rows[this.activeCell.rowIndex];
270
308
  const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
271
309
  if (!row || !column) return;
@@ -273,11 +311,17 @@ var GridNexaAngularComponent = class {
273
311
  }
274
312
  async pasteActiveCell() {
275
313
  if (!this.activeCell || typeof navigator === "undefined") return;
276
- const row = this.rows[this.activeCell.rowIndex];
277
- const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
278
- if (!row || !column || column.editable === false) return;
279
314
  const text = await navigator.clipboard?.readText();
280
- this.setCellValue(row, this.activeCell.rowIndex, column, text);
315
+ const columns = this.columns.filter((column) => !this.hiddenColumnIds.has(column.id) && !column.hidden);
316
+ const startColumn = columns.findIndex((entry) => entry.id === this.activeCell?.columnId);
317
+ text.split(/\r?\n/).forEach((line, rowOffset) => {
318
+ line.split(" ").forEach((value2, columnOffset) => {
319
+ const rowIndex = this.activeCell.rowIndex + rowOffset;
320
+ const row = this.rows[rowIndex];
321
+ const column = columns[startColumn + columnOffset];
322
+ if (row && column && column.editable !== false) this.setCellValue(row, rowIndex, column, value2);
323
+ });
324
+ });
281
325
  this.render();
282
326
  }
283
327
  moveRow(rowIndex, direction) {
@@ -289,6 +333,14 @@ var GridNexaAngularComponent = class {
289
333
  this.rows = rows;
290
334
  this.render();
291
335
  }
336
+ reorderRow(sourceIndex, targetIndex) {
337
+ if (sourceIndex === targetIndex || sourceIndex < 0 || targetIndex < 0 || sourceIndex >= this.rows.length || targetIndex >= this.rows.length) return;
338
+ const rows = [...this.rows];
339
+ const [row] = rows.splice(sourceIndex, 1);
340
+ rows.splice(targetIndex, 0, row);
341
+ this.rows = rows;
342
+ this.render();
343
+ }
292
344
  moveColumn(sourceId, targetId) {
293
345
  if (sourceId === targetId) return;
294
346
  const columns = [...this.columns];
@@ -300,6 +352,63 @@ var GridNexaAngularComponent = class {
300
352
  this.columns = columns;
301
353
  this.render();
302
354
  }
355
+ makeDisplayRows(rows) {
356
+ if (this.groupBy) {
357
+ const buckets = /* @__PURE__ */ new Map();
358
+ rows.forEach((row) => {
359
+ const key = String(row[this.groupBy] ?? "Ungrouped");
360
+ buckets.set(key, [...buckets.get(key) ?? [], row]);
361
+ });
362
+ return Array.from(buckets.entries()).flatMap(([key, bucket]) => [
363
+ { kind: "group", key, label: key, rows: bucket, summaries: buildGroupSummary(bucket, this.columns, this.groupBy) },
364
+ ...this.collapsedGroups.has(key) ? [] : bucket.map((row) => ({ kind: "data", row, rowIndex: this.rows.indexOf(row) }))
365
+ ]);
366
+ }
367
+ if (this.getTreeDataPath) {
368
+ return rows.map((row) => {
369
+ const path = this.getTreeDataPath?.(row).filter(Boolean) ?? [];
370
+ return { row, path, key: path.join("/") };
371
+ }).sort((left, right) => left.key.localeCompare(right.key)).filter((entry) => entry.path.slice(0, -1).every((_, index) => !this.collapsedTreeKeys.has(entry.path.slice(0, index + 1).join("/")))).map((entry, _index, entries) => ({
372
+ kind: "data",
373
+ row: entry.row,
374
+ rowIndex: this.rows.indexOf(entry.row),
375
+ depth: Math.max(0, entry.path.length - 1),
376
+ treeKey: entry.key,
377
+ hasChildren: entries.some((other) => other.key.startsWith(`${entry.key}/`))
378
+ }));
379
+ }
380
+ return rows.map((row) => ({ kind: "data", row, rowIndex: this.rows.indexOf(row) }));
381
+ }
382
+ isCellInRange(rowIndex, columnId, columns) {
383
+ if (!this.rangeAnchor || !this.rangeEnd || !this.enableRangeSelection) return false;
384
+ const columnIndex = columns.findIndex((column) => column.id === columnId);
385
+ const anchorIndex = columns.findIndex((column) => column.id === this.rangeAnchor?.columnId);
386
+ const endIndex = columns.findIndex((column) => column.id === this.rangeEnd?.columnId);
387
+ return rowIndex >= Math.min(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex) && rowIndex <= Math.max(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex) && columnIndex >= Math.min(anchorIndex, endIndex) && columnIndex <= Math.max(anchorIndex, endIndex);
388
+ }
389
+ startColumnResize(event, column) {
390
+ event.preventDefault();
391
+ event.stopPropagation();
392
+ const startX = event.clientX;
393
+ const startWidth = this.columnWidths.get(column.id) ?? column.width ?? 150;
394
+ const move = (moveEvent) => {
395
+ this.columnWidths.set(column.id, Math.max(72, startWidth + moveEvent.clientX - startX));
396
+ this.render();
397
+ };
398
+ const up = () => {
399
+ document.removeEventListener("mousemove", move);
400
+ document.removeEventListener("mouseup", up);
401
+ };
402
+ document.addEventListener("mousemove", move);
403
+ document.addEventListener("mouseup", up);
404
+ }
405
+ pinnedStyle(column, columns) {
406
+ if (!column.pinned) return "";
407
+ const index = columns.findIndex((entry) => entry.id === column.id);
408
+ const width = (entry) => this.columnWidths.get(entry.id) ?? entry.width ?? 150;
409
+ const offset = column.pinned === "left" ? columns.slice(0, index).filter((entry) => entry.pinned === "left").reduce((sum, entry) => sum + width(entry), 0) : columns.slice(index + 1).filter((entry) => entry.pinned === "right").reduce((sum, entry) => sum + width(entry), 0);
410
+ return `position:sticky;${column.pinned}:${offset}px;z-index:2;background:white;box-shadow:${column.pinned === "left" ? "inset -1px 0 #dbe3ef" : "inset 1px 0 #dbe3ef"};`;
411
+ }
303
412
  updateAdvancedFilter(columnId, operator, filterValue) {
304
413
  const model = {
305
414
  kind: "group",
@@ -328,13 +437,15 @@ var GridNexaAngularComponent = class {
328
437
  if (!this.host) return;
329
438
  const sourceRows = this.visibleRows();
330
439
  const pivot = buildPivot(sourceRows, this.columns, this.groupBy, this.pivotBy, this.pivotValueColumns, this.pivotAggregation);
331
- const columns = pivot.columns.filter((column) => !this.hiddenColumnIds.has(column.id) && !column.hidden);
440
+ const columns = pivot.columns.filter((column) => !this.hiddenColumnIds.has(column.id) && !column.hidden).sort((left, right) => (left.pinned === "left" ? 0 : left.pinned === "right" ? 2 : 1) - (right.pinned === "left" ? 0 : right.pinned === "right" ? 2 : 1));
332
441
  const pageRows = this.pageSize ? pivot.rows.slice(this.pageIndex * this.pageSize, this.pageIndex * this.pageSize + this.pageSize) : pivot.rows;
442
+ const displayRows = pivot.active ? pageRows.map((row) => ({ kind: "data", row, rowIndex: pivot.rows.indexOf(row) })) : this.makeDisplayRows(pageRows);
333
443
  const root = document.createElement("div");
334
444
  root.className = "gridnexa-angular-grid";
335
- root.append(this.renderToolbar(columns, pivot.rows), this.renderTable(columns, pageRows));
445
+ root.append(this.renderToolbar(columns, pivot.rows), this.renderTable(columns, displayRows));
336
446
  if (this.toolsOpen) root.appendChild(this.renderToolsPanel());
337
447
  root.appendChild(this.renderStatus(pivot.rows.length));
448
+ if (this.contextMenu) root.appendChild(this.renderContextMenu());
338
449
  this.host.nativeElement.replaceChildren(root);
339
450
  this.serverSideOperation.emit({
340
451
  sortModel: this.sortState ? [this.sortState] : [],
@@ -366,6 +477,16 @@ var GridNexaAngularComponent = class {
366
477
  this.findText = find.value;
367
478
  this.render();
368
479
  });
480
+ const quickFilter = document.createElement("input");
481
+ quickFilter.type = "search";
482
+ quickFilter.placeholder = "Quick filter";
483
+ quickFilter.value = this.quickFilterText;
484
+ quickFilter.style.cssText = "min-height:32px;padding:0 10px;border:1px solid #bfdbfe;border-radius:8px";
485
+ quickFilter.addEventListener("input", () => {
486
+ this.quickFilterText = quickFilter.value;
487
+ this.pageIndex = 0;
488
+ this.render();
489
+ });
369
490
  const pageCount = this.pageSize ? Math.max(1, Math.ceil(rows.length / this.pageSize)) : 1;
370
491
  if (this.pageSize) {
371
492
  const prev = this.button("Prev", () => {
@@ -380,7 +501,7 @@ var GridNexaAngularComponent = class {
380
501
  next.disabled = this.pageIndex >= pageCount - 1;
381
502
  actions.append(prev, ` Page ${this.pageIndex + 1} `, next);
382
503
  }
383
- actions.appendChild(find);
504
+ actions.append(quickFilter, find);
384
505
  if (this.enableUndoRedo) {
385
506
  const undo = this.button("Undo", () => this.undo());
386
507
  undo.disabled = !this.undoStack.length;
@@ -413,7 +534,7 @@ var GridNexaAngularComponent = class {
413
534
  if (this.rowNumbers) header.appendChild(cell("#", "th"));
414
535
  columns.forEach((column) => {
415
536
  const th = cell(`${column.headerName}${this.sortState?.columnId === column.id ? this.sortState.direction === "asc" ? " \u2191" : " \u2193" : ""}`, "th");
416
- th.style.cssText = "padding:10px;border:1px solid #dbe3ef;background:#f8fbff;text-align:left";
537
+ th.style.cssText = `padding:10px;border:1px solid #dbe3ef;background:#f8fbff;text-align:left;width:${this.columnWidths.get(column.id) ?? column.width ?? 150}px;${this.pinnedStyle(column, columns)}`;
417
538
  th.draggable = true;
418
539
  th.addEventListener("dragstart", () => {
419
540
  this.draggedColumnId = column.id;
@@ -429,12 +550,25 @@ var GridNexaAngularComponent = class {
429
550
  this.sortState = this.sortState?.columnId !== column.id ? { columnId: column.id, direction: "asc" } : this.sortState.direction === "asc" ? { columnId: column.id, direction: "desc" } : null;
430
551
  this.render();
431
552
  });
553
+ if (column.resizable !== false) {
554
+ const resizer = document.createElement("span");
555
+ resizer.style.cssText = "float:right;width:7px;height:24px;cursor:col-resize;border-right:2px solid #bfdbfe";
556
+ resizer.addEventListener("mousedown", (event) => this.startColumnResize(event, column));
557
+ th.appendChild(resizer);
558
+ }
432
559
  header.appendChild(th);
433
560
  });
434
561
  thead.appendChild(header);
435
562
  table.appendChild(thead);
436
563
  const tbody = document.createElement("tbody");
437
- rows.forEach((row, rowIndex) => this.appendRow(tbody, row, rowIndex, columns, leading));
564
+ rows.forEach((entry) => {
565
+ if (entry.kind === "group") this.appendGroupRow(tbody, entry, columns.length + leading);
566
+ if (entry.kind === "data") {
567
+ this.appendRow(tbody, entry.row, entry.rowIndex, columns, leading, entry);
568
+ const rowId = this.rowId(entry.row, entry.rowIndex);
569
+ if (this.masterDetailRenderer && this.expandedDetailIds.has(rowId)) this.appendDetailRow(tbody, entry.row, columns.length + leading);
570
+ }
571
+ });
438
572
  table.appendChild(tbody);
439
573
  return table;
440
574
  }
@@ -455,8 +589,41 @@ var GridNexaAngularComponent = class {
455
589
  });
456
590
  return row;
457
591
  }
458
- appendRow(tbody, row, rowIndex, columns, leading) {
592
+ appendGroupRow(tbody, entry, colSpan) {
593
+ const tr = document.createElement("tr");
594
+ const td = document.createElement("td");
595
+ td.colSpan = colSpan;
596
+ td.style.cssText = "padding:10px;border:1px solid #dbe3ef;background:#eef4ff;color:#153e90;font-weight:800;text-transform:uppercase";
597
+ const toggle = this.button(this.collapsedGroups.has(entry.key) ? "+" : "-", () => {
598
+ this.collapsedGroups.has(entry.key) ? this.collapsedGroups.delete(entry.key) : this.collapsedGroups.add(entry.key);
599
+ this.render();
600
+ });
601
+ td.append(toggle, `${entry.label} ${entry.rows.length} rows${entry.summaries ? ` ${entry.summaries}` : ""}`);
602
+ tr.appendChild(td);
603
+ tbody.appendChild(tr);
604
+ }
605
+ appendDetailRow(tbody, row, colSpan) {
606
+ const detailRow = document.createElement("tr");
607
+ const detail = document.createElement("td");
608
+ detail.colSpan = colSpan;
609
+ detail.style.cssText = "padding:12px;border:1px solid #dbe3ef;background:#f8fbff;color:#334155";
610
+ const content = this.masterDetailRenderer?.(row);
611
+ content instanceof Node ? detail.appendChild(content) : detail.textContent = String(content ?? "");
612
+ detailRow.appendChild(detail);
613
+ tbody.appendChild(detailRow);
614
+ }
615
+ appendRow(tbody, row, rowIndex, columns, leading, display) {
459
616
  const tr = document.createElement("tr");
617
+ tr.draggable = true;
618
+ tr.addEventListener("dragstart", () => {
619
+ this.draggedRowIndex = rowIndex;
620
+ });
621
+ tr.addEventListener("dragover", (event) => event.preventDefault());
622
+ tr.addEventListener("drop", (event) => {
623
+ event.preventDefault();
624
+ if (this.draggedRowIndex != null) this.reorderRow(this.draggedRowIndex, rowIndex);
625
+ this.draggedRowIndex = null;
626
+ });
460
627
  if (this.checkboxSelection) {
461
628
  const td = document.createElement("td");
462
629
  const checkbox = document.createElement("input");
@@ -481,45 +648,123 @@ var GridNexaAngularComponent = class {
481
648
  }
482
649
  columns.forEach((column, columnIndex) => {
483
650
  const td = cell(format(row, column, this.columns));
484
- td.style.cssText = "padding:10px;border:1px solid #dbe3ef";
651
+ td.style.cssText = `padding:10px;border:1px solid #dbe3ef;width:${this.columnWidths.get(column.id) ?? column.width ?? 150}px;${this.pinnedStyle(column, columns)}`;
485
652
  if (this.activeCell?.rowIndex === rowIndex && this.activeCell.columnId === column.id) {
486
653
  td.style.outline = "2px solid #2563eb";
487
654
  td.style.outlineOffset = "-2px";
488
655
  }
656
+ if (this.isCellInRange(rowIndex, column.id, columns)) td.style.background = "rgba(37,99,235,.12)";
489
657
  if (this.findText && format(row, column, this.columns).toLowerCase().includes(this.findText.toLowerCase())) {
490
658
  td.style.background = "rgba(37,99,235,.1)";
491
659
  }
492
- if (this.getTreeDataPath && columnIndex === 0) td.style.paddingLeft = `${12 + Math.max(0, this.getTreeDataPath(row).length - 1) * 24}px`;
493
- td.addEventListener("click", () => {
660
+ if (this.getTreeDataPath && columnIndex === 0) {
661
+ td.style.paddingLeft = `${12 + (display?.depth ?? Math.max(0, this.getTreeDataPath(row).length - 1)) * 24}px`;
662
+ if (display?.hasChildren && display.treeKey) {
663
+ const treeKey = display.treeKey;
664
+ const toggle = this.button(this.collapsedTreeKeys.has(treeKey) ? "+" : "-", () => {
665
+ this.collapsedTreeKeys.has(treeKey) ? this.collapsedTreeKeys.delete(treeKey) : this.collapsedTreeKeys.add(treeKey);
666
+ this.render();
667
+ });
668
+ td.prepend(toggle);
669
+ }
670
+ } else if (this.masterDetailRenderer && columnIndex === 0) {
671
+ const rowId = this.rowId(row, rowIndex);
672
+ const toggle = this.button(this.expandedDetailIds.has(rowId) ? "-" : "+", () => {
673
+ this.expandedDetailIds.has(rowId) ? this.expandedDetailIds.delete(rowId) : this.expandedDetailIds.add(rowId);
674
+ this.render();
675
+ });
676
+ td.prepend(toggle);
677
+ }
678
+ td.addEventListener("click", (event) => {
679
+ this.contextMenu = null;
494
680
  this.activeCell = { rowIndex, columnId: column.id };
681
+ if (event.shiftKey && this.rangeAnchor) {
682
+ this.rangeEnd = { rowIndex, columnId: column.id };
683
+ } else {
684
+ this.rangeAnchor = { rowIndex, columnId: column.id };
685
+ this.rangeEnd = { rowIndex, columnId: column.id };
686
+ }
495
687
  this.cellClick.emit({ row, rowIndex, column });
496
688
  this.render();
497
689
  });
690
+ td.addEventListener("contextmenu", (event) => {
691
+ event.preventDefault();
692
+ this.activeCell = { rowIndex, columnId: column.id };
693
+ this.contextMenu = { x: event.clientX, y: event.clientY, rowIndex, columnId: column.id };
694
+ this.render();
695
+ });
498
696
  if (column.editable) td.addEventListener("dblclick", () => this.editCell(td, row, rowIndex, column));
499
697
  tr.appendChild(td);
500
698
  });
501
699
  tbody.appendChild(tr);
502
- if (this.masterDetailRenderer) {
503
- const detailRow = document.createElement("tr");
504
- const detail = document.createElement("td");
505
- detail.colSpan = columns.length + leading;
506
- const content = this.masterDetailRenderer(row);
507
- content instanceof Node ? detail.appendChild(content) : detail.textContent = String(content ?? "");
508
- detailRow.appendChild(detail);
509
- tbody.appendChild(detailRow);
510
- }
511
700
  }
512
701
  editCell(td, row, rowIndex, column) {
513
702
  const oldValue = rawValue(row, column);
514
- const input = document.createElement("input");
515
- input.value = String(oldValue ?? "");
703
+ const input = this.createEditor(column, oldValue);
516
704
  td.replaceChildren(input);
517
705
  input.focus();
518
706
  input.addEventListener("blur", () => {
519
- this.setCellValue(row, rowIndex, column, input.value);
707
+ this.setCellValue(row, rowIndex, column, input instanceof HTMLInputElement && input.type === "checkbox" ? input.checked : input.value);
520
708
  this.render();
521
709
  }, { once: true });
522
710
  }
711
+ createEditor(column, current) {
712
+ const editor = column.editor;
713
+ if (editor === "checkbox") {
714
+ const input2 = document.createElement("input");
715
+ input2.type = "checkbox";
716
+ input2.checked = Boolean(current);
717
+ return input2;
718
+ }
719
+ if (editor === "number" || editor === "date") {
720
+ const input2 = document.createElement("input");
721
+ input2.type = editor;
722
+ input2.value = String(current ?? "");
723
+ return input2;
724
+ }
725
+ if (editor && typeof editor === "object" && (editor.type === "select" || editor.type === "advancedSelect")) {
726
+ const select = document.createElement("select");
727
+ (editor.values ?? []).forEach((item) => {
728
+ const option = document.createElement("option");
729
+ option.value = String(item);
730
+ option.textContent = String(item);
731
+ select.appendChild(option);
732
+ });
733
+ select.value = String(current ?? "");
734
+ return select;
735
+ }
736
+ const input = document.createElement("input");
737
+ input.value = String(current ?? "");
738
+ return input;
739
+ }
740
+ renderContextMenu() {
741
+ const menu = document.createElement("div");
742
+ menu.style.cssText = `position:fixed;z-index:9999;left:${this.contextMenu?.x ?? 0}px;top:${this.contextMenu?.y ?? 0}px;display:grid;min-width:150px;padding:6px;border:1px solid #dbe3ef;border-radius:10px;background:white;box-shadow:0 18px 48px rgba(15,23,42,.18)`;
743
+ const row = this.contextMenu ? this.rows[this.contextMenu.rowIndex] : void 0;
744
+ const column = this.columns.find((entry) => entry.id === this.contextMenu?.columnId);
745
+ menu.append(
746
+ this.button("Copy", () => void this.copyActiveCell()),
747
+ this.button("Paste", () => void this.pasteActiveCell()),
748
+ this.button("Edit cell", () => {
749
+ if (!row || !column || column.editable === false || !this.contextMenu) return;
750
+ const nextValue = window.prompt(`Edit ${column.headerName}`, String(rawValue(row, column) ?? ""));
751
+ if (nextValue != null) this.setCellValue(row, this.contextMenu.rowIndex, column, nextValue);
752
+ this.contextMenu = null;
753
+ this.render();
754
+ }),
755
+ this.button("Clear cell", () => {
756
+ if (row && column && column.editable !== false && this.contextMenu) this.setCellValue(row, this.contextMenu.rowIndex, column, "");
757
+ this.contextMenu = null;
758
+ this.render();
759
+ }),
760
+ this.button("Hide column", () => {
761
+ if (column) this.hiddenColumnIds.add(column.id);
762
+ this.contextMenu = null;
763
+ this.render();
764
+ })
765
+ );
766
+ return menu;
767
+ }
523
768
  renderToolsPanel() {
524
769
  const panel = document.createElement("div");
525
770
  panel.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;padding:12px;border:1px solid #dbe3ef;border-top:0;background:#f8fbff";
package/dist/index.d.cts CHANGED
@@ -57,9 +57,17 @@ declare class GridNexaAngularComponent<T = Record<string, unknown>> implements A
57
57
  private sortState;
58
58
  private hiddenColumnIds;
59
59
  private activeCell;
60
+ private rangeAnchor;
61
+ private rangeEnd;
62
+ private contextMenu;
63
+ private expandedDetailIds;
64
+ private collapsedGroups;
65
+ private collapsedTreeKeys;
60
66
  private findText;
61
67
  private toolsOpen;
62
68
  private draggedColumnId;
69
+ private draggedRowIndex;
70
+ private columnWidths;
63
71
  private undoStack;
64
72
  private redoStack;
65
73
  ngAfterViewInit(): void;
@@ -75,15 +83,24 @@ declare class GridNexaAngularComponent<T = Record<string, unknown>> implements A
75
83
  private copyActiveCell;
76
84
  private pasteActiveCell;
77
85
  private moveRow;
86
+ private reorderRow;
78
87
  private moveColumn;
88
+ private makeDisplayRows;
89
+ private isCellInRange;
90
+ private startColumnResize;
91
+ private pinnedStyle;
79
92
  private updateAdvancedFilter;
80
93
  private updatePivot;
81
94
  private render;
82
95
  private renderToolbar;
83
96
  private renderTable;
84
97
  private renderMergedHeaders;
98
+ private appendGroupRow;
99
+ private appendDetailRow;
85
100
  private appendRow;
86
101
  private editCell;
102
+ private createEditor;
103
+ private renderContextMenu;
87
104
  private renderToolsPanel;
88
105
  private select;
89
106
  private renderStatus;
package/dist/index.d.ts CHANGED
@@ -57,9 +57,17 @@ declare class GridNexaAngularComponent<T = Record<string, unknown>> implements A
57
57
  private sortState;
58
58
  private hiddenColumnIds;
59
59
  private activeCell;
60
+ private rangeAnchor;
61
+ private rangeEnd;
62
+ private contextMenu;
63
+ private expandedDetailIds;
64
+ private collapsedGroups;
65
+ private collapsedTreeKeys;
60
66
  private findText;
61
67
  private toolsOpen;
62
68
  private draggedColumnId;
69
+ private draggedRowIndex;
70
+ private columnWidths;
63
71
  private undoStack;
64
72
  private redoStack;
65
73
  ngAfterViewInit(): void;
@@ -75,15 +83,24 @@ declare class GridNexaAngularComponent<T = Record<string, unknown>> implements A
75
83
  private copyActiveCell;
76
84
  private pasteActiveCell;
77
85
  private moveRow;
86
+ private reorderRow;
78
87
  private moveColumn;
88
+ private makeDisplayRows;
89
+ private isCellInRange;
90
+ private startColumnResize;
91
+ private pinnedStyle;
79
92
  private updateAdvancedFilter;
80
93
  private updatePivot;
81
94
  private render;
82
95
  private renderToolbar;
83
96
  private renderTable;
84
97
  private renderMergedHeaders;
98
+ private appendGroupRow;
99
+ private appendDetailRow;
85
100
  private appendRow;
86
101
  private editCell;
102
+ private createEditor;
103
+ private renderContextMenu;
87
104
  private renderToolsPanel;
88
105
  private select;
89
106
  private renderStatus;
package/dist/index.js CHANGED
@@ -119,6 +119,12 @@ function buildPivot(rows, columns, groupBy, pivotBy, pivotValueColumns, pivotAgg
119
119
  });
120
120
  return { columns: pivotColumns, rows: pivotRows, active: true };
121
121
  }
122
+ function buildGroupSummary(rows, columns, groupBy) {
123
+ return columns.filter((column) => column.field !== groupBy).map((column) => {
124
+ const values = rows.map((row) => Number(value(row, column, columns))).filter(Number.isFinite);
125
+ return values.length ? `${column.headerName}: ${values.reduce((sum, entry) => sum + entry, 0).toLocaleString()}` : "";
126
+ }).filter(Boolean).slice(0, 3).join(" | ");
127
+ }
122
128
  function cell(text, tag = "td") {
123
129
  const element = document.createElement(tag);
124
130
  element.textContent = text;
@@ -160,9 +166,17 @@ var GridNexaAngularComponent = class {
160
166
  sortState = null;
161
167
  hiddenColumnIds = /* @__PURE__ */ new Set();
162
168
  activeCell = null;
169
+ rangeAnchor = null;
170
+ rangeEnd = null;
171
+ contextMenu = null;
172
+ expandedDetailIds = /* @__PURE__ */ new Set();
173
+ collapsedGroups = /* @__PURE__ */ new Set();
174
+ collapsedTreeKeys = /* @__PURE__ */ new Set();
163
175
  findText = "";
164
176
  toolsOpen = false;
165
177
  draggedColumnId = null;
178
+ draggedRowIndex = null;
179
+ columnWidths = /* @__PURE__ */ new Map();
166
180
  undoStack = [];
167
181
  redoStack = [];
168
182
  ngAfterViewInit() {
@@ -170,6 +184,9 @@ var GridNexaAngularComponent = class {
170
184
  }
171
185
  ngOnChanges() {
172
186
  this.hiddenColumnIds = new Set(this.columns.filter((column) => column.hidden).map((column) => column.id));
187
+ this.columns.forEach((column) => {
188
+ if (column.width && !this.columnWidths.has(column.id)) this.columnWidths.set(column.id, column.width);
189
+ });
173
190
  this.applyTransaction();
174
191
  this.render();
175
192
  }
@@ -241,15 +258,36 @@ var GridNexaAngularComponent = class {
241
258
  }
242
259
  fillDown() {
243
260
  if (!this.activeCell || !this.enableFillHandle) return;
261
+ const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
262
+ if (this.rangeAnchor && this.rangeEnd && column) {
263
+ const minRow = Math.min(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex);
264
+ const maxRow = Math.max(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex);
265
+ const sourceRow2 = this.rows[minRow];
266
+ if (!sourceRow2 || column.editable === false) return;
267
+ const sourceValue = rawValue(sourceRow2, column);
268
+ for (let rowIndex = minRow + 1; rowIndex <= maxRow; rowIndex += 1) {
269
+ const row = this.rows[rowIndex];
270
+ if (row) this.setCellValue(row, rowIndex, column, sourceValue);
271
+ }
272
+ this.render();
273
+ return;
274
+ }
244
275
  const sourceRow = this.rows[this.activeCell.rowIndex];
245
276
  const targetRow = this.rows[this.activeCell.rowIndex + 1];
246
- const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
247
277
  if (!sourceRow || !targetRow || !column || column.editable === false) return;
248
278
  this.setCellValue(targetRow, this.activeCell.rowIndex + 1, column, rawValue(sourceRow, column));
249
279
  this.render();
250
280
  }
251
281
  async copyActiveCell() {
252
282
  if (!this.activeCell || typeof navigator === "undefined") return;
283
+ if (this.rangeAnchor && this.rangeEnd) {
284
+ const columns = this.columns.filter((column2) => !this.hiddenColumnIds.has(column2.id) && !column2.hidden);
285
+ const anchorColumn = columns.findIndex((entry) => entry.id === this.rangeAnchor?.columnId);
286
+ const endColumn = columns.findIndex((entry) => entry.id === this.rangeEnd?.columnId);
287
+ const text = this.rows.slice(Math.min(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex), Math.max(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex) + 1).map((row2) => columns.slice(Math.min(anchorColumn, endColumn), Math.max(anchorColumn, endColumn) + 1).map((column2) => format(row2, column2, this.columns)).join(" ")).join("\n");
288
+ await navigator.clipboard?.writeText(text);
289
+ return;
290
+ }
253
291
  const row = this.rows[this.activeCell.rowIndex];
254
292
  const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
255
293
  if (!row || !column) return;
@@ -257,11 +295,17 @@ var GridNexaAngularComponent = class {
257
295
  }
258
296
  async pasteActiveCell() {
259
297
  if (!this.activeCell || typeof navigator === "undefined") return;
260
- const row = this.rows[this.activeCell.rowIndex];
261
- const column = this.columns.find((entry) => entry.id === this.activeCell?.columnId);
262
- if (!row || !column || column.editable === false) return;
263
298
  const text = await navigator.clipboard?.readText();
264
- this.setCellValue(row, this.activeCell.rowIndex, column, text);
299
+ const columns = this.columns.filter((column) => !this.hiddenColumnIds.has(column.id) && !column.hidden);
300
+ const startColumn = columns.findIndex((entry) => entry.id === this.activeCell?.columnId);
301
+ text.split(/\r?\n/).forEach((line, rowOffset) => {
302
+ line.split(" ").forEach((value2, columnOffset) => {
303
+ const rowIndex = this.activeCell.rowIndex + rowOffset;
304
+ const row = this.rows[rowIndex];
305
+ const column = columns[startColumn + columnOffset];
306
+ if (row && column && column.editable !== false) this.setCellValue(row, rowIndex, column, value2);
307
+ });
308
+ });
265
309
  this.render();
266
310
  }
267
311
  moveRow(rowIndex, direction) {
@@ -273,6 +317,14 @@ var GridNexaAngularComponent = class {
273
317
  this.rows = rows;
274
318
  this.render();
275
319
  }
320
+ reorderRow(sourceIndex, targetIndex) {
321
+ if (sourceIndex === targetIndex || sourceIndex < 0 || targetIndex < 0 || sourceIndex >= this.rows.length || targetIndex >= this.rows.length) return;
322
+ const rows = [...this.rows];
323
+ const [row] = rows.splice(sourceIndex, 1);
324
+ rows.splice(targetIndex, 0, row);
325
+ this.rows = rows;
326
+ this.render();
327
+ }
276
328
  moveColumn(sourceId, targetId) {
277
329
  if (sourceId === targetId) return;
278
330
  const columns = [...this.columns];
@@ -284,6 +336,63 @@ var GridNexaAngularComponent = class {
284
336
  this.columns = columns;
285
337
  this.render();
286
338
  }
339
+ makeDisplayRows(rows) {
340
+ if (this.groupBy) {
341
+ const buckets = /* @__PURE__ */ new Map();
342
+ rows.forEach((row) => {
343
+ const key = String(row[this.groupBy] ?? "Ungrouped");
344
+ buckets.set(key, [...buckets.get(key) ?? [], row]);
345
+ });
346
+ return Array.from(buckets.entries()).flatMap(([key, bucket]) => [
347
+ { kind: "group", key, label: key, rows: bucket, summaries: buildGroupSummary(bucket, this.columns, this.groupBy) },
348
+ ...this.collapsedGroups.has(key) ? [] : bucket.map((row) => ({ kind: "data", row, rowIndex: this.rows.indexOf(row) }))
349
+ ]);
350
+ }
351
+ if (this.getTreeDataPath) {
352
+ return rows.map((row) => {
353
+ const path = this.getTreeDataPath?.(row).filter(Boolean) ?? [];
354
+ return { row, path, key: path.join("/") };
355
+ }).sort((left, right) => left.key.localeCompare(right.key)).filter((entry) => entry.path.slice(0, -1).every((_, index) => !this.collapsedTreeKeys.has(entry.path.slice(0, index + 1).join("/")))).map((entry, _index, entries) => ({
356
+ kind: "data",
357
+ row: entry.row,
358
+ rowIndex: this.rows.indexOf(entry.row),
359
+ depth: Math.max(0, entry.path.length - 1),
360
+ treeKey: entry.key,
361
+ hasChildren: entries.some((other) => other.key.startsWith(`${entry.key}/`))
362
+ }));
363
+ }
364
+ return rows.map((row) => ({ kind: "data", row, rowIndex: this.rows.indexOf(row) }));
365
+ }
366
+ isCellInRange(rowIndex, columnId, columns) {
367
+ if (!this.rangeAnchor || !this.rangeEnd || !this.enableRangeSelection) return false;
368
+ const columnIndex = columns.findIndex((column) => column.id === columnId);
369
+ const anchorIndex = columns.findIndex((column) => column.id === this.rangeAnchor?.columnId);
370
+ const endIndex = columns.findIndex((column) => column.id === this.rangeEnd?.columnId);
371
+ return rowIndex >= Math.min(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex) && rowIndex <= Math.max(this.rangeAnchor.rowIndex, this.rangeEnd.rowIndex) && columnIndex >= Math.min(anchorIndex, endIndex) && columnIndex <= Math.max(anchorIndex, endIndex);
372
+ }
373
+ startColumnResize(event, column) {
374
+ event.preventDefault();
375
+ event.stopPropagation();
376
+ const startX = event.clientX;
377
+ const startWidth = this.columnWidths.get(column.id) ?? column.width ?? 150;
378
+ const move = (moveEvent) => {
379
+ this.columnWidths.set(column.id, Math.max(72, startWidth + moveEvent.clientX - startX));
380
+ this.render();
381
+ };
382
+ const up = () => {
383
+ document.removeEventListener("mousemove", move);
384
+ document.removeEventListener("mouseup", up);
385
+ };
386
+ document.addEventListener("mousemove", move);
387
+ document.addEventListener("mouseup", up);
388
+ }
389
+ pinnedStyle(column, columns) {
390
+ if (!column.pinned) return "";
391
+ const index = columns.findIndex((entry) => entry.id === column.id);
392
+ const width = (entry) => this.columnWidths.get(entry.id) ?? entry.width ?? 150;
393
+ const offset = column.pinned === "left" ? columns.slice(0, index).filter((entry) => entry.pinned === "left").reduce((sum, entry) => sum + width(entry), 0) : columns.slice(index + 1).filter((entry) => entry.pinned === "right").reduce((sum, entry) => sum + width(entry), 0);
394
+ return `position:sticky;${column.pinned}:${offset}px;z-index:2;background:white;box-shadow:${column.pinned === "left" ? "inset -1px 0 #dbe3ef" : "inset 1px 0 #dbe3ef"};`;
395
+ }
287
396
  updateAdvancedFilter(columnId, operator, filterValue) {
288
397
  const model = {
289
398
  kind: "group",
@@ -312,13 +421,15 @@ var GridNexaAngularComponent = class {
312
421
  if (!this.host) return;
313
422
  const sourceRows = this.visibleRows();
314
423
  const pivot = buildPivot(sourceRows, this.columns, this.groupBy, this.pivotBy, this.pivotValueColumns, this.pivotAggregation);
315
- const columns = pivot.columns.filter((column) => !this.hiddenColumnIds.has(column.id) && !column.hidden);
424
+ const columns = pivot.columns.filter((column) => !this.hiddenColumnIds.has(column.id) && !column.hidden).sort((left, right) => (left.pinned === "left" ? 0 : left.pinned === "right" ? 2 : 1) - (right.pinned === "left" ? 0 : right.pinned === "right" ? 2 : 1));
316
425
  const pageRows = this.pageSize ? pivot.rows.slice(this.pageIndex * this.pageSize, this.pageIndex * this.pageSize + this.pageSize) : pivot.rows;
426
+ const displayRows = pivot.active ? pageRows.map((row) => ({ kind: "data", row, rowIndex: pivot.rows.indexOf(row) })) : this.makeDisplayRows(pageRows);
317
427
  const root = document.createElement("div");
318
428
  root.className = "gridnexa-angular-grid";
319
- root.append(this.renderToolbar(columns, pivot.rows), this.renderTable(columns, pageRows));
429
+ root.append(this.renderToolbar(columns, pivot.rows), this.renderTable(columns, displayRows));
320
430
  if (this.toolsOpen) root.appendChild(this.renderToolsPanel());
321
431
  root.appendChild(this.renderStatus(pivot.rows.length));
432
+ if (this.contextMenu) root.appendChild(this.renderContextMenu());
322
433
  this.host.nativeElement.replaceChildren(root);
323
434
  this.serverSideOperation.emit({
324
435
  sortModel: this.sortState ? [this.sortState] : [],
@@ -350,6 +461,16 @@ var GridNexaAngularComponent = class {
350
461
  this.findText = find.value;
351
462
  this.render();
352
463
  });
464
+ const quickFilter = document.createElement("input");
465
+ quickFilter.type = "search";
466
+ quickFilter.placeholder = "Quick filter";
467
+ quickFilter.value = this.quickFilterText;
468
+ quickFilter.style.cssText = "min-height:32px;padding:0 10px;border:1px solid #bfdbfe;border-radius:8px";
469
+ quickFilter.addEventListener("input", () => {
470
+ this.quickFilterText = quickFilter.value;
471
+ this.pageIndex = 0;
472
+ this.render();
473
+ });
353
474
  const pageCount = this.pageSize ? Math.max(1, Math.ceil(rows.length / this.pageSize)) : 1;
354
475
  if (this.pageSize) {
355
476
  const prev = this.button("Prev", () => {
@@ -364,7 +485,7 @@ var GridNexaAngularComponent = class {
364
485
  next.disabled = this.pageIndex >= pageCount - 1;
365
486
  actions.append(prev, ` Page ${this.pageIndex + 1} `, next);
366
487
  }
367
- actions.appendChild(find);
488
+ actions.append(quickFilter, find);
368
489
  if (this.enableUndoRedo) {
369
490
  const undo = this.button("Undo", () => this.undo());
370
491
  undo.disabled = !this.undoStack.length;
@@ -397,7 +518,7 @@ var GridNexaAngularComponent = class {
397
518
  if (this.rowNumbers) header.appendChild(cell("#", "th"));
398
519
  columns.forEach((column) => {
399
520
  const th = cell(`${column.headerName}${this.sortState?.columnId === column.id ? this.sortState.direction === "asc" ? " \u2191" : " \u2193" : ""}`, "th");
400
- th.style.cssText = "padding:10px;border:1px solid #dbe3ef;background:#f8fbff;text-align:left";
521
+ th.style.cssText = `padding:10px;border:1px solid #dbe3ef;background:#f8fbff;text-align:left;width:${this.columnWidths.get(column.id) ?? column.width ?? 150}px;${this.pinnedStyle(column, columns)}`;
401
522
  th.draggable = true;
402
523
  th.addEventListener("dragstart", () => {
403
524
  this.draggedColumnId = column.id;
@@ -413,12 +534,25 @@ var GridNexaAngularComponent = class {
413
534
  this.sortState = this.sortState?.columnId !== column.id ? { columnId: column.id, direction: "asc" } : this.sortState.direction === "asc" ? { columnId: column.id, direction: "desc" } : null;
414
535
  this.render();
415
536
  });
537
+ if (column.resizable !== false) {
538
+ const resizer = document.createElement("span");
539
+ resizer.style.cssText = "float:right;width:7px;height:24px;cursor:col-resize;border-right:2px solid #bfdbfe";
540
+ resizer.addEventListener("mousedown", (event) => this.startColumnResize(event, column));
541
+ th.appendChild(resizer);
542
+ }
416
543
  header.appendChild(th);
417
544
  });
418
545
  thead.appendChild(header);
419
546
  table.appendChild(thead);
420
547
  const tbody = document.createElement("tbody");
421
- rows.forEach((row, rowIndex) => this.appendRow(tbody, row, rowIndex, columns, leading));
548
+ rows.forEach((entry) => {
549
+ if (entry.kind === "group") this.appendGroupRow(tbody, entry, columns.length + leading);
550
+ if (entry.kind === "data") {
551
+ this.appendRow(tbody, entry.row, entry.rowIndex, columns, leading, entry);
552
+ const rowId = this.rowId(entry.row, entry.rowIndex);
553
+ if (this.masterDetailRenderer && this.expandedDetailIds.has(rowId)) this.appendDetailRow(tbody, entry.row, columns.length + leading);
554
+ }
555
+ });
422
556
  table.appendChild(tbody);
423
557
  return table;
424
558
  }
@@ -439,8 +573,41 @@ var GridNexaAngularComponent = class {
439
573
  });
440
574
  return row;
441
575
  }
442
- appendRow(tbody, row, rowIndex, columns, leading) {
576
+ appendGroupRow(tbody, entry, colSpan) {
577
+ const tr = document.createElement("tr");
578
+ const td = document.createElement("td");
579
+ td.colSpan = colSpan;
580
+ td.style.cssText = "padding:10px;border:1px solid #dbe3ef;background:#eef4ff;color:#153e90;font-weight:800;text-transform:uppercase";
581
+ const toggle = this.button(this.collapsedGroups.has(entry.key) ? "+" : "-", () => {
582
+ this.collapsedGroups.has(entry.key) ? this.collapsedGroups.delete(entry.key) : this.collapsedGroups.add(entry.key);
583
+ this.render();
584
+ });
585
+ td.append(toggle, `${entry.label} ${entry.rows.length} rows${entry.summaries ? ` ${entry.summaries}` : ""}`);
586
+ tr.appendChild(td);
587
+ tbody.appendChild(tr);
588
+ }
589
+ appendDetailRow(tbody, row, colSpan) {
590
+ const detailRow = document.createElement("tr");
591
+ const detail = document.createElement("td");
592
+ detail.colSpan = colSpan;
593
+ detail.style.cssText = "padding:12px;border:1px solid #dbe3ef;background:#f8fbff;color:#334155";
594
+ const content = this.masterDetailRenderer?.(row);
595
+ content instanceof Node ? detail.appendChild(content) : detail.textContent = String(content ?? "");
596
+ detailRow.appendChild(detail);
597
+ tbody.appendChild(detailRow);
598
+ }
599
+ appendRow(tbody, row, rowIndex, columns, leading, display) {
443
600
  const tr = document.createElement("tr");
601
+ tr.draggable = true;
602
+ tr.addEventListener("dragstart", () => {
603
+ this.draggedRowIndex = rowIndex;
604
+ });
605
+ tr.addEventListener("dragover", (event) => event.preventDefault());
606
+ tr.addEventListener("drop", (event) => {
607
+ event.preventDefault();
608
+ if (this.draggedRowIndex != null) this.reorderRow(this.draggedRowIndex, rowIndex);
609
+ this.draggedRowIndex = null;
610
+ });
444
611
  if (this.checkboxSelection) {
445
612
  const td = document.createElement("td");
446
613
  const checkbox = document.createElement("input");
@@ -465,45 +632,123 @@ var GridNexaAngularComponent = class {
465
632
  }
466
633
  columns.forEach((column, columnIndex) => {
467
634
  const td = cell(format(row, column, this.columns));
468
- td.style.cssText = "padding:10px;border:1px solid #dbe3ef";
635
+ td.style.cssText = `padding:10px;border:1px solid #dbe3ef;width:${this.columnWidths.get(column.id) ?? column.width ?? 150}px;${this.pinnedStyle(column, columns)}`;
469
636
  if (this.activeCell?.rowIndex === rowIndex && this.activeCell.columnId === column.id) {
470
637
  td.style.outline = "2px solid #2563eb";
471
638
  td.style.outlineOffset = "-2px";
472
639
  }
640
+ if (this.isCellInRange(rowIndex, column.id, columns)) td.style.background = "rgba(37,99,235,.12)";
473
641
  if (this.findText && format(row, column, this.columns).toLowerCase().includes(this.findText.toLowerCase())) {
474
642
  td.style.background = "rgba(37,99,235,.1)";
475
643
  }
476
- if (this.getTreeDataPath && columnIndex === 0) td.style.paddingLeft = `${12 + Math.max(0, this.getTreeDataPath(row).length - 1) * 24}px`;
477
- td.addEventListener("click", () => {
644
+ if (this.getTreeDataPath && columnIndex === 0) {
645
+ td.style.paddingLeft = `${12 + (display?.depth ?? Math.max(0, this.getTreeDataPath(row).length - 1)) * 24}px`;
646
+ if (display?.hasChildren && display.treeKey) {
647
+ const treeKey = display.treeKey;
648
+ const toggle = this.button(this.collapsedTreeKeys.has(treeKey) ? "+" : "-", () => {
649
+ this.collapsedTreeKeys.has(treeKey) ? this.collapsedTreeKeys.delete(treeKey) : this.collapsedTreeKeys.add(treeKey);
650
+ this.render();
651
+ });
652
+ td.prepend(toggle);
653
+ }
654
+ } else if (this.masterDetailRenderer && columnIndex === 0) {
655
+ const rowId = this.rowId(row, rowIndex);
656
+ const toggle = this.button(this.expandedDetailIds.has(rowId) ? "-" : "+", () => {
657
+ this.expandedDetailIds.has(rowId) ? this.expandedDetailIds.delete(rowId) : this.expandedDetailIds.add(rowId);
658
+ this.render();
659
+ });
660
+ td.prepend(toggle);
661
+ }
662
+ td.addEventListener("click", (event) => {
663
+ this.contextMenu = null;
478
664
  this.activeCell = { rowIndex, columnId: column.id };
665
+ if (event.shiftKey && this.rangeAnchor) {
666
+ this.rangeEnd = { rowIndex, columnId: column.id };
667
+ } else {
668
+ this.rangeAnchor = { rowIndex, columnId: column.id };
669
+ this.rangeEnd = { rowIndex, columnId: column.id };
670
+ }
479
671
  this.cellClick.emit({ row, rowIndex, column });
480
672
  this.render();
481
673
  });
674
+ td.addEventListener("contextmenu", (event) => {
675
+ event.preventDefault();
676
+ this.activeCell = { rowIndex, columnId: column.id };
677
+ this.contextMenu = { x: event.clientX, y: event.clientY, rowIndex, columnId: column.id };
678
+ this.render();
679
+ });
482
680
  if (column.editable) td.addEventListener("dblclick", () => this.editCell(td, row, rowIndex, column));
483
681
  tr.appendChild(td);
484
682
  });
485
683
  tbody.appendChild(tr);
486
- if (this.masterDetailRenderer) {
487
- const detailRow = document.createElement("tr");
488
- const detail = document.createElement("td");
489
- detail.colSpan = columns.length + leading;
490
- const content = this.masterDetailRenderer(row);
491
- content instanceof Node ? detail.appendChild(content) : detail.textContent = String(content ?? "");
492
- detailRow.appendChild(detail);
493
- tbody.appendChild(detailRow);
494
- }
495
684
  }
496
685
  editCell(td, row, rowIndex, column) {
497
686
  const oldValue = rawValue(row, column);
498
- const input = document.createElement("input");
499
- input.value = String(oldValue ?? "");
687
+ const input = this.createEditor(column, oldValue);
500
688
  td.replaceChildren(input);
501
689
  input.focus();
502
690
  input.addEventListener("blur", () => {
503
- this.setCellValue(row, rowIndex, column, input.value);
691
+ this.setCellValue(row, rowIndex, column, input instanceof HTMLInputElement && input.type === "checkbox" ? input.checked : input.value);
504
692
  this.render();
505
693
  }, { once: true });
506
694
  }
695
+ createEditor(column, current) {
696
+ const editor = column.editor;
697
+ if (editor === "checkbox") {
698
+ const input2 = document.createElement("input");
699
+ input2.type = "checkbox";
700
+ input2.checked = Boolean(current);
701
+ return input2;
702
+ }
703
+ if (editor === "number" || editor === "date") {
704
+ const input2 = document.createElement("input");
705
+ input2.type = editor;
706
+ input2.value = String(current ?? "");
707
+ return input2;
708
+ }
709
+ if (editor && typeof editor === "object" && (editor.type === "select" || editor.type === "advancedSelect")) {
710
+ const select = document.createElement("select");
711
+ (editor.values ?? []).forEach((item) => {
712
+ const option = document.createElement("option");
713
+ option.value = String(item);
714
+ option.textContent = String(item);
715
+ select.appendChild(option);
716
+ });
717
+ select.value = String(current ?? "");
718
+ return select;
719
+ }
720
+ const input = document.createElement("input");
721
+ input.value = String(current ?? "");
722
+ return input;
723
+ }
724
+ renderContextMenu() {
725
+ const menu = document.createElement("div");
726
+ menu.style.cssText = `position:fixed;z-index:9999;left:${this.contextMenu?.x ?? 0}px;top:${this.contextMenu?.y ?? 0}px;display:grid;min-width:150px;padding:6px;border:1px solid #dbe3ef;border-radius:10px;background:white;box-shadow:0 18px 48px rgba(15,23,42,.18)`;
727
+ const row = this.contextMenu ? this.rows[this.contextMenu.rowIndex] : void 0;
728
+ const column = this.columns.find((entry) => entry.id === this.contextMenu?.columnId);
729
+ menu.append(
730
+ this.button("Copy", () => void this.copyActiveCell()),
731
+ this.button("Paste", () => void this.pasteActiveCell()),
732
+ this.button("Edit cell", () => {
733
+ if (!row || !column || column.editable === false || !this.contextMenu) return;
734
+ const nextValue = window.prompt(`Edit ${column.headerName}`, String(rawValue(row, column) ?? ""));
735
+ if (nextValue != null) this.setCellValue(row, this.contextMenu.rowIndex, column, nextValue);
736
+ this.contextMenu = null;
737
+ this.render();
738
+ }),
739
+ this.button("Clear cell", () => {
740
+ if (row && column && column.editable !== false && this.contextMenu) this.setCellValue(row, this.contextMenu.rowIndex, column, "");
741
+ this.contextMenu = null;
742
+ this.render();
743
+ }),
744
+ this.button("Hide column", () => {
745
+ if (column) this.hiddenColumnIds.add(column.id);
746
+ this.contextMenu = null;
747
+ this.render();
748
+ })
749
+ );
750
+ return menu;
751
+ }
507
752
  renderToolsPanel() {
508
753
  const panel = document.createElement("div");
509
754
  panel.style.cssText = "display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:12px;padding:12px;border:1px solid #dbe3ef;border-top:0;background:#f8fbff";
package/package.json CHANGED
@@ -1,37 +1,70 @@
1
1
  {
2
2
  "name": "@gridnexa/angular",
3
- "version": "0.0.1",
4
- "description": "Angular package for GridNexa",
3
+ "version": "0.0.3",
4
+ "description": "Enterprise Angular data grid for modern UI apps with Excel-like filtering, editing, formulas, grouping, pivoting, export, and native Angular UX.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "module": "./dist/index.js",
8
8
  "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist/index.cjs"
14
+ }
15
+ },
9
16
  "files": [
10
- "dist"
17
+ "dist",
18
+ "README.md"
11
19
  ],
12
- "scripts": {
13
- "build": "tsup src/index.ts --format esm,cjs --dts --external @angular/core",
14
- "dev": "tsup src/index.ts --watch --external @angular/core",
15
- "clean": "rimraf dist"
16
- },
17
20
  "keywords": [
18
21
  "gridnexa",
22
+ "angular-grid",
23
+ "angular-data-grid",
19
24
  "angular",
20
25
  "data-grid",
26
+ "datagrid",
27
+ "enterprise-grid",
28
+ "excel-grid",
29
+ "spreadsheet",
21
30
  "table",
22
- "excel"
31
+ "pivot-table",
32
+ "tree-grid",
33
+ "filtering",
34
+ "sorting",
35
+ "editable-grid",
36
+ "csv-export",
37
+ "excel-export",
38
+ "typescript"
23
39
  ],
40
+ "homepage": "https://www.gridnexa.in/",
41
+ "bugs": {
42
+ "url": "https://www.gridnexa.in/help"
43
+ },
44
+ "repository": {
45
+ "type": "git",
46
+ "url": "git+https://github.com/mhalungekar9/SmartGrid.git",
47
+ "directory": "packages/angular"
48
+ },
24
49
  "author": "Sachin M",
25
50
  "license": "MIT",
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
26
54
  "peerDependencies": {
27
55
  "@angular/core": ">=16"
28
56
  },
29
57
  "dependencies": {
30
- "@gridnexa/core": "workspace:*"
58
+ "@gridnexa/core": "^0.0.3"
31
59
  },
32
60
  "devDependencies": {
33
61
  "rimraf": "^6.1.3",
34
62
  "tsup": "^8.5.1",
35
63
  "typescript": "^5.9.3"
64
+ },
65
+ "scripts": {
66
+ "build": "tsup src/index.ts --format esm,cjs --dts --external @angular/core",
67
+ "dev": "tsup src/index.ts --watch --external @angular/core",
68
+ "clean": "rimraf dist"
36
69
  }
37
- }
70
+ }