Border Radius

Use border radius to create rounded worksheet ranges. You can apply radius to the outer edge of a range, to every cell in a range, or through range and style APIs.

SpreadJS supports rounded worksheet ranges with Range.setBorderRadius, Range.borderRadius, and Style.borderRadius. Border radius can be used to create card-like cell areas, tab headers, status steps, and other worksheet UI elements with rounded corners. Border radius can be applied to a range in different ways: Outer applies the radius only to the outside corners of the target range. This is useful when multiple cells should look like one rounded block. All applies the radius to every cell in the target range. This is useful when each cell should keep its own rounded shape. Passing undefined clears the border radius from the target range. The borderRadius value can be a string with one to four space-separated tokens. The tokens represent the corners in this order: top-left, top-right, bottom-right, and bottom-left. One token applies the same radius to all four corners: "14" means 14 14 14 14. Two tokens apply the first value to top-left and bottom-right, and the second value to top-right and bottom-left: "14 8" means 14 8 14 8. Three tokens apply the first value to top-left, the second value to top-right and bottom-left, and the third value to bottom-right: "14 8 4" means 14 8 4 8. Four tokens set each corner separately in top-left, top-right, bottom-right, bottom-left order: "14 8 4 0" means top-left is 14, top-right is 8, bottom-right is 4, and bottom-left is 0. You can also set border radius through a style when you want reusable cell formatting: Border radius clips the rendered cell shape, including the cell background and diagonal borders.
import { Component, NgModule, enableProdMode } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { SpreadSheetsModule } from '@mescius/spread-sheets-angular'; import GC from '@mescius/spread-sheets'; import './styles.css'; type Worksheet = GC.Spread.Sheets.Worksheet; type Range = GC.Spread.Sheets.CellRange; type Style = GC.Spread.Sheets.Style; type LineBorder = GC.Spread.Sheets.LineBorder; interface TableHeader { text: string; width: number; } interface NormalizedSelection { row: number; col: number; rowCount: number; colCount: number; } interface BorderRadiusOptions { all?: boolean; outer?: boolean; } @Component({ selector: 'app-component', templateUrl: 'src/app.component.html' }) export class AppComponent { spread: GC.Spread.Sheets.Workbook; sheet: Worksheet; showcaseSheet: Worksheet; hostStyle = { flex: '1 1 auto', minWidth: '0', width: 'auto', height: '100%' }; backgroundColor = '#f1f6ea'; backgroundNoFill = false; borderStyle = 'medium'; borderColor = '#6f8f45'; borderColorDisabled = false; radiusOption = 'outer'; topLeft = 14; topRight = 14; bottomRight = 14; bottomLeft = 14; initSpread($event: any): void { this.spread = $event.spread; this.spread.options.newTabVisible = false; this.spread.setSheetCount(2); this.sheet = this.spread.getSheet(0); this.showcaseSheet = this.spread.getSheet(1); this.spread.suspendPaint(); try { this.setupSheet(); this.buildSamples(); this.setupShowcaseSheet(this.showcaseSheet); this.buildShowcase(this.showcaseSheet); this.sheet.setSelection(3, 2, 7, 3); this.spread.setActiveSheetIndex(0); this.updatePanelFromSelection(); this.updatePanelVisibility(); } finally { this.spread.resumePaint(); } this.sheet.bind(GC.Spread.Sheets.Events.SelectionChanged, () => { this.updatePanelFromSelection(); }); this.spread.bind(GC.Spread.Sheets.Events.ActiveSheetChanged, () => { this.updatePanelVisibility(); }); this.updateBorderColorState(); } setupSheet(): void { this.sheet.name('Border Radius'); this.sheet.defaults.rowHeight = 24; this.sheet.setRowCount(28); this.sheet.setColumnCount(16); this.sheet.setRowHeight(1, 36); for (let col = 1; col <= 14; col++) { this.sheet.setColumnWidth(col, 48); } } buildSamples(): void { this.addTitle(); this.addCardSample(); this.addTabsSample(); this.addStatusStepsSample(); } addTitle(): void { this.sheet.getCell(1, 1) .value('Select a range, then adjust its style in the panel.') .font('13px Calibri') .foreColor('#64748b') .vAlign(GC.Spread.Sheets.VerticalAlign.center); } addCardSample(): void { this.addSectionLabel(3, 'Card'); this.addSpanCard(3, 2); this.addRangeCard(3, 6); this.addBackgroundOnlyCard(3, 10); } addSpanCard(row: number, col: number): void { const range = this.sheet.getRange(row, col, 7, 3); range.backColor('#f1f6ea'); range.setBorder(new GC.Spread.Sheets.LineBorder('#6f8f45', GC.Spread.Sheets.LineStyle.medium), { outline: true }); this.setRangeBorderRadius(range, '14', { outer: true }); this.centerBlock(row, col, 7, 3, 'Outer Radius'); range.foreColor('#2f3a25'); } addRangeCard(row: number, col: number): void { const range = this.sheet.getRange(row, col, 7, 3); range.backColor('#f3eff8'); range.setBorder(new GC.Spread.Sheets.LineBorder('#9a86b8', GC.Spread.Sheets.LineStyle.medium), { outline: true }); this.setRangeBorderRadius(range, '14', { all: true }); range.hAlign(GC.Spread.Sheets.HorizontalAlign.center); range.vAlign(GC.Spread.Sheets.VerticalAlign.center); range.font('600 12px Calibri'); range.foreColor('#4b3f5c'); this.sheet.getCell(row + 3, col + 1).value('All Cell Radius'); } addBackgroundOnlyCard(row: number, col: number): void { const range = this.sheet.getRange(row, col, 7, 3); range.backColor('#DCEFEF'); this.setRangeBorderRadius(range, '20', { outer: true }); this.centerBlock(row, col, 7, 3, 'Fill Only'); range.foreColor('#46514a'); } addTabsSample(): void { this.addSectionLabel(12, 'Tabs'); this.addTab(12, 2, 'Overview', false); this.addTab(12, 4, 'Details', false); this.addTab(12, 6, 'Data', true); this.addTab(12, 8, 'Settings', false); this.addTab(12, 10, 'History', false); } addTab(row: number, col: number, text: string, active: boolean): void { const range = this.sheet.getRange(row, col, 1, 2); range.backColor(active ? '#F6ECE8' : '#FCFAF9'); range.setBorder(new GC.Spread.Sheets.LineBorder('#E7DAD6', GC.Spread.Sheets.LineStyle.medium), { outline: true }); this.setRangeBorderRadius(range, '0 10 0 0', { all: true }); this.centerBlock(row, col, 1, 2, text); range.foreColor(active ? '#6B4E47' : '#8A756E'); if (active) { range.borderBottom(new GC.Spread.Sheets.LineBorder('#B67868', GC.Spread.Sheets.LineStyle.medium)); } } addStatusStepsSample(): void { this.addSectionLabel(15, 'Steps'); this.addStatusStep(15, 2, '1 Plan', '#EEF3F8', '#8CA3BE', '#3E5168', '14'); this.addStatusStep(15, 6, '2 Build', '#f8f3e8', '#b89b5e', '#5c4a22', '14'); this.addStatusStep(15, 10, '3 Ship', '#eef1ee', '#8a958d', '#3f4942', '14'); } addStatusStep(row: number, col: number, text: string, backColor: string, borderColor: string, foreColor: string, radius: string): void { const range = this.sheet.getRange(row, col, 2, 3); range.backColor(backColor); range.setBorder(new GC.Spread.Sheets.LineBorder(borderColor, GC.Spread.Sheets.LineStyle.medium), { outline: true }); this.setRangeBorderRadius(range, radius, { outer: true }); this.centerBlock(row, col, 2, 3, text); range.foreColor(foreColor); } centerBlock(row: number, col: number, rowCount: number, colCount: number, text: string): void { const range = this.sheet.getRange(row, col, rowCount, colCount); range.hAlign(GC.Spread.Sheets.HorizontalAlign.center); range.vAlign(GC.Spread.Sheets.VerticalAlign.center); range.font('600 12px Calibri'); this.sheet.getCell(row, col).value(text); if (rowCount > 1 || colCount > 1) { this.sheet.addSpan(row, col, rowCount, colCount); } } addSectionLabel(row: number, text: string): void { this.sheet.getCell(row, 1) .value(text) .font('600 12px Calibri') .foreColor('#64748b') .hAlign(GC.Spread.Sheets.HorizontalAlign.left) .vAlign(GC.Spread.Sheets.VerticalAlign.center); } setupShowcaseSheet(targetSheet: Worksheet): void { targetSheet.name('CRM Layout'); targetSheet.options.gridline = { showVerticalGridline: false, showHorizontalGridline: false }; targetSheet.options.rowHeaderVisible = false; targetSheet.options.colHeaderVisible = false; targetSheet.defaults.rowHeight = 20; targetSheet.setRowCount(34); targetSheet.setColumnCount(39); for (let col = 0; col < 39; col++) { targetSheet.setColumnWidth(col, 34); } for (let row = 0; row < 34; row++) { targetSheet.setRowHeight(row, 20); } } buildShowcase(targetSheet: Worksheet): void { targetSheet.suspendPaint(); try { targetSheet.getRange(0, 0, 34, 39).backColor('#ffffff'); this.addInfoPanel(targetSheet, 1, 1, 9, 10, [ ['Company no', '20'], ['Company name', 'Land of Toys Inc.'], ['Type', ''], ['Phone', '2125557818'], ['Fax', ''], ['Email', 'kwai@landoftoysinc.com'], ['Web', 'www.landoftoysinc.com'], ['Our rep', 'Joe Bloggs'], ['Inactive', ''] ]); this.addInfoPanel(targetSheet, 1, 12, 9, 10, [ ['Address', '897 Long Airport Avenue'], ['', ''], ['', ''], ['Town / City', 'NYC'], ['County / State', 'NY'], ['Zip / Postcode', '10022'], ['Country', 'USA'], ['Area', ''], ['Identity', 'Classic Models'] ]); this.addNotesPanel(targetSheet, 1, 23, 9, 15); this.addTabStrip(targetSheet, 11, 1); this.addEnquiriesTable(targetSheet, 13, 1, 15, 37); targetSheet.setSelection(30, 1, 1, 1); } finally { targetSheet.resumePaint(); } } addInfoPanel(targetSheet: Worksheet, row: number, col: number, rowCount: number, colCount: number, fields: string[][]): void { const labelCols = Math.floor(colCount * 0.36); const valueCols = colCount - labelCols; const panelRange = targetSheet.getRange(row, col, rowCount, colCount); panelRange.backColor('#eff5fb'); for (let i = 0; i < fields.length; i++) { const currentRow = row + i; targetSheet.getRange(currentRow, col, 1, labelCols) .backColor('#d8e3ee') .foreColor('#315b83') .font('600 12px Calibri') .hAlign(GC.Spread.Sheets.HorizontalAlign.right) .vAlign(GC.Spread.Sheets.VerticalAlign.center) .cellPadding('0 5'); targetSheet.getCell(currentRow, col + labelCols - 1).value(fields[i][0]); targetSheet.getRange(currentRow, col + labelCols, 1, valueCols) .backColor('#ffffff') .foreColor('#1f2937') .font('600 12px Calibri') .vAlign(GC.Spread.Sheets.VerticalAlign.center) .cellPadding('0 5'); targetSheet.getCell(currentRow, col + labelCols).value(fields[i][1]); targetSheet.getCell(currentRow, col + labelCols - 1).borderRight(new GC.Spread.Sheets.LineBorder('#dbe4ee', GC.Spread.Sheets.LineStyle.thin)); if (i < fields.length - 1) { this.setRowBottomBorder(targetSheet, currentRow, col, colCount, new GC.Spread.Sheets.LineBorder('#dbe4ee', GC.Spread.Sheets.LineStyle.thin)); } } this.applyRoundedOutline(panelRange, '12', '#9fbddd', targetSheet); } addNotesPanel(targetSheet: Worksheet, row: number, col: number, rowCount: number, colCount: number): void { const panelRange = targetSheet.getRange(row, col, rowCount, colCount); panelRange.backColor('#edf4fb'); const headerRange = targetSheet.getRange(row, col, 1, colCount); headerRange.backColor('#84a9ca').foreColor('#ffffff').font('600 12px Calibri').cellPadding('0 5'); targetSheet.addSpan(row, col, 1, colCount); targetSheet.getCell(row, col).value('Communications / notes'); const headers = ['Type', 'Date', 'From', 'To', 'Details']; const widths = [2, 2, 5, 4, 2]; let offset = 0; for (let i = 0; i < headers.length; i++) { targetSheet.addSpan(row + 1, col + offset, 1, widths[i]); targetSheet.getRange(row + 1, col + offset, 1, widths[i]) .backColor('#f5f7fa') .foreColor('#4f6f91') .font('600 12px Calibri') .hAlign(GC.Spread.Sheets.HorizontalAlign.left) .vAlign(GC.Spread.Sheets.VerticalAlign.center) .cellPadding('0 5'); targetSheet.getCell(row + 1, col + offset).value(headers[i]); offset += widths[i]; } this.setSegmentRowInteriorBorders(targetSheet, row + 1, col, widths, new GC.Spread.Sheets.LineBorder('#bed0e3', GC.Spread.Sheets.LineStyle.thin)); const records = [ ['Call', '26/05', 'Kwai Lee', 'Joe Bloggs', 'Quote'], ['Email', '27/05', 'Joe Bloggs', 'Kwai Lee', 'Info'], ['Note', '28/05', 'Kwai Lee', 'Support', 'Specs'], ['Call', '30/05', 'Joe Bloggs', 'Kwai Lee', 'Follow'] ]; for (let dataRow = row + 2; dataRow < row + rowCount - 1; dataRow++) { const backColor = dataRow % 2 === 0 ? '#f4f8fc' : '#ffffff'; targetSheet.getRange(dataRow, col, 1, colCount) .backColor(backColor) .foreColor('#24364a') .font('600 11px Calibri') .vAlign(GC.Spread.Sheets.VerticalAlign.center) .cellPadding('0 5') .setBorder(new GC.Spread.Sheets.LineBorder('#c2d5e8', GC.Spread.Sheets.LineStyle.thin), { bottom: true }); this.addNotesRecord(targetSheet, dataRow, col, widths, records[dataRow - row - 2] || ['', '', '', '', '']); } const footerRange = targetSheet.getRange(row + rowCount - 1, col, 1, colCount); footerRange.backColor('#84a9ca').foreColor('#ffffff').font('600 12px Calibri'); targetSheet.addSpan(row + rowCount - 1, col, 1, colCount); targetSheet.getCell(row + rowCount - 1, col).value(''); this.applyRoundedOutline(panelRange, '12', '#9fbddd', targetSheet); } addNotesRecord(targetSheet: Worksheet, row: number, col: number, widths: number[], values: string[]): void { let currentCol = col; for (let i = 0; i < widths.length; i++) { targetSheet.addSpan(row, currentCol, 1, widths[i]); targetSheet.getCell(row, currentCol).value(values[i]); currentCol += widths[i]; } } addTabStrip(targetSheet: Worksheet, row: number, col: number): void { const tabs = ['Contacts', 'Addresses', 'Enquiries', 'Jobs', 'Tasks', 'Products', 'Quotes', 'Orders', 'Shipping', 'Account', 'Documents']; const widths = [3, 3, 4, 3, 3, 4, 3, 3, 4, 4, 3]; let startCol = col; const stripRange = targetSheet.getRange(row, col, 1, 37); stripRange.backColor('#f7fbff').setBorder(new GC.Spread.Sheets.LineBorder('#bdd2e6', GC.Spread.Sheets.LineStyle.medium), { outline: true }); this.setRangeBorderRadius(stripRange, '8', { outer: true }, targetSheet); for (let i = 0; i < tabs.length; i++) { const active = tabs[i] === 'Enquiries'; targetSheet.addSpan(row, startCol, 1, widths[i]); const tabRange = targetSheet.getRange(row, startCol, 1, widths[i]); tabRange.backColor(active ? '#84a9ca' : '#f7fbff') .foreColor(active ? '#ffffff' : '#4f6f91') .font('600 12px Calibri') .hAlign(GC.Spread.Sheets.HorizontalAlign.center) .vAlign(GC.Spread.Sheets.VerticalAlign.center) .setBorder(new GC.Spread.Sheets.LineBorder('#bdd2e6', GC.Spread.Sheets.LineStyle.thin), { outline: true }); this.setRangeBorderRadius(tabRange, '4', { outer: true }, targetSheet); targetSheet.getCell(row, startCol).value(tabs[i]); startCol += widths[i]; } } addEnquiriesTable(targetSheet: Worksheet, row: number, col: number, rowCount: number, colCount: number): void { const panelRange = targetSheet.getRange(row, col, rowCount, colCount); panelRange.backColor('#f8fbfe'); const titleRange = targetSheet.getRange(row, col, 1, colCount); titleRange.backColor('#84a9ca').foreColor('#ffffff').font('600 12px Calibri'); this.setRangeBorderRadius(titleRange, '10 10 0 0', { outer: true }, targetSheet); targetSheet.addSpan(row, col, 1, colCount); targetSheet.getCell(row, col).value('Enquiries from this company').cellPadding('0 5'); const headers: TableHeader[] = [ { text: '', width: 1 }, { text: 'Ref no', width: 2 }, { text: 'Date', width: 2 }, { text: 'Status', width: 2 }, { text: 'Contact', width: 7 }, { text: 'Our rep', width: 6 }, { text: 'Referred by', width: 7 }, { text: 'Enquiry value', width: 4 }, { text: 'Requirements', width: 6 } ]; let currentCol = col; for (let i = 0; i < headers.length; i++) { targetSheet.addSpan(row + 1, currentCol, 1, headers[i].width); targetSheet.getRange(row + 1, currentCol, 1, headers[i].width) .backColor('#f5f7fa') .foreColor('#4f6f91') .font('600 12px Calibri') .hAlign(GC.Spread.Sheets.HorizontalAlign.left) .vAlign(GC.Spread.Sheets.VerticalAlign.center) .cellPadding('0 5'); targetSheet.getCell(row + 1, currentCol).value(headers[i].text); currentCol += headers[i].width; } this.setHeaderInteriorBorders(targetSheet, row + 1, col, headers, new GC.Spread.Sheets.LineBorder('#bed0e3', GC.Spread.Sheets.LineStyle.thin)); const records = [ ['>', '1', '26/05/16', 'Hot', 'Kwai Lee', 'Joe Bloggs', 'Google Adwords', '250.00', '1929 Model T Ford'], ['>', '3', '26/05/16', 'Hot', 'Kwai Lee', 'Joe Bloggs', 'Customer', '2,300.00', 'Rolls Royce Flying Spur (1: 15 scale)'], ['>', '4', '26/05/16', 'Cold', 'Kwai Lee', 'Joe Bloggs', 'Google Adwords', '195.00', 'Chevy Nova 1972 (plinth mounted)'], ['>', '2', '26/05/16', 'Deal', 'Kwai Lee', 'Joe Bloggs', 'Web site', '495.00', '1968 Camaro with softtop (rare)'] ]; for (let recordIndex = 0; recordIndex < records.length; recordIndex++) { this.addTableRecord(targetSheet, row + 2 + recordIndex, col, headers, records[recordIndex]); } for (let dataRow = row + 2; dataRow < row + rowCount - 1; dataRow++) { targetSheet.getRange(dataRow, col, 1, colCount) .backColor(dataRow % 2 === 0 ? '#eef5fc' : '#ffffff') .foreColor('#24364a') .font('600 11px Calibri') .vAlign(GC.Spread.Sheets.VerticalAlign.center) .cellPadding('0 5') .setBorder(new GC.Spread.Sheets.LineBorder('#c2d5e8', GC.Spread.Sheets.LineStyle.thin), { bottom: true }); } const footerRange = targetSheet.getRange(row + rowCount - 1, col, 1, colCount); footerRange.backColor('#84a9ca').foreColor('#ffffff').font('600 12px Calibri'); this.setRangeBorderRadius(footerRange, '0 0 10 10', { outer: true }, targetSheet); targetSheet.addSpan(row + rowCount - 1, col, 1, colCount); targetSheet.getCell(row + rowCount - 1, col).value(''); this.applyRoundedOutline(panelRange, '10', '#9fbddd', targetSheet); } addTableRecord(targetSheet: Worksheet, row: number, col: number, headers: TableHeader[], values: string[]): void { let currentCol = col; for (let i = 0; i < headers.length; i++) { targetSheet.addSpan(row, currentCol, 1, headers[i].width); targetSheet.getCell(row, currentCol).value(values[i]); currentCol += headers[i].width; } } applyRoundedOutline(range: Range, radius: string, borderColor: string, targetSheet: Worksheet): void { range.setBorder(new GC.Spread.Sheets.LineBorder(borderColor, GC.Spread.Sheets.LineStyle.medium), { outline: true }); this.setRangeBorderRadius(range, radius, { outer: true }, targetSheet); } setSegmentRowInteriorBorders(targetSheet: Worksheet, row: number, col: number, widths: number[], border: LineBorder): void { this.setRowBottomBorder(targetSheet, row, col, this.sumWidths(widths), border); this.setInternalVerticalBorders(targetSheet, row, col, widths, border); } setHeaderInteriorBorders(targetSheet: Worksheet, row: number, col: number, headers: TableHeader[], border: LineBorder): void { const widths = headers.map((header: TableHeader) => header.width); this.setSegmentRowInteriorBorders(targetSheet, row, col, widths, border); } setRowBottomBorder(targetSheet: Worksheet, row: number, col: number, colCount: number, border: LineBorder): void { targetSheet.getRange(row, col, 1, colCount).setBorder(border, { bottom: true }); } setInternalVerticalBorders(targetSheet: Worksheet, row: number, col: number, widths: number[], border: LineBorder): void { let currentCol = col; for (let i = 0; i < widths.length - 1; i++) { currentCol += widths[i]; targetSheet.getCell(row, currentCol - 1).borderRight(border); } } sumWidths(widths: number[]): number { let total = 0; for (let i = 0; i < widths.length; i++) { total += widths[i]; } return total; } updatePanelVisibility(): void { const isShowcaseSheet = this.spread.getActiveSheetIndex() === 1; const optionsContainer = document.getElementById('optionsContainer'); if (optionsContainer) { optionsContainer.style.display = isShowcaseSheet ? 'none' : 'block'; } this.refreshSpreadLayout(); if (!isShowcaseSheet) { this.updatePanelFromSelection(); } } refreshSpreadLayout(): void { this.spread.refresh(); setTimeout(() => { this.spread.refresh(); }, 0); } onBackgroundColorChange(): void { this.backgroundNoFill = false; this.applyBackgroundColorSettings(); } onBorderStyleChange(): void { this.updateBorderColorState(); this.applyBorderStyleSettings(); } updateBorderColorState(): void { this.borderColorDisabled = this.borderStyle === 'none'; } updatePanelFromSelection(): void { const selection = this.getActiveSelection(); const style = this.getSelectionStartStyle(selection); const border = this.getSelectionOutlineBorder(selection); const backgroundColor = this.normalizeHexColor(style && style.backColor); if (backgroundColor) { this.backgroundNoFill = false; this.backgroundColor = backgroundColor; } else { this.backgroundNoFill = true; } if (border) { this.borderStyle = this.getLineStyleName(border.style); if (this.normalizeHexColor(border.color)) { this.borderColor = this.normalizeHexColor(border.color); } } else { this.borderStyle = 'none'; } this.updateBorderColorState(); } getSelectionStartStyle(selection: NormalizedSelection): Style { return this.sheet.getActualStyle(selection.row, selection.col, GC.Spread.Sheets.SheetArea.viewport) || this.sheet.getStyle(selection.row, selection.col, GC.Spread.Sheets.SheetArea.viewport); } getSelectionOutlineBorder(selection: NormalizedSelection): LineBorder { const lastRow = selection.row + selection.rowCount - 1; const lastCol = selection.col + selection.colCount - 1; const borderNames = ['borderTop', 'borderRight', 'borderBottom', 'borderLeft']; const samples = [ [selection.row, selection.col], [selection.row, lastCol], [lastRow, lastCol], [lastRow, selection.col] ]; for (let index = 0; index < samples.length; index++) { const style = this.sheet.getStyle(samples[index][0], samples[index][1], GC.Spread.Sheets.SheetArea.viewport); if (!style) { continue; } for (let borderIndex = 0; borderIndex < borderNames.length; borderIndex++) { const border = (style as any)[borderNames[borderIndex]]; if (border) { return border; } } } return null; } getLineStyleName(styleValue: any): string { const lineStyle = GC.Spread.Sheets.LineStyle; if (styleValue === undefined || styleValue === null || styleValue === lineStyle.none || styleValue === lineStyle.empty) { return 'none'; } for (const name in lineStyle) { if (lineStyle.hasOwnProperty(name) && lineStyle[name] === styleValue) { return name; } } return 'none'; } normalizeHexColor(color: string): string { if (!color) { return ''; } const hex = String(color).toLowerCase(); if (/^#[0-9a-f]{6}$/.test(hex)) { return hex; } if (/^#[0-9a-f]{3}$/.test(hex)) { return '#' + hex.charAt(1) + hex.charAt(1) + hex.charAt(2) + hex.charAt(2) + hex.charAt(3) + hex.charAt(3); } return ''; } applyBackgroundColorSettings(): void { const selection = this.getActiveSelection(); const range = this.sheet.getRange(selection.row, selection.col, selection.rowCount, selection.colCount); this.sheet.suspendPaint(); try { if (this.backgroundNoFill) { this.clearRangeBackColor(range); } else { range.backColor(this.backgroundColor); } this.sheet.setSelection(selection.row, selection.col, selection.rowCount, selection.colCount); } finally { this.sheet.resumePaint(); } this.spread.focus(); } clearRangeBackColor(range: Range): void { this.forEachCell(range, (row: number, col: number) => { const style = this.sheet.getStyle(row, col, GC.Spread.Sheets.SheetArea.viewport) || new GC.Spread.Sheets.Style(); delete (style as any).backColor; this.sheet.setStyle(row, col, style, GC.Spread.Sheets.SheetArea.viewport); }); } applyBorderStyleSettings(): void { const selection = this.getActiveSelection(); const range = this.sheet.getRange(selection.row, selection.col, selection.rowCount, selection.colCount); this.sheet.suspendPaint(); try { this.applyBorderStyle(range); this.sheet.setSelection(selection.row, selection.col, selection.rowCount, selection.colCount); } finally { this.sheet.resumePaint(); } this.spread.focus(); } applyBorderColorSettings(): void { const selection = this.getActiveSelection(); const range = this.sheet.getRange(selection.row, selection.col, selection.rowCount, selection.colCount); this.sheet.suspendPaint(); try { this.applyBorderColor(range); this.sheet.setSelection(selection.row, selection.col, selection.rowCount, selection.colCount); } finally { this.sheet.resumePaint(); } this.spread.focus(); } applyRadiusSettings(): void { const selection = this.getActiveSelection(); const range = this.sheet.getRange(selection.row, selection.col, selection.rowCount, selection.colCount); this.sheet.suspendPaint(); try { this.applyRadius(range); this.sheet.setSelection(selection.row, selection.col, selection.rowCount, selection.colCount); } finally { this.sheet.resumePaint(); } this.spread.focus(); } applyBorderStyle(range: Range): void { const lineStyle = GC.Spread.Sheets.LineStyle[this.borderStyle]; this.updateOutlineBorder(range, (border: LineBorder) => { return new GC.Spread.Sheets.LineBorder(this.getBorderColor(border), lineStyle); }); } applyBorderColor(range: Range): void { const borderColor = this.borderColor; this.updateOutlineBorder(range, (border: LineBorder) => { return new GC.Spread.Sheets.LineBorder(borderColor, this.getBorderStyle(border)); }); } updateOutlineBorder(range: Range, updateBorder: (border: LineBorder) => LineBorder): void { const lastRow = range.row + range.rowCount - 1; const lastCol = range.col + range.colCount - 1; for (let col = range.col; col <= lastCol; col++) { this.updateCellBorder(range.row, col, 'borderTop', updateBorder); this.updateCellBorder(lastRow, col, 'borderBottom', updateBorder); } for (let row = range.row; row <= lastRow; row++) { this.updateCellBorder(row, range.col, 'borderLeft', updateBorder); this.updateCellBorder(row, lastCol, 'borderRight', updateBorder); } } updateCellBorder(row: number, col: number, borderName: string, updateBorder: (border: LineBorder) => LineBorder): void { const style = this.sheet.getStyle(row, col, GC.Spread.Sheets.SheetArea.viewport) || new GC.Spread.Sheets.Style(); (style as any)[borderName] = updateBorder((style as any)[borderName]); this.sheet.setStyle(row, col, style, GC.Spread.Sheets.SheetArea.viewport); } getBorderColor(border: LineBorder): string { return border && border.color ? border.color : this.borderColor; } getBorderStyle(border: LineBorder): any { return border && border.style !== undefined ? border.style : GC.Spread.Sheets.LineStyle[this.borderStyle]; } applyRadius(range: Range): void { const option = this.getSelectedRadiusOption(); this.setRangeBorderRadius(range, undefined, { all: true }); if (option === 'none') { return; } const setting = {}; setting[option] = true; this.setRangeBorderRadius(range, this.getRadiusValue(), setting); } getActiveSelection(): NormalizedSelection { const selection = this.sheet.getSelections()[0]; const row = selection.row < 0 ? 0 : selection.row; const col = selection.col < 0 ? 0 : selection.col; const rowCount = selection.row < 0 ? this.sheet.getRowCount() : selection.rowCount; const colCount = selection.col < 0 ? this.sheet.getColumnCount() : selection.colCount; return { row: row, col: col, rowCount: rowCount, colCount: colCount }; } setRangeBorderRadius(range: Range, value: string, options: BorderRadiusOptions, targetSheet?: Worksheet): void { targetSheet = targetSheet || this.sheet; const rangeAny = range as any; if (typeof rangeAny.setBorderRadius === 'function') { rangeAny.setBorderRadius(value, options); return; } if (options && options.outer) { this.setOuterBorderRadius(range, value, targetSheet); return; } this.setAllBorderRadius(range, value, targetSheet); } setAllBorderRadius(range: Range, value: string, targetSheet?: Worksheet): void { targetSheet = targetSheet || this.sheet; const rangeAny = range as any; if (typeof rangeAny.borderRadius === 'function') { rangeAny.borderRadius(value); return; } this.forEachCell(range, (row: number, col: number) => { this.setCellBorderRadius(row, col, value, targetSheet); }); } setOuterBorderRadius(range: Range, value: string, targetSheet?: Worksheet): void { targetSheet = targetSheet || this.sheet; this.setAllBorderRadius(range, undefined, targetSheet); if (value === undefined || value === null || value === '') { return; } const radii = this.parseRadiusValue(value); const lastRow = range.row + range.rowCount - 1; const lastCol = range.col + range.colCount - 1; const cells = {}; this.addOuterCorner(cells, range.row, range.col, 0, radii[0]); this.addOuterCorner(cells, range.row, lastCol, 1, radii[1]); this.addOuterCorner(cells, lastRow, lastCol, 2, radii[2]); this.addOuterCorner(cells, lastRow, range.col, 3, radii[3]); for (const key in cells) { if (cells.hasOwnProperty(key)) { const parts = key.split(':'); this.setCellBorderRadius(parseInt(parts[0], 10), parseInt(parts[1], 10), cells[key].join(' '), targetSheet); } } } addOuterCorner(cells: any, row: number, col: number, cornerIndex: number, radius: string): void { const key = row + ':' + col; if (!cells[key]) { cells[key] = ['0', '0', '0', '0']; } cells[key][cornerIndex] = String(radius); } parseRadiusValue(value: string): string[] { let parts = String(value).split(/\s+/).filter((item: string) => { return item !== ''; }); if (parts.length === 0) { parts = ['0']; } if (parts.length === 1) { return [parts[0], parts[0], parts[0], parts[0]]; } if (parts.length === 2) { return [parts[0], parts[1], parts[0], parts[1]]; } if (parts.length === 3) { return [parts[0], parts[1], parts[2], parts[1]]; } return [parts[0], parts[1], parts[2], parts[3]]; } forEachCell(range: Range, callback: (row: number, col: number) => void): void { const rowEnd = range.row + range.rowCount; const colEnd = range.col + range.colCount; for (let row = range.row; row < rowEnd; row++) { for (let col = range.col; col < colEnd; col++) { callback(row, col); } } } setCellBorderRadius(row: number, col: number, value: string, targetSheet?: Worksheet): void { targetSheet = targetSheet || this.sheet; const style = targetSheet.getStyle(row, col, GC.Spread.Sheets.SheetArea.viewport) || new GC.Spread.Sheets.Style(); (style as any).borderRadius = value; targetSheet.setStyle(row, col, style, GC.Spread.Sheets.SheetArea.viewport); } getRadiusValue(): string { return [ this.normalizeCornerValue('topLeft'), this.normalizeCornerValue('topRight'), this.normalizeCornerValue('bottomRight'), this.normalizeCornerValue('bottomLeft') ].join(' '); } normalizeCornerValue(id: string): number { const value = parseInt(String(this[id]), 10); if (isNaN(value) || value < 0) { return 0; } return value; } getSelectedRadiusOption(): string { return this.radiusOption || 'outer'; } } @NgModule({ imports: [BrowserModule, SpreadSheetsModule, FormsModule], declarations: [AppComponent], exports: [AppComponent], bootstrap: [AppComponent] }) export class AppModule {} enableProdMode(); platformBrowserDynamic().bootstrapModule(AppModule);
<!doctype html> <html style="height:100%;font-size:14px;"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <link rel="stylesheet" type="text/css" href="$DEMOROOT$/en/angular/node_modules/@mescius/spread-sheets/styles/gc.spread.sheets.excel2013white.css"> <!-- Polyfills --> <script src="$DEMOROOT$/en/angular/node_modules/zone.js/fesm2015/zone.min.js"></script> <!-- SystemJS --> <script src="$DEMOROOT$/en/angular/node_modules/systemjs/dist/system.js"></script> <script src="systemjs.config.js"></script> <script> // workaround to load 'rxjs/operators' from the rxjs bundle System.import('rxjs').then(function (m) { System.import('@angular/compiler'); System.set(SystemJS.resolveSync('rxjs/operators'), System.newModule(m.operators)); System.import('$DEMOROOT$/en/lib/angular/license.ts'); System.import('./src/app.component'); }); </script> </head> <body> <app-component></app-component> </body> </html>
<div class="sample-tutorial"> <gc-spread-sheets class="sample-spreadsheets" [hostStyle]="hostStyle" (workbookInitialized)="initSpread($event)"></gc-spread-sheets> <div id="optionsContainer" class="options-container"> <div class="panel-section"> <div class="panel-section-title"> <span>Style</span> </div> <div class="panel-section-description">Synced with the selected area. Changes apply instantly.</div> <div class="panel-card"> <div class="form-group-inline back-color-field"> <label class="form-label" for="backgroundColor">Back Color</label> <div class="back-color-controls"> <label class="no-fill-option"><input type="checkbox" id="backgroundNoFill" [(ngModel)]="backgroundNoFill" (change)="applyBackgroundColorSettings()"> No Fill</label> <input type="color" id="backgroundColor" [(ngModel)]="backgroundColor" [disabled]="backgroundNoFill" (change)="onBackgroundColorChange()" aria-label="Back color"> </div> </div> <div class="form-group-inline border-field"> <label class="form-label" for="borderStyle">Border</label> <div class="border-controls"> <select id="borderStyle" class="form-select" [(ngModel)]="borderStyle" (change)="onBorderStyleChange()"> <option value="none">None</option> <option value="thin">Thin</option> <option value="medium">Medium</option> <option value="dashed">Dashed</option> <option value="dotted">Dotted</option> <option value="thick">Thick</option> <option value="double">Double</option> <option value="hair">Hair</option> <option value="mediumDashed">Medium Dashed</option> <option value="dashDot">Dash Dot</option> <option value="mediumDashDot">Medium Dash Dot</option> <option value="dashDotDot">Dash Dot Dot</option> <option value="mediumDashDotDot">Medium Dash Dot Dot</option> <option value="slantedDashDot">Slanted Dash Dot</option> </select> <input type="color" id="borderColor" [(ngModel)]="borderColor" [disabled]="borderColorDisabled" (change)="applyBorderColorSettings()" aria-label="Border color"> </div> </div> <div class="field-hint">Border is applied to the selected range outline by default.</div> </div> </div> <div class="panel-section"> <div class="panel-section-title"> <span>Radius</span> </div> <div class="panel-section-description">Set radius values, then click Apply Radius.</div> <div class="panel-card"> <div class="form-group"> <label class="form-label">Apply To</label> <div class="radio-group"> <label><input type="radio" name="radiusOption" value="outer" [(ngModel)]="radiusOption"> Outer</label> <label><input type="radio" name="radiusOption" value="all" [(ngModel)]="radiusOption"> All</label> <label><input type="radio" name="radiusOption" value="none" [(ngModel)]="radiusOption"> None</label> </div> </div> <div class="corner-grid"> <label class="form-label" for="topLeft">Top Left <input class="form-input" type="number" id="topLeft" min="0" max="40" [(ngModel)]="topLeft"></label> <label class="form-label" for="topRight">Top Right <input class="form-input" type="number" id="topRight" min="0" max="40" [(ngModel)]="topRight"></label> <label class="form-label" for="bottomLeft">Bottom Left <input class="form-input" type="number" id="bottomLeft" min="0" max="40" [(ngModel)]="bottomLeft"></label> <label class="form-label" for="bottomRight">Bottom Right <input class="form-input" type="number" id="bottomRight" min="0" max="40" [(ngModel)]="bottomRight"></label> </div> <div class="panel-actions"> <button id="applyRadius" class="btn btn-primary" type="button" (click)="applyRadiusSettings()">Apply Radius</button> </div> </div> </div> </div> </div>
html, body { position: absolute; inset: 0; margin: 0; } .sample-tutorial { position: relative; display: flex; height: 100%; overflow: hidden; background: #ffffff; } .sample-spreadsheets { flex: 1 1 auto; min-width: 0; width: auto; height: 100%; overflow: hidden; } .options-container { flex: 0 0 400px; width: 400px; height: 100%; box-sizing: border-box; background: #ffffff; overflow-y: auto; overflow-x: hidden; border-left: 1px solid #eeeeee; color: #333; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; font-size: 13px; } .panel-section { padding: 12px 16px; border-bottom: 1px solid #f0f0f0; } .panel-section:last-child { border-bottom: none; } .panel-section-title { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; font-weight: 600; font-size: 13px; color: #555; text-transform: uppercase; letter-spacing: 0.3px; } .panel-section-description { margin-bottom: 10px; color: #64748b; font-size: 12px; line-height: 1.35; } .panel-card { background: #fafafa; border: 1px solid #e8e8e8; border-radius: 8px; padding: 14px; } .form-group { margin-bottom: 12px; } .form-group:last-child { margin-bottom: 0; } .form-group-inline { display: flex; align-items: center; margin-bottom: 12px; } .form-group-inline:last-child { margin-bottom: 0; } .form-group-inline .form-label { width: 135px; min-width: 135px; margin-bottom: 0; padding: 0 4px; box-sizing: border-box; } .form-group-inline .form-select { flex: 1; margin-left: 8px; } .form-label { display: block; font-size: 12px; font-weight: 500; color: #666; margin-bottom: 4px; } .form-select, .form-input { width: 100%; height: 32px; padding: 0 8px; box-sizing: border-box; border: 1px solid #d0d0d0; border-radius: 6px; background: #fff; color: #333; font-size: 13px; outline: none; transition: border-color 0.15s, box-shadow 0.15s; } .form-select { cursor: pointer; } .form-select:focus, .form-input:focus { border-color: #4a7a00; box-shadow: 0 0 0 2px rgba(74, 122, 0, 0.15); } .back-color-field .form-label { margin-bottom: 0; } .back-color-controls { display: flex; align-items: center; justify-content: flex-end; gap: 12px; flex: 1; margin-left: 8px; } .no-fill-option { display: flex; align-items: center; gap: 5px; color: #333; font-size: 13px; font-weight: 500; white-space: nowrap; cursor: pointer; } .back-color-controls input[type="color"] { width: 44px; height: 30px; padding: 2px; border: 1px solid #d0d0d0; border-radius: 6px; background: #ffffff; cursor: pointer; } .back-color-controls input[type="color"]:disabled { opacity: 0.45; cursor: not-allowed; } .border-field .form-label { margin-bottom: 0; } .border-controls { display: flex; align-items: center; gap: 8px; flex: 1; margin-left: 8px; } .border-controls .form-select { flex: 1; margin-left: 0; } .border-controls input[type="color"] { width: 44px; height: 30px; padding: 2px; border: 1px solid #d0d0d0; border-radius: 6px; background: #ffffff; cursor: pointer; } .border-controls input[type="color"]:disabled { opacity: 0.45; cursor: not-allowed; } .field-hint { margin: -6px 0 12px 143px; color: #64748b; font-size: 12px; line-height: 1.35; } .radio-group { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } .radio-group label { display: flex; align-items: center; justify-content: center; gap: 4px; min-height: 32px; margin: 0; border: 1px solid #d0d0d0; border-radius: 6px; background: #fff; color: #333; font-size: 13px; font-weight: 500; cursor: pointer; } .options-container input[type="radio"], .options-container input[type="checkbox"] { accent-color: #4a7a00; } .corner-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; } .corner-grid label { font-size: 12px; font-weight: 500; } .panel-actions { margin-top: 14px; display: flex; gap: 8px; } .options-container .btn { height: 34px; padding: 0 16px; border: none; border-radius: 6px; font-size: 13px; font-weight: 500; cursor: pointer; transition: all 0.15s; white-space: nowrap; } .options-container .btn-primary { background: #4a7a00; color: #fff; flex: 1; } .options-container .btn-primary:hover { background: #3d6b00; } #applyRadius { width: 100%; }
(function (global) { System.config({ transpiler: 'ts', typescriptOptions: { tsconfig: true }, meta: { 'typescript': { "exports": "ts" }, '*.css': { loader: 'css' } }, paths: { // paths serve as alias 'npm:': 'node_modules/', 'cdn:': 'https://cdn.mescius.io/demoapps/packages/spreadjs/19.2.2-master-2026-09-02-2133/' }, // map tells the System loader where to look for things map: { 'zone': 'npm:zone.js/fesm2015/zone.min.js', 'rxjs': 'npm:rxjs/dist/bundles/rxjs.umd.min.js', '@angular/core': 'npm:@angular/core/fesm2022', '@angular/common': 'npm:@angular/common/fesm2022/common.mjs', '@angular/compiler': 'npm:@angular/compiler/fesm2022/compiler.mjs', '@angular/platform-browser': 'npm:@angular/platform-browser/fesm2022/platform-browser.mjs', '@angular/platform-browser-dynamic': 'npm:@angular/platform-browser-dynamic/fesm2022/platform-browser-dynamic.mjs', '@angular/common/http': 'npm:@angular/common/fesm2022/http.mjs', '@angular/router': 'npm:@angular/router/fesm2022/router.mjs', '@angular/forms': 'npm:@angular/forms/fesm2022/forms.mjs', 'jszip': 'npm:jszip/dist/jszip.min.js', 'typescript': 'npm:typescript/lib/typescript.js', 'ts': './plugin.js', 'tslib':'npm:tslib/tslib.js', 'css': 'npm:systemjs-plugin-css/css.js', 'plugin-babel': 'npm:systemjs-plugin-babel/plugin-babel.js', 'systemjs-babel-build':'npm:systemjs-plugin-babel/systemjs-babel-browser.js', '@mescius/spread-sheets': 'cdn:@mescius/spread-sheets/index.js', '@mescius/spread-sheets-angular': 'cdn:@mescius/spread-sheets-angular/fesm2020/mescius-spread-sheets-angular.mjs', '@grapecity/jsob-test-dependency-package/react-components': 'npm:@grapecity/jsob-test-dependency-package/react-components/index.js' }, // packages tells the System loader how to load when no filename and/or no extension packages: { src: { defaultExtension: 'ts' }, rxjs: { defaultExtension: 'js' }, "node_modules": { defaultExtension: 'js' }, "node_modules/@angular": { defaultExtension: 'mjs' }, "@mescius/spread-sheets-angular": { defaultExtension: 'mjs' }, '@angular/core': { defaultExtension: 'mjs', main: 'core.mjs' } } }); })(this);