@coreui/data-grid 0.2.0-rc.0 → 0.4.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.
Files changed (143) hide show
  1. package/README.md +4 -2
  2. package/dist/css/data-grid.css +11 -11
  3. package/dist/css/data-grid.css.map +1 -1
  4. package/dist/css/data-grid.min.css +2 -2
  5. package/dist/css/data-grid.min.css.map +1 -1
  6. package/dist/css/data-grid.standalone.css +1066 -0
  7. package/dist/css/data-grid.standalone.css.map +1 -0
  8. package/dist/css/data-grid.standalone.min.css +5 -0
  9. package/dist/css/data-grid.standalone.min.css.map +1 -0
  10. package/dist/js/clipboard.cjs +2 -3
  11. package/dist/js/clipboard.mjs +2 -3
  12. package/dist/js/csv.cjs +5 -6
  13. package/dist/js/csv.cjs.map +1 -1
  14. package/dist/js/csv.mjs +5 -6
  15. package/dist/js/csv.mjs.map +1 -1
  16. package/dist/js/data-grid.esm.js +5685 -5422
  17. package/dist/js/data-grid.esm.js.map +1 -1
  18. package/dist/js/data-grid.esm.min.js +2 -2
  19. package/dist/js/data-grid.esm.min.js.map +1 -1
  20. package/dist/js/data-grid.js +5685 -5423
  21. package/dist/js/data-grid.js.map +1 -1
  22. package/dist/js/data-grid.lite.esm.js +2707 -2629
  23. package/dist/js/data-grid.lite.esm.js.map +1 -1
  24. package/dist/js/data-grid.lite.esm.min.js +2 -2
  25. package/dist/js/data-grid.lite.esm.min.js.map +1 -1
  26. package/dist/js/data-grid.lite.js +2707 -2630
  27. package/dist/js/data-grid.lite.js.map +1 -1
  28. package/dist/js/data-grid.lite.min.js +2 -2
  29. package/dist/js/data-grid.lite.min.js.map +1 -1
  30. package/dist/js/data-grid.lite.mjs +2707 -2629
  31. package/dist/js/data-grid.lite.mjs.map +1 -1
  32. package/dist/js/data-grid.min.js +2 -2
  33. package/dist/js/data-grid.min.js.map +1 -1
  34. package/dist/js/data-grid.mjs +5685 -5422
  35. package/dist/js/data-grid.mjs.map +1 -1
  36. package/dist/js/direction.cjs +25 -0
  37. package/dist/js/direction.cjs.map +1 -0
  38. package/dist/js/direction.mjs +23 -0
  39. package/dist/js/direction.mjs.map +1 -0
  40. package/dist/js/drag.cjs +20 -12
  41. package/dist/js/drag.cjs.map +1 -1
  42. package/dist/js/drag.mjs +20 -12
  43. package/dist/js/drag.mjs.map +1 -1
  44. package/dist/js/{export-table-CNAKStzj.mjs → export-table-BggGRmzQ.mjs} +3 -4
  45. package/dist/js/{export-table-CNAKStzj.mjs.map → export-table-BggGRmzQ.mjs.map} +1 -1
  46. package/dist/js/{export-table-doIwvE5U.cjs → export-table-BiB3_lVo.cjs} +3 -4
  47. package/dist/js/{export-table-doIwvE5U.cjs.map → export-table-BiB3_lVo.cjs.map} +1 -1
  48. package/dist/js/filters.cjs +2 -3
  49. package/dist/js/filters.mjs +2 -3
  50. package/dist/js/labels.cjs +2 -3
  51. package/dist/js/labels.mjs +2 -3
  52. package/dist/js/print.cjs +2 -3
  53. package/dist/js/print.mjs +2 -3
  54. package/dist/js/selector.cjs +22 -0
  55. package/dist/js/selector.cjs.map +1 -0
  56. package/dist/js/selector.mjs +21 -0
  57. package/dist/js/selector.mjs.map +1 -0
  58. package/dist/js/validate.cjs +2 -3
  59. package/dist/js/validate.mjs +2 -3
  60. package/dist/js/xlsx.cjs +5 -7
  61. package/dist/js/xlsx.cjs.map +1 -1
  62. package/dist/js/xlsx.mjs +5 -7
  63. package/dist/js/xlsx.mjs.map +1 -1
  64. package/dist/types/base-component.d.mts +29 -0
  65. package/dist/types/data-grid.d.mts +141 -0
  66. package/dist/types/dom/data.d.mts +15 -0
  67. package/dist/types/dom/event-handler.d.mts +22 -0
  68. package/dist/types/dom/manipulator.d.mts +16 -0
  69. package/dist/types/engine/active-cell.d.mts +33 -0
  70. package/dist/types/engine/active-cell.d.ts +1 -0
  71. package/dist/types/engine/data-source.d.mts +19 -0
  72. package/dist/types/engine/features-lite.d.mts +25 -0
  73. package/dist/types/engine/features.d.mts +35 -0
  74. package/dist/types/engine/scheduler.d.mts +11 -0
  75. package/dist/types/engine/table.d.mts +9 -0
  76. package/dist/types/engine/table.d.ts +1 -1
  77. package/dist/types/engine/validate.d.mts +20 -1
  78. package/dist/types/engine/virtualizer.d.mts +23 -0
  79. package/dist/types/filters.d.mts +13 -1
  80. package/dist/types/icons.d.mts +23 -0
  81. package/dist/types/index-lite.d.mts +19 -1
  82. package/dist/types/index.d.mts +21 -1
  83. package/dist/types/labels.d.mts +9 -1
  84. package/dist/types/types.d.mts +284 -0
  85. package/dist/types/ui/body.d.mts +37 -0
  86. package/dist/types/ui/column-chooser.d.mts +27 -0
  87. package/dist/types/ui/editor.d.mts +8 -0
  88. package/dist/types/ui/filter-menu.d.mts +21 -0
  89. package/dist/types/ui/header.d.mts +51 -0
  90. package/dist/types/ui/loading.d.mts +12 -0
  91. package/dist/types/ui/menu.d.mts +17 -0
  92. package/dist/types/ui/pagination.d.mts +17 -0
  93. package/dist/types/ui/pinning.d.mts +17 -0
  94. package/dist/types/ui/toolbar.d.mts +42 -0
  95. package/dist/types/ui/tooltip.d.mts +10 -0
  96. package/dist/types/util/clipboard.d.mts +29 -1
  97. package/dist/types/util/config.d.mts +28 -0
  98. package/dist/types/util/csv.d.mts +19 -1
  99. package/dist/types/util/debounce.d.mts +7 -0
  100. package/dist/types/util/direction.d.mts +17 -0
  101. package/dist/types/util/direction.d.ts +17 -0
  102. package/dist/types/util/drag.d.mts +48 -1
  103. package/dist/types/util/export-table.d.mts +47 -0
  104. package/dist/types/util/index.d.mts +66 -0
  105. package/dist/types/util/print.d.mts +32 -1
  106. package/dist/types/util/sanitizer.d.mts +15 -0
  107. package/dist/types/util/selector.d.mts +12 -0
  108. package/dist/types/util/selector.d.ts +12 -0
  109. package/dist/types/util/xlsx.d.mts +17 -1
  110. package/dist/types/util/zip.d.mts +11 -0
  111. package/package.json +42 -16
  112. package/scss/_base.scss +13 -0
  113. package/scss/_config.scss +14 -0
  114. package/scss/_functions.scss +1 -0
  115. package/scss/_mixins.scss +1 -0
  116. package/scss/base/_buttons.scss +113 -0
  117. package/scss/base/_forms.scss +157 -0
  118. package/scss/base/_helpers.scss +24 -0
  119. package/scss/base/_pagination.scss +100 -0
  120. package/scss/base/_reboot.scss +93 -0
  121. package/scss/base/_root.scss +85 -0
  122. package/scss/base/_spinner.scss +45 -0
  123. package/scss/base/_table.scss +78 -0
  124. package/scss/components/_data-grid.scss +807 -0
  125. package/scss/data-grid.scss +7 -743
  126. package/scss/data-grid.standalone.scss +16 -0
  127. package/scss/functions/_defaults.scss +25 -0
  128. package/scss/mixins/_tokens.scss +12 -0
  129. package/src/data-grid.ts +22 -6
  130. package/src/engine/active-cell.ts +6 -3
  131. package/src/engine/table.ts +9 -1
  132. package/src/index-lite.ts +1 -1
  133. package/src/index.ts +1 -1
  134. package/src/ui/column-chooser.ts +4 -4
  135. package/src/ui/filter-menu.ts +5 -5
  136. package/src/ui/menu.ts +4 -4
  137. package/src/ui/pagination.ts +1 -1
  138. package/src/ui/tooltip.ts +2 -2
  139. package/src/util/csv.ts +2 -2
  140. package/src/util/direction.ts +38 -0
  141. package/src/util/drag.ts +45 -9
  142. package/src/util/selector.ts +16 -0
  143. package/src/util/xlsx.ts +1 -1
@@ -0,0 +1,16 @@
1
+ /*!
2
+ * CoreUI PRO data-grid.standalone.scss v0.1.0
3
+ * License (https://coreui.io/pro/license/)
4
+ */
5
+
6
+ // The grid plus the base styles it would otherwise borrow from
7
+ // @coreui/coreui(-pro). Load this INSTEAD of data-grid.css when the CoreUI
8
+ // stylesheet is not on the page.
9
+
10
+ // scss-docs-start import-stack
11
+ @forward "config";
12
+ @forward "functions";
13
+ @forward "mixins";
14
+ @forward "base";
15
+ @forward "components/data-grid";
16
+ // scss-docs-end import-stack
@@ -0,0 +1,25 @@
1
+ // Vendored from @coreui/coreui-pro (scss/functions/_defaults.scss, v6 line).
2
+ // Sync manually — do not diverge.
3
+
4
+ @use "sass:map";
5
+ @use "sass:meta";
6
+
7
+ // Merge overrides on top of defaults, stripping null entries.
8
+ // Null values let users remove map keys via @use ... with().
9
+ // Accepts a list as $defaults (converted to a map with `true` values).
10
+ @function defaults($defaults, $overrides) {
11
+ @if meta.type-of($defaults) == "list" {
12
+ $map: ();
13
+ @each $key in $defaults {
14
+ $map: map.merge($map, ($key: true));
15
+ }
16
+ $defaults: $map;
17
+ }
18
+ $merged: map.merge($defaults, $overrides);
19
+ @each $key, $value in $merged {
20
+ @if $value == null {
21
+ $merged: map.remove($merged, $key);
22
+ }
23
+ }
24
+ @return $merged;
25
+ }
@@ -0,0 +1,12 @@
1
+ // Vendored from @coreui/coreui-pro (scss/mixins/_tokens.scss, v6 line).
2
+ // Sync manually — do not diverge.
3
+
4
+ // Output a token map as CSS custom properties
5
+
6
+ // scss-docs-start mixin-tokens
7
+ @mixin tokens($map) {
8
+ @each $prop, $value in $map {
9
+ #{$prop}: #{$value};
10
+ }
11
+ }
12
+ // scss-docs-end mixin-tokens
package/src/data-grid.ts CHANGED
@@ -20,6 +20,7 @@ import {
20
20
  } from './util/drag'
21
21
  import { createScheduler, type Scheduler } from './engine/scheduler'
22
22
  import { createGridTable, subscribeToTableState } from './engine/table'
23
+ import { isElementRTL, syncResizeDirection } from './util/direction'
23
24
  import { createGridVirtualizer, type GridVirtualizer } from './engine/virtualizer'
24
25
  import {
25
26
  DEFAULT_COLUMN_MENU_ICON, DEFAULT_FILTER_ICON, DEFAULT_HIDE_COLUMN_ICON, DEFAULT_MOVE_END_ICON, DEFAULT_MOVE_START_ICON,
@@ -50,6 +51,7 @@ import {
50
51
  } from './util/csv'
51
52
  import { debounce } from './util/debounce'
52
53
  import { DefaultAllowlist, sanitizeHtml, SVGAllowlist } from './util/sanitizer.js'
54
+ import { escapeAttributeValue } from './util/selector'
53
55
 
54
56
  /**
55
57
  * Constants
@@ -134,6 +136,12 @@ const STATE_STORAGE_PREFIX = 'coreui-data-grid:'
134
136
  const STATE_AUTOSAVE_DEBOUNCE_MS = 250
135
137
  const HISTORY_LIMIT = 100
136
138
 
139
+ // Under RTL the two horizontal keys point at the opposite logical neighbour.
140
+ const INVERTED_ARROWS: Record<string, string | undefined> = {
141
+ ArrowLeft: 'ArrowRight',
142
+ ArrowRight: 'ArrowLeft'
143
+ }
144
+
137
145
  const CELL_SELECTION_DIRECTIONS: Record<string, 'up' | 'down' | 'left' | 'right' | undefined> = {
138
146
  ArrowDown: 'down',
139
147
  ArrowLeft: 'left',
@@ -567,7 +575,7 @@ private _undoStack: DataGridEditCommitEntry[] = []
567
575
 
568
576
  private _createTable(): DataGridTable {
569
577
  this._unsubscribeState?.()
570
- const table = createGridTable(this._config)
578
+ const table = createGridTable(this._config, () => this._element)
571
579
  this._unsubscribeState = subscribeToTableState(table, (previous, next) => {
572
580
  this._notifyStateChange(previous, next)
573
581
  this._scheduler.schedule()
@@ -1293,7 +1301,9 @@ private _undoStack: DataGridEditCommitEntry[] = []
1293
1301
  // Shift+arrow grows the range from its anchor and leaves focus put; a
1294
1302
  // plain arrow moves focus and collapses the range to the new cell.
1295
1303
  if (this._config.cellSelection && found.cell.rowIndex >= 0 && result.cell.rowIndex >= 0) {
1296
- const direction = CELL_SELECTION_DIRECTIONS[event.key]
1304
+ const direction = CELL_SELECTION_DIRECTIONS[
1305
+ isElementRTL(this._element) ? (INVERTED_ARROWS[event.key] ?? event.key) : event.key
1306
+ ]
1297
1307
  if (event.shiftKey && direction) {
1298
1308
  this._table.extendCellSelection(direction)
1299
1309
  this._scheduler.schedule()
@@ -1643,19 +1653,20 @@ private _undoStack: DataGridEditCommitEntry[] = []
1643
1653
  pageStep: paginated ?
1644
1654
  this._table.store.state.pagination.pageSize :
1645
1655
  Math.max(1, viewportRows || rowCount),
1646
- paginated
1656
+ paginated,
1657
+ rtl: isElementRTL(this._element)
1647
1658
  }
1648
1659
  }
1649
1660
 
1650
1661
  private _cellElementFor(cell: ActiveCell): HTMLElement | null {
1651
1662
  if (cell.rowIndex === -1) {
1652
1663
  return this._header.element.querySelector<HTMLElement>(
1653
- `tr:first-child th[data-coreui-column="${cell.columnId}"]`
1664
+ `tr:first-child th[data-coreui-column="${escapeAttributeValue(cell.columnId)}"]`
1654
1665
  )
1655
1666
  }
1656
1667
 
1657
- const rowElement = this._body.element.querySelector<HTMLElement>(`tr[data-coreui-index="${cell.rowIndex}"]`)
1658
- return rowElement?.querySelector<HTMLElement>(`td[data-coreui-column="${cell.columnId}"]`) ?? null
1668
+ const rowElement = this._body.element.querySelector<HTMLElement>(`tr[data-coreui-index="${escapeAttributeValue(cell.rowIndex)}"]`)
1669
+ return rowElement?.querySelector<HTMLElement>(`td[data-coreui-column="${escapeAttributeValue(cell.columnId)}"]`) ?? null
1659
1670
  }
1660
1671
 
1661
1672
  // The roving-tabindex pass: runs after every render/recycle flush, in the
@@ -1975,6 +1986,11 @@ private _undoStack: DataGridEditCommitEntry[] = []
1975
1986
  event.preventDefault()
1976
1987
  }
1977
1988
 
1989
+ // The direction table-core snapshotted at construction may be stale — an
1990
+ // app that swaps language flips dir on a live grid. A resize is the only
1991
+ // moment it matters, and it is rare enough to re-read here.
1992
+ syncResizeDirection(this._table, this._element)
1993
+
1978
1994
  const header = this._table.getHeaderGroups()[0]?.headers.find(item => item.column.id === key)
1979
1995
  header?.getResizeHandler()(event)
1980
1996
  }
@@ -23,6 +23,9 @@ export interface ActiveCellContext {
23
23
  // page size (paginated).
24
24
  pageStep: number
25
25
  paginated: boolean
26
+ // Writing direction of the grid. Arrow keys are physical, column order is
27
+ // logical, so under RTL the two horizontal keys trade places.
28
+ rtl?: boolean
26
29
  }
27
30
 
28
31
  export interface NavigationResult {
@@ -72,7 +75,7 @@ export const resolveNavigation = (
72
75
  current: ActiveCell,
73
76
  context: ActiveCellContext
74
77
  ): NavigationResult | null => {
75
- const { columnIds, rowCount, pageStep, paginated } = context
78
+ const { columnIds, rowCount, pageStep, paginated, rtl } = context
76
79
  const columnIndex = columnIds.indexOf(current.columnId)
77
80
  if (columnIndex === -1 || columnIds.length === 0) {
78
81
  return null
@@ -86,11 +89,11 @@ export const resolveNavigation = (
86
89
 
87
90
  switch (key) {
88
91
  case 'ArrowLeft': {
89
- return cell(current.rowIndex, Math.max(columnIndex - 1, 0))
92
+ return cell(current.rowIndex, rtl ? Math.min(columnIndex + 1, lastColumn) : Math.max(columnIndex - 1, 0))
90
93
  }
91
94
 
92
95
  case 'ArrowRight': {
93
- return cell(current.rowIndex, Math.min(columnIndex + 1, lastColumn))
96
+ return cell(current.rowIndex, rtl ? Math.max(columnIndex - 1, 0) : Math.min(columnIndex + 1, lastColumn))
94
97
  }
95
98
 
96
99
  case 'ArrowUp': {
@@ -7,6 +7,7 @@
7
7
 
8
8
  import { constructTable, type FilterFn } from '@tanstack/table-core'
9
9
  import { evaluateColumnFilter } from '../filters'
10
+ import { isElementRTL } from '../util/direction'
10
11
  import type {
11
12
  DataGridColumn, DataGridColumnFilterValue, DataGridFeatures, DataGridItem, DataGridOptions,
12
13
  DataGridTable, DataGridTableState
@@ -27,7 +28,10 @@ const toTanStackFilterFn = (column: DataGridColumn): FilterFn<DataGridFeatures,
27
28
  evaluateColumnFilter(row.getValue(columnId), filterValue as DataGridColumnFilterValue, column.filterType)
28
29
  }
29
30
 
30
- export const createGridTable = (options: DataGridOptions): DataGridTable => {
31
+ export const createGridTable = (
32
+ options: DataGridOptions,
33
+ getElement?: () => Element | null
34
+ ): DataGridTable => {
31
35
  const sortingOptions = typeof options.sorting === 'object' ? options.sorting : {}
32
36
  const sizingOptions = typeof options.columnSizing === 'object' ? options.columnSizing : {}
33
37
  const paginationOptions = typeof options.pagination === 'object' ? options.pagination : {}
@@ -112,6 +116,10 @@ export const createGridTable = (options: DataGridOptions): DataGridTable => {
112
116
  }
113
117
  }),
114
118
  columnResizeMode: sizingOptions.mode ?? 'onChange',
119
+ // Construction-time only — table-core copies the options object, so a
120
+ // getter here would never be re-read. A direction flipped later is picked
121
+ // up by refreshing this through setOptions when a resize starts.
122
+ columnResizeDirection: isElementRTL(getElement?.()) ? 'rtl' : 'ltr',
115
123
  data: serverSide ? [] : options.items,
116
124
  enableCellSelection: Boolean(options.cellSelection),
117
125
  enableColumnFilters: Boolean(options.columnFilters),
package/src/index-lite.ts CHANGED
@@ -20,7 +20,7 @@ class DataGrid<TFeatures extends TableFeatures = DataGridLiteFeatures> extends D
20
20
  // The lite preset is deliberately narrower than the options' invariant
21
21
  // features type - the coverage assertion, not the type, polices a config
22
22
  // the set cannot serve.
23
- return { ...DataGridBase.Default, features: dataGridLiteFeatures as unknown as DataGridFeatures }
23
+ return { ...super.Default, features: dataGridLiteFeatures as unknown as DataGridFeatures }
24
24
  }
25
25
  }
26
26
 
package/src/index.ts CHANGED
@@ -16,7 +16,7 @@ import { defineJQueryPlugin } from './util/index.js'
16
16
  // keeps the full set out of the lite bundle.
17
17
  class DataGrid<TFeatures extends TableFeatures = DataGridFeatures> extends DataGridBase<TFeatures> {
18
18
  static get Default(): DataGridOptions {
19
- return { ...DataGridBase.Default, features: dataGridFeatures }
19
+ return { ...super.Default, features: dataGridFeatures }
20
20
  }
21
21
  }
22
22
 
@@ -57,8 +57,8 @@ export const createColumnChooser = (context: GridColumnChooserContext): GridColu
57
57
 
58
58
  trigger = null
59
59
  element.remove()
60
- document.removeEventListener('pointerdown', onOutsidePointerDown, true)
61
- window.removeEventListener('scroll', onWindowChange, true)
60
+ document.removeEventListener('pointerdown', onOutsidePointerDown, { capture: true })
61
+ window.removeEventListener('scroll', onWindowChange, { capture: true })
62
62
  window.removeEventListener('resize', onWindowChange)
63
63
  }
64
64
 
@@ -156,8 +156,8 @@ export const createColumnChooser = (context: GridColumnChooserContext): GridColu
156
156
  element.style.top = `${buttonRect.bottom - rootRect.top}px`
157
157
  element.style.left = `${Math.max(buttonRect.left - rootRect.left, 0)}px`
158
158
 
159
- document.addEventListener('pointerdown', onOutsidePointerDown, true)
160
- window.addEventListener('scroll', onWindowChange, true)
159
+ document.addEventListener('pointerdown', onOutsidePointerDown, { capture: true })
160
+ window.addEventListener('scroll', onWindowChange, { capture: true })
161
161
  window.addEventListener('resize', onWindowChange)
162
162
  element.querySelector<HTMLElement>('input:not(:disabled)')?.focus()
163
163
  }
@@ -82,8 +82,8 @@ export const createFilterMenu = (context: GridFilterMenuContext): GridFilterMenu
82
82
 
83
83
  trigger = null
84
84
  element.remove()
85
- document.removeEventListener('pointerdown', onOutsidePointerDown, true)
86
- window.removeEventListener('scroll', onWindowChange, true)
85
+ document.removeEventListener('pointerdown', onOutsidePointerDown, { capture: true })
86
+ window.removeEventListener('scroll', onWindowChange, { capture: true })
87
87
  window.removeEventListener('resize', onWindowChange)
88
88
  }
89
89
 
@@ -157,7 +157,6 @@ export const createFilterMenu = (context: GridFilterMenuContext): GridFilterMenu
157
157
  const readCondition = (row: HTMLElement): DataGridFilterCondition | null => {
158
158
  const operator = row.querySelector('select')!.value as DataGridFilterOperator
159
159
  const { value } = (row.querySelector<HTMLInputElement>('[data-coreui-filter-value]')!)
160
- const value2 = row.querySelector<HTMLInputElement>('[data-coreui-filter-value2]')!.value
161
160
 
162
161
  if (VALUELESS_OPERATORS.has(operator)) {
163
162
  return { operator }
@@ -167,6 +166,7 @@ export const createFilterMenu = (context: GridFilterMenuContext): GridFilterMenu
167
166
  return null
168
167
  }
169
168
 
169
+ const value2 = row.querySelector<HTMLInputElement>('[data-coreui-filter-value2]')!.value
170
170
  return {
171
171
  operator,
172
172
  value,
@@ -371,8 +371,8 @@ export const createFilterMenu = (context: GridFilterMenuContext): GridFilterMenu
371
371
  element.style.top = `${buttonRect.bottom - rootRect.top}px`
372
372
  element.style.left = `${Math.max(buttonRect.right - rootRect.left - element.offsetWidth, 0)}px`
373
373
 
374
- document.addEventListener('pointerdown', onOutsidePointerDown, true)
375
- window.addEventListener('scroll', onWindowChange, true)
374
+ document.addEventListener('pointerdown', onOutsidePointerDown, { capture: true })
375
+ window.addEventListener('scroll', onWindowChange, { capture: true })
376
376
  window.addEventListener('resize', onWindowChange)
377
377
  element.querySelector<HTMLElement>('select, input')?.focus()
378
378
  }
package/src/ui/menu.ts CHANGED
@@ -49,8 +49,8 @@ export const createMenu = (context: GridMenuContext): GridMenu => {
49
49
 
50
50
  trigger = null
51
51
  element.remove()
52
- document.removeEventListener('pointerdown', onOutsidePointerDown, true)
53
- window.removeEventListener('scroll', onWindowChange, true)
52
+ document.removeEventListener('pointerdown', onOutsidePointerDown, { capture: true })
53
+ window.removeEventListener('scroll', onWindowChange, { capture: true })
54
54
  window.removeEventListener('resize', onWindowChange)
55
55
  }
56
56
 
@@ -191,8 +191,8 @@ export const createMenu = (context: GridMenuContext): GridMenu => {
191
191
  // sits at the cell's right edge, where a left-aligned menu would overflow.
192
192
  element.style.left = `${Math.max(buttonRect.right - rootRect.left - element.offsetWidth, 0)}px`
193
193
 
194
- document.addEventListener('pointerdown', onOutsidePointerDown, true)
195
- window.addEventListener('scroll', onWindowChange, true)
194
+ document.addEventListener('pointerdown', onOutsidePointerDown, { capture: true })
195
+ window.addEventListener('scroll', onWindowChange, { capture: true })
196
196
  window.addEventListener('resize', onWindowChange)
197
197
  items()[0]?.focus()
198
198
  }
@@ -137,7 +137,7 @@ export const createPagination = (context: GridPaginationContext): GridPagination
137
137
 
138
138
  // A configured pageSize outside pageSizeOptions would leave the select
139
139
  // blank; surface it as an extra option in numeric position.
140
- if (![...select.options].some(option => option.value === String(state.pageSize))) {
140
+ if ([...select.options].every(option => option.value !== String(state.pageSize))) {
141
141
  const option = document.createElement('option')
142
142
  option.value = String(state.pageSize)
143
143
  option.textContent = String(state.pageSize)
package/src/ui/tooltip.ts CHANGED
@@ -96,7 +96,7 @@ export const createTooltip = (root: HTMLElement): GridTooltip => {
96
96
  root.addEventListener('pointerdown', hide)
97
97
  root.addEventListener('focusin', onFocusIn as EventListener)
98
98
  root.addEventListener('focusout', hide)
99
- window.addEventListener('scroll', hide, true)
99
+ window.addEventListener('scroll', hide, { capture: true })
100
100
 
101
101
  return {
102
102
  dispose() {
@@ -106,7 +106,7 @@ export const createTooltip = (root: HTMLElement): GridTooltip => {
106
106
  root.removeEventListener('pointerdown', hide)
107
107
  root.removeEventListener('focusin', onFocusIn as EventListener)
108
108
  root.removeEventListener('focusout', hide)
109
- window.removeEventListener('scroll', hide, true)
109
+ window.removeEventListener('scroll', hide, { capture: true })
110
110
  element.remove()
111
111
  }
112
112
  }
package/src/util/csv.ts CHANGED
@@ -43,12 +43,12 @@ export const exportCsv = (table: CsvTable, options: CsvOptions = {}): string =>
43
43
  const value = row.getValue(column.id)
44
44
  const formatter = formatterFor(column)
45
45
  // Coerce: a formatter may return a number/null in plain-JS usage.
46
- return formatter ? String(formatter(value, row.original) ?? '') : String(value ?? '')
46
+ return String(formatter ? formatter(value, row.original) ?? '' : value ?? '')
47
47
  }))
48
48
  )
49
49
 
50
50
  const csv = [header, ...body].join('\r\n')
51
- return options.bom ? `\uFEFF${csv}` : csv
51
+ return options.bom ? `\u{FEFF}${csv}` : csv
52
52
  }
53
53
 
54
54
  export const downloadCsv = (table: CsvTable, options: CsvDownloadOptions = {}): void => {
@@ -0,0 +1,38 @@
1
+ /**
2
+ * --------------------------------------------------------------------------
3
+ * CoreUI PRO data-grid util/direction.ts
4
+ * License (https://coreui.io/pro/license/)
5
+ * --------------------------------------------------------------------------
6
+ */
7
+
8
+ // Read from the element rather than the document: a grid can sit inside an RTL
9
+ // subtree of an LTR page, which is why the vendored isRTL() (it checks
10
+ // documentElement.dir) is not the helper for any of this.
11
+ //
12
+ // Angular prerenders in Node, where there is no getComputedStyle — and no
13
+ // layout to have a direction. LTR is the right answer there: the server-side
14
+ // pass emits logical markup either way, and the client corrects on hydration.
15
+ export const isElementRTL = (element: Element | null | undefined): boolean =>
16
+ typeof getComputedStyle === 'function' &&
17
+ element !== null &&
18
+ element !== undefined &&
19
+ getComputedStyle(element).direction === 'rtl'
20
+
21
+ interface ResizeDirectionTable {
22
+ options: { columnResizeDirection?: 'ltr' | 'rtl' }
23
+ setOptions: (updater: (previous: { columnResizeDirection?: 'ltr' | 'rtl' }) => never) => void
24
+ }
25
+
26
+ // table-core copies the options object at construction, so the direction it
27
+ // captured goes stale the moment an app swaps language on a live grid. A
28
+ // resize is the only place the value is read, and starting one is rare enough
29
+ // to re-read the element here.
30
+ export const syncResizeDirection = (
31
+ table: ResizeDirectionTable,
32
+ element: Element | null | undefined
33
+ ): void => {
34
+ const direction = isElementRTL(element) ? 'rtl' : 'ltr'
35
+ if (table.options.columnResizeDirection !== direction) {
36
+ table.setOptions(previous => ({ ...previous, columnResizeDirection: direction }) as never)
37
+ }
38
+ }
package/src/util/drag.ts CHANGED
@@ -5,6 +5,8 @@
5
5
  * --------------------------------------------------------------------------
6
6
  */
7
7
 
8
+ import { escapeAttributeValue } from './selector'
9
+
8
10
  // The one drag implementation behind every reorder in the grid: column drag
9
11
  // (axis 'x', over the header) and row drag (axis 'y', over the body). It is
10
12
  // headless in the sense that matters — it never reads the table, only what the
@@ -124,6 +126,17 @@ export const createDragController = (options: DragControllerOptions): DragContro
124
126
 
125
127
  let sourceHit: DragHit | null = null
126
128
  let dragging = false
129
+ // +1 while the axis runs the way the coordinates do, -1 under RTL, where the
130
+ // logical column order runs against increasing x. Negating the axis keeps
131
+ // every comparison below — the zone tiling, the hysteresis band, both edge
132
+ // clamps, `index > fromIndex` — written once, for one direction. Only the
133
+ // three places that touch pixels again convert back: the indicator's side,
134
+ // the preview transform and the settling ghost.
135
+ //
136
+ // Read from the grid's own computed direction rather than the document's: a
137
+ // grid can sit in an RTL subtree of an LTR page, which is exactly the case
138
+ // the vendored isRTL() cannot see.
139
+ let axisSign = 1
127
140
  let origin = { x: 0, y: 0 }
128
141
  let pointer = { x: 0, y: 0 }
129
142
  let drop: DragDrop | null = null
@@ -147,6 +160,18 @@ export const createDragController = (options: DragControllerOptions): DragContro
147
160
  const contentStarts = new Map<string, number>()
148
161
  const contentSizes = new Map<string, number>()
149
162
 
163
+ // A rect's logical start: its leading edge along the axis, in the negated
164
+ // space when the axis is negated, so the span stays [start, start + size].
165
+ const logicalStart = (rect: DOMRect): number => {
166
+ if (!horizontal) {
167
+ return rect.top
168
+ }
169
+
170
+ return axisSign === 1 ? rect.left : -rect.right
171
+ }
172
+
173
+ const logicalPointer = (): number => (horizontal ? pointer.x * axisSign : pointer.y)
174
+
150
175
  const stopAutoScroll = () => {
151
176
  if (scrollFrame) {
152
177
  cancelAnimationFrame(scrollFrame)
@@ -212,7 +237,9 @@ export const createDragController = (options: DragControllerOptions): DragContro
212
237
  const rootRect = root.getBoundingClientRect()
213
238
  indicator.hidden = false
214
239
  if (horizontal) {
215
- indicator.style.left = `${after ? rect.right : rect.left}px`
240
+ // `after` is logical; which pixel edge that is flips with the axis.
241
+ const trailing = after === (axisSign === 1)
242
+ indicator.style.left = `${trailing ? rect.right : rect.left}px`
216
243
  indicator.style.top = `${rootRect.top}px`
217
244
  indicator.style.height = `${rootRect.height}px`
218
245
  } else {
@@ -224,6 +251,7 @@ export const createDragController = (options: DragControllerOptions): DragContro
224
251
 
225
252
  const startDrag = () => {
226
253
  dragging = true
254
+ axisSign = horizontal && getComputedStyle(root).direction === 'rtl' ? -1 : 1
227
255
  // Chrome focuses a button on mousedown (a press on a header lands on the
228
256
  // sort button), and tearing a FOCUSED element out of the DOM — which the
229
257
  // drop's header re-append does — makes the browser reset the scroller it
@@ -284,7 +312,11 @@ export const createDragController = (options: DragControllerOptions): DragContro
284
312
  }
285
313
 
286
314
  const rect = scrollElement.getBoundingClientRect()
287
- return horizontal ? rect.left - scrollElement.scrollLeft : rect.top - scrollElement.scrollTop
315
+ // scrollLeft stays a physical axis in every engine (RTL just runs it
316
+ // negative), so it enters the negated space through the sign.
317
+ return horizontal ?
318
+ logicalStart(rect) - (axisSign * scrollElement.scrollLeft) :
319
+ rect.top - scrollElement.scrollTop
288
320
  }
289
321
 
290
322
  const captureItem = (id: string, origin: number): boolean => {
@@ -296,7 +328,7 @@ export const createDragController = (options: DragControllerOptions): DragContro
296
328
  const rect = element.getBoundingClientRect()
297
329
  // Corrected by the applied delta for items first seen mid-preview (a row
298
330
  // revealed by auto-scroll); at drag start every delta is zero.
299
- const start = (horizontal ? rect.left : rect.top) - (previewDeltas.get(id) ?? 0)
331
+ const start = logicalStart(rect) - (previewDeltas.get(id) ?? 0)
300
332
  contentStarts.set(id, start - origin)
301
333
  contentSizes.set(id, horizontal ? rect.width : rect.height)
302
334
  return true
@@ -312,7 +344,7 @@ export const createDragController = (options: DragControllerOptions): DragContro
312
344
  }
313
345
 
314
346
  const updateTarget = () => {
315
- const position = horizontal ? pointer.x : pointer.y
347
+ const position = logicalPointer()
316
348
  const origin = contentOrigin()
317
349
 
318
350
  const zones: Array<{ id: string; start: number; size: number }> = []
@@ -477,7 +509,9 @@ export const createDragController = (options: DragControllerOptions): DragContro
477
509
  }
478
510
 
479
511
  for (const element of options.items.getElements(id)) {
480
- element.style.transform = horizontal ? `translateX(${delta}px)` : `translateY(${delta}px)`
512
+ element.style.transform = horizontal ?
513
+ `translateX(${axisSign * delta}px)` :
514
+ `translateY(${delta}px)`
481
515
  element.classList.add(CLASS_NAME_PREVIEW)
482
516
  }
483
517
 
@@ -531,7 +565,9 @@ export const createDragController = (options: DragControllerOptions): DragContro
531
565
  }
532
566
 
533
567
  const delta = previewDeltas.get(sourceHit.id) ?? 0
534
- return horizontal ? { x: rect.left - delta, y: rect.top } : { x: rect.left, y: rect.top - delta }
568
+ return horizontal ?
569
+ { x: rect.left - (axisSign * delta), y: rect.top } :
570
+ { x: rect.left, y: rect.top - delta }
535
571
  }
536
572
 
537
573
  const cleanup = () => {
@@ -692,7 +728,7 @@ const columnItems = (root: HTMLElement): DragItems => ({
692
728
  [...root.querySelectorAll<HTMLTableCellElement>('thead tr:first-child th[data-coreui-column]')]
693
729
  .map(cell => cell.dataset.coreuiColumn ?? ''),
694
730
  getElements(id) {
695
- const header = root.querySelector<HTMLTableCellElement>(`thead th[data-coreui-column="${id}"]`)
731
+ const header = root.querySelector<HTMLTableCellElement>(`thead th[data-coreui-column="${escapeAttributeValue(id)}"]`)
696
732
  if (!header) {
697
733
  return []
698
734
  }
@@ -741,7 +777,7 @@ export const createColumnDragController = (options: ColumnDragOptions): DragCont
741
777
  // consumer's own veto — an immovable column is not somewhere to land.
742
778
  canDrop: (sourceId, targetId) =>
743
779
  sourceId !== targetId &&
744
- Boolean(options.root.querySelector(`th[data-coreui-column="${targetId}"][data-coreui-draggable]`)) &&
780
+ Boolean(options.root.querySelector(`th[data-coreui-column="${escapeAttributeValue(targetId)}"][data-coreui-draggable]`)) &&
745
781
  options.canDrop(sourceId, targetId),
746
782
  preview: true,
747
783
  onDrop: (sourceKey, drop) => options.onReorder(sourceKey, drop.id, drop.after)
@@ -754,7 +790,7 @@ const rowItems = (root: HTMLElement): DragItems => ({
754
790
  getOrderedIds: () =>
755
791
  [...root.querySelectorAll<HTMLElement>(`tbody ${SELECTOR_ROW}`)].map(row => row.dataset.coreuiIndex ?? ''),
756
792
  getElements(id) {
757
- const row = root.querySelector<HTMLElement>(`tbody tr[data-coreui-index="${id}"]`)
793
+ const row = root.querySelector<HTMLElement>(`tbody tr[data-coreui-index="${escapeAttributeValue(id)}"]`)
758
794
  return row ? [row] : []
759
795
  }
760
796
  })
@@ -0,0 +1,16 @@
1
+ /**
2
+ * --------------------------------------------------------------------------
3
+ * CoreUI PRO data-grid util/selector.ts
4
+ * License (https://coreui.io/pro/license/)
5
+ * --------------------------------------------------------------------------
6
+ */
7
+
8
+ /**
9
+ * Escapes a column key or row index for a double-quoted CSS attribute selector value.
10
+ * @param value The key or index interpolated into `[data-…="…"]`.
11
+ * @returns The value as a string, with `"` and `\` backslash-escaped and line breaks as hex escapes.
12
+ */
13
+ export const escapeAttributeValue = (value: string | number): string =>
14
+ String(value)
15
+ .replace(/["\\]/g, '\\$&')
16
+ .replace(/[\n\r\f]/g, character => `\\${character.codePointAt(0)!.toString(16)} `)
package/src/util/xlsx.ts CHANGED
@@ -33,7 +33,7 @@ const MAX_COLUMN_WIDTH = 120
33
33
  // XML 1.0 forbids most control characters outright - Excel refuses to open a
34
34
  // file containing them, so they are dropped rather than escaped.
35
35
  // eslint-disable-next-line no-control-regex -- matching them is the point
36
- const CONTROL_CHARACTERS = /[\u0000-\u0008\u000B\u000C\u000E-\u001F]/g
36
+ const CONTROL_CHARACTERS = /[\u{0}-\u{8}\u{B}\u{C}\u{E}-\u{1F}]/gu
37
37
 
38
38
  const escapeXml = (value: string): string =>
39
39
  value