@progress/kendo-angular-spreadsheet
Version:
A Spreadsheet Component for Angular
429 lines (428 loc) • 21.5 kB
JavaScript
/**-----------------------------------------------------------------------------------------
* Copyright © 2025 Progress Software Corporation. All rights reserved.
* Licensed under commercial license. See LICENSE.md in the project root for more information
*-------------------------------------------------------------------------------------------*/
import { Component, ElementRef, HostBinding, Input, NgZone, QueryList, Renderer2, ViewChild, ViewChildren } from '@angular/core';
import { NgFor, NgIf } from '@angular/common';
import { Subscription } from 'rxjs';
import { take } from 'rxjs/operators';
import { ButtonComponent, DropDownButtonComponent } from '@progress/kendo-angular-buttons';
import { Keys, isDocumentAvailable, isPresent, EventsOutsideAngularDirective } from '@progress/kendo-angular-common';
import { DialogService } from '@progress/kendo-angular-dialog';
import { TabStripComponent, TabTemplateDirective, TabStripTabComponent } from '@progress/kendo-angular-layout';
import { caretAltDownIcon, menuIcon, plusIcon, eyeIcon, eyeSlashIcon } from '@progress/kendo-svg-icons';
import { SpreadsheetService } from '../common/spreadsheet.service';
import { SpreadsheetLocalizationService } from '../localization/spreadsheet-localization.service';
import { ActionDialogComponent } from './action-dialog.component';
import { mapToSheetDescriptor } from '../utils';
import { getSheetActions } from './utils';
import * as i0 from "@angular/core";
import * as i1 from "../common/spreadsheet.service";
import * as i2 from "../localization/spreadsheet-localization.service";
import * as i3 from "@progress/kendo-angular-dialog";
/**
* @hidden
*/
export class SheetsBarComponent {
spreadsheetService;
element;
renderer;
localization;
dialogService;
ngZone;
hostClasses = true;
sheets;
sheetDescriptors;
subs = new Subscription();
constructor(spreadsheetService, element, renderer, localization, dialogService, ngZone) {
this.spreadsheetService = spreadsheetService;
this.element = element;
this.renderer = renderer;
this.localization = localization;
this.dialogService = dialogService;
this.ngZone = ngZone;
this.subs.add(spreadsheetService.onSheetsBarFocus.subscribe(() => ngZone.run(() => this.tabstrip.selectTab(this.sheets.findIndex(sh => sh.text === spreadsheetService.currentActiveSheet)))));
}
addButton;
menuButton;
tabstrip;
actionDdbs;
actionDdbRefs;
get activeSheet() {
return this.spreadsheetService.spreadsheet?.activeSheet()?.name();
}
plusIcon = plusIcon;
menuIcon = menuIcon;
caretAltDownIcon = caretAltDownIcon;
selected = false;
sheetsMenuList = [];
openedDdb = null;
tabListSub;
hiddenStateIcons = {
hidden: 'eye-slash',
visible: 'eye'
};
hiddenStateSVGIcons = {
hidden: eyeSlashIcon,
visible: eyeIcon
};
get tablistId() {
return this.spreadsheetService.tablistId;
}
ngAfterViewInit() {
if (!isDocumentAvailable() || !this.element.nativeElement) {
return;
}
const tablist = this.element.nativeElement.querySelector('.k-tabstrip-items');
this.renderer.setAttribute(tablist, 'id', this.tablistId);
this.tabListSub = this.renderer.listen(tablist, 'keydown', this.onTabListKeyDown.bind(this));
}
ngOnChanges(changes) {
if (changes['sheets']) {
this.sheetsMenuList = this.sheets?.map(sheet => ({
text: sheet.text,
icon: this.hiddenStateIcons[sheet.state],
svgIcon: this.hiddenStateSVGIcons[sheet.state]
}));
this.sheets = changes['sheets'].currentValue?.map((sheet, _, items) => ({
...sheet,
sheetActions: getSheetActions(items, sheet)
.map(item => ({ ...item, text: this.messageFor(item.messageKey) }))
}));
}
}
ngOnDestroy() {
if (this.tabListSub) {
this.tabListSub();
}
this.subs.unsubscribe();
}
onAddClick = () => {
if (this.spreadsheetService.spreadsheet) {
this.spreadsheetService.spreadsheet.view.sheetsbar.onAddSelect();
this.notifySheetsChange();
}
};
onTabSelect(ev) {
if (ev.title !== this.activeSheet) {
this.selectSheet(ev.title);
}
}
onOpen(ddb) {
if (isPresent(this.openedDdb) && this.openedDdb !== ddb) {
this.openedDdb.toggle(false);
}
this.openedDdb = ddb;
}
onClose() {
this.openedDdb = null;
const activeTabIdx = this.sheets.findIndex(sheet => sheet.active);
this.tabstrip.selectTab(activeTabIdx);
}
onActionClick(dataItem, sheet) {
if (dataItem.disabled) {
return;
}
if (dataItem.commandName === 'delete' || dataItem.commandName === 'rename') {
this.openDialog(dataItem, sheet);
}
else {
this.actionsCallback[dataItem.commandName](sheet, dataItem.messageKey);
}
}
onMenuItemClick(item) {
const sheet = this.sheets.find(s => s.text === item.text);
sheet.state = 'visible';
this.spreadsheetService.spreadsheet.sheets().find(sh => sh.name() === sheet.text)._state('visible');
this.selectSheet(sheet.text);
}
messageFor(key) {
return this.localization.get(key);
}
openDialog(dataItem, sheet) {
const dialogSettings = {
appendTo: this.spreadsheetService.dialogContainer,
title: this.messageFor(dataItem.commandName),
content: ActionDialogComponent,
actions: [{
text: this.messageFor(dataItem.dialogButton),
themeColor: 'primary'
}, {
text: this.messageFor('dialogCancel')
}],
actionsLayout: 'stretched',
autoFocusedElement: '.k-textbox .k-input-inner, .k-button-solid-primary'
};
const dialog = this.dialogService.open(dialogSettings);
const dialogInstance = dialog.dialog.instance;
const dialogContent = dialog.content.instance;
dialogInstance.action.pipe(take(1)).subscribe((event) => {
if (event.text === this.messageFor(dataItem.dialogButton)) {
const sheetsBar = this.spreadsheetService.spreadsheet.view.sheetsbar;
if (sheetsBar) {
const allSheets = this.spreadsheetService.spreadsheet.sheets();
const sheetIndex = allSheets.findIndex(s => s.name() === sheet.text);
dataItem.commandName === 'delete' ? sheetsBar.onSheetRemove(sheet.text) : sheetsBar.onSheetRename(dialogContent.value, sheetIndex);
this.notifySheetsChange();
}
}
});
dialogContent.setData({
value: sheet.text,
tabindex: -1,
commandName: dataItem.commandName
});
}
getCopyRegex(sheetName) {
const newName = sheetName.replaceAll('(', '\\(').replaceAll(')', '\\)');
const st = `(${newName})\\s?\\(`;
return new RegExp(st, 's');
}
actionsCallback = {
copy: (sheetInfo) => {
let copies = 0;
const regex = this.getCopyRegex(sheetInfo.text);
this.sheets.forEach(sheet => {
const isPresent = regex.test(sheet.text);
if (isPresent) {
copies += 1;
}
});
const sheetToCopy = this.spreadsheetService.spreadsheet.sheets().find(s => s.name() === sheetInfo.text);
const newName = `${sheetInfo.text} (${copies + 1})`;
this.spreadsheetService.spreadsheet.insertSheet({ data: { ...sheetToCopy.toJSON(), name: newName }, index: sheetInfo.index + 1 });
this.selectSheet(newName);
},
move: (sheetInfo, itemKey) => {
const isMoveRight = itemKey === 'sheetMoveRight';
let oldIndex = -1;
let newIndex = -1;
const sheets = this.spreadsheetService.spreadsheet.sheets();
if (isMoveRight) {
for (let i = 0; i < sheets.length; i++) {
if (sheets[i].name() === sheetInfo.text) {
oldIndex = i;
}
if (oldIndex > -1 && i > oldIndex && sheets[i]._state() === 'visible') {
newIndex = i;
break;
}
}
}
else {
for (let i = sheets.length - 1; i >= 0; i--) {
if (sheets[i].name() === sheetInfo.text) {
oldIndex = i;
}
if (oldIndex > -1 && (i < oldIndex) && sheets[i]._state() === 'visible') {
newIndex = i;
break;
}
}
}
const sheetsBar = this.spreadsheetService.spreadsheet.view.sheetsbar;
sheetsBar.onSheetReorderEnd({ oldIndex, newIndex });
this.selectSheet(sheetInfo.text);
this.notifySheetsChange();
},
hide: (sheet) => {
sheet.state = 'hidden';
const sheets = this.spreadsheetService.spreadsheet.sheets();
const sheetIndex = sheets.findIndex(s => s.name() === sheet.text);
sheets[sheetIndex]._state('hidden');
const newSelectedIndex = sheetIndex < sheets.length - 1 ? sheetIndex + 1 : 0;
const sheetToSelect = sheets[newSelectedIndex].name();
this.selectSheet(sheetToSelect);
this.notifySheetsChange();
}
};
selectSheet(sheetName) {
const spreadsheetSheet = this.spreadsheetService.spreadsheet.sheets().find(s => s.name() === sheetName);
this.spreadsheetService.spreadsheet.activeSheet(spreadsheetSheet);
this.spreadsheetService.currentActiveSheet = sheetName;
this.spreadsheetService.activeSheetChanged.next(spreadsheetSheet);
this.notifySheetsChange();
}
onTabListKeyDown(ev) {
const buttonEl = ev.target.querySelector('kendo-dropdownbutton');
const index = Array.from(this.actionDdbRefs).findIndex(el => el.nativeElement === buttonEl);
const ddb = Array.from(this.actionDdbs)[index];
if (!ddb) {
return;
}
const altKey = ev.altKey;
const arrowDown = ev.keyCode === Keys.ArrowDown;
const shouldOpenDdb = altKey && arrowDown && !ddb.isOpen;
if (shouldOpenDdb) {
ev.preventDefault();
ddb.togglePopupVisibility();
}
}
notifySheetsChange() {
this.ngZone.run(() => {
const newSheets = this.spreadsheetService.spreadsheet.sheets();
const sheetDesc = mapToSheetDescriptor(newSheets);
this.sheets = sheetDesc
.flatMap((item, index, items) => item.state === 'visible' ? [{
...item,
inEdit: false,
first: index === 0,
last: index === items.length - 1,
text: item.name,
active: (item.name === this.activeSheet) || items.length === 1,
index,
sheetActions: getSheetActions(items, item)
.map(item => ({ ...item, text: this.messageFor(item.messageKey) }))
}] : []);
this.sheetsMenuList = this.sheets?.map(sheet => ({
text: sheet.text,
icon: this.hiddenStateIcons[sheet.state],
svgIcon: this.hiddenStateSVGIcons[sheet.state]
}));
});
this.ngZone.onStable.pipe(take(1)).subscribe(() => this.spreadsheetService.spreadsheet.view.clipboard.focus());
}
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SheetsBarComponent, deps: [{ token: i1.SpreadsheetService }, { token: i0.ElementRef }, { token: i0.Renderer2 }, { token: i2.SpreadsheetLocalizationService }, { token: i3.DialogService }, { token: i0.NgZone }], target: i0.ɵɵFactoryTarget.Component });
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "16.2.12", type: SheetsBarComponent, isStandalone: true, selector: "[kendoSpreadsheetSheetsBar]", inputs: { sheets: "sheets", sheetDescriptors: "sheetDescriptors" }, host: { properties: { "class.k-spreadsheet-sheets-bar": "this.hostClasses" } }, viewQueries: [{ propertyName: "addButton", first: true, predicate: ["addButton"], descendants: true }, { propertyName: "menuButton", first: true, predicate: ["menuButton"], descendants: true }, { propertyName: "tabstrip", first: true, predicate: ["tabstrip"], descendants: true }, { propertyName: "actionDdbs", predicate: ["sheetDdb"], descendants: true }, { propertyName: "actionDdbRefs", predicate: ["sheetDdb"], descendants: true, read: ElementRef }], usesOnChanges: true, ngImport: i0, template: `
<button kendoButton #addButton
[title]="messageFor('addSheet')"
type="button"
fillMode="flat"
class="k-spreadsheet-sheet-add"
icon="plus"
[svgIcon]="plusIcon"
[kendoEventsOutsideAngular]="{click: onAddClick}"
[attr.aria-controls]="tablistId">
</button>
<kendo-dropdownbutton #menuButton
fillMode="flat"
buttonClass="k-spreadsheet-sheets-menu"
icon="menu"
[svgIcon]="menuIcon"
[data]="sheetsMenuList"
(itemClick)="onMenuItemClick($event)"
(open)="onOpen(menuButton)"
[buttonAttributes]="{title: messageFor('sheetsMenu')}"
[attr.aria-controls]="tablistId">
</kendo-dropdownbutton>
<kendo-tabstrip #tabstrip
[tabPosition]="'bottom'"
[showContentArea]="false"
[scrollable]="{ scrollButtonsPosition: 'end', mouseScroll: false }"
class="k-spreadsheet-sheets"
(tabSelect)="onTabSelect($event)">
<ng-container *ngFor="let sheet of sheets">
<kendo-tabstrip-tab
*ngIf="sheet.state === 'visible'"
[title]="sheet.text"
[selected]="sheet.text === activeSheet">
<ng-template kendoTabTemplate>
<span class="k-link">
<span class="k-link-text">{{sheet.text}}</span>
</span>
<span class="k-item-actions">
<kendo-dropdownbutton #sheetDdb
fillMode="flat"
icon="caret-alt-down"
[svgIcon]="caretAltDownIcon"
buttonClass="k-menu-button"
[data]="sheet.sheetActions"
[buttonAttributes]="{'aria-hidden': 'true', 'tabindex': '-1', role: 'presentation'}"
(open)="onOpen(sheetDdb)"
(close)="onClose()"
(click)="$event.stopPropagation()"
(itemClick)="onActionClick($event, sheet)">
</kendo-dropdownbutton>
</span>
</ng-template>
</kendo-tabstrip-tab>
</ng-container>
</kendo-tabstrip>
`, isInline: true, dependencies: [{ kind: "component", type: ButtonComponent, selector: "button[kendoButton]", inputs: ["arrowIcon", "toggleable", "togglable", "selected", "tabIndex", "imageUrl", "iconClass", "icon", "disabled", "size", "rounded", "fillMode", "themeColor", "svgIcon", "primary", "look"], outputs: ["selectedChange", "click"], exportAs: ["kendoButton"] }, { kind: "directive", type: EventsOutsideAngularDirective, selector: "[kendoEventsOutsideAngular]", inputs: ["kendoEventsOutsideAngular", "scope"] }, { kind: "component", type: DropDownButtonComponent, selector: "kendo-dropdownbutton", inputs: ["arrowIcon", "icon", "svgIcon", "iconClass", "imageUrl", "textField", "data", "size", "rounded", "fillMode", "themeColor", "buttonAttributes"], outputs: ["itemClick", "focus", "blur"], exportAs: ["kendoDropDownButton"] }, { kind: "component", type: TabStripComponent, selector: "kendo-tabstrip", inputs: ["height", "animate", "tabAlignment", "tabPosition", "keepTabContent", "closable", "scrollable", "size", "closeIcon", "closeIconClass", "closeSVGIcon", "showContentArea"], outputs: ["tabSelect", "tabClose", "tabScroll"], exportAs: ["kendoTabStrip"] }, { kind: "directive", type: NgFor, selector: "[ngFor][ngForOf]", inputs: ["ngForOf", "ngForTrackBy", "ngForTemplate"] }, { kind: "directive", type: NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "component", type: TabStripTabComponent, selector: "kendo-tabstrip-tab", inputs: ["title", "disabled", "cssClass", "cssStyle", "selected", "closable", "closeIcon", "closeIconClass", "closeSVGIcon"], exportAs: ["kendoTabStripTab"] }, { kind: "directive", type: TabTemplateDirective, selector: "[kendoTabTemplate]" }] });
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "16.2.12", ngImport: i0, type: SheetsBarComponent, decorators: [{
type: Component,
args: [{
selector: '[kendoSpreadsheetSheetsBar]',
template: `
<button kendoButton #addButton
[title]="messageFor('addSheet')"
type="button"
fillMode="flat"
class="k-spreadsheet-sheet-add"
icon="plus"
[svgIcon]="plusIcon"
[kendoEventsOutsideAngular]="{click: onAddClick}"
[attr.aria-controls]="tablistId">
</button>
<kendo-dropdownbutton #menuButton
fillMode="flat"
buttonClass="k-spreadsheet-sheets-menu"
icon="menu"
[svgIcon]="menuIcon"
[data]="sheetsMenuList"
(itemClick)="onMenuItemClick($event)"
(open)="onOpen(menuButton)"
[buttonAttributes]="{title: messageFor('sheetsMenu')}"
[attr.aria-controls]="tablistId">
</kendo-dropdownbutton>
<kendo-tabstrip #tabstrip
[tabPosition]="'bottom'"
[showContentArea]="false"
[scrollable]="{ scrollButtonsPosition: 'end', mouseScroll: false }"
class="k-spreadsheet-sheets"
(tabSelect)="onTabSelect($event)">
<ng-container *ngFor="let sheet of sheets">
<kendo-tabstrip-tab
*ngIf="sheet.state === 'visible'"
[title]="sheet.text"
[selected]="sheet.text === activeSheet">
<ng-template kendoTabTemplate>
<span class="k-link">
<span class="k-link-text">{{sheet.text}}</span>
</span>
<span class="k-item-actions">
<kendo-dropdownbutton #sheetDdb
fillMode="flat"
icon="caret-alt-down"
[svgIcon]="caretAltDownIcon"
buttonClass="k-menu-button"
[data]="sheet.sheetActions"
[buttonAttributes]="{'aria-hidden': 'true', 'tabindex': '-1', role: 'presentation'}"
(open)="onOpen(sheetDdb)"
(close)="onClose()"
(click)="$event.stopPropagation()"
(itemClick)="onActionClick($event, sheet)">
</kendo-dropdownbutton>
</span>
</ng-template>
</kendo-tabstrip-tab>
</ng-container>
</kendo-tabstrip>
`,
standalone: true,
imports: [ButtonComponent, EventsOutsideAngularDirective, DropDownButtonComponent, TabStripComponent, NgFor, NgIf, TabStripTabComponent, TabTemplateDirective]
}]
}], ctorParameters: function () { return [{ type: i1.SpreadsheetService }, { type: i0.ElementRef }, { type: i0.Renderer2 }, { type: i2.SpreadsheetLocalizationService }, { type: i3.DialogService }, { type: i0.NgZone }]; }, propDecorators: { hostClasses: [{
type: HostBinding,
args: ['class.k-spreadsheet-sheets-bar']
}], sheets: [{
type: Input
}], sheetDescriptors: [{
type: Input
}], addButton: [{
type: ViewChild,
args: ['addButton']
}], menuButton: [{
type: ViewChild,
args: ['menuButton']
}], tabstrip: [{
type: ViewChild,
args: ['tabstrip']
}], actionDdbs: [{
type: ViewChildren,
args: ['sheetDdb']
}], actionDdbRefs: [{
type: ViewChildren,
args: ['sheetDdb', { read: ElementRef }]
}] } });