dynamic_table 0.1.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.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 19f5fe5410456117b2cf8996ed417d1a796a0d7c0562644285d917ccb9e47fa2
4
+ data.tar.gz: abf278075e228753dfa36b64758887f5bb4db3e80ab4df23af44b9441e7f0d2b
5
+ SHA512:
6
+ metadata.gz: da0cb55a7774f75d8f5d77384f58c1cd509bfa2170fb837d989e629797ca8d47c6fa0a5587e090e10291c04e94d59219eea410da5568dd65987547323a598f98
7
+ data.tar.gz: 1fe4951f818c140b8ee3d2d224a697351c737c06fd2c45785bdacc3459cfd25427afdb796ab29a81ea148ea33cb5b8c62891bde3ff8837acc28ed559420b5731
@@ -0,0 +1,930 @@
1
+ (function(root, factory) {
2
+ if (typeof define === 'function' && define.amd) {
3
+ define([], factory);
4
+ } else if (typeof module === 'object' && module.exports) {
5
+ module.exports = factory();
6
+ } else {
7
+ root.DynamicTable = factory();
8
+ }
9
+ }(this, function() {
10
+ function DynamicTable(element, options) {
11
+ this.element = element; //
12
+ this.rows = options.rows || 3;
13
+ this.cols = options.cols || 3;
14
+ this.columnOptions = options.columnOptions || {};
15
+ this.appliedClasses = options.appliedClasses || {};
16
+ this.tableId = options.tableId || 'dynamicTable'; // ID for the table element
17
+ this.prefix = options.prefix || this.tableId;
18
+ this.inputType = options.inputType || 'text'; // Input type: 'text' or 'decimal'
19
+ this.dataMap = new Map();
20
+ this.headerNames = [];
21
+ this.addStyles();
22
+ this.specialControlFunctions = new Map();
23
+ this.renderTable(); // Generate the table when instantiated
24
+ this.selectionMap = Array.from({ length: this.rows }, () => Array(this.cols).fill(0));
25
+ this.pivotRow = -1;
26
+ this.pivotCol = -1;
27
+ this.isMouseDown = false; // Track mouse down state for drag selection
28
+ this.initClickOutsideListener(); // Detect clicks outside the table
29
+ this.initAllSpecialControls(); //controls such as dates needs datepicker
30
+ this.initiateDecimalRelatedVars();
31
+ }
32
+
33
+ DynamicTable.prototype.initiateDecimalRelatedVars = function() {
34
+ this.controlKeys = ["Backspace", "ArrowLeft", "ArrowRight", "Delete", "Tab"];
35
+ this.decimalKeyRange = [...Array.from({ length: 10 }, (_, i) => "" + i), ...this.controlKeys];
36
+ this.decimalKeyRange.push("."); //the dot operator to signify decimals
37
+ };
38
+
39
+ DynamicTable.prototype.initAllSpecialControls = function() {
40
+ for (const [kls,func] of this.specialControlFunctions) {
41
+ func();
42
+ }
43
+ };
44
+
45
+ //can add rows if there are pre existing rows. Else use table definition
46
+ DynamicTable.prototype.addRow = function(rowNo = null, options = null) {
47
+ let tbody = $('#'+this.tableId).find('tbody')[0];
48
+
49
+ let tr = document.createElement('tr');
50
+ let row = this.rows + 1;
51
+ //apply classes to tr
52
+ if (options != null) {
53
+ tr.classList.add(...options);
54
+ }
55
+ else if (this.appliedClasses['tr']?.[''+row]) {
56
+ tr.classList.add(...this.appliedClasses['tr'][''+row]);
57
+ } else if (this.appliedClasses['tr']?.[''+(row-1)]) { //else apply from previous row if unavailable
58
+ tr.classList.add(...this.appliedClasses['tr'][''+(row-1)]);
59
+ this.appliedClasses['tr'][''+(row)] = this.appliedClasses['tr'][''+(row-1)]
60
+ }
61
+
62
+ //add each column
63
+ for (var col = 1; col <= this.cols; col++) {
64
+ let td = document.createElement('td');
65
+
66
+ //apply classes to td
67
+ if (this.appliedClasses['td']?.[''+col]) {
68
+ td.classList.add(...this.appliedClasses['td'][''+col]);
69
+ }
70
+
71
+ var input = this.setInput(this.columnOptions[`${col}`]||{},row, col);
72
+
73
+ //add event listeners
74
+ this.initEventListeners(input);
75
+ this.element.addEventListener('keydown', this.handleKeyDown.bind(this,this));
76
+ input.addEventListener('keydown', this.handleKeyDown.bind(this, input));
77
+ input.addEventListener('mousedown', this.handleInputMouseDown.bind(this, input));
78
+ input.addEventListener('mouseover', this.handleMouseOver.bind(this, input)); // To handle dragging
79
+ input.addEventListener('change', this.addToDataMap.bind(this, input));
80
+
81
+ td.appendChild(input);
82
+ tr.appendChild(td);
83
+ }
84
+
85
+ tbody.appendChild(tr);
86
+ this.rows += 1; //on success add the row numbers
87
+ this.resizeSelectionMap();
88
+ }
89
+
90
+ DynamicTable.prototype.removeRow = function(rowNo = null) {
91
+ if (this.rows === 0) {
92
+ console.log("No more rows to remove in table.");
93
+ return;
94
+ }
95
+
96
+ let table = $('#'+this.tableId)[0];
97
+ if (rowNo === null) {
98
+ rowNo = this.rows;
99
+ }
100
+
101
+ table.deleteRow(rowNo);
102
+ this.rows -= 1;
103
+ this.resizeSelectionMap();
104
+
105
+ }
106
+
107
+ DynamicTable.prototype.addCol = function(colOptions) {
108
+ for (let key in colOptions) {
109
+ this.columnOptions[key] = colOptions[key];
110
+
111
+ //adding header
112
+ let tr = $(`#${this.tableId} thead tr`)[0];
113
+ // let tr = thead.find('tr')[0];
114
+ var th = document.createElement('th');
115
+ th.classList.add(...this.appliedClasses['thead']);
116
+ let colName = colOptions[key]?.['headerName'] || 'Column ' + key;
117
+ if (this.headerNames.includes(colName)) {
118
+ let index = 1;
119
+ let testName = colName;
120
+ while(this.headerNames.includes(testName)){testName = colName+index; ++index;}
121
+ this.headerNames.push(testName);th.innerText = testName;
122
+ }
123
+ else {this.headerNames.push(colName);th.innerText = colName;}
124
+
125
+ tr.appendChild(th);
126
+
127
+ //adding columns to rows
128
+ var parentObj = this;
129
+ $(`#${this.tableId} tbody tr`).each(function(i, tr) {
130
+ var row = i + 1; //row index starts from 1 rather than 0
131
+ var td = document.createElement('td');
132
+ //apply classes to td
133
+ if (parentObj.appliedClasses['td']?.[''+key]) {
134
+ td.classList.add(...parentObj.appliedClasses['td'][''+key]);
135
+ }
136
+
137
+ var input = parentObj.setInput(parentObj.columnOptions[key]||{},row, parentObj.cols+1);
138
+ //add event listeners
139
+ parentObj.initEventListeners(input);
140
+ parentObj.element.addEventListener('keydown', parentObj.handleKeyDown.bind(parentObj,parentObj));
141
+ input.addEventListener('keydown', parentObj.handleKeyDown.bind(parentObj, input));
142
+ input.addEventListener('mousedown', parentObj.handleInputMouseDown.bind(parentObj, input));
143
+ input.addEventListener('mouseover', parentObj.handleMouseOver.bind(parentObj, input)); // To handle dragging
144
+ input.addEventListener('change', parentObj.addToDataMap.bind(parentObj, input));
145
+
146
+ td.appendChild(input);
147
+ tr.appendChild(td);
148
+ });
149
+
150
+ // let table = document.getElementById('#'+this.tableId);
151
+ // for (var row = 1; row <= this.rows; row++) {
152
+ // var tr = table.rows[row];
153
+ // var td = document.createElement('td');
154
+ // //apply classes to td
155
+ // if (this.appliedClasses['td']?.[''+col]) {
156
+ // td.classList.add(...this.appliedClasses['td'][''+col]);
157
+ // }
158
+
159
+ // var input = this.setInput(this.columnOptions[key]||{},row, this.cols+1);
160
+ // //add event listeners
161
+ // this.initEventListeners(input);
162
+ // this.element.addEventListener('keydown', this.handleKeyDown.bind(this,this));
163
+ // input.addEventListener('keydown', this.handleKeyDown.bind(this, input));
164
+ // input.addEventListener('mousedown', this.handleInputMouseDown.bind(this, input));
165
+ // input.addEventListener('mouseover', this.handleMouseOver.bind(this, input)); // To handle dragging
166
+ // input.addEventListener('change', this.addToDataMap.bind(this, input));
167
+
168
+ // td.appendChild(input);
169
+ // tr.appendChild(td);
170
+
171
+ this.cols += 1; //on successful addition across all rows
172
+ this.resizeSelectionMap();
173
+ }
174
+ }
175
+
176
+ DynamicTable.prototype.removeCol = function() {
177
+ let columnIndex = this.cols - 1; //pick the last column
178
+ let tableId = '#' + this.tableId;
179
+
180
+ $(tableId + ' tr').each(function() {
181
+ // Find the column at the given index and remove it
182
+ $(this).find('th, td').eq(columnIndex).remove();
183
+ });
184
+
185
+ this.cols -= 1;
186
+ this.headerNames.pop(); //remove last column
187
+ this.resizeSelectionMap();
188
+ }
189
+
190
+ DynamicTable.prototype.resizeSelectionMap = function() {
191
+
192
+ this.selectionMap = Array.from({ length: this.rows }, () => Array(this.cols).fill(0));
193
+ this.repaintRange(-1,-1,-1,-1);
194
+ this.pivotRow = this.pivotCol = -1;
195
+
196
+ }
197
+
198
+ DynamicTable.prototype.renderTable = function() {
199
+ // Create table element
200
+ var table = document.createElement('table');
201
+ table.id = this.tableId;
202
+ table.className = 'dynamic-table';
203
+
204
+ //apply classes to table
205
+ if (this.appliedClasses['table']) {
206
+ table.classList.add(...this.appliedClasses['table']);
207
+ }
208
+
209
+ // Create the table header row
210
+ var thead = document.createElement('thead');
211
+ var headerRow = document.createElement('tr');
212
+ for (var col = 1; col <= this.cols; col++) {
213
+ var th = document.createElement('th');
214
+ let colName = this.columnOptions[`${col}`]?.['headerName'] || 'Column ' + col;
215
+ th.classList.add(...this.appliedClasses['thead']);
216
+
217
+ if (this.headerNames.includes(colName)) {
218
+ let index = 1;
219
+ let testName = colName;
220
+ while(this.headerNames.includes(testName)){testName = colName+index; ++index;}
221
+ this.headerNames.push(testName);th.innerText = testName;
222
+ }
223
+ else {this.headerNames.push(colName);th.innerText = colName;}
224
+
225
+ headerRow.appendChild(th);
226
+ }
227
+ thead.appendChild(headerRow);
228
+ table.appendChild(thead);
229
+
230
+ // Create the table body
231
+ var tbody = document.createElement('tbody');
232
+ for (var row = 1; row <= this.rows; row++) {
233
+ var tr = document.createElement('tr');
234
+
235
+ //apply classes to tr
236
+ if (this.appliedClasses['tr']?.[''+row]) {
237
+ tr.classList.add(...this.appliedClasses['tr'][''+row]);
238
+ }
239
+
240
+ for (var col = 1; col <= this.cols; col++) {
241
+ var td = document.createElement('td');
242
+
243
+ //apply classes to td
244
+ if (this.appliedClasses['td']?.[''+col]) {
245
+ td.classList.add(...this.appliedClasses['td'][''+col]);
246
+ }
247
+
248
+ var input = this.setInput(this.columnOptions[`${col}`]||{},row, col);
249
+
250
+ //add event listeners
251
+ this.initEventListeners(input);
252
+ this.element.addEventListener('keydown', this.handleKeyDown.bind(this,this));
253
+ input.addEventListener('keydown', this.handleKeyDown.bind(this, input));
254
+ input.addEventListener('mousedown', this.handleInputMouseDown.bind(this, input));
255
+ input.addEventListener('mouseover', this.handleMouseOver.bind(this, input)); // To handle dragging
256
+ input.addEventListener('change', this.addToDataMap.bind(this, input));
257
+
258
+ td.appendChild(input);
259
+ tr.appendChild(td);
260
+ }
261
+ tbody.appendChild(tr);
262
+ }
263
+ table.appendChild(tbody);
264
+
265
+ // Append the table to the specified element
266
+ this.element.appendChild(table);
267
+ };
268
+
269
+ DynamicTable.prototype.addToDataMap = function(input) {
270
+ const [row,col] = this.getRowCol(input);
271
+ const colName = this.headerNames[col];
272
+ value = input.value;
273
+ if (!this.dataMap.get(row)) {
274
+ this.dataMap.set(row,new Map());
275
+ }
276
+ let x = this.dataMap.get(row);
277
+ x.set(colName,value);
278
+ };
279
+
280
+ function mapToObject(map) {
281
+ let obj = {};
282
+ map.forEach((value, key) => {
283
+ // If value is a Map, convert it to an object recursively
284
+ if (value instanceof Map) {
285
+ obj[key] = mapToObject(value);
286
+ } else {
287
+ obj[key] = value;
288
+ }
289
+ });
290
+ return obj;
291
+ }
292
+
293
+
294
+
295
+ DynamicTable.prototype.getValue = function() {
296
+
297
+ return JSON.stringify(this.getValueAsObject());
298
+
299
+ };
300
+
301
+ DynamicTable.prototype.getValueAsObject = function() {
302
+
303
+ let obj = {};
304
+
305
+ for(i = 1; i <= this.rows; i++) {
306
+ obj[i] = {};
307
+ for (j = 1; j <= this.cols; j++) {
308
+ const val = document.getElementById(`${this.prefix}_r${i}c${j}`).value;
309
+ if (val) {obj[i][this.headerNames[j-1]] = val};
310
+ }
311
+ }
312
+
313
+ return obj; //return object itself to be able to process the values in their original form
314
+ };
315
+
316
+ DynamicTable.prototype.addStyles = function() {
317
+ let style = document.createElement('style');
318
+ style.type = 'text/css';
319
+ style.innerHTML = `
320
+ #${this.element.id} input[type=number]::-webkit-outer-spin-button,
321
+ #${this.element.id} input[type=number]::-webkit-inner-spin-button {
322
+ -webkit-appearance: none; /* Hides the spin buttons in Chrome, Safari, and Edge */
323
+ margin: 0; /* Remove default margin */
324
+ }
325
+ #${this.element.id} input[type=number] {
326
+ -moz-appearance: textfield; /* Hides the spin buttons in Firefox */
327
+ }
328
+ `;
329
+ document.head.appendChild(style); // Append the <style> to the <head>
330
+ };
331
+
332
+ DynamicTable.prototype.setValue = function(dyntable, values) {
333
+ Object.entries(values).forEach(
334
+ ([key,colValues]) => {
335
+ Object.entries(colValues).forEach(
336
+ ([k,v]) => {
337
+ const colIndex = dyntable.headerNames.indexOf(k);
338
+ if (colIndex != -1) {
339
+ const cellIndex = this.prefix + '_r' + key + 'c' + (colIndex+1);
340
+ cell = document.getElementById(cellIndex);
341
+ cell.value = v;
342
+ }
343
+ }
344
+ );
345
+ }
346
+ );
347
+ };
348
+
349
+ DynamicTable.prototype.handleKeyDownForInput = function(input, event) {
350
+
351
+ let [row,col] = this.getRowCol(input);
352
+ switch (event.keyCode) {
353
+ case 37:
354
+ event.preventDefault();
355
+ if (col > 0) {
356
+ col = col - 1;
357
+ this.onFirstCellSelection(input, row, col);
358
+ }
359
+ break;
360
+ case 38:
361
+ event.preventDefault();
362
+ if (row > 0 ) {
363
+ row = row - 1;
364
+ this.onFirstCellSelection(input, row, col);
365
+ }
366
+ break;
367
+ case 39:
368
+ event.preventDefault();
369
+ if (col < (this.cols - 1)) {
370
+ col = col + 1;
371
+ this.onFirstCellSelection(input, row, col);
372
+ }
373
+ break;
374
+ case 40:
375
+ event.preventDefault();
376
+ if (row < (this.rows - 1)) {
377
+ row = row + 1;
378
+ this.onFirstCellSelection(input, row, col);
379
+ }
380
+ break;
381
+ }
382
+
383
+
384
+ };
385
+
386
+ DynamicTable.prototype.onFirstCellSelection = function(input, newRow, newCol) {
387
+ let targetId = `${this.prefix}_r${newRow+1}c${newCol+1}`;
388
+ let cell = document.getElementById(targetId);
389
+ if (input.classList.contains('selected')) {
390
+ input.classList.remove('selected');
391
+ }
392
+ if (!cell.classList.contains('selected')) {
393
+ cell.classList.add('selected');
394
+ cell.focus();
395
+ }
396
+
397
+ this.initiateSelectionMap(cell);
398
+ [this.pivotRow, this.pivotCol] = this.getRowCol(cell);
399
+ const [lowestRow,highestRow,lowestCol,highestCol] = this.selectionMapRange();
400
+ this.repaintRange(lowestRow,highestRow,lowestCol,highestCol);
401
+ };
402
+
403
+ DynamicTable.prototype.handleKeyDown = function(input, event) {
404
+ if (input.classList && input.classList.contains('table-input')) {
405
+ if (!(event.metaKey || event.ctrlKey) && !event.shiftKey && ['ArrowDown','ArrowUp'].includes(event.key)) {
406
+ event.preventDefault(); // Prevents the default action
407
+ this.handleKeyDownForInput(input,event);
408
+ }
409
+
410
+ if ((!navigator) && (event.metaKey || event.ctrlKey) && !event.shiftKey && ['C','c'].includes(event.key)) {
411
+ this.handleCopy(event);
412
+ event.stopPropagation();
413
+ }
414
+
415
+ if ((!navigator) && (event.metaKey || event.ctrlKey) && !event.shiftKey && ['V','v'].includes(event.key)) {
416
+ if (input.type === 'date' || input.type === 'select-one') {
417
+ this.handlePaste(event);
418
+ event.stopPropagation();
419
+ }
420
+
421
+ }
422
+
423
+ }
424
+
425
+
426
+ // Detect if Cmd (macOS) or Ctrl (Windows/Linux), Shift, and ArrowDown are pressed together
427
+ if ((event.metaKey || event.ctrlKey) && event.shiftKey && event.key === 'ArrowDown') {
428
+ const [lowestRow,highestRow,lowestCol,highestCol] = this.selectionMapRange();
429
+ this.repaintRange(lowestRow, this.rows, lowestCol, highestCol);
430
+ event.preventDefault();
431
+ }
432
+
433
+ if (event.key === 'Delete') {
434
+ let twoDArray = [];
435
+ twoDArray.push(['']);
436
+ this.pasteDataToTable(twoDArray); // Prevents the default action
437
+ }
438
+ };
439
+
440
+ // Detect if someone clicks outside the table and remove the table selection
441
+ DynamicTable.prototype.initClickOutsideListener = function() {
442
+ const tableElement = this.element; // Table element
443
+ const dynTable = this;
444
+
445
+ document.addEventListener('click', function(event) {
446
+ // Check if the click was outside the table and clears selection
447
+ if (!tableElement.contains(event.target)) {
448
+ dynTable.resetSelectionMap();
449
+ dynTable.repaintRange(-1,-1,-1,-1);
450
+ }
451
+ });
452
+ };
453
+
454
+
455
+ DynamicTable.prototype.handleInputMouseDown = function(input, event) {
456
+ event.stopPropagation();
457
+ this.isMouseDown = true; // Start selection
458
+ this.initiateSelectionMap(input);
459
+ [this.pivotRow, this.pivotCol] = this.getRowCol(input);
460
+ const [lowestRow,highestRow,lowestCol,highestCol] = this.selectionMapRange();
461
+ this.repaintRange(lowestRow,highestRow,lowestCol,highestCol);
462
+ document.addEventListener('mouseup', this.handleMouseUp.bind(this,input)); // Attach mouseup listener
463
+ };
464
+
465
+ // Handle mouse over event to select cells while dragging
466
+ DynamicTable.prototype.handleMouseOver = function(input, event) {
467
+
468
+ if (this.isMouseDown) {
469
+ this.addToSelectionMap(input);
470
+ }
471
+ };
472
+
473
+ // Handle mouse up event to stop selecting
474
+ DynamicTable.prototype.handleMouseUp = function(input,event) {
475
+ this.isMouseDown = false; // Stop selection
476
+ document.removeEventListener('mouseup',this.handleMouseUp.bind(this,input)); // Remove mouseup listener
477
+ };
478
+
479
+ DynamicTable.prototype.resetSelectionMap = function() {
480
+ // Set all values to zero using forEach
481
+ this.selectionMap.forEach((row, i) => {
482
+ row.forEach((_, j) => {
483
+ this.selectionMap[i][j] = 0;
484
+ });
485
+ });
486
+ };
487
+
488
+ DynamicTable.prototype.initiateSelectionMap = function(input) {
489
+ this.resetSelectionMap();
490
+ const regex = new RegExp(this.prefix + "_r(\\d+)c(\\d+)");
491
+ let cellId = input.id;
492
+ const match = cellId.match(regex);
493
+ if (match) {
494
+ const row = parseInt(match[1], 10); // Extracted row value as an integer
495
+ const col = parseInt(match[2], 10); // Extracted column value as an integer
496
+ this.selectionMap[row-1][col-1] = 1;
497
+ } else {
498
+ console.log("No match found.");
499
+ }
500
+ };
501
+
502
+ DynamicTable.prototype.getRowCol = function(input) {
503
+ const regex = new RegExp(this.prefix + "_r(\\d+)c(\\d+)");
504
+ let cellId = input.id;
505
+ const match = cellId.match(regex);
506
+
507
+ if (match) {
508
+ const row = parseInt(match[1], 10); // Extracted row value as an integer
509
+ const col = parseInt(match[2], 10); // Extracted column value as an integer
510
+ return [row-1,col-1];
511
+ } else {
512
+ return [-1,-1];
513
+ }
514
+ };
515
+
516
+ DynamicTable.prototype.addToSelectionMap = function(input) {
517
+ let [row, col] = this.getRowCol(input);
518
+
519
+ lowestRow = Math.min(this.pivotRow,row);
520
+ lowestCol = Math.min(this.pivotCol,col);
521
+ highestRow = Math.max(this.pivotRow,row);
522
+ highestCol = Math.max(this.pivotCol,col);
523
+
524
+ this.repaintRange(lowestRow,highestRow,lowestCol,highestCol);
525
+
526
+ };
527
+
528
+ DynamicTable.prototype.selectionMapRange = function() {
529
+ let lowestRow = -1;
530
+ let highestRow = -1;
531
+ let lowestCol = -1;
532
+ let highestCol = -1;
533
+
534
+ for (let i = 0; i < this.rows; i++) {
535
+ for (let j = 0; j < this.cols; j++) {
536
+ if (this.selectionMap[i][j] === 1) {
537
+ if (lowestRow===-1) {
538
+ lowestRow = i;
539
+ highestRow = i;
540
+ lowestCol = j;
541
+ highestCol = j;
542
+ }
543
+
544
+ if (lowestRow > i) {
545
+ lowestRow = i;
546
+ }
547
+
548
+ if (highestRow < i) {
549
+ highestRow = i;
550
+ }
551
+
552
+ if (lowestCol > j) {
553
+ lowestCol = j;
554
+ }
555
+
556
+ if (highestCol < j) {
557
+ highestCol = j;
558
+ }
559
+ }
560
+ }
561
+ }
562
+
563
+
564
+
565
+ return [lowestRow,highestRow,lowestCol,highestCol];
566
+ }
567
+
568
+ DynamicTable.prototype.repaintRange = function(lowestRow,highestRow,lowestCol,highestCol) {
569
+ this.resetSelectionMap();
570
+ for (let i = 0; i < this.rows; i++) {
571
+ for (let j = 0; j < this.cols; j++) {
572
+ if ((i >= lowestRow && i <= highestRow) && (j >= lowestCol && j <= highestCol)) {
573
+ this.selectionMap[i][j] = 1;
574
+ const cellId = this.prefix + "_r" + (i+1) + "c" + (j+1);
575
+ input = document.getElementById(cellId);
576
+ if (input && !input.classList.contains('selected')) {
577
+ input.classList.add('selected');
578
+ }
579
+ } else {
580
+ this.selectionMap[i][j] = 0;
581
+ const cellId = this.prefix + "_r" + (i+1) + "c" + (j+1);
582
+ input = document.getElementById(cellId);
583
+ if (input && input.classList.contains('selected')) {
584
+ input.classList.remove('selected');
585
+ }
586
+ }
587
+ }
588
+ }
589
+ };
590
+
591
+
592
+
593
+ // Initialize event listeners for copy/paste
594
+ DynamicTable.prototype.initEventListeners = function(input) {
595
+
596
+ input.addEventListener('copy', this.handleCopy.bind(this));
597
+ // Handle paste functionality
598
+ input.addEventListener('paste', this.handlePaste.bind(this),false);
599
+
600
+ };
601
+
602
+ DynamicTable.prototype.getSelectionArray = function() {
603
+ let twoDArray = [];
604
+ const [lowestRow,highestRow,lowestCol,highestCol] = this.selectionMapRange();
605
+
606
+ for (let i = lowestRow; i <= highestRow; i++ ) {
607
+ rowArray = [];
608
+ for (let j = lowestCol; j <= highestCol; j++) {
609
+ let element = document.getElementById(this.prefix+'_r'+(i+1)+'c'+(j+1));
610
+ rowArray.push(element.value);
611
+ }
612
+ twoDArray.push(rowArray);
613
+ }
614
+
615
+ return twoDArray;
616
+ };
617
+
618
+ DynamicTable.prototype.copyToClipboard = function(event, text) {
619
+ if (navigator.clipboard) {
620
+ navigator.clipboard.writeText(text)
621
+ .then(() => {
622
+ console.log('Text copied to clipboard');
623
+ })
624
+ .catch(err => {
625
+ console.error('Failed to copy text: ', err);
626
+ });
627
+ } else {
628
+ event.clipboardData?.setData("text/plain", text);
629
+ console.warn('Navigator API not supported in this browser');
630
+ }
631
+ }
632
+
633
+ // Handle copy functionality
634
+ DynamicTable.prototype.handleCopy = function(event) {
635
+ var copiedData = this.getSelectionArray();
636
+ var clipboardPasteDataFormat = copiedData.map(row => row.join('\t')).join('\n');
637
+ this.copyToClipboard(event, clipboardPasteDataFormat);
638
+ event.preventDefault();
639
+ };
640
+
641
+
642
+ // Handle paste functionality
643
+ DynamicTable.prototype.handlePaste = function(event) {
644
+
645
+ event.preventDefault();
646
+ let pastedData = "";
647
+
648
+ if (event.clipboardData || window.clipboardData) {
649
+ pastedData = (event.clipboardData || window.clipboardData).getData("text");
650
+ } else {
651
+ if (navigator.clipboard) {
652
+ navigator.clipboard.readText()
653
+ .then(text => {
654
+ console.log('Pasted content from clipboard:', text);
655
+ pastedData = text;
656
+ // Convert the pasted data into a 2D array
657
+
658
+ })
659
+ .catch(err => {
660
+ console.error('Failed to read clipboard contents:', err);
661
+ });
662
+ }
663
+ }
664
+
665
+ const rows = pastedData.trim().split('\n').map(row => row.trim()) ; // Split into rows by newline
666
+
667
+ const twoDArray = rows.map(row => row.split('\t')); // Split each row by tab (\t)
668
+
669
+ this.pasteDataToTable(twoDArray);
670
+
671
+ };
672
+
673
+ DynamicTable.prototype.pasteDataToTable = function(twoDArray) {
674
+ const rowStart = this.pivotRow;
675
+ const colStart = this.pivotCol;
676
+
677
+ const [_, highestRow, __, highestCol] = this.selectionMapRange();
678
+
679
+ if (twoDArray && twoDArray.length > 0) {
680
+
681
+ let [maxRows,maxCols,isSingle] = (twoDArray.length === 1 && twoDArray[0].length ===1)?
682
+ [highestRow + 1 - rowStart,highestCol + 1 - colStart,true] : [twoDArray.length,twoDArray[0].length,false];
683
+
684
+ let addRows = Math.min(this.rows - rowStart, maxRows);
685
+ let addCols = Math.min(this.cols - colStart, maxCols);
686
+
687
+ for (let i = rowStart; i < rowStart + addRows; i++) {
688
+ for (let j = colStart; j < colStart + addCols; j++) {
689
+ let element = document.getElementById(this.prefix+'_r'+(i+1)+'c'+(j+1));
690
+ if (element.readOnly) {
691
+ continue;
692
+ }
693
+ let valueToPaste = isSingle ? twoDArray[0][0] : twoDArray[i-rowStart][j-colStart];
694
+ if (element.writeFunction) { //if there are specific limitations or exceptions to manage
695
+ element.writeFunction(element, valueToPaste);
696
+ } else {
697
+ element.value = valueToPaste;
698
+ }
699
+ }
700
+ }
701
+
702
+ this.repaintRange(rowStart, rowStart + addRows-1, colStart, colStart + addCols -1 );
703
+
704
+ }
705
+
706
+ };
707
+
708
+ //helper functions
709
+
710
+ DynamicTable.prototype.addSpecialControls = function(input, options) {
711
+ const specialKlass = options['specialControl']?.['class'];
712
+ if (specialKlass) {
713
+ input.classList.add(specialKlass);
714
+ }
715
+ let targetFunc = options['specialControl']?.['initializingFunction'];
716
+ if (targetFunc) {
717
+ this.specialControlFunctions.set(specialKlass,targetFunc);
718
+ }
719
+ };
720
+
721
+ DynamicTable.prototype.setInput = function(columnOptions,row,col) {
722
+ switch (columnOptions['input']) {
723
+ case 'text':
724
+ var input = document.createElement('input');
725
+ input.type = 'text';
726
+ break;
727
+ case 'decimal':
728
+ var input = document.createElement('input');
729
+ input.type = 'text';
730
+ input.precision = columnOptions['precision']+''||2;
731
+ this.addDecimalEvents(input);
732
+ break;
733
+ case 'date':
734
+ var input = document.createElement('input');
735
+ input.type = 'date';
736
+ input.value = '';
737
+ input.placeholder = '';
738
+ break;
739
+ case 'select':
740
+ var input = document.createElement('select');
741
+ addValuesToSelect(input,columnOptions);
742
+ break;
743
+ case 'div':
744
+ var input = document.createElement('div');
745
+ // var child = document.createElement('div');
746
+ // input.appendChild(child);
747
+ break;
748
+ default:
749
+ var input = document.createElement('input');
750
+ input.type = 'text';
751
+ }
752
+ setInputStandardOptions(this.prefix,input,row,col,columnOptions);
753
+ if (typeof child !== 'undefined') {
754
+ this.addSpecialControls(child, columnOptions);
755
+ }
756
+ else {
757
+ this.addSpecialControls(input, columnOptions);
758
+ }
759
+
760
+ addInputEventHooks(input,columnOptions);
761
+ return input;
762
+ }
763
+
764
+ function addInputEventHooks(input,options) {
765
+ Object.entries(options['eventHook']||{}).forEach(
766
+ function([event,funcHook]) {
767
+ if (isValidEvent(input,event)) {
768
+ //the first argument to bind, binds the 'this' element. The remaining arguments if any added,
769
+ //then are prepended to the function params. if you need event object as a param, add this in your definition post
770
+ //any planned prepended arguments. JS automatically adds the event object at the end
771
+ input.addEventListener(event,funcHook.bind(input));
772
+ }
773
+ }
774
+ );
775
+ }
776
+
777
+ function setInputStandardOptions(prefix, input,row,col,options) {
778
+ input.id = `${prefix}_r${row}c${col}`; // Assign ID in <row><col> format
779
+ input.name = `${prefix}_r${row}c${col}`; // Assign name so it can be passed in form
780
+ input.className = 'table-input'; // Class for styling the input
781
+
782
+ if (options['readonly']) {
783
+ input.setAttribute('readonly', options['readonly']);
784
+ }
785
+ }
786
+
787
+ function addValuesToSelect(input, options) {
788
+ let values = options['values'] || [];
789
+ values.forEach(function(val)
790
+ {
791
+ const newOption = document.createElement("option");
792
+ newOption.value = val;
793
+ newOption.text = val;
794
+ input.appendChild(newOption);
795
+ });
796
+ }
797
+
798
+ function isValidEvent(element,eventName) {
799
+ return typeof element[`on${eventName}`] !== 'undefined';
800
+ }
801
+
802
+ //adding decimal field related event functions
803
+
804
+ DynamicTable.prototype.addDecimalEvents = function(input) {
805
+ input.addEventListener("input",this.onDecimalFocus.bind(this));
806
+ input.addEventListener("blur",this.onDecimalBlur.bind(this));
807
+ input.addEventListener("keydown",this.onDecimalKeydownEvent.bind(this));
808
+ input.addEventListener("paste",this.onDecimalChangeEvent.bind(this));
809
+ input.writeFunction = writeValueToDecimal;
810
+ input.value = getInitialValue(input.precision);
811
+ }
812
+
813
+ DynamicTable.prototype.onDecimalChangeEvent = function(event) {
814
+ const input = event.target;
815
+ const regex = new RegExp("^\\d+(\\.\\d{0," + input.precision + "})?$");// Regex to allow up to precision decimal places
816
+ if (!regex.test(input.value)) {
817
+ input.value = getInitialValue(input.precision);
818
+ }
819
+
820
+ }
821
+
822
+ DynamicTable.prototype.onDecimalBlur = function(event) {
823
+ const input = event.target;
824
+ let value = input.value;
825
+ if (!value) return;
826
+
827
+ input.value = formatInputValue(value, input.precision);
828
+ }
829
+
830
+ // Function to remove commas when the input is being given
831
+ DynamicTable.prototype.onDecimalFocus = function(event) {
832
+ const input = event.target;
833
+ input.value = input.value.replace(/,/g, '');
834
+ }
835
+
836
+ DynamicTable.prototype.onDecimalKeydownEvent = function(event) {
837
+ // Get the value of the input field and simulate the new input if the key were added
838
+ const input = event.target;
839
+ if ((event.metaKey || event.ctrlKey) && !event.shiftKey) {
840
+ if ((['C','c'].includes(event.key))
841
+ ||(['V','v'].includes(event.key))) {
842
+ return;
843
+ }
844
+ }
845
+
846
+ // Allow control keys (e.g., backspace, delete, arrow keys)
847
+ if (!this.decimalKeyRange.includes(event.key)) {
848
+ event.preventDefault();
849
+ } else {
850
+ // Get the current caret position
851
+ const caretPosition = input.selectionStart;
852
+ // Get the current value of the input field
853
+ const currentValue = input.value;
854
+ const key = event.key;
855
+ const newValue = currentValue.slice(0, caretPosition) + key + currentValue.slice(caretPosition);
856
+ if (!this.controlKeys.includes(key) ) {
857
+ const regex = new RegExp("^\\d+(\\.\\d{0," + input.precision + "})?$");// Regex to allow up to precision decimal places
858
+ if (!regex.test(newValue)) {
859
+ event.preventDefault();
860
+ }
861
+ }
862
+
863
+ }
864
+
865
+ }
866
+
867
+ function formatInputValue(value, precision) {
868
+ // Remove any existing commas
869
+ value = value.replace(/,/g, '');
870
+
871
+ let parts = value.split('.');
872
+ //remove all leading zeros
873
+ parts[0] = parts[0].replace(/^0+/, '');
874
+ parts[0] = parts[0] === ''? '0': parts[0];
875
+ // Add commas to every third digit before the decimal point
876
+ parts[0] = parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ',');
877
+
878
+ //in case of missing decimal point
879
+ if (parts.length === 1 && precision > 0) {
880
+ parts[1] = appendDecimalZeros(precision);
881
+ }
882
+
883
+ //if zeros are less then precision, replace remaining zeros
884
+ if (parts[1].length < precision) {
885
+ parts[1] = parts[1] + appendDecimalZeros(precision - parts[1].length);
886
+ }
887
+
888
+ return parts.join('.');
889
+ }
890
+
891
+ function appendDecimalZeros(precision) {
892
+ if (precision && precision > 0) {
893
+ let precision_arr = Array.from({ length: precision }, (_, i) => 0);
894
+ if (precision_arr.length > 0) {
895
+ return precision_arr.join('');
896
+ }
897
+ }
898
+ return '';
899
+ }
900
+
901
+ function adjustToPrecision(value, targetPrecision) {
902
+ let parts = value.split('.');
903
+ if (parts[1] && parts[1].length > targetPrecision) {
904
+ parts[1] = parts[1].slice(0,targetPrecision);
905
+ }
906
+ return parts.join(".");
907
+ }
908
+
909
+ function writeValueToDecimal(input, value) {
910
+ //remove any commas for testing with regexpression
911
+ value = value.replace(/,/g, '');
912
+ value = adjustToPrecision(value, input.precision);
913
+ const regex = new RegExp("^\\d+(\\.\\d{0," + input.precision + "})?$");// Regex to allow up to precision decimal places
914
+ if (!regex.test(value)) {
915
+ input.value = getInitialValue(input.precision);
916
+ } else {
917
+ input.value = formatInputValue(value, input.precision);
918
+ }
919
+ }
920
+
921
+ function getInitialValue(precision) {
922
+ if (precision && precision > 0) {
923
+ return "0." + appendDecimalZeros(precision);
924
+ }
925
+ return "0";
926
+ }
927
+
928
+
929
+ return DynamicTable;
930
+ }));
@@ -0,0 +1,56 @@
1
+ .dynamic-table {
2
+ border-collapse: collapse;
3
+ overflow: auto;
4
+ /* width: 100%; */
5
+ /* table-layout: fixed; Ensures all columns are the same width */
6
+ }
7
+ .dynamic-table th, .dynamic-table td {
8
+ border: 1px solid #000;
9
+ padding: 0;
10
+ text-align: center;
11
+ position: relative; /* Needed for absolute positioning of the input */
12
+ }
13
+
14
+ .table-container {
15
+ overflow:scroll;
16
+ width: 100%;
17
+ }
18
+
19
+ .table-input {
20
+ width: 100%; /* Makes the input field fill the width of the table cell */
21
+ height: 100%; /* Makes the input field fill the height of the table cell */
22
+ box-sizing: border-box; /* Ensures padding and border are included in the width/height */
23
+ border: none; /* Removes the default input field border */
24
+ text-align: center;
25
+ }
26
+ .table-input:focus {
27
+ outline: none; /* Removes the focus outline to give a clean look */
28
+ }
29
+ .dynamic-table td .table-input.selected {
30
+ background-color: #d1e0e0; /* Highlight selected cells */
31
+ }
32
+ .dynamic-table td {
33
+ height: 40px; /* Set a fixed height for cells */
34
+ }
35
+
36
+ .dynamic-table th, .dynamic-table td {
37
+ width: 10rem;
38
+ }
39
+
40
+ .uneditable .table-input {
41
+ user-select: none; /* Prevent text selection */
42
+ pointer-events: none; /* Disable interaction */
43
+ color: gray; /* Change text color */
44
+ background-color: lightgray;
45
+ }
46
+
47
+ .selectize-dropdown {
48
+ position: absolute; /* Position it outside of normal flow */
49
+ z-index: 9999; /* Make sure it appears above other elements */
50
+ }
51
+
52
+ td {
53
+ position: relative; /* The parent <td> needs to be relative for absolute child positioning */
54
+ width: 150px;
55
+ height: 50px;
56
+ }
@@ -0,0 +1,9 @@
1
+ module DynamicTable
2
+ class Engine < ::Rails::Engine
3
+ initializer "dynamic_table.assets.precompile" do |app|
4
+ puts "DynamicTable Engine Loaded" # Debugging statemen
5
+ app.config.assets.precompile += %w(dynamic_table.scss dynamic_table.js)
6
+ end
7
+ puts "DynamicTable Engine Loaded2" # Debugging statemen
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module DynamicTable
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # lib/dynamic_table.rb
4
+ require "dynamic_table/version"
5
+ require "dynamic_table/engine" # This line loads the engine
6
+
7
+ module DynamicTable
8
+ puts "DynamicTable Module Loaded"
9
+ # class Error < StandardError; end
10
+ # Your code goes here...
11
+ end
metadata ADDED
@@ -0,0 +1,51 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dynamic_table
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - jayanth
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2024-10-30 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: |-
14
+ This GEM is an attempt to create editable tables wherein cells may contain text, dates, select,decimals and so on.
15
+ You can select a set of cells and paste data. Paste from excel or other spreadsheets on to the same. You can add styles at a column
16
+ or row level as well.
17
+ email:
18
+ - jayanth.ravindran@gmail.com
19
+ executables: []
20
+ extensions: []
21
+ extra_rdoc_files: []
22
+ files:
23
+ - app/assets/javascripts/dynamic_table.js
24
+ - app/assets/stylesheets/dynamic_table.scss
25
+ - lib/dynamic_table.rb
26
+ - lib/dynamic_table/engine.rb
27
+ - lib/dynamic_table/version.rb
28
+ homepage:
29
+ licenses: []
30
+ metadata:
31
+ allowed_push_host: https://rubygems.org
32
+ post_install_message:
33
+ rdoc_options: []
34
+ require_paths:
35
+ - lib
36
+ required_ruby_version: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: 3.0.0
41
+ required_rubygems_version: !ruby/object:Gem::Requirement
42
+ requirements:
43
+ - - ">="
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ requirements: []
47
+ rubygems_version: 3.5.9
48
+ signing_key:
49
+ specification_version: 4
50
+ summary: Gem to create editable tables dynamically using javascript
51
+ test_files: []