Diff -- /var/www/vhosts/3dshawn.com/crm.3dshawn.com/assets/javascript/grid_component.js
Diff

/var/www/vhosts/3dshawn.com/crm.3dshawn.com/assets/javascript/grid_component.js

added on local at 2026-07-01 15:02:37

Added
+5111
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to de4981d9ffd6
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1class ShawnGrid extends HTMLElement {
2 constructor() {
3 // Always call super first in constructor
4 super();
5 this.attachShadow({ mode: "open" });
6 console.log("Linked Version Edit");
7 // Setting the default global variables to be used.
8 // Full json data from the back-end. INJECTABLE
9 this.GLOBAL_JSON_RESULT = []; //INJECTABLE
10 // Filtered json load from the user filters and json data. Populated by filter_json() NOT-INJECTABLE
11 // THIS IS THE DATA THE GRID USES, IF YOU WANT TO MODIFY AND RELOAD, INJECT TO GLOBAL_JSON_RESULT
12 this.GLOBAL_JSON_RESULT_FILTERED = []; // DO NOT CHANGE - SYSTEM USE ONLY
13 // User filters for the table pulled from the back-end. NOT-INJECTABLE
14 this.GLOBAL_LOADED_FILTERS = []; // DO NOT CHANGE - SYSTEM USE ONLY
15 // To handle copying into the table, and undoing copy. Limited to 1 like the delete
16 this.UNDO_COPY_JSON = []; // DO NOT CHANGE - SYSTEM USE ONLY
17 this.UNDO_OVER_COPY_JSON = []; // DO NOT CHANGE - SYSTEM USE ONLY
18 this.UNDO_INSERT_STARTING_ID = -1; // DO NOT CHANGE - SYSTEM USE ONLY
19 this.UNDO_INSERT_AMOUNT = -1; // DO NOT CHANGE - SYSTEM USE ONLY
20 this.COPY_COUNT = 0; // DO NOT CHANGE - SYSTEM USE ONLY
21 this.ANCHOR_NODE; // DO NOT CHANGE - SYSTEM USE ONLY
22 this.ANCHOR_OFFSET; // DO NOT CHANGE - SYSTEM USE ONLY
23 this.FOCUS_OFFSET; // DO NOT CHANGE - SYSTEM USE ONLY
24 this.CONTENT_NODE; // DO NOT CHANGE - SYSTEM USE ONLY
25 this.SELECTION_STRING; // DO NOT CHANGE - SYSTEM USE ONLY
26 this.COLUMN_NAME_ORDER = []; // DO NOT CHANGE - SYSTEM USE ONLY
27 this.column_information; // DO NOT CHANGE - SYSTEM USE ONLY
28 this.LAST_ACTIVE_ELEMENT_ID = ''; // DO NOT CHANGE - SYSTEM USE ONLY
29 this.LAST_ACTIVE_ELEMENT_CURSOR = 0; // DO NOT CHANGE - SYSTEM USE ONLY
30 this.reset_zoom_setting = 14; // DO NOT CHANGE - SYSTEM USE ONLY
31 this.zoom_setting; // DO NOT CHANGE - SYSTEM USE ONLY
32 this.scroll_allowed = false; // Updated by the config
33 this.load_target_page = 0; // USED FOR INJECTION - NOTE ASK TEAM ABOUT
34
35 this.toolbar_allowed = true; // Default true, Provided by the config
36 this.current_sorting = ''; // DO NOT CHANGE - SYSTEM USE ONLY
37 this.default_styles = {}; // DO NOT CHANGE - SYSTEM USE ONLY
38 this.contextmenu = true; // Default true, Provided by the config
39 this.default_widths = {}; // DO NOT CHANGE - SYSTEM USE ONLY
40 this.configuration = {}; // Provided by the config
41
42 // Saving the most recently deleted row for recovery
43 this.recent_deletion = {}; // DO NOT CHANGE - SYSTEM USE ONLY
44 this.recent_deletion_index = -1; // DO NOT CHANGE - SYSTEM USE ONLY
45 this.recent_deletion_filtered_index = -1; // DO NOT CHANGE - SYSTEM USE ONLY
46
47 this.undo_delete_check = false; // DO NOT CHANGE - SYSTEM USE ONLY
48 this.grid_name; // Provided by the config
49 this.save_filters_url; // Provided by the config
50 this.load_filters_url; // Provided by the config
51 this.check_highlight_row_url; // Provided by the config
52 this.insert_row_url; // Provided by the config
53 this.column_rename = {}; // DO NOT CHANGE - SYSTEM USE ONLY
54 this.grid_settings = []; // DO NOT CHANGE - SYSTEM USE ONLY
55 this.grid_filters = []; // DO NOT CHANGE - SYSTEM USE ONLY
56 this.excel_edit = false; // DO NOT CHANGE - SYSTEM USE ONLY
57
58 this.grid_widths = []; // DO NOT CHANGE - SYSTEM USE ONLY
59 //Pagination Values
60 this.max_results; // Provided by the config
61 this.max_pagination_links = ''; // DO NOT CHANGE - SYSTEM USE ONLY
62 this.pagination_selected_value = ''; // DO NOT CHANGE - SYSTEM USE ONLY
63 this.grid_filter_list = ''; // DO NOT CHANGE - SYSTEM USE ONLY
64 this.grid_filter_columns = []; // DO NOT CHANGE - SYSTEM USE ONLY
65
66 this.stylesheet; // DO NOT CHANGE - SYSTEM USE ONLY
67 this.stylesheet_injection; // Provided by the config
68 this.data_url; // Provided by the config
69 // Identifier Variables
70 this.primary_key = ''; // Default empty, provided by the config
71 this.url_identifier = ''; // Default empty, provided by the config
72 // Context Menu Variables
73 this.context_menu_injection = ''; // Default empty, provided by the config
74 this.right_click_source_cell; // DO NOT CHANGE - SYSTEM USE ONLY
75 this.drag_increment_mode = false; // Default false, provided by the config
76 this.excel_button = true; // Default true, provided by the config
77 this.increment_button = true; // Default true, provided by the config
78 this.drag_copy_features = true; // Default true, provided by the config
79 this.filter_notification = true; // Default true, provided by the config
80 this.zoom_button = true; // Default true, provided by the config
81 this.highlight_checkbox; // Provided by the config
82 this.highlight_checked; // Provided by the config
83 // Table Data Manipulation Variables
84 this.starting_cell; // DO NOT CHANGE - SYSTEM USE ONLY
85 this.ending_cell; // DO NOT CHANGE - SYSTEM USE ONLY
86 this.copy_lock = false; // DO NOT CHANGE - SYSTEM USE ONLY
87 this.modified_data = {}; // DO NOT CHANGE - SYSTEM USE ONLY
88 this.save_data_url = ''; // Default empty, provided by the config
89 this.copy_element_array = []; // DO NOT CHANGE - SYSTEM USE ONLY
90 }
91 escape_json(new_json){
92 // Load Global JSON returns a string of text, that string needs to be stringified to properly escape quotes.
93 // let new_json = JSON.stringify(json);
94 // // Once stringified, we further escape special characters to prevent them from rendering in the dom.
95
96 // new_json = new_json.replace(/\\f/g, '\\\\f');
97 // new_json = new_json.replace(/\\n/g, '\\\\n');
98 // new_json = new_json.replace(/\\r/g, '\\\\r');
99 // new_json = new_json.replace(/\\t/g, '\\\\t');
100 new_json = new_json.replace(/'/g, `\'`);
101 new_json = new_json.replace(/"/g, `\\"`);
102 // new_json = new_json.replace(/'/g, "\\\\'");
103 // To get the resulting string back into an ARRAY of JSON, we parse it once to get it back to a proper JSON string, then parse it again
104 // To get it to properly evaluate as an array of json, not a broken json string.
105
106 return JSON.parse(new_json);
107 // return JSON.parse(JSON.parse(new_json));
108 }
109 async load_global_json(url){
110 const response = await fetch(url);
111 // Return the response as text instead of json, so we can escape special characters.
112 const jsonData = response.text();
113 return jsonData;
114
115
116 }
117 async get_request(url){
118 const rawResponse = await fetch(url, {
119 method: 'GET'
120 });
121 const content = rawResponse.text();
122 return content;
123 }
124 async send_json(url, json){
125 const rawResponse = await fetch(url, {
126 method: "POST",
127 headers: {
128 "Content-Type": "application/json",
129 },
130 body: JSON.stringify(json),
131 });
132 const content = rawResponse.text();
133 return content;
134 }
135 connectedCallback() {
136 // Styling for the component, style sheets outside the component DO NOT affect the component.
137 this.stylesheet = document.createElement('style');
138
139 if(this.highlight_checked){
140 this.stylesheet.innerHTML = `
141 .hyperlink-form-popup button{
142 user-select: none;
143 }
144 .disabled-mouse{
145 pointer-events: none;
146 }
147 a:hover{
148 cursor: pointer;
149 }
150 .hyperlink-form-hidden{
151 display: none;
152 }
153 .hyperlink-form-popup{
154 display: flex;
155 background-color: #d3d3d3;
156 flex-direction: column;
157 align-items: center;
158 justify-content: center;
159 height: 200px;
160 width: 200px;
161 border: 1px solid black;
162 border-radius: 10% 10% 10% 10%;
163 position: absolute;
164 z-index: 999999;
165 top: 0px;
166 left: 45%;
167 }
168 .zoom-button-container{
169 display: flex;
170 align-items: center;
171 justify-content: space-around;
172 width: 175px;
173 }
174 .zoom-button-container >*:hover{
175 transform: scale(1.05);
176 }
177 .empty-json-container{
178 display: flex;
179 flex-direction: column;
180 width: 100%;
181 align-items: center;
182 justify-content: center;
183 min-height: 300px;
184 }
185 .spinner-container * {
186 margin-left: 10px;
187 margin-right: 10px;
188 }
189 .spinner-container{
190 display: flex;
191 flex-direction: row;
192 align-items: center;
193 justify-content: center;
194 }
195 .load-message{
196 font: 30px Arial;
197 animation: breathe 3s linear infinite;
198 }
199 .spinner{
200 background: #FFFFFF;
201 height: 30px;
202 width: 30px;
203 border-radius: 50%;
204 border: 5px solid #0097d8;
205 border-top: 5px solid #003057;
206 animation: spin 2s linear infinite;
207 }
208 @keyframes spin {
209 0% {
210 transform: rotate(0deg);
211 }
212 100% {
213 transform: rotate(360deg);
214
215 }
216 }
217 @keyframes breathe {
218 0% {
219 opacity: 100%;
220 transform: scale(1.15);
221 }
222 50% {
223 transform: scale(0.95);
224 opacity: 25%;
225 }
226 100% {
227 opacity: 100%;
228 transform: scale(1.15);
229
230 }
231 }
232 /*Rare case of important, we want this to override any background styling*/
233 .blink-field-red{
234 background-color: red!important;
235 animation: blink 2s linear infinite;
236 }
237 @keyframes blink {
238 0% {
239 opacity: 100%;
240 }
241 50% {
242 opacity: 25%;
243 }
244 100% {
245 opacity: 100%;
246
247 }
248 }
249 table tr td:has(select.color-my-parent){
250 background: #ffff00;
251 }
252 table tr td:has(input.color-my-parent){
253 background: #ffff00;
254 }
255 .row_number{
256 color: #FFFFFF;
257 position: sticky;
258 left: 0px;
259 background-color: #888888;
260 z-index: 9998;
261 font-size: var(--mod-text-size);
262 }
263 .row_number.highlight_error{
264 color: #888888;
265 background-color: #ffe0e0;
266 }
267 .row_number.highlight_error2{
268 background-color: #992d28;
269 }
270 .row_number.highlight_error3{
271 background-color: #eb8a49;
272 }
273 thead{
274 position: sticky;
275 left: 0px;
276 z-index: 9999;
277 margin-top: -1px;
278 }
279 .super_header_row{
280 display: flex;
281 flex-direction: row;
282 border: solid 1px #dddddd;
283 position: sticky;
284 top: 0px;
285 left: 0px;
286 z-index: 9999;
287 }
288 .super_header{
289 text-align: left;
290 border-left-width: 0px;
291 padding-left: 0px;
292 padding-right: 0px;
293
294 color: #E3E3E3;
295 background-color: #666666;
296 height: 35px;
297 }
298 .super_header::before{
299 content: attr(super-header-name);
300 position: sticky;
301 left: 15px;
302 top: 10px;
303 margin-left: 15px;
304 font: 15px Arial;
305 font-weight: normal;
306 }
307 .icon_tooltip_container{
308 display: flex;
309 flex-direction: row;
310 align-items: center;
311 justify-content: flex-start;
312 }
313 .icon_container{
314 display: flex;
315 flex-direction: row;
316 align-items: center;
317 justify-content: center;
318 flex-wrap: wrap;
319 margin-right: 10px;
320 }
321 .increment-button{
322 border-radius: 2px;
323 background-color: #f5f5f5;
324 padding: 0px 10px 0px 10px;
325 font: 12px Verdana, Geneva, Arial, Helvetica, sans-serif;
326 cursor: pointer;
327 color: #555555;
328 border: 1px solid #aeaeae;
329 height: 25px;
330 }
331 .increment-button:hover{
332 transform: scale(1.05);
333 cursor: pointer;
334 }
335 .increment-toggle{
336 background-color: #ffaaaa;
337 }
338 .tooltips{
339 display: flex;
340 flex-direction: column;
341 align-items: flex-start;
342 justify-content: center;
343 }
344 .tooltips p{
345 margin-top: 0px;
346 margin-bottom: 0px;
347 }
348 .toolbar_container > * {
349 margin-left: 3px;
350 margin-right: 3px;
351 }
352 .no_data_container{
353 display: flex;
354 flex-direction: column;
355 align-items: center;
356 justify-content: center;
357 }
358 .highlight_section > * {
359 margin-left: 2px;
360 margin-right: 2px;
361 margin-top: 0px;
362 margin-bottom: 0px;
363 }
364 .highlight_section{
365 display: flex;
366 align-items: center;
367 justify-content: space-evenly;
368 }
369 .filter_dot_none{
370 height: 20px;
371 width: 20px;
372 display: flex;
373 align-items: center;
374 justify-content: center;
375 padding-bottom: 3px;
376 font-size: 16px;
377 background-color: #95cf8c;
378 border-radius: 30%;
379 }
380 .filter_dot_active{
381 height: 20px;
382 width: 20px;
383 display: flex;
384 align-items: center;
385 justify-content: center;
386 padding-bottom: 3px;
387 font-size: 16px;
388 background-color: #cf8c8c;
389 border-radius: 30%;
390 }
391 .filter_notification > * {
392 margin-left: 2px;
393 margin-right: 2px;
394 margin-top: 0px;
395 margin-bottom: 0px;
396 }
397 .filter_notification{
398 display: flex;
399 align-items: center;
400 justify-content: space-evenly;
401 }
402 .table_container{
403 display: flex;
404 flex-direction: column;
405 overflow-y: auto;
406 min-height: 20em;
407 max-height: calc(97lvh - 500px);
408 overflow-x: scroll;
409 }
410 .toolbar_container{
411 display: flex;
412 flex-direction: row;
413 padding-top: 5px;
414 padding-bottom: 5px;
415 align-items: center;
416 justify-content: space-between;
417 border-bottom: 2px solid #666666;
418 }
419 .grid-icon{
420 max-width: 50px;
421 }
422 .grid-icon:hover{
423 max-width: 50px;
424 transform: scale(1.1);
425 cursor: pointer;
426 }
427 .active_filter{
428 background-color: #828267;
429 }
430 @keyframes slideOutFromLeft {
431 0% {
432 transform: translateX(0);
433 }
434 50% {
435 transform: translateX(50%);
436 }
437 100% {
438 transform: translateX(100%);
439
440 }
441 }
442 @keyframes slideInFromLeft {
443 0% {
444 transform: translateX(-100%);
445 }
446 100% {
447 transform: translateX(0);
448 }
449 }
450 .grid_table_data:active .copy_drag{
451 display: inline-block;
452 color: red;
453 }
454 table:not(.grid-dragging) .grid_table_data:hover .copy_drag{
455 display: inline-block;
456 color: #bad7c0;
457 }
458 td.copied_over_wrong{
459 background-color: #ffaaaa;
460 }
461 td.copied_over{
462 background-color: #bad7c0;
463 }
464 td .copy_drag{
465 font: 28px Arial;
466 width: 1vw;
467
468 position: relative;
469 vertical-align: middle;
470 display: none;
471
472 }
473 td .copy_drag_inner{
474 /*
475 position: absolute;
476 right: -1px;
477 */
478 }
479 td .copy_drag_inner:hover{
480 /*
481 position: absolute;
482 right: -1px;
483 font: 30px Arial;
484 */
485 cursor: grab;
486
487 }
488 .confirmation_container{
489 /* display: flex;
490 flex-direction: column;
491 justify-content: space-around;
492 align-items: center; */
493 position: absolute;
494 left: 0%;
495 top: 0%;
496 z-index: 99999;
497 width: 100%;
498 height: 100%;
499 background-color: rgba(123, 133, 182, 0.19);
500 }
501 .confirmation_container_display{
502 display: flex;
503 flex-direction: column;
504 width: 20vw;
505 min-height: 10vw;
506 background-color: #c3c3c3;
507 padding: 1vw 1vw 1vw 1vw;
508 border-radius: 1vw;
509 left: calc(50vw - 10vw);
510 position: absolute;
511 }
512 .confirmation_button_container{
513 display: flex;
514 flex-direction: row;
515 width: 100%;
516 height: 15%;
517 justify-content: space-between;
518 align-items: center;
519 }
520 .confirmation_button_container button{
521 width: 75px;
522 }
523 .context-menu{
524 z-index: 10000;
525 position: fixed;
526 background: #c3c3c3;
527 color: #555;
528 font-family: sans-serif;
529 font-size: 11px;
530 -webkit-transition: opacity .5s ease-in-out;
531 -moz-transition: opacity .5s ease-in-out;
532 -ms-transition: opacity .5s ease-in-out;
533 -o-transition: opacity .5s ease-in-out;
534 transition: opacity .5s ease-in-out;
535 -webkit-box-shadow: 2px 2px 2px 0px rgba(143, 144, 145, 1);
536 -moz-box-shadow: 2px 2px 2px 0px rgba(143, 144, 145, 1);
537 box-shadow: 2px 2px 2px 0px rgba(143, 144, 145, 1);
538 padding: 0px;
539 border: 1px solid #C6C6C6;
540 }
541 .context-menu a {
542 display: block;
543 color: #555;
544 text-decoration: none;
545 padding: 6px 8px 6px 30px;
546 width: 250px;
547 position: relative;
548 }
549
550 .context-menu a span {
551 color: #BCB1B3;
552 float: right;
553 }
554
555 .context-menu a:hover {
556 color: #fff;
557 background: #3879D9;
558 }
559
560 .context-menu hr {
561 border: 1px solid #EBEBEB;
562 border-bottom: 0;
563 }
564 .grid_table{
565 border: solid 1px #dddddd;
566 border-collapse: collapse;
567 padding: 2px 3px;
568 text-align: center;
569 table-layout: fixed;
570 width: 100%;
571 }
572
573 .grid_table_header{
574 padding: 5px;
575 font: 12px Arial;
576 font-weight: normal;
577 color: #E3E3E3;
578 background-color: #666666;
579 height: 35px;
580 border: solid 1px #dddddd;
581 border-collapse: collapse;
582 text-align: center;
583 }
584 .grid_table_header form{
585 display: flex;
586 justify-content: flex-end;
587 }
588 .grid_table_header form div:not(.grid_menu):not(.ignore_overflow):not(.grid_menu_options):not(.suggestion-container) {
589 text-overflow: ellipsis;
590 overflow: hidden;
591 white-space: nowrap;
592 }
593 .excel_table_header{
594 padding: 5px;
595 font: 12px Arial;
596 font-weight: normal;
597 color: #E3E3E3;
598 background-color: #217346;
599 height: 35px;
600 border: solid 1px #dddddd;
601 border-collapse: collapse;
602 text-align: center;
603 }
604 .excel_table_header form{
605 display: flex;
606 justify-content: flex-end;
607 }
608 .excel_table_header form div:not(.grid_menu):not(.ignore_overflow):not(.grid_menu_options) {
609 text-overflow: ellipsis;
610 overflow: hidden;
611 white-space: nowrap;
612 }
613 .grid_table_data{
614
615 height: inherit;
616 font: var(--mod-text-size) Arial;
617 border: solid 1px #dddddd;
618 border-collapse: collapse;
619 padding: calc(var(--mod-text-size) / 4);
620 text-align: center;
621 }
622 .grid_table_data .cell-data-wrapper{
623 text-overflow: ellipsis;
624 text-wrap: nowrap;
625 overflow: hidden;
626 width: 90%;
627 }
628 .grid_table_data :is(select,input){
629 text-overflow: ellipsis;
630 text-wrap: nowrap;
631 overflow: hidden;
632 }
633 .grid_table_data textarea{
634 overflow: hidden;
635 }
636 .grid_table_data .contentarea{
637 overflow: hidden;
638 font-size: var(--mod-text-size);
639
640 }
641 .grid_table_data .contentarea a:visited{
642 color: #336699;
643 }
644 .grid_table_data .contentarea a{
645 color: #336699;
646 }
647 .grid_table_data .contentarea:has(a){
648 text-decoration: underline;
649 color: #336699;
650 -webkit-user-modify: read-write-plaintext-only;
651 overflow-wrap: break-word;
652 -webkit-line-break: after-white-space;
653 cursor: pointer;
654 }
655 .grid_table_data > * {
656 display: inline-block;
657 margin-left: 5px;
658 margin-right: 5px;
659 vertical-align: middle;
660 }
661 tr .grid_table_data > *:not(div) {
662 height: calc(var(--mod-text-size) + 4px);
663 display: inline-block;
664 font-size: var(--mod-text-size);
665 }
666
667 .grid_menu{
668 position: relative;
669 top: -1px;
670 left: -4px;
671 float: left;
672 padding: 0px 3px 0px 8px;
673 color: #CCCCCC;
674 z-index:1;
675 }
676 .grid_menu:hover{
677 color: #FFFFFF;
678 }
679 .grid_menu_options{
680 position:absolute;
681 width:calc(100% - 10px);
682 }
683 .grid_menu_options:hover{
684 background-color: #D3D3D3;
685 }
686 .filter_box{
687 position: relative;
688 width: 260px;
689 height: 220px;
690 border: 1px solid #888888;
691 background-color: #FFFFFF;
692 color: #000000;
693 padding: 5px;
694 text-align: left;
695 align: left;
696 float: left;
697 z-index: 3;
698 }
699 .pagination_container{
700 margin-left: 5px;
701 margin-top: 5px;
702 margin-bottom: 5px;
703 border-top: 2px solid #666666;
704 }
705 .pagination_area{
706 position: relative;
707 float: left;
708 font: 14px Arial;
709 color: #666666;
710 }
711 .pagination_links{
712 position: relative;
713 top: 2px;
714 font: 14px Arial;
715 color: #666666;
716 text-decoration:none;
717 margin-left: 2px;
718 margin-right: 2px;
719 }
720
721 .pagination_block{
722 position: relative;
723 top : 2px;
724 border: 1px solid #666666;
725 padding: 0px 5px 0px 5px;
726 }
727
728 .pagination_selected_link{
729 font-weight: bold;
730 font: 20px Arial;
731 color: #444444;
732 /* text-decoration: underline; */
733 }
734
735 .pagination_page_number_textbox{
736 position: relative;
737 width: 60px;
738 }
739
740 .max_results_per_page{
741 position: relative;
742 float: left;
743 top: 5px;
744 left: 5px;
745 font: 14px Arial;
746 color: #666666;
747 }
748
749 .round{
750 -webkit-border-radius: 3px;
751 -moz-border-radius: 3px;
752 border-radius: 3px;
753 }
754 .grid_tr_alternating tr:nth-child(odd):not([class^=""]){
755 height:1px;
756 color: #999999;
757 border: 1px solid #F2F2F2;
758 background-color:#EEEEEE;
759 }
760 .grid_tr_alternating tr:nth-child(even){
761 height:1px;
762 color: #999999;
763 border: 1px solid #ededed;
764 background-color:#F2F2F2;
765 }
766
767 .grid_tr_alternating tr:nth-child(odd):hover{
768 color: #FFFFFF;
769 border: 1px solid #ededed;
770 background-color: #888888;
771 }
772 .grid_tr_alternating tr:hover td.grid_table_data{
773 color: #FFFFFF;
774 border: 1px solid #ededed;
775 background-color: #888888;
776 }
777
778 .grid_data_cells{
779 height:18px;
780 position:relative;
781 font: 12px Arial;
782 font-weight: normal;
783 text-align:left;
784 padding-left: 5px;
785 }
786
787 .grid_standard_td{
788 color: #999999;
789 background-color:#FFFFFF;
790 }
791 .table th {
792 position: relative;
793 }
794 .resizer {
795 position: absolute;
796 top: 0;
797 right: 0;
798 width: 5px;
799 cursor: col-resize;
800 user-select: none;
801 height: 100%;
802 }
803 .resizer:hover,
804
805 .resizing {
806 border-right: 2px solid #333333;
807 }
808
809 .resizable {
810 border: 1px solid gray;
811 height: 100px;
812 width: 100px;
813 position: relative;
814 }
815 .cell_red{
816 font: 12px Arial;
817 text-align: center;
818 color: #FF0000;
819 background-color:#CCCCCC;
820 }
821 .cell_red:hover{
822 color: #FFFFFF;
823 background-color:#444444;
824 }
825 .cell_link{
826 font: 12px Arial;
827 text-align: center;
828 text-decoration: underline;
829 color: #336699;
830 }
831 .cell_link:hover{
832 font: 12px Arial;
833 text-align: center;
834 text-decoration: underline;
835 color: #000000;
836 }
837 .cell_green{
838 font: 12px Arial;
839 text-align: center;
840 color: #666666;
841 background-color:#ccff99;
842 }
843 .cell_green:hover{
844 color: #FFFFFF;
845 background-color:#444444;
846 }
847 .cell_yellow{
848 font: 12px Arial;
849 font-weight: bold;
850 text-align: center;
851 color: #666666;
852 background-color:#ffffcc;
853 }
854 .start-hidden-row{
855 display: none;
856 }
857 .end-hidden-row{
858 display: none;
859 }
860 .suggestion-popup{
861 width: 200px;
862 background-color: #FAFAFA;
863 padding: 10px 10px 10px 10px;
864 z-index: 999999;
865 position: fixed;
866 box-shadow: 1px 1px 1px rgba(0,0,0,0.8), inset 0px 1px 1px rgba(0, 0, 25, .3), inset -1px -1px 2px rgba(0, 0, 25, .3);
867 }
868 .grid_table_header .suggestion-popup .suggestion-container{
869 overflow-y: scroll;
870 overflow-x: hidden;
871 max-height: 100px;
872 }
873 .suggestion-item{
874 display: flex;
875 align-items: center;
876 justify-content: flex-start;
877 border-bottom: 1px dashed black;
878 }
879 .suggestion-item:hover{
880 background-color: #a3a3a3;
881 }
882 .suggestion-title{
883 margin-top: 0px;
884 margin-bottom: 0px;
885 display: flex;
886 align-items: center;
887 justify-content: center;
888 font-weight: bold;
889 }
890 .suggestion-link::before{
891 content: "\\1f517";
892 }
893 `;
894 }else{
895 this.stylesheet.innerHTML = `
896 .hyperlink-form-popup button{
897 user-select: none;
898 }
899 .disabled-mouse{
900 pointer-events: none;
901 }
902 a:hover{
903 cursor: pointer;
904 }
905 .hyperlink-form-hidden{
906 display: none;
907 }
908 .hyperlink-form-popup{
909 display: flex;
910 background-color: #d3d3d3;
911 flex-direction: column;
912 align-items: center;
913 justify-content: center;
914 height: 200px;
915 width: 200px;
916 border: 1px solid black;
917 border-radius: 10% 10% 10% 10%;
918 position: absolute;
919 z-index: 999999;
920 top: 0px;
921 left: 45%;
922 }
923 .zoom-button-container{
924 display: flex;
925 align-items: center;
926 justify-content: space-around;
927 width: 175px;
928 }
929 .zoom-button-container >*:hover{
930 transform: scale(1.05);
931 }
932 .empty-json-container{
933 display: flex;
934 flex-direction: column;
935 width: 100%;
936 align-items: center;
937 justify-content: center;
938 min-height: 300px;
939 }
940 .spinner-container * {
941 margin-left: 10px;
942 margin-right: 10px;
943 }
944 .spinner-container{
945 display: flex;
946 flex-direction: row;
947 align-items: center;
948 justify-content: center;
949 }
950 .load-message{
951 font: 30px Arial;
952 animation: breathe 3s linear infinite;
953 }
954 .spinner{
955 background: #FFFFFF;
956 height: 30px;
957 width: 30px;
958 border-radius: 50%;
959 border: 5px solid #0097d8;
960 border-top: 5px solid #003057;
961 animation: spin 2s linear infinite;
962 }
963 @keyframes spin {
964 0% {
965 transform: rotate(0deg);
966 }
967 100% {
968 transform: rotate(360deg);
969
970 }
971 }
972 @keyframes breathe {
973 0% {
974 opacity: 100%;
975 transform: scale(1.15);
976 }
977 50% {
978 transform: scale(0.95);
979 opacity: 25%;
980 }
981 100% {
982 opacity: 100%;
983 transform: scale(1.15);
984
985 }
986 }
987 /*Rare case of important, we want this to override any background styling*/
988 .blink-field-red{
989 background-color: red!important;
990 animation: blink 2s linear infinite;
991 }
992 @keyframes blink {
993 0% {
994 opacity: 100%;
995 }
996 50% {
997 opacity: 25%;
998 }
999 100% {
1000 opacity: 100%;
1001
1002 }
1003 }
1004 table tr td:has(select.color-my-parent){
1005 background: #ffff00;
1006 }
1007 table tr td:has(input.color-my-parent){
1008 background: #ffff00;
1009 }
1010 .row_number{
1011 color: #FFFFFF;
1012 position: sticky;
1013 left: 0px;
1014 background-color: #888888;
1015 z-index: 9998;
1016 font-size: var(--mod-text-size);
1017 }
1018 .row_number.highlight_error{
1019 color: #888888;
1020 background-color: #ffe0e0;
1021 }
1022 .row_number.highlight_error2{
1023 background-color: #992d28;
1024 }
1025 .row_number.highlight_error3{
1026 background-color: #eb8a49;
1027 }
1028 thead{
1029 position: sticky;
1030 left: 0px;
1031 z-index: 9999;
1032 margin-top: -1px;
1033 }
1034 .super_header_row{
1035 display: flex;
1036 flex-direction: row;
1037 border: solid 1px #dddddd;
1038 position: sticky;
1039 top: 0px;
1040 left: 0px;
1041 z-index: 9999;
1042 }
1043 .super_header{
1044 text-align: left;
1045 border-left-width: 0px;
1046 padding-left: 0px;
1047 padding-right: 0px;
1048
1049 color: #E3E3E3;
1050 background-color: #666666;
1051 height: 35px;
1052 }
1053 .super_header::before{
1054 content: attr(super-header-name);
1055 position: sticky;
1056 left: 15px;
1057 top: 10px;
1058 margin-left: 15px;
1059 font: 15px Arial;
1060 font-weight: normal;
1061 }
1062 .icon_tooltip_container{
1063 display: flex;
1064 flex-direction: row;
1065 align-items: center;
1066 justify-content: flex-start;
1067 }
1068 .icon_container{
1069 display: flex;
1070 flex-direction: row;
1071 align-items: center;
1072 justify-content: center;
1073 flex-wrap: wrap;
1074 margin-right: 10px;
1075 }
1076 .increment-button{
1077 border-radius: 2px;
1078 background-color: #f5f5f5;
1079 padding: 0px 10px 0px 10px;
1080 font: 12px Verdana, Geneva, Arial, Helvetica, sans-serif;
1081 cursor: pointer;
1082 color: #555555;
1083 border: 1px solid #aeaeae;
1084 height: 25px;
1085 }
1086 .increment-button:hover{
1087 transform: scale(1.05);
1088 cursor: pointer;
1089 }
1090 .increment-toggle{
1091 background-color: #ffaaaa;
1092 }
1093 .tooltips{
1094 display: flex;
1095 flex-direction: column;
1096 align-items: flex-start;
1097 justify-content: center;
1098 }
1099 .tooltips p{
1100 margin-top: 0px;
1101 margin-bottom: 0px;
1102 }
1103 .toolbar_container > * {
1104 margin-left: 3px;
1105 margin-right: 3px;
1106 }
1107 .no_data_container{
1108 display: flex;
1109 flex-direction: column;
1110 align-items: center;
1111 justify-content: center;
1112 }
1113 .highlight_section > * {
1114 margin-left: 2px;
1115 margin-right: 2px;
1116 margin-top: 0px;
1117 margin-bottom: 0px;
1118 }
1119 .highlight_section{
1120 display: flex;
1121 align-items: center;
1122 justify-content: space-evenly;
1123 }
1124 .filter_dot_none{
1125 height: 20px;
1126 width: 20px;
1127 display: flex;
1128 align-items: center;
1129 justify-content: center;
1130 padding-bottom: 3px;
1131 font-size: 16px;
1132 background-color: #95cf8c;
1133 border-radius: 30%;
1134 }
1135 .filter_dot_active{
1136 height: 20px;
1137 width: 20px;
1138 display: flex;
1139 align-items: center;
1140 justify-content: center;
1141 padding-bottom: 3px;
1142 font-size: 16px;
1143 background-color: #cf8c8c;
1144 border-radius: 30%;
1145 }
1146 .filter_notification > * {
1147 margin-left: 2px;
1148 margin-right: 2px;
1149 margin-top: 0px;
1150 margin-bottom: 0px;
1151 }
1152 .filter_notification{
1153 display: flex;
1154 align-items: center;
1155 justify-content: space-evenly;
1156 }
1157 .table_container{
1158 display: flex;
1159 flex-direction: column;
1160 overflow-y: auto;
1161 min-height: 20em;
1162 max-height: calc(97lvh - 500px);
1163 overflow-x: scroll;
1164 }
1165 .toolbar_container{
1166 display: flex;
1167 flex-direction: row;
1168 padding-top: 5px;
1169 padding-bottom: 5px;
1170 align-items: center;
1171 justify-content: space-between;
1172 border-bottom: 2px solid #666666;
1173 }
1174 .grid-icon{
1175 max-width: 50px;
1176 }
1177 .grid-icon:hover{
1178 max-width: 50px;
1179 transform: scale(1.1);
1180 cursor: pointer;
1181 }
1182 .active_filter{
1183 background-color: #828267;
1184 }
1185 @keyframes slideOutFromLeft {
1186 0% {
1187 transform: translateX(0);
1188 }
1189 50% {
1190 transform: translateX(50%);
1191 }
1192 100% {
1193 transform: translateX(100%);
1194
1195 }
1196 }
1197 @keyframes slideInFromLeft {
1198 0% {
1199 transform: translateX(-100%);
1200 }
1201 100% {
1202 transform: translateX(0);
1203 }
1204 }
1205 .grid_table_data:active .copy_drag{
1206 display: inline-block;
1207 color: red;
1208 }
1209 table:not(.grid-dragging) .grid_table_data:hover .copy_drag{
1210 display: inline-block;
1211 color: #bad7c0;
1212 }
1213 td.copied_over_wrong{
1214 background-color: #ffaaaa;
1215 }
1216 td.copied_over{
1217 background-color: #bad7c0;
1218 }
1219 td .copy_drag{
1220 font: 28px Arial;
1221 width: 1vw;
1222
1223 position: relative;
1224 vertical-align: middle;
1225 display: none;
1226
1227 }
1228 td .copy_drag_inner{
1229 /*
1230 position: absolute;
1231 right: -1px;
1232 */
1233 }
1234 td .copy_drag_inner:hover{
1235 /*
1236 position: absolute;
1237 right: -1px;
1238 font: 30px Arial;
1239 */
1240 cursor: grab;
1241
1242 }
1243 .confirmation_container{
1244 /* display: flex;
1245 flex-direction: column;
1246 justify-content: space-around;
1247 align-items: center; */
1248 position: absolute;
1249 left: 0%;
1250 top: 0%;
1251 z-index: 99999;
1252 width: 100%;
1253 height: 100%;
1254 background-color: rgba(123, 133, 182, 0.19);
1255 }
1256 .confirmation_container_display{
1257 display: flex;
1258 flex-direction: column;
1259 width: 20vw;
1260 min-height: 10vw;
1261 background-color: #c3c3c3;
1262 padding: 1vw 1vw 1vw 1vw;
1263 border-radius: 1vw;
1264 left: calc(50vw - 10vw);
1265 position: absolute;
1266 }
1267 .confirmation_button_container{
1268 display: flex;
1269 flex-direction: row;
1270 width: 100%;
1271 height: 15%;
1272 justify-content: space-between;
1273 align-items: center;
1274 }
1275 .confirmation_button_container button{
1276 width: 75px;
1277 }
1278 .context-menu{
1279 z-index: 10000;
1280 position: fixed;
1281 background: #c3c3c3;
1282 color: #555;
1283 font-family: sans-serif;
1284 font-size: 11px;
1285 -webkit-transition: opacity .5s ease-in-out;
1286 -moz-transition: opacity .5s ease-in-out;
1287 -ms-transition: opacity .5s ease-in-out;
1288 -o-transition: opacity .5s ease-in-out;
1289 transition: opacity .5s ease-in-out;
1290 -webkit-box-shadow: 2px 2px 2px 0px rgba(143, 144, 145, 1);
1291 -moz-box-shadow: 2px 2px 2px 0px rgba(143, 144, 145, 1);
1292 box-shadow: 2px 2px 2px 0px rgba(143, 144, 145, 1);
1293 padding: 0px;
1294 border: 1px solid #C6C6C6;
1295 }
1296 .context-menu a {
1297 display: block;
1298 color: #555;
1299 text-decoration: none;
1300 padding: 6px 8px 6px 30px;
1301 width: 250px;
1302 position: relative;
1303 }
1304
1305 .context-menu a span {
1306 color: #BCB1B3;
1307 float: right;
1308 }
1309
1310 .context-menu a:hover {
1311 color: #fff;
1312 background: #3879D9;
1313 }
1314
1315 .context-menu hr {
1316 border: 1px solid #EBEBEB;
1317 border-bottom: 0;
1318 }
1319 .grid_table{
1320 border: solid 1px #dddddd;
1321 border-collapse: collapse;
1322 padding: 2px 3px;
1323 text-align: center;
1324 table-layout: fixed;
1325 width: 100%;
1326 }
1327
1328 .grid_table_header{
1329 padding: 5px;
1330 font: 12px Arial;
1331 font-weight: normal;
1332 color: #E3E3E3;
1333 background-color: #666666;
1334 height: 35px;
1335 border: solid 1px #dddddd;
1336 border-collapse: collapse;
1337 text-align: center;
1338 }
1339 .grid_table_header form{
1340 display: flex;
1341 justify-content: flex-end;
1342 }
1343 .grid_table_header form div:not(.grid_menu):not(.ignore_overflow):not(.grid_menu_options):not(.suggestion-container) {
1344 text-overflow: ellipsis;
1345 overflow: hidden;
1346 white-space: nowrap;
1347 }
1348 .excel_table_header{
1349 padding: 5px;
1350 font: 12px Arial;
1351 font-weight: normal;
1352 color: #E3E3E3;
1353 background-color: #217346;
1354 height: 35px;
1355 border: solid 1px #dddddd;
1356 border-collapse: collapse;
1357 text-align: center;
1358 }
1359 .excel_table_header form{
1360 display: flex;
1361 justify-content: flex-end;
1362 }
1363 .excel_table_header form div:not(.grid_menu):not(.ignore_overflow):not(.grid_menu_options) {
1364 text-overflow: ellipsis;
1365 overflow: hidden;
1366 white-space: nowrap;
1367 }
1368 .grid_table_data{
1369
1370 height: inherit;
1371 font: var(--mod-text-size) Arial;
1372 border: solid 1px #dddddd;
1373 border-collapse: collapse;
1374 padding: calc(var(--mod-text-size) / 4);
1375 text-align: center;
1376 }
1377 .grid_table_data .cell-data-wrapper{
1378 text-overflow: ellipsis;
1379 text-wrap: nowrap;
1380 overflow: hidden;
1381 width: 90%;
1382 }
1383 .grid_table_data :is(select,input){
1384 text-overflow: ellipsis;
1385 text-wrap: nowrap;
1386 overflow: hidden;
1387 }
1388 .grid_table_data textarea{
1389 overflow: hidden;
1390 }
1391 .grid_table_data .contentarea{
1392 overflow: hidden;
1393 font-size: var(--mod-text-size);
1394
1395 }
1396 .grid_table_data .contentarea a:visited{
1397 color: #336699;
1398 }
1399 .grid_table_data .contentarea a{
1400 color: #336699;
1401 }
1402 .grid_table_data .contentarea:has(a){
1403 text-decoration: underline;
1404 color: #336699;
1405 -webkit-user-modify: read-write-plaintext-only;
1406 overflow-wrap: break-word;
1407 -webkit-line-break: after-white-space;
1408 cursor: pointer;
1409 }
1410 .grid_table_data > * {
1411 display: inline-block;
1412 margin-left: 5px;
1413 margin-right: 5px;
1414 vertical-align: middle;
1415 }
1416 tr .grid_table_data > *:not(div) {
1417 height: calc(var(--mod-text-size) + 4px);
1418 display: inline-block;
1419 font-size: var(--mod-text-size);
1420 }
1421
1422 .grid_menu{
1423 position: relative;
1424 top: -1px;
1425 left: -4px;
1426 float: left;
1427 padding: 0px 3px 0px 8px;
1428 color: #CCCCCC;
1429 z-index:1;
1430 }
1431 .grid_menu:hover{
1432 color: #FFFFFF;
1433 }
1434 .grid_menu_options{
1435 position:absolute;
1436 width:calc(100% - 10px);
1437 }
1438 .grid_menu_options:hover{
1439 background-color: #D3D3D3;
1440 }
1441 .filter_box{
1442 position: relative;
1443 width: 260px;
1444 height: 220px;
1445 border: 1px solid #888888;
1446 background-color: #FFFFFF;
1447 color: #000000;
1448 padding: 5px;
1449 text-align: left;
1450 align: left;
1451 float: left;
1452 z-index: 3;
1453 }
1454 .pagination_container{
1455 margin-left: 5px;
1456 margin-top: 5px;
1457 margin-bottom: 5px;
1458 border-top: 2px solid #666666;
1459 }
1460 .pagination_area{
1461 position: relative;
1462 float: left;
1463 font: 14px Arial;
1464 color: #666666;
1465 }
1466 .pagination_links{
1467 position: relative;
1468 top: 2px;
1469 font: 14px Arial;
1470 color: #666666;
1471 text-decoration:none;
1472 margin-left: 2px;
1473 margin-right: 2px;
1474 }
1475
1476 .pagination_block{
1477 position: relative;
1478 top : 2px;
1479 border: 1px solid #666666;
1480 padding: 0px 5px 0px 5px;
1481 }
1482
1483 .pagination_selected_link{
1484 font-weight: bold;
1485 font: 20px Arial;
1486 color: #444444;
1487 /* text-decoration: underline; */
1488 }
1489
1490 .pagination_page_number_textbox{
1491 position: relative;
1492 width: 60px;
1493 }
1494
1495 .max_results_per_page{
1496 position: relative;
1497 float: left;
1498 top: 5px;
1499 left: 5px;
1500 font: 14px Arial;
1501 color: #666666;
1502 }
1503
1504 .round{
1505 -webkit-border-radius: 3px;
1506 -moz-border-radius: 3px;
1507 border-radius: 3px;
1508 }
1509
1510 .grid_data_cells{
1511 height:18px;
1512 position:relative;
1513 font: 12px Arial;
1514 font-weight: normal;
1515 text-align:left;
1516 padding-left: 5px;
1517 }
1518
1519 .grid_standard_td{
1520 color: #999999;
1521 background-color:#FFFFFF;
1522 }
1523 .table th {
1524 position: relative;
1525 }
1526 .resizer {
1527 position: absolute;
1528 top: 0;
1529 right: 0;
1530 width: 5px;
1531 cursor: col-resize;
1532 user-select: none;
1533 height: 100%;
1534 }
1535 .resizer:hover,
1536
1537 .resizing {
1538 border-right: 2px solid #333333;
1539 }
1540
1541 .resizable {
1542 border: 1px solid gray;
1543 height: 100px;
1544 width: 100px;
1545 position: relative;
1546 }
1547 .cell_red{
1548 font: 12px Arial;
1549 text-align: center;
1550 color: #FF0000;
1551 background-color:#CCCCCC;
1552 }
1553 .cell_red:hover{
1554 color: #FFFFFF;
1555 background-color:#444444;
1556 }
1557 .cell_link{
1558 font: 12px Arial;
1559 text-align: center;
1560 text-decoration: underline;
1561 color: #336699;
1562 }
1563 .cell_link:hover{
1564 font: 12px Arial;
1565 text-align: center;
1566 text-decoration: underline;
1567 color: #000000;
1568 }
1569 .cell_green{
1570 font: 12px Arial;
1571 text-align: center;
1572 color: #666666;
1573 background-color:#ccff99;
1574 }
1575 .cell_green:hover{
1576 color: #FFFFFF;
1577 background-color:#444444;
1578 }
1579 .cell_yellow{
1580 font: 12px Arial;
1581 font-weight: bold;
1582 text-align: center;
1583 color: #666666;
1584 background-color:#ffffcc;
1585 }
1586 .start-hidden-row{
1587 display: none;
1588 }
1589 .end-hidden-row{
1590 display: none;
1591 }
1592 .suggestion-popup{
1593 width: 200px;
1594 background-color: #FAFAFA;
1595 padding: 10px 10px 10px 10px;
1596 z-index: 999999;
1597 position: fixed;
1598 box-shadow: 1px 1px 1px rgba(0,0,0,0.8), inset 0px 1px 1px rgba(0, 0, 25, .3), inset -1px -1px 2px rgba(0, 0, 25, .3);
1599 }
1600 .grid_table_header .suggestion-popup .suggestion-container{
1601 overflow-y: scroll;
1602 overflow-x: hidden;
1603 max-height: 100px;
1604 }
1605 .suggestion-item{
1606 display: flex;
1607 align-items: center;
1608 justify-content: flex-start;
1609 border-bottom: 1px dashed black;
1610 }
1611 .suggestion-item:hover{
1612 background-color: #a3a3a3;
1613 }
1614 .suggestion-title{
1615 margin-top: 0px;
1616 margin-bottom: 0px;
1617 display: flex;
1618 align-items: center;
1619 justify-content: center;
1620 font-weight: bold;
1621 }
1622 .suggestion-link::before{
1623 content: "\\1f517";
1624 }
1625 `;
1626 }
1627
1628 this.stylesheet.innerHTML += this.stylesheet_injection;
1629 // Inserting the styles into the component.
1630 this.shadowRoot.appendChild(this.stylesheet);
1631
1632 this.grid_name = this.getAttribute("name");
1633 this.zoom_setting = this.reset_zoom_setting;
1634 //First thing to do is set the ID of the grid component itself.
1635 this.setAttribute('id', `grid-${this.grid_name}`);
1636
1637 // Next create and insert the spinner
1638 this.classList.add('shawn-grid-spinner');
1639 let load_spinner_wrapper = document.createElement('div');
1640 let load_spinner = document.createElement('div');
1641 let load_message = document.createElement('div');
1642
1643 load_spinner_wrapper.setAttribute('id', `grid-${this.grid_name}-spinner`);
1644 load_spinner_wrapper.classList.add('spinner-container');
1645 load_spinner.classList.add(`spinner`);
1646 load_message.classList.add(`load-message`);
1647 load_message.innerHTML = "Loading...";
1648
1649
1650 load_spinner_wrapper.appendChild(load_spinner);
1651 load_spinner_wrapper.appendChild(load_message);
1652 this.shadowRoot.appendChild(load_spinner_wrapper);
1653
1654 // Slowly being deprecated
1655 this.grid_settings = [
1656 //How many results to show per page, this populates the dropdown below the grid
1657 {id:`grid-${this.grid_name}`, show_result_options:"20,50,100"},
1658 //Max results to show per page, this is responsible for pagination links
1659 {id:`grid-${this.grid_name}`, max_results:this.max_results},
1660 //Max pagination links to display on a page, this prevents you from having hundreds of pagination links of there are too many results returned
1661 {id:`grid-${this.grid_name}`, max_pagination_links:"10"},
1662 ]
1663 //Starting point of creating the grid.
1664 //Async call to get the loaded filters, resolve the promise, and then another async fetch call to get the actual data.
1665
1666 try{
1667 //retrieve the filters through the load_global_json function, then resolve the Fetch promise with .then()
1668 //inside the .then(){}, we process the filters, then load the table display data using the same process
1669 //finishing with calling create table now that we have all the data loaded.
1670 // THIS REPLACED THE AJAX/DIV-STORAGE METHOD.
1671 if(this.load_filters_url){
1672 this.load_global_json(this.load_filters_url).then(jsonData =>{
1673 // Escape the loaded json, making sure new line, carriage return, tab, etc are properly escaped in the json.
1674 let filtered_filters = this.escape_json(jsonData);
1675 this.GLOBAL_LOADED_FILTERS = filtered_filters;
1676
1677 this.grid_filters = this.GLOBAL_LOADED_FILTERS;
1678 this.load_global_json(this.data_url).then(jsonData =>{
1679 let filtered_data = this.escape_json(jsonData);
1680
1681 this.GLOBAL_JSON_RESULT = filtered_data;
1682 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
1683 if(this.load_target_page > 0){
1684 this.create_table(`grid-${this.grid_name}`, '', this.load_target_page);
1685 }else{
1686 this.create_table(`grid-${this.grid_name}`, '');
1687 }
1688
1689 });
1690 });
1691 }else{
1692 this.load_global_json(this.data_url).then(jsonData =>{
1693 let filtered_data = this.escape_json(jsonData);
1694 this.GLOBAL_JSON_RESULT = filtered_data;
1695 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
1696 this.create_table(`grid-${this.grid_name}`, '');
1697
1698 });
1699 }
1700 }
1701 catch(error){
1702 console.log(error);
1703 }
1704
1705 }
1706 // Currently not serving a useful function.
1707 disconnectedCallback(){
1708 console.log(this.default_styles);
1709 console.log(this.default_widths);
1710 console.log(this.grid_filters);
1711 }
1712 //Get a list of all filters to hide, then hide them
1713 hide_filter_divs(grid_id){
1714 let grid_component;
1715 if(this.nodeName != 'SHAWN-GRID'){
1716 grid_component = this.getRootNode();
1717 }else{
1718 grid_component = this;
1719 }
1720
1721 let elms = grid_component.shadowRoot.querySelectorAll(".filter_div");
1722 for(var i = 0; i < elms.length; i++){
1723 elms[i].style.display = 'none';
1724 }
1725 }
1726 create_table(grid_id, column_name, start_results_at){
1727
1728 //Reset ANY data changes
1729
1730 this.modified_data = {};
1731 // Check Data verifies if data came in or not, if no data comes back it cleans up any existing table elements, creates
1732 // a new element to display that there is no data, and returns true,
1733 // which should then not generate a table, so return 0.
1734 if(this.check_data()){
1735 return 0;
1736 }
1737 //Sets the values for our grid settings, such as max results, how many pagination links to show, how many rows to show, etc.
1738 this.set_grid_settings();
1739 // Setting the pagination to the correct page, default is 1
1740 let current_page = 1;
1741 //Grab the scroll offset of the existing table if it does exist.
1742 let scroll_offset_x = 0;
1743 let scroll_offset_y = 0;
1744
1745 // If a previous grid existed, scroll to and refocus the cell they were previously working on.
1746 let previous_grid_focus_array = this.get_previous_grid_scroll_focus();
1747 if(previous_grid_focus_array){
1748 current_page = previous_grid_focus_array[0];
1749 scroll_offset_x = previous_grid_focus_array[1];
1750 scroll_offset_y = previous_grid_focus_array[2];
1751 }
1752
1753 // Check for distinct values in JSON results
1754 // let distinct_values = {};
1755 // for(let j = 0; j < this.COLUMN_NAME_ORDER.length; j++){
1756 // // Extract all values based on the key name from column name order, build them into an array, then turn them into a set (guarantees uniques)
1757 // let distinct_col_values = [...new Set(this.GLOBAL_JSON_RESULT.map(item => item[this.COLUMN_NAME_ORDER[j]]))];
1758 // distinct_values[this.COLUMN_NAME_ORDER[j]] = distinct_col_values;
1759 // }
1760 // console.log(distinct_values);
1761
1762
1763 // Call the filter_json function and get back the filtered json_data to use in the rest of this create_table function that we are in
1764 // Also saves the filtered data into the component variable GLOBAL_JSON_RESULT_FILTERED
1765 let json_data = this.filter_json(grid_id, '', '', '', 'do_not_call_create_table');
1766
1767 // Primary key is required for more advanced functionality.
1768 // console.log(json_data);
1769 if(json_data.length > 0){
1770 if(!json_data[0][this.primary_key]){
1771 console.log("Primary Key Set Incorrectly")
1772 }
1773 }
1774
1775 // Check Filtered Data checks the dataset after filtering it, if there's no data remaining, creates a message to explain that filters
1776 // are preventing data from being displayed.
1777 if(this.check_filtered_data(json_data)){
1778 return 0;
1779 }
1780 //--------------------------------------------------------------------------------
1781 if(!start_results_at){
1782 start_results_at = 1;
1783 }
1784
1785 //Create the dynamic table
1786 let table = document.createElement("table");
1787
1788 //Add the table header classname
1789 table.className = "grid_table table grid_tr_alternating";
1790 //Set ID attribute on element
1791 table.setAttribute('id', grid_id+'_table');
1792 table.style.setProperty("--mod-text-size", `${this.zoom_setting}px`)
1793
1794 //Add the THEAD for the resizing code to work
1795 let thead = table.createTHead();
1796 if(this.configuration["super_header_information"]){
1797 thead.style.top = "35px";
1798 }else{
1799 thead.style.top = "0px";
1800 }
1801 //Create the table header rows using the extracted header values from above
1802 let tr = thead.insertRow(-1); //Table row
1803
1804 //Row Number Header Implementation
1805 let th_rowcount = document.createElement("th");
1806 th_rowcount.innerHTML = "#";
1807 th_rowcount.style.width = '25px';
1808 th_rowcount.classList.add("grid_table_header");
1809 tr.prepend(th_rowcount);
1810 //End Row Number Header Implementation
1811 for (let i = 0; i < this.COLUMN_NAME_ORDER.length; i++){
1812 // Extract all column values using the map function, then convert the result to a set to obtain the distinct values
1813 // let distinct_col_values = [...new Set(this.GLOBAL_JSON_RESULT.map(item => item[this.COLUMN_NAME_ORDER[i]]))];
1814 // if(this.id == 'grid-az-1' && distinct_col_values.length <= 10){
1815 // console.log(`Valid Column: ${this.COLUMN_NAME_ORDER[i]}`)
1816 // for(let j = 0; j < distinct_col_values.length; j++){
1817 // if(distinct_col_values[j]){
1818 // console.log(distinct_col_values[j]);
1819 // }
1820 // }
1821 // }
1822 let th = document.createElement("th");//Table header
1823 th.title = this.COLUMN_NAME_ORDER[i];
1824 // IF-ELSE is checking for if you provided column renames or not.
1825 let active_filter = '';
1826 if(this.grid_filter_columns.includes(this.COLUMN_NAME_ORDER[i])){
1827 active_filter = 'active_filter';
1828 }
1829
1830 //Determine the column name
1831 let col_name = '';
1832 if(this.configuration["column_information"][this.COLUMN_NAME_ORDER[i]]["rename"]){
1833 col_name = this.configuration["column_information"][this.COLUMN_NAME_ORDER[i]]["rename"];
1834 }else{
1835 col_name = this.COLUMN_NAME_ORDER[i];
1836 }
1837
1838 // Creating a custom filter box if there was a custom_filter name passed in from the config
1839 let custom_filter = '';
1840 custom_filter = this.column_information[this.COLUMN_NAME_ORDER[i]]["custom_filter"];
1841 let filter_menu = '';
1842 filter_menu = this.column_information[this.COLUMN_NAME_ORDER[i]]["filter_menu"];
1843 if(filter_menu == false){
1844 th.innerHTML = `
1845 ${col_name}
1846 `;
1847 }else if(custom_filter){
1848 th.innerHTML = `
1849 <form>
1850 <div style="position:relative;width:100%;" onClick="sort_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', 'asc', ${start_results_at});">${col_name}</div>
1851 <div class="grid_menu excel_menu ${active_filter}"
1852 onClick="
1853 window.gridReference.get('${this.id}').hide_filter_divs('${this.id}');
1854 const sub_form = window.gridReference.get('${this.id}').shadowRoot.getElementById('${custom_filter}');
1855 const table_container = window.gridReference.get('${this.id}').shadowRoot.getElementById('${grid_id}_container');
1856// sub_form.style.display='block';
1857 this.parentElement.style.justifyContent = '';
1858 let sub_form_x = sub_form.getBoundingClientRect()['x'];
1859 let table_container_x = table_container.getBoundingClientRect()['x'];
1860 if(sub_form_x < table_container_x){
1861 this.parentElement.style.justifyContent = 'flex-start';
1862 }else{
1863 this.parentElement.style.justifyContent = 'flex-end';
1864 }
1865
1866 if(window.gridReference.get('${this.id}').shadowRoot.getElementById('${custom_filter}').style.display == 'none'){
1867 window.gridReference.get('${this.id}').shadowRoot.getElementById('${custom_filter}').style.display = 'block';
1868 }else{
1869 window.gridReference.get('${this.id}').shadowRoot.getElementById('${custom_filter}').style.display = 'none';
1870 }
1871 "
1872 >&#9776; </div>
1873 </form>
1874
1875
1876 ${this.column_information[this.COLUMN_NAME_ORDER[i]]["custom_filter_code"]}
1877 `;
1878 }else{
1879 th.innerHTML = `
1880 <form>
1881 <div style="position:relative;width:100%;" onClick="sort_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', 'asc', ${start_results_at});">${col_name}</div>
1882 <div class="grid_menu excel_menu ${active_filter}" onClick="
1883 let prev_status = window.gridReference.get('${this.id}').shadowRoot.getElementById('grid_element_${grid_id}_${i}').style.display;
1884 window.gridReference.get('${this.id}').hide_filter_divs('${this.id}');
1885 if(prev_status == 'none'){
1886 console.log('NONE');
1887 //document.getElementById(grid_element_${grid_id}_${i}).style.display = 'block';
1888 window.gridReference.get('${this.id}').shadowRoot.getElementById('grid_element_${grid_id}_${i}').style.display = 'block';
1889 }else{
1890 //document.getElementById(grid_element_${grid_id}_${i}).style.display = 'none';
1891 window.gridReference.get('${this.id}').shadowRoot.getElementById('grid_element_${grid_id}_${i}').style.display = 'none';
1892 console.log('BLOCK');
1893 }
1894 ">
1895 <div>&#9776;</div>
1896 </div>
1897 <div class="ignore_overflow filter_div" id="grid_element_${grid_id}_${i}" style="position:absolute;float:left;top:30px;left:90px;display:none;z-index:12000;">
1898 <div class="ignore_overflow" style="position:relative;float:left;margin:6px 5px 10px 5px;text-align:left;" class="page_font">
1899
1900 <div class="ignore_overflow" style="position:relative;width:320px;height:220px;float:left;left:-5px;top:-2px;padding:5px 5px 5px 5px;background-color:#D3D3D3;border:1px solid #666666;color:#444444;">
1901
1902 <div class="ignore_overflow" style="position:relative;float:left;top:3px;">
1903
1904 <div class="grid_menu_options ignore_overflow" style="position:relative;float:left;" onclick="window.gridReference.get('${this.id}').sort_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', 'asc', ${start_results_at});">
1905 <div style="position:absolute;transform: rotate(-90deg);left:2px;top:-2px;">&gt;</div>
1906 <i style="position:relative;padding: 0px 0px 0px 13px;left:3px;">Sort Ascending</i>
1907 </div>
1908
1909 <div class="grid_menu_options ignore_overflow" style="position:relative;float:left;top:-14px;left:140px;" onclick="window.gridReference.get('${this.id}').sort_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', 'desc', ${start_results_at});">
1910 <div style="position:absolute;transform: rotate(90deg);left:162px;top:-2px;">&gt;</div>
1911 <i style="position:relative;padding: 0px 0px 0px 13px;left:56px;">Sort Descending</i>
1912 </div>
1913
1914 </div>
1915
1916 <br style="clear:both;">
1917
1918 <div class="ignore_overflow" style="position:relative;float:left;top:12px;">
1919 <div class="grid_menu_options grid_menu" style="position:relative;float:left;color:#444444;">Where
1920 <select name="${grid_id}_col${i}_filter_type" id="${grid_id}_col${i}_filter_type" style="width:80px;" onKeyPress="if(event.keyCode === 13){window.gridReference.get('${this.id}').filter_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', '${grid_id}_col${i}_filter_type', '${grid_id}_col${i}_filter_value');}">
1921 <option value="like">contains</option>
1922 <option value="=">=</option>
1923 <option value="!=">!=</option>
1924 <option value="<">&lt;</option>
1925 <option value=">">&gt;</option>
1926 <option value="<=">&lt;=</option>
1927 <option value=">=">&gt;=</option>
1928 <option value="!like">does not contain</option>
1929 </select>
1930 <input type="text" name="${grid_id}_col${i}_filter_value" id="${grid_id}_col${i}_filter_value" value="" style="width:170px;" autocomplete="off" onfocus="
1931 let grid = this.getRootNode().host; grid.get_distinct_filter_values(event, this);
1932 " onkeypress="
1933 if(event.keyCode === 13){window.gridReference.get('${this.id}').filter_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', '${grid_id}_col${i}_filter_type', '${grid_id}_col${i}_filter_value');}
1934 " onkeyup="
1935 let grid = this.getRootNode().host;
1936 grid.get_distinct_filter_values(event, this, this.value);
1937 ">
1938 <br>
1939 </div>
1940 </div>
1941
1942
1943
1944 <div class="ignore_overflow" style="position:relative;float:left;top:-6px;">
1945 <div class="grid_menu_options" style="position:relative;top:30px;left:90px;width:300px;">
1946 <input type="button" onclick="window.gridReference.get('${this.id}').filter_json('${grid_id}', '${this.COLUMN_NAME_ORDER[i]}', '${grid_id}_col${i}_filter_type', '${grid_id}_col${i}_filter_value');" value="Apply this filter"> &nbsp;
1947 <input type="button" onclick="window.gridReference.get('${this.id}').clear_filters('${grid_id}');" value="Clear all filters">
1948 </div>
1949 <hr style="position:relative;top:35px;">
1950 <br style="clear:both;">
1951 <div style="position:relative;top:24px;"><b>ACTIVE FILTERS</b><br>${this.grid_filter_list}</div><br>
1952 </div>
1953
1954 </div>
1955
1956 </div>
1957 </div>
1958 </form>
1959 `;
1960 }
1961 tr.appendChild(th);
1962 //Add the table header classname
1963 if(this.excel_edit){
1964 th.className = "excel_table_header";
1965 }else{
1966 th.className = "grid_table_header";
1967 }
1968 //--------------------------------------------------------------------------------
1969 /*Set the default widths of the table headers*/
1970 //Reload any table widths that the user changed/resized --------------------------
1971 for (let indx = 0; indx < this.COLUMN_NAME_ORDER.length; indx++){
1972 if ((this.COLUMN_NAME_ORDER[indx] == this.COLUMN_NAME_ORDER[i]) && (this.configuration["column_information"][this.COLUMN_NAME_ORDER[indx]]["width"])){
1973 th.style.width = this.configuration["column_information"][this.COLUMN_NAME_ORDER[indx]]["width"];
1974 }
1975 }
1976 /*Any user modified table header widths will be applied here*/
1977 for (let gw of this.grid_widths) {
1978 //If the id matches then this is the correct grid and column so we need to update the width
1979 if(gw.id == `${grid_id}_column_${i}`){
1980 //Grid and column match found, override the TH width
1981 th.style.width = gw.width;
1982 }
1983 }
1984 }
1985
1986 //Add the TBODY for the resizing code to work
1987 let tbody = table.createTBody();
1988 let start_results_at_record = ((start_results_at - 1) * this.max_results);
1989
1990 // Taking the styles provided by the config and converting them to a custom class and applying them to the element
1991 // This prevents in-line styles from tampering with class styles.
1992 this.COLUMN_NAME_ORDER.forEach((key) => {
1993 let default_formatting = this.column_information[key]["style"];
1994 let class_name = `${key}_config_styles`;
1995 let new_class = ``;
1996 if(default_formatting.match(/^style\:/i)){
1997 //Remove the command link: from the data
1998 default_formatting = default_formatting.replace(/(style\:\s{0,})/i, "");
1999 new_class += `.${class_name} {
2000 ${default_formatting}
2001 }`;
2002 this.stylesheet.innerHTML += new_class;
2003 }
2004 });
2005 // Creating the initial rows and populating them
2006 this.create_rows(start_results_at_record, json_data, tbody, this.max_results, "append");
2007
2008 // Make sure there is no existing table elements from a previous load.
2009 this.delete_existing_table();
2010
2011 //Populate the new table into the div for display
2012 let grid_container = this.shadowRoot;
2013 // Create the container to insert the table into
2014 let grid_wrapper = document.createElement("div");
2015 grid_wrapper.id = `${this.id}_container`;
2016 grid_wrapper.classList.add('table_container');
2017
2018 grid_wrapper.appendChild(table);
2019 grid_container.appendChild(grid_wrapper);
2020 this.create_pagination_area(this.id, json_data, start_results_at);
2021
2022 //Set the width of the bottom row below the dynamically created table so that the pagination links and the show results dropdown are the same width as the dynamically created table
2023
2024 // Remove the loading spinner, table should be visible by now.
2025 this.classList.remove('shawn-grid-spinner');
2026 if(this.shadowRoot.getElementById(`${this.id}-spinner`)){
2027 this.shadowRoot.getElementById(`${this.id}-spinner`).remove();
2028 }
2029 //Attaching event listeners to the row elements.
2030 //This has to happen last, otherwise while appending to innerHTML, we reparse the html and lose the event listeners.
2031 //This was only working previously with setting the onclick or oncontext menu's with setAttribute, this way
2032 //It will work with .onclick .oncontextmenu, or adding an event listener.
2033 this.add_additional_features(grid_id);
2034 this.load_resizing_code(`grid-${this.grid_name}`);
2035
2036 // Rescrolling to where the grid was previously scrolled to
2037 if(start_results_at == current_page){
2038 this.shadowRoot.querySelector('.table_container').scrollLeft = scroll_offset_x;
2039 this.shadowRoot.querySelector('.table_container').scrollTop = scroll_offset_y;
2040 }
2041
2042 // This is to refocus on the last focused cell/input element. It is jarring if a reload happens midfocus.
2043 if(this.LAST_ACTIVE_ELEMENT_ID){
2044 let last_active_element = this.shadowRoot.getElementById(this.LAST_ACTIVE_ELEMENT_ID);
2045 if(last_active_element){
2046 let last_active_tag = last_active_element.tagName;
2047 last_active_element.focus();
2048 if(last_active_tag == "INPUT" || last_active_tag == "TEXTAREA"){
2049 let set_selection_range = true;
2050 if(last_active_tag == "INPUT"){
2051 let input_type = last_active_element.type;
2052 if(input_type == 'checkbox'){
2053 set_selection_range = false;
2054 }
2055 }
2056 if(set_selection_range){
2057 last_active_element.setSelectionRange(this.LAST_ACTIVE_ELEMENT_CURSOR_POSITION, this.LAST_ACTIVE_ELEMENT_CURSOR_POSITION);
2058 }
2059 }
2060 }
2061 this.LAST_ACTIVE_ELEMENT_CURSOR_POSITION = 0;
2062 this.LAST_ACTIVE_ELEMENT_ID = '';
2063 }
2064 document.body.style.cursor = 'default';
2065 // const final_table_container = this.shadowRoot.querySelector(`div#${this.id}_container`);
2066 // Scroll Handler Feature, has not been moved to the add additional features section yet.
2067 // TODO:
2068 // final_table_container.addEventListener("scrollend", this.scroll_handler);
2069 // final_table_container.addEventListener("wheel", this.scroll_handler);
2070
2071 console.log("Table Created");
2072
2073 }
2074 create_pagination_area(grid_id, json_data, start_results_at){
2075 //PAGINATION CODE ///////////////////////////////////////////////////////////////////////////////////////////
2076 //Create the pagination links
2077 let grid_container = window.gridReference.get(grid_id).shadowRoot;
2078 let number_of_pagination_links = Math.ceil(json_data.length / this.max_results);
2079
2080 //Start the bottom row div, must put the HTML into a variable because pushing it straight into innerHTML will cause the browser to close all of my tags before I get a chance to close them first
2081 let bottom_div_html = '';
2082 bottom_div_html += `<div id="${grid_id}_bottom_row" class="pagination_container">`;
2083
2084 //Text for the patination links
2085 bottom_div_html += `<div class="pagination_area">Page: `;
2086
2087
2088 if(number_of_pagination_links > 0){
2089 //--------------------------------------------------------------------------------
2090 //Get the starting pagination link number ----------------------------------------
2091 let modified_start_at = start_results_at;
2092
2093 //Decide if we draw the left pagniation page number links
2094 if( start_results_at > (Number(this.max_pagination_links) / 2)){
2095 modified_start_at = (start_results_at - (Number(this.max_pagination_links) / 2));
2096 }else if(start_results_at <= (Number(this.max_pagination_links) / 2)){
2097 modified_start_at = (start_results_at - (Number(this.max_pagination_links) / 2));
2098 if(modified_start_at < 1){
2099 modified_start_at = 1;
2100 }
2101 }
2102 //--------------------------------------------------------------------------------
2103 //Get the ending pagination link number ------------------------------------------
2104 let stop_pagination_links_at = (Number(this.max_pagination_links) + Number(start_results_at));
2105
2106 //Decide if we draw the right pagination page number links
2107 if( start_results_at > (Number(this.max_pagination_links) / 2)){
2108 stop_pagination_links_at = ((Number(this.max_pagination_links) / 2) + Number(start_results_at));
2109 }else if(start_results_at <= (Number(this.max_pagination_links) / 2)){
2110 stop_pagination_links_at = ((Number(this.max_pagination_links) / 2) + Number(start_results_at));
2111 stop_pagination_links_at = (stop_pagination_links_at - (start_results_at - (Number(this.max_pagination_links) / 2)) );
2112 stop_pagination_links_at++;
2113 }
2114
2115 //Add buttons that allow you to go prior the pagination links that you are currently able to view
2116 if(modified_start_at > 1){
2117 //Only show these links if we aren't on the last page already
2118
2119 //Get the next page to display for the arrows in the pagination section
2120 let previous_page = (Number(start_results_at) - 1);
2121
2122 let start_pagination_at = (start_results_at + number_of_pagination_links);
2123
2124 //Last page of results
2125 bottom_div_html += `<a href="javascript:document.getElementById('${this.id}').create_table('${grid_id}', '', 1);" class="pagination_links pagination_block" title="Go to the first page: 1">&#10072;&#10092;&#10092;</a>`;
2126 //
2127 //Add one to move one pagination link past the currently displayed one
2128 bottom_div_html += `<a href="javascript:document.getElementById('${this.id}').create_table('${grid_id}', '', ${previous_page});" class="pagination_links pagination_block" title="Go to the previous page: ${previous_page}">&#10092;</a>`;
2129 }
2130 //--------------------------------------------------------------------------------
2131 //--------------------------------------------------------------------------------
2132
2133 for (let i=modified_start_at; i<=stop_pagination_links_at; i++){
2134
2135 //--------------------------------------------------------------------------------
2136 //Add buttons that allow you to go past the pagination links that you are currently able to view
2137 if(i > (stop_pagination_links_at - 1)){
2138 //Only show these links if we aren't on the last page already
2139
2140 //Get the next page to display for the arrows in the pagination section
2141 let next_page = (Number(start_results_at) + 1);
2142
2143 let start_pagination_at = (start_results_at + number_of_pagination_links);
2144
2145 if(start_results_at < number_of_pagination_links){
2146 //Add one to move one pagination link past the currently displayed one
2147 bottom_div_html += `<a href="javascript:document.getElementById('${this.id}').create_table('${grid_id}', '', ${next_page});" class="pagination_links pagination_block" title="Go to the next page: ${next_page}">&#10093;</a>`;
2148 //Last page of results
2149 bottom_div_html += `<a href="javascript:document.getElementById('${this.id}').create_table('${grid_id}', '', ${number_of_pagination_links});" class="pagination_links pagination_block" title="Go to the last page: ${number_of_pagination_links}">&#10093;&#10093;&#10072;</a>`;
2150 }
2151
2152 //We have hit the maximum number of pagination links to display for this grid so stop building them
2153 break;
2154 }
2155 //--------------------------------------------------------------------------------
2156 //--------------------------------------------------------------------------------
2157
2158
2159 //--------------------------------------------------------------------------------
2160 //Create the normal pagination links ---------------------------------------------
2161 //Subtract one for the page to show as we will multiply this times the total results per page to find out which records to display
2162 if(start_results_at == i){
2163 //Current selected page found, highlight the pagination number
2164 bottom_div_html += `<a href="javascript:document.getElementById('${this.id}').create_table('${grid_id}', '', ${i});" id="pagination_page_${i}" class="pagination_links pagination_selected_link" title="Go to page: ${i}">${i}</a>`;
2165 }else{
2166 //Standard pagination link, not the currenlty selected one
2167 bottom_div_html += `<a href="javascript:document.getElementById('${this.id}').create_table('${grid_id}', '', ${i});" id="pagination_page_${i}" class="pagination_links" title="Go to page: ${i}">${i}</a>`;
2168 }
2169
2170 //Don't draw more pagination links then we have pages for
2171 if(i >= number_of_pagination_links){
2172 break;
2173 }
2174 //--------------------------------------------------------------------------------
2175 //--------------------------------------------------------------------------------
2176
2177 }
2178 // let i = test
2179 //--------------------------------------------------------------------------------
2180 //Provide a textbox for letting the user type in a specific page -----------------
2181 bottom_div_html += `Page: <input type="text" id="${grid_id}_typein_page_number" name="" value="${start_results_at}" onKeyUp="if(event.keyCode === 13 && this.value && this.value > 0 && this.value <= ${number_of_pagination_links}){window.gridReference.get('${this.id}').create_table('${grid_id}', '', window.gridReference.get('${this.id}').shadowRoot.querySelector('#${grid_id}_typein_page_number').value);}" class="pagination_page_number_textbox">`;
2182 //--------------------------------------------------------------------------------
2183 //--------------------------------------------------------------------------------
2184 }
2185
2186 bottom_div_html += '</div>'; //Close the div we opened to go around the pagination section
2187 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
2188
2189
2190 //SHOW NUMBER OF RESULTS PER PAGE ///////////////////////////////////////////////////////////////////////////
2191 //Check which option should be selected
2192 // Deprecated to always show 50 rows by default, we have infinite scrolling and page hopping.
2193 bottom_div_html += `<div class="max_results_per_page">Results per page: <span id="${grid_id}_max_results">${this.max_results}</span></div>`;
2194 /////////////////////////////////////////////////////////////////////////////////////////////////////////////
2195
2196 //Close the bottom row div
2197 bottom_div_html += '</div>';
2198 let table_container = grid_container.getElementById(`${grid_id}_container`);
2199 table_container.insertAdjacentHTML("afterend", bottom_div_html);
2200 // grid_container.innerHTML += bottom_div_html;
2201 }
2202 create_rows(start_results_at_record, json_data, tbody, insert_amount, prepend_append){
2203 let grid_id = this.id;
2204 let row_id = start_results_at_record;
2205 let current_record_number = 0;
2206 let insert_direction = -1;
2207 json_data = this.GLOBAL_JSON_RESULT_FILTERED;
2208 if(prepend_append == "prepend"){
2209 insert_direction = 0;
2210 }
2211 // This for loop needs I to increment or decrement, depending on if we're prepending or appending rows
2212 // This has necessitated a weird loop control, where we just set I = the first record we're adding
2213 // to the table, then based off of variable prepend_append, we check conditionals, json_data.length
2214 // if we're appending rows (scrolling down), or comparing I to 0 (scrolling up).
2215 // At the very end of the loop, you will see logic to increment or decrement I.
2216 // and at the start of the loop, that's the conditional to break out of the loop.
2217 // As well as some logic inside the loop structure has if-else controls based on prepend_append.
2218 for (let i=start_results_at_record; ;){
2219 if(prepend_append == "append"){
2220 if(i >= json_data.length){
2221 break;
2222 }
2223 }else{
2224 if(i < 0){
2225 break;
2226 }
2227 }
2228 let tr = tbody.insertRow(insert_direction);
2229 tr.setAttribute("id", `row${row_id}`)
2230 if(prepend_append == "append"){
2231 row_id++;
2232 }else{
2233 row_id--;
2234 }
2235 // Row Number Body Implementation
2236
2237 let row_number = tr.insertCell(-1);
2238 let unfiltered_row_index = -1;
2239 for(let k = 0; k < this.GLOBAL_JSON_RESULT.length; k++){
2240 let result_string = JSON.stringify(this.GLOBAL_JSON_RESULT[k]);
2241 let json_data_string = JSON.stringify(json_data[i]);
2242 if(result_string == json_data_string){
2243 unfiltered_row_index = k;
2244 break;
2245 }
2246 }
2247 row_number.innerHTML = unfiltered_row_index + 1;
2248 row_number.classList.add("row_number");
2249 // End Row Number Body Implementation
2250 //Setting row ID's to remove/add back in rows;
2251
2252 for (let j=0; j < this.COLUMN_NAME_ORDER.length; j++){
2253 // Testing Code
2254
2255 // End Testing Code
2256 let cell = tr.insertCell(-1);
2257 // Potentially setting the title of the row to the ID of the grid will allow the in-JSON
2258 // html to access the ID of the grid, by referring to its direct parents title. Potentially a way to allow multiples dynamically?
2259 cell.setAttribute("title", `${this.id}`);
2260 // Set the headers attribute so the columns know which header they belong to. This helps in the undo, delete, and copy functions.
2261 cell.setAttribute("headers", this.COLUMN_NAME_ORDER[j]);
2262
2263 // A-Z HARDCODED
2264 const urlParams = new URLSearchParams(window.location.search);
2265 const project_id = urlParams.get(this.url_identifier);
2266 //I is the row index, J is the column index
2267 cell.setAttribute("id", `${i},${j},${project_id},${json_data[i][this.primary_key]},${this.COLUMN_NAME_ORDER[j]}`)
2268 // END A-Z HARDCODED
2269 let td_data;
2270 try{
2271 td_data = json_data[i][`${this.COLUMN_NAME_ORDER[j]}.html`];
2272 }catch(error){
2273 td_data = json_data[i][this.COLUMN_NAME_ORDER[j]];
2274 }
2275 //Add the value/data to the TD tag
2276
2277 // Adding the draggable arrow elements to each cell, for the copy-drag functionality to work.
2278
2279 let cell_drag_element = document.createElement('div');
2280 cell_drag_element.classList.add('copy_drag');
2281
2282 let cell_drag_element_inner = document.createElement('div');
2283 cell_drag_element_inner.classList.add('copy_drag_inner');
2284
2285
2286 let resize_handle_element = document.createElement('div');
2287 resize_handle_element.classList.add('resize-handle');
2288
2289 /// &#x02193 is Unicode for down-arrow.
2290 if(this.drag_increment_mode){
2291 cell_drag_element_inner.innerHTML = '&#x002B;';
2292 }else{
2293 cell_drag_element_inner.innerHTML = '&#x02193;';
2294 }
2295 cell_drag_element.appendChild(cell_drag_element_inner);
2296
2297 if(!this.configuration["column_information"][this.COLUMN_NAME_ORDER[j]]["unlock"] && this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] != 'html'){
2298 let cell_data = json_data[i][this.COLUMN_NAME_ORDER[j]];
2299 if(cell_data == null){
2300 cell_data = '';
2301 }
2302 // For text overflow purposes, we need to wrap raw text inputs with a div to overflow into ellipsis.
2303 cell.innerHTML = `<div class="cell-data-wrapper">${cell_data}</div>`;
2304 let reference_div = document.createElement('div');
2305 reference_div.value = cell_data;
2306 reference_div.style.display = 'none';
2307 reference_div.setAttribute('id', `${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}`);
2308 reference_div.setAttribute('reference-id', json_data[i][this.primary_key]);
2309
2310 cell.appendChild(reference_div);
2311 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_html']){
2312 let additional_html = this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_html'].trim();
2313 let placeholder = document.createElement('div');
2314 placeholder.insertAdjacentHTML('afterbegin', additional_html);
2315 let sub_elements = placeholder.querySelectorAll('*');
2316 for(let k = 0; k < sub_elements.length; k++){
2317 sub_elements[k].id += json_data[i][this.primary_key];
2318 }
2319 cell.insertAdjacentHTML('beforeend', placeholder.innerHTML);
2320 }
2321 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_javascript']){
2322 let additional_js = this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_javascript'].trim();
2323 let script_element = document.createElement('script');
2324 // Create the 3 default variables that let us access the correct item, grid_reference, cell_reference, inner_element
2325 let wrapping_js = `if(true){let grid_reference = window.gridReference.get('${this.id}'); let inner_element = grid_reference.shadowRoot.getElementById('${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}'); let cell_reference = grid_reference.shadowRoot.getElementById('${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}').parentElement; `;
2326 let closing_js = `}`;
2327 // Insert the additional javascript after the initial variable creation
2328 let complete_js = wrapping_js + additional_js + closing_js;
2329 script_element.style.display = 'none';
2330 script_element.textContent = complete_js;
2331 cell.appendChild(script_element);
2332 }
2333 if(json_data[i][`${this.COLUMN_NAME_ORDER[j]}.comment`]){
2334 cell.insertAdjacentHTML('afterbegin', json_data[i][`${this.COLUMN_NAME_ORDER[j]}.comment`]);
2335 }
2336 let listeners;
2337 // Loop over the listeners and attach them to the field.
2338 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['listeners']){
2339 listeners = this.column_information[this.COLUMN_NAME_ORDER[j]]['listeners'];
2340 Object.keys(listeners).forEach((key) => {
2341 cell.setAttribute(key, listeners[key]);
2342 });
2343
2344 }
2345 // Checking whether to use config based html/javascript, or have it coming from the backend.
2346 }else if(this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] && (this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] != 'html')){
2347 let inner_element;
2348 let cell_data = json_data[i][this.COLUMN_NAME_ORDER[j]];
2349 if(cell_data == null){
2350 cell_data = '';
2351 }
2352 // Content Area
2353 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] == 'contentarea'){
2354 inner_element = document.createElement('div');
2355 inner_element.insertAdjacentHTML('afterbegin', cell_data);
2356 inner_element.contentEditable = 'plaintext-only';
2357 inner_element.onclick = this.anchor_click_handler;
2358 inner_element.classList.add('contentarea');
2359 // Text Area
2360 }else if(this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] == 'textarea'){
2361 inner_element = document.createElement('textarea');
2362 inner_element.insertAdjacentHTML('afterbegin', cell_data);
2363 // Input
2364 }else if(this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] == 'input'){
2365 inner_element = document.createElement('input');
2366 inner_element.setAttribute('value', cell_data);
2367
2368 if(Number(this.column_information[this.COLUMN_NAME_ORDER[j]]['maxlength']) > 0){
2369 inner_element.setAttribute('maxlength', Number(this.column_information[this.COLUMN_NAME_ORDER[j]]['maxlength']));
2370 }
2371
2372 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['disable_field'] == true){
2373 inner_element.setAttribute('disabled', true);
2374 }
2375 // Select
2376 }else if(this.column_information[this.COLUMN_NAME_ORDER[j]]['type'] == 'select'){
2377 inner_element = document.createElement('select');
2378 let options = this.column_information[this.COLUMN_NAME_ORDER[j]]['options'];
2379 // Loop over the options provided in the config, then if one matches the data, set that as the selected option.
2380 Object.keys(options).forEach((key) => {
2381 let option = document.createElement('option');
2382 option.value = key;
2383 option.innerHTML = options[key];
2384 if(key == cell_data){
2385 option.setAttribute('selected', 'selected');
2386 }
2387 inner_element.appendChild(option);
2388
2389 });
2390 // Setting the value as well.
2391 inner_element.value = cell_data;
2392 }
2393 // The reference ID is the primary key ID number, such as if your primary key was az_project_item_id, the reference id would just be 7
2394 // This is how we can dynamicize the additional content and attached listeners, with ID's without knowing them in the template.
2395 inner_element.setAttribute('reference-id', json_data[i][this.primary_key]);
2396 // The ID includes the field name AND the primary key value. such as hub_a_hub7
2397 inner_element.setAttribute('id', `${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}`);
2398 // Names and ID's are shared, for different use cases
2399 inner_element.setAttribute('name', `${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}`);
2400
2401 let listeners;
2402 // Loop over the listeners and attach them to the field.
2403 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['listeners']){
2404 listeners = this.column_information[this.COLUMN_NAME_ORDER[j]]['listeners'];
2405 Object.keys(listeners).forEach((key) => {
2406 inner_element.setAttribute(key, listeners[key]);
2407 });
2408
2409 }
2410 // Attach sub styles
2411 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['substyle']){
2412 inner_element.style.cssText = `${this.column_information[this.COLUMN_NAME_ORDER[j]]['substyle']}`;
2413 }
2414 // Attach sub classes
2415 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['subclasses']){
2416 let classes = this.column_information[this.COLUMN_NAME_ORDER[j]]['subclasses'].split(' ');
2417 inner_element.classList.add(classes);
2418
2419 }
2420 // Append the initial field
2421 cell.appendChild(inner_element);
2422 // If the field is bringing additional html, such as helper tags, loop and attach them here.
2423 // This has a unique way to handle the ids, the ids in the config should leave the number off,
2424 // the number will get added here.
2425 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_html']){
2426 let additional_html = this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_html'].trim();
2427 let placeholder = document.createElement('div');
2428 placeholder.insertAdjacentHTML('afterbegin', additional_html);
2429 let sub_elements = placeholder.querySelectorAll('*');
2430 for(let k = 0; k < sub_elements.length; k++){
2431 sub_elements[k].id += json_data[i][this.primary_key];
2432 }
2433 cell.insertAdjacentHTML('beforeend', placeholder.innerHTML);
2434 }
2435 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_javascript']){
2436 let additional_js = this.column_information[this.COLUMN_NAME_ORDER[j]]['additional_javascript'].trim();
2437 let script_element = document.createElement('script');
2438 // Create the 3 default variables that let us access the correct item, grid_reference, cell_reference, inner_element
2439 let wrapping_js = `if(true){let grid_reference = window.gridReference.get('${this.id}'); let inner_element = grid_reference.shadowRoot.getElementById('${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}'); let cell_reference = grid_reference.shadowRoot.getElementById('${this.COLUMN_NAME_ORDER[j]}${json_data[i][this.primary_key]}').parentElement; `;
2440 let closing_js = `}`;
2441 // Insert the additional javascript after the initial variable creation
2442 let complete_js = wrapping_js + additional_js + closing_js;
2443 script_element.style.display = 'none';
2444 script_element.textContent = complete_js;
2445 cell.appendChild(script_element);
2446 }
2447 // Inserting the comments.
2448 if(json_data[i][`${this.COLUMN_NAME_ORDER[j]}.comment`]){
2449 cell.insertAdjacentHTML('afterbegin', json_data[i][`${this.COLUMN_NAME_ORDER[j]}.comment`]);
2450 }
2451 }else{
2452 // Insert the html, followed by any comments if available.
2453
2454 cell.innerHTML = `<div>${td_data}</div>`;
2455 if(json_data[i][`${this.COLUMN_NAME_ORDER[j]}.comment`]){
2456 cell.insertAdjacentHTML('afterbegin', json_data[i][`${this.COLUMN_NAME_ORDER[j]}.comment`]);
2457 }
2458 let listeners;
2459 if(this.column_information[this.COLUMN_NAME_ORDER[j]]['listeners']){
2460 listeners = this.column_information[this.COLUMN_NAME_ORDER[j]]['listeners'];
2461 Object.keys(listeners).forEach((key) => {
2462 cell.setAttribute(key, listeners[key]);
2463 });
2464
2465 }
2466 }
2467 // cell.innerHTML = `${td_data}`;
2468 //Add the table data classname
2469 cell.className = "grid_table_data";
2470
2471
2472 //--------------------------------------------------------------------------------
2473 //Apply defaults for the grid table cells ----------------------------------------
2474 try{
2475 this.COLUMN_NAME_ORDER.forEach((key) => {
2476 if(this.COLUMN_NAME_ORDER[j] == key){
2477 let class_name = `${key}_config_styles`;
2478 let new_class = ``;
2479 cell.classList.add(class_name);
2480 cell.style.setProperty('width', '');
2481 }
2482 });
2483 }catch(error){
2484 //We don't want an errors if the custom styles don't exist, we just wanted to only apply them if they did exist
2485 }
2486 if(this.configuration["column_information"][this.COLUMN_NAME_ORDER[j]]["unlock"]){
2487 if(this.drag_copy_features != false){
2488 cell.appendChild(cell_drag_element);
2489 }
2490 }
2491 cell.appendChild(resize_handle_element);
2492 // cell.addEventListener("keydown", this.copy_handler);
2493 // cell.addEventListener("paste", this.excel_edit);
2494 }
2495
2496 current_record_number++; //Add one to track how many records we have displayed
2497 if(current_record_number >= insert_amount){
2498 //If we have hit our max number of records then stop displaying them but keep adding pagination links
2499 break;
2500 }
2501
2502 if(prepend_append == "append"){
2503 i++;
2504 }else{
2505 i--;
2506 }
2507 }
2508 let final_table = tbody;
2509 for(let element of final_table.children){
2510 // Handling the context menu, whether custom or default
2511 if(this.contextmenu && !this.excel_edit){
2512 element.oncontextmenu = this.right_click_row;
2513 }else if(this.excel_edit){
2514 for(let cell of element.children){
2515 cell.setAttribute("contenteditable", "true");
2516 cell.addEventListener('keydown', this.excel_keydown_handler);
2517 cell.addEventListener('paste', this.copy_handler, true);
2518 }
2519 }
2520 // Adding the event listeners for mouse up and mouse down to the down arrow element, to handle copy dragging.
2521 let draggables = element.querySelectorAll('.copy_drag');
2522 for(let drag_element of draggables){
2523 drag_element.addEventListener('mousedown', this.cell_down_handler);
2524 drag_element.parentElement.addEventListener('mouseup', this.cell_up_handler);
2525 }
2526 }
2527 }
2528 get_json_row(row_number){
2529 return this.GLOBAL_JSON_RESULT_FILTERED[row_number];
2530 }
2531
2532 get_json_single(row_number, key_name){
2533 return this.GLOBAL_JSON_RESULT_FILTERED[row_number][key_name];
2534 }
2535 get_row_from_cell(cell){
2536 if(this.shadowRoot.getElementById(`${cell.id}`)){
2537 let row_number = cell.id.split(',')[0];
2538 return this.GLOBAL_JSON_RESULT_FILTERED[row_number];
2539 }else{
2540 console.log("Warning: Cell is not an element in this grid.");
2541 }
2542 }
2543 get_value_from_cell(cell){
2544 if(this.shadowRoot.getElementById(`${cell.id}`)){
2545 let row_number = cell.id.split(',')[0];
2546 let header = cell.headers;
2547 return this.GLOBAL_JSON_RESULT_FILTERED[row_number][header];
2548 }else{
2549 console.log("Warning: Cell is not an element in this grid.");
2550 }
2551 }
2552 set_value_from_cell(cell, value){
2553 if(this.shadowRoot.getElementById(`${cell.id}`)){
2554 let row_number = cell.id.split(',')[0];
2555 let header = cell.headers;
2556 this.GLOBAL_JSON_RESULT_FILTERED[row_number][header] = value;
2557 }else{
2558 console.log("Warning: Cell is not an element in this grid.");
2559 }
2560 }
2561 // Check Data checks that we have valid json, if we don't, display an unable to load message.
2562 // Also cleans up elements of a previous table if they exist.
2563 check_data(){
2564 // If no data returned from the backend, remove the spinner and any old grid elements, and display a no data message.
2565 // The else if is incase the grid gets reloaded, we need to remove the no data message.
2566 if(this.GLOBAL_JSON_RESULT.length == 0){
2567 if(this.shadowRoot.querySelector('.empty-json-container')){
2568 this.shadowRoot.querySelector('.empty-json-container').remove();
2569 }
2570 if(this.shadowRoot.querySelector('.spinner-container')){
2571 let spinner = this.shadowRoot.querySelector('.spinner-container');
2572 spinner.remove();
2573 }
2574 if(this.shadowRoot.querySelector('.table_container')){
2575 this.shadowRoot.querySelector('.table_container').remove();
2576 }
2577 if(this.shadowRoot.querySelector('.pagination_container')){
2578 this.shadowRoot.querySelector('.pagination_container').remove()
2579 }
2580 let empty_json_container = document.createElement('div');
2581 let empty_json_message = document.createElement('div');
2582 empty_json_container.classList.add('empty-json-container');
2583 empty_json_message.innerHTML = `Table Unable To Load. No Data Provided.`
2584 empty_json_container.appendChild(empty_json_message);
2585 this.shadowRoot.appendChild(empty_json_container);
2586 return true;
2587 }else if(this.shadowRoot.querySelector('.empty-json-container')){
2588 this.shadowRoot.querySelector('.empty-json-container').remove();
2589 return false;
2590 }
2591 return false;
2592 }
2593 delete_existing_table(){
2594 while(this.shadowRoot.children.length > 1){
2595 if(this.shadowRoot.children[0].tagName == 'STYLE'){
2596 this.shadowRoot.children[1].remove();
2597 }else{
2598 this.shadowRoot.children[0].remove();
2599 }
2600 }
2601 }
2602 check_filtered_data(filtered_data){
2603 if(filtered_data.length == 0){
2604 if(this.shadowRoot.querySelector('.spinner-container')){
2605 let spinner = this.shadowRoot.querySelector('.spinner-container');
2606 spinner.remove();
2607 }
2608 if(this.shadowRoot.querySelector('table')){
2609 console.log("Table Found");
2610 if(this.shadowRoot.querySelector('.no_data_container')){
2611 this.shadowRoot.querySelector('.no_data_container').remove();
2612 }
2613 if(this.shadowRoot.querySelector('.table_container')){
2614 this.shadowRoot.querySelector('.table_container').remove();
2615 }
2616
2617 if(this.shadowRoot.querySelector('.toolbar_container')){
2618 this.shadowRoot.querySelector('.toolbar_container').remove();
2619 }
2620
2621 if(this.shadowRoot.querySelector('.pagination_container')){
2622 this.shadowRoot.querySelector('.pagination_container').remove();
2623 }
2624 if(this.shadowRoot.querySelector('.pagination_container')){
2625 this.shadowRoot.querySelector('.pagination_container').remove();
2626 }
2627 }
2628 // Creating the div to convey there is no data with the active filters
2629 let no_data_container = document.createElement("div");
2630 no_data_container.classList.add("no_data_container");
2631 let no_data_display = document.createElement("div");
2632 no_data_display.id = 'no-data-div';
2633 no_data_display.innerHTML = this.grid_filter_list;
2634 let no_data_message = document.createElement("p");
2635 //TODO: If statement wrap around this to verify whether there's no actual data, or if there's no data because of filters
2636 no_data_message.innerHTML = "Data cannot be displayed with these active filters. Remove filters from the list below."
2637 no_data_container.appendChild(no_data_message);
2638 no_data_container.appendChild(no_data_display);
2639 this.shadowRoot.appendChild(no_data_container);
2640
2641
2642 document.body.style.cursor = 'default';
2643
2644 // Don't try to load the rest of the grid, wasted effort.
2645 return true;
2646 }
2647 return false;
2648 }
2649 get_previous_grid_scroll_focus(){
2650 // This is to refocus on the last focused cell/input element. This section is to prevent onBlur being called every reload.
2651 // We had an infinite loop of blur to reload, click new cell, reload grid, lose onblur, refocus etc. This prevents that
2652 // At the cost of not being up to date while editing the cell.
2653 if(this.shadowRoot.activeElement){
2654 let tag_name = this.shadowRoot.activeElement.tagName;
2655 //Store the ID so we can refocus it later.
2656 this.LAST_ACTIVE_ELEMENT_ID = this.shadowRoot.activeElement.id;
2657 //If the last focused element was an input field, set the cursor back to the right character.
2658 if(tag_name == "SELECT" || tag_name == "INPUT" || tag_name == "TEXTAREA"){
2659 this.LAST_ACTIVE_ELEMENT_CURSOR_POSITION = this.shadowRoot.activeElement.selectionStart;
2660 }
2661 }
2662 if(this.shadowRoot.querySelector('.table_container')){
2663 const current_table = this.shadowRoot.querySelector('.table_container');
2664 let scroll_offset_x = current_table.scrollLeft;
2665 let scroll_offset_y = current_table.scrollTop;
2666 let current_page = Number(this.shadowRoot.querySelector('.pagination_selected_link').innerHTML);
2667 return [current_page, scroll_offset_x, scroll_offset_y];
2668 }
2669 return false;
2670 }
2671 set_grid_settings(){
2672 //Extract the header values ------------------------------------------------------
2673 let col = [];
2674
2675 this.column_information = this.configuration["column_information"];
2676 //Convert the string back into a variable and dereference the data
2677
2678 Object.keys(this.column_information).forEach((key) => {
2679 col.push(key);
2680 });
2681 // Copy the array from col into the Global COLUMN_NAME_ORDER, for use throughout the grid.
2682 this.COLUMN_NAME_ORDER = Array.from(col);
2683 for (let gs of this.grid_settings) {
2684 //Get the max pagination links to display per page
2685 if(this.id == gs.id && gs.max_pagination_links){
2686 this.max_pagination_links = gs.max_pagination_links;
2687 }
2688 }
2689 //Loop through the filters displaying the ones that have been applied to this grid
2690
2691
2692 this.grid_filter_list = '';
2693 this.grid_filter_columns = [];
2694 for(let gf of this.grid_filters) {
2695 if(gf.grid_id == this.id){
2696 //Currently reaching out ot the dom to get the component, then calling its functions.
2697 //Potentially updateable to pass a direct reference to this component.
2698 // REMOVE THIS TO GO BACK TO NORMAL -----------------------------------------------------------------------------------------------------------------------
2699 // Some grid templates have differing values for key-value pairs, most are x:x but some are x:y, x being the actually
2700 // we have to do some renaming for display purposes, displaying Y, but processing and using X for the backend.
2701 let grid_filter_display_value = gf.filter_value;
2702 if(gf.column_name){
2703 let grid_component = this;
2704 let column_type = grid_component.column_information[gf.column_name]["type"];
2705 if(column_type == "select"){
2706 let option_mapping = grid_component.column_information[gf.column_name]["options"];
2707 let filter_value_key_map = option_mapping[gf.filter_value];
2708 grid_filter_display_value = filter_value_key_map;
2709
2710 }
2711 }
2712
2713 // REMOVE THIS TO GO BACK TO NORMAL -----------------------------------------------------------------------------------------------------------------------
2714
2715
2716 this.grid_filter_list += `<a href="javascript:window.gridReference.get('${this.id}').remove_filter('${this.id}','${gf.column_name}','${gf.filter_type}','${gf.filter_value}');" title="Click to remove this filter" style="color:#24406d">remove</a> - Where ${gf.column_name} ${gf.filter_type} '${grid_filter_display_value}'<br>`;
2717 this.grid_filter_columns.push(gf.column_name);
2718 }
2719 }
2720 //If no filters currently applied then set it to no filters are currently selected
2721 if(this.grid_filter_list == ''){
2722 this.grid_filter_list += "no filters are currently selected<br>";
2723 }
2724 }
2725 add_additional_features(grid_id){
2726 let final_table = this.shadowRoot.getElementById(`${grid_id}_table`).querySelector('tbody');
2727
2728 // Start in default mode, which gives the user our custom right click menu
2729 // When the Excel button is hit, the value is flipped, the rows no longer use our right click menu
2730 // And the user gets the default browser right click menu back, for pasting purposes.
2731 for(let element of final_table.children){
2732 // Handling the context menu, whether custom or default
2733 if(this.contextmenu && !this.excel_edit){
2734 element.oncontextmenu = this.right_click_row;
2735 }else if(this.excel_edit){
2736 for(let cell of element.children){
2737 cell.setAttribute("contenteditable", "true");
2738 cell.addEventListener('keydown', this.excel_keydown_handler);
2739 cell.addEventListener('paste', this.copy_handler, true);
2740 }
2741 }
2742 // Adding the event listeners for mouse up and mouse down to the down arrow element, to handle copy dragging.
2743 let draggables = element.querySelectorAll('.copy_drag');
2744 for(let drag_element of draggables){
2745 drag_element.addEventListener('mousedown', this.cell_down_handler);
2746 drag_element.parentElement.addEventListener('mouseup', this.cell_up_handler);
2747 }
2748 }
2749 // Setting up the merge columns/super headers, if provided in the config json
2750 if(this.configuration["super_header_information"]){
2751 // Create the container div for the pseudo header elements
2752 let thead = this.shadowRoot.querySelector('thead');
2753 thead.style.top ="35px";
2754
2755 let super_div_row = document.createElement("div");
2756 super_div_row.classList.add("super_header_row");
2757 let super_header_information = this.configuration["super_header_information"];
2758 let column_information = this.configuration["column_information"];
2759 // Column offset to iterate over all columns used, allows multiple headers. 0 means no row number column, 1 means row number column.
2760 let offset = 1;
2761 // The container div needs to have a starting with to properly fit the table.
2762 let super_div_row_width = 0;
2763 // Loop over the header keys, and grab the colspan value from them.
2764 let sub_header_columns = this.shadowRoot.getElementById(`${grid_id}_table`).querySelector('thead').children[0];
2765 let super_header_number = 0;
2766 // Loop over each key in the super header information section of the config
2767 // and calculate how many columns they encompass using offsetWidth.
2768 Object.keys(super_header_information).forEach((key) => {
2769 let super_header = document.createElement("div");
2770 super_header.id = `super_${super_header_number}`;
2771 // How many headers to span
2772 let colspan = super_header_information[key]["colspan"];
2773 let offset_increase = 0;
2774 let super_header_width = 0;
2775 for(let i = 0; i < colspan; i++){
2776 let current_header = sub_header_columns.children[i+offset]
2777 current_header.setAttribute("super-header-owner", `super_${super_header_number}`);
2778
2779 let child_header_width = current_header.offsetWidth;
2780 // Increment the super header's width to match its children.
2781 super_header_width += child_header_width;
2782 offset_increase++;
2783
2784 }
2785
2786 // Increment the container row to properly match the width of all super headers.
2787 super_div_row_width += super_header_width;
2788 offset += offset_increase;
2789 super_header_number++;
2790
2791 if(super_header_information[key]["style"]){
2792 super_header.setAttribute("style", super_header_information[key]["style"]);
2793 }
2794
2795 super_header.style.width = `${super_header_width}px`;
2796 // Renaming the super headers to their rename value if provided.
2797 if(!key.includes("spacer")){
2798 if(super_header_information[key]["rename"]){
2799 super_header.setAttribute("super-header-name", super_header_information[key]["rename"]);
2800 }else{
2801 super_header.setAttribute("super-header-name", key);
2802 }
2803 }
2804 super_header.classList.add("super_header");
2805 super_div_row.appendChild(super_header);
2806 });
2807 // Set the final width of the entire super header row, which is the total width of all child super headers
2808 super_div_row.style.width = `${super_div_row_width}px`;
2809 let final_container = this.shadowRoot.getElementById(`${grid_id}_container`);
2810 // Prepend to the top of the table.
2811 final_container.prepend(super_div_row);
2812 }
2813 // Setting up the interactive toolbar for the top row.
2814 if(this.toolbar_allowed){
2815 // Remove existing toolbar on table redraw if exists.
2816 if(this.shadowRoot.querySelector('.toolbar_container')){
2817 this.shadowRoot.querySelector('.toolbar_container').remove();
2818 }
2819 let toolbar_container = document.createElement("div");
2820
2821
2822 this.shadowRoot.prepend(toolbar_container);
2823 toolbar_container.classList.add("toolbar_container");
2824 let grid_owner = this.id;
2825
2826 let icon_tooltip_container = document.createElement("div");
2827 icon_tooltip_container.classList.add("icon_tooltip_container");
2828
2829
2830 // Building the Excel Button
2831 let excel_button = document.createElement("img");
2832 excel_button.classList.add("grid-icon");
2833 excel_button.setAttribute("title", "Enter Excel-Paste Mode");
2834 excel_button.setAttribute("src", "/assets/images/Microsoft-Excel-Logo.png");
2835 excel_button.setAttribute("grid-owner", grid_owner);
2836
2837 // Building the Undo Button
2838 let undo_button = document.createElement("button");
2839 undo_button.innerHTML = "<span>&#8630;</span> Undo"
2840 undo_button.style.height = "30px";
2841 undo_button.style.width = "65px";
2842 undo_button.setAttribute("grid-owner", grid_owner);
2843
2844 // Building the button container
2845 let button_container = document.createElement("div");
2846 button_container.classList.add("icon_container");
2847 button_container.appendChild(excel_button);
2848 button_container.appendChild(undo_button);
2849
2850 let toolbar_tooltips = document.createElement("div");
2851 toolbar_tooltips.classList.add("tooltips");
2852 let tooltip_1 = document.createElement("p");
2853
2854 tooltip_1.innerHTML = 'Click the Excel icon to enable/disable pasting from Excel.';
2855 tooltip_1.classList.add("page_font");
2856 let tooltip_2 = document.createElement("p");
2857 tooltip_2.classList.add("page_font");
2858 tooltip_2.innerHTML = 'Click Undo to undo a paste command. Currently limited to a history of 1.';
2859 toolbar_tooltips.appendChild(tooltip_1);
2860 toolbar_tooltips.appendChild(tooltip_2);
2861 // -----------------------------------------------------------------------------------
2862 // Building the drag increment toggle feature
2863 let drag_toggle_container = document.createElement("div");
2864 drag_toggle_container.classList.add("increment-toggle-container");
2865 let drag_toggle_button = document.createElement("button");
2866 drag_toggle_button.id = `${this.id}-increment-toggle`;
2867 drag_toggle_button.setAttribute("grid-reference", this.id);
2868 drag_toggle_button.classList.add("increment-button");
2869 if(this.drag_increment_mode){
2870 drag_toggle_button.classList.add('increment-toggle');
2871 }
2872 drag_toggle_button.insertAdjacentHTML('afterbegin', "Enable Increment on Drag");
2873
2874 drag_toggle_container.appendChild(drag_toggle_button);
2875 // ----------------------------------------------------------------------------------------
2876 // Building the zoom in/out feature (increments text size);
2877 let zoom_button_container = document.createElement("div");
2878 zoom_button_container.classList.add("zoom-button-container");
2879
2880 let zoom_in_button = document.createElement("button");
2881 let zoom_out_button = document.createElement("button");
2882 let zoom_reset_button = document.createElement("button");
2883
2884 zoom_in_button.id = `${this.id}-zoom-in`;
2885 zoom_in_button.classList.add("zoom-in-button");
2886 zoom_in_button.setAttribute("grid-reference", this.id);
2887 zoom_in_button.insertAdjacentHTML("afterbegin", `<svg fill="#000000" width="15px" height="15px" viewBox="0 0 200 200">
2888 <path d="M126.5,84.25h-22v-22a10,10,0,0,0-20,0v22h-22a10,10,0,0,0,0,20h22v22a10,10,0,0,0,20,0v-22h22a10,10,0,0,0,0-20Z"/>
2889 <path d="M154.5,140.75a77.3,77.3,0,0,0,16-47c.5-42.5-34-77-76.5-77a77,77,0,0,0,0,154,76.21,76.21,0,0,0,47-16l25.5,25.5c4,4,10,4,13.5,0a9.67,9.67,0,0,0,0-14Zm-60.5,10a57,57,0,1,1,57-57A57,57,0,0,1,94,150.75Z"/>
2890 </svg>`);
2891
2892 zoom_out_button.id = `${this.id}-zoom-out`;
2893 zoom_out_button.classList.add("zoom-out-button");
2894 zoom_out_button.setAttribute("grid-reference", this.id);
2895 zoom_out_button.insertAdjacentHTML("afterbegin", `<svg fill="#000000" width="15px" height="15px" viewBox="0 0 200 200">
2896 <path d="M154.5,140.75a77.3,77.3,0,0,0,16-47c.5-42.5-34-77-76.5-77a77,77,0,0,0,0,154,76.21,76.21,0,0,0,47-16l25.5,25.5c4,4,10,4,13.5,0a9.67,9.67,0,0,0,0-14Zm-60.5,10a57,57,0,1,1,57-57A57,57,0,0,1,94,150.75Z"/>
2897 <path d="M126.5,84.25h-64a10,10,0,0,0,0,20h64a10,10,0,0,0,0-20Z"/>
2898 </svg>`);
2899
2900 zoom_reset_button.id = `${this.id}-zoom-reset`;
2901 zoom_reset_button.classList.add("zoom-reset-button");
2902 zoom_reset_button.setAttribute("grid-reference", this.id);
2903 zoom_reset_button.insertAdjacentHTML("afterbegin", "Reset Zoom");
2904
2905 //! Micah Edit for highlighted rows
2906 // Building the checkbox for turning filter rows on and off.
2907 let highlight_checkbox = document.createElement("input");
2908 highlight_checkbox.type = "checkbox";
2909 highlight_checkbox.id = "highlight_checkbox_id";
2910 highlight_checkbox.name = "highlight_checkbox_name";
2911 highlight_checkbox.title = "Toggle row highlighting on mouse over on or off.";
2912 if(this.highlight_checked){
2913 highlight_checkbox.checked = true; // Optional: set initial checked state to true
2914 }else{
2915 highlight_checkbox.checked = false;
2916 }
2917 //highlight_checkbox.setAttribute("onclick", "check_row_highlight()");
2918 // 2. Create the label element
2919 let highlight_label = document.createElement('label');
2920 highlight_label.htmlFor = "highlight_checkbox_id";
2921 highlight_label.title = "Toggle row highlighting on mouse over on or off.";
2922 highlight_label.appendChild(document.createTextNode("Highlight Row"));
2923
2924 // 3. Create a container (e.g., a div) to hold both, if desired
2925 let highlight_container = document.createElement('div');
2926 highlight_container.classList.add("highlight_section");
2927 highlight_container.appendChild(highlight_checkbox);
2928 highlight_container.appendChild(highlight_label);
2929 //!
2930
2931 // Building the filter symbol.
2932 let filter_notification_div = document.createElement("div");
2933 filter_notification_div.classList.add("filter_notification");
2934
2935 let filter_notification_dot = document.createElement("div");
2936
2937 let notification_message = document.createElement("p");
2938 if(this.grid_filter_columns.length < 1){
2939 notification_message.innerHTML = `No Active Filters`;
2940 filter_notification_dot.classList.add("filter_dot_none");
2941 filter_notification_dot.innerHTML = `&#10007;`;
2942 filter_notification_div.appendChild(filter_notification_dot);
2943 filter_notification_div.appendChild(notification_message);
2944 }else{
2945 notification_message.innerHTML = `There are Active Filters `;
2946 filter_notification_dot.classList.add("filter_dot_active");
2947 filter_notification_dot.innerHTML = `&#10003;`;
2948 filter_notification_div.appendChild(filter_notification_dot);
2949 filter_notification_div.appendChild(notification_message);
2950 }
2951
2952 // Assign onclicks after building, or we lose the listener.
2953 highlight_checkbox.onclick = this.check_row_highlight;
2954 undo_button.onclick = this.undo_copy;
2955 excel_button.onclick = this.excel_mode;
2956 drag_toggle_button.onclick = this.enable_increment;
2957 zoom_in_button.onclick = this.zoom_in;
2958 zoom_out_button.onclick = this.zoom_out;
2959 zoom_reset_button.onclick = this.zoom_reset;
2960 zoom_button_container.appendChild(zoom_reset_button);
2961 zoom_button_container.appendChild(zoom_out_button);
2962 zoom_button_container.appendChild(zoom_in_button);
2963
2964 icon_tooltip_container.appendChild(button_container);
2965 icon_tooltip_container.appendChild(toolbar_tooltips);
2966 if(this.excel_button){
2967 toolbar_container.appendChild(icon_tooltip_container);
2968 }
2969 if(this.increment_button){
2970 toolbar_container.appendChild(drag_toggle_container);
2971 }
2972 if(this.zoom_button){
2973 toolbar_container.appendChild(zoom_button_container);
2974 }
2975 //! Micah Edit
2976 // Add the checkbox to turn highlighting on and off
2977 if(this.highlight_checkbox){
2978 toolbar_container.appendChild(highlight_container);
2979 }
2980 if(this.filter_notification){
2981 toolbar_container.appendChild(filter_notification_div);
2982 }
2983
2984 }
2985 if(this.scroll_allowed){
2986 const final_table_container = this.shadowRoot.querySelector(`div#${this.id}_container`);
2987 // final_table_container.addEventListener("scrollend", this.scroll_handler);
2988 final_table_container.addEventListener("wheel", this.scroll_handler);
2989 }
2990 }
2991 scroll_handler(event){
2992 let grid_reference = window.gridReference.get(this.id.replace('_container', ''));
2993 let scroll_direction;
2994 if(!event.shiftKey){
2995 let scroll_bypass_down = false;
2996 let scroll_bypass_up = false;
2997 if(this.scrollHeight == this.clientHeight){
2998 if(event.deltaY > 0){
2999 scroll_bypass_down = true;
3000 }else{
3001 scroll_bypass_up = true;
3002 }
3003 }
3004 // if there is no scroll bar (we have too little rows, or screen is fully displayed, this will always run, unless you check for this.scrollHeight)
3005 // Scroll bypasses are for when there's no scroll bar, but potentially more data, such as last page scroll backwards.
3006 let scroll_position = this.scrollHeight - this.scrollTop - this.clientHeight + 1;
3007 // if((0 < scroll_position && scroll_position < 20) || (scroll_bypass_down)){
3008 if(((0 < scroll_position && scroll_position < 20) && event.deltaY > 0) || (scroll_bypass_down)){
3009 scroll_direction = "down";
3010 let rows = Array.from(grid_reference.shadowRoot.querySelectorAll("tbody tr:not([class*='hidden-row'])"));
3011 let next_row = Number(rows[rows.length - 1].id.replace("row",'')) + 1;
3012 if(next_row < grid_reference.GLOBAL_JSON_RESULT_FILTERED.length){
3013 for(let i = 0; i < 2; i++){
3014 let row = rows.shift();
3015 row.classList.add('start-hidden-row');
3016 }
3017
3018 let tbody = grid_reference.shadowRoot.querySelector("tbody");
3019 let end_hidden_rows = Array.from(grid_reference.shadowRoot.querySelectorAll("tr.end-hidden-row"));
3020 let insert_count = 2;
3021 if(end_hidden_rows.length >= insert_count){
3022 for(let i = 0; i < insert_count; i++){
3023 end_hidden_rows[i].classList.remove('end-hidden-row');
3024
3025 }
3026 this.scrollTop = this.scrollHeight - 10;
3027 }else{
3028 insert_count = insert_count - end_hidden_rows.length;
3029 for(let i = 0; i < end_hidden_rows.length; i++){
3030 end_hidden_rows[i].classList.remove('end-hidden-row');
3031 }
3032 grid_reference.create_rows(next_row, grid_reference.GLOBAL_JSON_RESULT_FILTERED, tbody, insert_count, "append");
3033 this.scrollTop = this.scrollHeight - 10;
3034 }
3035 }
3036
3037 // }else if((this.scrollTop <= 10) || (scroll_bypass_up)){
3038 }else if(((this.scrollTop <= 10) && event.deltaY < 0) || (scroll_bypass_up)){
3039 scroll_direction = "up";
3040 let hidden_rows = Array.from(grid_reference.shadowRoot.querySelectorAll("tr.start-hidden-row"));
3041 let rows = Array.from(grid_reference.shadowRoot.querySelectorAll("tbody tr:not([class*='hidden-row'])"));
3042 let row_count = 2;
3043 if(hidden_rows.length > 0){
3044 if(hidden_rows.length < row_count){
3045 row_count = hidden_rows.length;
3046 }
3047 for(let i = 0; i < row_count; i++){
3048 if(hidden_rows.length - i - 1 >= 0){
3049 hidden_rows[hidden_rows.length - i - 1].classList.remove('start-hidden-row');
3050 }
3051 }
3052 if(rows.length > 0){
3053 row_count = 2;
3054 if(rows.length < row_count){
3055 row_count = rows.length;
3056 }
3057 for(let i = 0; i < row_count; i++){
3058 if(rows.length - i - 1 >= 0 && (this.scrollHeight != this.clientHeight)){
3059 rows[rows.length - i - 1].classList.add('end-hidden-row');
3060 }
3061 }
3062 }
3063 }else if(Number(rows[0].id.replace("row", "")) - 1 >= 0){
3064 // We subtract 1 here because we're going backwards through the data not forwards.
3065 rows = Array.from(grid_reference.shadowRoot.querySelectorAll("tbody tr:not([class*='hidden-row'])"));
3066 for(let i = 0; i < row_count; i++){
3067 if(rows.length - i - 1 >= 0 && (this.scrollHeight != this.clientHeight)){
3068 rows[rows.length - i - 1].classList.add('end-hidden-row');
3069 }
3070 }
3071 let next_row = Number(rows[0].id.replace("row", "")) - 1;
3072 let tbody = grid_reference.shadowRoot.querySelector("tbody");
3073 let insert_count = 2;
3074 grid_reference.create_rows(next_row, grid_reference.GLOBAL_JSON_RESULT_FILTERED, tbody, insert_count, "prepend");
3075
3076 }
3077 }
3078 // Updating pagination after scrolling logic has completed.
3079 // Re-query the rows to make sure the new NodeList is updated after the scroll logic has been applied
3080 if(scroll_direction){
3081 let existing_rows = grid_reference.shadowRoot.querySelectorAll("tbody tr:not([class*='hidden-row'])");
3082 let first_element = grid_reference.shadowRoot.querySelectorAll("tbody tr:not([class*='hidden-row'])")[0];
3083 let last_element = existing_rows[existing_rows.length - 1];
3084 let first_row = Number(first_element.id.replace("row", ""));
3085 let last_row = Number(last_element.id.replace("row", ""));
3086 let results_per_page = grid_reference.max_results;
3087 console.log(results_per_page);
3088 let current_page;
3089 if(scroll_direction == "up"){
3090 current_page = Math.floor(first_row/results_per_page) + 1;
3091 }else if(scroll_direction == "down"){
3092 current_page = Math.floor(last_row/results_per_page) + 1;
3093 }
3094 if(current_page <= 0){
3095 current_page = 1;
3096 }
3097 let pagination_number;
3098 if(grid_reference.shadowRoot.getElementById(`pagination_page_${current_page}`)){
3099 pagination_number = grid_reference.shadowRoot.getElementById(`pagination_page_${current_page}`);
3100 let page_input_element = grid_reference.shadowRoot.getElementById(`${grid_reference.id}_typein_page_number`);
3101 page_input_element.value = current_page;
3102 grid_reference.shadowRoot.querySelector('.pagination_selected_link').classList.remove('pagination_selected_link');
3103 pagination_number.classList.add("pagination_selected_link");
3104 }else{
3105 let pagination_container = grid_reference.shadowRoot.getElementById(`${grid_reference.id}_bottom_row`);
3106 pagination_container.remove();
3107 grid_reference.create_pagination_area(grid_reference.id, grid_reference.GLOBAL_JSON_RESULT_FILTERED, current_page);
3108 }
3109 }
3110 }
3111 }
3112 zoom_reset(){
3113 let grid_reference = window.gridReference.get(this.getAttribute("grid-reference"));
3114 let grid_table = grid_reference.shadowRoot.querySelector("table");
3115 grid_reference.zoom_setting = grid_reference.reset_zoom_setting;
3116 grid_table.style.setProperty("--mod-text-size", `${grid_reference.zoom_setting}px`);
3117 }
3118 zoom_in(){
3119 let grid_reference = window.gridReference.get(this.getAttribute("grid-reference"));
3120 let grid_table = grid_reference.shadowRoot.querySelector("table");
3121 let current_text_size = Number(getComputedStyle(grid_table).getPropertyValue('--mod-text-size').replace("px", ""));
3122 let new_text_size = current_text_size + 2;
3123 grid_reference.zoom_setting = new_text_size;
3124 grid_table.style.setProperty("--mod-text-size", `${new_text_size}px`);
3125 }
3126 zoom_out(){
3127 let grid_reference = window.gridReference.get(this.getAttribute("grid-reference"));
3128 let grid_table = grid_reference.shadowRoot.querySelector("table");
3129 let current_text_size = Number(getComputedStyle(grid_table).getPropertyValue('--mod-text-size').replace("px", ""));
3130 let new_text_size = current_text_size - 2;
3131 if(new_text_size <= 6){
3132 new_text_size = 6;
3133 }
3134 grid_reference.zoom_setting = new_text_size;
3135 grid_table.style.setProperty("--mod-text-size", `${new_text_size}px`);
3136 // Resizers maintain the height of the original table, they have to get updated here.
3137 // let resizer_height = grid_table.getBoundingClientRect()["height"];
3138 // let resizers = Array.from(grid_reference.shadowRoot.querySelectorAll('.resizer'));
3139 // for(let i = 0; i < resizers.length; i++){
3140 // resizers[i].style.height = `${resizer_height}px`;
3141 // }
3142 }
3143 enable_increment(){
3144 // This refers to the button that called this function, NOT the web component.
3145 let grid_reference = window.gridReference.get(`${this.getAttribute("grid-reference")}`);
3146 let drag_elements = grid_reference.shadowRoot.querySelectorAll(".copy_drag_inner");
3147 if(this.classList.contains("increment-toggle")){
3148 this.classList.remove("increment-toggle");
3149 this.innerHTML = "Enable Increment on Drag";
3150 grid_reference.drag_increment_mode = !grid_reference.drag_increment_mode;
3151 for(let i = 0; i < drag_elements.length; i++){
3152 let drag_element = drag_elements[i];
3153 drag_element.innerHTML = "&#x02193;";
3154 }
3155 }else{
3156 this.classList.add("increment-toggle");
3157 this.innerHTML = "Disable Increment on Drag";
3158 grid_reference.drag_increment_mode = !grid_reference.drag_increment_mode;
3159 for(let i = 0; i < drag_elements.length; i++){
3160 let drag_element = drag_elements[i];
3161 drag_element.innerHTML = "&#x002B;";
3162 }
3163 }
3164 }
3165 // Filter the loaded json from the back, sets component variable GLOBAL_JSON_RESULT_FILTERED, and returns a copy of the filtered set.
3166 // Not to be called from the template
3167 manual_filter_json(grid_id, column_name, filter_type, filter_value, do_not_call_create_table){
3168 let table_element = window.gridReference.get(this.id).shadowRoot;
3169 document.body.style.cursor = "wait";
3170 /*Copy the JSON from the global object */
3171 let json_data = Array.from(this.GLOBAL_JSON_RESULT);
3172 //If re-loading filters that were pre-set or saved from before then only apply it to the grid that matches the saved filter data/records
3173 if(!do_not_call_create_table){
3174 try{
3175 //Push this new filter data into the filters JSON blob
3176 this.grid_filters.push({grid_id:grid_id, column_name:column_name, filter_type:filter_type, filter_value:filter_value});
3177 //Call the save filters function so that we can save these filters
3178 this.save_filters(grid_id);
3179 }catch(error){
3180 console.log(error);
3181 }
3182 }
3183 //Applying all filters that had been saved and were loaded back in
3184 for (let gf of this.grid_filters) {
3185 //Make sure we are applying filters to the correct grid, skip each filter that doesn't match the grid we are on
3186 if(grid_id != gf.grid_id){
3187 continue;
3188 }
3189
3190 column_name = gf.column_name;
3191
3192 filter_type = gf.filter_type;
3193
3194 //Lowercase for better match abilities
3195 filter_value = gf.filter_value.toLowerCase();
3196
3197 //Remove starting and trailing spaces
3198 filter_value = filter_value.trim();
3199 for (let i=0; i<json_data.length; i++){
3200 if(!Object.keys(json_data[i]).includes(column_name)){
3201 continue;
3202 }
3203 //Lowercase for better match abilities
3204 //Account for empty values
3205 let column_data;
3206 if(json_data[i][column_name]){
3207 column_data = json_data[i][column_name].toLowerCase();
3208 }else{
3209 column_data = '';
3210 }
3211 //Remove starting and trailing spaces
3212 column_data = column_data.trim();
3213 //Regex data to search for
3214 let re = new RegExp(filter_value);
3215 if(filter_type){
3216 if(filter_type == '=' && column_data == filter_value){
3217 //console.log('=');
3218 //console.log('MATCH FOUND:(' + column_data + ')');
3219
3220 }
3221 else if(filter_type == 'in' && ((Number(column_data) >= Number(filter_value.split(',')[0]) || (Number(column_data) <= Number(filter_value.split(',')[1]))))){
3222 //console.log('<');
3223 //console.log('MATCH FOUND:(' + column_data + ')');
3224
3225 }
3226 else if(filter_type == 'notin' && ((Number(column_data) <= Number(filter_value.split(',')[0]) || (Number(column_data) >= Number(filter_value.split(',')[1]))))){
3227
3228 //console.log('<');
3229 //console.log('MATCH FOUND:(' + column_data + ')');
3230
3231 }
3232 else if(filter_type == '<' && Number(column_data) < Number(filter_value)){
3233 //console.log('<');
3234 //console.log('MATCH FOUND:(' + column_data + ')');
3235
3236 }else if(filter_type == '>' && Number(column_data) > Number(filter_value)){
3237 //console.log('>');
3238 //console.log('MATCH FOUND:(' + column_data + ')');
3239
3240 }else if(filter_type == '<=' && Number(column_data) <= Number(filter_value)){
3241 //console.log('<=');
3242 //console.log('MATCH FOUND:(' + column_data + ')');
3243
3244 }else if(filter_type == '>=' && Number(column_data) >= Number(filter_value)){
3245 //console.log('>=');
3246 //console.log('MATCH FOUND:(' + column_data + ')');
3247
3248 }else if(filter_type == 'like' && re.test(column_data) == true){
3249 //console.log('like');
3250 //console.log('MATCH FOUND:(' + column_data + ')');
3251
3252 }else if(filter_type == '!like' && re.test(column_data) == false){
3253 //console.log('!like');
3254 //console.log('MATCH FOUND:(' + column_data + ')');
3255
3256 }else if(filter_type == '!=' && column_data != filter_value){
3257 //console.log('!=');
3258 //console.log('MATCH FOUND:(' + column_data + ')');
3259
3260 }else{
3261 //Remove this row of data
3262 //console.log('Removed row containing non match on' + column_name + ':(' + json_data[i][column_name][0] + ')');
3263 let removed_element = json_data.splice(i, 1);
3264 i--;
3265 }
3266 }
3267 }
3268 }
3269 // console.log(json_data);
3270 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(json_data);
3271 //Re-build the table if we didn't specify not to, as long as this was called from someone clicking on filters it is ok to call create_table but not if it is in a loop being auto loaded from prior data, this would create endless loops
3272 if(!do_not_call_create_table){
3273 this.create_table(grid_id, column_name, 0);
3274 }
3275 //Return the filtered out/cleaned json_data
3276
3277 return(json_data);
3278 }
3279 filter_json(grid_id, column_name, filter_type, filter_value, do_not_call_create_table){
3280 let table_element = window.gridReference.get(this.id).shadowRoot;
3281
3282
3283
3284
3285 document.body.style.cursor = "wait";
3286 /*Copy the JSON from the global object */
3287 let json_data = Array.from(this.GLOBAL_JSON_RESULT);
3288 let filter_lookup = true;
3289 if(filter_type === 'like'){
3290 filter_lookup = false;
3291 }
3292 //If re-loading filters that were pre-set or saved from before then only apply it to the grid that matches the saved filter data/records
3293 if(!do_not_call_create_table){
3294 try{
3295 if(filter_lookup){
3296 filter_type = table_element.getElementById(filter_type).value;
3297 filter_value = table_element.getElementById(filter_value).value;
3298 // REMOVE THIS TO GO BACK TO NORMAL -----------------------------------------------------------------------------------------------------------------------
3299 // Some grid templates have differing values for key-value pairs, most are x:x but some are x:y, x being the actually
3300 // we have to do some renaming for display purposes, displaying Y, but processing and using X for the backend.
3301 if(column_name){
3302 let grid_component = table_element.host;
3303 let column_type = grid_component.column_information[column_name]["type"];
3304 if(column_type == "select"){
3305 let option_mapping = grid_component.column_information[column_name]["options"];
3306 let filter_value_key_map = Object.keys(option_mapping).find(key => option_mapping[key] === filter_value);
3307 filter_value = filter_value_key_map;
3308
3309 }
3310 }
3311 // REMOVE THIS TO GO BACK TO NORMAL -----------------------------------------------------------------------------------------------------------------------
3312 }
3313 //Push this new filter data into the filters JSON blob
3314 this.grid_filters.push({grid_id:grid_id, column_name:column_name, filter_type:filter_type, filter_value:filter_value});
3315
3316 //Call the save filters function so that we can save these filters
3317 this.save_filters(grid_id);
3318 }catch(error){
3319 console.log(error);
3320 }
3321 }
3322 //Applying all filters that had been saved and were loaded back in
3323 for (let gf of this.grid_filters) {
3324 //Make sure we are applying filters to the correct grid, skip each filter that doesn't match the grid we are on
3325 if(grid_id != gf.grid_id){
3326 continue;
3327 }
3328
3329 column_name = gf.column_name;
3330
3331 filter_type = gf.filter_type;
3332
3333 //Lowercase for better match abilities
3334 filter_value = gf.filter_value.toLowerCase();
3335
3336 //Remove starting and trailing spaces
3337 filter_value = filter_value.trim();
3338 for (let i=0; i<json_data.length; i++){
3339 if(!Object.keys(json_data[i]).includes(column_name)){
3340 continue;
3341 }
3342 //Lowercase for better match abilities
3343 //Account for empty values
3344 let column_data;
3345 if(json_data[i][column_name]){
3346 column_data = json_data[i][column_name].toLowerCase();
3347 if(column_data.includes('<a href=')){
3348 const regex = /contenteditable="plaintext-only"\>(.*)\<\/a\>/m;
3349 let re_match = column_data.match(regex);
3350
3351 // get the first match group, which is the capturing group of the anchor tag
3352 // Some grids inject links into the html, and NOT into a content editable div, HANDLE IT
3353 // If we have a match, it's a contenteditable field, if we don't, it's not.
3354 if(re_match){
3355 column_data = re_match[1];
3356 }
3357 }
3358 }else{
3359 column_data = '';
3360 }
3361 //Remove starting and trailing spaces
3362 column_data = column_data.trim();
3363 //Regex data to search for
3364 let re = new RegExp(filter_value);
3365 if(filter_type){
3366 if(filter_type == '=' && column_data == filter_value){
3367 //console.log('=');
3368 //console.log('MATCH FOUND:(' + column_data + ')');
3369
3370 }
3371 else if(filter_type == 'in' && ((Number(column_data) >= Number(filter_value.split(',')[0]) || (Number(column_data) <= Number(filter_value.split(',')[1]))))){
3372 console.log("in");
3373 //console.log('<');
3374 //console.log('MATCH FOUND:(' + column_data + ')');
3375
3376 }
3377 else if(filter_type == 'notin' && ((Number(column_data) <= Number(filter_value.split(',')[0]) || (Number(column_data) >= Number(filter_value.split(',')[1]))))){
3378 console.log("notin");
3379 console.log(Number(column_data));
3380 //console.log('<');
3381 //console.log('MATCH FOUND:(' + column_data + ')');
3382
3383 }
3384 else if(filter_type == '<' && Number(column_data) < Number(filter_value)){
3385 //console.log('<');
3386 //console.log('MATCH FOUND:(' + column_data + ')');
3387
3388 }else if(filter_type == '>' && Number(column_data) > Number(filter_value)){
3389 //console.log('>');
3390 //console.log('MATCH FOUND:(' + column_data + ')');
3391
3392 }else if(filter_type == '<=' && Number(column_data) <= Number(filter_value)){
3393 //console.log('<=');
3394 //console.log('MATCH FOUND:(' + column_data + ')');
3395
3396 }else if(filter_type == '>=' && Number(column_data) >= Number(filter_value)){
3397 //console.log('>=');
3398 //console.log('MATCH FOUND:(' + column_data + ')');
3399
3400 }else if(filter_type == 'like' && re.test(column_data) == true){
3401 //console.log('like');
3402 //console.log('MATCH FOUND:(' + column_data + ')');
3403
3404 }else if(filter_type == '!like' && re.test(column_data) == false){
3405 //console.log('!like');
3406 //console.log('MATCH FOUND:(' + column_data + ')');
3407
3408 }else if(filter_type == '!=' && column_data != filter_value){
3409 //console.log('!=');
3410 //console.log('MATCH FOUND:(' + column_data + ')');
3411
3412 }else{
3413 //Remove this row of data
3414 //console.log('Removed row containing non match on' + column_name + ':(' + json_data[i][column_name][0] + ')');
3415 let removed_element = json_data.splice(i, 1);
3416 i--;
3417 }
3418 }
3419 }
3420 }
3421 //Re-build the table if we didn't specify not to, as long as this was called from someone clicking on filters it is ok to call create_table but not if it is in a loop being auto loaded from prior data, this would create endless loops
3422 if(!do_not_call_create_table){
3423 this.create_table(grid_id, column_name, 0);
3424 }
3425 //Return the filtered out/cleaned json_data
3426 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(json_data);
3427 return(json_data);
3428 }
3429 // Generate a list of quick select options for filtering, based on number of distinct values
3430 get_distinct_filter_values(e, source, search_value=''){
3431 let grid_component = source.getRootNode().host;
3432 let column_name = source.closest('th').title;
3433 let column_type = grid_component.column_information[column_name]["type"];
3434
3435 // console.log(grid_component.column_information[this.COLUMN_NAME_ORDER[j]]['options']);
3436 let filter_box = source.parentElement.parentElement;
3437 let target_id = source.id;
3438 let source_dimensions = source.getBoundingClientRect();
3439 let source_x = source_dimensions.x;
3440 let source_y = source_dimensions.y;
3441 // Below gets all distinct values from the json result.
3442 // For this column, search through the array of key/value stores ([{'key': value, 'key2': value2}, {'key': value, 'key2': value2}])
3443 // Pull out all values related to this columns key, so arr[i][column_name], convert to Set, to get distinct values.
3444 if(grid_component.shadowRoot.getElementById(`${grid_component.id}_${column_name}_filter_suggestion`)){
3445 grid_component.shadowRoot.getElementById(`${grid_component.id}_${column_name}_filter_suggestion`).remove();
3446 }
3447 let distinct_col_values = [...new Set(grid_component.GLOBAL_JSON_RESULT.map(item => item[column_name]))];
3448 // REMOVE THIS TO GO BACK TO NORMAL -----------------------------------------------------------------------------------------------------------------------
3449 // Some grid templates have differing values for key-value pairs, most are x:x but some are x:y, x being the actually
3450 // we have to do some renaming for display purposes, displaying Y, but processing and using X for the backend.
3451 if(column_type == "select"){
3452 let option_mapping = grid_component.column_information[column_name]["options"];
3453 for(let i = 0; i < distinct_col_values.length; i++){
3454 distinct_col_values[i] = option_mapping[distinct_col_values[i]];
3455 }
3456
3457 }
3458 // REMOVE THIS TO GO BACK TO NORMAL -----------------------------------------------------------------------------------------------------------------------
3459
3460 // Remove any "falsy" values, IE NaN, '', null, undefined, etc.
3461 distinct_col_values = distinct_col_values.filter(value=>value);
3462 if(search_value){
3463 distinct_col_values = distinct_col_values.filter((value)=> {return value.toLowerCase().includes(search_value.toLowerCase());});
3464 }
3465
3466 distinct_col_values.sort();
3467 if(!grid_component.shadowRoot.getElementById(`${grid_component.id}_${column_name}_filter_suggestion`)){
3468 // if(distinct_col_values.length <= 10 && !grid_component.shadowRoot.getElementById(`${grid_component.id}_${column_name}_filter_suggestion`)){
3469 let suggestion_popup = document.createElement('div');
3470 suggestion_popup.id = `${grid_component.id}_${column_name}_filter_suggestion`;
3471 suggestion_popup.classList.add('suggestion-popup');
3472 suggestion_popup.style.top = `${source_y}px`;
3473 suggestion_popup.style.left = `${source_x + 90}px`;
3474
3475 let suggestion_title = document.createElement('p');
3476 suggestion_title.innerText = '-Unique Values-'
3477 suggestion_title.classList.add('suggestion-title');
3478
3479 let suggestion_container = document.createElement('div');
3480 suggestion_container.classList.add('suggestion-container');
3481
3482 suggestion_popup.appendChild(suggestion_title);
3483 suggestion_popup.appendChild(suggestion_container);
3484 for(let i = 0; i < distinct_col_values.length; i++){
3485 if(distinct_col_values[i]){
3486 let suggestion = document.createElement('div');
3487 // Checking for hyperlinked values
3488 if(distinct_col_values[i].includes('<a href="') || distinct_col_values[i].includes("<a href='")){
3489 // distinct_col_values[i].replace('<br>', '');
3490 let regex = /contenteditable="plaintext-only"\>(.*)\<\/a\>/m;
3491 let re_match = distinct_col_values[i].match(regex);
3492 // get the first match group, which is the capturing group of the anchor tag
3493 if(!re_match){
3494 regex = /\>(.*)\<\/a\>/m;
3495 re_match = distinct_col_values[i].match(regex);
3496 }
3497 suggestion.innerHTML = re_match[1];
3498 suggestion.classList.add('suggestion-link');
3499 suggestion.style.color = '#336699';
3500 // suggestion.innerText = ;
3501 }else{
3502 suggestion.innerText = distinct_col_values[i];
3503 }
3504 suggestion.onclick = (e) => {
3505 source.value = e.target.innerText;
3506 suggestion_popup.remove();
3507 }
3508 suggestion.classList.add('suggestion-item');
3509 suggestion_container.appendChild(suggestion);
3510 }
3511 }
3512 filter_box.appendChild(suggestion_popup);
3513 }
3514 }
3515 // Increase or decrease the number of displayed rows.
3516 // Not to be called from the template
3517 // Deprecated Function
3518 update_max_results(grid_id, column_name){
3519 document.body.style.cursor = "wait";
3520 //Get the current max rows to display for this particular grid
3521 // let dropdown_value = this.shadowRoot.getElementById(`${grid_id}_max_results`).value;
3522
3523 //Re-build the table
3524 this.create_table(grid_id, column_name, 0);
3525 }
3526 // Remove filters from the grid, and clear them out from the backend
3527 // Not to be called from the template
3528 clear_filters(grid_id){
3529 document.body.style.cursor = "wait";
3530 //Loop through all of the filters, if we find a matching grid_id then wipe that tables filters
3531 let row_num = 0;
3532 for (let gf of this.grid_filters) {
3533 //Compare the ids, then make sure that we only remove filters by making sure there is a column name next to the id
3534 if(grid_id == gf.grid_id && gf.column_name){
3535 this.grid_filters.splice(row_num, 1);
3536
3537 this.clear_filters(grid_id);
3538 }else{
3539 //Only increment if not found so that we keep in sync with not incrementing when we delete rows
3540 row_num++;
3541 }
3542 }
3543
3544 //Call the save filters function so that we can save these filters using AJAX to a DB
3545
3546 this.save_filters(grid_id, 'clear_filters');
3547 // Set GLOBAL_JSON_RESULT_FILTERED back to the original unfiltered data.
3548
3549 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
3550
3551 //The empty quotes are the column name to sort on, it is blank by default but if you always want to sort by default on a column then add it into these quotes
3552 this.create_table(grid_id, '', 0);
3553 }
3554 // Remove individual filter and clear it out from the backend
3555 // Not to be called from the template
3556 remove_filter(grid_id, column_name, filter_type, filter_value){
3557 document.body.style.cursor = "wait";
3558 if(this.shadowRoot.querySelector('.no_data_container')){
3559 this.shadowRoot.querySelector('.no_data_container').remove();
3560 }
3561 //Loop through all of the filters, if we find a matching grid_id then wipe that tables filters
3562 let row_num = 0;
3563 for (let gf of this.grid_filters) {
3564 //Compare the ids, then make sure that we only remove filters by making sure there is a column name next to the id
3565 if(grid_id == gf.grid_id && column_name == gf.column_name && filter_type == gf.filter_type && filter_value == gf.filter_value){
3566 this.grid_filters.splice(row_num, 1);
3567 }else{
3568 //Only increment if not found so that we keep in sync with not incrementing when we delete rows
3569 row_num++;
3570 }
3571 }
3572 //Call the save filters function so that we can save these filters using AJAX to a DB
3573 this.save_filters(grid_id, 'clear_filters');
3574 //The empty quotes are the column name to sort on, it is blank by default but if you always want to sort by default on a column then add it into these quotes
3575 this.create_table(grid_id, '', 0);
3576 }
3577 // Save the active filters on the grid so they are active when the grid is reloaded for that user.
3578 // Not to be called from the template
3579 save_filters(grid_id, clear_filters){
3580 //Loop through all of the filters, if we find a matching grid_id then wipe that tables filters
3581 let json_exists = 0;
3582 let grid_json = '';
3583 grid_json += '[';
3584 for (let gf of this.grid_filters) {
3585 //Loop through the fiters and convert them to JSON for saving to the DB, ingore any that are not filters
3586 if(gf.grid_id && gf.column_name){
3587 //Add a comma if this is not the first row
3588 if(json_exists > 0){
3589 grid_json += ',';
3590 }
3591
3592 //Save the JSON for these
3593 grid_json += `{"grid_id": "${gf.grid_id}", "column_name": "${gf.column_name}", "filter_type": "${gf.filter_type}", "filter_value": "${gf.filter_value}"}`;
3594
3595 //Mark that we have created some JSON and it now exists
3596 json_exists++;
3597 }else{
3598 //Nothing matched, ignore
3599 }
3600 }
3601 grid_json += ']';
3602 // Grid ID is always passed with the filters, even if they're empty.
3603 //Call the URL to save the filters
3604 try{
3605 //Check if there is any text other than square brackets
3606 let matches = grid_json.match(/(\d|\w)+/i);
3607 if(this.save_filters_url){
3608 if(matches){
3609 //Actual JSON found not just empty brackets, save this data
3610 //console.log(grid_json);
3611 //this.get_request(this.save_filters_url + encodeURIComponent(grid_json) + '&grid_id=' + grid_id + ).then(response=>{console.log(response)});
3612 let params = `&grid_id=${this.id}&filters=${encodeURIComponent(grid_json)}`;
3613 this.get_request(this.save_filters_url + params).then(response=>{console.log(response)});
3614 }else if(clear_filters){
3615 //The clear filters function is what called this function so it is ok to update the filters without JSON in this instance
3616 //console.log(grid_json);
3617 let params = `&grid_id=${this.id}&filters=${encodeURIComponent(grid_json)}`;
3618 this.get_request(this.save_filters_url + params).then(response=>{console.log(response)});
3619 }else{
3620 //No JSON found, do nothing
3621 }
3622 }
3623 }
3624 catch(error){
3625 console.log(error);
3626 }
3627 }
3628 //! Micah Edit
3629 check_row_highlight(e){
3630 console.log("run it")
3631 //Call the URL to save the check preference
3632 try{
3633 console.log(this);
3634 let grid_root = this.getRootNode();
3635 let grid_component = grid_root.host;
3636 console.log(grid_root);
3637 console.log(grid_component);
3638 console.log(grid_component.check_highlight_row_url);
3639 // "this" reference in this case refers to the input element, not the grid.
3640 // I should make that more clear and standardize the references to the grid_root/grid_component method.
3641 // -Anthony
3642 // if(this.check_highlight_row_url){
3643 // this.get_request(this.check_highlight_row_url).then(response=>{console.log(response)});
3644 // }
3645 if(grid_component.check_highlight_row_url){
3646 console.log("On Click Succeeding to here");
3647 grid_component.get_request(grid_component.check_highlight_row_url).then(response=>{console.log(response); window.location.reload();});
3648 }
3649 }
3650 catch(error){
3651 console.log(error);
3652 }
3653 }
3654 //!
3655
3656
3657 // Creates the resizer divs that let the user change the width of the column after initial load.
3658 // Does not get saved anywhere.
3659 // Not to be called from the template
3660 load_resizing_code(grid_id){
3661 //nested functions can't callback to the class "this", so we create a local reference to it
3662 let this_reference = this;
3663 const createResizableTable = function (table){
3664 const cols = table.querySelectorAll('th');
3665 let column_id =0;
3666 [].forEach.call(cols, function (col){
3667 //It appears that without this added check, every mouse movement on the table creates hundreds of resizer divs
3668 //It appears to be unnecessary to have more than 1 per column.
3669 if(col.querySelectorAll(".resizer").length < 1){
3670 // Add a resizer element to the column
3671 const resizer = document.createElement('div');
3672 resizer.classList.add('resizer');
3673
3674 //Set the height
3675 // if(table.offsetHeight < 1){
3676 // let displayed_rows = Number(this_reference.shadowRoot.getElementById(`${this_reference.id}_max_results`).value);
3677 // resizer.style.height = `${(displayed_rows * 35) + 45}px`
3678 // }else{
3679 // let table_height = table.getBoundingClientRect()['height'];
3680 // // resizer.style.height = `${table.offsetHeight}px`;
3681 // resizer.style.height = `${table_height}px`;
3682
3683 // }
3684
3685 col.appendChild(resizer);
3686
3687 createResizableColumn(col, resizer);
3688 //Add an id to each column so that we can track which one we resized and save that data later if needed
3689 col.setAttribute('id', `${grid_id}_column_${column_id}`);
3690
3691 column_id++;
3692 }
3693 });
3694 };
3695
3696 const createResizableColumn = function (col, resizer){
3697 let x = 0;
3698 let w = 0;
3699 let previous_col_width = 0;
3700 let super_header;
3701 let super_header_row;
3702 let row_old_width;
3703 let header_old_width;
3704
3705
3706 const mouseDownHandler = function (e){
3707 x = e.clientX;
3708 if(col.getAttribute("super-header-owner")){
3709 super_header = this_reference.shadowRoot.getElementById(col.getAttribute("super-header-owner"));
3710 super_header_row = super_header.parentElement;
3711 row_old_width = Number(super_header_row.style.width.replace("px",''));
3712 header_old_width = Number(super_header.style.width.replace("px",''));
3713 // super_header_row.style.width = `${row_old_width + dx}px`;
3714 // super_header.style.width = `${header_old_width + dx}px`;
3715 }
3716 let styles = window.getComputedStyle(col);
3717
3718 w = parseInt(styles.width, 10);
3719
3720 this_reference.shadowRoot.addEventListener('mousemove', mouseMoveHandler);
3721 this_reference.shadowRoot.addEventListener('mouseup', mouseUpHandler);
3722
3723 resizer.classList.add('resizing');
3724 };
3725
3726 const mouseMoveHandler = function (e){
3727 const dx = e.clientX - x;
3728 let new_width = w+dx;
3729 // Set the new width of the table header and table, as well as updating the json styles
3730 if(this_reference.configuration["column_information"][col.title]["min_width"]){
3731 if((new_width) >= this_reference.configuration["column_information"][col.title]["min_width"]){
3732 if(col.getAttribute("super-header-owner")){
3733 super_header_row.style.width = `${row_old_width + dx}px`;
3734 super_header.style.width = `${header_old_width + dx}px`;
3735 }
3736 col.style.width = `${new_width}px`;
3737 }
3738 }else{
3739 if((new_width) >= 10){
3740 if(col.getAttribute("super-header-owner")){
3741 super_header_row.style.width = `${row_old_width + dx}px`;
3742 super_header.style.width = `${header_old_width + dx}px`;
3743 }
3744 col.style.width = `${new_width}px`;
3745 }
3746 }
3747
3748 };
3749
3750 const mouseUpHandler = function (){
3751 previous_col_width = 0;
3752 resizer.classList.remove('resizing');
3753
3754 this_reference.shadowRoot.removeEventListener('mousemove', mouseMoveHandler);
3755 this_reference.shadowRoot.removeEventListener('mouseup', mouseUpHandler);
3756
3757 //Save the width id and width object data ----------------------------------------
3758 const width_data = this_reference.grid_widths.find(data => data.id === col.id);
3759 try{
3760 //Cause an error if the object has no data
3761 if(width_data.id){
3762 console.log("FOUND: " + width_data.id);
3763 }
3764
3765 let found = 0;
3766 for (let i of this_reference.grid_widths) {
3767 if(i.id == col.id){
3768 //Update the existing record
3769 i.width = col.style.width;
3770 found = 1;
3771 }
3772 }
3773
3774 //If not found then add a new record
3775 if(found == 0){
3776
3777 this_reference.grid_widths.push({id:col.id, width:col.style.width});
3778 }
3779 }catch(err){
3780 //The object had no data, save the data to the object
3781 this_reference.grid_widths.push({id:col.id, width:col.style.width});
3782 }
3783 };
3784
3785 resizer.addEventListener('mousedown', mouseDownHandler);
3786 };
3787 createResizableTable(this_reference.shadowRoot.getElementById(`${grid_id}_table`));
3788 }
3789 // Sort the json either ascending or descending, blank fields always come last in sort order, no matter the order
3790 // Not to be called from the template
3791 sort_json(grid_id, column_name, sort_direction, start_results_at){
3792 this.current_sorting = sort_direction;
3793 document.body.style.cursor = "wait";
3794 //Retrieve the filtered json to be sorted.
3795 let json_data = Array.from(this.GLOBAL_JSON_RESULT);
3796
3797 let reA = /[^a-zA-Z]/g;
3798 let reN = /[^0-9]/g;
3799 json_data.sort( function( a, b ){
3800 // console.log(b);
3801 // Error handling for NULL data
3802 if(a[column_name]){
3803 a = a[column_name].toLowerCase();
3804 }else{
3805 a = ""
3806 }
3807 if(b[column_name]){
3808 b = b[column_name].toLowerCase();
3809 }else{
3810 b = ""
3811 }
3812 // This section is to handle always putting empty fields LAST. Invert the signs (negative/positive) on the 1's to invert the behavior.
3813 if(sort_direction == 'asc'){
3814 if(a === ""){
3815 if(b === ""){
3816 return 0;
3817 }else{
3818 return 1
3819 }
3820 }else{
3821 if(b === ""){
3822 return -1;
3823 }
3824 }
3825 }else{
3826 if(a === ""){
3827 if(b === ""){
3828 return 0;
3829 }else{
3830 return -1;
3831 }
3832 }else{
3833 if(b === ""){
3834 return 1;
3835 }
3836 }
3837 }
3838
3839 let aA = a.replace(reA, "");
3840 let bA = b.replace(reA, "");
3841 if(aA === bA){
3842 let aN = parseFloat(a);
3843 let bN = parseFloat(b);
3844 return aN === bN ? 0 : aN > bN ? 1 : -1;
3845 }else{
3846 return aA > bA ? 1 : -1;
3847 }
3848 });
3849
3850 if(sort_direction == 'desc'){
3851 json_data.reverse();
3852 }
3853 //Reassign the json
3854 this.GLOBAL_JSON_RESULT = Array.from(json_data);
3855 //this.GLOBAL_JSON_RESULT_FILTERED = Array.from(json_data);
3856 this.create_table(grid_id, column_name, start_results_at);
3857 }
3858 // Used by the context menu, this deletes 1 row from the backend and reloads the grid.
3859 // Not to be called from the template
3860 delete_row(id){
3861 //Get the index of the displayed results, based on the table row id (which is it's index in the displayed results, filtered or unfiltered).
3862 let data_index = Number(id.replace("row", ""));
3863 let removed_entry;
3864 //If the results and filtered results are not the same length, we are actively filtering the data, and the row positions will be different.
3865 if(this.GLOBAL_JSON_RESULT.length != this.GLOBAL_JSON_RESULT_FILTERED.length){
3866 //Using the index/rowID of the displayed data to remove it. Save the resulting array of json [{}] and retrieve the singular element
3867 removed_entry = this.GLOBAL_JSON_RESULT_FILTERED.splice(data_index, 1);
3868 //Get the index before removing it from the JSON
3869 this.recent_deletion_index = this.GLOBAL_JSON_RESULT.indexOf(removed_entry)
3870 //Remove the element from the unfiltered JSON
3871 this.GLOBAL_JSON_RESULT.splice(this.GLOBAL_JSON_RESULT.indexOf(removed_entry), 1);
3872
3873 this.recent_deletion = removed_entry;
3874
3875 this.recent_deletion_filtered_index = data_index;
3876 }else{
3877 //Unfiltered results are being displayed, row positions will line up exactly.
3878 removed_entry = this.GLOBAL_JSON_RESULT.splice(data_index, 1);
3879 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
3880
3881 this.recent_deletion = removed_entry;
3882 this.recent_deletion_index = data_index;
3883 this.recent_deletion_filtered_index = -1;
3884 }
3885 this.modified_data["action"] = "delete";
3886 console.log(removed_entry);
3887 // TODO: This seems like a temporary measure, but we're testing it.
3888 this.modified_data["source_data"] = removed_entry[0];
3889 // Reload the table, to properly recreate pagination
3890 try{
3891 console.log("Attempting to save data.");
3892 this.save_data(`grid-${this.grid_name}`, Number(this.shadowRoot.querySelector('.pagination_selected_link').innerHTML), 'delete');
3893 }catch(error){
3894 console.log("Deleted data not saved.");
3895 console.log(error);
3896 }
3897 this.undo_delete_check = true;
3898 }
3899 // Undo the most recently deleted row, DOES NOT INTERACT WITH THE UNDO BUTTON
3900 // Not to be called from the template
3901 undo_delete_row(pagination_number){
3902 // Actively filtering the data, recover the deleted row into the filtered dataset and display table.
3903 // Are we looking at filtered or unfiltered data
3904 if(this.GLOBAL_JSON_RESULT_FILTERED.length != this.GLOBAL_JSON_RESULT){
3905 if(this.undo_delete_check){
3906 this.modified_data["action"] = 'undo';
3907 console.log(this.recent_deletion);
3908 this.modified_data["source_data"] = this.recent_deletion[0];
3909 this.GLOBAL_JSON_RESULT_FILTERED.splice(this.recent_deletion_filtered_index, 0, this.recent_deletion);
3910 this.GLOBAL_JSON_RESULT.splice(this.recent_deletion_index, 0, this.recent_deletion);
3911 this.save_data(`grid-${this.grid_name}`, pagination_number, 'undo');
3912 }else{
3913 window.appToast && window.appToast({ message: 'Nothing to Undo. History only stores 1 record.', type: 'warning' });
3914 }
3915 }else{
3916 if(this.undo_delete_check){
3917 this.modified_data["action"] = 'undo';
3918 console.log(this.recent_deletion);
3919 this.modified_data["source_data"] = this.recent_deletion[0];
3920 this.GLOBAL_JSON_RESULT.splice(this.recent_deletion_index, 0, this.recent_deletion);
3921 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
3922 this.save_data(`grid-${this.grid_name}`, pagination_number, 'undo');
3923 }else{
3924 window.appToast && window.appToast({ message: 'Nothing to Undo. History only stores 1 record.', type: 'warning' });
3925 }
3926 }
3927 this.undo_delete_check = false;
3928 }
3929 // Function used to create our custom context menu for copying, deleting, or adding comments.
3930 // Not to be called from the template
3931 right_click_row(e){
3932 // "this" refers to the element that the right click is bound to, NOT THE COMPONENT.
3933 // "this_reference" DOES REFER TO THE COMPONENT;
3934 // Prevent default behavior (in this case, intercepting the context menu on right click)
3935 e.preventDefault();
3936 // Not always guaranteed to have right clicked the table cell element itself, because we allow
3937 // html to come in the json/populate the cells. Loop until the table cell is retrieved.
3938 let active_cell = e.target;
3939 if(active_cell.nodeName != 'TD'){
3940 let cell_retrieved = false;
3941 while(!cell_retrieved){
3942 active_cell = active_cell.parentElement;
3943 if(active_cell.nodeName == 'TD'){
3944 cell_retrieved=true;
3945 }
3946 }
3947 }
3948 console.log(active_cell);
3949 //Obtain grid ID from cell title.
3950 let grid_id = active_cell.title;
3951 //Obtain component reference from the gridReference map.
3952 let this_reference = window.gridReference.get(grid_id);
3953 this_reference.right_click_source_cell = active_cell;
3954 //Obtain the header key-name from the cell, to retrieve the data from the JSON, not the DOM element.
3955 let header_key = active_cell.headers;
3956 //Obtain the ID from the row
3957 let id = this.id;
3958 let pagination_number = Number(this_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML);
3959 let context_menu_container = document.createElement('div');
3960 // Populate the context menu with the default options of Delete Row, Undo Delete, and Copy Value
3961 context_menu_container.innerHTML = `<a href='javascript:' onClick="this.parentElement.remove(); window.gridReference.get('${grid_id}').show_delete_confirmation('${id}', '${grid_id}');"><i class=''></i> Delete Row </a></hr>`;
3962 context_menu_container.innerHTML += `<a href='javascript:' onClick="this.parentElement.remove(); window.gridReference.get('${grid_id}').undo_delete_row(${pagination_number});"><i class=''></i> Undo Delete</a></hr>`;
3963 context_menu_container.innerHTML += `<a href='javascript:' onClick="window.gridReference.get('${grid_id}').copy_cell_value('${id}', '${header_key}'); this.parentElement.remove();"><i class=''></i> Copy Cell Value</a></hr>`;
3964 context_menu_container.innerHTML += `<a href='javascript:' onClick="window.gridReference.get('${grid_id}').insert_row('${grid_id}', -1); this.parentElement.remove();"><i class=''></i> Insert Row Before</a></hr>`;
3965 context_menu_container.innerHTML += `<a href='javascript:' onClick="window.gridReference.get('${grid_id}').insert_row('${grid_id}', 0); this.parentElement.remove();"><i class=''></i> Insert Row After</a></hr>`;
3966
3967 //Section for inserting links into notes fields
3968 let inner_field_element = active_cell.children[0];
3969 if(inner_field_element.contentEditable == 'plaintext-only'){
3970 context_menu_container.innerHTML += `<a href='javascript:' onClick="window.gridReference.get('${grid_id}').hyperlink_handler(event); this.parentElement.remove();"><i class=''></i> Add/Edit Hyperlink</a></hr>`;
3971 context_menu_container.innerHTML += `<a href='javascript:' onClick="window.gridReference.get('${grid_id}').remove_anchor_tag(event); this.parentElement.remove();"><i class=''></i> Remove Hyperlink</a></hr>`;
3972 }
3973
3974 //This is how the injected context menu options are set. context_menu_injection is a the provided html string, or defaults to empty.
3975 context_menu_container.innerHTML += this_reference.context_menu_injection;
3976
3977 context_menu_container.classList.add('context-menu');
3978 let bounds = this_reference.getBoundingClientRect();
3979 context_menu_container.style = `position: absolute; left: ${e.clientX - 5 - bounds.left}px; top: ${e.clientY + 25 - bounds.top}px; z-index:10000`;
3980 context_menu_container.onmouseleave = (e) => {e.target.remove();};
3981 this_reference.shadowRoot.prepend(context_menu_container);
3982 // this_reference.addEventListener('mouseleave', this_reference.clear_listeners);
3983 }
3984 hyperlink_handler(e){
3985
3986 let grid_root = this.shadowRoot;
3987 let grid_component = grid_root.host;
3988 let allow_multi_links = this.multiple_links;
3989 const selection = grid_root.getSelection();
3990 console.log(selection);
3991 let selection_type = selection.type;
3992
3993 let hyperlink_form = document.createElement('div');
3994 hyperlink_form.id = 'hyperlink-form';
3995 hyperlink_form.classList.add('hyperlink-form-popup');
3996 // Create the div to contain the title and input field
3997 let display_text_container = document.createElement('div');
3998 display_text_container.classList.add('field-holder');
3999 let display_title_container = document.createElement('p');
4000 display_title_container.innerHTML = 'Display Text';
4001 let display_input_field = document.createElement('input');
4002 display_input_field.type="text";
4003 display_input_field.id = "display-field";
4004 //Building inner component
4005 display_text_container.appendChild(display_title_container);
4006 display_text_container.appendChild(display_input_field);
4007 // Create the div to contain the title and link field
4008 let link_field_container = document.createElement('div');
4009 link_field_container.classList.add('field-holder');
4010 let link_title_container = document.createElement('p');
4011 link_title_container.innerHTML = "Hyperlink";
4012 let link_input_field = document.createElement('input');
4013 link_input_field.type = "text";
4014 link_input_field.id = "link-field";
4015 //Building inner component
4016 link_field_container.appendChild(link_title_container);
4017 link_field_container.appendChild(link_input_field);
4018 //Build the buttons to finish out the form
4019 let add_hyperlink_button = document.createElement('button');
4020 let edit_hyperlink_button = document.createElement('button');
4021 let cancel_hyperlink_button = document.createElement('button');
4022 add_hyperlink_button.innerText = 'Add Hyperlink'
4023 add_hyperlink_button.id = 'create-link-button';
4024 add_hyperlink_button.style.display = "none";
4025 edit_hyperlink_button.innerText = 'Update Hyperlink'
4026 edit_hyperlink_button.id = 'edit-link-button';
4027 edit_hyperlink_button.style.display = "none";
4028 cancel_hyperlink_button.innerText = "Cancel";
4029 cancel_hyperlink_button.id = 'cancel-hyperlink-button';
4030 hyperlink_form.appendChild(display_text_container);
4031 hyperlink_form.appendChild(link_field_container);
4032 hyperlink_form.appendChild(add_hyperlink_button);
4033 hyperlink_form.appendChild(edit_hyperlink_button);
4034 hyperlink_form.appendChild(cancel_hyperlink_button);
4035
4036 if(allow_multi_links){
4037 if(selection_type == 'Range'){
4038 let anchor_node = selection.anchorNode;
4039 let anchor_tag = anchor_node.parentElement;
4040 // Creating the form on the fly
4041 //Create the container element for the form
4042 add_hyperlink_button.onclick = this.create_anchor_tag;
4043 edit_hyperlink_button.onclick = this.update_anchor_tag;
4044 cancel_hyperlink_button.onclick = this.remove_anchor_form;
4045 //Append everything into the form, hyperlink_form should now be complete and ready to display.
4046 //Avoiding using innerHTML with a large html string to create this because we can lose listeners.
4047 if(anchor_tag.nodeName == "A"){
4048 let display_text = selection.toString();
4049 display_input_field.value = display_text;
4050 let display_link = anchor_tag.href;
4051 link_input_field.value = display_link;
4052 edit_hyperlink_button.style.display = 'flex';
4053 add_hyperlink_button.style.display = 'none';
4054 this.ANCHOR_NODE = anchor_node;
4055 this.SELECTION_STRING = display_text;
4056
4057 this.shadowRoot.appendChild(hyperlink_form);
4058 }else{
4059 let anchor_offset = selection.anchorOffset;
4060 let focus_offset = selection.focusOffset;
4061 this.ANCHOR_NODE = anchor_node;
4062 this.ANCHOR_OFFSET = anchor_offset;
4063 this.FOCUS_OFFSET = focus_offset;
4064 let display_text = selection.toString();
4065 this.SELECTION_STRING = display_text;
4066 display_input_field.value = display_text;
4067 edit_hyperlink_button.style.display = 'none';
4068 add_hyperlink_button.style.display = 'flex';
4069
4070 hyperlink_form.appendChild(display_text_container);
4071 hyperlink_form.appendChild(link_field_container);
4072 hyperlink_form.appendChild(add_hyperlink_button);
4073 hyperlink_form.appendChild(edit_hyperlink_button);
4074 hyperlink_form.appendChild(cancel_hyperlink_button);
4075 this.shadowRoot.appendChild(hyperlink_form);
4076 }
4077 }
4078 }else{
4079
4080 let cell_container = grid_component.right_click_source_cell;
4081 let content_area = cell_container.querySelector('.contentarea');
4082 let content_anchor = content_area.querySelector('a');
4083 grid_component.CONTENT_NODE = content_area;
4084
4085 if(content_anchor){
4086 // If single link and already has a link, we have to account for potential unlinked text
4087 grid_component.ANCHOR_NODE = content_anchor.childNodes[0];
4088 let current_display = content_area.innerText;
4089 let current_href = content_anchor.href;
4090 display_input_field.value = current_display;
4091 link_input_field.value = current_href;
4092 edit_hyperlink_button.onclick = this.update_anchor_tag;
4093 cancel_hyperlink_button.onclick = this.remove_anchor_form;
4094 edit_hyperlink_button.style.display = 'flex';
4095 add_hyperlink_button.style.display = 'none';
4096 this.shadowRoot.appendChild(hyperlink_form);
4097 }else{
4098 grid_component.ANCHOR_NODE = null;
4099 display_text_container.style.display = 'none';
4100 let current_display = content_area.innerText;
4101 let current_href = '';
4102 display_input_field.value = current_display;
4103 link_input_field.value = current_href;
4104 add_hyperlink_button.onclick = this.create_anchor_tag;
4105 cancel_hyperlink_button.onclick = this.remove_anchor_form;
4106 edit_hyperlink_button.style.display = 'none';
4107 add_hyperlink_button.style.display = 'flex';
4108 this.shadowRoot.appendChild(hyperlink_form);
4109 }
4110 }
4111 }
4112 create_anchor_tag(e){
4113 let grid_root = this.getRootNode();
4114 let grid_component = grid_root.host;
4115 let allow_multi_links = this.multiple_links;
4116 // text_node is either the selection text, or the entire div, depending on the multi_link flag
4117 let text_node = grid_component.ANCHOR_NODE;
4118 let content_node = grid_component.CONTENT_NODE;
4119 let link_node = document.createElement("a");
4120
4121 let url_string = grid_root.getElementById('link-field').value;
4122 let new_url;
4123 // Assuming users will fail to always preppend https:// to their links, we will handle that for them
4124 try{
4125 new_url = new URL(url_string);
4126 }catch{
4127 console.log("Malformed URL, attempting to fix");
4128 new_url = new URL(`https://${url_string}`);
4129 }
4130 link_node.href = new_url;
4131 link_node.title = `Ctrl-click to follow to ${new_url}`;
4132 link_node.target = "_blank";
4133 link_node.contentEditable = "plaintext-only";
4134 let range = new Range();
4135 let form = grid_root.getElementById('hyperlink-form');
4136 let display_field = grid_root.getElementById('display-field');
4137 let link_field = grid_root.getElementById('link-field');
4138 if(allow_multi_links){
4139 let start_offset = grid_component.ANCHOR_OFFSET;
4140 let end_offset = grid_component.FOCUS_OFFSET;
4141 let selected_text = grid_component.SELECTION_STRING;
4142 let last_char = selected_text.slice(-1);
4143
4144 // Handling right to left, or left to right selections.
4145 // Anchor Offset is where the selection started, focusOffset is where it ended, if selected right-to-left, focusOffset will be less than anchorOffset
4146 if(start_offset < end_offset){
4147 if(last_char == " "){
4148 console.log("last char was a space");
4149 range.setStart(text_node, start_offset);
4150 range.setEnd(text_node, end_offset - 1);
4151 }else{
4152 range.setStart(text_node, start_offset);
4153 range.setEnd(text_node, end_offset);
4154 }
4155 }else{
4156 if(last_char == " "){
4157 console.log("last char was a space");
4158 range.setStart(text_node, end_offset);
4159 range.setEnd(text_node, start_offset);
4160 }else{
4161 range.setStart(text_node, end_offset);
4162 range.setEnd(text_node, start_offset - 1);
4163 }
4164 }
4165 range.surroundContents(link_node);
4166 display_field.value = '';
4167 link_field.value = '';
4168 form.remove();
4169 content_node.focus();
4170 content_node.blur();
4171 }else{
4172 // If the endNode is a Node of type Text, Comment, or CDataSection, then endOffset is the number of characters from the start of endNode.
4173 // For other Node types (such as here which is a DIV), endOffset is the number of child nodes between the start of the endNode.
4174 range.setStart(content_node, 0);
4175 range.setEnd(content_node, 1);
4176 range.surroundContents(link_node);
4177 display_field.value = '';
4178 link_field.value = '';
4179 form.remove();
4180 // This forces a save, TO DO, potentially unwarranted side effects down the road.
4181 // Text_node is the DIV here, because of only 1 potential link. it is content node above, because with multi links
4182 // The text_node may be an actual text_node, it's parent an anchor tag, the ANCHOR's parent is the content node,
4183 // here its text node -> content node
4184 content_node.focus();
4185 content_node.blur();
4186 }
4187 }
4188 update_anchor_tag(e){
4189 let grid_root = this.getRootNode();
4190 let grid_component = grid_root.host;
4191 let anchor_node = grid_component.ANCHOR_NODE;
4192 let anchor_tag = anchor_node.parentElement;
4193 let content_node = grid_component.CONTENT_NODE;
4194
4195 let new_display_text = grid_root.getElementById('display-field').value;
4196 let url_string = grid_root.getElementById('link-field').value;
4197 let new_display_link;
4198 // Assuming users will fail to always preppend https:// to their links, we will handle that for them
4199 try{
4200 new_display_link = new URL(url_string);
4201 }catch{
4202 console.log("Malformed URL, attempting to fix");
4203 new_display_link = new URL(`https://${url_string}`);
4204 }
4205 if(grid_component.multiple_links){
4206 anchor_tag.innerHTML = new_display_text;
4207 anchor_tag.href = new_display_link;
4208 let form = grid_root.getElementById('hyperlink-form');
4209 let display_field = grid_root.getElementById('display-field');
4210 let link_field = grid_root.getElementById('link-field');
4211 display_field.value = '';
4212 link_field.value = '';
4213 form.remove();
4214 content_node.focus();
4215 content_node.blur();
4216 }else{
4217 anchor_tag.innerHTML = new_display_text;
4218 content_node.replaceChildren(anchor_tag);
4219 let form = grid_root.getElementById('hyperlink-form');
4220 let display_field = grid_root.getElementById('display-field');
4221 let link_field = grid_root.getElementById('link-field');
4222 display_field.value = '';
4223 link_field.value = '';
4224 form.remove();
4225 content_node.focus();
4226 content_node.blur();
4227 }
4228 }
4229 remove_anchor_tag(e){
4230 let grid_component = this;
4231 let grid_root = this.shadowRoot;
4232 let allow_multi_links = this.multiple_links;
4233 if(allow_multi_links){
4234 const selection = grid_root.getSelection();
4235 let selection_type = selection.type;
4236 if(selection_type == 'Range'){
4237 let anchor_node = selection.anchorNode;
4238 let parent_node = anchor_node.parentElement;
4239 if(parent_node.nodeName == 'A'){
4240 const content_node = parent_node.parentElement;
4241 console.log(content_node);
4242 parent_node.replaceWith(selection.toString());
4243 console.log(content_node);
4244 content_node.focus();
4245 content_node.blur();
4246
4247 }
4248
4249 }
4250 }else{
4251 let cell_container = grid_component.right_click_source_cell;
4252 let content_node = grid_component.CONTENT_NODE;
4253 if(!content_node){
4254 content_node = cell_container.querySelector('.contentarea');
4255 }
4256 let display_text = content_node.innerText;
4257 content_node.innerHTML = display_text;
4258 content_node.focus();
4259 content_node.blur();
4260 }
4261 }
4262 anchor_click_handler(e){
4263 let target = e.target;
4264 if(e.ctrlKey && target.nodeName == 'A'){
4265 let a_tag = target;
4266 a_tag.contentEditable = false;
4267 a_tag.click();
4268 a_tag.contentEditable = "plaintext-only";
4269 } else if (e.ctrlKey && target.nodeName == 'DIV'){
4270 let potential_anchor = target.querySelector("a");
4271 if(potential_anchor){
4272
4273 potential_anchor.contentEditable = false;
4274 potential_anchor.click();
4275 potential_anchor.contentEditable = "plaintext-only";
4276 }
4277 }
4278 }
4279 remove_anchor_form(e){
4280 let grid_root = e.target.getRootNode();
4281 let grid_component = grid_root.host;
4282 grid_root.getElementById('hyperlink-form').remove();
4283 grid_component.ANCHOR_NODE = null;
4284 grid_component.CONTENT_NODE = null;
4285 grid_component.ANCHOR_OFFSET = null;
4286 grid_component.FOCUS_OFFSET = null;
4287 grid_component.SELECTION_STRING = null;
4288 }
4289 insert_row(grid_id, direction){
4290
4291 let this_reference = window.gridReference.get(grid_id);
4292 let source_cell = this_reference.right_click_source_cell;
4293 // Obtain the row_id from the comma seperated element ID;
4294 // The direction determines whether we're inserting before or after.
4295 let grid_row_id = Number(source_cell.getAttribute("id").split(",")[0]);
4296 let database_row_id = Number(this_reference.GLOBAL_JSON_RESULT_FILTERED[grid_row_id]["row_number"]) + direction;
4297 let insert_url = `${this_reference.insert_row_url}&row=${database_row_id}&iteration=${1}`;
4298 console.log(`Grid Row: ${grid_row_id} Database Row: ${database_row_id}`);
4299 console.log(insert_url);
4300 document.body.style.cursor = "wait";
4301 this_reference.get_request(insert_url).then(result => {
4302 this_reference.refresh_grid();
4303 })
4304 }
4305 // This handles the excel keyboard commands, such as ctrl-c ctrl-v, tab, enter, and the arrow keys
4306 // Not to be called from the template
4307 excel_keydown_handler(e){
4308 // Get a reference to the grid object
4309 let this_reference = window.gridReference.get(this.title);
4310 // "this" will almost always refer to the <td> element, which we can get the positional ID from.
4311 let current_position = this.id.split(',');
4312 // ID's are oriented with row number, column number
4313 let row_number = Number(current_position[0]);
4314 let column_number = Number(current_position[1]);
4315 let project_id = Number(current_position[2]);
4316 let item_id = Number(current_position[3]);
4317 let column_name = current_position[4];
4318
4319 // COLUMN_NAME_ORDER is created upon table creation, it lets us know the order of the columns based on the config
4320 let column_name_position = this_reference.COLUMN_NAME_ORDER.indexOf(column_name);
4321 let next_cell_id;
4322 let next_cell;
4323 // If they have a cell selected, CTRL-A will automatically focus and select the text in the input area.
4324 if(e.shiftKey && !e.target.classList.contains("grid_table_data")){
4325 console.log("Shift-Enter");
4326 }else if(e.ctrlKey && e.code=="KeyA" && e.target.classList.contains("grid_table_data")){
4327 e.preventDefault();
4328 let input_element = e.target.querySelector("select,input,textarea");
4329 console.log("Ctrl-A");
4330 input_element.focus();
4331 console.log(input_element);
4332 if(input_element.tagName == "INPUT" || input_element.tagName == "TEXTAREA"){
4333 console.log(input_element);
4334 input_element.setSelectionRange(0, input_element.value.length);
4335 }
4336 // Up Arrow Navigation
4337 }else if(e.code == "ArrowUp"){
4338 // Ignoring the shift key allows us to still use arrow keys to highlight within the cell input.
4339 if(!e.shiftKey){
4340 e.preventDefault();
4341 row_number--;
4342 if(row_number >= 0){
4343 // With the added insert/removal of rows, we can no longer guarantee item ID's follow eachother correctly.
4344 let next_cell_primary_key = this_reference.GLOBAL_JSON_RESULT_FILTERED[row_number][this_reference.primary_key];
4345 next_cell_id = `${row_number},${column_number},${project_id},${next_cell_primary_key},${column_name}`;
4346 next_cell = this_reference.shadowRoot.getElementById(next_cell_id);
4347 next_cell.focus();
4348 next_cell.scrollIntoView({
4349 behavior: 'instant',
4350 block: 'center',
4351 inline: 'center'
4352 });
4353 }
4354 }
4355 // Left Arrow Navigation
4356 }else if(e.code == "ArrowLeft"){
4357 // Ignoring the shift key allows us to still use arrow keys to highlight within the cell input.
4358 if(!e.shiftKey){
4359 e.preventDefault();
4360 column_number--;
4361
4362 if(column_number >= 0){
4363
4364 next_cell_id = `${row_number},${column_number},${project_id},${item_id},${this_reference.COLUMN_NAME_ORDER[column_name_position-1]}`;
4365 next_cell = this_reference.shadowRoot.getElementById(next_cell_id);
4366 // table_container.scrollLeft -= next_cell.offsetWidth;
4367 next_cell.scrollIntoView({
4368 behavior: 'instant',
4369 block: 'center',
4370 inline: 'center'
4371 });
4372 next_cell.focus();
4373
4374 }
4375 }
4376 // Right Arrow and Tab Navigation
4377 }else if(e.code == "ArrowRight" || (!e.target.classList.contains("grid_table_data") && e.code == "Tab")){
4378 // Ignoring the shift key allows us to still use arrow keys to highlight within the cell input.
4379 if(!e.shiftKey){
4380 e.preventDefault();
4381 let max_columns = this_reference.COLUMN_NAME_ORDER.length;
4382 column_number++;
4383
4384 if(column_number < max_columns){
4385 next_cell_id = `${row_number},${column_number},${project_id},${item_id},${this_reference.COLUMN_NAME_ORDER[column_name_position+1]}`;
4386 next_cell = this_reference.shadowRoot.getElementById(next_cell_id);
4387 // table_container.scrollLeft += next_cell.offsetWidth
4388 next_cell.scrollIntoView({
4389 behavior: 'instant',
4390 block: 'center',
4391 inline: 'center'
4392 });
4393 next_cell.focus();
4394 }
4395 }
4396 // Down Arrow and Enter Navigation
4397 }else if(e.code == "ArrowDown" || (!e.target.classList.contains("grid_table_data") && e.code == "Enter")){
4398 // Ignoring the shift key allows us to still use arrow keys to highlight within the cell input.
4399 if(!e.shiftKey){
4400 e.preventDefault();
4401 let max_rows = this_reference.max_results;
4402 row_number++;
4403 try{
4404 if(row_number % max_rows != 0){
4405 let next_cell_primary_key = this_reference.GLOBAL_JSON_RESULT_FILTERED[row_number][this_reference.primary_key];
4406 next_cell_id = `${row_number},${column_number},${project_id},${next_cell_primary_key},${column_name}`;
4407 next_cell = this_reference.shadowRoot.getElementById(next_cell_id);
4408 next_cell.focus();
4409 next_cell.scrollIntoView({
4410 behavior: 'instant',
4411 block: 'center',
4412 inline: 'center'
4413 });
4414 }
4415 }catch{
4416 // We don't care if it errors.
4417 }
4418 }
4419 }else if(e.target.classList.contains("grid_table_data") && (e.code !== "KeyV" && !e.ctrlKey)){
4420 // e.preventDefault();
4421 let input_element = e.target.querySelector("select,input,textarea");
4422
4423 input_element.focus();
4424 if(input_element.tagName == "INPUT" || input_element.tagName == "TEXTAREA"){
4425 input_element.setSelectionRange(input_element.value.length, input_element.value.length);
4426 }
4427
4428 // this.selectionStart = this.selectionEnd = this.value.length;
4429 }
4430 }
4431 // Handler for pasting from excel.
4432 // Not to be called from the template
4433 copy_handler(e){
4434 let grid_reference = window.gridReference.get(e.currentTarget.title);
4435 let active_element = grid_reference.shadowRoot.activeElement;
4436 let row_change_array = [];
4437 let undo_row_change_array = [];
4438 // If the activeElement is the <td class="grid_table_data">, then we are currently clicked on the cell, not the inner form field.
4439 // That is the paste target we want to handle, so prevent default.
4440 if(active_element.classList.contains("grid_table_data")){
4441 e.preventDefault();
4442
4443 let position_array = active_element.id.split(',');
4444 let row_position = Number(position_array[0]);
4445 let column_position = Number(position_array[1]);
4446 let project_id = Number(position_array[2]);
4447 let project_item_id = Number(position_array[3]);
4448 let used_columns = [];
4449 //Convert the string back into a variable and dereference the data
4450 Object.keys(grid_reference.configuration["column_information"]).forEach((key) => {
4451 used_columns.push(key);
4452 });
4453 let starting_column_key = used_columns[column_position];
4454 // Let the work begin here.
4455 let clipboard_text = e.clipboardData.getData("text");
4456 // Replace last /r/n in the string (pasting from Excel on Mac does not include the trailing /r/n)
4457
4458 clipboard_text = clipboard_text.replace(/[\r\n]+$/, '');
4459
4460 let clipboard_matrix = [];
4461 //Excel separates its rows by /r/n, columns by tabs \t
4462 let clipboard_rows = clipboard_text.split('\r\n');
4463 for(let i = 0; i < clipboard_rows.length; i++){
4464 let column_split = clipboard_rows[i].split("\t");
4465 let column_name_value = {};
4466 for(let j = 0; j < column_split.length; j++){
4467 if(used_columns[column_position+j]){
4468 column_name_value[used_columns[column_position+j]] = column_split[j];
4469 }
4470 }
4471 clipboard_matrix.push(column_name_value);
4472 }
4473 // let max_rows = Number(grid_reference.shadowRoot.getElementById(`${grid_reference.id}_max_results`).value);
4474 // let max_rows = 1000;
4475 let current_row_position = row_position;
4476 let current_column_position = column_position;
4477 // Empty the old data array, a new copy has occurred.
4478 grid_reference.UNDO_COPY_JSON = [];
4479 // Increase the count of how many times something has been copied (This is incase we want to expand upon it later)
4480 grid_reference.COPY_COUNT++;
4481 let over_copy_matrix = [];
4482 let undo_over_copy_matrix = [];
4483 let insert_copy_matrix = [];
4484 let current_row_number = 0;
4485 for(let i = 0; i < clipboard_matrix.length; i++){
4486 let copy_row = clipboard_matrix[i];
4487 // if((current_row_position+i) < max_rows){
4488 if((current_row_position+i) < grid_reference.GLOBAL_JSON_RESULT_FILTERED.length){
4489 let target_row = grid_reference.GLOBAL_JSON_RESULT_FILTERED[current_row_position+i];
4490 let column_names = Object.keys(copy_row);
4491 let current_item_id = '';
4492 // HARDCODE: Believe this can be updated to be set with grid_reference.configuration['primary_key']
4493 if(target_row["az_bundle_id_assignments_id"]){
4494 current_item_id = target_row["az_bundle_id_assignments_id"];
4495 }else if(target_row["az_master_power_id"]){
4496 current_item_id = target_row["az_master_power_id"];
4497 }else{
4498 current_item_id = target_row["az_project_item_id"];
4499 }
4500 copy_row["item_id"] = current_item_id;
4501 copy_row["project_id"] = project_id;
4502
4503 current_row_number = target_row["row_number"];
4504 copy_row["row_number"] = current_row_number;
4505
4506 let undo_copy_row = Object.assign({}, copy_row);
4507 over_copy_matrix.push(copy_row);
4508 for(let j = 0; j < column_names.length; j++){
4509 if((current_column_position+j) < (used_columns.length)){
4510
4511 // TODO: Rework this section, shouldn't be duplicating code. Look at over_copy and the individual cells, we can marry them I think
4512 let target_new_value = copy_row[column_names[j]];
4513 let target_original_value = target_row[used_columns[current_column_position+j]];
4514 let target_column_name = used_columns[current_column_position+j];
4515 undo_copy_row[column_names[j]] = target_original_value;
4516 let copy_json = {
4517 'project_id': project_id,
4518 'project_item_id': current_item_id,
4519 'column_name': target_column_name,
4520 'original_value': target_original_value,
4521 'new_value': target_new_value,
4522 }
4523 let undo_json = {
4524 'project_id': project_id,
4525 'project_item_id': current_item_id,
4526 'column_name': target_column_name,
4527 'original_value': target_new_value,
4528 'new_value': target_original_value,
4529 }
4530 row_change_array.push(copy_json);
4531 undo_row_change_array.push(undo_json);
4532 }else{
4533 break;
4534 }
4535 }
4536 undo_over_copy_matrix.push(undo_copy_row);
4537 grid_reference.UNDO_OVER_COPY_JSON = Array.from(undo_over_copy_matrix);
4538 grid_reference.UNDO_COPY_JSON = Array.from(undo_row_change_array);
4539 }else{
4540 // Increment by 1 from the previous max
4541 current_row_number++;
4542 copy_row["item_id"] = '';
4543 copy_row["project_id"] = project_id;
4544 copy_row["row_number"] = current_row_number;
4545 insert_copy_matrix.push(copy_row);
4546
4547 }
4548 }
4549 console.log(over_copy_matrix);
4550 console.log(insert_copy_matrix);
4551 grid_reference.modified_data["action"] = "excel_paste";
4552 grid_reference.modified_data["grid_name"] = grid_reference.getAttribute("name");
4553 grid_reference.modified_data["source_data"] = row_change_array;
4554 grid_reference.modified_data["over_copy_data"] = over_copy_matrix;
4555 grid_reference.modified_data["insert_copy_data"] = insert_copy_matrix;
4556 grid_reference.save_data(
4557 `grid-${grid_reference.grid_name}`
4558 , Number(grid_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML)
4559 ,'excel_paste'
4560 );
4561 }else{
4562 console.log("Pasting inside the form field.");
4563 }
4564 }
4565 // Undo most recent paste from Excel, HAS NOTHING TO DO WITH THE CONTEXT MENU UNDO
4566 // How To Template Call: window.gridReference.get('grid-id-here').undo_copy(event);
4567 undo_copy(e){
4568 let grid_reference = window.gridReference.get(e.target.getAttribute('grid-owner'));
4569 if(grid_reference.COPY_COUNT > 0){
4570 grid_reference.modified_data = {};
4571 grid_reference.modified_data["action"] = "excel_paste";
4572 grid_reference.modified_data["grid_name"] = grid_reference.getAttribute("name");
4573 grid_reference.modified_data["source_data"] = grid_reference.UNDO_COPY_JSON;
4574 grid_reference.modified_data["over_copy_data"] = grid_reference.UNDO_OVER_COPY_JSON;
4575 if(grid_reference.UNDO_INSERT_STARTING_ID > 0){
4576 grid_reference.modified_data["undo_insert_id"] = grid_reference.UNDO_INSERT_STARTING_ID;
4577 grid_reference.modified_data["undo_insert_amount"] = grid_reference.UNDO_INSERT_AMOUNT;
4578 }
4579 //TODO, update this to wipe out the inserted data.
4580 grid_reference.modified_data["insert_copy_data"] = [];
4581 grid_reference.save_data(
4582 `grid-${grid_reference.grid_name}`
4583 , Number(grid_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML)
4584 ,'excel_paste'
4585 );
4586 grid_reference.COPY_COUNT = 0;
4587 grid_reference.UNDO_INSERT_STARTING_ID = -1;
4588 grid_reference.UNDO_INSERT_AMOUNT = -1;
4589 }
4590 }
4591 // The grid will enter excel mode, this is a boolean call that flips the grid between excel-mode and default-mode
4592 // How To Template Call: window.gridReference.get('grid-id-here').excel_mode(event);
4593 excel_mode(e){
4594 let grid_reference = window.gridReference.get(this.getAttribute("grid-owner"));
4595 let grid_id = grid_reference.id;
4596 if(grid_reference.excel_edit){
4597 grid_reference.excel_edit = false;
4598 }else{
4599 grid_reference.excel_edit = true;
4600 }
4601 let current_page = Number(grid_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML);
4602 grid_reference.create_table(grid_id, '', current_page);
4603 }
4604 // Function called by OUTSIDE helper functions, this simulates refreshing the page.
4605 // How To Template Call: window.gridReference.get('grid-id-here').refresh_grid();
4606 refresh_grid(){
4607 let this_reference = this;
4608
4609 let active_element = this.shadowRoot.activeElement;
4610 console.log(this.shadowRoot);
4611 console.log(active_element);
4612 if(active_element){
4613
4614 if(active_element.tagName != 'SELECT' && active_element.tagName != "INPUT" && active_element.tagName != "TEXTAREA" && active_element.tagName != "TD"){
4615 console.log("Loading new table");
4616 document.body.style.cursor = "wait";
4617 this.shadowRoot.getElementById(`grid-${this.grid_name}_container`).classList.add('disabled-mouse');
4618 if(this.load_filters_url){
4619 this.load_global_json(this.load_filters_url).then(jsonData =>{
4620 // Escape the loaded json, making sure new line, carriage return, tab, etc are properly escaped in the json.
4621 let filtered_filters = this.escape_json(jsonData);
4622 this.GLOBAL_LOADED_FILTERS = filtered_filters;
4623
4624 this.grid_filters = this.GLOBAL_LOADED_FILTERS;
4625 this.load_global_json(this.data_url).then(jsonData =>{
4626
4627 let filtered_data = this.escape_json(jsonData);
4628 this.GLOBAL_JSON_RESULT = filtered_data;
4629 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
4630 this.create_table(`grid-${this.grid_name}`, '', Number(this.shadowRoot.querySelector('.pagination_selected_link').innerHTML));
4631 });
4632 });
4633 }else{
4634 this.load_global_json(this.data_url).then(jsonData =>{
4635
4636 let filtered_data = this.escape_json(jsonData);
4637 this.GLOBAL_JSON_RESULT = filtered_data;
4638 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
4639 this.create_table(`grid-${this.grid_name}`, '', Number(this.shadowRoot.querySelector('.pagination_selected_link').innerHTML));
4640 });
4641 }
4642 }else{
4643 console.log("Not Loading New Table")
4644 if(!active_element.onblur){
4645 // If trying to set directly to the timeout, it will not work,the timeout will go global, and it just fires recursively.
4646 // Setting the timeout in an anonymous function allows the onBlur to be the function that sets the timeout, and removes itself.
4647 active_element.addEventListener('blur', function(e){
4648 setTimeout(() => {
4649 console.log('replacingstuff');
4650 if(!this_reference.excel_edit){
4651 this.contentEditable='false'
4652 }
4653 this_reference.refresh_grid();
4654 active_element.onblur = null
4655 }, 1000);
4656 });
4657 }
4658 }
4659 }else{
4660 console.log("Loading new table NO ACTIVE ELEMENT");
4661 this.shadowRoot.getElementById(`grid-${this.grid_name}_container`).classList.add('disabled-mouse');
4662 document.body.style.cursor = "wait";
4663 if(this.load_filters_url){
4664 this.load_global_json(this.load_filters_url).then(jsonData =>{
4665 // Escape the loaded json, making sure new line, carriage return, tab, etc are properly escaped in the json.
4666 let filtered_filters = this.escape_json(jsonData);
4667 this.GLOBAL_LOADED_FILTERS = filtered_filters;
4668
4669 this.grid_filters = this.GLOBAL_LOADED_FILTERS;
4670 this.load_global_json(this.data_url).then(jsonData =>{
4671
4672 let filtered_data = this.escape_json(jsonData);
4673 this.GLOBAL_JSON_RESULT = filtered_data;
4674 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
4675 this.create_table(`grid-${this.grid_name}`, '', Number(this.shadowRoot.querySelector('.pagination_selected_link').innerHTML));
4676 });
4677 });
4678 }else{
4679 this.load_global_json(this.data_url).then(jsonData =>{
4680
4681 let filtered_data = this.escape_json(jsonData);
4682 this.GLOBAL_JSON_RESULT = filtered_data;
4683 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
4684 this.create_table(`grid-${this.grid_name}`, '', Number(this.shadowRoot.querySelector('.pagination_selected_link').innerHTML));
4685 });
4686 }
4687 }
4688
4689 }
4690 // Function called by OUTSIDE helper functions, this redraws the grid, without calling to the back for data.
4691 // Used in conjunction with updating the JSON from outside the component.
4692 // How To Template Call: window.gridReference.get('grid-id-here').redraw_grid();
4693 redraw_grid(){
4694 let this_reference = this;
4695 let pagination_number = 1;
4696 if(this_reference.shadowRoot.querySelector('.pagination_selected_link')){
4697 pagination_number = Number(this_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML);
4698 }
4699 this_reference.create_table(`grid-${this_reference.grid_name}`, '', pagination_number);
4700 }
4701 // Does nothing ignore this, its for dev work.
4702 test_function(){
4703 console.log("This is an injected function interoperating within the webcomponent.");
4704 }
4705 // Prevent the user from accidently pressing delete. This highlights the clicked row and pops up a confirmation box.
4706 // Not to be called from the template.
4707 show_delete_confirmation(id, grid_id){
4708 let pending_row = this.shadowRoot.querySelector(`#${id}`);
4709 pending_row.style.backgroundColor = '#ffaaaa';
4710 console.log(pending_row);
4711 let confirmation_container = document.createElement('div');
4712 confirmation_container.innerHTML = `
4713 <div class="confirmation_container_display">
4714 <h2> Are you sure you want to delete this row? </h2>
4715 <div class="confirmation_button_container">
4716 <button onclick="this.parentElement.parentElement.parentElement.remove(); window.gridReference.get('${grid_id}').shadowRoot.querySelector('#${id}').style.backgroundColor = '';"> No </button>
4717 <button onclick="this.parentElement.parentElement.parentElement.remove(); window.gridReference.get('${grid_id}').delete_row('${id}');"> Yes </button>
4718 </div>
4719 </div>
4720 `
4721 confirmation_container.id = 'confirmation-container';
4722 confirmation_container.classList.add('confirmation_container');
4723 window.gridReference.get(grid_id).shadowRoot.appendChild(confirmation_container);
4724 // document.body.appendChild(confirmation_container);
4725 // Prevent the user from being able to scroll away from the delete prompt.
4726 confirmation_container.addEventListener('wheel', (e) =>{
4727 e.preventDefault();
4728 e.stopPropagation();
4729
4730 return false;},
4731 {passive: false}
4732 );
4733 confirmation_container.children[0].style.top = window.scrollY + 'px';
4734 }
4735 //using DEPRECATED functions, because we are serving over HTTP and not HTTPS. Navigator.clipboard.writeText only works in HTTPS contexts.
4736 // Custom contextmenu to allow you to copy the value out of a field, useful for dropdowns and textareas.
4737 // Not to be called from the template.
4738 copy_cell_value(row_id, header_key){
4739 let row_json = this.GLOBAL_JSON_RESULT[Number(row_id.replace("row", ""))];
4740
4741 // This whole section is the deprecated functionality.
4742 // #################################################################################
4743 let text_area_holder = document.createElement("textarea");
4744 this.shadowRoot.appendChild(text_area_holder);
4745 text_area_holder.value = row_json[header_key];
4746 text_area_holder.select();
4747 document.execCommand('copy', false, row_json[header_key]);
4748 text_area_holder.remove();
4749 // #################################################################################
4750 // END DEPRECATED FUNCTIONALITY
4751
4752 // Modern usage, unservable over HTTP.
4753 // navigator.clipboard.writeText(row_json[header_key][0]);
4754 }
4755 // Used in the copy drag functionality, to highlight the starting cell, and apply the correct listeners for dragging around.
4756 // Not to be called from the template.
4757 cell_down_handler(e){
4758 // Make sure right clicking does not trigger the copy-drag process
4759 console.log("Down handler");
4760 if(e.which == 1){
4761 let this_reference = window.gridReference.get(e.target.parentElement.parentElement.title);
4762 let table_container = this_reference.shadowRoot.querySelector("table");
4763 this_reference.starting_cell = e.target.parentElement.parentElement;
4764 this_reference.starting_cell.classList.add('copied_over');
4765 this_reference.ending_cell = null;
4766 this_reference.copy_lock = true;
4767 table_container.classList.add("grid-dragging");
4768 this_reference.addEventListener('mousemove', this_reference.cell_copy_move_handler)
4769 // this_reference.addEventListener('mouseleave', this_reference.clear_listeners);
4770 }
4771
4772 }
4773 // Escape hatch for any hanging listeners, this is called when the mouse leaves the grid area, to prevent any hanging behavior.
4774 // Not to be called from the template.
4775 clear_listeners(e){
4776 console.log('CLEAR LISTENERS');
4777 let this_reference = e.target;
4778
4779 for(let i = 0; i < this_reference.copy_element_array.length; i++){
4780 this_reference.copy_element_array[i].classList.remove("copied_over");
4781 this_reference.copy_element_array[i].classList.remove("copied_over_wrong");
4782 }
4783 let dangling_menus = this_reference.shadowRoot.querySelectorAll(".context-menu");
4784 for(let menu of dangling_menus){
4785 menu.remove();
4786 }
4787 //Reset all variables that held data from the cells.
4788 this_reference.starting_cell = null;
4789 this_reference.ending_cell = null;
4790 this_reference.copy_lock = false;
4791 this_reference.modified_data = {};
4792 this_reference.copy_element_array = [];
4793 this_reference.removeEventListener('mousemove', this_reference.cell_copy_move_handler);
4794 this_reference.removeEventListener('mouseleave', this_reference.clear_listeners);
4795 this_reference.create_table(`grid-${this_reference.grid_name}`, '', Number(this_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML))
4796 }
4797 //This is a nightmare and needs to be cleaned up.
4798 // TODO: CLEAN!
4799 // When the user has finished copy dragging, this gathers the cells between the start and finish line, regardless of which column
4800 // the user ended in.
4801 // Not to be called from the template.
4802 cell_up_handler(e){
4803 let this_reference = window.gridReference.get(this.title);
4804 let table_container = this_reference.shadowRoot.querySelector("table");
4805 if(table_container.classList.contains("grid-dragging")){
4806 table_container.classList.remove("grid-dragging");
4807 }
4808 //Don't do the copy handling if right click was pressed.
4809 this.style.cursor = "wait";
4810 if(e.which == 1){
4811
4812 //Handling single cell copy attempts
4813 if(this_reference.starting_cell){
4814 this_reference.starting_cell.classList.remove('copied_over');
4815 }
4816 //Clear copy element array, we're done copying.
4817 for(let i = 0; i < this_reference.copy_element_array.length; i++){
4818 this_reference.copy_element_array[i].classList.remove("copied_over");
4819 }
4820 //Remove the listener from the component, we don't need to process any more cells.
4821 this_reference.removeEventListener('mousemove', this_reference.cell_copy_move_handler);
4822 //If ending_cell is null and starting cell is not null, we know we're in the correct situation to copy.
4823 if(this_reference.ending_cell == null && this_reference.starting_cell && this_reference.starting_cell != e.target){
4824 //Get the cell that was mouse-up'd over.
4825 let active_cell = e.target;
4826 //Occasionally they'll let up on the mouse while hovering over the embedded html, or the drag icon itself
4827 //Loop through the parent elements until we have the table cell.
4828 if(active_cell.nodeName != 'TD'){
4829 let cell_retrieved = false;
4830 while(!cell_retrieved){
4831 active_cell = active_cell.parentElement;
4832 if(active_cell.nodeName == 'TD'){
4833 cell_retrieved=true;
4834 }
4835 }
4836 }
4837 //If the active cell header is the same as the starting cell header, we're in the same column.
4838 if(active_cell.headers != this_reference.starting_cell.headers){
4839 let parent_row = active_cell.parentElement;
4840 let column_name = this_reference.starting_cell.headers;
4841 let current_row_number = Number(parent_row.id.replace("row", ''));
4842 active_cell = this_reference.shadowRoot.querySelector(`#row${current_row_number}`).querySelector(`td[headers='']`);
4843 }
4844 if(active_cell.headers == this_reference.starting_cell.headers){
4845 console.log("Same Row");
4846 //Set the ending cell
4847 this_reference.ending_cell = active_cell;
4848 //Get the indexes to pull the data out of the global json objects.
4849 let starting_cell_index = Number(this_reference.starting_cell.parentElement.id.replace("row",""));
4850 let ending_cell_index = Number(this_reference.ending_cell.parentElement.id.replace("row",""));
4851 //Same as row deletion, if the 2 json objects are the same length, we're not filtering.
4852 try{
4853 this_reference.modified_data["action"] = "copy";
4854 this_reference.modified_data["grid_name"] = this_reference.getAttribute("name");
4855
4856 let copy_target_array = [];
4857 if(this_reference.GLOBAL_JSON_RESULT.length == this_reference.GLOBAL_JSON_RESULT_FILTERED.length){
4858
4859 console.log("Same Length");
4860
4861 this_reference.modified_data["source_column"] = active_cell.headers;
4862 this_reference.modified_data["source_data"] = this_reference.GLOBAL_JSON_RESULT[starting_cell_index];
4863 console.log(this_reference.modified_data["source_data"]);
4864 //Obtain the value from the JSON, and copy into the other indexes
4865 let original_replacement_value = this_reference.GLOBAL_JSON_RESULT[starting_cell_index][active_cell.headers];
4866 if(original_replacement_value == null){
4867 original_replacement_value = "";
4868 }
4869 let increment_regex = new RegExp('\\d*\\.?\\d*$');
4870 let leading_value = '';
4871 // exec returns an array of result, index, matching groups
4872 let regex_array = increment_regex.exec(original_replacement_value);
4873 console.log(regex_array);
4874 let regex_result = regex_array[0];
4875 // The regex returned array is weird, the 0 index is the only accessible index by number.
4876 // The rest of the entries are named, it's like a mix of an array and a object/dictionary/hashref
4877 let trailing_number_start_index = regex_array["index"];
4878 let leading_string = original_replacement_value.slice(0, trailing_number_start_index);
4879 let increment_start_value = Number(regex_result);
4880 if(regex_result[0] == "0" && regex_result != "0"){
4881 leading_value = '0';
4882 }
4883 let increment_amount = 1;
4884 if(regex_array[0] != '' && this_reference.drag_increment_mode){
4885 for(let i = starting_cell_index+1; i <= ending_cell_index; i++){
4886 copy_target_array.push(this_reference.GLOBAL_JSON_RESULT[i]);
4887 let final_incremented_value = `${leading_string}${leading_value}${increment_start_value + increment_amount}`;
4888 increment_amount++;
4889 this_reference.GLOBAL_JSON_RESULT[i][active_cell.headers] = final_incremented_value;
4890 }
4891 }else{
4892 for(let i = starting_cell_index+1; i <= ending_cell_index; i++){
4893 copy_target_array.push(this_reference.GLOBAL_JSON_RESULT[i]);
4894 this_reference.GLOBAL_JSON_RESULT[i][active_cell.headers] = original_replacement_value;
4895 }
4896 }
4897 //Handling filtered results.
4898 }else{
4899
4900 console.log("Different Lengths");
4901
4902 this_reference.modified_data["source_column"] = active_cell.headers;
4903 this_reference.modified_data["source_data"] = this_reference.GLOBAL_JSON_RESULT[starting_cell_index];
4904
4905
4906 let original_replacement_value = this_reference.GLOBAL_JSON_RESULT[starting_cell_index][active_cell.headers]
4907 let increment_regex = new RegExp('\\d*\\.?\\d*$');
4908 let leading_value = '';
4909 // exec returns an array of result, index, matching groups
4910 let regex_array = increment_regex.exec(original_replacement_value);
4911 console.log(regex_array);
4912 let regex_result = regex_array[0];
4913 // The regex returned array is weird, the 0 index is the only accessible index by number.
4914 // The rest of the entries are named, it's like a mix of an array and a object/dictionary/hashref
4915 let trailing_number_start_index = regex_array["index"];
4916 let leading_string = original_replacement_value.slice(0, trailing_number_start_index);
4917 let increment_start_value = Number(regex_result);
4918 if(regex_result[0] == "0" && regex_result != "0"){
4919 leading_value = '0';
4920 }
4921 let increment_amount = 1;
4922 if(regex_array[0] != '' && this_reference.drag_increment_mode){
4923 for(let i = starting_cell_index+1; i <= ending_cell_index; i++){
4924 copy_target_array.push(this_reference.GLOBAL_JSON_RESULT_FILTERED[i]);
4925 let final_incremented_value = `${leading_string}${leading_value}${increment_start_value + increment_amount}`;
4926 increment_amount++;
4927 this_reference.GLOBAL_JSON_RESULT_FILTERED[i][active_cell.headers] = final_incremented_value;
4928 }
4929 }else{
4930 for(let i = starting_cell_index+1; i <= ending_cell_index; i++){
4931 copy_target_array.push(this_reference.GLOBAL_JSON_RESULT_FILTERED[i]);
4932 this_reference.GLOBAL_JSON_RESULT_FILTERED[i][active_cell.headers] = original_replacement_value;
4933 }
4934 }
4935 }
4936 this_reference.modified_data["copy_targets"] = copy_target_array;
4937 //REMOVE THIS
4938 }catch(e){
4939 console.log(e);
4940 window.appToast && window.appToast({ message: 'You set the primary key wrong.', type: 'error' });
4941 }
4942 //Send the data to the server to save it
4943 this_reference.save_data(`grid-${this_reference.grid_name}`, Number(this_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML), 'copy');
4944 //this_reference.create_table(`grid-${this_reference.grid_name}`, '', Number(this_reference.shadowRoot.querySelector('.pagination_selected_link').innerHTML));
4945 }else{
4946 window.appToast && window.appToast({ message: 'Cannot copy data into different columns.', type: 'error' });
4947 }
4948
4949
4950 }else if(this_reference.copy_lock && this_reference.starting_cell == e.target){
4951 // Do nothing, can't copy into the same cell, that'd be silly.
4952 }else if(this_reference.copy_lock){
4953 window.appToast && window.appToast({ message: 'Cannot copy data into different columns.', type: 'error' });
4954 }
4955 //We're done copying, everything has finished, reset the cells.
4956 this_reference.ending_cell = null;
4957 this_reference.starting_cell = null;
4958 this_reference.copy_lock = false;
4959 }
4960 this.style.cursor = "default";
4961 }
4962 // Handler for moving while copy dragging, highlights passed over cells and adds them to a list to be processed by cell_up_handler
4963 // Not to be called from the template.
4964 cell_copy_move_handler(e){
4965 e.preventDefault();
4966 let this_reference = e.currentTarget;
4967 const hovered_element = this_reference.shadowRoot.elementFromPoint(e.clientX, e.clientY);
4968
4969 let column_name = this_reference.starting_cell.getAttribute("headers");
4970 let row_parent = hovered_element.closest("TR");
4971 let starting_row_number = Number(this_reference.starting_cell.parentElement.id.replace("row", ''));
4972 let current_row_number = Number(row_parent.id.replace("row", ''));
4973 let table_parent = row_parent.parentElement;
4974
4975 let copy_array_index = this_reference.copy_element_array.indexOf(hovered_element);
4976 let corrected_element;
4977 if(this_reference.starting_cell.headers != hovered_element.headers){
4978 corrected_element = this_reference.shadowRoot.querySelector(`#row${current_row_number}`).querySelector(`td[headers='']`);
4979 copy_array_index = this_reference.copy_element_array.indexOf(corrected_element);
4980 }
4981
4982 if(hovered_element.nodeName == "TD" && copy_array_index < 0){
4983 // If they're not hovering the correct column, we must grab and highlight the proper element.
4984 if(this_reference.starting_cell.headers != hovered_element.headers){
4985 let corrected_element = this_reference.shadowRoot.querySelector(`#row${current_row_number}`).querySelector(`td[headers='']`);
4986 let cell_targets = table_parent.querySelectorAll(`td[headers="${column_name}"]`);
4987 for(let i = starting_row_number; i < current_row_number; i++){
4988 cell_targets[i].classList.add("copied_over");
4989 }
4990 this_reference.copy_element_array.push(corrected_element);
4991 }else{
4992 let cell_targets = table_parent.querySelectorAll(`td[headers="${column_name}"]`);
4993 for(let i = starting_row_number; i < current_row_number; i++){
4994 cell_targets[i].classList.add("copied_over");
4995 }
4996 this_reference.copy_element_array.push(hovered_element);
4997
4998 }
4999 }else if(hovered_element.nodeName == "TD" && (copy_array_index < this_reference.copy_element_array.length - 1)){
5000 const removed_element = this_reference.copy_element_array.pop();
5001 let cell_targets = table_parent.querySelectorAll(`td[headers="${column_name}"]`);
5002 for(let i = cell_targets.length-1; i > current_row_number; i--){
5003 cell_targets[i].classList.remove("copied_over");
5004 }
5005 }
5006 }
5007 // Used by the excel paste, copy drag, and delete actions, along with their respective undos.
5008 // Uses the component variable modified data to keep track of what needs to be saved or not.
5009 // Not to be called from the template.
5010 save_data(grid_id, pagination_number, action){
5011
5012 if(action == "excel_paste" || action == "copy"){
5013 document.body.style.cursor = "wait";
5014
5015 }
5016 let data_to_send = this.modified_data;
5017 console.log(data_to_send);
5018 let dummy_url = this.save_data_url;
5019
5020 console.log(`Save Data Action to take: ${action}`);
5021 let save_url_json_payload = `${dummy_url}${action}=1`;
5022 this.send_json(save_url_json_payload, data_to_send).then(responseContent =>{
5023 if(responseContent.includes(',')){
5024 let split_response = responseContent.split(',');
5025 let first_insert_id = split_response[0];
5026 let insert_amount = split_response[1];
5027 this.UNDO_INSERT_STARTING_ID = split_response[0];
5028 this.UNDO_INSERT_AMOUNT = split_response[1];
5029 }
5030 this.load_global_json(this.data_url).then(jsonData =>{
5031 let filtered_data = this.escape_json(jsonData);
5032 this.GLOBAL_JSON_RESULT = filtered_data;
5033 this.GLOBAL_JSON_RESULT_FILTERED = Array.from(this.GLOBAL_JSON_RESULT);
5034 document.body.style.cursor="default";
5035
5036 this.create_table(grid_id, '', pagination_number);
5037 });
5038 });
5039 }
5040
5041}
5042// Helper function to be called by the template to generate this grid. Provided a json configuration file
5043// Refer to template for example
5044function generate_grid_config(config){
5045 let id = config["grid_id"];
5046 let load_data_url = config["load_url"];
5047 let save_data_url = config["save_url"];
5048 let load_filters_url = config["load_filters_url"];
5049 let save_filters_url = config["save_filters_url"];
5050 //! Micah edit
5051 // Put new config items here
5052 let check_highlight_row_url = config["check_highlight_row_url"];
5053 //!
5054 let insert_row_url = config["insert_row_url"];
5055 // Apply the grid id to the load_filter url, as we need it upon grid load.
5056 if(load_filters_url){
5057 load_filters_url += `&grid_id=grid-${id}`;
5058 }
5059
5060 let primary_key = config["primary_key"];
5061 let contextmenu = config["contextmenu"];
5062 let stylesheet_injection = config["stylesheet"];
5063 let toolbar_allowed = config["toolbar_allowed"];
5064 let excel_button = config["excel_button"];
5065 let increment_button = config["increment_button"];
5066 let drag_copy_features = config["drag_copy_features"];
5067 let filter_notification = config["filter_notification"];
5068 let zoom_button = config["zoom_button"];
5069 let new_grid = document.createElement("shawn-grid");
5070 let drag_increment_mode = config["drag_increment_mode"];
5071 let scroll_allowed = config["scroll_allowed"];
5072 let multiple_links = config["multiple_links"];
5073 let highlight_checkbox = config["highlight_checkbox"];
5074 let highlight_checked = config["highlight_checked"];
5075 let max_results = config["max_results"] || 50;
5076
5077
5078 new_grid.setAttribute('name', id);
5079
5080
5081 new_grid.configuration = config;
5082 new_grid.contextmenu = contextmenu;
5083 new_grid.data_url = load_data_url;
5084 new_grid.save_data_url = save_data_url;
5085 new_grid.insert_row_url = insert_row_url;
5086 new_grid.save_filters_url = save_filters_url;
5087 new_grid.load_filters_url = load_filters_url;
5088 new_grid.check_highlight_row_url = check_highlight_row_url;
5089 new_grid.primary_key = primary_key;
5090 // This is a hardcoded value for the AZ_Project, still needs to be addressed
5091 new_grid.url_identifier = 'project_id';
5092 new_grid.stylesheet_injection = stylesheet_injection;
5093 new_grid.toolbar_allowed = toolbar_allowed;
5094 new_grid.drag_increment_mode = drag_increment_mode;
5095 new_grid.scroll_allowed = scroll_allowed;
5096 new_grid.increment_button = increment_button;
5097 new_grid.excel_button = excel_button;
5098 new_grid.drag_copy_features = drag_copy_features;
5099 new_grid.filter_notification = filter_notification;
5100 new_grid.zoom_button = zoom_button;
5101 new_grid.multiple_links = multiple_links;
5102 new_grid.highlight_checkbox = highlight_checkbox;
5103 new_grid.highlight_checked = highlight_checked;
5104 new_grid.max_results = max_results;
5105
5106 window.gridReference.set(`grid-${id}`, new_grid);
5107 return new_grid;
5108}
5109window.gridReference = new Map();
5110//Automatically define the grid when imported.
5111customElements.define('shawn-grid', ShawnGrid);
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help