@jh-grid/jhgrid-js 0.1.3 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.d.ts CHANGED
@@ -34,10 +34,6 @@ export interface GridTheme {
34
34
  scrollbarThumb?: string;
35
35
  scrollbarRadius?:number;
36
36
  frozenBorder?: string;
37
- groupHeaderBg?: string;
38
- groupHeaderText?:string;
39
- footerBg?: string;
40
- footerText?: string;
41
37
  filterIconBg?: string;
42
38
  sortIconBg?: string;
43
39
  filterIconColor?: string;
@@ -82,13 +78,13 @@ export interface GridTheme {
82
78
 
83
79
  /*
84
80
  * ── DOM surfaces ─────────────────────────────────────────────────────────────────────────
85
- * Everything below styles the parts of the grid that are real DOM rather than canvas the
81
+ * Everything below styles the parts of the grid that are real DOM rather than canvas - the
86
82
  * filter panel, context menus, the column chooser, cell editors, and the pager. They are
87
83
  * listed here because `theme` reaches them, but they are also bridged to CSS custom
88
84
  * properties (`--jhg-overlay-bg`, `--jhg-pager-bg`, …; see `GRID_CLASSES`). Each is emitted
89
85
  * as `var(--jhg-…, <theme value>)`: nothing in the library sets the variable, so the theme
90
86
  * value normally wins and a consumer who declares the variable takes over. Style through
91
- * whichever suits the theme for a value known at construction, the variable for one a
87
+ * whichever suits - the theme for a value known at construction, the variable for one a
92
88
  * stylesheet decides.
93
89
  */
94
90
  /** Background of panels, menus and dialogs. Defaults to `'#FFFFFF'`. */
@@ -101,13 +97,13 @@ export interface GridTheme {
101
97
  overlayDivider?: string;
102
98
  /** Primary text on those surfaces. Defaults to `'#212121'`. */
103
99
  overlayText?: string;
104
- /** Secondary text labels, counts. Defaults to `'#595959'`. */
100
+ /** Secondary text - labels, counts. Defaults to `'#595959'`. */
105
101
  overlayMutedText?: string;
106
102
  /** Placeholder and hint text. Defaults to `'#909090'`. */
107
103
  overlayHintText?: string;
108
104
  /** Hover background for a row in a list, such as a filter checklist. Defaults to `'#F5F5F5'`. */
109
105
  overlayHoverBg?: string;
110
- /** Hover and keyboard-focus background for a menu item. Defaults to `'#F0F0F0'`. */
106
+ /** Hover - and keyboard-focus - background for a menu item. Defaults to `'#F0F0F0'`. */
111
107
  overlayItemHoverBg?: string;
112
108
  /** Shadow under panels and dialogs. Defaults to `'0 4px 16px rgba(0,0,0,0.15)'`. */
113
109
  overlayShadow?: string;
@@ -152,7 +148,7 @@ export interface CellContextMenuItemContext {
152
148
  col: number;
153
149
  field: string;
154
150
  rowData: Record<string, unknown> | null;
155
- /** Viewport coordinates of the invoking click (unlike the grid's internal canvas-local x/y)
151
+ /** Viewport coordinates of the invoking click (unlike the grid's internal canvas-local x/y) -
156
152
  * for positioning a host-built DOM overlay (e.g. a comment editor) at the invoked cell. */
157
153
  clientX: number;
158
154
  clientY: number;
@@ -177,7 +173,7 @@ export declare const CellRenderers: {
177
173
  /**
178
174
  * Renders `value` (an image URL or `data:` URI) scaled to fill the cell, via a shared,
179
175
  * byte-budgeted LRU cache that decodes through `fetch()` + `createImageBitmap()` resized to
180
- * the cell's on-screen size a large source image shown as a small thumbnail only ever costs
176
+ * the cell's on-screen size - a large source image shown as a small thumbnail only ever costs
181
177
  * a small decoded bitmap, not a full-resolution one. Non-blocking: shows a placeholder while
182
178
  * loading and redraws itself automatically once the image resolves. `fit: 'cover'` (default)
183
179
  * crops to fill like CSS `object-fit: cover`; `'contain'` letterboxes to show the whole image.
@@ -192,6 +188,17 @@ export declare const CellRenderers: {
192
188
  date(opts?: { format?: string; locale?: string; dateStyle?: 'full' | 'long' | 'medium' | 'short'; align?: 'left' | 'center' | 'right' }): CellRendererFn;
193
189
  currency(opts?: { locale?: string; currency?: string }): CellRendererFn;
194
190
  /** 드롭다운 셀 렌더러: 현재 값 + ▾ 화살표 표시 */
191
+ /**
192
+ * Draws the cell as a link - coloured, underlined, ellipsized to fit. `linked` decides per row
193
+ * whether it reads as actionable, so the look and the behaviour cannot be decided separately.
194
+ */
195
+ link(opts?: {
196
+ color?: string;
197
+ mutedColor?: string;
198
+ underline?: boolean;
199
+ align?: 'left' | 'center' | 'right';
200
+ linked?: boolean | ((rowData: Record<string, unknown> | null) => boolean);
201
+ }): CellRendererFn;
195
202
  dropdown(opts?: { placeholder?: string }): CellRendererFn;
196
203
  /** 다중선택 셀 렌더러: 선택된 값 목록 + ▾ 화살표 표시 */
197
204
  multiselect(opts?: { placeholder?: string }): CellRendererFn;
@@ -215,12 +222,106 @@ export { CellRenderers as BuiltinRenderers };
215
222
 
216
223
  export declare const CellEditors: Record<string, (...args: any[]) => unknown>;
217
224
 
218
- /** Registers (or overrides) a `CellEditors` entry the editor counterpart of `registerCellRenderer`. */
225
+ /** Registers (or overrides) a `CellEditors` entry - the editor counterpart of `registerCellRenderer`. */
219
226
  export declare function registerCellEditor(name: string, factory: (...args: any[]) => unknown): void;
220
227
 
228
+ /**
229
+ * The context every cell editor receives. Alongside the cell's geometry it carries the three
230
+ * anchored-DOM surfaces (`cellBox`, `popup`, `done`) a custom editor uses to mount its UI over a
231
+ * cell without doing any coordinate math of its own - see `core/EditorSurface.js`.
232
+ */
233
+ export interface CellEditorCtx {
234
+ row: number;
235
+ col: number;
236
+ field: string;
237
+ rowData: Record<string, unknown> | null;
238
+ /** Cell geometry relative to the grid wrapper. Prefer `cellBox()`/`popup()` over using these directly. */
239
+ x: number;
240
+ y: number;
241
+ colW: number;
242
+ rowH: number;
243
+ wrapper: HTMLElement;
244
+ kbProxy: HTMLInputElement;
245
+ theme: Required<GridTheme>;
246
+ i18n: Record<string, any>;
247
+ columnLabel: string;
248
+ initialValue: string;
249
+
250
+ /** Commits the editor's current `value` to the cell and closes it. */
251
+ commit(): void;
252
+ /** Closes the editor, discarding whatever it holds. */
253
+ cancel(): void;
254
+ insertLineBreak(): void;
255
+ /** Moves the selection by (rows, cols) - what Tab/Enter do after committing. */
256
+ moveSel(dr: number, dc: number): void;
257
+ focusWrapper(): void;
258
+ /** Tells the grid not to reopen this cell's popup when a click closed it by landing on that cell. */
259
+ suppressReopen?(): void;
260
+
261
+ /**
262
+ * A box covering exactly the cell being edited, carrying the accent border the built-in editors
263
+ * draw. Wrapper-relative, so it tracks the grid on its own; removed with the editor.
264
+ */
265
+ cellBox<T extends HTMLElement = HTMLDivElement>(opts?: {
266
+ /** Place this element over the cell instead of making a <div>. Defaults `interactive` to true. */
267
+ el?: T;
268
+ border?: boolean;
269
+ interactive?: boolean;
270
+ }): T;
271
+
272
+ /**
273
+ * An empty panel anchored to the cell, mounted on <body> so it escapes any `overflow: hidden`
274
+ * between the grid and the page, and re-anchored as ancestors scroll or the window resizes.
275
+ */
276
+ popup(opts?: {
277
+ placement?: 'below' | 'above' | 'over';
278
+ align?: 'left' | 'right' | 'stretch';
279
+ minWidth?: 'cell' | number | null;
280
+ maxHeight?: number | null;
281
+ /** Flip past the cell and clamp into the viewport when the chosen side has no room. Default true. */
282
+ flip?: boolean;
283
+ /** Apply the standard overlay background/border/shadow. Default true. */
284
+ chrome?: boolean;
285
+ role?: string;
286
+ /** true (default) for the grid's standard edit label, a string for your own, false for none. */
287
+ ariaLabel?: boolean | string;
288
+ }): HTMLDivElement;
289
+
290
+ /**
291
+ * Returns the element shape the grid commits from, and wires the keyboard and outside-click
292
+ * behaviour shared by every editor. Return this from a custom editor.
293
+ */
294
+ done(opts: {
295
+ value: (() => unknown) | unknown;
296
+ /** What a mousedown outside the editor does. Default 'commit'. */
297
+ outside?: 'commit' | 'cancel' | 'ignore';
298
+ keys?: {
299
+ enter?: 'commit' | 'cancel' | 'commit-and-move' | false;
300
+ escape?: 'commit' | 'cancel' | 'commit-and-move' | false;
301
+ tab?: 'commit' | 'cancel' | 'commit-and-move' | false;
302
+ };
303
+ /**
304
+ * Seen before the mapping above, for keys the editor handles itself (arrow navigation in a
305
+ * listbox, say). Return true to say it was handled and stop. Torn down with the rest.
306
+ */
307
+ onKey?(e: KeyboardEvent): boolean | void;
308
+ onCommit?(): void;
309
+ onCancel?(): void;
310
+ }): CellEditorEl;
311
+ }
312
+
313
+ /** What a cell editor returns: the grid reads `value` once at commit, then calls `remove()`. */
314
+ export interface CellEditorEl {
315
+ readonly value: string;
316
+ remove(): void;
317
+ /** Present on the object `ctx.done()` returns, for an editor that closes itself from its own UI. */
318
+ commit?(): void;
319
+ cancel?(): void;
320
+ }
321
+
221
322
  // Column Definition
222
323
 
223
- /** 드롭다운 옵션 항목 문자열 또는 { value, label } 객체 */
324
+ /** 드롭다운 옵션 항목 - 문자열 또는 { value, label } 객체 */
224
325
  export type DropdownOption = string | { value: string; label?: string };
225
326
 
226
327
  export interface ColumnDef {
@@ -244,7 +345,7 @@ export interface ColumnDef {
244
345
  /** Date format for type: 'date' columns (e.g. 'YYYY-MM-DD', 'YY/MM/DD'). Applied to both rendering and clipboard copy. Default: 'YYYY-MM-DD'. */
245
346
  format?: string;
246
347
  /** Custom editor: a CellEditors key (e.g. 'date') or a direct editor function receiving the editor context */
247
- editor?: string | ((ctx: Record<string, unknown>) => { value: string; remove: () => void } | null | undefined);
348
+ editor?: string | ((ctx: CellEditorCtx) => CellEditorEl | null | undefined);
248
349
  /** Options forwarded to the named CellEditors factory when editor is a string key */
249
350
  editorOptions?: Record<string, unknown>;
250
351
  /**
@@ -260,21 +361,30 @@ export interface ColumnDef {
260
361
  /** 버튼 설정. type이 'button'일 때 사용. */
261
362
  button?: ButtonColumnDef;
262
363
  /**
263
- * 그룹헤더 행/푸터에 표시할 집계 함수. 내장 타입(`sum`/`avg`/`min`/`max`)은 `Number(row[field])`로
264
- * 캐스팅해 계산하며 `NaN`은 무시한다. `count`는 non-null 개수. 커스텀 `fn`은 그룹(또는 전체)
265
- * 속한 원본 row 배열을 받아 값을 계산하고, `format`으로 표시 문자열을 지정할 있다.
266
- *
267
- * 행 그룹핑을 제공하는 플러그인(`setGrouping`)이 설치되어 있을 때만 의미가 있다. 플러그인 없이
268
- * 지정하면 값은 보관되지만 아무 곳에도 표시되지 않는다 — 집계를 읽고 그리는 쪽이 플러그인이다.
364
+ * A small always-there click hotspot pinned to this column's right edge, independent of the
365
+ * column's `type` (unlike `button`, which turns the *whole* cell into a button). Give it an
366
+ * `icon` and the grid draws it too, using the same width it hit-tests against.
269
367
  */
270
- aggregate?: 'sum' | 'avg' | 'count' | 'min' | 'max' | {
271
- fn: (rows: Record<string, unknown>[], field: string) => number | string;
272
- format?: (value: number | string) => string;
273
- };
368
+ cellButton?: CellButtonDef;
274
369
  /** When true, renders a clickable checkbox in this column's last header row. Fires `onHeaderCheckboxChange` on click. */
275
370
  headerCheckbox?: boolean;
276
371
  }
277
372
 
373
+ export interface CellButtonDef {
374
+ /** Hotspot width in px, measured in from the cell's right edge. Defaults to 20 (the dropdown arrow's width). */
375
+ width?: number;
376
+ /**
377
+ * Glyph to draw in the hotspot, e.g. '+'. Omit it and the grid draws nothing (the hotspot stays
378
+ * clickable) - which is what a host wanting to draw its own mark through `cellDecorator` does.
379
+ */
380
+ icon?: string;
381
+ /** 'plain' drops the shaded band behind the icon; the default draws it, matching a dropdown cell. */
382
+ style?: 'plain';
383
+ iconSize?: number;
384
+ iconColor?: string;
385
+ onClick(rowIndex: number, rowData: Record<string, unknown> | null, field: string): void;
386
+ }
387
+
278
388
  export type ButtonVariant = 'primary' | 'success' | 'danger' | 'neutral';
279
389
 
280
390
  export interface ButtonColumnDef {
@@ -306,8 +416,8 @@ export interface ColumnValidation {
306
416
  maxLength?: number;
307
417
  /**
308
418
  * Custom check, run after all built-in rules pass. Return `true` (valid),
309
- * `false` (invalid uses `message` or the default i18n message), or a
310
- * string (invalid used verbatim as the error message).
419
+ * `false` (invalid - uses `message` or the default i18n message), or a
420
+ * string (invalid - used verbatim as the error message).
311
421
  */
312
422
  validator?: (value: string, rowData: Record<string, unknown>) => boolean | string;
313
423
  /** Overrides the default i18n message for every built-in rule above. */
@@ -318,7 +428,7 @@ export interface ColumnValidation {
318
428
 
319
429
  export interface HeaderRowDef {
320
430
  label?: string;
321
- /** Field names this group spans updates automatically after column reorder */
431
+ /** Field names this group spans - updates automatically after column reorder */
322
432
  fields?: string[];
323
433
  colspan?: number;
324
434
  rowspan?: number;
@@ -326,12 +436,11 @@ export interface HeaderRowDef {
326
436
  }
327
437
 
328
438
  /**
329
- * Computes grouped-header cell layout from `opts.headerRows` + the current column order the
439
+ * Computes grouped-header cell layout from `opts.headerRows` + the current column order - the
330
440
  * same layout engine the canvas header draw uses internally, exposed publicly so a custom
331
- * exporter/renderer can reproduce the same merged-header shape. Returns the same cell shape as
332
- * {@link ExcelHeaderCell} (declared further below, alongside `ExcelExportSchema`).
441
+ * exporter/renderer can reproduce the same merged-header shape.
333
442
  */
334
- export declare function computeHeaderCells(headerRows: HeaderRowDef[][] | undefined, columns: string[]): ExcelHeaderCell[];
443
+ export declare function computeHeaderCells(headerRows: HeaderRowDef[][] | undefined, columns: string[]): HeaderCell[];
335
444
 
336
445
  // Data Source
337
446
 
@@ -349,7 +458,7 @@ export interface GridFilterState {
349
458
  /**
350
459
  * Per-column filter values. A plain `string` is a substring-match text filter
351
460
  * (setFilter()); a `string[]` is a Set filter's exact-match checkbox selection
352
- * (setFilterValues()) interpretation of both is entirely up to fetchData/fetchMeta.
461
+ * (setFilterValues()) - interpretation of both is entirely up to fetchData/fetchMeta.
353
462
  */
354
463
  filters: Record<string, string | string[]>;
355
464
  /** Global quick filter term (setQuickFilter()), '' when inactive. Interpretation (which
@@ -366,46 +475,32 @@ export interface GridState {
366
475
  frozenCols: number;
367
476
  frozenColsRight: number;
368
477
  sorts: { field: string; dir: 'asc' | 'desc' }[];
369
- /** See {@link GridFilterState.filters} string (text filter) or string[] (Set filter) per field. */
478
+ /** See {@link GridFilterState.filters} - string (text filter) or string[] (Set filter) per field. */
370
479
  filters: Record<string, string | string[]>;
371
480
  /** See {@link GridFilterState.quickFilter}. */
372
481
  quickFilter: string;
373
- /**
374
- * Row-grouping state, or `null` when the grid isn't grouped. Always present in the snapshot
375
- * shape (even as `null`), so a `getState()`/`setState()` round-trip never drops this key.
376
- * Single-field grouping keeps the legacy shape; multi-field grouping's `collapsedKeys` are
377
- * path arrays.
378
- */
379
- grouping: { field: string; collapsedKeys: string[] } | { fields: string[]; collapsedKeys: string[][] } | null;
380
- /** Tree/hierarchical row state, or `null` when unused. Same always-present shape as {@link GridState.grouping}. */
381
- treeData: { idField: string; parentField: string; collapsedKeys: string[][] } | null;
382
- /**
383
- * Active color filters, field → color (e.g. `{ COL_4: 'rgba(22,163,74,0.14)' }`), or `null`
384
- * when none are set. Same always-present shape as {@link GridState.grouping}.
385
- */
386
- colorFilters: Record<string, string> | null;
387
- /** Selected row indices (rowSelection mode 'single'|'multi'). Always present (as `[]` when nothing is selected) — row selection is installed unconditionally, even on a base `@jhgrid/jhgrid` import. */
482
+ /** Selected row indices (rowSelection mode 'single'|'multi'). Always present (as `[]` when nothing is selected) - row selection is installed unconditionally, even on a base `@jh-grid/jhgrid-js` import. */
388
483
  selectedRows: number[];
389
484
  /** `field -> checked` for every `headerCheckbox` column's header checkbox (see `setHeaderCheckbox`/`getHeaderCheckbox`). */
390
485
  headerCheckboxState: Record<string, boolean>;
391
486
  /**
392
487
  * Definitions of columns added via `addColumn()` that haven't been committed yet (see
393
- * `getNewColumns()`/`commitColumns()`) restored via `_addColumnImpl` before column order, so a
488
+ * `getNewColumns()`/`commitColumns()`) - restored via `_addColumnImpl` before column order, so a
394
489
  * `setState()` round-trip doesn't silently drop a locally-added column. Function-valued def
395
490
  * fields (a custom `renderer`/`editor`, function-form `options`, a `validation.validator`, a
396
- * `button.onClick`) survive an in-memory round-trip but won't survive `JSON.stringify`/`parse`
491
+ * `button.onClick`) survive an in-memory round-trip but won't survive `JSON.stringify`/`parse` -
397
492
  * same inherent limitation as any function-valued columnDefs entry.
398
493
  */
399
494
  localColumns: (Omit<ColumnDef, 'field'> & { field: string })[];
400
495
  /** Field names of server columns marked for deletion via `deleteColumn()` but not yet committed (see `getDeletedColumns()`/`commitColumns()`). */
401
496
  deletedColumns: string[];
402
497
  /**
403
- * Unsaved row work the row counterpart of {@link GridState.localColumns} /
498
+ * Unsaved row work - the row counterpart of {@link GridState.localColumns} /
404
499
  * {@link GridState.deletedColumns}, which have always been carried here.
405
500
  *
406
501
  * Everything is named by **server index**, never by screen position: a screen position only
407
502
  * means something alongside the exact arrangement that produced it, and the point of a snapshot
408
- * is to outlive that. `restoring` therefore assumes the same result set the same query, the
503
+ * is to outlive that. `restoring` therefore assumes the same result set - the same query, the
409
504
  * same underlying rows. Restore against changed server data and the indices name different
410
505
  * records, the same way {@link GridState.filters} assumes the fields still exist.
411
506
  *
@@ -419,9 +514,9 @@ export interface GridState {
419
514
  * keyed separately they would be two lists that have to agree about ordering.
420
515
  */
421
516
  added: { anchor: number; data: Record<string, unknown>; edits: Record<string, string> }[];
422
- /** Server indices removed from the screen see {@link JHGrid.getRemovedRows}. */
517
+ /** Server indices removed from the screen - see {@link JHGrid.getRemovedRows}. */
423
518
  removed: number[];
424
- /** Server indices marked for deletion see {@link JHGrid.getDeletedRows}. */
519
+ /** Server indices marked for deletion - see {@link JHGrid.getDeletedRows}. */
425
520
  marked: number[];
426
521
  /** Unsaved cell edits on server rows, as `serverIndex -> field -> value`. */
427
522
  edits: Record<number, Record<string, string>>;
@@ -451,7 +546,6 @@ export interface GridI18n {
451
546
  filterResetAll?: string;
452
547
  filterClose?: string;
453
548
  filterDialog?: (col: string) => string;
454
- filterColorLabel?: string;
455
549
  filterValuesLabel?: string;
456
550
  filterSelectAll?: string;
457
551
  /** Placeholder for the tag filter's search box (see {@link JHGridOptions.fetchFilterValues}). */
@@ -507,7 +601,7 @@ export interface GridI18n {
507
601
  rowInsertBelow?: string;
508
602
  /** Label for the "add row" control (e.g. a toolbar button a host wires up itself). */
509
603
  rowAddEnd?: string;
510
- /** Row context menu label for deleting a locally-added (unsaved) row always removed outright. */
604
+ /** Row context menu label for deleting a locally-added (unsaved) row - always removed outright. */
511
605
  rowDelete?: string;
512
606
  /** Row context menu label for marking a server row deleted (`deleteRow(i, { permanent: false })`). */
513
607
  rowDeleteMark?: string;
@@ -535,9 +629,6 @@ export interface GridI18n {
535
629
  rowsSelectedAnnounce?: (n: number) => string;
536
630
  unsavedEditsWarning?: string;
537
631
  exportCsvFilename?: string;
538
- /** Default filename for an Excel-format export, kept separate from `exportCsvFilename` above since `exportCsv()` always uses that one. */
539
- exportExcelFilename?: string;
540
- exportSheetName?: string;
541
632
  printButton?: string;
542
633
  validationRequired?: (col: string) => string;
543
634
  validationPattern?: (col: string) => string;
@@ -551,22 +642,15 @@ export interface GridI18n {
551
642
  pagerNext?: string;
552
643
  pagerLast?: string;
553
644
  pagerPageLabel?: (page: number, pageCount: number) => string;
554
- aggSum?: string;
555
- aggAvg?: string;
556
- aggCount?: string;
557
- aggMin?: string;
558
- aggMax?: string;
559
- groupLabel?: (field: string, key: string, count: number) => string;
560
- groupFooterLabel?: string;
561
645
  }
562
646
 
563
- /** Korean locale strings pass as i18n option for Korean UI */
647
+ /** Korean locale strings - pass as i18n option for Korean UI */
564
648
  export declare const KO_I18N: Required<GridI18n>;
565
649
 
566
- /** Japanese locale strings pass as i18n option for Japanese UI */
650
+ /** Japanese locale strings - pass as i18n option for Japanese UI */
567
651
  export declare const JA_I18N: Required<GridI18n>;
568
652
 
569
- /** Simplified Chinese locale strings pass as i18n option for Chinese UI */
653
+ /** Simplified Chinese locale strings - pass as i18n option for Chinese UI */
570
654
  export declare const ZH_I18N: Required<GridI18n>;
571
655
 
572
656
  // Pagination
@@ -576,7 +660,7 @@ export interface PaginationOptions {
576
660
  enabled: boolean;
577
661
  /**
578
662
  * Rows per page. Fixed at construction time (not changeable at runtime).
579
- * Also becomes the effective `chunkSize` `chunkSize` is ignored when set.
663
+ * Also becomes the effective `chunkSize` - `chunkSize` is ignored when set.
580
664
  * Default: 50.
581
665
  */
582
666
  pageSize?: number;
@@ -596,12 +680,12 @@ export interface JHGridOptions {
596
680
  fetchData?: (page: number, size: number, state?: GridFilterState | null) => Promise<GridData>;
597
681
  /**
598
682
  * `fetchMeta`+`fetchData` collapsed into one call, for a backend that returns the page and the
599
- * total together a `COUNT(*) OVER()` alongside the paged rows, say. Resolve
683
+ * total together - a `COUNT(*) OVER()` alongside the paged rows, say. Resolve
600
684
  * `{ rows, totalRows }`, plus `columns` unless `columnDefs` names them.
601
685
  *
602
686
  * `state` is the sort and filter the grid wants applied, exactly as {@link fetchData} receives
603
687
  * it: honour it server-side and return `totalRows` for the filtered result, not the table. A
604
- * callback that ignores the argument still works it simply never filters or sorts, which is
688
+ * callback that ignores the argument still works - it simply never filters or sorts, which is
605
689
  * what every `fetchPage` grid did before the argument was passed at all.
606
690
  *
607
691
  * The grid asks for chunk 0 once at boot even though it needs both the count and the rows from
@@ -616,7 +700,7 @@ export interface JHGridOptions {
616
700
  * memory (prototyping, small/medium lookup tables, tests). Columns are inferred from
617
701
  * `columnDefs` if given, else from the keys of `data[0]`. Filtering/sorting/quick-filter are
618
702
  * applied against the array directly with the same semantics a host's own `fetchMeta`/
619
- * `fetchData` are expected to follow (see {@link GridFilterState}) there is no indexing, so
703
+ * `fetchData` are expected to follow (see {@link GridFilterState}) - there is no indexing, so
620
704
  * this re-scans the full array on every state change and isn't a fit for very large datasets.
621
705
  * Ignored if `fetchMeta`/`fetchData`/`fetchPage` is also provided.
622
706
  */
@@ -640,8 +724,8 @@ export interface JHGridOptions {
640
724
  */
641
725
  selectionMoveMs?: number;
642
726
  /**
643
- * Glide length in ms for mouse-wheel scrolling. Deltas smaller than one row a precision
644
- * trackpad's dense stream, which is already smooth are applied immediately regardless.
727
+ * Glide length in ms for mouse-wheel scrolling. Deltas smaller than one row - a precision
728
+ * trackpad's dense stream, which is already smooth - are applied immediately regardless.
645
729
  * `0` applies every wheel delta immediately. Forced to 0 under
646
730
  * `prefers-reduced-motion: reduce`. Default: 120.
647
731
  */
@@ -664,7 +748,7 @@ export interface JHGridOptions {
664
748
  * Enables classic fixed-size pagination (a built-in pager bar with
665
749
  * Prev/Next/page-number controls) instead of continuous virtual scrolling.
666
750
  * Row indices everywhere in the public API (getEdits(), onCellChange, setCellValue())
667
- * stay global/absolute regardless of this option pagination only changes what's
751
+ * stay global/absolute regardless of this option - pagination only changes what's
668
752
  * scrollable/visible at once.
669
753
  */
670
754
  pagination?: PaginationOptions;
@@ -676,25 +760,35 @@ export interface JHGridOptions {
676
760
  * filtered/sorted dataset client-side first (row data, unlike column metadata, isn't
677
761
  * normally resident in memory under server-paged virtualization).
678
762
  * Runs one full-dataset scan via `fetchData` after every load/reload; dragging is disabled
679
- * until that scan completes, and while row grouping or tree data is active. Default: false.
763
+ * until that scan completes, and while an installed plugin supplies its own alternate row
764
+ * source. Default: false.
680
765
  */
681
766
  rowReorder?: boolean;
767
+ /**
768
+ * Worker-pool size for the full-dataset scan `rowReorder` (or a plugin calling its own
769
+ * full-table scan) runs via `fetchData`. Default: `navigator.hardwareConcurrency` clamped to
770
+ * `[2, 8]`, so low-core machines aren't flooded with concurrent requests while the scan still
771
+ * parallelizes meaningfully.
772
+ */
773
+ fullScanConcurrency?: number;
774
+ /** Page size used by that same full-dataset scan. Default: falls back to {@link JHGridOptions.chunkSize}. */
775
+ fullScanPageSize?: number;
682
776
  editableCols?: string[] | '*';
683
777
  /**
684
778
  * What {@link JHGrid.deleteRow} does to a **server** row when the call doesn't say.
685
779
  *
686
- * - `'mark'` (default) the row stays on screen, dimmed with a strikethrough, and is reported
780
+ * - `'mark'` (default) - the row stays on screen, dimmed with a strikethrough, and is reported
687
781
  * by {@link JHGrid.getDeletedRows}. Suits a screen with an explicit save step, where the user
688
782
  * should be able to change their mind before committing.
689
- * - `'permanent'` the row comes off the screen and is reported by
783
+ * - `'permanent'` - the row comes off the screen and is reported by
690
784
  * {@link JHGrid.getRemovedRows}. Suits a list that commits as you go.
691
785
  *
692
786
  * Either way the server is untouched; the grid only records the choice. Rows added with
693
- * {@link JHGrid.addRow} ignore this they were never sent anywhere, so there is nothing to
787
+ * {@link JHGrid.addRow} ignore this - they were never sent anywhere, so there is nothing to
694
788
  * mark and they always go immediately.
695
789
  *
696
790
  * `deleteRow(i, { permanent })` overrides it per call, because one screen can legitimately need
697
- * both marking a saved record while discarding a draft, say.
791
+ * both - marking a saved record while discarding a draft, say.
698
792
  */
699
793
  deleteMode?: 'mark' | 'permanent';
700
794
  /**
@@ -725,7 +819,7 @@ export interface JHGridOptions {
725
819
  * named items, omitted shows all of them.
726
820
  *
727
821
  * Valid keys: `'col-insert-left'`, `'col-insert-right'`, `'col-delete'`, `'row-insert-below'`,
728
- * `'row-delete'`. Unrelated to {@link JHGridOptions.cellContextMenuExtraItems} below that adds
822
+ * `'row-delete'`. Unrelated to {@link JHGridOptions.cellContextMenuExtraItems} below - that adds
729
823
  * items instead of filtering these, and always applies regardless of this option.
730
824
  */
731
825
  cellContextMenuItems?: false | ('col-insert-left' | 'col-insert-right' | 'col-delete' | 'row-insert-below' | 'row-delete')[];
@@ -735,13 +829,15 @@ export interface JHGridOptions {
735
829
  * menu opens for a given cell; return `null`/`undefined`/`[]` to add nothing for that cell.
736
830
  *
737
831
  * This is the one general-purpose extension point for host- or plugin-supplied context menu
738
- * actions the grid renders the label, routes the click to `onClick`, and closes the menu
832
+ * actions - the grid renders the label, routes the click to `onClick`, and closes the menu
739
833
  * afterwards, same as a built-in item. It has no opinion about what `onClick` does.
740
834
  */
741
835
  cellContextMenuExtraItems?: (ctx: CellContextMenuItemContext) => CellContextMenuItem[] | null | undefined;
742
836
  columnDefs?: ColumnDef[];
743
837
  /** Multi-row header groups. Each element is one group-header row. */
744
838
  headerRows?: HeaderRowDef[][];
839
+ /** Adds an Excel-style A/B/C/... row above the normal header row(s), one non-interactive cell per column (no sort/filter/checkbox). Default: false. */
840
+ columnLetterHeader?: boolean;
745
841
  /** Show a built-in row-number column on the left. Default: true. */
746
842
  showRowNumbers?: boolean;
747
843
  /** Width of the row-number column in pixels. Default: 50. */
@@ -764,6 +860,8 @@ export interface JHGridOptions {
764
860
  // Callbacks
765
861
  /** oldValue is the pre-edit value: the prior edit if the cell was already dirty, otherwise the row's original data. */
766
862
  onCellChange?: (params: { row: number; field: string; newValue: string; oldValue: string }) => void;
863
+ /** Fired on every cell double-click, editable or not - alongside the built-in edit-start, not instead of it. */
864
+ onCellDoubleClick?: (rowIndex: number, rowData: Record<string, unknown> | null, field: string) => void;
767
865
  /** Fired when the cell/range selection changes. null = selection cleared. */
768
866
  onSelectionChange?: (sel: { type: 'single'; row: number; col: number } | { type: 'range'; r1: number; c1: number; r2: number; c2: number } | null) => void;
769
867
  /** Fired after sorts are applied or cleared. Empty array = all sorts cleared. */
@@ -774,7 +872,7 @@ export interface JHGridOptions {
774
872
  * Looks up candidate values for a column's header filter, given whatever the user has typed.
775
873
  * Return a bounded list; only what fits the query is useful, and the panel shows it as-is.
776
874
  *
777
- * The tag picker itself is **not** conditional on this it appears on any column whose values
875
+ * The tag picker itself is **not** conditional on this - it appears on any column whose values
778
876
  * could not be enumerated locally (the ones where the checklist does not appear). What this
779
877
  * changes is where the candidates come from. Omit it and they are scanned out of the rows
780
878
  * already loaded, and the list is labelled as covering only those; supply it and it answers for
@@ -798,13 +896,15 @@ export interface JHGridOptions {
798
896
  onColumnReorder?: (columns: string[]) => void;
799
897
  /** Fired after a column is resized (mouse-up). */
800
898
  onColumnResize?: (field: string, width: number) => void;
899
+ /** Fired after hideColumn()/showColumn() or the column chooser's Apply changes which columns are hidden. Not fired if the change didn't actually alter the hidden set. */
900
+ onColumnVisibilityChange?: (hiddenColumns: string[]) => void;
801
901
  /** Fired after a row's individual height is resized by dragging a boundary in the row-number gutter (mouse-up). */
802
902
  onRowHeightResize?: (rowIndex: number, height: number) => void;
803
903
  /**
804
904
  * Fired after a row drag-reorder completes (requires `rowReorder: true`).
805
905
  * `fromIndex`/`toIndex` are absolute row indices at drop time; `rowData` is the moved row's
806
906
  * raw data. Row-indexed transient state (edits, selection, undo history) is cleared on
807
- * reorder the same way it is on a sort/filter change persist `toIndex` server-side here if
907
+ * reorder the same way it is on a sort/filter change - persist `toIndex` server-side here if
808
908
  * the new order needs to survive a refresh.
809
909
  */
810
910
  onRowReorder?: (fromIndex: number, toIndex: number, rowData: Record<string, unknown>) => void;
@@ -824,7 +924,7 @@ export interface JHGridOptions {
824
924
  onHeaderCheckboxChange?: (field: string, checked: boolean) => void;
825
925
  /**
826
926
  * Optional callback returning a CSS color string for a row, or null/undefined for default.
827
- * Called on every render keep it fast.
927
+ * Called on every render - keep it fast.
828
928
  * @example rowHighlighter: (row) => row.status === 'ERROR' ? 'rgba(239,68,68,0.12)' : null
829
929
  */
830
930
  rowHighlighter?: (rowData: Record<string, unknown> | null, rowIndex: number) => string | null | undefined;
@@ -832,7 +932,7 @@ export interface JHGridOptions {
832
932
  * Optional callback returning a CSS color string for an individual cell's background,
833
933
  * or null/undefined for no override. Painted on top of the row background/highlight
834
934
  * (rowHighlighter, selection) and beneath cell content/renderers. Called for every
835
- * visible cell on every render keep it fast. `rowData` is null for rows not yet
935
+ * visible cell on every render - keep it fast. `rowData` is null for rows not yet
836
936
  * loaded from the server, so check for that before reading fields. Exceptions thrown
837
937
  * here are caught and logged; they don't interrupt rendering.
838
938
  * @example cellBackground: (row, rowIndex, field) => row && field === 'score' && row.score < 60 ? '#fee2e2' : null
@@ -840,12 +940,12 @@ export interface JHGridOptions {
840
940
  cellBackground?: (rowData: Record<string, unknown> | null, rowIndex: number, field: string, colIndex: number) => string | null | undefined;
841
941
  /**
842
942
  * Optional callback that draws on top of a cell after everything else in it (content,
843
- * gridlines, strikethrough, validation border) close to the call shape of a
943
+ * gridlines, strikethrough, validation border) - close to the call shape of a
844
944
  * `columnDefs[i].renderer` (`(ctx, args) => void`), but invoked for every rendered cell instead
845
945
  * of replacing one column's content, and `args` includes `field` since a decorator (unlike a
846
946
  * column renderer, already scoped to one column) needs it to tell columns apart. Use it for a
847
947
  * small corner mark, icon, or badge that layers on top of whatever the cell already shows.
848
- * Called for every visible, loaded cell on every render keep it fast, and do nothing (return
948
+ * Called for every visible, loaded cell on every render - keep it fast, and do nothing (return
849
949
  * without drawing) for cells that need no mark. Exceptions thrown here are caught and logged;
850
950
  * they don't interrupt rendering.
851
951
  * @example cellDecorator: (ctx, { x, y, w, field, rowIndex }) => { if (hasFlag(rowIndex, field)) { ctx.fillStyle = 'red'; ctx.beginPath(); ctx.moveTo(x + w - 8, y); ctx.lineTo(x + w, y); ctx.lineTo(x + w, y + 8); ctx.fill(); } }
@@ -855,16 +955,17 @@ export interface JHGridOptions {
855
955
  * Optional callback returning tooltip text to show while the pointer idles over a cell, or
856
956
  * null/undefined for none. Same call shape as `cellBackground`. Shown immediately (no hover
857
957
  * delay), and takes priority over the built-in overflow-text tooltip and over anything a plugin
858
- * supplies via its own `cellTooltip` a validation error on the cell still wins over this.
958
+ * supplies via its own `cellTooltip` - a validation error on the cell still wins over this.
859
959
  * `rowData` is null for rows not yet loaded from the server.
860
960
  * @example cellTooltip: (row, rowIndex, field) => hasNote(rowIndex, field) ? getNote(rowIndex, field) : null
861
961
  */
862
962
  cellTooltip?: (rowData: Record<string, unknown> | null, rowIndex: number, field: string, colIndex: number) => string | null | undefined;
863
963
  }
864
964
 
865
- // Excel export schema
965
+ // Header cell layout (see computeHeaderCells)
866
966
 
867
- export interface ExcelHeaderCell {
967
+ /** One cell of a computed grouped-header layout - a leaf column header or a group spanning several. */
968
+ export interface HeaderCell {
868
969
  row: number;
869
970
  col: number;
870
971
  colspan: number;
@@ -874,18 +975,6 @@ export interface ExcelHeaderCell {
874
975
  isLeaf: boolean;
875
976
  }
876
977
 
877
- export interface ExcelExportSchema {
878
- sheetName: string;
879
- columns: string[];
880
- columnLabels: string[];
881
- headerCells: ExcelHeaderCell[] | undefined;
882
- includeHeaders: boolean;
883
- colWidths: number[];
884
- headerBg: string;
885
- headerText: string;
886
- headerBorder: string;
887
- }
888
-
889
978
  // JHGrid
890
979
 
891
980
  /**
@@ -902,7 +991,7 @@ export interface JHGridPlugin {
902
991
  export declare class JHGrid {
903
992
  /**
904
993
  * Installs a plugin. Installing the same object twice is a no-op. Plugin packages call this
905
- * on import, so an application normally never has to import the package and its API appears
994
+ * on import, so an application normally never has to - import the package and its API appears
906
995
  * on the grid instance.
907
996
  */
908
997
  static use(plugin: JHGridPlugin): void;
@@ -911,8 +1000,8 @@ export declare class JHGrid {
911
1000
 
912
1001
  // Boot
913
1002
  /**
914
- * Resolves once the first `fetchMeta`/`fetchData` boot has settled either way, including
915
- * on failure so that `this._columns` and friends are populated and any structural API will
1003
+ * Resolves once the first `fetchMeta`/`fetchData` boot has settled - either way, including
1004
+ * on failure - so that `this._columns` and friends are populated and any structural API will
916
1005
  * actually run instead of silently no-op'ing while a boot is still in flight.
917
1006
  *
918
1007
  * Await it before calling one straight after construction:
@@ -921,13 +1010,13 @@ export declare class JHGrid {
921
1010
  * await grid.ready();
922
1011
  * grid.hideColumn('email');
923
1012
  * ```
924
- * Not needed inside a user-triggered handler (a button's `onclick`, say) by then the page,
1013
+ * Not needed inside a user-triggered handler (a button's `onclick`, say) - by then the page,
925
1014
  * and therefore the initial boot, has necessarily finished.
926
1015
  */
927
1016
  ready(): Promise<void>;
928
1017
 
929
1018
  /**
930
- * Schedules a repaint without touching data, scroll, edits, filters, or sort for when
1019
+ * Schedules a repaint without touching data, scroll, edits, filters, or sort - for when
931
1020
  * something a `cellBackground`/`rowHighlighter` callback reads changed outside the grid (host
932
1021
  * state a plugin keeps, say) and the next frame needs to pick it up. Much cheaper than
933
1022
  * {@link JHGrid.refresh} when nothing about the data itself changed.
@@ -970,7 +1059,7 @@ export declare class JHGrid {
970
1059
  */
971
1060
  setCellValue(row: number, field: string, value: string): void;
972
1061
  /**
973
- * Bulk counterpart to setCellValue() applies every entry through the same
1062
+ * Bulk counterpart to setCellValue() - applies every entry through the same
974
1063
  * pipeline as one edit/undo step and a single redraw, instead of one redraw
975
1064
  * per cell. Use this for large-scale updates (e.g. a header checkbox toggling
976
1065
  * every filtered row) where looping setCellValue() would redraw once per row.
@@ -1023,7 +1112,7 @@ export declare class JHGrid {
1023
1112
  /** Remove a single column filter (text or Set) and reload. */
1024
1113
  removeFilter(field: string): void;
1025
1114
  /**
1026
- * Set the global quick filter (a single term searched across every column interpretation
1115
+ * Set the global quick filter (a single term searched across every column - interpretation
1027
1116
  * is entirely up to fetchData/fetchMeta, via state.quickFilter) and reload. Pass null/''
1028
1117
  * to clear. There is no built-in search box; wire this to your own input.
1029
1118
  */
@@ -1086,7 +1175,7 @@ export declare class JHGrid {
1086
1175
  // Row height
1087
1176
  /** Changes the default pixel height used by every row without its own override. Throws if height is not a positive finite number. */
1088
1177
  setRowHeight(height: number): void;
1089
- /** Sets rowIndex's individual pixel height (Excel-style row resize same as dragging a row-number gutter boundary). Throws if height is not a positive finite number or rowIndex is out of range. */
1178
+ /** Sets rowIndex's individual pixel height (Excel-style row resize - same as dragging a row-number gutter boundary). Throws if height is not a positive finite number or rowIndex is out of range. */
1090
1179
  setRowHeight(rowIndex: number, height: number): void;
1091
1180
  /** Returns rowIndex's current pixel height (its own override, or the shared default). */
1092
1181
  getRowHeight(rowIndex: number): number;
@@ -1114,7 +1203,7 @@ export declare class JHGrid {
1114
1203
  addColumn(field: string, def?: Omit<ColumnDef, 'field'>, opts?: { index?: number }): boolean;
1115
1204
  /**
1116
1205
  * Deletes a column. Local columns (added via addColumn) are removed immediately.
1117
- * Server columns are hidden from view call getDeletedColumns() to collect
1206
+ * Server columns are hidden from view - call getDeletedColumns() to collect
1118
1207
  * their field names for server-side processing, or undeleteColumn() to restore.
1119
1208
  */
1120
1209
  deleteColumn(field: string): boolean;
@@ -1125,7 +1214,7 @@ export declare class JHGrid {
1125
1214
  /** Returns the field names of all server columns marked for deletion. */
1126
1215
  getDeletedColumns(): string[];
1127
1216
  /**
1128
- * Call after your own save request has persisted pending column changes to the server marks
1217
+ * Call after your own save request has persisted pending column changes to the server - marks
1129
1218
  * them as no longer pending (getNewColumns() / getDeletedColumns() stop reporting them) without
1130
1219
  * touching the rendered grid. Pass a field name, an array of field names, or omit to commit every
1131
1220
  * pending column change at once. Fields with no pending add/delete are silently ignored.
@@ -1141,7 +1230,7 @@ export declare class JHGrid {
1141
1230
  *
1142
1231
  * Resolves once the grid is showing that state. If the snapshot carries `sorts`, `filters` or
1143
1232
  * `quickFilter`, those only describe what the *server* should return, so the grid asks again and
1144
- * the promise waits for the answer otherwise the header would claim a filter over rows that
1233
+ * the promise waits for the answer - otherwise the header would claim a filter over rows that
1145
1234
  * were never re-fetched. A snapshot that only moves columns around has nothing to ask for and
1146
1235
  * resolves immediately.
1147
1236
  *
@@ -1158,7 +1247,7 @@ export declare class JHGrid {
1158
1247
  // Export
1159
1248
  /**
1160
1249
  * Download data as a CSV file.
1161
- * By default only loaded (cached) chunks are included pass `full: true`
1250
+ * By default only loaded (cached) chunks are included - pass `full: true`
1162
1251
  * to fetch every row matching the current filter/sort state (text filters
1163
1252
  * via the server, plus any active color filter) and export the complete
1164
1253
  * filtered dataset.
@@ -1175,13 +1264,13 @@ export declare class JHGrid {
1175
1264
  // Row add / delete (client-side)
1176
1265
  /**
1177
1266
  * Adds a new row (client-side only). By default it appends at the bottom; `{ index }` puts it
1178
- * at that visual position instead **anywhere**, including between server rows. Returns the
1267
+ * at that visual position instead - **anywhere**, including between server rows. Returns the
1179
1268
  * zero-based visual index of the new row.
1180
1269
  *
1181
1270
  * The row is anchored to the record it precedes, not to the screen position, so it stays where
1182
1271
  * you put it as rows above are added or removed. A sort or filter is the one thing that moves
1183
1272
  * it: the anchor described a place in the old ordering, which the new one replaces, so the row
1184
- * survives but goes to the end. It is never hidden by a filter a blank new row matches
1273
+ * survives but goes to the end. It is never hidden by a filter - a blank new row matches
1185
1274
  * almost nothing, and hiding it is indistinguishable from having lost it.
1186
1275
  */
1187
1276
  addRow(rowData?: Record<string, unknown>, opts?: { index?: number }): number;
@@ -1196,7 +1285,7 @@ export declare class JHGrid {
1196
1285
  * It is reported by {@link getRemovedRows} and can still be brought back with
1197
1286
  * {@link undeleteRow}.
1198
1287
  *
1199
- * Neither server case touches the server the grid only records what you chose. A sort or
1288
+ * Neither server case touches the server - the grid only records what you chose. A sort or
1200
1289
  * filter discards both kinds, since they are recorded against a row numbering the reload
1201
1290
  * replaces.
1202
1291
  */
@@ -1210,7 +1299,7 @@ export declare class JHGrid {
1210
1299
  /** Returns shallow copies of all locally added rows (via addRow). */
1211
1300
  getNewRows(): Record<string, unknown>[];
1212
1301
  /**
1213
- * Server indices of rows marked for deletion the ones still on screen with a strikethrough.
1302
+ * Server indices of rows marked for deletion - the ones still on screen with a strikethrough.
1214
1303
  * Server indices rather than screen positions, so removing some other row cannot change what
1215
1304
  * this names.
1216
1305
  */
@@ -1218,7 +1307,7 @@ export declare class JHGrid {
1218
1307
  /**
1219
1308
  * Server indices of rows removed from the screen via `deleteRow(i, { permanent: true })`.
1220
1309
  * Kept separate from {@link getDeletedRows} because the two mean different things to whoever
1221
- * chose them a mark is still being decided, a removal has been decided but both still need
1310
+ * chose them - a mark is still being decided, a removal has been decided - but both still need
1222
1311
  * deleting server-side.
1223
1312
  */
1224
1313
  getRemovedRows(): number[];
@@ -1249,7 +1338,7 @@ export declare class DataManager {
1249
1338
  constructor(opts: {
1250
1339
  fetchData: JHGridOptions['fetchData'];
1251
1340
  chunkSize?: number;
1252
- /** LRU limit evicts oldest chunk when exceeded. Default: 50 */
1341
+ /** LRU limit - evicts oldest chunk when exceeded. Default: 50 */
1253
1342
  maxChunks?: number;
1254
1343
  });
1255
1344
  getRow(rowIndex: number): Record<string, unknown> | null;
@@ -1264,7 +1353,7 @@ export declare class DataManager {
1264
1353
  /**
1265
1354
  * Class names applied to the grid's DOM surfaces (the canvas-painted body is styled through
1266
1355
  * {@link GridTheme} instead). Exposed so consumer code can target a surface without hardcoding
1267
- * the strings see "Styling with your own CSS" in the README for the full table and the
1356
+ * the strings - see "Styling with your own CSS" in the README for the full table and the
1268
1357
  * matching `--jhg-*` custom properties.
1269
1358
  */
1270
1359
  export declare const GRID_CLASSES: {
@@ -1278,7 +1367,6 @@ export declare const GRID_CLASSES: {
1278
1367
  menu: string;
1279
1368
  menuItem: string;
1280
1369
  btn: string;
1281
- swatch: string;
1282
1370
  pager: string;
1283
1371
  pagerBtn: string;
1284
1372
  editor: string;