@helveg/ngx-spreadsheet
Version:
Lightweight spreadsheet module for Angular
940 lines (923 loc) • 46.8 kB
JavaScript
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