Release Notes
All notable changes to @reogrid/lite
and @reogrid/pro.
Format follows Keep a Changelog;
versioning follows Semantic Versioning.
Feature release. Headline items: zoom — 10%–400% per sheet, with Ctrl/Cmd + wheel zooming around the pointer, where only the view scales and everything that is measured, printed or exported stays at 100% — and hyperlinks that are read from and written to xlsx and open on click. A workbook can now go into one PDF, a form's blank bordered boxes count toward the printed area, an exported xlsx opens in Excel looking like the grid, and iPad / iPhone can type into cells and download files properly. The worksheet-level single-sheet loaders are renamed to say what they do (loadSheetFrom*); the old names still work and warn, so there are no breaking public-API changes — but Ctrl/Cmd + wheel over the grid now zooms instead of reaching the browser's page zoom, plain cells in an imported xlsx take the workbook's default font instead of Arial 10, and numbers typed in full-width digits are stored as numbers.
✨ Highlights
- ▸ Zoom (both editions).
worksheet.setZoom(1.5), Ctrl/Cmd + wheel or a trackpad pinch scales the view of each sheet from 10% to 400%, anchored at the pointer — like Excel's zoom. Row heights, column widths, fonts, printing and exports stay at 100%, grid lines keep their 1px weight, and the grab zones for resizing, drag-to-move and image grips keep their size on screen. xlsx zoomScale round-trips, so a workbook saved at 85% opens at 85%. - ▸ Hyperlinks (both editions; authoring is Pro). Links in an xlsx file now open in a new tab on click — click and hold to select the cell instead — show their address on hover, and are kept on save.
=HYPERLINK() works too (Pro). Only http / https / mailto / tel addresses are ever opened. - ▸ **Single-sheet loaders renamed to
loadSheet* (both editions).** grid.worksheet.loadFromFile(file) read like "open this file" but kept only the first sheet, so a workbook whose first tab is a cover page came up blank. The single-sheet loaders now say so in their names (loadSheetFromFile / loadSheetFromUrl / loadSheetFromBuffer), the workbook-level grid.loadFromFile() is the one that opens everything, and the old names keep working as deprecated aliases until 2.0. - ▸ Whole-workbook PDF (Pro).
grid.saveAsPdf({ sheets: 'all' }) puts every visible sheet into one PDF — Excel's "Print Entire Workbook" — each on its own paper size and page setup, with page numbers running on across sheets. - ▸ Forms print whole (Pro). Blank bordered boxes, shaded bands, merged remarks fields and stamp images now count toward the printed area, so "fit to 1 page" no longer shrinks a form to the cells that happen to hold values and cuts the rest off.
- ▸ Exported xlsx looks the same in Excel (Pro). Row heights, number formats, numeric values, line breaks and empty styled cells are now written the way Excel expects, so a saved form no longer opens with taller rows, unformatted amounts and cut-off labels.
+ Added
- ▸ Zoom.
worksheet.setZoom(zoom, anchor?) / getZoom() scale the view per sheet from 10% to 400%, like Excel's zoom box, and Ctrl/Cmd + wheel (or a trackpad pinch) zooms around the pointer. Only the view scales: row heights, column widths, fonts, printing and exports stay at 100%, grid lines keep their 1px weight, and the grab zones for resizing, drag-to-move and image grips stay the same size on screen. xlsx sheetView@zoomScale is now read and written, so a workbook saved at 85% opens at 85%. Follow changes with instance.onZoomChange(listener). Available in both editions. - ▸ Hyperlinks. Links in an xlsx file (
<hyperlinks>) are now read and written, and clicking a linked cell opens it in a new tab — click and hold to select the cell instead, as in Excel. Hovering shows a pointer and the address the link goes to. =HYPERLINK(address, [label]) shows its label and opens the address on click (Pro, like every built-in function). Links follow row/column insert and delete, sort and drag-to-move, and round-trip through JSON (sheet.hyperlinks). Only http / https / mailto / tel addresses are opened, with noopener,noreferrer; anything else (javascript:, file: paths, relative paths) shows a short message on the cell instead. instance.onHyperlinkClick fires first — set event.cancel = true to route or confirm the address yourself. Author links with worksheet.setHyperlink(row, col, url, { tooltip }) / cell.setHyperlink (Pro); loading, showing and clicking links works in both editions. Links to a place inside the workbook are kept but not followed yet. A grid embedded in a sandboxed iframe needs allow-popups allow-popups-to-escape-sandbox for a click to open anything. - ▸ Several sheets in one PDF (Pro).
grid.exportPdf() / grid.saveAsPdf() take sheets: 'active' (the default, as before), 'all' for every visible sheet in tab order, or a list of sheet indexes and/or names. Each sheet paginates on its own terms — with usePageBreaks, its own paper size, orientation, margins, fit-to-pages and page breaks — so one document can mix A4 portrait and A3 landscape pages. A header/footer's &P counts from the document's first page and &N is the document's total, as Excel numbers a whole-workbook print; &A still names each page's own sheet. Sheets with nothing to print are skipped. The headless counterpart is exportWorksheetsPdf(worksheets, options).
~ Changed
- ▸ Ctrl/Cmd + mouse wheel over the grid now zooms the sheet instead of passing through to the browser's page zoom. Pass
createReogrid({ wheelZoom: false }) to keep the old behaviour. If you overlay your own elements using getCellRect() / getRangeRect() / getImageRect(), note that those stay in 100% sheet px — multiply by worksheet.getZoom(). - ▸
worksheet.loadXlsx / loadFromBuffer / loadFromFile / loadFromUrl are renamed to loadSheetFromBuffer / loadSheetFromFile / loadSheetFromUrl, and the old names are deprecated. These import a single sheet, but carried exactly the same names as the workbook-level methods on the instance — so grid.worksheet.loadFromFile(file) read like "open this file" while quietly keeping only the first sheet, with no sheet tab bar and no way to reach the other tabs. Use grid.loadFromFile() / loadFromUrl() / loadXlsx() to open a whole workbook, and worksheet.loadSheet* only when you deliberately want one sheet (options.sheetName picks which). The old names still work exactly as before, but are marked @deprecated and print a one-time console warning naming both replacements. They will be removed in 2.0.
🐛 Fixed
- ▸ Blank bordered cells, fills, merges and images are part of the printed area (Pro). With no print area set, the printed range ran from A1 to the last cell holding a value. A typical form — a bordered table whose rows are still empty, a merged remarks field whose text sits in its first cell, a stamp box, a logo — reaches much further than that, so the page-break preview, PDF export and browser print cut the form off at its last value, and fit-to-pages shrank the sheet to fit only that part. The range now reaches every cell that leaves a mark on paper: values, borders, fills (not white), the full extent of merged cells, and images (
worksheet.getPrintContentExtent() reports it). Formatting that prints nothing — a font, a number format, a white fill — still does not extend it, so it cannot add blank pages. - ▸ xlsx export: what Excel shows now matches the grid (Pro). Row heights were written in pixels where the file format expects points, so every row came out a third taller in Excel — and a third taller again on each re-import; they are now converted, and a round trip gives back the same heights. Number formats (
#,##0, [$¥]#,##0, dates, percentages …) were not written at all; they now are, including on empty input cells that carry a format. Numbers typed into cells ("135000") were written as text, so Excel neither calculated with them nor applied their format; plain decimals are now numbers, while text that only looks numeric ("007", "1e3") stays text. A value containing a line break gets wrap text on, since Excel shows a break only then. Empty styled cells are written without a value, so a right-aligned label can spill left across them in Excel instead of being cut off. - ▸ xlsx import: cells without a style use the workbook's default font. A cell Excel writes without a style takes the workbook's Normal style, but the importer left it on ReoGrid's own Arial 10 — so in a workbook whose Normal font is, say, Candara 12, the untouched cells of a table came out in a different font and size from the formatted cells beside them. A typical Excel file now shows its plain cells in Calibri 11 / 游ゴシック 11 / MS Pゴシック 11 rather than Arial 10. Empty cells that are not in the file at all still start in ReoGrid's default when typed into.
- ▸ xlsx import: a table with Excel's "None" style is shown plain. A table set to no style in Excel came up with the default blue header, white bold header text and banded rows. It now paints nothing and is written back without a style;
addTable(range, { style: '' }) creates one. - ▸ xlsx import: formulas ReoGrid cannot read show Excel's result, not their text. A formula that refers to another workbook (
=[1]Sheet1!A1*2), uses a structured table reference (=SUM(Table1[Qty])) or other syntax the engine does not parse used to show its formula text in the cell. The cell now shows the result Excel saved in the file, other formulas can use it, and it moves with the cell on insert/delete, sort and move; the formula itself is kept and written back on export. It is not recalculated, and editing the cell drops it. A quoted reference to another workbook (='[1]My Sheet'!A1) is no longer mistaken for a missing local sheet (#REF!). - ▸ Overwriting a formula with a value recalculates the formulas that read it. Typing a value over a formula cell left the formulas that referenced it on their old result until the next full recalculation.
- ▸ Numbers typed in full-width digits are numbers. With a Japanese IME on, "120" went into the cell as text: a formula multiplying it came out blank and a decimal validation rejected it. As in Excel, an entry that is a number once its full-width digits (and + - .) are folded is now stored in half-width ("120"). Text that merely contains one — "3階" — is kept as typed.
- ▸ Form navigation: a number typed with the IME on takes one Enter. The Enter that confirms an IME composition belongs to the IME, so a quantity or price field in a form took two Enters — one to confirm "120", one to move on. With
formNavigation on, when that Enter leaves the editor holding nothing but a number, it now commits and moves on as well. Text ("3階") still only confirms, so a word can be confirmed and typed on. - ▸ Picking from a cell's ▼ list is undoable, and moves on in a form. A pick made by clicking or tapping the dropdown button wrote the cell directly, so Ctrl/Cmd+Z could not take it back — unlike the same pick made from the keyboard. Pointer picks now go onto the undo stack too, and with
formNavigation on, the form moves to the next field as if the option had been typed and Enter pressed. Outside a form the selection stays on the cell, as in Excel. - ▸ Cells drawn after a dropdown keep their own font and colour. A list-validation (or
dropdown cell type) cell holding a value left its font and colour set on the canvas, so the cells drawn after it could lose their bold — every later cell with the same style, until something changed the font — and a placeholder could lose its muted colour. The dropdown now restores what it changes, and every cell-type handler call is fenced, so a handler registered with registerCellTypeHandler that forgets to clean up cannot leak into the rest of the sheet either. - ▸
hyperlink cell type: script addresses are no longer opened. The cell type opened its address (by default the cell text) with window.open and wrote it into <a href> on HTML export as is, so a javascript: address would run in the page. It now opens only http / https / mailto / tel addresses — relative paths still work, resolved against the page — and exports anything else as plain text. - ▸ iPad: typing into a cell is no longer auto-capitalised or auto-corrected. With a hardware keyboard, iPadOS applied its sentence capitalisation and auto-correction to the cell editor, so
m2 was stored as M2. The editor now opts out. Tapping one cell quickly after another also no longer triggers the browser's double-tap zoom, and the grey tap flash over the grid is gone. - ▸ Downloads work on iPad and iPhone Safari (Pro).
saveAsPdf and saveAsXlsx released the file right after starting the download, but Safari first asks whether to view or download the file and reads it only afterwards — so nothing was saved. The file now stays available long enough. - ▸ Printing on iPad no longer loses the page (Pro).
printWorksheet removed its hidden print frame one second after calling print(). iPadOS returns from print() at once and lays the pages out while its print sheet is open, so the frame could be gone before it was printed. The frame now stays until the next print replaces it. - ▸ Lite: the "Pro feature" console warning links to a page that exists. Calling a Pro-only method in
@reogrid/lite logged an upgrade link that returned 404; it now points to the pricing page. - ▸ npm package pages.
@reogrid/pro now ships a real README (features, license key, xlsx / PDF / printing, upgrading from Lite) instead of a short placeholder, and the @reogrid/lite README is rewritten around the framework-agnostic createReogrid() API, with React and Vue as included components. Both packages' homepage is now this site, and their license field points to the README's License section — Lite: free license, commercial use allowed; Pro: paid license.
Feature release. Headline items: floating images you can insert, drag and resize — and that finally survive a save — multi-range selection with Ctrl/Cmd+click, vertical & rotated text rendered the same way on screen, in xlsx, in browser print and in PDF, and a form mode that turns a protected sheet into a keyboard-only fill-in form. Printing gains Excel's "fit to N pages", both the print area and cell comments are now written into xlsx instead of being silently dropped on save, browser print finally matches what the canvas draws, and PDF export stops coming out hairline. No breaking public-API changes.
✨ Highlights
- ▸ Images: insert, move, resize (Pro). Images used to be read-only — a picture loaded from an xlsx was displayed, then silently dropped when the workbook was saved. Now
worksheet.addImage(bytes, { row, column, width, height }) places one, and the mouse edits it: click to select, drag to move, drag one of the eight grips to resize (Shift keeps the aspect ratio), Delete to remove — one Ctrl+Z per drag. Excel's three cell anchors are all supported, and there is a real xl/media + xl/drawings round-trip. - ▸ Multi-range selection (both editions). Hold Ctrl (Cmd on macOS) and click or drag to add a range instead of replacing it — on cells and on row/column headers alike. Delete clears every range as one undoable step, styles apply to all of them, auto-fit fits each Ctrl+clicked column, and copy follows Excel's rule that the ranges have to line up to travel as one block.
ws.selection.ranges / count / add() mirror the interaction. - ▸ Vertical & rotated text (both editions).
setTextRotation(-90..90) gives you the slanted headers that make narrow numeric columns readable, and setVerticalText() does Japanese vertical writing — characters stacked top-to-bottom, columns running right to left. Auto-fit measures the rotated outline, so a 90°-turned label gets a column as narrow as the row is tall, and screen, xlsx, browser print and PDF all agree. - ▸ Form mode (both editions). Four switches turn a protected sheet into a fill-in form:
layoutLocked stops the user resizing anything, clickToEdit opens the editor on a single click, setCellPlaceholder() draws a hint in an empty cell, and formNavigation makes Enter / Tab walk the input cells in reading order. List validation can now be driven from the keyboard too (↓ while editing, Alt+↓ while selected), so a form fills in end-to-end without the mouse. - ▸ Fit to pages (Pro).
setPrintSettings({ fitToPages: { width: 1 } }) is Excel's "fit to N pages": instead of picking a shrink percentage by hand, say how many pages wide (and/or tall) the sheet should come out and the scale is worked out for you. Like Excel it only ever shrinks, and the page-break preview, PDF export and browser print all agree on the result. - ▸ PDF no longer prints hairline (Pro). The built-in font tags pointed at Google's variable fonts, whose default axis position is wght=100 — so every exported document came out Thin, and bold headings had nothing solid to thicken. Tags now carry a real face per weight,
preloadPdfFont() fetches regular and bold by default, and the JP payload dropped from one 9.15MB variable font to 2.5MB per face.
+ Added
- ▸ Form mode.
ws.layoutLocked (stop header drag-resize, double-click auto-fit and whole-line reorder — API sizing still works), ws.clickToEdit (a single click opens the editor with the caret at the end, and the pointer shows an I-beam over editable cells), ws.setCellPlaceholder(row, column, text) with getCellPlaceholder / clearCellPlaceholders / getCellPlaceholderEntries (hint text drawn only on screen — never in the value, PDF, print, xlsx or JSON), and ws.formNavigation with ws.setFormNavigator(fn) and ws.nextInputCell(row, column, direction). The built-in order is a reading-order scan that skips protected cells, hidden lines and merge interiors, and wraps at the end of a row. None of it is persisted, so re-apply after loadJson / reset. Available in both editions. - ▸ List validation from the keyboard. ↓ while editing, or Alt+↓ while the cell is selected, opens the same dropdown the mouse opens; ↑↓ move through the options, Enter commits, Escape closes. The list opens at the current value (or the first entry), so ↓ then Enter is a two-keystroke pick — and because the choice is committed through the editor, validation and form navigation still apply.
- ▸ Header font.
createReogrid({ headerFont: { fontFamily: 'Meiryo', fontSize: 14 } }) for the whole workbook, instance.setHeaderFont(...) at runtime (inherited by sheets added later), and ws.setHeaderFont(...) per sheet, with getHeaderFont() / getHeaderFontCss(). The patch is partial — { fontSize: 14 } keeps the family — and null restores the default. The header band widens to fit, so a larger font does not clip the numbers. A screen setting, independent of CellStyle: not stored in xlsx or JSON. - ▸ Text rotation & vertical text.
ws.range(...).setTextRotation(degrees) takes Excel's -90..90 (positive reads up to the right; out-of-range values are clamped), and setVerticalText(value?) switches on Japanese vertical writing — long vowel marks and brackets are turned to their vertical forms, and 、。 sit at the top-right of the character box. The two are one control, as in Excel: setting an angle clears vertical writing and vice versa. Wrapping follows the direction the text actually runs, and it round-trips as Excel's <alignment textRotation> (255 for vertical). Available in both editions. - ▸ Images (Pro).
worksheet.addImage(source, anchor, options?) takes a Uint8Array, ArrayBuffer or data: URL; addImageFromFile(file, anchor, options?) takes a File/Blob and defaults to the image's natural size. moveImage / resizeImage / setImageAnchor / removeImage edit, getImage / getImages / getImageRect read. All three OOXML anchors are supported — oneCell (moves with the cell, fixed size), twoCell (moves and sizes with the cells) and absolute (pinned to the sheet) — and they follow row/column inserts and deletes, so deleting rows under a logo no longer deletes the logo. Round-trips through xlsx (identical images stored once), JSON (sheet.images; toJson({ includeImages: false }) leaves the bytes out) and PDF export. setImageEditEnabled(false) and sheet protection switch off the interaction while leaving the images visible; setImagesEnabled(false) turns the feature off entirely without touching the model, so the images still round-trip. - ▸ Multi-range selection.
ws.selection.ranges (a RangeHandle each, active last), ws.selection.count, and ws.selection.add('D4:E6') — the programmatic form of Ctrl+click. bounds / range / activeCell / moveTo keep their single-range meaning (the active range), and onSelectionChange gains a second argument listing every range, so existing listeners are unaffected. A plain click or an arrow key collapses back to one range; Shift+click and Shift+Arrow extend the newest range and keep the others. The fill handle and drag-to-move are off while several ranges are selected. - ▸ Fit to pages (Pro).
setPrintSettings({ fitToPages: { width, height } }) — omit an axis (or pass 0) to leave it unconstrained. While fit mode is on, scale is ignored but remembered, and comes back when you turn it off; worksheet.getEffectivePrintScale() reports what was worked out. Round-trips through xlsx (<pageSetUpPr fitToPage> + <pageSetup fitToWidth/fitToHeight>) and JSON. - ▸ The print area is written to xlsx (Pro).
setPrintableRange(...) used to round-trip through ReoGrid JSON only, so saving to xlsx printed the whole sheet again on the next open. It now travels as Excel's own _xlnm.Print_Area, and is read back the same way. - ▸ Cell comments survive a save to xlsx. Comments could be read from an xlsx but not written back, so saving a commented workbook silently dropped every note. Export now writes the comments part and the legacy VML box Excel expects alongside it, so a note round-trips with its text, its author, and whether it is pinned open. Nothing to call — it happens on
saveAsXlsx. - ▸ Undoable page-break and print-area drags (Pro). Dragging a page-break line, or a print-area edge, in the page-break preview now goes on the undo stack like every other pointer edit — one drag, one Ctrl+Z. Undoing a break move also puts back the print scale the drag lowered and any manual breaks it swept over. Re-dropping a line exactly where it sat costs no undo step; dropping an automatic line in place still counts, because that pins it as manual.
- ▸ Japanese era first year (元年). The
[$-ja-JP-x-gannen] locale tag prints an era's first year as 元 instead of 1 — [$-ja-JP-x-gannen]ggge"年"m"月"d"日" gives 令和元年7月1日, which government and financial forms require. Every other year is unaffected, and a colour or condition bracket is never mistaken for a locale tag. - ▸ Headless xlsx import.
XlsxImporter and convertImageInfo are exported from the public entry, for build tools that turn customer .xlsx files into templates outside a browser (CanvasWorksheet.loadXlsx needs the DOM). Images come back in the result's images; hand each to worksheet.loadImageFromImport(convertImageInfo(image)).
🐛 Fixed
- ▸ PDF export came out hairline (Thin) (Pro). The built-in font tags pointed straight at Google's variable fonts. A variable font keeps its default-position outlines in
glyf and the deltas in gvar; PDF export embeds glyf outlines and does not apply gvar, so output was baked at the fvar default of wght=100 — Thin — across the whole page (/BaseFont /…+NotoSansJP-Thin). Bold is a synthetic 4% stroke of the outline, so it could not rescue a Thin base. Since a PDF weight is a separate font file, a tag now holds a face per weight, pointing at the static faces of @reogrid/font-{jp,sc,tc,kr}. preloadPdfFont(tag) fetches normal and bold by default and skips thin; it still resolves with the regular bytes, so existing calls are unchanged. registerPdfFont(tag, source) still accepts a single source as normal, and now also takes a { normal, bold, thin } record — the shape @reogrid/font-* exports. Synthetic bold is demoted to a fallback used only when no bold face is present, and options.fonts adds weights to a font you loaded yourself. - ▸ PDF ignored a number format's bracket colour (
[Red]). On screen a format colour wins over the cell style's text colour, but PDF export only looked at the cell style — so a negative under #,##0;[Red]"▲"#,##0 was red on screen and black on paper, which quietly erases the red figures on a printed accounting report. PDF now follows the same order as the screen: format colour → cell style colour → default. Rich-text cells carry a colour per run and are string cells, so a format colour never applies to them (same as on screen). Browser print (via HTML) is unaffected and keeps using the cell style colour. - ▸ Browser print dropped characters (「発…」). Text visible on screen was lost on paper, because canvas and HTML disagreed in three ways. (1) No spill into empty neighbours — canvas, like Excel, lets text wider than the column spill into empty adjacent cells, but a
<td> always clips. The overflow width is now computed with the same pure function the screen uses, and the text is wrapped in a box allowed to draw that far out (still stopping inside the box when the neighbour is occupied). (2) An ellipsis was added — a 2px overhang turned the last character into …; neither canvas nor Excel does that on paper, so text-overflow: ellipsis is gone. (3) Rows were taller than specified — <tr height> is a minimum in HTML, so a wrapping detail row stretched and pushed later rows down until the ones at the foot of the page (payee, notes) fell off the paper. Wrapping cells are pinned to exactly the row height and clipped, with line-height matched to canvas. Only printing changes; clipboard copy is untouched. - ▸ Content taller than its row was drawn over the rows above and below. Wrapped text exceeding the row height spilled up and down when centered, or up when bottom-aligned, overlapping the neighbouring rows' text — three lines in a two-line-high row, for instance. It is now clipped to the row, as in Excel.
- ▸ Browser print did not draw cell-anchored images. A logo or 印影 appeared on screen, in PDF and in xlsx, but was missing from
printWorksheet() / toPagedHtmlDocument() — the HTML print path was the one place image support was not wired up when insertion landed. Images are now positioned absolutely against each printed page band and scale with it, so the shrink factor, page breaks and print area come out the same as in PDF. toPagedHtmlDocument gains showImages (default: whatever the sheet has). - ▸ Setting a column width or row height one line at a time did not move the page breaks.
worksheet.column(c).width = w / row(r).height = h did not rebuild the prefix-sum boundaries, so getPrintPageRanges() returned breaks computed from the old sizes. In a browser the next paint usually rebuilt them and hid it, but reading before a paint returned the stale result (a template that sets column widths right after reset() occasionally reporting "two pages wide"), and headless there is no paint, so it was always wrong. The bulk APIs (applyColumnWidths and friends) already rebuilt; only the one-at-a-time path was missing it. - ▸ Committing with Enter put the previous value into the same cell twice. The editor's Enter committed without moving, and the same keydown carried on up to the
KeyboardController — where, editing having ended, the Enter branch reopened the editor on the same cell with the committed text still in it. Typing 120⏎180⏎ gave you 120180. Tab fell through to the browser default and moved focus out of the grid. Enter now commits and moves down, Shift+Enter up, Tab right, Shift+Tab left (a commit rejected by a stop validation rule keeps editing and does not move), and the event stops there. The IME proxy textarea is also cleared when editing ends, so the next keystroke is not appended to the old text. - ▸ Thousands scaling (a trailing comma) did not work in a format code with decimals.
#,##0, and #,##0,,.00 — a comma at the end of the integer part — worked, but 0.0, and #,##0.0,,, where the comma follows the decimal part (the arrangement Excel itself writes), did not scale. Worse, the comma was not stripped from the format, so parsing the numeric pattern failed outright and 0.0,,"M" displayed 2,400,000 as 2400000.0,,M rather than 2.4M. Trailing-comma detection only looked at the integer part; it now looks both at the end of the integer part and at the end of the whole format. Grouping commas (#,##0) are excluded as before, so the summary-column staple [>=1000000]#,##0.0,,"M";[>=1000]#,##0.0,"K";0 now matches Excel. - ▸
autoFitMaxScanRows could not be set from createReogrid(). The auto-fit scan cap added in v1.5.0 (default 1000 rows) existed only on WorksheetOptions, with no declaration or forwarding in the public ReogridOptions — so package users could not reach it. createReogrid({ autoFitMaxScanRows: 5000 }) now works and is inherited by every sheet in the workbook (initial, added at runtime, or imported). worksheet.setAutoFitMaxScanRows(n) / getAutoFitMaxScanRows() change it at runtime (values below 1 are clamped to 1; non-finite values restore the default). - ▸ Text in a hidden column was drawn over the column to its right. A value left in a hidden column painted over the next visible column, so that column's value looked doubled — which is what happens to an imported business template that hides its working columns. Hidden columns are now excluded the way hidden rows already were.
- ▸ Sorting left comments, cell types and lock state behind on the original rows.
sortRows (internally Worksheet.permuteRows) reordered values, styles and borders but left cell comments, cell-type configuration (checkboxes and friends) and explicit lock state on the original rows, so after a sort a comment appeared to belong to a different row's content. Comments now move with their id intact; anything outside the sorted column range, or on a hidden (non-participating) row, stays put. Row and column moves already carried these through CellPayload — only sorting (and its undo) was missing it.
Feature release. Headline items: Excel-style find & replace (Ctrl+F / Ctrl+H), drag-to-move for selected ranges, rows and columns, and page header / footer with Excel format codes — plus PDF export by locale, which embeds the right CJK font for you. No breaking public-API changes.
✨ Highlights
- ▸ Find & replace (both editions). A built-in bar on Ctrl/Cmd+F and Ctrl/Cmd+H searches the sheet, the selection, or the whole workbook — activating the sheet a match lands on — with match-case, entire-cell and regular-expression options. Every hit is highlighted on the canvas, Enter steps through them, and replace-all undoes in one step. Drive your own UI with
createReogrid({ showFindBar: false }) plus grid.find / replaceAll / …. - ▸ Range / row / column move. Drag the selection border to relocate a block (overwriting the destination, as in Excel), or drag an already-selected row/column header to reorder whole lines as a cut-and-insert. Value, formula, style, number format, cell type, border, comment and lock all travel together.
onBeforeRangeMove is cancellable, merged cells are never torn, and every move is one undo entry. - ▸ Page header & footer (Pro).
PrintSettings.headerFooter prints a page number, date, sheet name or file name in the paper margins using Excel's own format codes (&P / &N / &D), with optional different odd/even and first-page bands. Drawn by PDF export and the paged print HTML, and round-tripped through xlsx. - ▸ PDF export by locale (Pro).
saveAsPdf({ locale: 'zh-CN' }) embeds the right CJK font instead of making you carry the bytes — ja / zh-CN / zh-TW / ko are registered out of the box, and registerPdfFont adds your own. A Japanese font covers only about a third of common simplified-Chinese business vocabulary, so this is what stops Chinese exports coming out as tofu.
+ Added
- ▸ Find & replace.
instance.find / findNext / findPrevious / findAll / replace / replaceAll / clearFind / showFindBar / hideFindBar, plus worksheet.findAll and friends. Options: matchCase, wholeCell, useRegex, lookIn ('formula' | 'value'), order, range, includeHidden, and an instance-level scope ('sheet' | 'selection' | 'workbook'). Hidden lines, merged followers and unloaded delay-load rows are skipped; protected cells are never rewritten. - ▸ Range / row / column move.
RangeHandle.moveTo(row, column), worksheet.moveRange / moveRows / moveColumns with canMoveRange / canMoveRows / canMoveColumns pre-checks, worksheet.setRangeMoveEnabled(false) to turn the interaction off, and the instance.onBeforeRangeMove (cancellable) / onAfterRangeMove events. - ▸ Page header / footer (Pro).
PrintSettings.headerFooter with left/center/right sections per band, evenHeader / evenFooter and firstHeader / firstFooter behind differentOddEven / differentFirst, and real margins.header / margins.footer (Excel's 0.3 in default). A tall band pushes the body margin out, so it costs rows per page rather than printing over cells. - ▸ PDF
locale + font registry (Pro). ExportPdfOptions.locale names the font to embed; registerPdfFont, preloadPdfFont, getPdfFont, isPdfFontLoaded, isPdfFontRegistered, getRegisteredPdfFontTags and clearPdfFontCache are exported from @reogrid/pro. Export stays synchronous, so preload the locale once at an async point you control. - ▸
instance.focus() — return keyboard focus to the grid after a host toolbar or menu action, so the next shortcut (undo/redo, copy/paste, arrow keys) reaches the grid instead of the browser. - ▸
worksheet.clearRowOutlines() / clearColumnOutlines() (Pro) and syncOutlines() — drop every outline group in one call, or re-apply outline state after mutating an OutlineManager directly. - ▸
WorksheetOptions.autoFitMaxScanRows (default 1000) — cap how many rows an auto-fit scans, bounding text-measurement cost on very large sheets.
~ Changed
- ▸ Double-clicking a column/row border auto-fits the whole selection. Select A:D (or rows 3:6) and double-click any of their borders and every selected line is fitted, as in Excel; it used to fit only the line under the pointer.
- ▸ HiDPI re-scale on monitor change. The grid listens for
devicePixelRatio changes, so moving the window between a Retina and a standard monitor no longer leaves the canvas blurry until the next manual resize.
🐛 Fixed
- ▸ Overflowing text keeps rendering after its own cell scrolls out of view. A long value spilling across empty cells to its right disappeared entirely once its own column left the viewport.
- ▸ A dropdown near the bottom of the window opens upward instead of off-screen. Cell dropdowns (list validation / dropdown cell type) and the auto-filter popup now flip above the cell when there is no room below, and stay clamped inside the viewport horizontally.
- ▸ A floating image no longer covers the native scrollbars, and the last row/column is no longer hidden behind one on hosts with classic space-reserving scrollbars (Windows, or macOS set to always show scrollbars).
- ▸ Freezing panes no longer leaves dead travel at the end of the scrollbar — the scroll spacer is now re-computed on structure changes, so the native and worksheet scroll maxima agree immediately.
- ▸
readReoGridJson() replaces the worksheet instead of merging into it, so loading a second document no longer leaves the previous one's rows, styles and merges underneath — and writeReoGridJson(worksheet) keeps the sheet's name instead of renaming it to Sheet1 on every save. - ▸ A number-formatted formula cell no longer keeps a stale value after a bulk recalculation (sort, JSON load, report bind, or a move).
- ▸ PDF: B4 / B5 / Tabloid / Executive are honored in
pageSize instead of silently falling back to A4; grid lines follow the sheet's own setting; and a border on a page boundary prints on both pages. - ▸ PDF export rejects a font-family name with a message you can act on.
font takes font file bytes, so saveAsPdf({ font: 'Arial' }) used to surface as a RangeError from deep inside the font parser. The option now names the mistake and points at loadFont(url) — or at locale, when the string is a known one. ArrayBufferView inputs (a Uint8Array subarray) are accepted without re-wrapping. - ▸
worksheet.selection is in the published type declarations — the documented façade worked at runtime but was missing from the bundled .d.ts. - ▸ Removing an outline group gives its rows/columns back, and the group bracket's closing tick is drawn at the end away from the toggle button, matching Excel.
Feature release. Headline items: Excel-style page layout & printing (page-break preview), PDF export powered by our own in-house engine, pivot tables, table styles (Format as Table), data validation, report binding, cell comments, and named ranges — plus a batch of xlsx-import fidelity fixes for Japanese business forms. No breaking public-API changes.
✨ Highlights
- ▸ Page layout & printing (Pro). Excel-style page-break preview with draggable breaks, a print-settings model (paper size, orientation, margins, scale, page order), manual breaks, and a printable range. Round-trips through xlsx (
pageSetup / pageMargins / rowBreaks / colBreaks) and JSON. - ▸ PDF export (Pro). PDF generation via our own in-house engine — paginates exactly like the on-screen page-break preview.
instance.exportPdf() / instance.saveAsPdf(). Supply a glyf TrueType font via options.font (browser helper loadDefaultJapaneseFont()). - ▸ Pivot tables (Pro). API-driven live pivot tables that recompute from a source range — rows, columns, values, and aggregations (
sum / count / average / min / max / …). worksheet.createPivot(def). - ▸ Table styles / Format as Table (Pro). Excel built-in table styles rendered as a live style overlay (banding re-flows on insert/delete), with real
xl/tables/*.xml xlsx round-trip. worksheet.addTable(range, options). - ▸ Data validation (Pro). List, whole / decimal / date / time, text-length, and custom-formula rules with stop / warning / information alerts; list rules auto-render a dropdown. Read side works in Lite.
- ▸ Report binding (Pro). Header / detail(×N) / footer template sections materialized in place from bound data, with
{{token}} interpolation, per-row formula shift, footer aggregates, and report-driven page breaks. - ▸ Cell comments (Pro). Classic Excel comments (one per cell) with a red corner marker and hover bubble; xlsx read, JSON round-trip.
- ▸ Named ranges / defined names.
=SUM(MyRange), =Tax*2 with workbook or sheet scope, cascading recalculation, and xlsx/JSON round-trip. Pro-gated write side; reads work in Lite.
+ Added
- ▸ Page layout & printing (Pro).
PagingManager owns PrintSettings and manual page breaks; the page-break preview renders dashed-blue (automatic) and solid (manual/edge) lines, and breaks are drag-movable. Worksheet delegators: setPrintSettings, setShowPageBreaks, insertRowPageBreak / insertColumnPageBreak, getPrintPageRanges, … - ▸ PDF export (Pro).
instance.exportPdf(options): Uint8Array and instance.saveAsPdf({ filename, ... }) render the worksheet with no external library. The paginating renderer draws cell backgrounds, text (alignment, overflow, wrapping, alt-row colors, faux bold), grid lines, borders, merged-cell borders, embedded images, and rich-text runs. - ▸ Pivot tables (Pro). A pivot engine +
worksheet.createPivot() compute a live pivot from a source range and write the result into the grid; refreshPivot / PivotHandle.update() recompute. New demo pivot-demo.html. Known limitation: stored source/output ranges do not shift on row/column insert/delete — call refreshPivot after a structural edit. - ▸ Table styles / Format as Table (Pro).
worksheet.addTable / RangeHandle.formatAsTable(options) apply a live banded overlay keyed by OOXML style name (TableStyleMedium2 default) that re-flows on insert/delete; getTable / getTables read in Lite. Round-trips via real xl/tables/tableN.xml and JSON. - ▸ Data validation (Pro).
setValidation / removeValidation / clearValidations (Pro) + getValidations / validate (Lite); RangeHandle.setValidation / CellHandle.setValidation. Rules: list, whole / decimal / date / time / textLength, custom, and any. Round-trips via xlsx <dataValidations> and JSON. - ▸ Report binding (Pro).
defineReportTemplate / bindReport / unbindReport (Pro) + getReportTemplate / getReportState (Lite); instance.report handle. pageBreakEvery: N pins a manual break every N records. JSON round-trip (sheet.reportTemplate). - ▸ Cell comments (Pro).
setComment / removeComment / clearComments / setCommentVisible (Pro) + getComment / hasComment (Lite); CellHandle.setComment, RangeHandle.setComment. Round-trips via JSON; xlsx is read-only (VML write deferred). - ▸ Named ranges / defined names.
instance.defineName(name, address, scope?) / removeName / getName / getNames. Round-trips via xlsx <definedNames> (localSheetId for sheet scope) and JSON. - ▸ Persistent range-highlight overlay.
worksheet.setRangeHighlights draws persistent colored overlays on a set of ranges, independent of selection / formula-reference highlighting. - ▸ xlsx import — embedded OLE picture objects (StaticDib). Pasted-as-picture screenshots stored as OLE objects are now shown via a minimal CFBF/OLE2 reader that extracts the DIB/BMP.
~ Changed
- ▸ PDF export follows the page-break / print-settings model. With the new
usePageBreaks option, paper size, orientation, margins, scale, and page bands are all taken from worksheet.getPrintSettings() and its computed breaks, so the PDF paginates exactly like the on-screen preview. - ▸ Export filename defaults to the loaded document name.
loadFromFile / loadFromUrl record the source base name (getDocumentName() / setDocumentName()); saveAsPdf and saveAsXlsx default their download name to it (e.g. purchase-order.xlsx → purchase-order.pdf).
🐛 Fixed
- ▸ xlsx import — locale "short date" (numFmtId 14 / 22) showed the US form. Built-in 14/22 are locale-aware, so they now map to the East-Asian short date (
yyyy/m/d) — consistent with the 27–36 / 50–58 JP mapping and internationally unambiguous. - ▸ Copy — clipboard held the raw value, not the displayed text. Both clipboard payloads now use the displayed text (a date cell copies
2023/2/28, not the serial 44985); an empty-result formula copies as empty. Raw value + number format still round-trip via data-rg-* attributes. - ▸ Image overlay no longer paints over headers. A floating image is clipped to the cell area, so it no longer bleeds over the row/column headers when scrolled.
- ▸ xlsx import — Japanese forms. Locale-reserved date formats (numFmtId 27–36 / 50–58) render as dates; phonetic guides (
<rPh> furigana) no longer leak into shared-string text; _xlfn.IFS(…) future-function markers resolve instead of #NAME?. - ▸ xlsx import — small sheets & multi-line cells. A tiny used range no longer collapses the grid below the default size;
_xHHHH_ escapes are decoded (_x000D_); rows with hard line breaks auto-fit their height.
! Known issues
- ▸ Cell comments and pivot tables write to JSON losslessly, but xlsx support is partial: comments are read-only (VML write deferred) and a pivot persists its rendered cells only (
<pivotTableDefinition> deferred).
Feature release. Headline item: multi-sheet workbooks — a grid is now a workbook of N worksheets with a core-provided sheet tab bar, per-sheet undo, whole-workbook xlsx/JSON I/O, and cross-sheet formula references (=Sheet1!A1). No breaking public-API changes.
✨ Highlights
- ▸ Multi-sheet workbooks. A grid is now a workbook of N worksheets with one active sheet and a core-provided sheet tab bar (add, rename, delete, drag-to-reorder, hide, tab color). Each sheet has its own data, selection, scroll, frozen panes, and undo history. New
instance.workbook coordinator (addSheet / removeSheet / renameSheet / moveSheet / setActiveSheet / getSheets / onActiveSheetChange / onSheetsChange). Whole-workbook I/O on the instance: saveAsXlsx, loadXlsx / loadFromFile / loadFromUrl (loads all sheets), and toJson / loadJson. Available in both Lite and Pro; tier guards and row/col limits apply per sheet. - ▸ Cross-sheet formula references. Formulas can reference cells on other sheets:
=Sheet1!A1, ='My Sheet'!A1:B2, and unquoted non-ASCII sheet names (=シート1!A1). Aggregates and the LOOKUP family work over cross-sheet ranges (=SUM(明細!A1:A3)). Excel-compatible lifecycle: renaming a sheet rewrites referencing formulas, deleting one turns references into #REF!, row/column insert/delete shifts other sheets' references, and cross-sheet cycles are detected (#CYCLE!). Available in both Lite and Pro. - ▸ Instance-level events that follow the active sheet. New
instance.onSelectionChange / onCellValueChange / onBulkCellsChange / onScrollChange / onViewportSizeChange / onStructureChange / onContextMenu forward the active sheet's events and re-point automatically on a sheet switch — subscribe once instead of re-binding per sheet.
~ Changed
- ▸
instance.worksheet and instance.actionManager are now getters returning the active sheet's. Existing single-sheet code is unchanged. Note: single-sheet embeds now show a ~28px sheet tab bar by default — pass createReogrid({ showSheetTabs: false }) to hide it. - ▸ Multi-sheet xlsx import parses the file once and reuses the parsed workbook for every per-sheet import (previously O(N) re-parses for an N-sheet file). New
parseXlsx() helper + ParsedXlsx type. - ▸ Workbook view state round-trips through xlsx: active sheet, per-sheet hidden state, and sheet tab colors are written on export and restored on load. ReoGrid-JSON also persists per-sheet
view.showGridlines.
Patch release fixing native scrolling on very tall/wide sheets — notably the 1,000,000-row delay-load scenario introduced in 1.2.2. No public-API changes.
🐛 Fixed
- ▸ Huge sheets can now scroll all the way to the last row. Browsers clamp an element's scroll extent around 2^24 px, so a 1,000,000-row sheet (~22M px tall) silently stopped scrolling at row ~762,600. The scroll spacer is now capped at 14M px and native scroll positions are proportionally mapped onto the full worksheet range; wheel deltas are compensated so scroll speed is unchanged. Sheets below the cap keep the exact 1:1 mapping — no behavior change for normal-sized sheets.
Headline item: a Pro-only delay-load data source for displaying 1,000,000-row datasets. Also Excel-style horizontal text overflow, decimal capping in General number display, vertical text-metric fixes matching Excel/Google Sheets, and a package-entry audit that ships previously documented-but-unexported APIs. No breaking public-API changes.
✨ Highlights
- ▸ Delay-load data source (Pro) — display 1,000,000-row datasets.
worksheet.setDataSource(config) attaches an on-demand row loader: only visible rows ± a configurable buffer are fetched through the user-supplied load(rows) callback (sync or async). Load requests are microtask-batched and deduplicated, scroll prefetch is debounced, a flood guard caps automatic request size (so select-all + copy doesn't scan the whole dataset), and epoch-based invalidation (invalidateAll() / invalidateRows()) discards stale in-flight results. The source auto-detaches on loadCells / loadCellsAsync / reset. v1 is display-only: no formulas / sort / filter over unloaded rows. Types DelayLoadConfig, DataRecord, DataLoadCallback, and DataSourceHandle are exported. - ▸ Excel-style horizontal text overflow. Un-wrapped cell text wider than its column now spills into adjacent empty cells and is clipped at the first cell that has content, an explicit fill, or belongs to a merge. Spill direction follows alignment (left→right, right→left, center→both).
- ▸ General number display caps long decimals. Numeric cells without an explicit number format round their displayed decimals to a limit (default 4) —
0.30000000000000004 → 0.3, =1/3 → 0.3333. Display-only: stored values keep full precision. Configurable via createReogrid({ generalMaxFractionDigits }).
+ Added
- ▸ New demo page:
delay-load-demo.html — 1M rows with simulated server latency, fetch log, and jump-to-row. - ▸ Lite and Pro React/Vue wrappers now expose the worksheet event props/emits announced in v1.2:
onSelectionChange, onCellValueChange, onBulkCellsChange, onScrollChange, onViewportSizeChange, onStructureChange. Pro wrappers type options as ReogridProOptions so licenseKey can be passed. - ▸
@reogrid/pro now exports the cell-type registration API (registerCellTypeHandler et al.), NumberFormat, the undoable action classes (ActionManager, ClipboardService, PasteAction, …), buildXlsxFromSnapshot, computeAutoFillValues / AutoFillAction, and common data types. - ▸
@reogrid/lite now exports the ReoGrid JSON I/O and the common data types. KeyboardController.clipboardService is now public readonly as documented.
🐛 Fixed
- ▸ Cell text vertical metrics now match Excel / Google Sheets: default-font cells no longer auto-expand their row on first input; the inline editor is WYSIWYG (mirrors renderer line height and
verticalAlign); bottom-aligned text sits close to the cell bottom (~2 px) instead of floating high. - ▸ Cells whose content is taller than their row now honor middle / bottom vertical alignment instead of clamping to the top.
- ▸ HTML / browser-print output now draws merged-cell perimeter borders — the outer frame of merged title rows, notes boxes, and totals boxes is no longer dropped.
Patch release. Extends the formula reference editing introduced in 1.2.0 with Excel-style drag-to-move and drag-to-resize on the on-grid reference highlights. No public-API changes.
+ Added
- ▸ Drag-to-move and drag-to-resize for formula reference highlights. While editing a formula, dragging the dashed border of a reference rectangle translates the range; dragging one of the four corner grips resizes it. Every occurrence of that reference in the formula text is rewritten in step, preserving each token's absolute-reference (
$) flags. Resize uses cell-center boundaries — a cell only joins or leaves the range once the pointer passes its midpoint — matching Excel and the .NET edition. Small colored corner squares are drawn on each highlight to make the resize hot-zone discoverable, with hover cursors (move, nwse-resize, nesw-resize) on the border and corners.
Feature release. The formula library grows from 32 to 109 built-in functions, the cell editor gains Excel-style color-coded formula reference editing with click-to-insert, the selection now has a drag-fill handle, and conditional format rules can override borders per side. React and Vue wrappers expose worksheet events as component props. No breaking public-API changes.
✨ Highlights
- ▸ Auto-fill (drag fill handle). A small square at the bottom-right of the selection drags to extend values down / up / left / right. Single-cell drags tile-copy; two or more numeric cells extrapolate as an arithmetic progression (
1, 2 → 3, 4, 5); a single date-formatted cell increments by one day; formulas shift their relative references the way Excel does ($-anchored cells preserved). Styles, number formats, and cell types propagate. Undo restores prior values. - ▸ Excel-style formula reference editing. While editing a formula (cell starting with
=), clicking another cell inserts that cell's address at the caret; drag extends it into a range. Each unique reference is colored from a shared palette, and the grid draws a matching dashed rectangle around each referenced range. Highlight rectangles expand to enclose merged regions. - ▸ Enter starts cell editing. Mac-friendly default — Enter now begins edit mode (caret at end), matching Numbers and Excel for Mac. F2 still works.
- ▸ 109 built-in formula functions (up from 32). New:
VLOOKUP / HLOOKUP / INDEX / MATCH / XLOOKUP / XMATCH / ADDRESS; SUMIFS / COUNTIFS / AVERAGEIFS / MAXIFS / MINIFS / SWITCH; date TODAY / NOW / DATE / YEAR / MONTH / DAY / HOUR / MINUTE / SECOND / WEEKDAY / EDATE / EOMONTH / DAYS / DATEDIF; math SUMPRODUCT / CEILING / FLOOR / MROUND / MEDIAN / LARGE / SMALL / RANK / EXP / LN / LOG / LOG10 / SIGN / PI / RAND / RANDBETWEEN; trig SIN / COS / TAN / ASIN / ACOS / ATAN / ATAN2; text SEARCH / EXACT / PROPER / CHAR / CODE; info ROW / COLUMN / ROWS / COLUMNS. - ▸ Per-side border overrides in conditional formatting. CF rules can carry a
border payload that paints right / top / bottom / left edges on matching cells, overriding manual borders per side. Read / written through the xlsx <dxf><border> round-trip. - ▸ React / Vue worksheet events as component props. Both wrappers now expose idiomatic event props for selection, cell value, bulk cell, scroll, viewport size, and structure changes. No more imperative
worksheet.on*() subscriptions for the common cases. - ▸
reogrid-json I/O from the package main entry. Hosts persisting worksheets (e.g. ReoGrid Studio) no longer need to reach into deep paths — writeReoGridJson / readReoGridJson / parseReoGridJson and ReoGridJsonDocument / JsonWorksheet / JsonCell types are re-exported from both @reogrid/lite and @reogrid/pro.
+ Added
- ▸
ReogridOptions.autoFill (default true) and Worksheet.setAutoFillEnabled() to toggle the fill handle. - ▸
AutoFillAction and computeAutoFillValues() exposed for programmatic auto-fill. - ▸
Worksheet.setFormulaRefHighlights() / CanvasWorksheet.setFormulaRefHighlights() and a THEME.formulaRefColors palette shared between editor and grid renderer. - ▸ Conditional format
border payload (right / top / bottom / left, each with style + color), evaluated end-to-end through the engine, renderer, and xlsx writer. - ▸ React component event props:
onSelectionChange, onCellValueChange, onBulkCellValueChange, onScroll, onViewportSizeChange, onStructureChange (in addition to onReady). - ▸ Vue component emits the same set of events with typed payloads.
- ▸
@reogrid/lite and @reogrid/pro main entries now re-export writeReoGridJson, readReoGridJson, parseReoGridJson, plus the ReoGridJsonDocument / JsonWorksheet / JsonCell document types and the ReoGridJsonReadOptions / ReoGridJsonWriteOptions option types. - ▸ Formula function reference doc covering all 109 built-ins across 10 categories.
~ Changed
- ▸ Formula reference highlight in the editor expands to enclose merged regions when a referenced cell or range overlaps a merge — matches Excel rather than Google Sheets.
🐛 Fixed
- ▸ Auto-fill hover cursor is now applied on the scroll container as well, so the cross-hair cursor appears reliably across the entire fill-handle hit area.
- ▸ Render-cache key no longer contains an embedded NUL byte; it is replaced with
|, avoiding rare measurement-cache collisions and string-handling oddities.
⚡ Performance
- ▸ Per-frame canvas state cache cuts redundant API calls on Book1.xlsx scroll:
ctx.font= 129 → 34 (-74%), ctx.fillStyle= 179 → 46 (-74%), ctx.measureText() 126 → 35 (-72%), save()/restore() 122 → 85 (-30%). In real browsers, where ctx.font parsing and measureText shaping dominate per-cell cost, this translates directly to smoother scrolling on large sheets. - ▸
layoutPlainTextLines() output is cached per frame for wrapped / multi-line cells. Text-heavy sheets with wrap-text see a large reduction in per-frame work during scroll. Single-line unwrapped text takes a fast path that bypasses the cache.
Maintenance release focused on xlsx I/O fidelity and load performance, plus outline read/write parity with the .NET edition. No breaking public-API changes.
✨ Highlights
- ▸ xlsx import is ~40% faster on large files. A 1945×503 sheet with ~440k cells now loads in 3.7 s instead of 6.2 s.
- ▸ Optional chunked async load (
LoadXlsxOptions.chunked) for snappier UI on big files — first paint at ~40 ms instead of one multi-second freeze. Default remains synchronous. - ▸ xlsx outline round-trip. Outline level, hidden, collapsed state, and summary direction (above/below, left/right) now read and write, matching the .NET edition.
- ▸ ReoGrid JSON file format. Native lossless serialization covering cells, styles, number formats, rich text, merges, borders, sizes/visibility, freeze, conditional formats, outlines, filter, cell types, protection, and alternate rows.
- ▸ Outline summary direction.
setRowSummaryBelow / setColumnSummaryRight flip the toggle button to the leading edge of a group. The outline panel now reserves an "expand all" innermost slot, matching Excel.
+ Added
- ▸
LoadXlsxOptions.chunked — false (default, sync), true (chunked with default batch size), or { batchSize } for explicit control. await ws.loadFromFile(file, { chunked: true }). - ▸
Worksheet.loadCellsAsync(cells, { batchSize }) — chunked async variant that yields between batches and renders progressively. Default batch size 5000 cells. - ▸ ReoGrid JSON read/write APIs (writer + reader).
- ▸ xlsx read/write support for row and column outlines: level, hidden, collapsed, and summary direction.
- ▸
setRowSummaryBelow(value) / setColumnSummaryRight(value) on Worksheet, with undo/redo support. - ▸ "Expand all" outline level button (innermost slot of the outline panel).
- ▸
AutoFitMode option ('fit' | 'expandOnly' | 'shrinkOnly') on autoFitColumns, autoFitRows, and the RowHandle / ColumnHandle autoFit({ mode }) overloads. Default 'fit' preserves existing behavior. - ▸
value accepted as alias for value1 on conditional format cellIs rules. value2 remains required for between / notBetween. If both value and value1 are provided, value1 wins. - ▸ xlsx import now resolves theme colors and tint on cell fills.
- ▸ xlsx import now reads dxf fills expressed via
<bgColor> (in addition to <fgColor>), so conditional format fills written by Excel come through correctly. - ▸ Code runner demo page.
~ Changed
- ▸ xlsx import now applies outlines and conditional formats before the async cell ingest, so the first paint shows the correct collapsed state instead of expanding then snapping.
- ▸ Canvas font strings always include a generic
sans-serif fallback so the italic / bold prefix survives when the named family isn't installed (e.g. xlsx files referencing "Aptos Narrow" on machines without Office).
🐛 Fixed
- ▸ Borders no longer rendered for cells in hidden rows or columns. Previously, a left border on column A would still paint as a vertical line at the row-header boundary when columns A-J were hidden by a collapsed outline group. Matches .NET behavior.
- ▸ xlsx import: bold defined via cellXf/font on plain shared strings now renders correctly. Previously every plain shared string was flagged as rich text and the renderer ignored the cell-level style.
- ▸ Number format conditional sections (
[=0], [<>N], [<N], [<=N], [>N], [>=N]) now evaluate independent of value type. Format codes like [=0]"-";m/d/yy no longer always pick the first section. - ▸ Excel serial date epoch off-by-one corrected. Serial 45658 now resolves to 2025-01-01 as expected.
- ▸ xlsx import now resolves theme colors, tint, and dxf fills expressed via
<bgColor> — all common in Excel-saved files. - ▸ Multi-word font families (e.g. "Aptos Narrow") on the plain-text render path are no longer double-quoted, which previously caused Canvas 2D to silently drop the
italic / bold prefix. - ▸ Column header labels are now skipped for hidden columns. Collapsing an outline group no longer causes header letters to overprint at the next visible column.
- ▸ Outline toggle buttons and bracket lines for groups hidden by a collapsed parent are now skipped — the inner toggle no longer paints on top of the outer toggle row.
- ▸ Adding partially-overlapping outline groups now throws a clear error. Outline groups must form a strict tree; previously crossings produced visible-but-unclickable inner toggles.
- ▸ Outline corner-area level-button hit test now aligns with the renderer when both row and column outline panels are present.
- ▸ Malformed conditional format
cellIs rules (missing value1) no longer crash the render loop; the rule simply returns no-match. - ▸ Playground demo: opening an xlsx file on the first click now works (the toolbar previously held a stale
worksheet=null closure until a re-render).
⚡ Performance
- ▸ xlsx end-to-end load time on a 1945×503 / ~440k-cell test file: 6.2 s → 3.9 s (-37%) in node. Improvements span the parser, the cell-extraction walk, the worksheet bulk-load path, and the formula engine's initial rebuild.
- ▸ Lazy row auto-fit on import: only viewport-visible rows are measured up front, additional rows are measured once on first scroll into view. Skips ~1M useless cell visits on Book1-class sheets.
- ▸ Optional chunked ingest (
{ chunked: true }) collapses the user-visible "stuck" window from one multi-second freeze to 16-30 ms slices.
First stable release. The public API is now considered frozen under semver: any breaking change after 1.0.0 will require a major bump.
✨ Highlights
- ▸ Handle-based public API. All structural and styling operations go through chained handles (
worksheet.row(i), worksheet.column('B'), worksheet.range('A1:C5').setBold(), worksheet.selection.range?.setStyle(...)) instead of flat top-level methods. This is the API users should build against going forward. - ▸ Internal Manager layering.
Worksheet is now an entry point; state mutation lives in StyleOperations, SizingManager, VisibilityManager, and StructureManager. - ▸ Formula engine. Lexer → parser → evaluator → dependency graph + 32 built-in functions. Imported xlsx formulas are re-evaluated. 151 formula tests across
tests/formula.spec.ts and tests/formula-ext.spec.ts. - ▸ Pluggable cell types.
CellTypeHandler registry with 8 built-in types: checkbox, dropdown, button, progress, rating, sparkline (line / area), and hyperlink. - ▸ Conditional formatting (Pro).
addConditionalFormat, removeConditionalFormat, clearConditionalFormats, getConditionalFormats. - ▸ Cell tooltips (Pro).
setCellTooltip, showCellTooltip, hideCellTooltip, clearCellTooltip. - ▸ Browser printing (Pro).
printWorksheet(...) standalone function. In Lite this is a console.warn stub for upgrade prompting. - ▸ xlsx round-trip. Import/export with cross-validation against the .NET ReoGrid edition.
- ▸ Lite / Pro tier guards. Lite enforces 100 rows × 26 columns and stubs Pro-only methods at both the public-handle layer and the internal Manager chokepoint.
- ▸ React and Vue wrappers. Published as separate entry points:
@reogrid/lite/react, @reogrid/lite/vue, @reogrid/pro/react, @reogrid/pro/vue.
+ Added
- ▸
worksheet.reset(options?) — wipe the worksheet back to a blank state in-place. Clears values, styles, cell types, header dropdowns, conditional formats, cell protection, borders, merges, hidden rows/columns, outlines, frozen panes, custom row/column sizes, scroll, and selection; resets showGridLines to true. Optionally resizes the grid via options.rows / options.columns. CanvasWorksheet extends this to also clear images, the auto-filter, and tooltips. The xlsx importer now uses this as its single-shot pre-load reset. - ▸
worksheet.rows.setCount(count) and worksheet.columns.setCount(count) on RowCollection / ColumnCollection for single-axis resizing. - ▸
printWorksheet re-export from @reogrid/lite as a console.warn stub so the guard mechanism is uniform with PRO_METHODS. - ▸
PRO_MANAGER_METHODS + applyLiteWorksheetGuards close the Manager backdoor (structureManager.insertRows, visibilityManager.setRowHidden, …) so per-handle entry points cannot bypass tier guards. - ▸
yarn test:perf script and tests/**/*-benchmark.spec.ts naming convention so day-to-day yarn test stays fast while benchmarks remain opt-in. - ▸
ResizeObserver on the worksheet container so the canvas reacts to flex / sidebar / panel layout changes. - ▸
requestAnimationFrame-based render batching (Worksheet.scheduleRender). - ▸
bulkSetCells() API for fast bulk loading of large datasets. - ▸ Excel-style keyboard selection: anchor + focus model, Shift+click selection extension, direction-aware merge navigation, auto-scroll on keyboard / drag.
- ▸ Auto-expanding cell editor with grid-snapped resize.
- ▸
tsconfig.json stripInternal: true so @internal members no longer leak into published .d.ts.
~ Changed
- ▸ Breaking:
worksheet.setRowCount(n) and worksheet.setColumnCount(n) removed. Use worksheet.rows.setCount(n) / worksheet.columns.setCount(n), or worksheet.setGridSize(rows, cols) for atomic both-axes resize. - ▸ Breaking:
worksheet.rows / worksheet.columns now refer to the RowCollection / ColumnCollection handles. The numeric counts moved to worksheet.rowCount / worksheet.columnCount. - ▸ Breaking:
worksheet.selection is now a thin SelectionHandle exposing bounds / isEmpty / activeCell / range / moveTo only. Style and value operations on the current selection should go through worksheet.selection.range?.setBold() instead of delegators on the handle itself. - ▸ Breaking: Flat
setSelection*Bold/Italic/Style/... methods removed in favour of the handle API (worksheet.range(...).setBold()). - ▸
KeyboardController now listens on the worksheet container, not on window, so multiple grid instances on the same page do not cross-fire keyboard events. - ▸
CellEditor inline textarea now uses position: absolute relative to the worksheet container instead of position: fixed, fixing misalignment inside CSS transform ancestors, modal dialogs, and iframes. - ▸ Merged cell rendering now draws content from the topmost-leftmost still-visible cell when the merge anchor has scrolled out of view.
🐛 Fixed
- ▸
COUNT(1, 1/0, 2) and similar — errors in direct arguments now propagate (matches Excel semantics). Errors inside range arguments remain ignored. - ▸
#REF! correctly cached after row/column delete (was previously surfacing as #NAME? because EXCEL_ERROR_LITERALS was not consulted in the evaluator's name node). - ▸
bulkSetCells initial render path on big-data demo. - ▸ Editor backgrounds now respect cell
backgroundColor. - ▸ Various double-click and edit-commit edge cases in the pointer / editor interaction.
− Removed
- ▸ Legacy flat
setSelection* API surface (use the handle API). - ▸
worksheet.setRowCount / setColumnCount (use collection setCount).
! Known issues
- ▸
worksheet.columnWidths / rowHeights arrays are still mutable via JS array indexing (not enforced by readonly). - ▸ React
<Reogrid> ignores options prop changes after mount (use key to force remount). - ▸ Auto-fit is O(rows) per column.
- ▸
devicePixelRatio is read once per resize() and is not updated on monitor change.
For the raw changelog and earlier history, see the
@reogrid/lite
package on npm.