import type { Theme, IArea, Extent, CellValue, ICellAddress, ImportedSheetData, Color, CellStyle, IRectangle } from '../../../treb-base-types/src/index'; import { Area } from '../../../treb-base-types/src/index'; import type { StatsEntry } from './tab_bar'; import type { GridEvent, GridSelectionEvent } from './grid_events'; import type { SerializedNamed, DataModel, GridSelection, LegacySerializedSheet } from '../../../treb-data-model/src/index'; import { Annotation, type AnnotationData, Sheet } from '../../../treb-data-model/src/index'; import { FormulaBar } from '../editors/formula_bar'; import type { GridOptions } from './grid_options'; import { BorderConstants } from './border_constants'; import type { Command, FreezeCommand, InsertRowsCommand, InsertColumnsCommand, ActivateSheetCommand, ResizeRowsCommand, ResizeColumnsCommand, SelectCommand, CreateAnnotationCommand, RemoveAnnotationCommand } from './grid_command'; import { DOMContext } from '../../../treb-base-types/src/index'; import { GridBase } from './grid_base'; import type { SetRangeOptions } from './set_range_options'; import type { ExternalEditorConfig } from './external_editor_config'; import type { ClipboardData, PasteOptions } from './clipboard_data2'; export declare class Grid extends GridBase { hide_selection: boolean; headless: boolean; /** * we're tracking the current selected style so we can use it for new * cells. conceptually, if you are typing in a font (stack) like handwritten, * and you enter a new cell, you probably want to type in the same font * there. so we want to make that happen. TODO: size */ readonly edit_state: CellStyle; get scale(): number; set scale(value: number); /** * the theme object exists so we can pass it to constructors for * various components, but it's no longer initialized until the * initialization step (when we have a node). */ readonly theme: Theme; /** * this was private, which made sense, but there's a case where the * client (embedded sheet) wants to check if an annotation is selected. * we need to allow that somehow, ideally without any reacharounds. * * I guess the concern is a client could modify it, but at this point * we really only have one client and we trust it. making this public. * we could maybe switch to an accessor or have a "is this selected?" method. */ selected_annotation?: Annotation; private hover_data; /** are we editing? */ private editing_state; /** if we are editing, what is the cell? */ private editing_cell; /** */ private editing_selection; /** */ private editing_annotation?; /** */ private pending_reset_selection; /** */ private view_node?; /** containing element, passed in */ private container?; /** dom structure for grid */ private readonly layout; /** * this flag is used to lazily rebuild tiles. that prevents flashing if * there is a lot of work between a reset/document change and the next * paint call. */ private tile_update_pending; private scroll_offset_pending?; /** * for coediting/remotes, we may make a structural change that requires * a full repaint of another sheet -- we won't be able to do that until * the sheet is displayed. so let's keep track of these. * * these are numbers so we can use a sparse array. (or we could use a set...) */ private pending_layout_update; /** this is used when testing if a typed character is numeric */ private decimal_separator_code; /** new key capture overlay/ICE */ private overlay_editor?; /** moving autocomplete to a class field */ private autocomplete?; /** formula bar editor (optional) */ protected formula_bar?: FormulaBar; private RESIZE_PIXEL_BUFFER; /** * formalizing the concept of external selection to support outside tooling. * * FIXME: stop testing on this field. we need a better way to figure out * if the external editor is active. */ private external_editor_config?; /** * support for external editor. created on demand. */ private external_editor?; /** * flag indicating we're resizing, or hovering over a resize. * we use this so we know what to do when we see a click on the headers. */ private cell_resize; /** * this is the only thing that was used in the old 'render state', * so we dropped the container. */ private render_tiles; /** * the main selection for interacting with the spreadsheet */ protected readonly primary_selection: GridSelection; /** reusing type. FIXME? we don't need a target */ protected readonly spill_selection: GridSelection; /** * active selection when selecting arguments (while editing) */ protected readonly active_selection: GridSelection; /** * this flag is for testing if we were previously in the nub */ private nub_select_flag; /** * additional selections that are rendered but not otherwise used. * this array is now readonly, so we can bind it to the selection * renderer (we do this with the primary selection as well) */ private readonly additional_selections; /** * state data for handling double-clicks. because we're using * mousedown, we need to synthesize double-clicks with a timeout. */ private double_click_data; /** * window timer/request animation frame token. this is used to prevent * excess repaints on scroll. FIXME: can this merge with the delayed * render token? I think originally it was separate because that other * method was too slow for scrolling... maybe... */ private layout_token; /** window timer/request animation frame token */ private render_token; /** */ private tile_renderer?; /** */ private selection_renderer?; private tab_bar?; private DOM; /** * FIXME: NO PARAMETER INITIALIZATIONS */ constructor(options: GridOptions | undefined, model: DataModel, theme: Theme | undefined, initialze_dom: boolean | undefined, DOM: DOMContext); /** * internal composite for cut/copy. mostly identical except we * read data as A1 for cut, so it will retain references. also * cut clears the data. * * FIXME: merge with grid cut/copy/paste routines. we already * handle recycling and relative addressing, the only thing missing * is alternate formats. */ CopyArea(resolved: Area, semantics?: 'cut' | 'copy'): ClipboardData; /** * paste clipboard data into a target range. this method does not use * the system clipboard; pass in clipboard data returned from the Cut or * Copy method. * * @param target - the target to paste data into. this can be larger * than the clipboard data, in which case values will be recycled in * blocks. if the target is smaller than the source data, we will expand * it to fit the data. * * @param data - clipboard data to paste. * * @privateRemarks LLM API * * @privateRemarks this was async when we were thinking of using the * system clipboard, but that's pretty broken so we're not going to * bother atm. */ PasteArea(resolved: Area, data: ClipboardData, options?: PasteOptions): void; /** * re-select the current selection, for side effects. used when switching * languages to update the function view (if any) */ Reselect(): void; /** * set note at the given address, or current selection * @param address optional address; if not used, note will be set/cleared * at current selection * @param note new note, or undefined to clear note */ SetNote(address?: ICellAddress, note?: string): void; /** find an annotation, given a node */ FindAnnotation(node: HTMLElement): Annotation | undefined; CreateAnnotation(properties?: Partial, sheet?: Sheet, add_to_sheet?: boolean, offset?: boolean, target?: IArea | IRectangle, focus?: boolean): void; /** * create an annotation, with properties, without an original object. * optionally (and by default) add to sheet. * * @param offset check for a matching position (top-left) and if found, * shift by (X) pixels. intended for copy-paste, where we don't want to * paste immediately on top of the original. * * @param target new parameter allows setting annotation as rect or as * cell range */ protected CreateAnnotationInternal(command: CreateAnnotationCommand): void; /** placeholder */ UpdateAnnotationLayout(): void; AnnotationZOrder(annotation: Annotation, operation: number | 'top' | 'bottom'): void; /** add an annotation. it will be returned with a usable node. */ AddAnnotation(annotation: Annotation, toll_events?: boolean, add_to_layout?: boolean): void; /** * call this method if an annotation is updated externally -- we're not * watching for mutation, so if they change and we need to update, we * won't know about it unless you tell us. sometimes this will be * superfluous but (assuming it's one at a time) should not be too expensive */ AnnotationUpdated(annotation: Annotation): void; /** * removes an annotation from the list, and removes the node its * the parent (although the node still exists in the annotation, if * it existed before). */ RemoveAnnotation(annotation: Annotation, sheet?: Sheet): void; protected RemoveAnnotationInternal(command: RemoveAnnotationCommand): void; /** * this method removes annotation nodes from the grid/layout, but doesn't * affect the underlying data. this should be used to remove annotations * when switching sheets. * * you can also use it when cleaning up, if the underlying data will also * be wiped from the model. */ RemoveAnnotationNodes(): void; /** * show or hide headers * * FIXME: this shouldn't be sent if the current value === the desired value * * FIXME: this is a display option. I'm not sure it should go through the * command queue, because it's a local choice. leaving for now, but FIXME */ ShowHeaders(show?: boolean): void; /** * this method is called after an XLSX import. * * FIXME: should this not be in base? (...) * * I suppose it's specific to an environment that _could_ import XLSX, * so it's not strictly speaking generic. */ FromImportData(import_data: { sheets: ImportedSheetData[]; named?: SerializedNamed[]; active_tab?: number; }, render?: boolean): void; /** * This function is called via Shift+PageUp/PageDown. We need * to update to account for hidden sheets, which can't be activated. */ NextSheet(step?: number, user?: boolean): void; /** * UpdateSheets means "set these as the sheets, drop any old stuff". * there's an implicit reset (in fact we may do that twice in some * cases). * */ UpdateSheets(data: LegacySerializedSheet[], render?: boolean, activate_sheet?: number | string): void; /** * rebuild layout on a resize. we are not trapping resize events, clients * should do that (also this works for embedded elements that are not * directly affected by document resize). */ UpdateLayout(): void; /** * we need this method for split views */ UpdateTabBar(): void; /** * @param initial first call, from the grid Initialize() method */ UpdateTheme(initial?: boolean, additional_properties?: Partial): void; /** set scale directly */ SetScale(scale: number): void; /** * @param container html container element */ Initialize(view_node: HTMLElement, toll_initial_render?: boolean): void; /** * merge target area or selection */ MergeCells(area?: Area): void; /** * unmerge cells */ UnmergeCells(area?: Area): void; /** * */ GetAnnotationStyle(): CellStyle | undefined; /** * */ ApplyAnnotationStyle(style?: CellStyle, delta?: boolean): void; /** * */ AnnotationSelected(): boolean; /** * focus on the container. not sure what that text parameter was, * legacy? TODO: remove */ Focus(text?: string, click?: boolean): void; /** * set "data validation", which (atm) only supports a list of options * and will render as a dropdown; the list can be a list of values or * a range reference. */ SetValidation(target?: IArea, data?: CellValue[] | IArea, error?: boolean): void; /** * set or clear name. optionally overwrite existing name. note that * you cannot overwrite function names, only existing named ranges/expressions. * * set name to refer to a range (named range) or expression/value (named * expression). range has priority if you set both (FIXME: make that impossible) * * note that expression here must be a valid expression in SL. if you want * to set a literal string, enclose it in double quotes (as you would when * using a string as an argument to a function). */ SetName(name: string, range?: ICellAddress | Area, expression?: string, scope?: number, overwrite?: boolean): void; /** * this version selects blocks first, if the block is already selected * then it selects everything */ SelectAll2(): void; SelectAll(): void; /** * set or remove external editor. this is not an accessor because I don't * want to have to use a duplicate internal field, it's clumsy and this * class isn't public-facing so it's not super important. */ ExternalEditor(config: Partial | undefined): void; /** API method */ SelectRange(range?: Area): void; /** * I can't figure out a way in typescript to overload the GetRange function * but call it with a variable to determine the overload (the aim is to have * different return types). * * it seems to work fine if we use static values for the parameter (type, * below) but not a variable which is (theoretically) type-restricted to the * same values. One to work on, maybe. * * @param range * @returns */ GetRangeStyle(range: ICellAddress | IArea, apply_theme?: boolean): CellStyle | CellStyle[][] | undefined; /** * render the given data as R1C1. the source address is a base for * rendering relative addresses (is there a case for this method * handling absolute offsets as well? ...) * * NOTE: this method modifies the input data (at least it does if * it's an array). so this method should only be called with scratchpad * data. * */ FormatR1C1(data: CellValue | CellValue[][], source: ICellAddress | IArea): CellValue[][]; /** * get data in a given range, optionally formulas * API method */ GetRange(range: ICellAddress | IArea, type?: 'formatted' | 'A1' | 'R1C1'): CellValue | CellValue[][] | undefined; /** * set data in given range * API method * * not sure why we have support for ArrayBufferView in here. this is an API * method, called by the embed sheet's API method, so there are no particular * requirements. we should lock down the allowable types. * * @param range target range. if range is smaller than data, range controls. * if range is larger, behavior depends on the recycle parameter. * @param data single value, array (column), or 2d array * @param recycle recycle values. we only recycle single values or single * rows/columns -- we will not recycle a matrix. * @param transpose transpose before inserting (data is column-major) * @param r1c1 - support R1C1 notation. this does not mean the data _is_ in * R1C1, just that we need to check for it and handle it. R1C1 is useful for * relative offsets. */ SetRange(range: Area, data: CellValue | CellValue[] | CellValue[][], options?: SetRangeOptions): void; /** * applies the given style properties to the passed array, or to the * current primary selection * * API method */ ApplyStyle(area?: Area, properties?: CellStyle, delta?: boolean): void; /** * returns the primary selection. we use a reference to the real selection * sp callers can track; however, you can break things if you modify it. * so don't modify it. FIXME: proxy view? (...) * * API method */ GetSelection(): GridSelection; /** repaint after an external event (calculation) */ Update(force?: boolean, area?: IArea | IArea[]): void; /** * update annotation layouts (not content) */ UpdateAnnotations(): void; Indent(area?: Area, delta?: number): void; /** updated API method, probably change the name */ ApplyBorders2(area?: Area, borders?: BorderConstants, color?: Color, width?: number): void; /** * batch updates. returns all the events that _would_ have been sent. * also does a paint (can disable). * * update for nesting/stacking. we won't return events until the last * batch is complete. paint will similarly toll until the last batch * is complete, and we'll carry forward any paint requirement from * inner batch funcs. * * @param func */ Batch(func: () => void, paint?: boolean): GridEvent[]; /** * scrolls so that the given cell is in the top-left (assuming that is * possible) */ ScrollTo(address: ICellAddress, x?: boolean, y?: boolean, smooth?: boolean): void; /** * scrolls the given address into view (assuming it's not in view now) * * FIXME: we need a way to do this without scrolling the containing * page, in the event we do a scroll-on-load. small problem. */ ScrollIntoView(address: ICellAddress, smooth?: boolean): void; GetScrollOffset(): { x: number; y: number; }; /** * get/set the raw scroll offset (ignoring headers). support for API method. * @param offset */ ScrollOffset(offset?: { x: number; y: number; }): { x: number; y: number; } | undefined; protected RenameSheetInternal(target: Sheet, name: string): void; private StyleDefaultFromTheme; private AutoSizeRow; private AutoSizeColumn; /** * we have to handle the case where we have a split view and the model * changes in some way -- remove a sheet, for example -- that invalidates * our active sheet. if that happens, we need to switch to a different * sheet. * * we don't get events about this (should we?) so someone will have to * call it. FIXME: we should get events about that. */ EnsureActiveSheet(force?: boolean): void; /** * this is intended to synchronize views when annotations are added/removed. * there should be no need to call it if there's only one view. */ RefreshAnnotations(): void; /** * specialization for grid. note that we don't call superclass, * so we need to do everything that method does as well. */ protected ActivateSheetInternal(command: ActivateSheetCommand): void; /** * handle any tasks the sheet needs to do on activation */ private ActivateSheetTasks; /** * why is this not in layout? (...) * how is this layout? it's an effect. make an effects class. */ private HighlightFreezeArea; /** * layout has changed, and needs update. we clear the rectangle cache * immediately, to prevent any garbage, but we don't actually do the layout * until the next paint. * * FIXME: that makes no sense -- because rectangles will be calculated * incorrectly until the layout has been updated. */ private QueueLayoutUpdate; private HandleAddressLabelEvent; private InitFormulaBar; private InitOverlayEditor; private DelayedRender; protected Repaint(force?: boolean, full_tile?: boolean, force_headers?: boolean): void; private MouseMove_RowHeader; private MouseMove_ColumnHeader; /** * handler for mousedown events on the row (left) header. * handles selection and resizing. * * FIXME: argument selection */ private MouseDown_RowHeader; /** * handler for mousedown events on the column (top) header. * handles selection and resizing. * * FIXME: argument selection */ private MouseDown_ColumnHeader; private HoverCell; protected HideHoverInfo(): void; /** * grid move handler for hit-testing various areas */ private MouseMove_Grid; /** * special case: we don't want the previous click to be treated * as part of a double-click. */ private ClearDoubleClick; /** * unifying double-click. pass the test address. returns true if this looks * like a double-click on that address. otherwise sets flags to capture the * next one. * * FIXME: parameterize timeout? */ private IsDoubleClick; private PointInTextPart; /** * handles mouse down events on the grid area: * selection (click-drag) and editing (double-click) */ private MouseDown_Grid; /** * FIXME: -> util library * @param arr */ private Transpose; /** * when you drag from the nub, we copy the contents of the original * selection into the new selection, but there are some special recycling * rules. * * FIXME: expand tables */ private RecycleNubArea; protected CreateTypedReferences(start: ICellAddress, end?: ICellAddress): string[]; private UpdateSelectedArgument; /** * unified method to check if we are selecting an argument in the formula * bar editor or the in-cell editor * * FIXME: why is this not an accessor? */ protected SelectingArgument(): boolean; /** * for external clients. the expected pattern is * - start overlay editor * - click on "insert function" * - do something * - release editor (this function) */ ReleaseOverlayEditor(): void; RestoreOverlayEditor(): void; /** * consolidated event handler for overlay, which both handles grid keys * and acts as the ICE, depending on state. going to be a little tricky * to keep track of code paths. * * old comment: * * event handler for keyboard events. some we handle directly (directional * navigation), some we ignore (most control-key combinations), and if you * type text we start the in-cell editor and pass on the event. * * which is largely still true, except that we can handle the ICE more * directly. * */ private OverlayKeyDown; /** * select the array containing the current cell, if any. if there's no * array, do nothing. updated to support selecting tables as well as arrays. */ protected SelectArrayOrTable(target_selection: GridSelection): void; /** * render selections. we are wrapping this up in a method so we can * hide the primary selection in some cases (one case). */ protected RenderSelections(rerender?: boolean): void; /** * select a block. returns true if we've handled it; returns false * if we want to revert to the standard behavior. * * (block selection refers to selecting more than one cell at once, * using ctrl+arrow. selection jumps across all populated cells in * a given direction for a given row/column). */ protected BlockSelection(selection: GridSelection, expand_selection: boolean, columns: number, rows: number, render?: boolean): boolean; /** * deletes (clears) the selected area. * * FIXME: should probably be an API method, or have a second method that * clears the primary selection */ private DeleteSelection; /** * sets cell value, inferring type and (possibly) inferring cell style * (numbers only), so that 10% and $1,000 retain styles. this should only * be used for direct editing -- copy and paste can copy and paste styles. * * @param address cell address * * @param value value entered, usually this will be a string (we will try * to parse numbers/booleans) * * in what case would these not be strings? (...) not sure that's a thing. * * * @param exec execute commands immediately; alternatively, return the list * of commands. the former is the default for editor commits; the latter * is used for paste. */ protected SetInferredType(selection: GridSelection, value: string | undefined, array?: boolean, exec?: boolean, apply_style?: CellStyle): Command[] | undefined; /** * tries to correct some basic formula errors, opening with a space, * not closing all parens, (...) */ private FixFormula; /** * normalize addresses (UC), function names (-> canonical) and * defined names (UC, for now) */ private NormalizeFormula; /** * dismisses the in-cell editor and returns to normal behavior. * removes any highlighted selections (arguments). */ private DismissEditor; /** * this prepares the cell value for _editing_ -- it's not the displayed * value, it's how we want the value to be displayed in the editor and * formula bar. * * NOTE: is this the spot to do translation -> local language? (...) * */ private NormalizeCellValue; /** * this is used to handle a trailing % sign when entering a new value. * we need to decide if the user is typing a number, in which case we * retain the %; or something else, like a formula or a string, in which * case we want to drop the %. * * FIXME: move to utils lib */ private IsNumeric; /** * start the ICE, using the new overlay editor * * old comment: * * starts the in-cell editor at the given sheet address. this method doesn't * handle scroll-into-view, do that first if necessary. * * @param address the cell address. we'll check for merge head before editing. * @param flush flush contents before editing -- default for typing, !default * for double-click * @param event if this is triggered by typing, we want to pass the key * event directly to the editor (actually we'll pass a synthetic copy) * * @param selection * @param flush * @param event */ private OverlayEditCell; private BoundAddressArea; /** * step from row X to row (X+Y). Y can be negative. we add this method to * support stepping across hidden (0-height) rows, so if you down arrow * it doesn't get stuck inside the hidden row (bad UX). * * UPDATE: no longer clamping. various methods rely on tests for < 0, so * we can't change that behavior. [FIXME: return an underflow flag?]. just * treat negative rows as not hidden, return the result. */ private StepVisibleRows; /** * step columns. * @see StepVisibleRows */ private StepVisibleColumns; /** * if the address is outside of current extent, expand */ protected EnsureAddress(address: ICellAddress, step?: number, toll_layout?: boolean): boolean; /** * advances selection by x rows and columns. you can also step around * within a selection, generally by using enter and tab when there is * a multi-cell selection. * * not sure why this function renders by default, but we don't want that * in the case of closing the ICE. since normal operation may rely on this * behavior, at least for now, we'll make a parameter to disable. but it * should probably not render. */ protected AdvanceSelection(delta: Extent, selection: GridSelection, within_selection?: boolean, expand_selection?: boolean, render?: boolean): void; /** highlight formula dependencies */ private HighlightDependencies; /** remove all additonla (argument) selections */ private ClearAdditionalSelections; /** * utility method, internally calls Select with an undefined area */ protected ClearSelection(selection: GridSelection, reason?: GridSelectionEvent['reason']): void; private HideGridSelection; private ShowGridSelection; /** * updates a selection, and handles marking headers as dirty * for subsequent renders (including any old selection). * * if the selection contains part of a merge area, it will be expanded * to include the full merge area (because you can't select part of a merge). * * @param area selection area * @param target optional selection target. if no target is passed and * the preseve_target field is not set, target will be set to the first * cell of the selection area * @param preserve_target preserve existing selection target */ protected Select(selection: GridSelection, area?: Area, target?: ICellAddress, preserve_target?: boolean, reason?: GridSelectionEvent['reason']): void; /** * */ protected UpdateFormulaBarFormula(override?: string): void; /** * splitting the stats update call and the rendering method, * if we want to make a pluggable stats method. * * default will show count/sum/average of numbers, either real * or complex. */ protected RenderStats(values: CellValue | CellValue[][]): StatsEntry[]; UpdateStats(): void; protected UpdateAddressLabelArea(selection: GridSelection): void; protected UpdateAddressLabel(selection?: GridSelection, text?: string): void; private OnDropdownSelect; private OnScroll; private AttachListeners; private HandleCopy; private HandleCut; private RecyclePasteAreas; private HandlePaste; /** * utility method returns all selections (primary, active and any additional * selections) as an array. defaults to non-empty selections only, but * can optionally returns empty non-null selections. */ private AllSelections; /** * UI method for inserting rows; updates layout */ protected InsertRowsInternal(command: InsertRowsCommand): { error?: boolean; update_annotations_list?: Annotation[]; resize_annotations_list?: Annotation[]; delete_annotations_list?: Annotation[]; }; /** * UI method for inserting columns; updates layout */ protected InsertColumnsInternal(command: InsertColumnsCommand): { error?: boolean; update_annotations_list?: Annotation[]; resize_annotations_list?: Annotation[]; delete_annotations_list?: Annotation[]; }; /** * specialization */ protected ResetInternal(): void; /** * overload: see ResizeRowsInternal */ protected ResizeColumnsInternal(command: ResizeColumnsCommand): void; Screenshot({ type, quality, download }: { type?: 'png' | 'webp' | 'jpeg'; quality?: number; download?: boolean; }): Promise; /** * UI grid supports scale and auto-size, so we're overloading. * @param command */ protected ResizeRowsInternal(command: ResizeRowsCommand): IArea | undefined; /** * specialization */ protected SelectInternal(command: SelectCommand): void; /** * specialization. all the base class method does is set the sheet * fields, so we don't need to call it, we can do that. * */ protected FreezeInternal(command: FreezeCommand): void; ExecCommand(commands: Command | Command[], queue?: boolean): import("./update_flags").UpdateFlags; }