@itrocks/table 0.0.1

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/freeze.js ADDED
@@ -0,0 +1,190 @@
1
+ import Plugin from '../plugin/plugin.js';
2
+ export default class TableFreeze extends Plugin {
3
+ columns;
4
+ full;
5
+ leftColumnCount;
6
+ rightColumnCount;
7
+ zIndex = '1';
8
+ constructor(table) {
9
+ super(table);
10
+ this.columns = this.getColumns();
11
+ this.leftColumnCount = this.countLeftColumns();
12
+ this.rightColumnCount = this.countRightColumns();
13
+ }
14
+ init() {
15
+ this.freezeFootRows();
16
+ this.freezeHeadRows();
17
+ this.freezeLeftColumns();
18
+ this.freezeRightColumns();
19
+ }
20
+ closestScrollable(element) {
21
+ let parent = element.closest('table')?.parentElement;
22
+ while (parent && (parent.scrollHeight <= parent.clientHeight)) {
23
+ parent = parent.parentElement;
24
+ }
25
+ return parent ? ((parent instanceof HTMLHtmlElement) ? window : parent) : undefined;
26
+ }
27
+ countLeftColumns() {
28
+ let count = 0;
29
+ while ((count < this.columns.length - 1) && (this.columns[count].dataset.freeze !== undefined)) {
30
+ count++;
31
+ }
32
+ return count;
33
+ }
34
+ countRightColumns() {
35
+ let count = this.columns.length - 1;
36
+ while ((count > 0) && (this.columns[count].dataset.freeze !== undefined)) {
37
+ count--;
38
+ }
39
+ return this.columns.length - 1 - count;
40
+ }
41
+ freezeFootRows() {
42
+ const table = this.of;
43
+ if (!table.element.tFoot?.rows.length)
44
+ return;
45
+ let counter = 1, bottom = .0, previousBottom = table.element.getBoundingClientRect().bottom;
46
+ for (const row of Array.from(table.element.tFoot.querySelectorAll(':scope > tr')).reverse()) {
47
+ const actualBottom = row.getBoundingClientRect().bottom;
48
+ bottom += previousBottom - actualBottom;
49
+ previousBottom = actualBottom;
50
+ table.styleSheet.push(`
51
+ ${table.selector} > tfoot > tr:nth-last-child(${counter}) > * {
52
+ bottom: ${this.position(bottom, counter, row.firstElementChild, 'bottom')};
53
+ }
54
+ `);
55
+ counter++;
56
+ }
57
+ const zIndex = this.full ? `z-index: ${this.full.row};` : '';
58
+ table.styleSheet.push(`
59
+ ${table.selector} > tfoot > tr > * {
60
+ position: sticky;
61
+ ${zIndex}
62
+ }
63
+ `);
64
+ }
65
+ freezeHeadRows() {
66
+ const table = this.of;
67
+ if (!table.element.tHead?.rows.length)
68
+ return;
69
+ let counter = 1, top = .0, previousTop = table.element.getBoundingClientRect().top;
70
+ table.element.tHead.querySelectorAll(':scope > tr').forEach(row => {
71
+ const actualTop = row.getBoundingClientRect().top;
72
+ top += actualTop - previousTop;
73
+ previousTop = actualTop;
74
+ table.styleSheet.push(`
75
+ ${table.selector} > thead > tr:nth-child(${counter}) > * {
76
+ top: ${this.position(top, counter, row.firstElementChild, 'top')};
77
+ }
78
+ `);
79
+ counter++;
80
+ });
81
+ const zIndex = this.full ? `z-index: ${this.full.row};` : '';
82
+ table.styleSheet.push(`
83
+ ${table.selector} > thead > tr > * {
84
+ position: sticky;
85
+ ${zIndex}
86
+ }
87
+ `);
88
+ }
89
+ freezeLeftColumns() {
90
+ if (!this.leftColumnCount)
91
+ return;
92
+ const table = this.of;
93
+ const bodySel = [];
94
+ const cornerSel = [];
95
+ let counter = 1, left = .0, previousLeft = table.element.getBoundingClientRect().left;
96
+ for (const colCell of Array.from(this.columns).toSpliced(this.leftColumnCount)) {
97
+ const actualLeft = colCell.getBoundingClientRect().left;
98
+ left += actualLeft - previousLeft;
99
+ previousLeft = actualLeft;
100
+ table.styleSheet.push(`
101
+ ${table.selector} > * > tr > :nth-child(${counter}) {
102
+ left: ${this.position(left, counter, colCell, 'left')};
103
+ }
104
+ `);
105
+ bodySel.push(`${table.selector} > tbody > tr > :nth-child(${counter})`);
106
+ cornerSel.push(`${table.selector} > tfoot > tr > :nth-child(${counter})`);
107
+ cornerSel.push(`${table.selector} > thead > tr > :nth-child(${counter})`);
108
+ counter++;
109
+ }
110
+ const zIndex = this.full ? `z-index: ${this.full.column};` : '';
111
+ const zIndexValue = this.full ? this.full.corner : this.zIndex;
112
+ table.styleSheet.push(`
113
+ ${bodySel.join(', ')} {
114
+ position: sticky;
115
+ ${zIndex}
116
+ }
117
+ ${cornerSel.join(', ')} {
118
+ z-index: ${zIndexValue};
119
+ }
120
+ `);
121
+ }
122
+ freezeRightColumns() {
123
+ if (!this.rightColumnCount)
124
+ return;
125
+ const table = this.of;
126
+ const bodySel = [];
127
+ const cornerSel = [];
128
+ let counter = 1, right = .0, previousRight = table.element.getBoundingClientRect().right;
129
+ for (const colCell of Array.from(this.columns).reverse().toSpliced(this.rightColumnCount)) {
130
+ const actualRight = colCell.getBoundingClientRect().right;
131
+ right += previousRight - actualRight;
132
+ previousRight = actualRight;
133
+ table.styleSheet.push(`
134
+ ${table.selector} > * > tr > :nth-last-child(${counter}) {
135
+ right: ${this.position(right, counter, colCell, 'right')};
136
+ }
137
+ `);
138
+ bodySel.push(`${table.selector} > tbody > tr > :nth-last-child(${counter})`);
139
+ if (this.full) {
140
+ cornerSel.push(`${table.selector} > tfoot > tr > :nth-last-child(${counter})`);
141
+ }
142
+ cornerSel.push(`${table.selector} > thead > tr > :nth-last-child(${counter})`);
143
+ counter++;
144
+ }
145
+ const zIndex = this.full ? `z-index: ${this.full.column};` : '';
146
+ const zIndexValue = this.full ? this.full.corner : this.zIndex;
147
+ table.styleSheet.push(`
148
+ ${bodySel.join(', ')} {
149
+ position: sticky;
150
+ ${zIndex}
151
+ }
152
+ ${cornerSel.join(', ')} {
153
+ z-index: ${zIndexValue};
154
+ }
155
+ `);
156
+ }
157
+ getColumns() {
158
+ if (this.columns)
159
+ return this.columns;
160
+ const table = this.of;
161
+ let columns = table.element.querySelectorAll(':scope > colgroup > col');
162
+ if (!columns.length) {
163
+ columns = table.element.querySelectorAll(':scope > thead > tr:first-child > *');
164
+ if (!columns.length) {
165
+ columns = table.element.querySelectorAll(':scope > tbody > tr:first-child > *');
166
+ }
167
+ }
168
+ return columns;
169
+ }
170
+ position(position, _counter, _colCell, _side) {
171
+ return `${position}px`;
172
+ }
173
+ visibleInnerRect() {
174
+ const tableElement = this.of.element;
175
+ const rect = tableElement.getBoundingClientRect();
176
+ if (this.leftColumnCount) {
177
+ rect.x = this.columns[this.leftColumnCount - 1].getBoundingClientRect().right;
178
+ }
179
+ if (tableElement.tHead?.lastElementChild?.firstElementChild) {
180
+ rect.y = tableElement.tHead.lastElementChild.firstElementChild.getBoundingClientRect().bottom;
181
+ }
182
+ if (this.rightColumnCount) {
183
+ rect.width = this.columns[this.columns.length - this.rightColumnCount].getBoundingClientRect().left - rect.x + 1;
184
+ }
185
+ if (tableElement.tFoot?.firstElementChild?.firstElementChild) {
186
+ rect.height = tableElement.tFoot.firstElementChild.firstElementChild.getBoundingClientRect().top - rect.y + 1;
187
+ }
188
+ return rect;
189
+ }
190
+ }
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "author": {
3
+ "name": "Baptiste Pillot",
4
+ "email": "baptiste@pillot.fr"
5
+ },
6
+ "dependencies": {
7
+ "@itrocks/contenteditable": "latest",
8
+ "@itrocks/plugin": "latest"
9
+ },
10
+ "description": "A lightweight, modular HTML table offering near-spreadsheet features such as edit, freeze, lock, scroll, and more",
11
+ "devDependencies": {
12
+ "@itrocks/prepare-module": "latest",
13
+ "@types/node": "^22.9",
14
+ "typescript": "5.6"
15
+ },
16
+ "files": [
17
+ "LICENSE",
18
+ "README.md",
19
+ "**/*.d.ts",
20
+ "**/*.js",
21
+ "!demo/**",
22
+ "!node_modules/**",
23
+ "!src/**"
24
+ ],
25
+ "keywords": [
26
+ "drag",
27
+ "drop",
28
+ "edit",
29
+ "editable",
30
+ "table",
31
+ "fix",
32
+ "fixed",
33
+ "lock",
34
+ "reorder",
35
+ "scroll"
36
+ ],
37
+ "license": "LGPL-3.0-or-later",
38
+ "module": "./table.js",
39
+ "name": "@itrocks/table",
40
+ "repository": "https://github.com/itrocks-ts/table",
41
+ "scripts": {
42
+ "build": "tsc",
43
+ "prepare": "prepare-module"
44
+ },
45
+ "type": "module",
46
+ "types": "./table.d.ts",
47
+ "version": "0.0.1"
48
+ }
package/table.d.ts ADDED
@@ -0,0 +1,19 @@
1
+ import { HasPlugins, Options as PluginOptions } from '../node_modules/@itrocks/plugin/plugin.js';
2
+ export declare function applyStyleSheets(): void;
3
+ export declare function garbageCollector(): void;
4
+ export declare function getTables(): Table[];
5
+ export type Options = PluginOptions<Table>;
6
+ export default class Table extends HasPlugins<Table> {
7
+ readonly element: HTMLTableElement;
8
+ readonly id: number;
9
+ readonly selector: string;
10
+ readonly onReset: (() => void)[];
11
+ readonly styleSheet: string[];
12
+ constructor(element: HTMLTableElement, options?: Partial<Options>);
13
+ addEventListener<T extends keyof GlobalEventHandlersEventMap>(element: Document | Element | Window, type: T, listener: (this: Element, ev: GlobalEventHandlersEventMap[T]) => any, options?: AddEventListenerOptions | boolean): void;
14
+ cellColumnNumber(cell: HTMLTableCellElement): number;
15
+ reset(): Table;
16
+ }
17
+ export declare function tableByElement(element: HTMLTableElement, options?: Partial<Options>): Table;
18
+ export declare function tableByElements(elements: Array<HTMLTableElement> | NodeListOf<HTMLTableElement>, options?: Partial<Options>): Table[];
19
+ export declare function tableBySelector(selector: string, options?: Partial<Options>): Table[];
package/table.js ADDED
@@ -0,0 +1,74 @@
1
+ import { HasPlugins } from '../plugin/plugin.js';
2
+ const styleSheets = new CSSStyleSheet;
3
+ document.adoptedStyleSheets.push(styleSheets);
4
+ let tableCounter = 0;
5
+ let tables = [];
6
+ export function applyStyleSheets() {
7
+ styleSheets.replaceSync(tables.map(table => table.styleSheet.join(`\n`)).join(`\n`));
8
+ }
9
+ export function garbageCollector() {
10
+ const length = tables.length;
11
+ tables = tables.filter(table => document.body.querySelectorAll(table.selector).length);
12
+ if (tables.length < length) {
13
+ applyStyleSheets();
14
+ }
15
+ }
16
+ export function getTables() {
17
+ return tables;
18
+ }
19
+ function nextTableId(table) {
20
+ tables.push(table);
21
+ tableCounter++;
22
+ if (tableCounter > 999999999) {
23
+ tableCounter = 1;
24
+ }
25
+ return tableCounter;
26
+ }
27
+ export default class Table extends HasPlugins {
28
+ element;
29
+ id;
30
+ selector;
31
+ onReset = [];
32
+ styleSheet = [];
33
+ constructor(element, options = {}) {
34
+ super(options);
35
+ this.element = element;
36
+ this.id = nextTableId(this);
37
+ this.selector = `table.itrocks[data-table-id="${this.id}"]`;
38
+ this.element.classList.add('itrocks');
39
+ this.element.setAttribute('data-table-id', this.id.toString());
40
+ garbageCollector();
41
+ this.constructPlugins();
42
+ this.initPlugins();
43
+ applyStyleSheets();
44
+ }
45
+ addEventListener(element, type, listener, options) {
46
+ element.addEventListener(type, listener, options);
47
+ this.onReset.push(() => element.removeEventListener(type, listener, options));
48
+ }
49
+ cellColumnNumber(cell) {
50
+ let count = 0;
51
+ let previous = cell.previousElementSibling;
52
+ while (previous) {
53
+ if ((previous.tagName === 'TD') || (previous.tagName === 'TH')) {
54
+ count++;
55
+ }
56
+ previous = previous.previousElementSibling;
57
+ }
58
+ return count;
59
+ }
60
+ reset() {
61
+ for (const onReset of this.onReset)
62
+ onReset.call(this);
63
+ return tableByElement(this.element, this.options);
64
+ }
65
+ }
66
+ export function tableByElement(element, options = {}) {
67
+ return new Table(element, options);
68
+ }
69
+ export function tableByElements(elements, options = {}) {
70
+ return Array.from(elements).map(element => tableByElement(element, options));
71
+ }
72
+ export function tableBySelector(selector, options = {}) {
73
+ return tableByElements(document.body.querySelectorAll(selector), options);
74
+ }