kempo-ui 0.0.2 → 0.0.4

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 (278) hide show
  1. package/.github/copilot-instructions.md +149 -149
  2. package/.github/workflows/publish-major.yml +39 -0
  3. package/.github/workflows/publish-minor.yml +39 -0
  4. package/.github/workflows/{publish-npm.yml → publish-patch.yml} +44 -44
  5. package/.vscode/settings.json +2 -0
  6. package/CONTRIBUTING.md +149 -149
  7. package/README.md +0 -0
  8. package/dist/kempo-hljs.css +1 -0
  9. package/dist/kempo-vars.css +0 -0
  10. package/dist/src/components/Accordion.js +36 -0
  11. package/dist/src/components/Card.js +37 -0
  12. package/dist/src/components/Collapsible.js +41 -0
  13. package/dist/src/components/ContentSlider.js +44 -0
  14. package/dist/src/components/Dialog.js +113 -0
  15. package/dist/src/components/FocusCapture.js +7 -0
  16. package/dist/src/components/Icon.js +10 -0
  17. package/dist/src/components/Import.js +1 -0
  18. package/dist/src/components/PersistantCollapsible.js +1 -0
  19. package/dist/src/components/PhotoViewer.js +135 -0
  20. package/dist/src/components/Resize.js +96 -0
  21. package/dist/src/components/ShadowComponent.js +1 -0
  22. package/dist/src/components/ShowMore.js +42 -0
  23. package/dist/src/components/SideMenu.js +97 -0
  24. package/dist/src/components/Split.js +84 -0
  25. package/dist/src/components/Tabs.js +155 -0
  26. package/dist/src/components/Tags.js +69 -0
  27. package/dist/src/components/ThemeSwitcher.js +23 -0
  28. package/dist/src/components/Timestamp.js +1 -0
  29. package/dist/src/lit-all.min.js +120 -0
  30. package/dist/src/utils/debounce.js +1 -0
  31. package/dist/src/utils/drag.js +1 -0
  32. package/dist/src/utils/formatTimestamp.js +1 -0
  33. package/dist/src/utils/propConverters.js +1 -0
  34. package/dist/src/utils/watchWindowSize.js +1 -0
  35. package/docs/components/accordion.html +166 -0
  36. package/docs/components/card.html +92 -0
  37. package/docs/components/collapsible.html +198 -0
  38. package/docs/components/content-slider.html +223 -0
  39. package/docs/components/dialog.html +251 -0
  40. package/docs/components/focus-capture.html +67 -0
  41. package/docs/components/icon.html +158 -0
  42. package/docs/components/import.html +73 -0
  43. package/docs/components/persistant-collapsible.html +123 -0
  44. package/docs/components/photo-viewer.html +226 -0
  45. package/docs/components/resize.html +178 -0
  46. package/docs/components/show-more.html +129 -0
  47. package/docs/components/side-menu.html +159 -0
  48. package/docs/components/split.html +147 -0
  49. package/docs/components/table.html +312 -0
  50. package/docs/components/tableControls.html +77 -0
  51. package/docs/components/tableCustomFields.html +113 -0
  52. package/docs/components/tableFetchRecords.html +114 -0
  53. package/docs/components/tableFieldSortHide.html +75 -0
  54. package/docs/components/tablePagination.html +100 -0
  55. package/docs/components/tableRecordEditing.html +144 -0
  56. package/docs/components/tableRecordFiltering.html +88 -0
  57. package/docs/components/tableRecordHiding.html +80 -0
  58. package/docs/components/tableRecordSearching.html +75 -0
  59. package/docs/components/tableRecordSelection.html +75 -0
  60. package/docs/components/tableRowControls.html +78 -0
  61. package/docs/components/tableSorting.html +73 -0
  62. package/docs/components/tabs.html +180 -0
  63. package/docs/components/tags.html +110 -0
  64. package/docs/components/theme-switcher.html +126 -0
  65. package/docs/components/timestamp.html +82 -0
  66. package/docs/components/toast.html +319 -0
  67. package/docs/components/toggle.html +156 -0
  68. package/docs/dev.config.json +20 -0
  69. package/docs/icons/add.svg +1 -0
  70. package/docs/icons/arrow-back.svg +1 -0
  71. package/docs/icons/arrow-down-double.svg +1 -0
  72. package/docs/icons/arrow-down.svg +1 -0
  73. package/docs/icons/arrow-forward.svg +1 -0
  74. package/docs/icons/arrow-up-double.svg +1 -0
  75. package/docs/icons/arrow-up.svg +1 -0
  76. package/docs/icons/cards.svg +1 -0
  77. package/docs/icons/check.svg +1 -0
  78. package/docs/icons/chevron-left.svg +1 -0
  79. package/docs/icons/chevron-right.svg +1 -0
  80. package/docs/icons/close.svg +2 -0
  81. package/docs/icons/compare.svg +1 -0
  82. package/docs/icons/delete.svg +1 -0
  83. package/docs/icons/drag-handle.svg +1 -0
  84. package/docs/icons/drawing2.svg +124 -0
  85. package/docs/icons/edit.svg +1 -0
  86. package/docs/icons/error.svg +1 -0
  87. package/docs/icons/export-file.svg +1 -0
  88. package/docs/icons/file-text.svg +1 -0
  89. package/docs/icons/file.svg +1 -0
  90. package/docs/icons/filter-off.svg +1 -0
  91. package/docs/icons/filter.svg +1 -0
  92. package/docs/icons/first.svg +1 -0
  93. package/docs/icons/folder-clear.svg +1 -0
  94. package/docs/icons/folder-create.svg +1 -0
  95. package/docs/icons/folder-open.svg +1 -0
  96. package/docs/icons/folder.svg +1 -0
  97. package/docs/icons/hide.svg +1 -0
  98. package/docs/icons/image.svg +1 -0
  99. package/docs/icons/label-add.svg +1 -0
  100. package/docs/icons/label.svg +1 -0
  101. package/docs/icons/last.svg +1 -0
  102. package/docs/icons/menu.svg +1 -0
  103. package/docs/icons/mode-auto.svg +1 -0
  104. package/docs/icons/mode-dark.svg +1 -0
  105. package/docs/icons/mode-light.svg +1 -0
  106. package/docs/icons/open-in-browser.svg +1 -0
  107. package/docs/icons/pause.svg +1 -0
  108. package/docs/icons/play.svg +1 -0
  109. package/docs/icons/refresh.svg +1 -0
  110. package/docs/icons/remove.svg +1 -0
  111. package/docs/icons/save.svg +1 -0
  112. package/docs/icons/search.svg +1 -0
  113. package/docs/icons/settings.svg +1 -0
  114. package/docs/icons/show.svg +1 -0
  115. package/docs/icons/storage.svg +1 -0
  116. package/docs/icons/table-visibility.svg +1 -0
  117. package/docs/icons/tag.svg +1 -0
  118. package/docs/icons/tools.svg +1 -0
  119. package/docs/icons/trash-x.svg +1 -0
  120. package/docs/icons/warning.svg +1 -0
  121. package/docs/index.html +28 -21
  122. package/docs/kempo-hljs.css +1 -0
  123. package/docs/media/civic.jpg +0 -0
  124. package/docs/media/corvette.jpg +0 -0
  125. package/docs/media/evo.jpg +0 -0
  126. package/docs/media/gtr.jpg +0 -0
  127. package/docs/media/nsx.jpg +0 -0
  128. package/docs/nav.inc.html +67 -0
  129. package/docs/nav.inc.js +4 -0
  130. package/{config/production.json → docs/prod.config.json} +20 -16
  131. package/docs/src/components/Accordion.js +36 -0
  132. package/docs/src/components/Card.js +37 -0
  133. package/docs/src/components/Collapsible.js +41 -0
  134. package/docs/src/components/ContentSlider.js +44 -0
  135. package/docs/src/components/Dialog.js +113 -0
  136. package/docs/src/components/FocusCapture.js +7 -0
  137. package/docs/src/components/HybridComponent.js +1 -0
  138. package/docs/src/components/Icon.js +10 -0
  139. package/docs/src/components/Import.js +1 -0
  140. package/docs/src/components/LightComponent.js +1 -0
  141. package/docs/src/components/PersistantCollapsible.js +1 -0
  142. package/docs/src/components/PhotoViewer.js +135 -0
  143. package/docs/src/components/Resize.js +96 -0
  144. package/docs/src/components/ShadowComponent.js +1 -0
  145. package/docs/src/components/ShowMore.js +42 -0
  146. package/docs/src/components/SideMenu.js +97 -0
  147. package/docs/src/components/Split.js +84 -0
  148. package/docs/src/components/Tabs.js +155 -0
  149. package/docs/src/components/Tags.js +69 -0
  150. package/docs/src/components/ThemeSwitcher.js +23 -0
  151. package/docs/src/components/Timestamp.js +1 -0
  152. package/docs/src/lit-all.min.js +120 -0
  153. package/docs/src/utils/debounce.js +1 -0
  154. package/docs/src/utils/drag.js +1 -0
  155. package/docs/src/utils/formatTimestamp.js +1 -0
  156. package/docs/src/utils/propConverters.js +1 -0
  157. package/docs/src/utils/watchWindowSize.js +1 -0
  158. package/docs/styles.css +6 -0
  159. package/docs/utils/debounce.html +78 -0
  160. package/docs/utils/drag.html +104 -0
  161. package/docs/utils/formatTimestamp.html +114 -0
  162. package/docs/utils/propConverters.html +132 -0
  163. package/docs/utils/watchWindowSize.html +166 -0
  164. package/icons/add.svg +1 -0
  165. package/icons/arrow-back.svg +1 -0
  166. package/icons/arrow-down-double.svg +1 -0
  167. package/icons/arrow-down.svg +1 -0
  168. package/icons/arrow-forward.svg +1 -0
  169. package/icons/arrow-up-double.svg +1 -0
  170. package/icons/arrow-up.svg +1 -0
  171. package/icons/cards.svg +1 -0
  172. package/icons/check.svg +1 -0
  173. package/icons/chevron-left.svg +1 -0
  174. package/icons/chevron-right.svg +1 -0
  175. package/icons/close.svg +2 -0
  176. package/icons/compare.svg +1 -0
  177. package/icons/delete.svg +1 -0
  178. package/icons/drag-handle.svg +1 -0
  179. package/icons/drawing2.svg +124 -0
  180. package/icons/edit.svg +1 -0
  181. package/icons/error.svg +1 -0
  182. package/icons/export-file.svg +1 -0
  183. package/icons/file-text.svg +1 -0
  184. package/icons/file.svg +1 -0
  185. package/icons/filter-off.svg +1 -0
  186. package/icons/filter.svg +1 -0
  187. package/icons/first.svg +1 -0
  188. package/icons/folder-clear.svg +1 -0
  189. package/icons/folder-create.svg +1 -0
  190. package/icons/folder-open.svg +1 -0
  191. package/icons/folder.svg +1 -0
  192. package/icons/hide.svg +1 -0
  193. package/icons/image.svg +1 -0
  194. package/icons/label-add.svg +1 -0
  195. package/icons/label.svg +1 -0
  196. package/icons/last.svg +1 -0
  197. package/icons/menu.svg +1 -0
  198. package/icons/mode-auto.svg +1 -0
  199. package/icons/mode-dark.svg +1 -0
  200. package/icons/mode-light.svg +1 -0
  201. package/icons/open-in-browser.svg +1 -0
  202. package/icons/pause.svg +1 -0
  203. package/icons/play.svg +1 -0
  204. package/icons/refresh.svg +1 -0
  205. package/icons/remove.svg +1 -0
  206. package/icons/save.svg +1 -0
  207. package/icons/search.svg +1 -0
  208. package/icons/settings.svg +1 -0
  209. package/icons/show.svg +1 -0
  210. package/icons/storage.svg +1 -0
  211. package/icons/table-visibility.svg +1 -0
  212. package/icons/tag.svg +1 -0
  213. package/icons/tools.svg +1 -0
  214. package/icons/trash-x.svg +1 -0
  215. package/icons/warning.svg +1 -0
  216. package/package.json +2 -2
  217. package/scripts/build.js +109 -138
  218. package/scripts/docs.js +62 -60
  219. package/src/components/Accordion.js +229 -0
  220. package/src/components/Card.js +66 -0
  221. package/src/components/Collapsible.js +109 -0
  222. package/src/components/ContentSlider.js +205 -0
  223. package/src/components/Dialog.js +368 -0
  224. package/src/components/FocusCapture.js +23 -0
  225. package/src/components/HybridComponent.js +40 -40
  226. package/src/components/Icon.js +155 -0
  227. package/src/components/Import.js +104 -0
  228. package/src/components/LightComponent.js +32 -32
  229. package/src/components/PersistantCollapsible.js +69 -0
  230. package/src/components/PhotoViewer.js +368 -0
  231. package/src/components/Resize.js +210 -0
  232. package/src/components/ShadowComponent.js +32 -17
  233. package/src/components/ShowMore.js +109 -0
  234. package/src/components/SideMenu.js +167 -0
  235. package/src/components/Sortable.js +193 -0
  236. package/src/components/Split.js +192 -0
  237. package/src/components/Table.js +1202 -0
  238. package/src/components/Tabs.js +428 -0
  239. package/src/components/Tags.js +253 -0
  240. package/src/components/ThemeSwitcher.js +108 -0
  241. package/src/components/Timestamp.js +31 -0
  242. package/src/components/Toast.js +454 -0
  243. package/src/components/Toggle.js +173 -0
  244. package/src/components/tableControls/DeleteRecord.js +29 -0
  245. package/src/components/tableControls/Edit.js +88 -0
  246. package/src/components/tableControls/ExportCSV.js +71 -0
  247. package/src/components/tableControls/ExportJson.js +55 -0
  248. package/src/components/tableControls/FieldSortHide.js +76 -0
  249. package/src/components/tableControls/Filters.js +114 -0
  250. package/src/components/tableControls/FirstPage.js +65 -0
  251. package/src/components/tableControls/HiddenCount.js +45 -0
  252. package/src/components/tableControls/Hide.js +34 -0
  253. package/src/components/tableControls/LastPage.js +65 -0
  254. package/src/components/tableControls/NextPage.js +65 -0
  255. package/src/components/tableControls/PageSelect.js +109 -0
  256. package/src/components/tableControls/PageSize.js +68 -0
  257. package/src/components/tableControls/PrevPage.js +65 -0
  258. package/src/components/tableControls/Search.js +58 -0
  259. package/src/components/tableControls/ShowAll.js +34 -0
  260. package/src/components/tableControls/TableControl.js +105 -0
  261. package/src/kempo-hljs.css +146 -0
  262. package/src/utils/debounce.js +9 -0
  263. package/src/utils/drag.js +80 -0
  264. package/src/utils/formatTimestamp.js +27 -0
  265. package/src/utils/propConverters.js +11 -0
  266. package/src/utils/toTitleCase.js +9 -0
  267. package/src/utils/watchWindowSize.js +16 -0
  268. package/tests/HybridComponent.browser-test.js +214 -214
  269. package/tests/LightComponent.browser-test.js +169 -169
  270. package/tests/ShadowComponent.browser-test.js +130 -130
  271. package/config/development.json +0 -14
  272. package/docs/components/ShadowComponent.js +0 -1
  273. package/src/utils/cli.js +0 -43
  274. package/src/utils/fs-utils.js +0 -41
  275. /package/{docs → dist/src}/components/HybridComponent.js +0 -0
  276. /package/{docs → dist/src}/components/LightComponent.js +0 -0
  277. /package/docs/{utils → src/utils}/cli.js +0 -0
  278. /package/docs/{utils → src/utils}/fs-utils.js +0 -0
@@ -0,0 +1,1202 @@
1
+ import { html, css, unsafeStatic, literal } from '../lit-all.min.js';
2
+ import ShadowComponent from './ShadowComponent.js';
3
+ import toTitleCase from '../utils/toTitleCase.js';
4
+ import { boolExists } from '../utils/propConverters.js';
5
+
6
+ const selected = Symbol('selected');
7
+ const hidden = Symbol('hidden');
8
+ const index = Symbol('index');
9
+ const editing = Symbol('editing');
10
+
11
+ export default class Table extends ShadowComponent {
12
+ static properties = {
13
+ enablePages: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-pages' },
14
+ pageSize: { type: Number, reflect: true, attribute: 'page-size' },
15
+ currentPage: { type: Number, reflect: true, attribute: 'current-page' },
16
+ pageSizeOptions: { type: Array, attribute: 'page-size-options' },
17
+ enableSelection: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-selection' },
18
+ enableSorting: { type: Boolean, reflect: true, converter: boolExists, attribute: 'enable-sorting' },
19
+ caseSensitiveFilters: { type: Boolean, reflect: true, converter: boolExists, attribute: 'case-sensitive-filters' },
20
+ fields: { type: Array },
21
+ records: { type: Array },
22
+ filters: { type: Array },
23
+ sort: { type: Array },
24
+ columnSizes: { type: Object },
25
+ fetchPending: { type: Boolean }
26
+ };
27
+
28
+ static styles = css`
29
+ :host {
30
+ display: block;
31
+ width: 100%;
32
+ overflow: auto;
33
+ margin-bottom: var(--spacer);
34
+ }
35
+ #wrapper {
36
+ width: min-content;
37
+ border: 1px solid var(--c_border);
38
+ border-radius: var(--radius);
39
+ }
40
+ #table {
41
+ width: min-content;
42
+ }
43
+ #fields,
44
+ .record {
45
+ display: flex;
46
+ }
47
+ #fields {
48
+ background-color: var(--c_bg__alt);
49
+ border-bottom: 1px solid var(--c_border);
50
+ }
51
+ .record:not([editing="true"]) .cell:not(.controls),
52
+ #fields .cell:not(.controls) {
53
+ padding: calc(0.5 * var(--spacer)) var(--spacer);
54
+ }
55
+ .cell {
56
+ display: flex;
57
+ align-items: center;
58
+ }
59
+ .cell:not(:first-child) {
60
+ border-left: 1px solid var(--c_border);
61
+ }
62
+ .record:not(:last-child) .cell {
63
+ border-bottom: 1px solid var(--c_border);
64
+ }
65
+ #top, #bottom {
66
+ display: flex;
67
+ width: 100%;
68
+ }
69
+ #top slot {
70
+ display: block;
71
+ width: 100%;
72
+ border-bottom: 1px solid var(--c_border);
73
+ }
74
+ #bottom slot {
75
+ display: block;
76
+ width: 100%;
77
+ border-top: 1px solid var(--c_border);
78
+ }
79
+ :host(:not([top-controls])) #top,
80
+ :host(:not([bottom-controls])) #bottom {
81
+ display: none;
82
+ }
83
+ .field-select,
84
+ .selection {
85
+ display: flex;
86
+ justify-content: center;
87
+ align-items: center;
88
+ }
89
+ .field-select input,
90
+ .selection input {
91
+ width: 1.25rem;
92
+ height: 1.25rem;
93
+ }
94
+ .icon-sort {
95
+ float: right;
96
+ opacity: 0.5;
97
+ }
98
+ `;
99
+
100
+ constructor(options = {}) {
101
+ super();
102
+
103
+ // Set defaults for reactive properties (must be after super())
104
+ if(this.pageSize === undefined) this.pageSize = 50;
105
+ if(this.currentPage === undefined) this.currentPage = 1;
106
+ if(this.pageSizeOptions === undefined) this.pageSizeOptions = [10, 25, 50, 100, 500];
107
+ if(this.records === undefined) this.records = options.records || [];
108
+ if(this.fields === undefined) this.fields = options.fields || [];
109
+ if(this.filters === undefined) this.filters = options.filters || [];
110
+ if(this.sort === undefined) this.sort = [];
111
+ if(this.columnSizes === undefined) this.columnSizes = {};
112
+ if(this.fetchPending === undefined) this.fetchPending = false;
113
+ }
114
+
115
+ /*
116
+ Event Handlers
117
+ */
118
+
119
+ handleSelectAllChange = (e) => {
120
+ if (e.target.checked) {
121
+ this.selectAllOnPage();
122
+ } else {
123
+ this.deselectAllOnPage();
124
+ }
125
+ };
126
+
127
+ handleFieldClick = (fieldName) => {
128
+ const currentSort = this.sort.find(item => item.name === fieldName);
129
+ const asc = currentSort ? !currentSort.asc : true;
130
+ this.sortBy(fieldName, asc);
131
+ };
132
+
133
+ handleRecordSelectionChange = (record, e) => {
134
+ record[selected] = !!e.target.checked;
135
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
136
+ };
137
+
138
+ /*
139
+ Lifecycle Callbacks
140
+ */
141
+
142
+ firstUpdated() {
143
+ this.setData({
144
+ records: this.records,
145
+ fields: this.fields,
146
+ filters: this.filters
147
+ });
148
+ }
149
+
150
+ updated(changedProperties) {
151
+ super.updated(changedProperties);
152
+
153
+ // Update select-all checkbox state when selection or page changes
154
+ if (this.enableSelection) {
155
+ const selectAllCheckbox = this.shadowRoot.getElementById('select-all');
156
+ if (selectAllCheckbox) {
157
+ selectAllCheckbox.checked = this.allOnPageSelected();
158
+ }
159
+ }
160
+
161
+ // Update container widths
162
+ this.updateContainerWidths();
163
+ }
164
+
165
+ updateContainerWidths() {
166
+ const totalWidth = this.columnSizes.total + 'px';
167
+ const fieldsEl = this.shadowRoot.getElementById('fields');
168
+ const topEl = this.shadowRoot.getElementById('top');
169
+ const bottomEl = this.shadowRoot.getElementById('bottom');
170
+ const recordsEl = this.shadowRoot.getElementById('records');
171
+
172
+ if (fieldsEl) fieldsEl.style.width = totalWidth;
173
+ if (topEl) topEl.style.width = totalWidth;
174
+ if (bottomEl) bottomEl.style.width = totalWidth;
175
+ if (recordsEl) recordsEl.style.width = totalWidth;
176
+ }
177
+
178
+ /*
179
+ Rendering Functions
180
+ */
181
+
182
+ renderFieldsTemplate() {
183
+ this.calculateColumnSizes();
184
+ this.hasTopControls() ? this.setAttribute('top-controls', 'true') : this.removeAttribute('top-controls');
185
+ this.hasBottomControls() ? this.setAttribute('bottom-controls', 'true') : this.removeAttribute('bottom-controls');
186
+
187
+ const fieldCells = [];
188
+
189
+ if (this.enableSelection) {
190
+ fieldCells.push(html`
191
+ <div class="field controls cell field-select" style="width: 40px">
192
+ <input
193
+ type="checkbox"
194
+ id="select-all"
195
+ @change=${this.handleSelectAllChange}
196
+ />
197
+ </div>
198
+ `);
199
+ }
200
+
201
+ if (this.hasBeforeControls()) {
202
+ fieldCells.push(html`
203
+ <div class="field cell field-before-controls" style="width: ${this.columnSizes.beforeControls}px"></div>
204
+ `);
205
+ }
206
+
207
+ this.fields.forEach(({ name, label, hidden }) => {
208
+ if (hidden) return;
209
+ const sortItem = this.sort.find(item => item.name === name);
210
+ const isCurrentSort = this.sort.length > 0 && this.sort[this.sort.length - 1].name === name;
211
+ const sortClass = sortItem ? (sortItem.asc ? 'sort-asc' : 'sort-desc') : '';
212
+
213
+ fieldCells.push(html`
214
+ <div
215
+ class="field cell ${sortClass}"
216
+ style="width: ${this.columnSizes[name]}px; ${this.enableSorting ? 'cursor: pointer;' : ''}"
217
+ @click=${this.enableSorting ? () => this.handleFieldClick(name) : null}
218
+ >
219
+ ${label}
220
+ ${isCurrentSort ? html`<k-icon name="${sortItem.asc ? 'arrow-down' : 'arrow-up'}" class="icon-sort"></k-icon>` : ''}
221
+ </div>
222
+ `);
223
+ });
224
+
225
+ if (this.hasAfterControls()) {
226
+ fieldCells.push(html`
227
+ <div class="field cell field-after-controls" style="width: ${this.columnSizes.afterControls}px"></div>
228
+ `);
229
+ }
230
+
231
+ return fieldCells;
232
+ }
233
+
234
+ renderRecordsTemplate() {
235
+ let displayedRecords = this.getDisplayedRecords();
236
+ let start = 0;
237
+ let end = this.pageSize;
238
+
239
+ if (this.enablePages) {
240
+ start = (this.currentPage - 1) * this.pageSize;
241
+ end = start + this.pageSize;
242
+ displayedRecords = displayedRecords.slice(start, end);
243
+ }
244
+
245
+ let fetchStart = null;
246
+ let fetchCount = 0;
247
+
248
+ const recordTemplates = displayedRecords.map((record, idx) => {
249
+ if (record !== null) {
250
+ return this.renderRecordTemplate(record);
251
+ } else {
252
+ if (fetchStart === null) fetchStart = start + idx;
253
+ fetchCount++;
254
+ return html`<div class="record fetching"><div class="cell">Loading...</div></div>`;
255
+ }
256
+ });
257
+
258
+ if (fetchStart !== null && !this.fetchPending) {
259
+ setTimeout(() => {
260
+ if (!this.fetchPending) {
261
+ this.dispatchEvent(new CustomEvent('fetchRecords', {
262
+ detail: { start: fetchStart, count: fetchCount },
263
+ bubbles: true
264
+ }));
265
+ }
266
+ }, 0);
267
+ }
268
+
269
+ return recordTemplates;
270
+ }
271
+
272
+ renderRecordTemplate(record) {
273
+ const recordCells = [];
274
+
275
+ if (this.enableSelection) {
276
+ recordCells.push(html`
277
+ <div class="cell selection controls" style="width: 40px">
278
+ <input
279
+ type="checkbox"
280
+ .checked=${record[selected]}
281
+ @change=${(e) => this.handleRecordSelectionChange(record, e)}
282
+ />
283
+ </div>
284
+ `);
285
+ }
286
+
287
+ if (this.hasBeforeControls()) {
288
+ recordCells.push(this.renderBeforeControlsTemplate());
289
+ }
290
+
291
+ this.fields.forEach(({ name, formatter, calculator, type, editor, hidden }) => {
292
+ if (hidden) return;
293
+ let value = record[name] || '';
294
+
295
+ recordCells.push(html`
296
+ <div class="cell" data-field=${name} style="width: ${this.columnSizes[name]}px">
297
+ ${record[editing] ? this.renderEditingCell(record, name, value, calculator, editor, type) : this.renderDisplayCell(record, name, value, calculator, formatter)}
298
+ </div>
299
+ `);
300
+ });
301
+
302
+ if (this.hasAfterControls()) {
303
+ recordCells.push(this.renderAfterControlsTemplate());
304
+ }
305
+
306
+ return html`
307
+ <div class="record ${record[editing] ? 'editing' : ''}" data-index=${record[index]}>
308
+ ${recordCells}
309
+ </div>
310
+ `;
311
+ }
312
+
313
+ renderEditingCell(record, name, value, calculator, editor, type) {
314
+ if (calculator) {
315
+ return html`<input disabled .value=${calculator(record, this)} />`;
316
+ } else if (editor) {
317
+ // Custom editors return DOM elements, need to handle differently
318
+ const editorElement = editor(value);
319
+ return html`${editorElement}`;
320
+ } else {
321
+ const editorType = type || typeof value;
322
+ switch (editorType) {
323
+ case 'number':
324
+ return html`<input type="number" .value=${value} />`;
325
+ case 'date':
326
+ return html`<input type="date" .value=${value} />`;
327
+ case 'boolean':
328
+ return html`
329
+ <select .value=${value}>
330
+ <option value="true" ?selected=${value}>True</option>
331
+ <option value="false" ?selected=${!value}>False</option>
332
+ </select>
333
+ `;
334
+ default:
335
+ return html`<input type="text" .value=${value} />`;
336
+ }
337
+ }
338
+ }
339
+
340
+ renderDisplayCell(record, name, value, calculator, formatter) {
341
+ if (calculator) {
342
+ return calculator(record, this);
343
+ } else if (formatter) {
344
+ return formatter(value);
345
+ } else {
346
+ return value;
347
+ }
348
+ }
349
+
350
+ renderBeforeControlsTemplate() {
351
+ const controls = [];
352
+
353
+ this.querySelectorAll('[slot="before"]').forEach(control => {
354
+ const newControl = new control.constructor();
355
+
356
+ // Copy attributes
357
+ Array.from(control.attributes).forEach(attr => {
358
+ if(attr.name !== 'slot'){
359
+ newControl.setAttribute(attr.name, attr.value);
360
+ }
361
+ });
362
+
363
+ // Copy innerHTML if needed
364
+ if(control.innerHTML){
365
+ newControl.innerHTML = control.innerHTML;
366
+ }
367
+
368
+ controls.push(newControl);
369
+ });
370
+
371
+ return html`
372
+ <div class="cell controls controls-before" style="width: ${this.columnSizes.beforeControls}px">
373
+ ${controls}
374
+ </div>
375
+ `;
376
+ }
377
+
378
+ renderAfterControlsTemplate() {
379
+ const controls = [];
380
+
381
+ this.querySelectorAll('[slot="after"]').forEach(control => {
382
+ const newControl = new control.constructor();
383
+
384
+ // Copy attributes
385
+ Array.from(control.attributes).forEach(attr => {
386
+ if(attr.name !== 'slot'){
387
+ newControl.setAttribute(attr.name, attr.value);
388
+ }
389
+ });
390
+
391
+ // Copy innerHTML if needed
392
+ if(control.innerHTML){
393
+ newControl.innerHTML = control.innerHTML;
394
+ }
395
+
396
+ controls.push(newControl);
397
+ });
398
+
399
+ return html`
400
+ <div class="cell controls controls-after" style="width: ${this.columnSizes.afterControls}px">
401
+ ${controls}
402
+ </div>
403
+ `;
404
+ }
405
+
406
+ /*
407
+ Utility Functions
408
+ */
409
+
410
+ hasBeforeControls() {
411
+ return !!this.querySelector('[slot="before"]');
412
+ }
413
+
414
+ hasAfterControls() {
415
+ return !!this.querySelector('[slot="after"]');
416
+ }
417
+
418
+ hasTopControls() {
419
+ return !!this.querySelector('[slot="top"]');
420
+ }
421
+
422
+ hasBottomControls() {
423
+ return !!this.querySelector(':scope > :not([slot])');
424
+ }
425
+
426
+ /*
427
+ Public Methods
428
+ */
429
+
430
+ editRecord(record) {
431
+ record[editing] = true;
432
+ const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
433
+ if(recordEl){
434
+ recordEl.classList.add('editing');
435
+ recordEl.setAttribute('editing', 'true');
436
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
437
+ const field = $cell.dataset.field;
438
+ const fieldDef = this.fields.find(f => f.name === field);
439
+ if(fieldDef){
440
+ const value = record[field] || '';
441
+ $cell.innerHTML = '';
442
+ if(fieldDef.calculator){
443
+ const input = document.createElement('input');
444
+ input.disabled = true;
445
+ input.value = fieldDef.calculator(record, this);
446
+ $cell.appendChild(input);
447
+ } else if(fieldDef.editor){
448
+ $cell.appendChild(fieldDef.editor(value));
449
+ } else {
450
+ const type = fieldDef.type || typeof value;
451
+ const editorGen = Table.editors[type] || Table.editors.string;
452
+ $cell.appendChild(editorGen(value));
453
+ }
454
+ }
455
+ });
456
+ }
457
+ this.dispatchEvent(new CustomEvent('editingChange', {
458
+ detail: { record, editing: true },
459
+ bubbles: true
460
+ }));
461
+ }
462
+
463
+ saveEditedRecord(record) {
464
+ const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
465
+ if(recordEl){
466
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
467
+ const field = $cell.dataset.field;
468
+ const fieldDef = this.fields.find(f => f.name === field);
469
+ if(fieldDef && !fieldDef.calculator){
470
+ const $input = $cell.querySelector('input, select');
471
+ if($input){
472
+ record[field] = $input.value;
473
+ }
474
+ }
475
+ });
476
+ record[editing] = false;
477
+ recordEl.classList.remove('editing');
478
+ recordEl.removeAttribute('editing');
479
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
480
+ const field = $cell.dataset.field;
481
+ const fieldDef = this.fields.find(f => f.name === field);
482
+ if(fieldDef){
483
+ const value = record[field] || '';
484
+ if(fieldDef.calculator){
485
+ $cell.textContent = fieldDef.calculator(record, this);
486
+ } else if(fieldDef.formatter){
487
+ $cell.innerHTML = fieldDef.formatter(value);
488
+ } else {
489
+ $cell.textContent = value;
490
+ }
491
+ }
492
+ });
493
+ }
494
+ this.dispatchEvent(new CustomEvent('editingChange', {
495
+ detail: { record, editing: false },
496
+ bubbles: true
497
+ }));
498
+ }
499
+
500
+ cancelEditedRecord(record) {
501
+ record[editing] = false;
502
+ const recordEl = this.shadowRoot.querySelector(`.record[data-index="${record[index]}"]`);
503
+ if(recordEl){
504
+ recordEl.classList.remove('editing');
505
+ recordEl.removeAttribute('editing');
506
+ recordEl.querySelectorAll('.cell[data-field]').forEach($cell => {
507
+ const field = $cell.dataset.field;
508
+ const fieldDef = this.fields.find(f => f.name === field);
509
+ if(fieldDef){
510
+ const value = record[field] || '';
511
+ if(fieldDef.calculator){
512
+ $cell.textContent = fieldDef.calculator(record, this);
513
+ } else if(fieldDef.formatter){
514
+ $cell.innerHTML = fieldDef.formatter(value);
515
+ } else {
516
+ $cell.textContent = value;
517
+ }
518
+ }
519
+ });
520
+ }
521
+ this.dispatchEvent(new CustomEvent('editingChange', {
522
+ detail: { record, editing: false },
523
+ bubbles: true
524
+ }));
525
+ }
526
+
527
+ recordIsEditing(record) {
528
+ return record[editing];
529
+ }
530
+
531
+ getCurrentPage() {
532
+ return this.currentPage;
533
+ }
534
+
535
+ getTotalPages() {
536
+ return Math.ceil(this.getDisplayedRecords().length / this.pageSize);
537
+ }
538
+
539
+ setPage(page) {
540
+ if (page < 1 || page > this.getTotalPages()) return;
541
+ this.currentPage = page;
542
+ this.requestUpdate();
543
+ this.dispatchEvent(new CustomEvent('pageChange', { bubbles: true }));
544
+ }
545
+
546
+ firstPage() {
547
+ if (this.currentPage !== 1) {
548
+ this.setPage(1);
549
+ }
550
+ }
551
+
552
+ nextPage() {
553
+ if (this.currentPage < this.getTotalPages()) {
554
+ this.setPage(this.currentPage + 1);
555
+ }
556
+ }
557
+
558
+ prevPage() {
559
+ if (this.currentPage > 1) {
560
+ this.setPage(this.currentPage - 1);
561
+ }
562
+ }
563
+
564
+ lastPage() {
565
+ if (this.currentPage !== this.getTotalPages()) {
566
+ this.setPage(this.getTotalPages());
567
+ }
568
+ }
569
+
570
+ setPageSize(pageSize) {
571
+ this.pageSize = pageSize;
572
+ this.currentPage = 1;
573
+ this.requestUpdate();
574
+ this.dispatchEvent(new CustomEvent('pageSizeChange', { bubbles: true }));
575
+ }
576
+
577
+ getPageSize() {
578
+ return this.pageSize;
579
+ }
580
+
581
+ getPageSizeOptions() {
582
+ return this.pageSizeOptions;
583
+ }
584
+
585
+ setPageSizeOptions(options) {
586
+ this.pageSizeOptions = options;
587
+ this.requestUpdate();
588
+ }
589
+
590
+ setData({ records = false, fields = false, pageSize = false, pageSizeOptions = false, currentPage = false, enableSelection } = {}) {
591
+ let rerender = false;
592
+ let pageCountBefore = this.getTotalPages();
593
+ let pageBefore = this.currentPage;
594
+
595
+ if (records) {
596
+ this.records = records.map(r => ({ ...r }));
597
+ this.records.forEach((record, idx) => {
598
+ record[index] = idx;
599
+ record[selected] = false;
600
+ record[hidden] = false;
601
+ record[editing] = false;
602
+ });
603
+ this.fields = fields || Table.extractFieldsFromRecords(this.records);
604
+ rerender = true;
605
+ }
606
+
607
+ if (pageSize) {
608
+ this.pageSize = pageSize;
609
+ rerender = true;
610
+ }
611
+
612
+ if (pageSizeOptions) {
613
+ this.pageSizeOptions = pageSizeOptions;
614
+ }
615
+
616
+ if (currentPage) {
617
+ this.currentPage = currentPage;
618
+ rerender = true;
619
+ }
620
+
621
+ if (enableSelection !== undefined) {
622
+ this.enableSelection = enableSelection;
623
+ rerender = true;
624
+ }
625
+
626
+ if (rerender) {
627
+ this.requestUpdate();
628
+ }
629
+
630
+ const newPageCount = this.getTotalPages();
631
+ if (newPageCount !== pageCountBefore) {
632
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
633
+ detail: { totalPages: this.getTotalPages() },
634
+ bubbles: true
635
+ }));
636
+ }
637
+
638
+ if (pageBefore > newPageCount) {
639
+ this.setPage(newPageCount);
640
+ }
641
+ }
642
+
643
+ setRecords(records, fields) {
644
+ let pageCountBefore = this.getTotalPages();
645
+ let pageBefore = this.currentPage;
646
+
647
+ this.records = records.map(r => ({ ...r }));
648
+ this.records.forEach((record, idx) => {
649
+ record[index] = idx;
650
+ record[selected] = false;
651
+ record[hidden] = false;
652
+ record[editing] = false;
653
+ });
654
+ this.fields = fields || Table.extractFieldsFromRecords(this.records);
655
+ this.requestUpdate();
656
+ this.dispatchEvent(new CustomEvent('recordsSet', {
657
+ detail: { records },
658
+ bubbles: true
659
+ }));
660
+
661
+ const newPageCount = this.getTotalPages();
662
+ if (newPageCount !== pageCountBefore) {
663
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
664
+ detail: { totalPages: this.getTotalPages() },
665
+ bubbles: true
666
+ }));
667
+ }
668
+
669
+ if (pageBefore > newPageCount) {
670
+ this.setPage(newPageCount);
671
+ }
672
+ }
673
+
674
+ setupFetchRecords(totalRecords, callback) {
675
+ const previousLength = this.records.length;
676
+ const pageCountBefore = this.getTotalPages();
677
+
678
+ if (previousLength < totalRecords) {
679
+ this.records.length = totalRecords;
680
+ this.records.fill(null, previousLength);
681
+
682
+ // Trigger re-render and page count changed event
683
+ this.requestUpdate();
684
+
685
+ const newPageCount = this.getTotalPages();
686
+ if (newPageCount !== pageCountBefore) {
687
+ setTimeout(() => {
688
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
689
+ detail: { totalPages: newPageCount },
690
+ bubbles: true
691
+ }));
692
+ }, 0);
693
+ }
694
+ }
695
+
696
+ this.addEventListener('fetchRecords', async (event) => {
697
+ if (this.fetchPending) return;
698
+ this.fetchPending = true;
699
+ const { start, count } = event.detail;
700
+ const records = await callback(start, count);
701
+
702
+ // Initialize symbol properties on fetched records
703
+ records.forEach((record, idx) => {
704
+ record[index] = start + idx;
705
+ if(record[selected] === undefined) record[selected] = false;
706
+ if(record[hidden] === undefined) record[hidden] = false;
707
+ if(record[editing] === undefined) record[editing] = false;
708
+ });
709
+
710
+ this.records.splice(start, records.length, ...records);
711
+ this.fetchPending = false;
712
+ this.requestUpdate();
713
+ });
714
+ }
715
+
716
+ addRecord(record) {
717
+ record[selected] = false;
718
+ record[hidden] = false;
719
+ record[index] = this.records.length;
720
+ this.records.push(record);
721
+ this.requestUpdate();
722
+ this.dispatchEvent(new CustomEvent('recordAdded', {
723
+ detail: { record },
724
+ bubbles: true
725
+ }));
726
+ }
727
+
728
+ updateRecord(record, newData) {
729
+ let updated = false;
730
+ let originalRecord = this.records.find(r => r === record);
731
+ if (!originalRecord && record[index] !== undefined) {
732
+ originalRecord = this.records[record[index]];
733
+ }
734
+ Object.keys(newData).forEach(key => {
735
+ if (originalRecord.hasOwnProperty(key)) {
736
+ originalRecord[key] = newData[key];
737
+ updated = true;
738
+ }
739
+ });
740
+ if (updated) {
741
+ const start = (this.currentPage - 1) * this.pageSize;
742
+ const end = start + this.pageSize;
743
+ if (!this.enablePages || (originalRecord[index] >= start && originalRecord[index] < end)) {
744
+ this.requestUpdate();
745
+ }
746
+ }
747
+ }
748
+
749
+ deleteRecord(record) {
750
+ let originalRecord = this.records.find(r => r === record);
751
+ let totalPagesBefore = this.getTotalPages();
752
+
753
+ if (!originalRecord && record[index] !== undefined) {
754
+ originalRecord = this.records[record[index]];
755
+ }
756
+
757
+ if (originalRecord) {
758
+ const recordIndex = this.records.indexOf(originalRecord);
759
+ this.records.splice(recordIndex, 1);
760
+ this.records.forEach((rec, idx) => {
761
+ rec[index] = idx;
762
+ });
763
+ this.requestUpdate();
764
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
765
+ this.dispatchEvent(new CustomEvent('recordDeleted', {
766
+ detail: { index: recordIndex },
767
+ bubbles: true
768
+ }));
769
+
770
+ const totalPages = this.getTotalPages();
771
+ if (this.currentPage > totalPages) {
772
+ this.setPage(totalPages);
773
+ }
774
+ if (totalPages !== totalPagesBefore) {
775
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
776
+ detail: { totalPages },
777
+ bubbles: true
778
+ }));
779
+ }
780
+ }
781
+ }
782
+
783
+ deleteSelected() {
784
+ let totalPagesBefore = this.getTotalPages();
785
+ const selectedRecords = this.getSelectedRecords();
786
+
787
+ selectedRecords.forEach(record => {
788
+ let originalRecord = this.records.find(r => r === record);
789
+ if (!originalRecord && record[index] !== undefined) {
790
+ originalRecord = this.records[record[index]];
791
+ }
792
+ if (originalRecord) {
793
+ const recordIndex = this.records.indexOf(originalRecord);
794
+ this.records.splice(recordIndex, 1);
795
+ }
796
+ });
797
+
798
+ this.records.forEach((rec, idx) => {
799
+ rec[index] = idx;
800
+ });
801
+ this.requestUpdate();
802
+
803
+ const totalPages = this.getTotalPages();
804
+ if (this.currentPage > totalPages) {
805
+ this.setPage(totalPages);
806
+ }
807
+ if (totalPages !== totalPagesBefore) {
808
+ this.dispatchEvent(new CustomEvent('pageCountChanged', {
809
+ detail: { totalPages },
810
+ bubbles: true
811
+ }));
812
+ }
813
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
814
+ }
815
+
816
+ getSelectedRecords() {
817
+ return this.records.filter(record => record[selected]);
818
+ }
819
+
820
+ selectAllOnPage() {
821
+ const start = (this.currentPage - 1) * this.pageSize;
822
+ const end = Math.min(start + this.pageSize, this.records.length);
823
+ for (let i = start; i < end; i++) {
824
+ this.records[i][selected] = true;
825
+ }
826
+ this.requestUpdate();
827
+ setTimeout(() => {
828
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
829
+ }, 0);
830
+ }
831
+
832
+ deselectAllOnPage() {
833
+ const start = (this.currentPage - 1) * this.pageSize;
834
+ const end = Math.min(start + this.pageSize, this.records.length);
835
+ for (let i = start; i < end; i++) {
836
+ this.records[i][selected] = false;
837
+ }
838
+ this.requestUpdate();
839
+ setTimeout(() => {
840
+ this.dispatchEvent(new CustomEvent('selectionChange', { bubbles: true }));
841
+ }, 0);
842
+ }
843
+
844
+ allOnPageSelected() {
845
+ const start = (this.currentPage - 1) * this.pageSize;
846
+ const end = Math.min(start + this.pageSize, this.records.length);
847
+ for (let i = start; i < end; i++) {
848
+ if (!this.records[i][selected]) {
849
+ return false;
850
+ }
851
+ }
852
+ return true;
853
+ }
854
+
855
+ sortBy(field, asc = true) {
856
+ this.sort = this.sort.filter(item => item.name !== field);
857
+ this.sort.push({ name: field, asc });
858
+ this.requestUpdate();
859
+ }
860
+
861
+ hideRecord(record) {
862
+ let originalRecord = this.records.find(r => r === record);
863
+ if (!originalRecord && record[index] !== undefined) {
864
+ originalRecord = this.records[record[index]];
865
+ }
866
+ if (originalRecord) {
867
+ originalRecord[hidden] = true;
868
+ this.requestUpdate();
869
+ this.dispatchEvent(new CustomEvent('recordHidden', { bubbles: true }));
870
+ }
871
+ }
872
+
873
+ showRecord(record) {
874
+ let originalRecord = this.records.find(r => r === record);
875
+ if (!originalRecord && record[index] !== undefined) {
876
+ originalRecord = this.records[record[index]];
877
+ }
878
+ if (originalRecord) {
879
+ originalRecord[hidden] = false;
880
+ this.requestUpdate();
881
+ this.dispatchEvent(new CustomEvent('recordShown', { bubbles: true }));
882
+ }
883
+ }
884
+
885
+ showAllRecords() {
886
+ this.records.forEach(record => {
887
+ record[hidden] = false;
888
+ });
889
+ if (this.filters.length) {
890
+ this.filters = [];
891
+ this.dispatchEvent(new CustomEvent('filterRemoved', { bubbles: true }));
892
+ this.dispatchEvent(new CustomEvent('filterChange', { bubbles: true }));
893
+ }
894
+ this.requestUpdate();
895
+ this.dispatchEvent(new CustomEvent('recordShown', { bubbles: true }));
896
+ this.dispatchEvent(new CustomEvent('allRecordsShown', { bubbles: true }));
897
+ }
898
+
899
+ addFilter(field, condition, value) {
900
+ this.filters.push({ field, condition, value });
901
+ this.dispatchEvent(new CustomEvent('filterAdded', { bubbles: true }));
902
+ this.dispatchEvent(new CustomEvent('filterChange', { bubbles: true }));
903
+ this.requestUpdate();
904
+ }
905
+
906
+ removeFilter(field, condition, value, rerender = true) {
907
+ const filterIndex = this.filters.findIndex(f => f.field === field && f.condition === condition && f.value === value);
908
+ if (filterIndex !== -1) {
909
+ this.records.forEach(record => {
910
+ if (!this.testFilter(record, field, condition, value)) {
911
+ record[hidden] = false;
912
+ }
913
+ });
914
+ this.filters.splice(filterIndex, 1);
915
+ this.dispatchEvent(new CustomEvent('filterRemoved', { bubbles: true }));
916
+ this.dispatchEvent(new CustomEvent('filterChange', { bubbles: true }));
917
+ if (rerender) this.requestUpdate();
918
+ }
919
+ }
920
+
921
+ testFilter(record, field, condition, value) {
922
+ let recordValue = record[field];
923
+ let compareValue = value;
924
+
925
+ if(!this.caseSensitiveFilters && typeof recordValue === 'string' && typeof value === 'string'){
926
+ recordValue = recordValue.toLowerCase();
927
+ compareValue = value.toLowerCase();
928
+ }
929
+
930
+ switch(condition){
931
+ case 'equals':
932
+ return recordValue === compareValue;
933
+ case 'not-equals':
934
+ return recordValue !== compareValue;
935
+ case 'contains':
936
+ return recordValue.includes(compareValue);
937
+ case 'not-contains':
938
+ return !recordValue.includes(compareValue);
939
+ case 'greater-than':
940
+ return recordValue > compareValue;
941
+ case 'less-than':
942
+ return recordValue < compareValue;
943
+ case 'greater-than-or-equal':
944
+ return recordValue >= compareValue;
945
+ case 'less-than-or-equal':
946
+ return recordValue <= compareValue;
947
+ default:
948
+ return true;
949
+ }
950
+ }
951
+
952
+ removeAllFilters() {
953
+ if (this.filters.length) {
954
+ this.filters.forEach(({ field, condition, value }) => {
955
+ this.removeFilter(field, condition, value, false);
956
+ });
957
+ this.requestUpdate();
958
+ }
959
+ }
960
+
961
+ search(term) {
962
+ const t = term.trim().toLowerCase();
963
+ let changed = false;
964
+ this.records.forEach(record => {
965
+ if (record[hidden]) return;
966
+ let match = false;
967
+ this.fields.forEach(({ name }) => {
968
+ const val = record[name]?.toString().toLowerCase() || '';
969
+ if (val.includes(t)) {
970
+ match = true;
971
+ }
972
+ });
973
+ if (record[hidden] !== !match) {
974
+ record[hidden] = !match;
975
+ changed = true;
976
+ }
977
+ });
978
+ if (changed) {
979
+ this.dispatchEvent(new CustomEvent('recordHidden', { bubbles: true }));
980
+ this.requestUpdate();
981
+ }
982
+ this.dispatchEvent(new CustomEvent('search', {
983
+ detail: { term },
984
+ bubbles: true
985
+ }));
986
+ }
987
+
988
+ getDisplayedRecords() {
989
+ this.filters.forEach(({ field, condition, value }) => {
990
+ this.records.forEach(record => {
991
+ if(record === null) return;
992
+ if (!this.testFilter(record, field, condition, value)) {
993
+ record[hidden] = true;
994
+ }
995
+ });
996
+ });
997
+
998
+ let displayedRecords = this.records.filter(record => record === null || !record[hidden]);
999
+
1000
+ this.sort.forEach(({ name, asc }) => {
1001
+ displayedRecords.sort((a, b) => {
1002
+ if(a === null || b === null) return 0;
1003
+ if (a[name] < b[name]) return asc ? -1 : 1;
1004
+ if (a[name] > b[name]) return asc ? 1 : -1;
1005
+ return 0;
1006
+ });
1007
+ });
1008
+
1009
+ return displayedRecords;
1010
+ }
1011
+
1012
+ getHiddenRecords() {
1013
+ return this.records.filter(record => record[hidden]);
1014
+ }
1015
+
1016
+ calculateColumnSizes() {
1017
+ this.columnSizes = {};
1018
+ this.columnSizes.total = 0;
1019
+
1020
+ if (this.enableSelection) this.columnSizes.total += 40;
1021
+
1022
+ this.columnSizes.beforeControls = Array.from(this.querySelectorAll('[slot="before"]')).reduce((total, el) => total + (el.maxWidth || 40), 0);
1023
+ this.columnSizes.afterControls = Array.from(this.querySelectorAll('[slot="after"]')).reduce((total, el) => total + (el.maxWidth || 40), 0);
1024
+
1025
+ if (this.hasBeforeControls()) this.columnSizes.total += this.columnSizes.beforeControls;
1026
+ if (this.hasAfterControls()) this.columnSizes.total += this.columnSizes.afterControls;
1027
+
1028
+ this.fields.forEach(field => {
1029
+ if (field.size) {
1030
+ this.columnSizes[field.name] = field.size;
1031
+ this.columnSizes.total += field.size;
1032
+ } else {
1033
+ let maxLength = 0;
1034
+ this.records.slice(0, 100).forEach(record => {
1035
+ if(record === null) return;
1036
+ let value = record[field.name];
1037
+ if (field.calculator) {
1038
+ value = field.calculator(record, this);
1039
+ }
1040
+ if (field.formatter) {
1041
+ value = field.formatter(value);
1042
+ }
1043
+ if (value && value.toString().length > maxLength) {
1044
+ maxLength = value.toString().length;
1045
+ }
1046
+ });
1047
+ this.columnSizes[field.name] = Math.max((maxLength * 10 + 32), 128);
1048
+ if (!field.hidden) this.columnSizes.total += this.columnSizes[field.name];
1049
+ }
1050
+ });
1051
+
1052
+ return this.columnSizes;
1053
+ }
1054
+
1055
+ setFieldHiddenState(fieldName, hidden) {
1056
+ const field = this.fields.find(f => f.name === fieldName);
1057
+ if (field) {
1058
+ field.hidden = hidden;
1059
+ this.calculateColumnSizes();
1060
+ this.requestUpdate();
1061
+ this.dispatchEvent(new CustomEvent('fieldVisibilityChanged', {
1062
+ detail: { field },
1063
+ bubbles: true
1064
+ }));
1065
+ this.dispatchEvent(new CustomEvent(hidden ? 'fieldHidden' : 'fieldShown', {
1066
+ detail: { field },
1067
+ bubbles: true
1068
+ }));
1069
+ }
1070
+ }
1071
+
1072
+ hideField(fieldName) {
1073
+ this.setFieldHiddenState(fieldName, true);
1074
+ }
1075
+
1076
+ showField(fieldName) {
1077
+ this.setFieldHiddenState(fieldName, false);
1078
+ }
1079
+
1080
+ reorderFields(newOrder) {
1081
+ const newFields = [];
1082
+ newOrder.forEach(fieldName => {
1083
+ const field = this.fields.find(f => f.name === fieldName);
1084
+ if (field) {
1085
+ newFields.push(field);
1086
+ }
1087
+ });
1088
+ this.fields = newFields;
1089
+ this.requestUpdate();
1090
+ }
1091
+
1092
+ /*
1093
+ Rendering Logic
1094
+ */
1095
+
1096
+ render() {
1097
+ if (!this.records || !this.fields) {
1098
+ return html`
1099
+ <div id="wrapper">
1100
+ <div id="top"><slot name="top"></slot></div>
1101
+ <div id="table">
1102
+ <div id="fields"></div>
1103
+ <div id="records"></div>
1104
+ </div>
1105
+ <div id="bottom"><slot></slot></div>
1106
+ </div>
1107
+ <div style="display: none">
1108
+ <slot name="before"></slot>
1109
+ <slot name="after"></slot>
1110
+ </div>
1111
+ `;
1112
+ }
1113
+
1114
+ this.calculateColumnSizes();
1115
+ this.hasTopControls() ? this.setAttribute('top-controls', 'true') : this.removeAttribute('top-controls');
1116
+ this.hasBottomControls() ? this.setAttribute('bottom-controls', 'true') : this.removeAttribute('bottom-controls');
1117
+
1118
+ return html`
1119
+ <div id="wrapper">
1120
+ <div id="top" style="width: ${this.columnSizes.total}px"><slot name="top"></slot></div>
1121
+ <div id="table">
1122
+ <div id="fields" style="width: ${this.columnSizes.total}px">
1123
+ ${this.renderFieldsTemplate()}
1124
+ </div>
1125
+ <div id="records" style="width: ${this.columnSizes.total}px">
1126
+ ${this.renderRecordsTemplate()}
1127
+ </div>
1128
+ </div>
1129
+ <div id="bottom" style="width: ${this.columnSizes.total}px"><slot></slot></div>
1130
+ </div>
1131
+ <div style="display: none">
1132
+ <slot name="before"></slot>
1133
+ <slot name="after"></slot>
1134
+ </div>
1135
+ `;
1136
+ }
1137
+
1138
+ /*
1139
+ Static Methods
1140
+ */
1141
+
1142
+ static extractFieldsFromRecords(records, recordLimit = 100) {
1143
+ const names = new Set();
1144
+ records.slice(0, recordLimit).forEach(record => {
1145
+ Object.keys(record).forEach(name => names.add(name));
1146
+ });
1147
+ return [...names].map(name => ({ name, label: toTitleCase(name) }));
1148
+ }
1149
+
1150
+ static format(value) {
1151
+ const f = Array.isArray(value) ? Table.formatters.array : Table.formatters[typeof value];
1152
+ return f(value);
1153
+ }
1154
+
1155
+ static formatters = {
1156
+ string: v => v,
1157
+ number: v => `${v}`,
1158
+ date: v => v.toLocaleDateString(),
1159
+ boolean: v => v ? 'True' : 'False',
1160
+ array: v => v.map(i => Table.format(i)).join(', '),
1161
+ 'undefined': v => '',
1162
+ 'null': v => '<code>null</code>'
1163
+ };
1164
+
1165
+ static editors = {
1166
+ string: (value) => {
1167
+ const $i = document.createElement('input');
1168
+ $i.value = value;
1169
+ return $i;
1170
+ },
1171
+ number: (value) => {
1172
+ const $i = document.createElement('input');
1173
+ $i.type = 'number';
1174
+ $i.value = value;
1175
+ return $i;
1176
+ },
1177
+ date: (value) => {
1178
+ const $i = document.createElement('input');
1179
+ $i.type = 'date';
1180
+ $i.value = value;
1181
+ return $i;
1182
+ },
1183
+ boolean: (value) => {
1184
+ const $i = document.createElement('select');
1185
+ $i.innerHTML = `
1186
+ <option value="true" ${value ? 'selected' : ''}>True</option>
1187
+ <option value="false" ${!value ? 'selected' : ''}>False</option>
1188
+ `;
1189
+ $i.value = value;
1190
+ return $i;
1191
+ },
1192
+ calculated: (value) => {
1193
+ const $i = document.createElement('input');
1194
+ $i.disabled = true;
1195
+ $i.value = value;
1196
+ return $i;
1197
+ }
1198
+ };
1199
+ }
1200
+
1201
+ // Register custom element
1202
+ window.customElements.define('k-table', Table);