-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathwsGrid.js
More file actions
3006 lines (2596 loc) · 108 KB
/
wsGrid.js
File metadata and controls
3006 lines (2596 loc) · 108 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/**
* @class
* This module contains a Grid class that will create a table/grid for displaying and editing tabular data.
*
*Options:
*
* column_defaults: - An object containing default values for all columns.
* column_model: - a list of options that will define how each column displays it's data.
* name - String - Name of data field passed into grid.
* label - String - Label to show at the top of the column.
* visible - Boolean - Is the column visible?
* width - Number - How wide to make the column, (number only) but this is calculated in pixels.
* align - String - A string: left, right, or center.
* fixed - Boolean - When loading always start with the width given.
* type - String/function - The type is used to determine the sorting.
* Valid values are: text, string, number, date, datetime, time, dropdown
* NOTE: you can also assign a function and it will run the custom function.
* options - Object - Key/value pairs for a type: "dropdown" column.
* editable - Boolean/function - Can this column be edited?
* frozen_left - Boolean - Is this column locked in place on the left?
* frozen_right - Boolean - Is this column locked in place on the left?
* classes - String/function - Custom CSS classes to apply to the column
* format - String/function - On how to format the data, a function should return a string.
* String can be 'date', 'currency', 'boolean', or 'nonzero'
* style - String/function - Is or returns a string of CSS styles to apply to the cell
* max_length - Number - How long can the string/number be?
* min_length - Number - Minimum length of input.
*
* column_reorder: - Enable reordering of columns using drag and drop.
* connection:
* type - String - Type of connection used: 'Ajax' or 'Socket'.
* url - String - URL used to connect to the server.
* examples: '/api/grid/data', 'wss://example.com/api/grid/data'
* currency: - Override function for how to format numbers as currency.
* events: - an object containing user orverride event functions as elements.
* all events are executed with 'this' set to the current grid.
* click( row, column, row_data ) - Click event for a given cell.
* dblclick( row, column_name, row_data ) - Double click event for cells, the grid passes in row,
* column name, and row data.
* load_complete( data ) - After the data has been given to the grid,
* but before the ui is generated.
* data_loaded( data ) - After the data is loaded, but before the grid is generated.
* data_changed( change ) - Fires when the internal data for the grid is changed.
* change contains row_id, column_name, new_value, old_value,
* row_classes( row_id, row_data ) - Allows the user to apply classes to a row, return array of strings.
* cell_classes( row_id, column_name, row_data ) - Allows the user to apply classes to all cells, return array of strings.
*
* sort_row - After the user has manually changed the row order.
* after_edit - After the user has edited the data, and the editor has closed.
* after_save - After the editor has closed and the data has been saved.
*
* before_inline_opened( row, column_name, value, row_data ) - return '' to prevent a dialog opening.
* before_close( row, column_name, value, row_data ) - can prevent the editor from closing and loosing focus.
* before_inline_submitted( row, column_name, value, row_data ) - can manipulate the data before it's saved to the local data model.
* bindable events:
* cell.changed
* column.moved
* recordset.changed
* row.moved
* row.resized
* selection.changed
* filters: - An object with filtering functions.
* height: - Height of grid. Set the height to an empty string to allow the grid to be the height of the data.
* id: - ID of DOM element that will contain this grid.
* overflow: - Allow the columns to overflow the width of the grid. (default: false)
* width: - Width of grid.
* grouping: - Array of objects containing grouping options. A grouping object can contain the following options:
* column: - column name to sort or
* sort_order - sort order asc, or desc
* header
* footer
*
**/
import { Number_Utility } from './number_utility.js';
import { Object_Base } from './object_base.js';
/**
* Define names for parts of the grid.
*/
const wsgrid_prefix = 'wsgrid_';
const wsgrid_table = `${wsgrid_prefix}_table`;
const wsgrid_header = `${wsgrid_prefix}_header`;
const wsgrid_body = `${wsgrid_prefix}_body`;
const wsgrid_footer = `${wsgrid_prefix}_footer`;
const wsgrid_row = `${wsgrid_prefix}_row`;
const wsgrid_column = `${wsgrid_prefix}_column`;
const wsgrid_cell = `${wsgrid_prefix}_cell`;
const wsgrid_editor = `${wsgrid_prefix}_editor`;
const wsgrid_totals = `${wsgrid_prefix}_totals`;
const wsgrid_multiselect = `${wsgrid_prefix}_multiselect`;
const wsgrid_data = `${wsgrid_prefix}_data`;
function _default_format( value ) {
if( value === undefined ) {
return '';
}
return value;
}
/**
* This function allows the user to specify a function to test if deleting
* a record should be allowed. It returns a Promise so that it can prompt or
* do other async calls to make the determination.
* @param {Number} row_id - ID of the row to be deleted.
* @param {String} column_name - Name of the column clicked on.
* @param {Object} row_data - Data for the row in question.
* @constructor
* @return {Promise} - return a Promise to know if we should delete the record.
*/
function _default_delete_record( row_id, column_name, row_data ) {
return new Promise( ( resolve, reject ) => {
resolve();
} );
}
/**
* Default values for the column options.
*/
let column_defaults = {
align: 'left',
classes: '',
editable: false,
fixed: true,
format: _default_format,
frozen_left: false,
frozen_right: false,
label: '',
locked: false,
max_length: undefined,
min_length: undefined,
name: '',
options: {},
sort: undefined,
style: '',
tooltip: '',
type: 'text',
visible: true,
width: 100,
};
/**
* Define default values for grid options.
*/
let grid_defaults = {
background: 'white',
background_alt: 'lightcyan',
cell_word_wrap: true,
column_defaults: {},
column_model: [],
column_reorder: false,
column_resize: true,
column_sort: true,
connection_type: 'socket',
connection_options: {
url: '',
},
delete_record: _default_delete_record,
events: {},
filters: [],
grouping_model: [],
height: 200,
multi_select: false,
overflow: true,
row_reorder: false,
sort_column: '',
sort_direction: 'asc',
width: 200,
};
/**
* This structure contains data about the cells.
* each cell has a changed flag and classes placeholder.
*
* These contain the state of the cell so that when we refresh
* the grid those changes are not lost.
*/
let cell_metadata = {
background: 'white',
changed: false,
classes: '',
old_value: undefined,
selected: false,
};
/**
* Convert HTML entities into encoded elements that can be displayed on the page.
* @param {String} string - String we are parsing
* @return {String} - New string with the HTML elements converted.
*/
function convert_html_entities( string ) {
let tags_to_replace = {
'&': '&',
'<': '<',
'>': '>',
'"': '"',
};
return string.replace( /[&<>\"]/g, ( tag ) => {
return tags_to_replace[ tag ] || tag;
} );
}
/**
* Test if the HTML Element we are looking at is a part of this grid.
* If so return true otherwise return false.
*
* @param {HTMLElement} target - element that we are testing
* @return {Boolean} - is the target element a part of the grid?
*/
function _is_grid_element( target ) {
if( target.tagName == 'TABLE'
|| target.tagName == 'TH'
|| target.tagName == 'TR'
|| target.tagName == 'TD'
|| target.classList.contains( `${wsgrid_multiselect}_header` )
|| target.classList.contains( `${wsgrid_header}_column_move_target` )
|| target.classList.contains( `${wsgrid_editor}_main_editor` )
) {
return true;
}
return false;
}
/**
* Grid class.
* @class
*/
export class Grid extends Object_Base {
/**
* Constructor for the Grid class. Initialize the class using options from the user.
* @param {Object} options - a list of options supplied by the user, these override the defaults.
*/
constructor( options ) {
super( options );
this._required_options( options, [
'id', 'column_model',
] );
//extend the default options with the user options
let all_options = Object.assign( {}, grid_defaults, options );
this._setup_object( all_options );
// Make sure the element the user wants is actually in the DOM, if not throw an error the user can figure out.
let gridElement = document.getElementById( this.id );
if( gridElement == null ) {
throw new Error( `Could not find grid element. Is ${this.id} an element in the DOM?` );
}
// keep track of the drag events.
this.drag = {
started: false,
type: undefined,
};
// keep track of the event that triggered the inline editor to open.
this.event_trigger = undefined;
// Define empty structure for the data to be displayed in the grid.
this.data = [];
// Define empty structure for the metadata about each cell in the grid.
this.metadata = [];
// Define placeholder structure for the footer row.
this.totals_data = undefined;
this._modify_style_sheet();
/**
* Is the data being displayed filtered? Used to determine if
* the filters should be applied when the grid is refreshed.
*/
this.is_filtered = false;
this._create_lookup_tables( all_options.column_model, all_options.grouping_model );
this._create_lookup_tables( all_options.column_model, all_options.grouping_model, 'original_column_settings' );
this._calculate_columns();
this.word_wrap( this.cell_word_wrap );
this.grid = this._create_base_table();
this.sort_column = '';
this.sort_direction = 'asc';
// conect events.
this.grid.addEventListener( 'click', ( event ) => { this.click.call( this, event ); } );
this.grid.addEventListener( 'dblclick', ( event ) => { this.dblclick.call( this, event ); } );
this.grid.addEventListener( 'change', ( event ) => { this.change.call( this, event ); } );
this.grid.addEventListener( 'load_complete', ( event ) => { this.load_complete.call( this, event ); } );
this.grid.addEventListener( 'mousedown', ( event ) => { this.mousedown.call( this, event ); } );
this.grid.addEventListener( 'mousemove', ( event ) => { this.mousemove.call( this, event ); } );
this.grid.addEventListener( 'mouseup', ( event ) => { this.mouseup.call( this, event ); } );
this.grid.addEventListener( `${wsgrid_data}.cell_changed`, ( event ) => { this.data_changed.call( this, event ); } );
this.grid.addEventListener( `${wsgrid_data}.row_moved`, ( event ) => { this.row_moved.call( this, event ); } );
window.addEventListener( 'resize', ( event ) => { this.resize.call( this, event ); } );
// Needed for grid resizing.
this.grid.style.position = 'relative';
}
/**
* This function turns the column model inside out so we can do property lookups using the column name.
* @param {Object[]} column_model - Column model used to create this grid.
* @param {Object[]} grouping_model - Grouping model used to create grouping headers.
*/
_create_lookup_tables( column_model = null, grouping_model = null, destination = 'columns' ) {
if( column_model === null ) {
column_model = this.column_model;
}
if( grouping_model === null ) {
grouping_model = this.grouping_model;
}
let colCount = column_model.length;
// Define empty object to contain all properties by column.
this[ destination ] = {};
this[ destination ].order = [];
// loop through the columns and create a set of lookup tables for all properties.
for( let i = 0; i < colCount; i++ ) {
// fill in any missing default values.
this.column_model[ i ] = Object.assign(
{}, // new object to become the model
column_defaults, // built-in default values
this.column_defaults, // User overrides for default values
column_model[ i ] // User created column model
);
let keys = Object.keys( column_defaults );
let column_name = this.column_model[ i ].name;
// Keep track of the column order.
this[ destination ].order[ i ] = column_name;
// keep track of all properties by column name.
for( let key of keys ) {
if( typeof( this[ destination ][ key ] ) == 'undefined' ) {
this[ destination ][ key ] = {};
}
this[ destination ][ key ][ column_name ] = this.column_model[ i ][ key ];
}
}
this.grouping = {
columns: [],
sort_order: {},
header: {},
footer: {},
};
if( grouping_model.length != 0 ) {
for( let i = 0; i < grouping_model.length; i++ ) {
this.grouping.columns[ i ] = grouping_model[ i ].column;
this.grouping.sort_order[ grouping_model[ i ].column ] = grouping_model[ i ].sort_order;
this.grouping.header[ grouping_model[ i ].column ] = grouping_model[ i ].header;
this.grouping.footer[ grouping_model[ i ].column ] = grouping_model[ i ].footer;
}
}
}
/**
* Modify the stylesheet with some settings from the user.
* We're modifying the stylesheet directly because adding styles directly
* to the elements breaks the row selection coloring.
*/
_modify_style_sheet() {
this._modify_stylesheet_rule( `.${wsgrid_row}_odd`, {
'background-color': this.background,
} );
this._modify_stylesheet_rule( `.${wsgrid_row}_even`, {
'background-color': this.background_alt
} );
}
/**
* Modify the stylesheet with some settings from the user.
* We're modifying the stylesheet directly because adding styles directly
* to the elements breaks the row selection coloring.
*
* @param {String} selector - CSS selector
* @param {Object} properties - Object of key/value pairs representing properties and the values to set them to.
*/
_modify_stylesheet_rule( selector, properties ) {
for( let s = 0; s < document.styleSheets.length; s++ ) {
let rules = document.styleSheets[ s ].cssRules;
let ruleCount = rules.length;
for( let r = 0; r < ruleCount; r++ ) {
if( rules[ r ].selectorText == selector ) {
let rule = rules[ r ];
for( let key in properties ) {
rule.style[ key ] = properties[ key ];
}
}
}
}
}
/**
* Modify the background color of the odd rows.
* This function programmatically updates the CSS for the odd rows.
* @param {String} color - HTML color or Hex value.
*/
background_odd( color ) {
if( typeof( color ) == 'undefined' ) {
return this.background;
}
else {
this.background = color;
this._modify_style_sheet();
}
}
/**
* Modify the background color of the even rows.
* This function programmatically updates the CSS for the even rows.
* @param {String} color - HTML color or Hex value.
*/
background_even( color ) {
if( typeof( color ) == 'undefined' ) {
return this.background_alt;
}
else {
this.background_alt = color;
this._modify_style_sheet();
}
}
/**
* Create the base table element and the header for the table
* based on the column model information.
*/
_create_base_table() {
let table_header = this._generate_column_headers();
let html = `<table class="${wsgrid_table} ${wsgrid_table}_${this.id}">`
+ `<thead class="${wsgrid_header} ${wsgrid_header}_${this.id}">${table_header}</thead>`
+ `<tbody class="${wsgrid_body} ${wsgrid_body}_${this.id}"></tbody>`
+ `<tfoot class="${wsgrid_footer}"></tfoot>`
+ '</table>';
// Insert Table
document.getElementById( this.id ).innerHTML = html;
return document.querySelector( `table.${wsgrid_table}_${this.id}` );
}
/**
* using all the properties from the grid figure out
* what all the widths for all the columns should be
*
* Rules for calculating column widths:
* 1) fixed widths are always the same width. (fixed)
* 2) all remaining columns are calculated as a percentage of the remaining space.
* That percentage is calculated from the default width of the column / the width of all flexable columns.
*
* @param {Boolean} [user_set=false] - If the user changes the column width, don't override
* it with variable column size calculations
*/
_calculate_columns( user_set = false ) {
let count = this.column_model.length;
let grid_width = document.getElementById( this.id ).offsetWidth;
if( this.row_reorder ) {
grid_width -= 5;
}
if( this.multi_select ) {
grid_width -= 20;
}
let fixed_width = 0;
let fixed_width_visible = 0;
let flex_width = 0;
// loop through the columns and gather info
for( let i = 0; i < count; i++ ) {
let column_name = this.columns.order[ i ];
let isFixed = this.columns.fixed[ column_name ];
fixed_width += ( isFixed ? this.columns.width[ column_name ] : 0 );
flex_width += ( isFixed ? 0 : this.columns.width[ column_name ] );
if( this.columns.visible[ column_name ] ) {
fixed_width_visible += ( isFixed ? this.columns.width[ column_name ] : 0 );
}
}
// calculate the widths of flexable columns.
let remaining_width = grid_width - fixed_width_visible;
for( let i = 0; i < count; i++ ) {
let column_name = this.columns.order[ i ];
let new_width = this.columns.width[ column_name ];
if( ! user_set && ! this.columns.fixed[ column_name ] ) {
let percent = new_width / flex_width;
new_width = Math.floor( remaining_width * percent );
}
this.columns.width[ column_name ] = new_width;
}
if( this.overflow ) {
this.min_row_width = fixed_width + flex_width;
}
else {
this.min_row_width = 0; //grid_width; //fixed_width + flex_width;
}
}
/**
* Mouse down resize event handler, start the resize event.
* @param {Event} e - trigger event for resize start.
*/
_column_resize_start( e ) {
e.preventDefault();
if( typeof( this.seperator ) == 'undefined' ) {
this.seperator = document.createElement( 'div' );
this.seperator.id = `${wsgrid_header}_column_resize_visual`;
this.seperator.dataset.width_delta = 0;
}
this.seperator.dataset.column = e.target.dataset.column;
let rect = this.grid.getBoundingClientRect();
this.seperator.style.top = `${rect.top}px`;
this.seperator.style.height = `${rect.height}px`;
this.seperator.style.left = `${e.pageX}px`;
this.seperator.start_drag = e.pageX;
this.grid.append( this.seperator );
}
/**
* Mouse move resize event handler
* @param {Event} e - trigger event for mouse moving
*/
_column_resize( e ) {
e.preventDefault();
this.seperator.dataset.width_delta = ( e.pageX - this.seperator.start_drag );
this.seperator.style.left = `${e.pageX}px`;
}
/**
* Mouse up resize event handler
* @param {Event} e - trigger event for mouse up
*/
_column_resize_end( e ) {
e.preventDefault();
let column_name = this.seperator.dataset.column;
let delta = Number( this.seperator.dataset.width_delta );
if( this.columns.frozen_right[ column_name ] ) {
delta *= -1;
}
let width = Number( this.columns.width[ column_name ] ) + delta;
this.set_column_width( column_name, width );
// reset delta so we don't keep changning the column width, by just clicking on it.
this.seperator.dataset.width_delta = 0;
this.seperator.parentElement.removeChild( this.seperator );
}
/**
* Mouse down column move event handler, start the column move event.
* @param {Event} e - trigger event for mouse down
*/
_column_move_start( e ) {
e.preventDefault();
if( typeof( this.seperator ) == 'undefined' ) {
this.seperator = document.createElement( 'div' );
this.seperator.id = `${wsgrid_header}_column_resize_visual`;
}
this.seperator.dataset.column = e.target.dataset.column;
let rect = this.grid.getBoundingClientRect();
this.seperator.style.top = `${rect.top}px`;
this.seperator.style.height = `${rect.height}px`;
this.seperator.style.left = `${e.pageX}px`;
this.seperator.start_drag = e.pageX;
this.grid.append( this.seperator );
this.grid.style.cursor = 'move';
}
/**
* Mouse move column move event handler
* @param {Event} e - trigger event for mouse moving
*/
_column_move( e ) {
e.preventDefault();
let header_row = this.grid.querySelector( 'thead tr' );
let children = header_row.children;
for( let i = 0; i < children.length; i++ ) {
let rect = children[ i ].getBoundingClientRect();
let split = rect.left + ( rect.width * ( 2 / 3 ) );
if( e.pageX > rect.left && e.pageX <= rect.right ) {
if( e.pageX < split ) {
this.seperator.style.left = `${rect.left}px`;
continue;
}
else {
this.seperator.style.left = `${rect.right}px`;
continue;
}
}
}
}
/**
* Mouse up column move event handler
* @param {Event} e - trigger event for mouse up
*/
_column_move_end( e ) {
e.preventDefault();
// Only change the column position if we actually moved the column.
if( Math.abs( this.seperator.start_drag - e.pageX ) > 3 ) {
let column_name = this.seperator.dataset.column;
let next_column = undefined;
let header_row = this.grid.querySelector( 'thead tr' );
let children = header_row.children;
for( let i = 0; i < children.length; i++ ) {
let rect = children[ i ].getBoundingClientRect();
let split = rect.left + ( rect.width * ( 2 / 3 ) );
if( e.pageX > rect.left && e.pageX <= rect.right ) {
if( e.pageX < split ) {
this.seperator.style.left = `${rect.left}px`;
next_column = children[ i ];
continue;
}
else {
this.seperator.style.left = `${rect.right}px`;
next_column = children[ i + 1 ];
continue;
}
}
}
let next_column_name = undefined;
if( typeof( next_column ) !== 'undefined' ) {
next_column_name = next_column.dataset.column;
}
if( column_name != next_column_name ) {
this.set_column_position( column_name, next_column_name );
}
}
this.seperator.parentElement.removeChild( this.seperator );
this.grid.style.cursor = '';
}
/**
* When the mousedown event is triggered on the correct elements this row move start event is triggered.
* @param {Event} e - The event that triggered this call.
*/
_row_move_start( e ) {
e.preventDefault();
if( typeof( this.row_seperator ) == 'undefined' ) {
this.row_seperator = document.createElement( 'div' );
this.row_seperator.id = `${wsgrid_header}_row_resize_visual`;
}
this.row_seperator.dataset.source = e.target.dataset.rowid;
let rect = this.grid.getBoundingClientRect();
this.row_seperator.style.left = `${rect.left}px`;
this.row_seperator.style.width = `${rect.width}px`;
this.row_seperator.style.top = `${e.pageY}px`;
this.row_seperator.start_drag = e.pageY;
this.grid.append( this.row_seperator );
this.grid.style.cursor = 'move';
}
/**
* When the mousemove event is triggered on the correct elements this row move event is triggered.
* @param {Event} e - The event that triggered this call.
*/
_row_move( e ) {
e.preventDefault();
let move_targets = this.grid.querySelectorAll( `.${wsgrid_column}_row_move_target` );
for( let i = 0; i < move_targets.length; i++ ) {
let rect = move_targets[ i ].getBoundingClientRect();
let split = rect.top + ( rect.height * ( 2 / 3 ) );
if( e.pageY > rect.top && e.pageY <= rect.bottom ) {
if( e.pageY < split ) {
this.row_seperator.style.top = `${rect.top}px`;
continue;
}
else {
this.row_seperator.style.top = `${rect.bottom}px`;
continue;
}
}
}
}
/**
* When the mouseup event is triggered on the correct elements this row move end event is triggered.
* @param {Event} e - The event that triggered this call.
*/
_row_move_end( e ) {
let previous_row = 0;
e.preventDefault();
let move_targets = this.grid.querySelectorAll( `.${wsgrid_column}_row_move_target` );
for( let i = 0; i < move_targets.length; i++ ) {
let rect = move_targets[ i ].getBoundingClientRect();
let split = rect.top + ( rect.height * ( 2 / 3 ) );
if( e.pageY > rect.top && e.pageY <= rect.bottom ) {
if( e.pageY < split ) {
this.row_seperator.style.top = `${rect.top}px`;
previous_row = move_targets[ i ];
continue;
}
else {
this.row_seperator.style.top = `${rect.bottom}px`;
previous_row = move_targets[ i ];
continue;
}
}
}
let previous_record = 0;
if( previous_row ) {
previous_record = previous_row.dataset.rowid;
}
this.set_row_position( this.row_seperator.dataset.source, previous_record );
this.row_seperator.parentElement.removeChild( this.row_seperator );
this.grid.style.cursor = '';
}
/**
* Fill the grid with the given data and generate a table for display.
* @param {Array} data - Array of objects containing data in the form key: value
*/
display( data ) {
if( typeof( data ) != 'undefined' ) {
this.data = data;
this.metadata = [];
let size = this.data.length;
for( let i = 0; i < size; i++ ) {
let row = {};
for( let c = 0; c < this.columns.order.length; c++ ) {
// clone the metadata for each cell.
row[ this.columns.order[ c ] ] = Object.assign( {}, cell_metadata );
}
this.metadata.push( row );
}
}
this._sort_data( '', 'asc' );
this.refresh();
let e = new Event( 'recordset.changed', { bubbles: true } );
this.grid.dispatchEvent( e );
}
/**
* Generate the table rows using the internal data array
* @return {String} - the row data as a string of HTML
*/
_generate_rows() {
let row_html = '';
let count = this.data.length;
let zebra = 1;
for( let i = 0; i < count; i++ ) {
if( this._is_selected( i ) == false ) {
continue;
}
let classes = '';
if( zebra % 2 == 0 ) {
classes += ` ${wsgrid_row}_even `;
}
else {
classes += ` ${wsgrid_row}_odd `;
}
zebra++;
// Generate the grouping headers
if( this.grouping.columns.length > 0 ) {
let group_count = this.grouping.columns.length;
let flag_new_group = false;
for( let g = 0; g < group_count; g++ ) {
let column = this.grouping.columns[ g ];
if( i == 0
|| this.data[ i ][ column ] !== this.data[ i - 1 ][ column ]
|| flag_new_group
) {
flag_new_group = true;
if( typeof( this.grouping.header ) !== 'undefined' ) {
if( typeof( this.grouping.header[ column ] ) == 'function' ) {
row_html += `<tr class="${wsgrid_row}_group_header ${wsgrid_row}_group_header_${column}">`
+ `<td class="${wsgrid_column}_group_header ${wsgrid_column}_group_header_${column}">`
+ this.grouping.header[ column ]( column, this.data[ i ][ column ], this.data[ i ] )
+ '</td></tr>';
}
else {
row_html += `<tr class="${wsgrid_row}_group_header ${wsgrid_row}_group_header_${column}">`
+ `<td class="${wsgrid_column}_group_header ${wsgrid_column}_group_header_${column}">`
+ this.grouping.header[ column ]
+ '</td></tr>';
}
}
}
}
}
// Generate regular rows
row_html += this._generate_row( i, this.data[ i ], '', classes );
// Generate the grouping footer
if( this.grouping.columns.length > 0 ) {
let group_count = this.grouping.columns.length;
let flag_new_group = false;
for( let g = 0; g < group_count; g++ ) {
let column = this.grouping.columns[ g ];
if( i == 0
|| this.data[ i ][ column ] !== this.data[ i - 1 ][ column ]
|| flag_new_group
) {
flag_new_group = true;
if( typeof( this.grouping.footer ) !== 'undefined' ) {
if( typeof( this.grouping.footer[ column ] ) == 'function' ) {
row_html += `<tr class="${wsgrid_row}_group_footer ${wsgrid_row}_group_footer_${column}">`
+ `<td class="${wsgrid_column}_group_footer ${wsgrid_column}_group_footer_${column}">`
+ this.grouping.footer[ column ]( column, this.data[ i ][ column ], this.data[ i ] )
+ '</td></tr>';
}
else {
row_html += `<tr class="${wsgrid_row}_group_footer ${wsgrid_row}_group_footer_${column}">`
+ `<td class="${wsgrid_column}_group_footer ${wsgrid_column}_group_footer_${column}">`
+ this.grouping.footer[ column ]
+ '</td></tr>';
}
}
}
}
}
}
return row_html;
}
/**
* Generate the HTML for the given row_id.
* @param {Number} row_id - Id of the record to generate HTML for.
* @param {Object} data - All key, value pairs to generate HTML for.
* @param {String} row_classes - List of classes to add to the row.
* @param {String} column_classes - List of classes to add to each column
* @param {Boolean} is_header - Is this row a header row? Default: false.
* @return {String} - All the HTML for this row in a string.
*/
_generate_row( row_id, data, row_classes = '', column_classes = '', is_header = false ) {
let user_classes = [];
if( row_id !== '' && typeof( this.events.row_classes ) == 'function' ) {
user_classes = user_classes.concat( this.events.row_classes( row_id, data ) );
}
else if( row_id !== '' && typeof( this.events.row_classes ) == 'array' ) {
user_classes = user_classes.concat( this.events.row_classes );
}
/*********************************************************************************
* Generate Row
*********************************************************************************/
let row_html = `<tr class="${wsgrid_row} ${wsgrid_row}_${row_id} ${row_classes} ${user_classes.join( ' ' )}"`
+ ( row_id == '' ? '' : `data-rowid="${row_id}"` )
//+ ` style="min-width:${this.min_row_width}px;`
+ ( this.overflow ? '' : `max-width:${this.min_row_width};` )
+ `">`;
let column_type = 'td';
if( is_header ) {
column_type = 'th';
}
if( this.row_reorder ) {
row_html += `<${column_type} class="${wsgrid_column}_row_move_target frozen_left" style="position:sticky;left:0px;z-index:5;width:5px;" data-rowid="${row_id}"></${column_type}>`;
}
if( this.multi_select ) {
row_html += `<${column_type} class="${wsgrid_multiselect}_cell ${column_classes}" style="position:sticky;left:0;z-index:5;">`
+ this._generate_multiselect( ( is_header ? 'header' : row_id ) ) + `</${column_type}>`;
}
for( let column = 0; column < this.columns.order.length; column++ ) {
row_html += this._generate_cell( row_id, column, data, column_type, column_classes, is_header );
}
row_html += '</tr>';
return row_html;
}
/**
* Generate the contents of the cell at row_id, column_id.
* @param {Number} row_id - Row ID of the cell we're generating.
* @param {Number} column_id - Column ID of the cell we're generating.
* @param {Object} data - data to be displayed in the cell.
* @param {String} column_type - Is this a header or body cell? th or td
* @param {String} [column_classes=''] - A list of classes to add to the cell.
* @param {Boolean} [is_header=false] - is this a header cell?
* @return {String} - HTML string containing the contents of the cell.
*/
_generate_cell( row_id, column_id, data, column_type, column_classes = '', is_header = false ) {
let column_name = this.columns.order[ column_id ];
let cell_html = '';
let value = '';
let sort_styling = '';
let move_handle = '';
let resize_handle = '';
if( is_header ) {
let decoration_side = 'left';
if( column_name == this.sort_column ) {
if( this.columns.align[ column_name ] == 'left' ) {
decoration_side = 'right';
}
sort_styling = `<span class="${wsgrid_header}_sort ${wsgrid_header}_column" style="${decoration_side}:5px"><i class="fa fa-sort-${this.sort_direction} fa-sm ${wsgrid_header}_column"></i></span>`;
}
// if this isn't the first column in a multi_select then...
if( this.multi_select && column_id == 0 ) {
move_handle = '';
resize_handle = '';
}
else {
// if the column can be re-ordered and the column isn't frozen, include the move handler.
if( this.column_reorder
&& ( ! this.columns.frozen_right[ column_name ] && ! this.columns.frozen_left[ column_name ] ) ) {
move_handle = `<span class='${wsgrid_header}_column_move_target' data-column='${column_name}'></span>`;
}
if( this.column_resize && ! this.columns.locked[ column_name ] ) {
resize_handle = `<span class="${wsgrid_header}_column_resize" data-column="${column_name}"></span>`;
}
}
value = this.columns.label[ column_name ];
}
else {
// if there is data to display figure out how to display it.
if( typeof( data[ column_name ] ) !== 'undefined' ) {
if( this.columns.type[ column_name ] == 'dropdown' ) {
let val = data[ column_name ];
value = this.columns.options[ column_name ][ val ];
}
else if( typeof( this.columns.format[ column_name ] ) == 'string' ) {
let formatType = this.columns.format[ column_name ];
value = this[ `format_${formatType.toLowerCase()}` ]( data[ column_name ] );
}
else if( typeof( this.columns.format[ column_name ] ) == 'function' ) {
value = this.columns.format[ column_name ]( data[ column_name ], data );
}
else {
value = data[ column_name ];
}
}
// a column can display data even if it doesn't have data from the backend.
else if( typeof( this.columns.format[ column_name ] ) !== 'undefined' ) {
// display data from a user function
if( typeof( this.columns.format[ column_name ] ) == 'function' ) {
value = this.columns.format[ column_name ]( data[ column_name ], data );
}
// display data from a built-in function
else {