UNPKG

@helveg/ngx-spreadsheet

Version:

Lightweight spreadsheet module for Angular

940 lines (923 loc) 46.8 kB
import * as i0 from '@angular/core'; import { InjectionToken, inject, EventEmitter, Component, Input, Output, ViewChild, ContentChildren, HostListener, Directive, Injector, runInInjectionContext, NgModule } from '@angular/core'; import { deepmergeCustom, deepmerge } from 'deepmerge-ts'; import { __decorate } from 'tslib'; import { Subjectize } from 'subjectize'; import { ReplaySubject, merge } from 'rxjs'; import { map, scan, distinctUntilChanged } from 'rxjs/operators'; import * as i1 from '@angular/common'; import { CommonModule } from '@angular/common'; import * as i2 from '@ngrx/component'; import { LetDirective } from '@ngrx/component'; class Cell { constructor(tableId, row, col, value, editable = false) { this.tableId = tableId; this.row = row; this.col = col; this.value = value; this.editable = editable; this.id = `${tableId}-${row}-${col}`; } withRow(index) { return new Cell(this.tableId, index, this.col, this.value, this.editable); } withCol(index) { return new Cell(this.tableId, this.row, index, this.value, this.editable); } } const CHARS$1 = [ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', ]; const LENGTH$1 = CHARS$1.length; const generateHeader = (index) => { index -= 1; const remain = Math.floor(index / LENGTH$1); return remain > 0 ? generateHeader(remain) + CHARS$1[index % LENGTH$1] : CHARS$1[index % LENGTH$1]; }; const CHARS = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; const LENGTH = CHARS.length; const generateId = () => new Array(8) .fill(null) .map(() => CHARS.charAt(Math.floor(Math.random() * LENGTH))) .join(''); const NSS_DEFAULT_ROWS = new InjectionToken('NgxSpreadSheetDefaultRows'); const NSS_DEFAULT_COLS = new InjectionToken('NgxSpreadSheetDefaultRows'); const NSS_I18N = new InjectionToken('NgxSpreadSheetInternationalization'); const noArrayDeepMerge = deepmergeCustom({ mergeArrays: false, }); function getDefault(token, defaultValue) { try { return inject(token, { optional: true }) ?? defaultValue; } catch (err) { console.warn(`You are creating a spreadsheet table outside of any injection context. ` + `Any configuration you have provided for ${token} can't be retrieved and ` + `the default value '${defaultValue}' will be used.`); } return defaultValue; } class Table { constructor(id, head, body, canInsertRows, canInsertCols, options) { this.id = id; this.head = head; this.body = body; this.canInsertRows = canInsertRows; this.canInsertCols = canInsertCols; this.options = options; } get data() { return this.body.map((row) => row.map((cell) => cell.value)); } get editing() { return this.body.some((r) => r.some((c) => c.editable)); } recreate(options) { return Table.create(noArrayDeepMerge({}, this.options, options)); } static create(options) { options = noArrayDeepMerge({}, options); const tableId = generateId(); const rows = options.data?.length ?? options.rows ?? getDefault(NSS_DEFAULT_ROWS, 10); const cols = options.data?.[0]?.length ?? options.cols ?? options.columns?.length ?? getDefault(NSS_DEFAULT_COLS, 5); const emptyRow = Array(cols).fill(undefined); const head = emptyRow.map((v, c) => options.columns?.[c]?.header ?? generateHeader(c + 1)); const body = Array(rows) .fill(undefined) .map((v, r) => emptyRow.map((v, c) => new Cell(tableId, r, c, options.data?.[r]?.[c] ?? ''))); return new Table(tableId, head, body, options.canInsertRows ?? true, options.canInsertCols ?? true, options); } findCell(row, col) { for (const record of this.body) { for (const field of record) { if (field.row === row && field.col === col) { return field; } } } return null; } findOrCreateCell(row, col) { for (const record of this.body) { for (const field of record) { if (field.row === row && field.col === col) { return field; } } } const resize = {}; if (this.rowCount <= row) { if (!this.canInsertRows) return null; resize.rows = row + 1; } if (this.colCount <= col) { if (!this.canInsertCols) return null; resize.cols = col + 1; } this.resize(resize); const cell = this.findCell(row, col); if (!cell) { throw new Error(`Unknown table error, could not find or create (${row}, ${col})`); } return cell; } insertColumn(colIndex) { { const remains = this.head.slice(0, colIndex); const updates = Array(this.head.length - colIndex + 1) .fill('') .map((v, c) => generateHeader(c + 1 + colIndex)); this.head = [...remains, ...updates]; } { const body = []; for (let r = 0; r < this.body.length; r++) { const row = this.body[r]; const above = row.slice(0, colIndex); const present = new Cell(this.id, r, colIndex, ''); const below = row .slice(colIndex) .map((cell) => cell.withCol(cell.col + 1)); const newRow = [...above, present, ...below]; body.push(newRow); } this.body = body; } } deleteColumn(colIndex) { { const remains = this.head.slice(0, colIndex); const updates = this.head .slice(colIndex + 1) .map((v, c) => generateHeader(c + 1 + colIndex)); this.head = [...remains, ...updates]; } { const body = []; for (let r = 0; r < this.body.length; r++) { const row = this.body[r]; const above = row.slice(0, colIndex); const below = row .slice(colIndex + 1) .map((cell) => cell.withCol(cell.col + 1)); const newRow = [...above, ...below]; body.push(newRow); } this.body = body; } } insertRow(rowIndex) { const above = this.body.slice(0, rowIndex); const present = Array(this.colCount) .fill('') .map((v, c) => new Cell(this.id, rowIndex, c, '')); const below = this.body .slice(rowIndex) .map((row) => row.map((cell) => cell.withRow(cell.row + 1))); this.body = [...above, present, ...below]; } deleteRow(rowIndex) { const above = this.body.slice(0, rowIndex); const below = this.body .slice(rowIndex + 1) .map((row) => row.map((cell) => cell.withRow(cell.row + 1))); this.body = [...above, ...below]; } get rowCount() { return this.body.length; } get colCount() { return this.head.length; } resize({ rows, cols }) { if (rows !== undefined) { while (this.rowCount < rows) { this.insertRow(this.rowCount); } } if (cols !== undefined) { while (this.colCount < cols) { this.insertColumn(this.colCount); } } } setOption(attr, value) { this.options[attr] = value; this[attr] = value; } } const DELIMITER = '\t'; const PARSE_PATTERN = new RegExp('(\\' + DELIMITER + '|\\r?\\n|\\r|^)' + '(?:"([^"]*(?:""[^"]*)*)"|' + '([^"\\' + DELIMITER + '\\r\\n]*))', 'gi'); const csvToArray = (strData) => { if (strData.endsWith('\r\n')) { strData = strData.slice(0, strData.length - 2); } const arrData = [[]]; let arrMatches = null; while ((arrMatches = PARSE_PATTERN.exec(strData))) { const strMatchedDelimiter = arrMatches[1]; if (strMatchedDelimiter.length && strMatchedDelimiter != DELIMITER) { arrData.push([]); } const strMatchedValue = arrMatches[2] ? arrMatches[2].replace(new RegExp('""', 'g'), '"') : arrMatches[3]; arrData[arrData.length - 1].push(strMatchedValue); } return arrData; }; class Anchor { constructor(r, c) { this.r = r; this.c = c; } } class Range { constructor(r1, c1, r2, c2) { this.r1 = r1; this.c1 = c1; this.r2 = r2; this.c2 = c2; } calc(row, col) { if (row < this.r1) { this.r1 = row; } if (row > this.r2) { this.r2 = row; } if (col < this.c1) { this.c1 = col; } if (col > this.c2) { this.c2 = col; } } includes(row, col) { return row >= this.r1 && row <= this.r2 && col >= this.c1 && col <= this.c2; } equals(range) { return (this.r1 === range.r1 && this.c1 === range.c1 && this.r2 === range.r2 && this.c2 === range.c2); } static of(row, col, row2 = row, col2 = col) { return new Range(row, col, row2, col2); } static marge(a1, a2) { const r1 = a1.r < a2.r ? a1.r : a2.r; const r2 = a1.r > a2.r ? a1.r : a2.r; const c1 = a1.c < a2.c ? a1.c : a2.c; const c2 = a1.c > a2.c ? a1.c : a2.c; return new Range(r1, c1, r2, c2); } } class NgxContextMenuItemComponent { constructor() { this.click = new EventEmitter(); } clicked(index) { if (!this.disabled) { this.click.emit(index); } } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxContextMenuItemComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.10", type: NgxContextMenuItemComponent, selector: "ngx-context-menu-item", inputs: { label: "label", disabled: "disabled", divider: "divider" }, outputs: { click: "click" }, ngImport: i0, template: '', isInline: true }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxContextMenuItemComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-context-menu-item', template: '' }] }], propDecorators: { label: [{ type: Input }], disabled: [{ type: Input }], divider: [{ type: Input }], click: [{ type: Output }] } }); class NgxContextMenuComponent { constructor() { this.closed = new EventEmitter(); this.target = -1; } show(ev, index) { this.target = index; this.menuElement.style.display = 'flex'; const menuTop = ev.clientY + this.menuHeight > this.documentHeight ? ev.pageY - this.menuHeight : ev.pageY; const menuLeft = ev.clientX + this.menuWidth > this.documentWidth ? ev.pageX - this.menuWidth : ev.pageX; this.menuElement.style.top = `${menuTop}px`; this.menuElement.style.left = `${menuLeft}px`; } click() { this.menuElement.style.display = 'none'; this.closed.emit(); } get menuElement() { return this.menuElementRef.nativeElement; } get menuStyle() { return getComputedStyle(this.menuElement); } get menuWidth() { return (this.menuElement.offsetWidth + parseInt(this.menuStyle.marginLeft) + parseInt(this.menuStyle.marginRight) + parseInt(this.menuStyle.paddingLeft) + parseInt(this.menuStyle.paddingRight)); } get menuHeight() { return (this.menuElement.offsetHeight + parseInt(this.menuStyle.marginTop) + parseInt(this.menuStyle.marginBottom) + parseInt(this.menuStyle.paddingTop) + parseInt(this.menuStyle.paddingBottom)); } get documentWidth() { return document.documentElement.clientWidth; } get documentHeight() { return document.documentElement.clientHeight; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxContextMenuComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.10", type: NgxContextMenuComponent, selector: "ngx-context-menu", outputs: { closed: "closed" }, host: { listeners: { "document:click": "click($event)" } }, queries: [{ propertyName: "itemTemplates", predicate: NgxContextMenuItemComponent }], viewQueries: [{ propertyName: "menuElementRef", first: true, predicate: ["menu"], descendants: true, static: true }], ngImport: i0, template: "<div #menu class=\"menu\">\n <ng-container *ngFor=\"let item of itemTemplates\">\n <div class=\"item\" *ngIf=\"!item.divider; else divider\" (click)=\"item.clicked(target)\"\n [class.disabled]=\"item.disabled\">\n {{item.label}}\n </div>\n <ng-template #divider>\n <div class=\"divider\"></div>\n </ng-template>\n </ng-container>\n</div>", styles: [".menu{position:absolute;background-color:#fff;-webkit-user-select:none;user-select:none;min-width:16rem;box-shadow:0 .5rem .8rem #0000001a;z-index:9;display:none;flex-direction:column;padding:.5em 0;border-radius:4px}.menu .item{cursor:pointer;padding:.5em 1em}.menu .item:hover:not(.disabled){background:#f0f0f0}.menu .item.disabled{opacity:.5;cursor:default}.menu .divider{width:100%;margin-top:.5em;padding-top:.5em;border-top:1px solid #ddd}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxContextMenuComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-context-menu', template: "<div #menu class=\"menu\">\n <ng-container *ngFor=\"let item of itemTemplates\">\n <div class=\"item\" *ngIf=\"!item.divider; else divider\" (click)=\"item.clicked(target)\"\n [class.disabled]=\"item.disabled\">\n {{item.label}}\n </div>\n <ng-template #divider>\n <div class=\"divider\"></div>\n </ng-template>\n </ng-container>\n</div>", styles: [".menu{position:absolute;background-color:#fff;-webkit-user-select:none;user-select:none;min-width:16rem;box-shadow:0 .5rem .8rem #0000001a;z-index:9;display:none;flex-direction:column;padding:.5em 0;border-radius:4px}.menu .item{cursor:pointer;padding:.5em 1em}.menu .item:hover:not(.disabled){background:#f0f0f0}.menu .item.disabled{opacity:.5;cursor:default}.menu .divider{width:100%;margin-top:.5em;padding-top:.5em;border-top:1px solid #ddd}\n"] }] }], propDecorators: { menuElementRef: [{ type: ViewChild, args: ['menu', { static: true }] }], itemTemplates: [{ type: ContentChildren, args: [NgxContextMenuItemComponent] }], closed: [{ type: Output }], click: [{ type: HostListener, args: ['document:click', ['$event']] }] } }); class ContentEditableDirective { set content(value) { this.element.innerText = value || ''; } constructor(elementRef) { this.elementRef = elementRef; this.contentChange = new EventEmitter(); this.element.tabIndex = 0; } blur() { this.contentChange.emit(this.element.innerText); } get element() { return this.elementRef.nativeElement; } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: ContentEditableDirective, deps: [{ token: i0.ElementRef }], target: i0.ɵɵFactoryTarget.Directive }); } static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "16.2.10", type: ContentEditableDirective, selector: "[nssContentEditable]", inputs: { content: "content" }, outputs: { contentChange: "contentChange" }, host: { listeners: { "blur": "blur($event.target.value)" } }, ngImport: i0 }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: ContentEditableDirective, decorators: [{ type: Directive, args: [{ selector: '[nssContentEditable]', }] }], ctorParameters: function () { return [{ type: i0.ElementRef }]; }, propDecorators: { content: [{ type: Input }], contentChange: [{ type: Output }], blur: [{ type: HostListener, args: ['blur', ['$event.target.value']] }] } }); function setPipe(obs$, attr) { return obs$.pipe(map((value) => (table) => { table.setOption(attr, value); return table; })); } class NgxSpreadsheetComponent { constructor() { this.injector = inject(Injector); this.i18n = deepmerge({ INSERT_COLUMN_LEFT: 'Insert column left', INSERT_COLUMN_RIGHT: 'Insert column right', DELETE_COLUMN: 'Delete column', DELETE_ROW: 'Delete row', INSERT_ROW_BELOW: 'Insert row below', INSERT_ROW_ABOVE: 'Insert row above', }, inject(NSS_I18N, { optional: true }) ?? {}); this.data = null; this.dataChanged = new EventEmitter(); this.rows = null; this.cols = null; this.columns = null; this.canInsertCols = null; this.canInsertRows = null; this.data$ = new ReplaySubject(1); this.rows$ = new ReplaySubject(1); this.cols$ = new ReplaySubject(1); this.columns$ = new ReplaySubject(1); this.canInsertRows$ = new ReplaySubject(1); this.canInsertCols$ = new ReplaySubject(1); /** * The table observable integrates all the reactive pipes into a higher order scan that * can mutate or replace the table reference. */ this.table$ = merge(setPipe(this.canInsertCols$, 'canInsertCols'), setPipe(this.canInsertRows$, 'canInsertRows'), // Data object reference changed, create new table based on data this.data$.pipe(map((data) => (table) => table.recreate({ data }))), // Row input changed, resize table this.rows$.pipe(map((rows) => (table) => table.resize({ rows }))), // Col input changed, resize table this.cols$.pipe(map((cols) => (table) => table.resize({ cols }))), // Columns changed, recreate table this.columns$.pipe(map((columns) => (table) => table.recreate({ columns })))).pipe(scan((table, modifier) => (this.table = runInInjectionContext(this.injector, () => modifier(table)) ?? table), Table.create({})), distinctUntilChanged()); this.copied = new EventEmitter(); this.table = null; this.activatedCell = null; this.range = null; this.anchor = null; this.activeTheadIndex = -1; this.activeTbodyIndex = -1; } onMouseDown(ev) { const { row, col, valid } = this.getPositionFromId(ev.target); if (!valid) { return; } this.range = Range.of(row, col); if (!ev.shiftKey || !this.anchor) { this.anchor = new Anchor(row, col); } } onMouseMove(ev) { if (!this.range || !this.anchor) { return; } const self = this.getPositionFromId(ev.target); if (self.valid) { const range = Range.marge({ r: self.row, c: self.col }, this.anchor); if (!this.range?.equals(range)) { this.range = range; } } } onMouseUp(ev) { if (ev.shiftKey && this.anchor) { const self = this.getPositionFromId(ev.target); if (self.valid) { const range = Range.marge({ r: self.row, c: self.col }, this.anchor); if (!this.range?.equals(range)) { this.range = range; } } } this.anchor = null; } onKeyDown(ev) { const key = ev.key.toLowerCase(); const isCtrl = (ev.ctrlKey && !ev.metaKey) || (!ev.ctrlKey && ev.metaKey); if (!this.table) { return; } if (!this.anchor && ev.shiftKey && this.activatedCell) { const { row, col } = this.activatedCell; this.anchor = new Anchor(row, col); } if (key === 'enter' && this.activatedCell) { const { row, col, editable } = this.activatedCell; ev.preventDefault(); this.moveTo(row + 1, col, false); } else if (key === 'tab' && this.activatedCell) { ev.preventDefault(); const { rowCount, colCount } = this.table; const { row, col, editable } = this.activatedCell; const next = ev.shiftKey ? col - 1 : col + 1; if (next < 0 && row > 0) { this.moveTo(row - 1, colCount - 1, false); } else if (next >= colCount && row < rowCount) { this.moveTo(row + 1, 0, false); } else { this.moveTo(row, next, false); } } else if (key === 'f2') { this.setEditable(ev, true); } else if (key === 'escape') { this.setEditable(ev, false); } else if (key === 'a' && isCtrl) { this.selectAll(ev); } else if (key === 'c' && isCtrl) { this.copy(); } else if (key === 'v' && isCtrl) { this.paste(); } else if (key === 'delete') { this.delete(); } else if (this.activatedCell && !this.activatedCell.editable && /^.$/u.test(key)) { this.activatedCell.value = ''; this.setEditable(ev, true); this.forceFocus(ev.target); } this.blockArrowKeys(ev); } blockArrowKeys(ev) { if (ev.key.toLowerCase().startsWith('arrow')) { ev.stopPropagation(); ev.preventDefault(); } } onKeyUp(ev) { if (!this.activatedCell || this.activatedCell.editable) { return; } if (!ev.shiftKey) { this.anchor = null; } const { row, col } = this.activatedCell; switch (ev.key.toLowerCase()) { case 'arrowup': this.moveTo(row - 1, col, ev.shiftKey); break; case 'arrowdown': this.moveTo(row + 1, col, ev.shiftKey); break; case 'arrowleft': this.moveTo(row, col - 1, ev.shiftKey); break; case 'arrowright': this.moveTo(row, col + 1, ev.shiftKey); break; } this.blockArrowKeys(ev); } trackByCell(index, value) { return value ? value.id : null; } clickHeader(colIndex) { const rowLength = this.table?.body.length || 0; if (rowLength > 0) { this.range = Range.of(0, colIndex, rowLength, colIndex); } } clickRow(rowIndex) { if (!this.table) { return; } if (rowIndex >= 0 && rowIndex < this.table.body.length) { const cols = this.table.body[rowIndex]; this.range = Range.of(rowIndex, 0, rowIndex, cols.length); } } focus(ev) { const found = this.findCellByEventTarget(ev.target); this.activatedCell = found; } blur(ev) { const found = this.findCellByEventTarget(ev.target); if (found) { found.editable = false; } } cellMouseUp(ev, target) { const td = ev.target; if (target === this.activatedCell) { target.editable = true; } } setValue(ev, target) { const value = ev.target.innerText || ''; target.value = value; } setEditable(ev, editable) { ev.stopPropagation(); const found = this.findCellByEventTarget(ev.target); if (found) { found.editable = editable; } } showTheadMenu(ev, index) { ev.stopPropagation(); this.theadContextMenu.show(ev, index); // Return false to prevent browser from opening its own context menu on top return false; } showTbodyMenu(ev, index) { ev.stopPropagation(); this.tbodyContextMenu.show(ev, index); // Return false to prevent browser from opening its own context menu on top return false; } moveTo(row, col, shiftKey) { if (!this.table) { return; } const { rowCount, colCount } = this.table; const resize = {}; if (rowCount <= row && this.table.canInsertRows) { resize.rows = row + 1; } if (colCount <= col && this.table.canInsertCols) { resize.cols = col + 1; } this.table.resize(resize); const { body } = this.table; if (row >= 0 && row < body.length) { const cols = body[row]; if (col >= 0 && col < cols.length) { const cell = cols[col]; setTimeout(() => { const e = document.getElementById(cell.id); if (e) { this.forceFocus(e); } }); if (shiftKey && this.range && this.anchor) { this.range = Range.marge(this.anchor, { r: row, c: col }); } else { this.range = Range.of(cell.row, cell.col); } } } } forceFocus(el) { el.focus(); const s = window.getSelection(); const r = document.createRange(); r.setStart(el, el.childElementCount); r.setEnd(el, el.childElementCount); s?.removeAllRanges(); s?.addRange(r); } findCellByEventTarget(target) { const { row, col, valid } = this.getPositionFromId(target); return valid ? this.table?.findCell(row, col) || null : null; } getPositionFromId(target) { const element = target; if (!this.table || !element?.id?.match(/(\w+)-(\d+)-(\d+)/)) { return { row: NaN, col: NaN, valid: false }; } const valid = RegExp.$1 === this.table.id; const row = parseInt(RegExp.$2 || '', 10); const col = parseInt(RegExp.$3 || '', 10); return { row, col, valid }; } copy() { if (!this.table || !this.range) { return; } const lines = []; for (let r = this.range.r1; r <= this.range.r2; r++) { const line = []; for (let c = this.range.c1; c <= this.range.c2; c++) { const cell = this.table.findCell(r, c); if (cell) { const value = cell.value.match(/[\t\n\r  "]+/) ? '"' + cell.value.split('"').join('""') + '"' : cell.value; line.push(value); } } lines.push(line.join('\t')); } const text = lines.join('\n'); if (text) { navigator.clipboard.writeText(text).then(() => this.copied.emit(text)); } } paste() { if (!this.table || !this.range) { return; } const { r1, c1, r2, c2 } = this.range; navigator.clipboard.readText().then((data) => { const ar = csvToArray(data); if (!ar.length) { return; } if (ar.length === 1 && ar[0].length === 1) { // There is only 1 pasted value, paste it everywhere const clipboardText = ar[0][0]; for (let r = r1; r <= r2; r++) { for (let c = c1; c <= c2; c++) { const cell = this.table.findCell(r, c); if (cell) { cell.value = clipboardText; } } } } else { let mr = 0, mc = 0; for (let r = 0, tableRow = r1; r < ar.length; r++, tableRow++) { const row = ar[r]; for (let c = 0, tableCol = c1; c < row.length; c++, tableCol++) { const col = row[c]; const cell = this.table.findOrCreateCell(tableRow, tableCol); if (cell) { cell.value = col; mr = Math.max(cell.row, mr); mc = Math.max(cell.col, mc); } } } this.range = Range.of(r1, c1, mr, mc); } }); this.dataChanged.emit(this.table.data); } delete() { if (!this.table || !this.range) { return; } const { r1, c1, r2, c2 } = this.range; for (let r = r1; r <= r2; r++) { for (let c = c1; c <= c2; c++) { const cell = this.table.findCell(r, c); if (cell) { cell.value = ''; } } } this.dataChanged.emit(this.table.data); } updateValue(table, cell, $event) { if (cell.value != $event) { cell.value = $event; this.dataChanged.emit(table.data); } } selectAll(event$) { if (!this.table || this.table.editing) { return; } this.range = Range.of(0, 0, this.table.rowCount, this.table.colCount); if (event$) { event$.stopPropagation(); event$.preventDefault(); } } newRow(col) { if (!this.table) return; this.table.resize({ rows: this.table.rowCount + 1 }); this.activatedCell = this.table.findCell(this.table.rowCount - 1, col); this.range = new Range(this.table.rowCount - 1, col, this.table.rowCount - 1, col); setTimeout(() => { if (this.activatedCell) { const el = document.getElementById(this.activatedCell.id); if (el) { this.forceFocus(el); } } }); } static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxSpreadsheetComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); } static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.10", type: NgxSpreadsheetComponent, selector: "ngx-spreadsheet", inputs: { data: "data", rows: "rows", cols: "cols", columns: "columns", canInsertCols: "canInsertCols", canInsertRows: "canInsertRows" }, outputs: { dataChanged: "dataChanged", copied: "copied" }, host: { listeners: { "mousedown": "onMouseDown($event)", "document:mousemove": "onMouseMove($event)", "document:mouseup": "onMouseUp($event)", "document:keydown": "onKeyDown($event)", "document:keyup": "onKeyUp($event)" } }, viewQueries: [{ propertyName: "theadContextMenu", first: true, predicate: ["theadMenu"], descendants: true }, { propertyName: "tbodyContextMenu", first: true, predicate: ["tbodyMenu"], descendants: true }], ngImport: i0, template: "<ng-container *ngrxLet=\"table$ as table\">\n <div class=\"container\">\n <table #htmlTable>\n <!--Table header-->\n <thead>\n <tr>\n <!--Empty top left header cell-->\n <th></th>\n <!--Column header cells-->\n <th\n *ngFor=\"let col of table.head; let c = index\"\n (click)=\"clickHeader(c)\"\n (contextmenu)=\"showTheadMenu($event, c)\"\n (mouseenter)=\"activeTheadIndex = c\"\n (mouseleave)=\"activeTheadIndex = -1\"\n >\n {{ col }}\n </th>\n </tr>\n </thead>\n <!--Table body-->\n <tbody style=\"overflow-y: auto\">\n <!--Table rows-->\n <tr *ngFor=\"let row of table.body; let r = index\">\n <!--Row header cell-->\n <th\n (click)=\"clickRow(r)\"\n (contextmenu)=\"showTbodyMenu($event, r)\"\n (mouseenter)=\"activeTbodyIndex = r\"\n (mouseleave)=\"activeTbodyIndex = -1\"\n >\n {{ r + 1 }}\n </th>\n <!--Table data cell-->\n <td\n [id]=\"cell.id\"\n *ngFor=\"let cell of row; let c = index; trackBy: trackByCell\"\n [class.focus]=\"cell === activatedCell\"\n [class.sel]=\"range?.includes(cell.row, cell.col)\"\n nssContentEditable\n [content]=\"cell.value\"\n (contentChange)=\"updateValue(table, cell, $event)\"\n (focus)=\"focus($event)\"\n (blur)=\"blur($event)\"\n (mousedown)=\"cellMouseUp($event, cell)\"\n [attr.contenteditable]=\"cell.editable\"\n ></td>\n </tr>\n <!--New data row, click to increase row size-->\n <tr *ngIf=\"table.canInsertRows\">\n <th>*</th>\n <td\n *ngFor=\"let _ of [].constructor(table.colCount); let i = index\"\n (click)=\"newRow(i)\"\n ></td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--Column header context menu-->\n <ngx-context-menu #theadMenu (closed)=\"activeTheadIndex = -1\">\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_COLUMN_LEFT\"\n (click)=\"table.insertColumn($event)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_COLUMN_RIGHT\"\n (click)=\"table.insertColumn($event + 1)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item [divider]=\"true\"></ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.DELETE_COLUMN\"\n [disabled]=\"table.colCount <= 1\"\n (click)=\"table.deleteColumn($event)\"\n >\n </ngx-context-menu-item>\n </ngx-context-menu>\n\n <!--Row header context menu-->\n <ngx-context-menu #tbodyMenu (closed)=\"activeTbodyIndex = -1\">\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_ROW_ABOVE\"\n (click)=\"table.insertRow($event)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_ROW_BELOW\"\n (click)=\"table.insertRow($event + 1)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item [divider]=\"true\"></ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.DELETE_ROW\"\n [disabled]=\"table.rowCount <= 1\"\n (click)=\"table.deleteRow($event)\"\n >\n </ngx-context-menu-item>\n </ngx-context-menu>\n</ng-container>\n", styles: [".container{font-size:14px}.container table,.container td,.container th,.container tr{border-spacing:0;height:100%}.container table{margin-bottom:2px}.container table thead{background:#f1f1f1;position:sticky;top:0;z-index:2}.container table thead th:first-child{background:#f1f1f1;z-index:3}.container table tbody th{min-width:5rem;background:#fafafa;z-index:1}.container table th{font-weight:400;padding:.2rem .4rem;border:1px solid #ddd;-webkit-user-select:none;user-select:none}.container table th:first-child{position:sticky;left:0}.container table th .head{display:grid;grid-template-columns:1fr auto}.container table th .head .label{grid-column:1/3;grid-row:1}.container table th .head .dropdown{grid-column:2/3;grid-row:1}.container table th .head .dropdown button{background-color:#e2e2e2d0;color:#00000080;padding:0rem .3rem;border:none;cursor:pointer;transition:ease .4s;transform:rotate(90deg);font-family:ui-monospace;-webkit-user-select:none;user-select:none}.container table td{min-width:10rem;padding:.2rem .5rem;border:1px solid #ddd;-webkit-user-select:none;user-select:none}.container table td.focus{outline:2px solid dodgerblue;-webkit-user-select:auto;user-select:auto;position:relative;z-index:4}.container table td.sel{background:#eaf1fd}.container table td[contenteditable=true]{-webkit-user-select:auto;user-select:auto;outline:2px solid #48c21a;background:#eaffe2}\n"], dependencies: [{ kind: "directive", type: i1.NgForOf, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: i1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "directive", type: i2.LetDirective, selector: "[ngrxLet]", inputs: ["ngrxLet", "ngrxLetSuspenseTpl"] }, { kind: "component", type: NgxContextMenuComponent, selector: "ngx-context-menu", outputs: ["closed"] }, { kind: "component", type: NgxContextMenuItemComponent, selector: "ngx-context-menu-item", inputs: ["label", "disabled", "divider"], outputs: ["click"] }, { kind: "directive", type: ContentEditableDirective, selector: "[nssContentEditable]", inputs: ["content"], outputs: ["contentChange"] }] }); } } __decorate([ Subjectize('data') ], NgxSpreadsheetComponent.prototype, "data$", void 0); __decorate([ Subjectize('rows') ], NgxSpreadsheetComponent.prototype, "rows$", void 0); __decorate([ Subjectize('cols') ], NgxSpreadsheetComponent.prototype, "cols$", void 0); __decorate([ Subjectize('columns') ], NgxSpreadsheetComponent.prototype, "columns$", void 0); __decorate([ Subjectize('canInsertRows') ], NgxSpreadsheetComponent.prototype, "canInsertRows$", void 0); __decorate([ Subjectize('canInsertCols') ], NgxSpreadsheetComponent.prototype, "canInsertCols$", void 0); i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxSpreadsheetComponent, decorators: [{ type: Component, args: [{ selector: 'ngx-spreadsheet', template: "<ng-container *ngrxLet=\"table$ as table\">\n <div class=\"container\">\n <table #htmlTable>\n <!--Table header-->\n <thead>\n <tr>\n <!--Empty top left header cell-->\n <th></th>\n <!--Column header cells-->\n <th\n *ngFor=\"let col of table.head; let c = index\"\n (click)=\"clickHeader(c)\"\n (contextmenu)=\"showTheadMenu($event, c)\"\n (mouseenter)=\"activeTheadIndex = c\"\n (mouseleave)=\"activeTheadIndex = -1\"\n >\n {{ col }}\n </th>\n </tr>\n </thead>\n <!--Table body-->\n <tbody style=\"overflow-y: auto\">\n <!--Table rows-->\n <tr *ngFor=\"let row of table.body; let r = index\">\n <!--Row header cell-->\n <th\n (click)=\"clickRow(r)\"\n (contextmenu)=\"showTbodyMenu($event, r)\"\n (mouseenter)=\"activeTbodyIndex = r\"\n (mouseleave)=\"activeTbodyIndex = -1\"\n >\n {{ r + 1 }}\n </th>\n <!--Table data cell-->\n <td\n [id]=\"cell.id\"\n *ngFor=\"let cell of row; let c = index; trackBy: trackByCell\"\n [class.focus]=\"cell === activatedCell\"\n [class.sel]=\"range?.includes(cell.row, cell.col)\"\n nssContentEditable\n [content]=\"cell.value\"\n (contentChange)=\"updateValue(table, cell, $event)\"\n (focus)=\"focus($event)\"\n (blur)=\"blur($event)\"\n (mousedown)=\"cellMouseUp($event, cell)\"\n [attr.contenteditable]=\"cell.editable\"\n ></td>\n </tr>\n <!--New data row, click to increase row size-->\n <tr *ngIf=\"table.canInsertRows\">\n <th>*</th>\n <td\n *ngFor=\"let _ of [].constructor(table.colCount); let i = index\"\n (click)=\"newRow(i)\"\n ></td>\n </tr>\n </tbody>\n </table>\n </div>\n <!--Column header context menu-->\n <ngx-context-menu #theadMenu (closed)=\"activeTheadIndex = -1\">\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_COLUMN_LEFT\"\n (click)=\"table.insertColumn($event)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_COLUMN_RIGHT\"\n (click)=\"table.insertColumn($event + 1)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item [divider]=\"true\"></ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.DELETE_COLUMN\"\n [disabled]=\"table.colCount <= 1\"\n (click)=\"table.deleteColumn($event)\"\n >\n </ngx-context-menu-item>\n </ngx-context-menu>\n\n <!--Row header context menu-->\n <ngx-context-menu #tbodyMenu (closed)=\"activeTbodyIndex = -1\">\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_ROW_ABOVE\"\n (click)=\"table.insertRow($event)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.INSERT_ROW_BELOW\"\n (click)=\"table.insertRow($event + 1)\"\n >\n </ngx-context-menu-item>\n <ngx-context-menu-item [divider]=\"true\"></ngx-context-menu-item>\n <ngx-context-menu-item\n [label]=\"i18n.DELETE_ROW\"\n [disabled]=\"table.rowCount <= 1\"\n (click)=\"table.deleteRow($event)\"\n >\n </ngx-context-menu-item>\n </ngx-context-menu>\n</ng-container>\n", styles: [".container{font-size:14px}.container table,.container td,.container th,.container tr{border-spacing:0;height:100%}.container table{margin-bottom:2px}.container table thead{background:#f1f1f1;position:sticky;top:0;z-index:2}.container table thead th:first-child{background:#f1f1f1;z-index:3}.container table tbody th{min-width:5rem;background:#fafafa;z-index:1}.container table th{font-weight:400;padding:.2rem .4rem;border:1px solid #ddd;-webkit-user-select:none;user-select:none}.container table th:first-child{position:sticky;left:0}.container table th .head{display:grid;grid-template-columns:1fr auto}.container table th .head .label{grid-column:1/3;grid-row:1}.container table th .head .dropdown{grid-column:2/3;grid-row:1}.container table th .head .dropdown button{background-color:#e2e2e2d0;color:#00000080;padding:0rem .3rem;border:none;cursor:pointer;transition:ease .4s;transform:rotate(90deg);font-family:ui-monospace;-webkit-user-select:none;user-select:none}.container table td{min-width:10rem;padding:.2rem .5rem;border:1px solid #ddd;-webkit-user-select:none;user-select:none}.container table td.focus{outline:2px solid dodgerblue;-webkit-user-select:auto;user-select:auto;position:relative;z-index:4}.container table td.sel{background:#eaf1fd}.container table td[contenteditable=true]{-webkit-user-select:auto;user-select:auto;outline:2px solid #48c21a;background:#eaffe2}\n"] }] }], propDecorators: { theadContextMenu: [{ type: ViewChild, args: ['theadMenu'] }], tbodyContextMenu: [{ type: ViewChild, args: ['tbodyMenu'] }], data: [{ type: Input }], dataChanged: [{ type: Output }], rows: [{ type: Input }], cols: [{ type: Input }], columns: [{ type: Input }], canInsertCols: [{ type: Input }], canInsertRows: [{ type: Input }], data$: [], rows$: [], cols$: [], columns$: [], canInsertRows$: [], canInsertCols$: [], copied: [{ type: Output }], onMouseDown: [{ type: HostListener, args: ['mousedown', ['$event']] }], onMouseMove: [{ type: HostListener, args: ['document:mousemove', ['$event']] }], onMouseUp: [{ type: HostListener, args: ['document:mouseup', ['$event']] }], onKeyDown: [{ type: HostListener, args: ['document:keydown', ['$event']] }], onKeyUp: [{ type: HostListener, args: ['document:keyup', ['$event']] }] } }); class NgxSpreadsheetModule { static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxSpreadsheetModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule }); } static { this.ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "16.2.10", ngImport: i0, type: NgxSpreadsheetModule, declarations: [NgxSpreadsheetComponent, NgxContextMenuComponent, NgxContextMenuItemComponent, ContentEditableDirective], imports: [CommonModule, LetDirective], exports: [NgxSpreadsheetComponent] }); } static { this.ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxSpreadsheetModule, imports: [CommonModule] }); } } i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.10", ngImport: i0, type: NgxSpreadsheetModule, decorators: [{ type: NgModule, args: [{ declarations: [ NgxSpreadsheetComponent, NgxContextMenuComponent, NgxContextMenuItemComponent, ContentEditableDirective, ], imports: [CommonModule, LetDirective], exports: [NgxSpreadsheetComponent], }] }] }); /* * Public API Surface of ngx-spreadsheet */ /** * Generated bundle index. Do not edit. */ export { NgxSpreadsheetComponent, NgxSpreadsheetModule, Table }; //# sourceMappingURL=helveg-ngx-spreadsheet.mjs.map