├── docs ├── _includes │ ├── _topnav.html │ ├── _layout_head.html │ ├── _layout_footer.html │ ├── _example_about.html │ ├── _layout_js.html │ ├── _layout_meta.html │ ├── _example_controls.html │ ├── _example_demonstrates.html │ ├── _example_sources.html │ └── _layout_css.html ├── _data │ └── globals.json ├── favicon.ico ├── _config_local.yml ├── README.md ├── index.md ├── _layouts │ ├── default.html │ └── layout-classic-examples.html ├── assets │ ├── style.css │ └── examples-classic.css ├── _config.yml ├── examples │ └── classic │ │ ├── example01-simple.html │ │ ├── example95-plugin-autotooltips.html │ │ ├── example97-frozen-columns.html │ │ ├── example03-events.html │ │ ├── example06-editing-with-undo.html │ │ ├── example05-editing.html │ │ ├── example02-formatters.html │ │ ├── example04-highlighting.html │ │ ├── example07-compound-editors.html │ │ └── example08-modal-editor-form.html └── _plugins │ └── cachebust.rb ├── src ├── core │ ├── viewrange.ts │ ├── base.ts │ ├── selection-model.ts │ ├── viewportinfo.ts │ ├── grid-plugin.ts │ ├── index.ts │ ├── grid-signals.ts │ ├── cellnavigation.ts │ ├── idataview.ts │ ├── cellrange.ts │ ├── eventargs.ts │ ├── group.ts │ ├── util.tsx │ ├── column.ts │ ├── editing.ts │ ├── draggable.ts │ └── formatting.ts ├── grid │ ├── index.ts │ ├── types.ts │ ├── render-args.ts │ ├── eventargs.ts │ ├── event-utils.ts │ ├── internal.tsx │ ├── render-row.ts │ ├── render-cell.ts │ ├── layout.ts │ ├── column-sorting.tsx │ ├── tree-columns.ts │ └── draggable.ts ├── editors │ └── index.ts ├── layouts │ ├── layout-host.ts │ ├── layout-engine.ts │ ├── basic-layout.tsx │ ├── frozen-layout.tsx │ └── layout-components.tsx ├── index.ts ├── formatters │ ├── index.ts │ └── formatters.ts └── plugins │ ├── autotooltips.ts │ ├── rowmovemanager.ts │ └── rowselectionmodel.ts ├── vitest.config.ts ├── test ├── tsconfig.json ├── core │ ├── base.spec.ts │ ├── group.spec.ts │ ├── range.spec.ts │ ├── column.spec.ts │ └── editlock.spec.ts ├── mocks │ ├── mock-signal.ts │ └── mock-layout-host.ts ├── grid │ ├── internal.spec.ts │ ├── grid.plugin.spec.ts │ ├── grid.selectionModel.spec.ts │ ├── basiclayout.spec.ts │ └── frozenlayout.spec.ts ├── layouts │ └── basic-layout.spec.tsx └── plugins │ └── autotooltips.spec.ts ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── Serenity.SleekGrid.csproj ├── LICENSE ├── README.md └── package.json /docs/_includes/_topnav.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/_data/globals.json: -------------------------------------------------------------------------------- 1 | { 2 | } 3 | -------------------------------------------------------------------------------- /docs/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serenity-is/sleekgrid/HEAD/docs/favicon.ico -------------------------------------------------------------------------------- /docs/_config_local.yml: -------------------------------------------------------------------------------- 1 | local: true 2 | packageurl: 3 | sleekgrid: 4 | '/sleekgrid/assets/local' 5 | -------------------------------------------------------------------------------- /docs/_includes/_layout_head.html: -------------------------------------------------------------------------------- 1 | {% include _layout_meta.html %} 2 | {{ page.title }} 3 | {% include _layout_css.html %} 4 | -------------------------------------------------------------------------------- /docs/_includes/_layout_footer.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/core/viewrange.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ViewRange { 3 | top?: number; 4 | bottom?: number; 5 | leftPx?: number; 6 | rightPx?: number; 7 | } 8 | -------------------------------------------------------------------------------- /docs/_includes/_example_about.html: -------------------------------------------------------------------------------- 1 | {% if page.about %} 2 |
3 |

About:

4 |
5 | {{ page.about }} 6 |
7 |
8 | {% endif %} 9 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | 3 | export default defineConfig({ 4 | test: { 5 | name: "sleekgrid", 6 | environment: "jsdom", 7 | globals: true 8 | } 9 | }) 10 | -------------------------------------------------------------------------------- /docs/_includes/_layout_js.html: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # SleekGrid Docs 2 | 3 | This folder contains SleekGrid website prepared in a format suitable for Github Pages / Jekyll static site generator. 4 | 5 | Please visit https://serenity-is.github.io/sleekgrid/ to see the documentation and examples 6 | -------------------------------------------------------------------------------- /docs/_includes/_layout_meta.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/core/base.ts: -------------------------------------------------------------------------------- 1 | /*** 2 | * A base class that all special / non-data rows (like Group and GroupTotals) derive from. 3 | */ 4 | export class NonDataRow { 5 | __nonDataRow: boolean = true; 6 | } 7 | 8 | export const preClickClassName = "slick-edit-preclick"; 9 | -------------------------------------------------------------------------------- /docs/_includes/_example_controls.html: -------------------------------------------------------------------------------- 1 | {% if page.controls %} 2 |
3 |

Controls:

4 |
5 | {% for c in page.controls %} 6 | {{ c }} 7 | {% endfor %} 8 |
9 |
10 | {% endif %} 11 | -------------------------------------------------------------------------------- /docs/_includes/_example_demonstrates.html: -------------------------------------------------------------------------------- 1 | {% if page.demonstrates %} 2 |
3 |

Demonstrates:

4 | 9 |
10 | {% endif %} 11 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: SleekGrid Home 3 | --- 4 | 5 | ### Classic Samples (to check backward compatibility) 6 | 7 | {% assign classic_examples_pages = site.pages | where: "dir", "/examples/classic/" %} 8 | 9 | {% for p in classic_examples_pages %} 10 | - [{{ p.title }}]({{ site.baseurl }}{{ p.url | replace: ".html", "" }}){% endfor %} 11 | -------------------------------------------------------------------------------- /src/core/selection-model.ts: -------------------------------------------------------------------------------- 1 | import type { CellRange, EventEmitter } from "."; 2 | import type { GridPlugin } from "./grid-plugin"; 3 | 4 | export interface SelectionModel extends GridPlugin { 5 | setSelectedRanges(ranges: CellRange[]): void; 6 | onSelectedRangesChanged: EventEmitter; 7 | refreshSelections?(): void; 8 | } 9 | 10 | -------------------------------------------------------------------------------- /test/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "noImplicitAny": false, 5 | "declaration": false, 6 | "skipLibCheck": true, 7 | "sourceMap": true, 8 | "types": [ 9 | "vitest/globals" 10 | ] 11 | }, 12 | "include": [ 13 | "." 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.css] 12 | indent_size = 2; 13 | 14 | [{*.json}] 15 | indent_size = 2 16 | insert_final_newline = false 17 | 18 | [*.md] 19 | trim_trailing_whitespace = false 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vs/ 2 | _site 3 | obj/ 4 | bin/ 5 | /artifacts 6 | /coverage 7 | /dist/**/*.js 8 | /dist/**/*.map 9 | /dist/**/*.min.js 10 | /wwwroot/**/*.js 11 | /wwwroot/**/*.map 12 | /wwwroot/**/*.min.js 13 | .jekyll-* 14 | GemFile* 15 | node_modules/ 16 | /docs/assets/local 17 | *.bak 18 | *.orig 19 | *.log 20 | *.tgz 21 | 22 | /out/typedoc.json 23 | 24 | *.tsbuildinfo 25 | -------------------------------------------------------------------------------- /docs/_layouts/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {% include _layout_head.html %} 6 | 7 | 8 | 9 |
10 |

{{ page.title }}

11 | {{ content }} 12 | {% include _layout_footer.html %} 13 |
14 | {% include _layout_js.html %} 15 | 16 | -------------------------------------------------------------------------------- /src/grid/index.ts: -------------------------------------------------------------------------------- 1 | export * from "../layouts/basic-layout"; 2 | export * from "../layouts/frozen-layout"; 3 | export * from "../layouts/layout-components"; 4 | export type { LayoutEngine } from "../layouts/layout-engine"; 5 | export type { LayoutHost } from "../layouts/layout-host"; 6 | export type { BandKey, GridBandRefs, GridLayoutRefs, PaneKey } from "../layouts/layout-refs"; 7 | export * from "./sleekgrid"; 8 | 9 | -------------------------------------------------------------------------------- /docs/assets/style.css: -------------------------------------------------------------------------------- 1 | :root { 2 | font-size: 14px; 3 | } 4 | 5 | footer { 6 | color: #ccc; 7 | margin-top: 2rem; 8 | } 9 | 10 | body { 11 | padding: 1rem; 12 | } 13 | 14 | .page-title { 15 | margin-bottom: 1.5rem; 16 | font-size: 2rem; 17 | } 18 | 19 | .active-link { 20 | font-weight: bold; 21 | } 22 | 23 | .example-sidenav li { 24 | padding: 3px 0; 25 | } 26 | 27 | #myGrid { 28 | min-height: 450px; 29 | } 30 | -------------------------------------------------------------------------------- /src/core/viewportinfo.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface ViewportInfo { 3 | height: number; 4 | width: number; 5 | hasVScroll: boolean; 6 | hasHScroll: boolean; 7 | headerHeight: number; 8 | groupingPanelHeight: number; 9 | virtualHeight: number; 10 | realScrollHeight: number; 11 | topPanelHeight: number; 12 | headerRowHeight: number; 13 | footerRowHeight: number; 14 | numVisibleRows: number; 15 | } 16 | -------------------------------------------------------------------------------- /src/grid/types.ts: -------------------------------------------------------------------------------- 1 | import type { CellRange, EventEmitter } from "../core"; 2 | import type { Grid } from "./grid"; 3 | 4 | export interface IPlugin { 5 | init(grid: Grid): void; 6 | pluginName?: string; 7 | destroy?: () => void; 8 | } 9 | 10 | export interface SelectionModel extends IPlugin { 11 | setSelectedRanges(ranges: CellRange[]): void; 12 | onSelectedRangesChanged: EventEmitter; 13 | refreshSelections?(): void; 14 | } 15 | 16 | -------------------------------------------------------------------------------- /docs/_includes/_example_sources.html: -------------------------------------------------------------------------------- 1 |
2 |

View Source:

3 | 9 |
10 | -------------------------------------------------------------------------------- /src/core/grid-plugin.ts: -------------------------------------------------------------------------------- 1 | import type { ISleekGrid } from "./isleekgrid"; 2 | 3 | export interface GridPlugin { 4 | init(grid: ISleekGrid): void; 5 | pluginName?: string; 6 | destroy?: () => void; 7 | } 8 | 9 | /** @deprecated Use GridPlugin instead */ 10 | export interface IPlugin extends GridPlugin { 11 | 12 | } 13 | 14 | export interface GridPluginHost { 15 | getPluginByName(name: string): GridPlugin; 16 | registerPlugin(plugin: GridPlugin): void; 17 | unregisterPlugin(plugin: GridPlugin): void; 18 | } 19 | -------------------------------------------------------------------------------- /docs/_includes/_layout_css.html: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /test/core/base.spec.ts: -------------------------------------------------------------------------------- 1 | import { NonDataRow, preClickClassName } from "../../src/core/base"; 2 | 3 | describe('NonDataRow', () => { 4 | it('should set __nonDataRow to true', () => { 5 | const nonDataRow = new NonDataRow(); 6 | 7 | expect(nonDataRow.__nonDataRow).toBeTruthy(); 8 | }); 9 | }); 10 | 11 | it('exports not null or undefined preClickClassName longer than zero characters', () => { 12 | expect(preClickClassName).toBeDefined(); 13 | expect(preClickClassName).not.toBeNull(); 14 | 15 | expect(preClickClassName.length).toBeGreaterThan(0); 16 | }); 17 | -------------------------------------------------------------------------------- /src/core/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./base"; 2 | export * from "./cellnavigation"; 3 | export * from "./cellrange"; 4 | export * from "./column"; 5 | export * from "./editing"; 6 | export * from "./event"; 7 | export * from "./eventargs"; 8 | export * from "./formatting"; 9 | export * from "./grid-plugin"; 10 | export * from "./grid-signals"; 11 | export * from "./gridoptions"; 12 | export * from "./group"; 13 | export * from "./idataview"; 14 | export * from "./isleekgrid"; 15 | export * from "./selection-model"; 16 | export * from "./util"; 17 | export * from "./viewportinfo"; 18 | export * from "./viewrange"; 19 | 20 | -------------------------------------------------------------------------------- /src/editors/index.ts: -------------------------------------------------------------------------------- 1 | import { CheckboxCellEdit, DateCellEdit, FloatCellEdit, IntegerCellEdit, LongTextCellEdit, PercentCompleteCellEdit, TextCellEdit, YesNoSelectCellEdit } from "./editors"; 2 | 3 | export * from "./editors"; 4 | 5 | export namespace Editors { 6 | export const Text = TextCellEdit; 7 | export const Integer = IntegerCellEdit; 8 | export const Float = FloatCellEdit; 9 | export const Date = DateCellEdit; 10 | export const YesNoSelect = YesNoSelectCellEdit; 11 | export const Checkbox = CheckboxCellEdit; 12 | export const PercentComplete = PercentCompleteCellEdit; 13 | export const LongText = LongTextCellEdit; 14 | } 15 | -------------------------------------------------------------------------------- /src/layouts/layout-host.ts: -------------------------------------------------------------------------------- 1 | import type { GridPluginHost } from "../core/grid-plugin"; 2 | import type { GridSignals } from "../core/grid-signals"; 3 | import type { ISleekGrid } from "../core/isleekgrid"; 4 | import { ViewportInfo } from "../core/viewportinfo"; 5 | import type { GridLayoutRefs } from "./layout-refs"; 6 | 7 | export interface LayoutHost extends Pick, GridPluginHost { 9 | getSignals(): GridSignals; 10 | getViewportInfo(): ViewportInfo; 11 | removeNode(node: HTMLElement): void; 12 | readonly refs: GridLayoutRefs; 13 | } 14 | -------------------------------------------------------------------------------- /src/core/grid-signals.ts: -------------------------------------------------------------------------------- 1 | import type { Computed, Signal } from "@serenity-is/domwise"; 2 | 3 | export interface GridSignals { 4 | readonly showColumnHeader: Signal; 5 | readonly hideColumnHeader: Computed; 6 | readonly showTopPanel: Signal; 7 | readonly hideTopPanel: Computed; 8 | readonly showHeaderRow: Signal; 9 | readonly hideHeaderRow: Computed; 10 | readonly showFooterRow: Signal; 11 | readonly hideFooterRow: Computed; 12 | readonly pinnedStartCols: Signal; 13 | readonly pinnedEndCols: Signal; 14 | readonly frozenTopRows: Signal; 15 | readonly frozenBottomRows: Signal; 16 | } 17 | -------------------------------------------------------------------------------- /src/layouts/layout-engine.ts: -------------------------------------------------------------------------------- 1 | import { Column } from "../core/column"; 2 | import { GridOptions } from "../core/gridoptions"; 3 | import type { LayoutHost } from "./layout-host"; 4 | import type { GridLayoutRefs } from "./layout-refs"; 5 | 6 | export interface LayoutEngine { 7 | layoutName: string; 8 | init(host: LayoutHost): void; 9 | destroy(): void; 10 | adjustFrozenRowsOption?(): void; 11 | afterSetOptions(args: GridOptions): void; 12 | /** this might be called before init, chicken egg situation */ 13 | reorderViewColumns?(viewCols: Column[], refs: GridLayoutRefs): Column[]; 14 | supportPinnedCols?: boolean; 15 | supportPinnedEnd?: boolean; 16 | supportFrozenRows?: boolean; 17 | supportFrozenBottom?: boolean; 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "allowJs": false, 5 | "declaration": true, 6 | "emitBOM": true, 7 | "lib": [ 8 | "ES2017", 9 | "DOM" 10 | ], 11 | "jsx": "react-jsx", 12 | "jsxImportSource": "@serenity-is/domwise", 13 | "module": "ESNext", 14 | "moduleResolution": "Bundler", 15 | "newLine": "lf", 16 | "noEmit": true, 17 | "noEmitHelpers": false, 18 | "noImplicitAny": true, 19 | "isolatedModules": true, 20 | "sourceMap": false, 21 | "skipLibCheck": false, 22 | "outDir": "./dist", 23 | "target": "ESNext", 24 | "useDefineForClassFields": false 25 | }, 26 | "include": ["src"] 27 | } 28 | -------------------------------------------------------------------------------- /test/mocks/mock-signal.ts: -------------------------------------------------------------------------------- 1 | import type { SignalLike } from "@serenity-is/sleekdom"; 2 | 3 | export function mockSignal(initialValue: T): SignalLike & { callbacks?: Array<(value: T) => void> } { 4 | const signal = { 5 | currentValue: initialValue, 6 | peek: vi.fn(() => signal.currentValue) as () => T, 7 | subscribe: vi.fn(function (callback) { 8 | signal.listeners.push(callback); return () => { 9 | signal.listeners = signal.listeners.filter(cb => cb !== callback); 10 | } 11 | }), 12 | get value() { return signal.currentValue }, 13 | set value(val: T) { if (signal.currentValue !== val) { signal.currentValue = val; signal.listeners.forEach(cb => cb.call(this, val)); } }, 14 | listeners: [] as Array<(value: any) => void> 15 | }; 16 | return signal; 17 | } 18 | -------------------------------------------------------------------------------- /src/core/cellnavigation.ts: -------------------------------------------------------------------------------- 1 | export type CellNavigationDirection = "up" | "down" | "left" | "right" | "next" | "prev" | "home" | "end"; 2 | 3 | export interface CellNavigation { 4 | navigateBottom(): void; 5 | navigateDown(): boolean; 6 | navigateLeft(): boolean; 7 | navigateNext(): boolean; 8 | navigatePageDown(): void; 9 | navigatePageUp(): void; 10 | navigatePrev(): boolean; 11 | navigateRight(): boolean; 12 | navigateRowEnd(): boolean; 13 | navigateRowStart(): boolean; 14 | navigateTop(): void; 15 | navigateToRow(row: number): boolean; 16 | navigateUp(): boolean; 17 | /** 18 | * Navigate the active cell in the specified direction. 19 | * @param dir Navigation direction. 20 | * @return Whether navigation resulted in a change of active cell. 21 | */ 22 | navigate(dir: CellNavigationDirection): boolean; 23 | } 24 | -------------------------------------------------------------------------------- /docs/assets/examples-classic.css: -------------------------------------------------------------------------------- 1 | .slick-large-editor-text button { 2 | border-width: 1px; 3 | margin-right: 4px; 4 | } 5 | 6 | .ui-datepicker { 7 | background: white; 8 | border: 1px solid #ccc; 9 | } 10 | 11 | .ui-datepicker-trigger { 12 | vertical-align: -2px; 13 | } 14 | 15 | .ui-datepicker table .ui-state-highlight { 16 | border-color: #5F83B9; 17 | } 18 | 19 | .ui-datepicker table .ui-state-hover { 20 | background: #5F83B9; 21 | color: #FFF; 22 | text-shadow: 0 1px 1px #234386; 23 | box-shadow: 0 0px 0 rgba(255, 255, 255, 0.6) inset; 24 | border-color: #5F83B9; 25 | } 26 | 27 | .ui-datepicker-calendar .ui-state-default { 28 | background: transparent; 29 | border-color: #FFF; 30 | } 31 | 32 | .ui-datepicker-calendar .ui-state-active { 33 | background: #5F83B9; 34 | border-color: #5F83B9; 35 | color: #FFF; 36 | font-weight: bold; 37 | text-shadow: 0 1px 1px #234386; 38 | } 39 | -------------------------------------------------------------------------------- /docs/_config.yml: -------------------------------------------------------------------------------- 1 | title: 'SleekGrid' 2 | description: 'A modern Data Grid / Spreadsheet component (inspired by SlickGrid)' 3 | baseurl: /sleekgrid 4 | 5 | packageurl: 6 | bootstrap: https://cdn.jsdelivr.net/npm/bootstrap@5.2.0 7 | jquery: https://cdn.jsdelivr.net/npm/jquery@3.6.0 8 | jqueryui: https://cdn.jsdelivr.net/npm/jquery-ui@1.13.2 9 | sleekgrid: https://cdn.jsdelivr.net/npm/@serenity-is/sleekgrid@1.9.6 10 | slickgrid: https://cdn.jsdelivr.net/npm/slickgrid@2.4.45 11 | 12 | srcurl: 13 | docs: https://github.com/serenity-is/sleekgrid/blob/main/docs 14 | 15 | defaults: 16 | - 17 | scope: 18 | path: '' 19 | values: 20 | layout: default 21 | - 22 | scope: 23 | path: 'examples' 24 | values: 25 | layout: examples 26 | - 27 | scope: 28 | path: 'examples/classic' 29 | values: 30 | layout: layout-classic-examples 31 | -------------------------------------------------------------------------------- /Serenity.SleekGrid.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.1.0 4 | NuGet version of @serenity-is/sleekgrid NPM package 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ## SleekGrid (@serenity-is/sleekgrid) 3 | * 4 | * A modern, lightweight, and highly customizable data grid component for web applications. 5 | * 6 | * - This is a complete rewrite of the original [SlickGrid](https://github.com/mleibman/SlickGrid) in TypeScript with ES6 modules 7 | * - Includes many of the extra features and fixes from the [6pac fork](https://github.com/6pac/SlickGrid/) 8 | * - Designed for high performance with large datasets and smooth scrolling 9 | * - Modular architecture with support for plugins, custom formatters, editors, and data providers 10 | * 11 | * @packageDocumentation 12 | */ 13 | 14 | export * from "./core"; 15 | export * from "./grid"; 16 | export * from "./layouts/frozen-layout"; 17 | export * from "./formatters"; 18 | export * from "./editors"; 19 | export * from "./data/groupitemmetadataprovider"; 20 | export * from "./plugins/autotooltips"; 21 | export * from "./plugins/rowmovemanager"; 22 | export * from "./plugins/rowselectionmodel"; 23 | -------------------------------------------------------------------------------- /docs/examples/classic/example01-simple.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Basic grid' 3 | demonstrates: 4 | - basic grid with minimal configuration 5 | --- 6 | 7 |
8 | 9 | 35 | -------------------------------------------------------------------------------- /src/core/idataview.ts: -------------------------------------------------------------------------------- 1 | import { ItemMetadata } from "./column"; 2 | import { EventEmitter, EventData } from "./event"; 3 | import { Group, IGroupTotals } from "./group"; 4 | 5 | 6 | export interface IDataView { 7 | /** Gets the grand totals for all aggregated data. */ 8 | getGrandTotals(): IGroupTotals; 9 | /** Gets the total number of rows in the view. */ 10 | getLength(): number; 11 | /** Gets the item at the specified row index. */ 12 | getItem(row: number): (TItem | Group | IGroupTotals); 13 | /** Gets metadata for the item at the specified row index. */ 14 | getItemMetadata?(row: number): ItemMetadata; 15 | /** Event fired when the underlying data changes */ 16 | readonly onDataChanged?: EventEmitter<{}>; 17 | /** Event fired when the row count changes */ 18 | readonly onRowCountChanged?: EventEmitter<{ previous: number; current: number }>; 19 | /** Event fired when specific rows change */ 20 | readonly onRowsChanged?: EventEmitter<{ rows: number[] }>; 21 | } 22 | -------------------------------------------------------------------------------- /src/formatters/index.ts: -------------------------------------------------------------------------------- 1 | import { formatterContext } from "../core"; 2 | import { CheckboxFormatter, CheckmarkFormatter, PercentCompleteBarFormatter, PercentCompleteFormatter, YesNoFormatter } from "./formatters"; 3 | 4 | export * from "./formatters"; 5 | 6 | export namespace Formatters { 7 | export function PercentComplete(_row: number, _cell: number, value: any) { 8 | return PercentCompleteFormatter(formatterContext({ value })); 9 | } 10 | 11 | export function PercentCompleteBar(_row: number, _cell: number, value: any) { 12 | return PercentCompleteBarFormatter(formatterContext({ value })); 13 | } 14 | 15 | export function YesNo(_row: number, _cell: number, value: any) { 16 | return YesNoFormatter(formatterContext({ value })); 17 | } 18 | 19 | export function Checkbox(_row: number, _cell: number, value: any) { 20 | return CheckboxFormatter(formatterContext({ value })); 21 | } 22 | 23 | export function Checkmark(_row: number, _cell: number, value: any) { 24 | return CheckmarkFormatter(formatterContext({ value })); 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /src/layouts/basic-layout.tsx: -------------------------------------------------------------------------------- 1 | import { FooterRow, Header, HeaderRow, TopPanel, Viewport } from "./layout-components"; 2 | import { LayoutEngine } from "./layout-engine"; 3 | import type { LayoutHost } from "./layout-host"; 4 | import { type GridLayoutRefs } from "./layout-refs"; 5 | 6 | export class BasicLayout implements LayoutEngine { 7 | protected host: LayoutHost; 8 | protected refs: GridLayoutRefs; 9 | 10 | init(host: LayoutHost) { 11 | this.host = host; 12 | const signals = host.getSignals(); 13 | const refs = this.refs = host.refs; 14 | const common = { refs, signals }; 15 | 16 | this.host.getContainerNode().append(<> 17 |
18 | 19 | 20 | 21 | 22 | ); 23 | } 24 | 25 | public destroy(): void { 26 | this.host = this.refs = null; 27 | } 28 | 29 | public afterSetOptions(): void { } 30 | 31 | readonly layoutName = "BasicLayout"; 32 | } 33 | -------------------------------------------------------------------------------- /docs/examples/classic/example95-plugin-autotooltips.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'AutoTooltips Plugin' 3 | about: 'Resize the columns until see ellipsis in column or header. Hover over cell to see tooltip.' 4 | demonstrates: 5 | - AutoTooltips plugin 6 | 7 | requires_scripts: 8 | - dist/compat/plugins/slick.autotooltips.js 9 | --- 10 | 11 |
12 | 13 | 40 | -------------------------------------------------------------------------------- /src/grid/render-args.ts: -------------------------------------------------------------------------------- 1 | import type { ISleekGrid } from "../core"; 2 | import type { CellStylesHash } from "../core/formatting"; 3 | import type { ViewRange } from "../core/viewrange"; 4 | import type { CachedRow } from "./internal"; 5 | 6 | export interface RowCellCommonRenderArgs { 7 | activeCell: number; 8 | activeRow: number; 9 | cachedRow?: CachedRow; 10 | cellCssClasses?: Record; 11 | colLeft: number[]; 12 | colRight: number[]; 13 | frozenPinned: { 14 | frozenBottomFirst: number; 15 | frozenTopLast: number; 16 | pinnedStartLast: number; 17 | pinnedEndFirst: number; 18 | }; 19 | grid: ISleekGrid; 20 | item: TItem; 21 | row: number; 22 | rtl: boolean; 23 | } 24 | 25 | export interface RowRenderArgs extends RowCellCommonRenderArgs { 26 | range: ViewRange; 27 | sbCenter: string[]; 28 | sbEnd: string[]; 29 | sbStart: string[]; 30 | getRowTop: (row: number) => number; 31 | } 32 | 33 | export interface CellRenderArgs extends RowCellCommonRenderArgs { 34 | cell: number; 35 | colMetadata?: any; 36 | colspan: number; 37 | sb: string[]; 38 | } 39 | 40 | export interface RowCellRenderArgs extends CellRenderArgs, RowRenderArgs { 41 | } 42 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | (c) 2017-present Volkan Ceylan, Furkan Evran and Victor Tomaili, https://github.com/serenity-is/SleekGrid 4 | (c) 2009-2019 Michael Leibman and Ben McIntyre, https://github.com/6pac/slickgrid, https://github.com/mleibman/slickgrid 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | -------------------------------------------------------------------------------- /docs/_plugins/cachebust.rb: -------------------------------------------------------------------------------- 1 | # could use like {{ "style.css" | hash_assets }} and return style.css?v=abc123... but it does not work in github 2 | module Jekyll 3 | module CacheBust 4 | class CacheDigester 5 | require 'digest/md5' 6 | 7 | attr_accessor :file_name, :directory 8 | 9 | def initialize(file_name:, directory: nil) 10 | self.file_name = file_name 11 | self.directory = directory 12 | end 13 | 14 | def digest! 15 | [file_name, '?v=', Digest::MD5.hexdigest(file_contents)].join 16 | end 17 | 18 | private 19 | 20 | def directory_files_content 21 | target_path = File.join(directory, '**', '*') 22 | Dir[target_path].map{|f| File.read(f) unless File.directory?(f) }.join 23 | end 24 | 25 | def file_content 26 | FIle.read(file_name) 27 | end 28 | 29 | def file_contents 30 | is_directory? ? file_content : directory_files_content 31 | end 32 | 33 | def is_directory? 34 | directory.nil? 35 | end 36 | end 37 | 38 | def hash_assets(file_name) 39 | CacheDigester.new(file_name: file_name, directory: 'assets').digest! 40 | end 41 | end 42 | end 43 | 44 | Liquid::Template.register_filter(Jekyll::CacheBust) 45 | -------------------------------------------------------------------------------- /test/grid/internal.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | simpleArrayEquals 3 | } from '../../src/grid/internal'; 4 | 5 | describe('simpleArrayEquals', () => { 6 | it('should return false when first argument is not an array', () => { 7 | expect(simpleArrayEquals(null, [])).toBe(false); 8 | }); 9 | 10 | it('should return false when second argument is not an array', () => { 11 | expect(simpleArrayEquals([], null)).toBe(false); 12 | }); 13 | 14 | it('should return false when arrays have different length', () => { 15 | expect(simpleArrayEquals([1, 2], [1, 2, 3])).toBe(false); 16 | }); 17 | 18 | it('should return false when arrays have different values', () => { 19 | expect(simpleArrayEquals([1, 2], [1, 3])).toBe(false); 20 | }); 21 | 22 | it('should return true when arrays have same values', () => { 23 | expect(simpleArrayEquals([1, 2], [2, 1])).toBe(true); 24 | }); 25 | 26 | it('should return true when arrays have same values but different order', () => { 27 | expect(simpleArrayEquals([1, 2], [2, 1])).toBe(true); 28 | }); 29 | 30 | it('should not modify the original arrays', () => { 31 | const arr1 = [1, 2]; 32 | const arr2 = [2, 1]; 33 | 34 | simpleArrayEquals(arr1, arr2); 35 | 36 | expect(arr1).toEqual([1, 2]); 37 | expect(arr2).toEqual([2, 1]); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /docs/examples/classic/example97-frozen-columns.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Frozen Columns' 3 | 4 | demonstrates: 5 | - basic grid with frozen layout 6 | 7 | requires_scripts: 8 | - dist/compat/slick.formatters.js 9 | - dist/compat/layouts/slick.frozenlayout.js 10 | --- 11 | 12 |
13 | 14 | 46 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SleekGrid 2 | 3 | [![npm version](https://img.shields.io/npm/v/@serenity-is/sleekgrid.svg?style=flat-square)](https://www.npmjs.com/package/@serenity-is/sleekgrid) [![npm downloads](https://img.shields.io/npm/dm/@serenity-is/sleekgrid.svg?style=flat-square)](https://www.npmjs.com/package/@serenity-is/sleekgrid) ![gzip size](https://img.badgesize.io/https:/cdn.jsdelivr.net/npm/@serenity-is/sleekgrid/dist/index.js?compression=gzip) 4 | 5 | ## A modern Data Grid / Spreadsheet component 6 | 7 | - This is a complete rewrite of the original [SlickGrid](https://github.com/mleibman/SlickGrid) in TypeScript with ES6 modules 8 | - Includes many of the extra features and fixes from the [6pac fork](https://github.com/6pac/SlickGrid/) 9 | - Can work without jQuery or jQuery UI (but can use them for column ordering / resizing if available in global namespace) 10 | - Backward compatible with the original via the provided compat files. Just replace SlickGrid scripts with the ones in the [dist/compat](https://www.jsdelivr.com/package/npm/@serenity-is/sleekgrid?path=dist%2Fcompat) directory. 11 | 12 | Please visit [the web site](https://serenity-is.github.io/sleekgrid/) to see the documentation and examples. 13 | 14 | SleekGrid is used extensively in [Serenity](https://serenity.is), our [open source](https://github.com/serenity-is/serenity) ASP.NET Core / TypeScript based business application framework. Visit [the demo](https://serenity.is/demo) to see SleekGrid in action! 15 | -------------------------------------------------------------------------------- /src/formatters/formatters.ts: -------------------------------------------------------------------------------- 1 | import { FormatterContext, FormatterResult } from "../core"; 2 | 3 | export function PercentCompleteFormatter(ctx: FormatterContext) { 4 | if (ctx.value == null || ctx.value === "") 5 | return "-"; 6 | const span = document.createElement('span'); 7 | span.textContent = ctx.value + "%"; 8 | span.style.fontWeight = 'bold'; 9 | if (ctx.value < 50) 10 | span.style.color = 'red'; 11 | else 12 | span.style.color = 'green'; 13 | return span; 14 | } 15 | 16 | export function PercentCompleteBarFormatter(ctx: FormatterContext): FormatterResult { 17 | if (ctx.value == null || ctx.value === "") 18 | return ""; 19 | 20 | var color; 21 | if (ctx.value < 30) 22 | color = "red"; 23 | else if (ctx.value < 70) 24 | color = "silver"; 25 | else 26 | color = "green"; 27 | 28 | const span = document.createElement('span'); 29 | span.className = 'percent-complete-bar slick-percentcomplete-bar'; 30 | span.style.background = color; 31 | span.style.width = ctx.value + '%'; 32 | span.title = ctx.value + '%'; 33 | return span; 34 | } 35 | 36 | export function YesNoFormatter(ctx: FormatterContext): FormatterResult { 37 | return ctx.value ? 'Yes' : 'No'; 38 | } 39 | 40 | 41 | export function CheckboxFormatter(ctx: FormatterContext): FormatterResult { 42 | const i = document.createElement('i'); 43 | i.className = 'slick-checkbox slick-edit-preclick' + (ctx.value ? ' checked' : ''); 44 | return i; 45 | } 46 | 47 | export function CheckmarkFormatter(ctx: FormatterContext): FormatterResult { 48 | if (!ctx.value) 49 | return ''; 50 | 51 | const i = document.createElement('i'); 52 | i.className = 'slick-checkmark'; 53 | return i; 54 | } 55 | 56 | -------------------------------------------------------------------------------- /src/grid/eventargs.ts: -------------------------------------------------------------------------------- 1 | import type { CellStylesHash, Column, Editor, ValidationResult } from "../core"; 2 | import type { Grid } from "./grid"; 3 | 4 | export interface ArgsGrid { 5 | grid?: Grid; 6 | } 7 | 8 | export interface ArgsColumn extends ArgsGrid { 9 | column: Column; 10 | } 11 | 12 | export interface ArgsColumnNode extends ArgsColumn { 13 | node: HTMLElement; 14 | } 15 | 16 | export type ArgsSortCol = { 17 | sortCol: Column; 18 | sortAsc: boolean; 19 | } 20 | 21 | export interface ArgsSort extends ArgsGrid { 22 | multiColumnSort: boolean; 23 | sortAsc?: boolean; 24 | sortCol?: Column; 25 | sortCols?: ArgsSortCol[]; 26 | } 27 | 28 | export interface ArgsSelectedRowsChange extends ArgsGrid { 29 | rows: number[]; 30 | changedSelectedRows?: number[]; 31 | changedUnselectedRows?: number[]; 32 | previousSelectedRows?: number[]; 33 | caller: any; 34 | } 35 | 36 | export interface ArgsScroll extends ArgsGrid { 37 | scrollLeft: number; 38 | scrollTop: number; 39 | } 40 | 41 | export interface ArgsCssStyle extends ArgsGrid { 42 | key: string; 43 | hash: CellStylesHash; 44 | } 45 | 46 | export interface ArgsCell extends ArgsGrid { 47 | row: number; 48 | cell: number; 49 | } 50 | 51 | export interface ArgsCellChange extends ArgsCell { 52 | item: any; 53 | } 54 | 55 | export interface ArgsCellEdit extends ArgsCellChange { 56 | column: Column; 57 | } 58 | 59 | export interface ArgsAddNewRow extends ArgsColumn { 60 | item: any; 61 | } 62 | 63 | export interface ArgsEditorDestroy extends ArgsGrid { 64 | editor: Editor; 65 | } 66 | 67 | export interface ArgsValidationError extends ArgsCell { 68 | editor: Editor, 69 | column: Column; 70 | cellNode: HTMLElement; 71 | validationResults: ValidationResult; 72 | } 73 | -------------------------------------------------------------------------------- /src/grid/event-utils.ts: -------------------------------------------------------------------------------- 1 | import { type EventEmitter, type EventData } from "../core/event"; 2 | import type { ArgsGrid } from "../core/eventargs"; 3 | import type { ISleekGrid } from "../core/isleekgrid"; 4 | 5 | export function triggerGridEvent(this: ISleekGrid, 6 | evt: EventEmitter, args?: Omit, e?: TEventData): EventData & { getReturnValue(): any; getReturnValues(): any[]; args: TArgs } { 7 | args ??= {} as any; 8 | (args as TArgs).grid = this; 9 | return evt.notify(args as TArgs, e, this); 10 | } 11 | 12 | export function addListener(this: { 13 | jQuery: (el: HTMLElement) => { on: (type: string, listener: any) => void }, 14 | eventDisposer: AbortController, 15 | uid: string 16 | }, el: HTMLElement, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, args?: { capture?: boolean, oneOff?: boolean, signal?: AbortSignal, passive?: boolean }): void { 17 | // can't use jQuery on with options, so we fallback to native addEventListener 18 | if (!args?.capture && !args?.signal && !args?.passive && this.jQuery) { 19 | this.jQuery(el).on(type + "." + this.uid, listener as any); 20 | } 21 | else { 22 | el.addEventListener(type, listener, { 23 | signal: this.eventDisposer?.signal, 24 | ...args 25 | }); 26 | } 27 | } 28 | 29 | export function removeListener(this: { 30 | jQuery: (el: HTMLElement) => { off: (type: string, listener: any) => void }, 31 | uid: string 32 | }, el: HTMLElement, type: K, listener: (this: HTMLElement, ev: HTMLElementEventMap[K]) => any, args?: { capture?: boolean }): void { 33 | // can't use jQuery off with options, so we fallback to native removeEventListener 34 | if (this.jQuery) { 35 | this.jQuery(el).off(type + "." + this.uid, listener as any); 36 | } 37 | else { 38 | el.removeEventListener(type, listener, !!args?.capture); 39 | } 40 | }; 41 | 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@serenity-is/sleekgrid", 3 | "version": "2.1.0", 4 | "description": "A modern Data Grid / Spreadsheet component", 5 | "type": "module", 6 | "types": "dist/index.d.ts", 7 | "main": "dist/index.js", 8 | "module": "dist/index.js", 9 | "devDependencies": { 10 | "@serenity-is/domwise": "workspace:*" 11 | }, 12 | "exports": { 13 | ".": { 14 | "types": "./dist/index.d.ts", 15 | "main": "./dist/index.js", 16 | "import": "./dist/index.js" 17 | } 18 | }, 19 | "files": [ 20 | "dist/**/*.ts", 21 | "dist/**/*.css", 22 | "dist/**/*.js", 23 | "dist/**/*.map", 24 | "lib/**/*.js", 25 | "css/**/*.css", 26 | "src/**/*.ts", 27 | "src/**/*.css", 28 | "tsconfig.json" 29 | ], 30 | "scripts": { 31 | "build": "node build/build", 32 | "build:full": "node build/build --full", 33 | "doc": "typedoc --plugin typedoc-plugin-markdown --exclude **/lib/**/* --excludePrivate --readme none --githubPages false --hidePageHeader --sourceLinkTemplate https://github.com/serenity-is/Serenity/blob/master/{path}#L{line} --readme none --out /serenity-is/SerenityIs/SerenityIs.Web/Docs/api/js/sleekgrid --json ./out/typedoc.json src", 34 | "dts": "dts-bundle-generator -o dist/index.d.ts src/index.ts --project=./tsconfig.json --no-banner --disable-symlinks-following", 35 | "prepublishOnly": "pnpm build:full && pnpm run test --coverage && pnpm dts", 36 | "test": "pnpm build && vitest run", 37 | "tsc": "tsc", 38 | "www": "pnpm build:full && cd docs && jekyll serve --livereload --config _config.yml,_config_local.yml" 39 | }, 40 | "keywords": [ 41 | "grid", 42 | "datagrid", 43 | "datatable", 44 | "SleekGrid", 45 | "SlickGrid", 46 | "spreadsheet", 47 | "table" 48 | ], 49 | "author": "Volkan Ceylan, Furkan Evran, Victor Tomaili (https://serenity.is)", 50 | "repository": "serenity-is/sleekgrid", 51 | "license": "MIT", 52 | "bugs": "https://github.com/serenity-is/sleekgrid/issues", 53 | "homepage": "https://github.com/serenity-is/sleekgrid#readme" 54 | } 55 | -------------------------------------------------------------------------------- /test/core/group.spec.ts: -------------------------------------------------------------------------------- 1 | import { Group, GroupTotals } from "../../src/core/group"; 2 | 3 | describe('Group', () => { 4 | it('should set readonly __group to true', () => { 5 | const group = new Group(); 6 | 7 | expect(group.__group).toBeTruthy(); 8 | }); 9 | 10 | it('should not be equal if value is not equal', () => { 11 | const group1 = new Group(); 12 | const group2 = new Group(); 13 | 14 | group1.value = 1; 15 | group2.value = 2; 16 | 17 | expect(group1.equals(group2)).toBeFalsy(); 18 | 19 | }); 20 | 21 | it('should not be equal if count is not equal', () => { 22 | const group1 = new Group(); 23 | const group2 = new Group(); 24 | 25 | group1.count = 1; 26 | group2.count = 2; 27 | 28 | expect(group1.equals(group2)).toBeFalsy(); 29 | 30 | }); 31 | 32 | it('should not be equal if collapsed is not equal', () => { 33 | const group1 = new Group(); 34 | const group2 = new Group(); 35 | 36 | group1.collapsed = true; 37 | group2.collapsed = false; 38 | 39 | expect(group1.equals(group2)).toBeFalsy(); 40 | 41 | }); 42 | 43 | it('should be equal if properties are empty', () => { 44 | const group1 = new Group(); 45 | const group2 = new Group(); 46 | 47 | expect(group1.equals(group2)).toBeTruthy(); 48 | }); 49 | 50 | it('should be equal if properties are equal', () => { 51 | const group1 = new Group(); 52 | const group2 = new Group(); 53 | 54 | group1.value = 1; 55 | group2.value = 1; 56 | 57 | group1.count = 1; 58 | group2.count = 1; 59 | 60 | group1.collapsed = true; 61 | group2.collapsed = true; 62 | 63 | expect(group1.equals(group2)).toBeTruthy(); 64 | }); 65 | }); 66 | 67 | describe('GroupTotals', () => { 68 | it('should set readonly __group to true', () => { 69 | const groupTotals = new GroupTotals(); 70 | 71 | expect(groupTotals.__groupTotals).toBeTruthy(); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /src/core/cellrange.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * A structure containing a range of cells. 3 | * @param fromRow {Integer} Starting row. 4 | * @param fromCell {Integer} Starting cell. 5 | * @param toRow {Integer} Optional. Ending row. Defaults to fromRow. 6 | * @param toCell {Integer} Optional. Ending cell. Defaults to fromCell. 7 | */ 8 | export class CellRange { 9 | 10 | declare public fromRow: number; 11 | declare public fromCell: number; 12 | declare public toRow: number; 13 | declare public toCell: number; 14 | 15 | constructor(fromRow: number, fromCell: number, toRow?: number, toCell?: number) { 16 | if (toRow === undefined && toCell === undefined) { 17 | toRow = fromRow; 18 | toCell = fromCell; 19 | } 20 | 21 | this.fromRow = Math.min(fromRow, toRow); 22 | this.fromCell = Math.min(fromCell, toCell); 23 | this.toRow = Math.max(fromRow, toRow); 24 | this.toCell = Math.max(fromCell, toCell); 25 | } 26 | 27 | /*** 28 | * Returns whether a range represents a single row. 29 | */ 30 | isSingleRow(): boolean { 31 | return this.fromRow == this.toRow; 32 | } 33 | 34 | /*** 35 | * Returns whether a range represents a single cell. 36 | */ 37 | isSingleCell(): boolean { 38 | return this.fromRow == this.toRow && this.fromCell == this.toCell; 39 | } 40 | 41 | /*** 42 | * Returns whether a range contains a given cell. 43 | */ 44 | contains(row: number, cell: number): boolean { 45 | return row >= this.fromRow && row <= this.toRow && 46 | cell >= this.fromCell && cell <= this.toCell; 47 | } 48 | 49 | /*** 50 | * Returns a readable representation of a range. 51 | */ 52 | toString(): string { 53 | if (this.isSingleCell()) { 54 | return "(" + this.fromRow + ":" + this.fromCell + ")"; 55 | } 56 | else { 57 | return "(" + this.fromRow + ":" + this.fromCell + " - " + this.toRow + ":" + this.toCell + ")"; 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /test/mocks/mock-layout-host.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from "../../src/core"; 2 | import type { Column } from "../../src/core/column"; 3 | import type { RowCell } from "../../src/core/editing"; 4 | import type { GridSignals } from "../../src/core/grid-signals"; 5 | import { GridOptions } from "../../src/core/gridoptions"; 6 | import type { ViewportInfo } from "../../src/core/viewportinfo"; 7 | import type { LayoutHost } from "../../src/layouts/layout-host"; 8 | import { createGridSignalsAndRefs } from "../../src/layouts/layout-refs"; 9 | 10 | export function mockLayoutHost(): LayoutHost & { 11 | signals: GridSignals, 12 | opt: GridOptions, 13 | container: HTMLDivElement 14 | } { 15 | const { signals, refs } = createGridSignalsAndRefs(); 16 | const host = { 17 | container: document.createElement("div"), 18 | opt: { 19 | get showColumnHeader() { return host.signals.showColumnHeader.peek(); }, 20 | set showColumnHeader(value: boolean) { host.signals.showColumnHeader.value = value; }, 21 | get showHeaderRow() { return host.signals.showHeaderRow.peek(); }, 22 | set showHeaderRow(value: boolean) { host.signals.showHeaderRow.value = value; }, 23 | get showFooterRow() { return host.signals.showFooterRow.peek(); }, 24 | set showFooterRow(value: boolean) { host.signals.showFooterRow.value = value; }, 25 | get showTopPanel() { return !host.signals.hideTopPanel.peek(); }, 26 | set showTopPanel(value: boolean) { host.signals.showTopPanel.value = value; } 27 | } as GridOptions, 28 | refs, 29 | signals, 30 | bindAncestorScroll: vi.fn(), 31 | cleanUpAndRenderCells: vi.fn(), 32 | getAvailableWidth: vi.fn(() => 1000), 33 | getCellFromPoint: vi.fn(() => ({ row: 0, cell: 0 } as RowCell)), 34 | getAllColumns: vi.fn(() => [] as Column[]), 35 | getColumns: vi.fn(() => [] as Column[]), 36 | getContainerNode: vi.fn(() => host.container), 37 | getDataLength: vi.fn(() => 0), 38 | getOptions: vi.fn(() => host.opt), 39 | getSignals: vi.fn(() => host.signals), 40 | getRowFromNode: vi.fn(() => null), 41 | getScrollDims: vi.fn(() => ({ width: 0, height: 0 })), 42 | getViewportInfo: vi.fn(() => ({} as ViewportInfo)), 43 | removeNode: vi.fn(), 44 | renderRows: vi.fn(), 45 | registerPlugin: vi.fn(), 46 | unregisterPlugin: vi.fn(), 47 | getPluginByName: vi.fn(() => null), 48 | onAfterInit: new EventEmitter() 49 | }; 50 | return host; 51 | } 52 | -------------------------------------------------------------------------------- /src/layouts/frozen-layout.tsx: -------------------------------------------------------------------------------- 1 | import { Column, GridOptions } from "../core"; 2 | import { FooterRow, Header, HeaderRow, TopPanel, Viewport } from "./layout-components"; 3 | import type { LayoutEngine } from "./layout-engine"; 4 | import type { LayoutHost } from "./layout-host"; 5 | import type { GridLayoutRefs } from "./layout-refs"; 6 | 7 | export class FrozenLayout implements LayoutEngine { 8 | private host: LayoutHost; 9 | private refs: GridLayoutRefs; 10 | 11 | init(host: LayoutHost) { 12 | this.host = host; 13 | this.refs = host.refs; 14 | const signals = host.getSignals(); 15 | const common = { refs: this.refs, signals }; 16 | 17 | host.getContainerNode().append(<> 18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ); 30 | 31 | this.adjustFrozenRowsOption(); 32 | } 33 | 34 | public reorderViewColumns(viewCols: Column[], refs: GridLayoutRefs): Column[] { 35 | const pinnedStartCols = viewCols.filter(x => x.frozen && x.frozen !== 'end'); 36 | refs.config.pinnedStartCols = pinnedStartCols.length; 37 | if (pinnedStartCols.length > 0) 38 | return pinnedStartCols.concat(viewCols.filter(x => !x.frozen)); 39 | return null; 40 | } 41 | 42 | public afterSetOptions(arg: GridOptions) { 43 | if (arg.frozenRows != null || arg.frozenBottom != null) 44 | this.adjustFrozenRowsOption(); 45 | if (arg.frozenColumns != null && arg.columns == null) { 46 | const columns = this.reorderViewColumns(this.host.getAllColumns(), this.refs); 47 | if (columns != null) 48 | arg.columns = columns; 49 | } 50 | } 51 | 52 | public adjustFrozenRowsOption(): void { 53 | const { frozenRows, frozenBottom } = this.host.getOptions(); 54 | this.refs.config.frozenTopRows = frozenBottom === true ? 0 : (frozenRows ?? 0); 55 | } 56 | 57 | public destroy(): void { 58 | this.host = null; 59 | } 60 | 61 | readonly layoutName = "FrozenLayout"; 62 | 63 | supportPinnedCols: true = true; 64 | supportFrozenRows: true = true; 65 | } 66 | -------------------------------------------------------------------------------- /src/grid/internal.tsx: -------------------------------------------------------------------------------- 1 | import { invokeDisposingListeners } from "@serenity-is/domwise"; 2 | 3 | export function simpleArrayEquals(arr1: number[], arr2: number[]) { 4 | if (!Array.isArray(arr1) || !Array.isArray(arr2) || arr1.length !== arr2.length) 5 | return false; 6 | arr1 = arr1.slice().sort(); 7 | arr2 = arr2.slice().sort(); 8 | for (var i = 0; i < arr1.length; i++) { 9 | if (arr1[i] !== arr2[i]) 10 | return false; 11 | } 12 | return true; 13 | } 14 | 15 | export interface CachedRow { 16 | rowNodeS: HTMLElement, 17 | rowNodeC: HTMLElement, 18 | rowNodeE: HTMLElement, 19 | // ColSpans of rendered cells (by column idx). 20 | // Can also be used for checking whether a cell has been rendered. 21 | cellColSpans: number[], 22 | 23 | // Cell nodes (by column idx). Lazy-populated by ensureCellNodesInRowsCache(). 24 | cellNodesByColumnIdx: { [key: number]: HTMLElement }, 25 | 26 | // Column indices of cell nodes that have been rendered, but not yet indexed in 27 | // cellNodesByColumnIdx. These are in the same order as cell nodes added at the 28 | // end of the row. 29 | cellRenderQueue: number[]; 30 | 31 | // Elements returned from formatters for cells in cellRenderQueue. 32 | cellRenderContent: (Element | DocumentFragment)[]; 33 | } 34 | 35 | export interface GoToResult { 36 | row: number; 37 | cell: number; 38 | posX: number; 39 | } 40 | 41 | export interface PostProcessCleanupEntry { 42 | groupId: number, 43 | cellNode?: HTMLElement, 44 | columnIdx?: number, 45 | rowNodeS?: HTMLElement; 46 | rowNodeC?: HTMLElement; 47 | rowNodeE?: HTMLElement; 48 | rowIdx?: number; 49 | } 50 | 51 | export const defaultRemoveNode = (node: HTMLElement) => { 52 | if (!node) 53 | return; 54 | invokeDisposingListeners(node, { descendants: true }); 55 | node.remove(); 56 | } 57 | 58 | export const defaultEmptyNode = (node: HTMLElement) => { 59 | if (!node) 60 | return; 61 | invokeDisposingListeners(node, { descendants: true, excludeSelf: true }); 62 | node.innerHTML = ""; 63 | } 64 | 65 | export function defaultJQueryEmptyNode(this: { (node: HTMLElement): { empty: () => void }, fn: any }, node: HTMLElement) { 66 | if (!node) 67 | return; 68 | if (!this || this.fn) 69 | defaultEmptyNode(node); 70 | else 71 | this(node).empty(); 72 | } 73 | 74 | export function defaultJQueryRemoveNode(this: { (node: HTMLElement): { remove: () => void }, fn: any }, node: HTMLElement) { 75 | if (!node) 76 | return; 77 | if (!this || this.fn) 78 | defaultRemoveNode(node); 79 | else 80 | this(node).remove(); 81 | } 82 | -------------------------------------------------------------------------------- /src/plugins/autotooltips.ts: -------------------------------------------------------------------------------- 1 | import type { GridPlugin, HeaderColumnEvent, ISleekGrid } from "../core"; 2 | 3 | export interface AutoTooltipsOptions { 4 | enableForCells?: boolean; 5 | enableForHeaderCells?: boolean; 6 | maxToolTipLength?: number; 7 | replaceExisting?: boolean; 8 | } 9 | 10 | export class AutoTooltips implements GridPlugin { 11 | 12 | declare private grid: ISleekGrid; 13 | declare private options: AutoTooltipsOptions; 14 | 15 | constructor(options?: AutoTooltipsOptions) { 16 | this.options = Object.assign({}, AutoTooltips.defaults, options); 17 | } 18 | 19 | public static readonly defaults: AutoTooltipsOptions = { 20 | enableForCells: true, 21 | enableForHeaderCells: false, 22 | maxToolTipLength: null, 23 | replaceExisting: true 24 | } 25 | 26 | init(grid: ISleekGrid) { 27 | this.grid = grid; 28 | 29 | if (this.options.enableForCells) 30 | this.grid.onMouseEnter.subscribe(this.handleMouseEnter); 31 | 32 | if (this.options.enableForHeaderCells) 33 | this.grid.onHeaderMouseEnter.subscribe(this.handleHeaderMouseEnter); 34 | } 35 | 36 | destroy() { 37 | if (this.options.enableForCells) 38 | this.grid.onMouseEnter.unsubscribe(this.handleMouseEnter); 39 | 40 | if (this.options.enableForHeaderCells) 41 | this.grid.onHeaderMouseEnter.unsubscribe(this.handleHeaderMouseEnter); 42 | } 43 | 44 | private handleMouseEnter = (e: MouseEvent) => { 45 | var cell = this.grid.getCellFromEvent(e); 46 | if (!cell) 47 | return; 48 | var node = this.grid.getCellNode(cell.row, cell.cell); 49 | if (!node) 50 | return; 51 | var text; 52 | if (!node.title || this.options.replaceExisting) { 53 | if (node.clientWidth < node.scrollWidth) { 54 | text = node.textContent?.trim() ?? ""; 55 | if (this.options.maxToolTipLength && 56 | text.length > this.options.maxToolTipLength) { 57 | text = text.substring(0, this.options.maxToolTipLength - 3) + "..."; 58 | } 59 | } else { 60 | text = ""; 61 | } 62 | node.title = text; 63 | } 64 | node = null; 65 | } 66 | 67 | private handleHeaderMouseEnter = (e: HeaderColumnEvent) => { 68 | var column = e.column; 69 | if (column && !column.toolTip) { 70 | var node = (e.target as HTMLElement).closest(".slick-header-column") as HTMLElement; 71 | node && (node.title = (node.clientWidth < node.scrollWidth ? (typeof column.name === "string" ? column.name : "") : "")); 72 | } 73 | } 74 | 75 | public pluginName = "AutoTooltips"; 76 | } 77 | -------------------------------------------------------------------------------- /test/grid/grid.plugin.spec.ts: -------------------------------------------------------------------------------- 1 | import type { ISleekGrid } from "../../src/core"; 2 | import type { GridPlugin } from "../../src/core/grid-plugin"; 3 | import { SleekGrid } from "../../src/grid/sleekgrid"; 4 | 5 | it('should call plugin init with grid instance', () => { 6 | const grid = new SleekGrid(document.createElement('div'), [], [], {}); 7 | 8 | let pluginInitGrid: ISleekGrid | null = null; 9 | const plugin: GridPlugin = { 10 | init: (grid: ISleekGrid) => { 11 | pluginInitGrid = grid; 12 | } 13 | } 14 | 15 | grid.registerPlugin(plugin); 16 | 17 | expect(pluginInitGrid).toBe(grid); 18 | }); 19 | 20 | it('should be able to get plugin by name if it exists', () => { 21 | const grid = new SleekGrid(document.createElement('div'), [], [], {}); 22 | 23 | const plugin: GridPlugin = { 24 | init: (_grid: ISleekGrid) => {}, 25 | pluginName: 'test' 26 | } 27 | 28 | grid.registerPlugin(plugin); 29 | 30 | expect(grid.getPluginByName('test')).toBe(plugin); 31 | }); 32 | 33 | it('should be able to unregister a plugin', () => { 34 | const grid = new SleekGrid(document.createElement('div'), [], [], {}); 35 | 36 | let pluginDestroyCalled = false; 37 | const plugin: GridPlugin = { 38 | init: (_grid: ISleekGrid) => {}, 39 | destroy: () => { 40 | pluginDestroyCalled = true; 41 | }, 42 | pluginName: 'test' 43 | } 44 | 45 | grid.registerPlugin(plugin); 46 | expect(grid.getPluginByName('test')).toBe(plugin); 47 | 48 | grid.unregisterPlugin(plugin); 49 | 50 | expect(grid.getPluginByName('test')).toBeUndefined(); 51 | expect(pluginDestroyCalled).toBe(true); 52 | }); 53 | 54 | it('should call plugin.destroy if it exists', () => { 55 | const grid = new SleekGrid(document.createElement('div'), [], [], {}); 56 | 57 | let pluginDestroyCalled = false; 58 | const plugin: GridPlugin = { 59 | init: (_grid: ISleekGrid) => {}, 60 | destroy: () => { 61 | pluginDestroyCalled = true; 62 | } 63 | } 64 | 65 | grid.registerPlugin(plugin); 66 | grid.destroy(); 67 | 68 | expect(pluginDestroyCalled).toBe(true); 69 | }); 70 | 71 | it('should be able to get plugins without names', () => { 72 | const grid = new SleekGrid(document.createElement('div'), [], [], {}); 73 | 74 | const newPlugin = (): GridPlugin => ({ 75 | init: (_grid: ISleekGrid) => { 76 | } 77 | }); 78 | 79 | const firstPlugin = newPlugin(); 80 | const secondPlugin = newPlugin(); 81 | 82 | grid.registerPlugin(firstPlugin); 83 | grid.registerPlugin(secondPlugin); 84 | 85 | expect(grid.getPluginByName(undefined)).toEqual(firstPlugin); 86 | 87 | grid.unregisterPlugin(firstPlugin); 88 | 89 | expect(grid.getPluginByName(undefined)).toEqual(secondPlugin); 90 | }); 91 | -------------------------------------------------------------------------------- /docs/examples/classic/example03-events.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Events' 3 | 4 | demonstrates: 5 | - handling events from the grid 6 | - right-click the row to open the context menu 7 | - click the priority cell to toggle values 8 | 9 | requires_scripts: 10 | - dist/compat/slick.editors.js 11 | --- 12 | 13 |
14 | 15 | 21 | 22 | 84 | -------------------------------------------------------------------------------- /docs/examples/classic/example06-editing-with-undo.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Editing with Undo 3 | 4 | demonstrates: 5 | - using "editCommandHandler" option to intercept edit commands and implement undo supportl 6 | 7 | controls: 8 | - 9 | 10 | requires_scripts: 11 | - dist/compat/slick.editors.js 12 | - dist/compat/slick.formatters.js 13 | --- 14 | 15 |
16 | 17 | 73 | 74 | 79 | -------------------------------------------------------------------------------- /src/core/eventargs.ts: -------------------------------------------------------------------------------- 1 | import type { CellStylesHash, Column, Editor, EventData, ValidationResult } from "."; 2 | import type { ISleekGrid } from "./isleekgrid"; 3 | 4 | export interface ArgsGrid { 5 | grid: ISleekGrid; 6 | } 7 | 8 | export interface ArgsColumn extends ArgsGrid { 9 | column: Column; 10 | } 11 | 12 | export interface ArgsDrag extends ArgsGrid { 13 | mode: string; 14 | row: number; 15 | cell: number; 16 | item: any; 17 | helper: HTMLElement; 18 | } 19 | 20 | let a: EventData; 21 | 22 | 23 | export interface ArgsColumnNode extends ArgsColumn { 24 | node: HTMLElement; 25 | } 26 | 27 | export type ArgsSortCol = { 28 | sortCol: Column; 29 | sortAsc: boolean; 30 | } 31 | 32 | export interface ArgsSort extends ArgsGrid { 33 | multiColumnSort: boolean; 34 | sortAsc: boolean; 35 | sortCol: Column; 36 | sortCols: ArgsSortCol[]; 37 | } 38 | 39 | export interface ArgsSelectedRowsChange extends ArgsGrid { 40 | rows: number[]; 41 | changedSelectedRows: number[]; 42 | changedUnselectedRows: number[]; 43 | previousSelectedRows: number[]; 44 | caller: any; 45 | } 46 | 47 | export interface ArgsScroll extends ArgsGrid { 48 | scrollLeft: number; 49 | scrollTop: number; 50 | } 51 | 52 | export interface ArgsCssStyle extends ArgsGrid { 53 | key: string; 54 | hash: CellStylesHash; 55 | } 56 | 57 | export interface ArgsCell extends ArgsGrid { 58 | row: number; 59 | cell: number; 60 | } 61 | 62 | export interface ArgsCellChange extends ArgsCell { 63 | item: any; 64 | } 65 | 66 | export interface ArgsCellEdit extends ArgsCellChange { 67 | column: Column; 68 | } 69 | 70 | export interface ArgsAddNewRow extends ArgsColumn { 71 | item: any; 72 | } 73 | 74 | export interface ArgsEditorDestroy extends ArgsGrid { 75 | editor: Editor; 76 | } 77 | 78 | export interface ArgsValidationError extends ArgsCell { 79 | editor: Editor, 80 | column: Column; 81 | cellNode: HTMLElement; 82 | validationResults: ValidationResult; 83 | } 84 | 85 | export type CellEvent = EventData; 86 | export type CellKeyboardEvent = EventData; 87 | export type CellMouseEvent = EventData; 88 | export type HeaderColumnEvent = EventData; 89 | export type HeaderMouseEvent = EventData; 90 | export type HeaderRenderEvent = EventData; 91 | export type FooterColumnEvent = HeaderColumnEvent; 92 | export type FooterMouseEvent = HeaderMouseEvent; 93 | export type FooterRenderEvent = HeaderRenderEvent; 94 | export type GridEvent = EventData; 95 | export type GridDragEvent = EventData; 96 | export type GridMouseEvent = EventData; 97 | export type GridSortEvent = EventData; 98 | 99 | 100 | -------------------------------------------------------------------------------- /docs/examples/classic/example05-editing.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: Editing 3 | 4 | demonstrates: 5 | - adding basic keyboard navigation and editing 6 | - custom editors and validators 7 | - auto-edit settings 8 | 9 | controls: 10 | - '' 11 | - '' 12 | 13 | requires_scripts: 14 | - dist/compat/slick.editors.js 15 | - dist/compat/slick.formatters.js 16 | 17 | requires_slick_scripts: 18 | - plugins/slick.cellrangedecorator.js 19 | - plugins/slick.cellrangeselector.js 20 | - plugins/slick.cellselectionmodel.js 21 | --- 22 | 23 |
24 | 25 | 75 | 76 | 85 | -------------------------------------------------------------------------------- /docs/examples/classic/example02-formatters.html: -------------------------------------------------------------------------------- 1 | --- 2 | title: 'Formatters' 3 | 4 | demonstrates: 5 | - width, minWidth, maxWidth, resizable, cssClass column attributes 6 | - custom column formatters 7 | - an extended formatter setting { addClass, tooltip } context properties, allowing adding classes and a tooltip to the cell. 8 | 9 | requires_scripts: 10 | - dist/compat/slick.editors.js 11 | - dist/compat/slick.formatters.js 12 | --- 13 | 14 |
15 | 16 | 70 | 71 | 84 | -------------------------------------------------------------------------------- /test/core/range.spec.ts: -------------------------------------------------------------------------------- 1 | import { CellRange } from "../../src/core/cellrange"; 2 | 3 | describe('Range', () => { 4 | it('sets toRow to fromRow if toRow is undefined', () => { 5 | const range = new CellRange(1, 2); 6 | 7 | expect(range.toRow).toBe(range.fromRow); 8 | }); 9 | 10 | it('sets toCell to fromCell if toRow is undefined', () => { 11 | const range = new CellRange(1, 2); 12 | 13 | expect(range.toCell).toBe(range.fromCell); 14 | }); 15 | 16 | it('isSingleRow returns true if toRow and fromRow is the same', () => { 17 | const range = new CellRange(1, 2); 18 | 19 | expect(range.isSingleRow()).toBeTruthy(); 20 | }); 21 | 22 | it('isSingleRow returns false if toRow and fromRow is not the same', () => { 23 | const range = new CellRange(1, 2, 3, 2); 24 | 25 | expect(range.isSingleRow()).toBeFalsy(); 26 | }); 27 | 28 | it('isSingleCell returns true if row and the cell is the same ', () => { 29 | const range = new CellRange(1, 2, 1, 2); 30 | 31 | expect(range.isSingleCell()).toBeTruthy(); 32 | }); 33 | 34 | it('isSingleCell returns false if row is not the same ', () => { 35 | const range = new CellRange(1, 2, 2, 2); 36 | 37 | expect(range.isSingleCell()).toBeFalsy(); 38 | }); 39 | 40 | it('isSingleCell returns false if cell is not the same ', () => { 41 | const range = new CellRange(1, 2, 1, 3); 42 | 43 | expect(range.isSingleCell()).toBeFalsy(); 44 | }); 45 | 46 | it('isSingleCell returns false if row and cell is not the same ', () => { 47 | const range = new CellRange(1, 2, 2, 3); 48 | 49 | expect(range.isSingleCell()).toBeFalsy(); 50 | }); 51 | 52 | it('contains returns true if range contains it', () => { 53 | const [fromRow, fromCell, toRow, toCell] = [2, 5, 3, 6]; 54 | const range = new CellRange(fromRow, fromCell, toRow, toCell); 55 | 56 | for (let currentRow = 0; currentRow < toRow + 4; currentRow++) { 57 | for (let currentCell = 0; currentCell < toCell + 4; currentCell++) { 58 | 59 | const expected = currentRow >= fromRow && currentRow <= toRow && 60 | currentCell >= fromCell && currentCell <= toCell; 61 | 62 | expect(range.contains(currentRow, currentCell)).toBe(expected); 63 | } 64 | } 65 | }); 66 | 67 | it('toString should return only (fromRow:fromCell) if range contains only one cell', () => { 68 | const range = new CellRange(1, 2); 69 | 70 | expect(range.toString()).toBe('(1:2)'); 71 | }); 72 | 73 | it('toString should return (fromRow:fromCell - toRow:toCell) if range is not same row', () => { 74 | const range = new CellRange(1, 2, 2, 2); 75 | 76 | expect(range.toString()).toBe('(1:2 - 2:2)'); 77 | }); 78 | 79 | it('toString should return (fromRow:fromCell - toRow:toCell) if range is not same cell', () => { 80 | const range = new CellRange(1, 2, 1, 3); 81 | 82 | expect(range.toString()).toBe('(1:2 - 1:3)'); 83 | }); 84 | 85 | it('toString should return (fromRow:fromCell - toRow:toCell) if range is not single cell', () => { 86 | const range = new CellRange(1, 2, 2, 3); 87 | 88 | expect(range.toString()).toBe('(1:2 - 2:3)'); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /src/grid/render-row.ts: -------------------------------------------------------------------------------- 1 | import { escapeHtml, type IDataView } from "../core"; 2 | import type { Column, ColumnMetadata } from "../core/column"; 3 | import type { CellRenderArgs, RowRenderArgs } from "./render-args"; 4 | import { renderCell } from "./render-cell"; 5 | 6 | export function renderRow(this: void, args: RowRenderArgs): void { 7 | 8 | const { activeRow, colLeft, colRight, grid, item, frozenPinned, range, row, sbStart, sbCenter, sbEnd } = args; 9 | const { pinnedStartLast, pinnedEndFirst, frozenTopLast, frozenBottomFirst } = frozenPinned; 10 | const dataLoading = row < grid.getDataLength() && !item; 11 | const cols = grid.getColumns(); 12 | let rowCss = "slick-row" + 13 | (row <= frozenTopLast || frozenBottomFirst <= row ? ' frozen' : '') + 14 | (dataLoading ? " loading" : "") + 15 | (activeRow === row ? " active" : "") + 16 | (row % 2 == 1 ? " odd" : " even"); 17 | 18 | if (item == null && row < 0) 19 | rowCss += " " + grid.getOptions().addNewRowCssClass; 20 | 21 | const itemMetadata = (grid.getData() as IDataView).getItemMetadata?.(row); 22 | 23 | if (itemMetadata && itemMetadata.cssClasses) { 24 | rowCss += " " + itemMetadata.cssClasses; 25 | } 26 | 27 | const rowTag = `
`; 28 | 29 | sbCenter.push(rowTag); 30 | 31 | if (frozenPinned.pinnedStartLast >= 0) { 32 | sbStart.push(rowTag); 33 | } 34 | 35 | if (frozenPinned.pinnedEndFirst != Infinity) { 36 | sbEnd.push(rowTag); 37 | } 38 | 39 | let colspan: number | "*", col: Column; 40 | for (let cell = 0, colCount = cols.length; cell < colCount; cell++) { 41 | let colMetadata: ColumnMetadata = null; 42 | col = cols[cell]; 43 | colspan = 1; 44 | 45 | if (itemMetadata && itemMetadata.columns) { 46 | colMetadata = itemMetadata.columns[col.id] || itemMetadata.columns[cell]; 47 | colspan = (colMetadata && colMetadata.colspan) || 1; 48 | if (colspan === "*") { 49 | colspan = colCount - cell; 50 | } 51 | } 52 | 53 | const pinnedStart = cell <= pinnedStartLast; 54 | const pinnedEnd = cell >= pinnedEndFirst; 55 | // Do not render cells outside of the viewport. 56 | if (pinnedStart || pinnedEnd || colRight[Math.min(colCount - 1, cell + colspan - 1)] > range.leftPx) { 57 | if (!(pinnedStart || pinnedEnd) && colLeft[cell] > range.rightPx) { 58 | // All columns to the right are outside the range. 59 | if (pinnedEndFirst != Infinity) 60 | break; 61 | cell = pinnedEndFirst - 1; 62 | continue; 63 | } 64 | const cellArgs = args as unknown as CellRenderArgs; 65 | cellArgs.cell = cell; 66 | cellArgs.colspan = colspan; 67 | cellArgs.colMetadata = colMetadata; 68 | cellArgs.sb = pinnedStart ? sbStart : pinnedEnd ? sbEnd : sbCenter; 69 | renderCell(cellArgs); 70 | } 71 | 72 | if (colspan > 1) { 73 | cell += (colspan - 1); 74 | } 75 | } 76 | 77 | sbCenter.push("
"); 78 | 79 | if (pinnedStartLast >= 0) { 80 | sbStart.push(""); 81 | } 82 | 83 | if (pinnedEndFirst != Infinity) { 84 | sbEnd.push(""); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/grid/render-cell.ts: -------------------------------------------------------------------------------- 1 | import { formatterContext, type FormatterResult } from "../core/formatting"; 2 | import { escapeHtml } from "../core/util"; 3 | import type { CellRenderArgs } from "./render-args"; 4 | 5 | export function renderCell(this: void, { activeCell, activeRow, cell, cellCssClasses, colMetadata, 6 | colspan, grid, item, sb, row, rtl, frozenPinned, cachedRow }: CellRenderArgs): void { 7 | const cols = grid.getColumns(); 8 | const column = cols[cell]; 9 | let klass = "slick-cell" + (rtl ? " r" : " l") + cell + (rtl ? " l" : " r") + Math.min(cols.length - 1, cell + colspan - 1) + 10 | (column.cssClass ? " " + column.cssClass : ""); 11 | 12 | if (cell <= frozenPinned.pinnedStartLast) 13 | klass += ' frozen pinned-start'; 14 | else if (cell >= frozenPinned.pinnedEndFirst) 15 | klass += ' frozen pinned-end'; 16 | 17 | if (activeCell === cell && activeRow === row) 18 | klass += " active"; 19 | 20 | if (colMetadata && colMetadata.cssClass) { 21 | klass += " " + colMetadata.cssClass; 22 | } 23 | 24 | for (const key in cellCssClasses) { 25 | const cls = cellCssClasses[key][row]; 26 | if (cls && cls[column.id]) { 27 | klass += (" " + cls[column.id]); 28 | } 29 | } 30 | 31 | // if there is a corresponding row (if not, this is the Add New row or this data hasn't been loaded yet) 32 | var fmtResult: FormatterResult; 33 | const ctx = formatterContext({ 34 | cell, 35 | column, 36 | item, 37 | row, 38 | grid 39 | }); 40 | 41 | if (item) { 42 | ctx.value = grid.getDataItemValueForColumn(item, column); 43 | fmtResult = grid.getFormatter(row, column)(ctx); 44 | if (typeof fmtResult === "string" && fmtResult.length) { 45 | if (ctx.enableHtmlRendering) 46 | fmtResult = (ctx.sanitizer ?? escapeHtml)(fmtResult); 47 | else 48 | fmtResult = escapeHtml(fmtResult); 49 | } 50 | } 51 | 52 | klass = escapeHtml(klass); 53 | 54 | if (ctx.addClass?.length || ctx.addAttrs?.length || ctx.tooltip?.length) { 55 | if (ctx.addClass?.length) 56 | klass += (" " + escapeHtml(ctx.addClass)); 57 | 58 | sb.push('
' + fmtResult + '
'); 79 | else 80 | sb.push('>'); 81 | } 82 | else if (fmtResult != null && !(fmtResult instanceof Node)) 83 | sb.push('
' + fmtResult + '
'); 84 | else 85 | sb.push('
'); 86 | 87 | cachedRow.cellRenderQueue.push(cell); 88 | cachedRow.cellRenderContent.push(fmtResult instanceof Node ? fmtResult : void 0); 89 | cachedRow.cellColSpans[cell] = colspan; 90 | } 91 | -------------------------------------------------------------------------------- /src/grid/layout.ts: -------------------------------------------------------------------------------- 1 | import type { Signal } from "@serenity-is/signals"; 2 | import { RowCell } from "../core"; 3 | import { Column } from "../core/column"; 4 | import { GridOptions } from "../core/gridoptions"; 5 | import { ViewportInfo } from "../core/viewportinfo"; 6 | import { ViewRange } from "../core/viewrange"; 7 | 8 | export interface GridOptionSignals { 9 | showColumnHeader: Signal; 10 | showHeaderRow: Signal; 11 | showFooterRow: Signal; 12 | showTopPanel: Signal; 13 | } 14 | 15 | export interface LayoutHost { 16 | bindAncestorScroll(el: HTMLElement): void; 17 | cleanUpAndRenderCells(range: ViewRange): void; 18 | getAvailableWidth(): number; 19 | getCellFromPoint(x: number, y: number): RowCell; 20 | getColumnCssRules(idx: number): { right: any; left: any; } 21 | getColumns(): Column[]; 22 | getInitialColumns(): Column[]; 23 | getContainerNode(): HTMLElement; 24 | getDataLength(): number; 25 | getOptions(): GridOptions; 26 | getOptionSignals(): GridOptionSignals; 27 | getRowFromNode(rowNode: HTMLElement): number; 28 | getScrollDims(): { width: number, height: number }; 29 | getScrollLeft(): number; 30 | getScrollTop(): number; 31 | getViewportInfo(): ViewportInfo; 32 | renderRows(range: ViewRange): void; 33 | } 34 | 35 | export interface LayoutEngine { 36 | appendCachedRow(row: number, rowNodeS: HTMLElement, rowNodeC: HTMLElement, rowNodeE: HTMLElement): void; 37 | afterHeaderColumnDrag(): void; 38 | afterSetOptions(args: GridOptions): void; 39 | applyColumnWidths(): void; 40 | beforeCleanupAndRenderCells(rendered: ViewRange): void; 41 | afterRenderRows(rendered: ViewRange): void; 42 | bindAncestorScrollEvents(): void; 43 | calcCanvasWidth(): number; 44 | updateHeadersWidth(): void; 45 | isFrozenRow(row: number): boolean; 46 | destroy(): void; 47 | getCanvasNodeFor(cell: number, row: number): HTMLElement; 48 | getCanvasNodes(): HTMLElement[]; 49 | getCanvasWidth(): number; 50 | getRowFromCellNode(cellNode: HTMLElement, clientX: number, clientY: number): number; 51 | getFooterRowCols(): HTMLElement[]; 52 | getFooterRowColsFor(cell: number): HTMLElement; 53 | getFooterRowColumn(cell: number): HTMLElement; 54 | getFrozenTopLastRow(): number; 55 | getFrozenBottomFirstRow(): number; 56 | getFrozenRowOffset(row: number): number; 57 | getPinnedStartLastCol(): number; 58 | getPinnedEndFirstCol(): number; 59 | getHeaderCols(): HTMLElement[]; 60 | getHeaderColsFor(cell: number): HTMLElement; 61 | getHeaderColumn(cell: number): HTMLElement; 62 | getHeaderRowCols(): HTMLElement[]; 63 | getHeaderRowColsFor(cell: number): HTMLElement; 64 | getHeaderRowColumn(cell: number): HTMLElement; 65 | getScrollCanvasY(): HTMLElement; 66 | getScrollContainerX(): HTMLElement; 67 | getScrollContainerY(): HTMLElement; 68 | getTopPanelFor(arg0: number): HTMLElement; 69 | getViewportNodeFor(cell: number, row: number): HTMLElement; 70 | getViewportNodes(): HTMLElement[]; 71 | handleScrollH(): void; 72 | handleScrollV(): void; 73 | init(host: LayoutHost): void; 74 | layoutName: string; 75 | realScrollHeightChange(): void; 76 | /** this might be called before init, chicken egg situation */ 77 | reorderViewColumns(viewCols: Column[], options?: GridOptions): Column[]; 78 | resizeCanvas(): void; 79 | setPaneVisibility(): void; 80 | setScroller(): void; 81 | setOverflow(): void; 82 | updateCanvasWidth(): boolean; 83 | } 84 | -------------------------------------------------------------------------------- /src/layouts/layout-components.tsx: -------------------------------------------------------------------------------- 1 | import { type SignalLike, computed } from "@serenity-is/domwise"; 2 | import type { GridSignals } from "../core"; 3 | import type { BandKey, GridLayoutRefs, PaneKey } from "./layout-refs"; 4 | 5 | function bandHidden(band: BandKey, hide: SignalLike, signals: Pick): SignalLike { 6 | if (band === "main") 7 | return hide; 8 | return computed(() => hide.value || 9 | (band === "start" && signals.pinnedStartCols.value < 0) || 10 | (band === "end" && signals.pinnedEndCols.value == Infinity) 11 | ); 12 | } 13 | 14 | function paneBandHidden(pane: PaneKey, band: BandKey, signals: Pick): boolean | SignalLike { 15 | if (pane === "body" && band === "main") 16 | return false; 17 | 18 | return computed(() => 19 | (pane === "top" && signals.frozenTopRows.value <= 0) || 20 | (pane === "bottom" && signals.frozenBottomRows.value <= 0) || 21 | (band === "start" && signals.pinnedStartCols.value <= 0) || 22 | (band === "end" && signals.pinnedEndCols.value <= 0)); 23 | } 24 | 25 | export const Header = ({ band, refs, signals }: { 26 | band: BandKey, 27 | refs: GridLayoutRefs, 28 | signals: Pick 29 | }) => { 30 | const bandRefs = refs[band]; 31 | return