/** * @author: joshr@timetrex.com * @description wrapper class for jqgrid in timetrex. * Requires free-jqgrid 4.15.4+ and jquery 3.3.1+ * * @param setup * @constructor */ TTGrid = function( table_id, setup, column_info_array ) { this.getGridId = function() { return this.ui_id; }; this.setTableIDElement = function() { this.table_id_element = $( '#' + this.ui_id ); return true; }; this.getTableIDElement = function() { return this.table_id_element; }; if ( !table_id || !setup || !column_info_array ) { Debug.Text( 'ERROR: constructor requires all 3 arguments', 'TTGrid.js', 'TTGrid', 'constructor', 10 ); return; } this.ui_id = table_id; // Issue #2891 - Forcing GridUnload can cause grids inside a AComboBox to disappear after repeated opening. Check for container_selector is to mitigate that. if ( !setup.container_selector ) { // We are unloading grids with the same ID if they exist to help prevent: Uncaught TypeError: Failed to execute 'replaceChild' on 'Node': parameter 2 is not of type 'Node'. // Looping through array in reverse to make sure no index issues after splicing. for ( var i = LocalCacheData.resizeable_grids.length - 1; i >= 0; i-- ) { if ( LocalCacheData.resizeable_grids[i] != null && LocalCacheData.resizeable_grids[i].grid != null && LocalCacheData.resizeable_grids[i].ui_id == this.ui_id ) { LocalCacheData.resizeable_grids[i].grid.jqGrid( 'GridUnload' ); LocalCacheData.resizeable_grids.splice( i, 1 ); } else if ( LocalCacheData.resizeable_grids[i] == null || LocalCacheData.resizeable_grids[i].grid == null ) { // LocalCacheData.resizeable_grids array gets full of null values from resize event and we are making sure to clear it out. LocalCacheData.resizeable_grids.splice( i, 1 ); } } } var max_height = null; this.setTableIDElement(); var table_div = this.getTableIDElement(); this.grid = null; if ( table_div[0] == false ) { Debug.Text( 'ERROR: table_id not found in DOM', 'TTGrid.js', 'TTGrid', 'constructor', 10 ); return; } //Default grid settings. var default_setup = { container_selector: 'body', sub_grid_mode: false, altRows: true, data: [], datatype: 'local', //quickEmpty: 'true', //Default is 'quickest', might fix JS Exception: Uncaught TypeError: Failed to execute 'replaceChild' on 'Node': parameter 2 is not of type 'Node', but causes this instead: TTGrid.js?v=11.6.1-20191108:99 Uncaught TypeError: Cannot read property 'cells' of undefined sortable: false, height: table_div.parents( '.view' ).height(), width: table_div.parent().width(), rowNum: 10000, colNames: [], colModel: column_info_array, multiselect: true, multiselectWidth: 22, multiboxonly: true, viewrecords: true, autoencode: true, scrollOffset: 0, verticalResize: true, //when the grid resizes do we reisize it vertically? needed for timesheet and subgrid views. resizeGrid: true, winMultiSelect: true // resizeStop: function(width, index){ // //$this.setGridColumnsWidth(width, index); // } }; if ( setup.max_height === true ) { setup.max_height = max_height; } if ( setup ) { setup = $.extend( {}, default_setup, setup ); } else { setup = default_setup; } this.setup = setup; table_div.empty(); //should unbind all events bound to the grid. this.grid = table_div.jqGrid( setup ); LocalCacheData.resizeable_grids.push( this ); if ( setup.winMultiSelect === true ) { this.grid.winMultiSelect(); } //turn off grid resize event (for schedule grids that need to be rebuilt on every resize) this.noResize = function() { this.setup.onResizeGrid = false; }; /** * * @param data */ this.setData = function( data, clear_grid ) { if ( this.grid ) { //Clear grid by default. clear_grid = ( clear_grid == null ) ? true : clear_grid; if ( clear_grid == true ) { this.grid.clearGridData(); } //ACombo might send data as "true" when the unselected grid is blank and "Show All" is clicked. Prevent anything but an array from being passed into setGridParam( 'data' ). if ( Array.isArray( data ) == false ) { data = []; } this.grid.setGridParam( { 'data': data } ).trigger( 'reloadGrid' ); //this.setGridColumnsWidth(); } else { throw( 'ERROR: Grid is not ready yet.' ); } }; this.getData = function() { return this.getGridParam( 'data' ); }; this.getSetup = function() { return this.getGridParam( 'data' ); }; this.getHeight = function() { return $( this.grid ).height(); }; this.getWidth = function() { return $( this.grid ).width(); }; /** * * @param value */ this.deleteRow = function( value ) { this.grid.jqGrid( 'delRowData', value ); }; this.resetSelection = function() { this.grid.resetSelection(); }; this.setSelection = function( x1, y1, x2, y2, noScale ) { this.grid.setSelection( x1, y1, x2, y2, noScale ); }; this.getGridParam = function( parameter_name ) { return this.grid.getGridParam( parameter_name ); }; this.setGridParam = function( parameter_name, value ) { return this.grid.setGridParam( parameter_name, value ); }; this.unload = function() { if ( this.grid ) { this.grid.jqGrid( 'GridUnload' ); this.grid = null; } return true; }; this.clearGridData = function() { this.grid.jqGrid( 'clearGridData', true ); }; this.reloadGrid = function() { this.grid.trigger( 'reloadGrid' ); }; this.getRecordFromGridById = function( id ) { var data = this.grid.getGridParam( 'data' ); var result = null; /* jshint ignore:start */ //id could be string or number. $.each( data, function( index, value ) { if ( value['_id_'] == id ) { result = Global.clone( value ); return false; } } ); /* jshint ignore:end */ if ( result ) { result.id = result['_id_']; } return result; }; this.getGridWidth = function() { if ( this.grid ) { return this.grid.width(); } return 0; }; this.setGridWidth = function( w, inner_width ) { if ( this.grid ) { if ( inner_width > w ) { w = inner_width; } this.grid.setGridWidth( w ); } }; this.getGridHeight = function() { if ( this.grid ) { return this.grid.height(); } return 0; }; this.setGridHeight = function( h ) { if ( this.setup.static_height ) { h = this.setup.static_height; } if ( this.grid ) { this.grid.setGridHeight( h ); } }; this.setRowData = function( id, new_record ) { this.grid.setRowData( id, new_record ); }; this.getRowData = function( row_id ) { var row = false; var row_data = this.getData(); for ( var i in row_data ) { if ( row_data[i].id == row_id ) { row = row_data[i]; break; } } return row; }; this.getColumnModel = function() { return this.getGridParam( 'colModel' ); }; this.setColumnModel = function( val ) { this.getGridParam( 'colModel', val ); }; this.grid2csv = function( filename ) { // TODO: Add in more robust quote escaping to handle data strings that have quotes. Currently we are just wrapping everything in double quotes. let csv_data = []; // Get grid data for csv. // Column headers are in a different table. Seems to be a jqGrid thing? // Parsing column headers using aria-describedby on the first none blank (2nd) which links to the id of the header and from there gets the textContent. let i = 1; let headers = []; for ( let tr of document.querySelectorAll( '#' + this.getGridId() + ' tr' ) ) { let row = []; for ( let td of tr.querySelectorAll( 'td' ) ) { row.push( '"' + td.textContent.trim() + '"' ); if ( i === 2 ) { // Column headers sometimes contain HTML such as Claim
Dependents. // Using a temporary element to replace
with a space and then grabbing the textContent to strip the HTML. let temp_element = document.createElement( 'div' ); temp_element.innerHTML = document.getElementById( td.getAttribute( 'aria-describedby' ) ).innerHTML.replace( '
', ' ' ); headers.push( '"' + temp_element.textContent.trim() + '"' ); } } csv_data.push( row.join( ',' ) ); i++; } // Replace blank row with table headers csv_data[0] = headers.join( ',' ); let csv_content = csv_data.join( '\r\n' ); Global.JSFileDownload( filename + '.csv', csv_content, 'text/csv;encoding:utf-8' ); } this.getRecordCount = function() { if ( !this.grid ) { return false; } return this.grid.getGridParam( 'reccount' ); }; //Gets a single row this.getSelectedRow = function() { if ( !this.grid ) { return false; } var result = this.grid.getGridParam( 'selrow' ); if ( !result ) { result = false; } return result; }; //Gets an array of rows if multiple are selected. this.getSelectedRows = function() { if ( !this.grid ) { return []; //Return empty array so .length on the result doesn't fail with Cannot read property 'length' of undefined } var result = this.grid.getGridParam( 'selarrrow' ); if ( !result ) { result = []; } return result; }; this.getSelection = function() { var tds = this.grid.find( 'td.ui-state-highlight' ); var selection = []; for ( var x = 0; x < tds.length; x++ ) { selection.push( { tr: $( tds[x] ).parent( 'tr' ).index(), td: $( tds[x] ).index() } ); } return selection; }, this.setTimesheetSelection = function( selection_obj ) { //This is currently broken, it highlights the cells, but they aren't actually considered "selected". // So if you go to Attendance -> TimeSheet, select two cells, Mass Edit them, click Save, the "Mass Edit" is no longer available to be clicked again. // As well if you hold in SHIFT to try and expand the selection (select more cells), that doesn't work either. var trs = this.grid.find( 'tr' ); for ( var i = 0; i < selection_obj.length; i++ ) { for ( var x = 0; x < trs.length; x++ ) { if ( $( trs[x] ).index() == selection_obj[i].tr ) { var tds = $( trs[x] ).find( 'td' ); for ( var w = 0; w < tds.length; w++ ) { if ( $( tds[w] ).index() == selection_obj[i].td ) { $( tds[w] ).addClass( 'ui-state-highlight' ); break; } } break; } } } }; this.setGridColumnsWidth = function( column_model, options ) { // this.grid.autoResizeAllColumns(); // return; if ( this.setup.treeGrid || this.setup.tree_mode ) { this.setGridWidth( $( this.setup.container_selector ).width() - 12 ); return; } if ( typeof options === 'undefined' ) { options = {}; } if ( this.setup.container_selector && ( !options.min_grid_width || options.min_grid_width == 0 ) ) { var parent_container = this.grid.parents( this.setup.container_selector ).find( '.edit-view-tab' ); if ( parent_container.length > 0 ) { //Sub-View grid, check if parent div is visible, and if not don't bother resizing grid. if ( parent_container.is( ':visible' ) === true ) { options.min_grid_width = parent_container.innerWidth(); } else { Debug.Text( ' Parent container of grid is not visible, skip setting column widths...', 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); return; } } else { //Main grid let grid_parent = $( '#gbox_' + this.ui_id ); if ( grid_parent.length !== 0 ) { options.min_grid_width = grid_parent.parent().width(); } else { options.min_grid_width = this.grid.parents( this.setup.container_selector ).innerWidth(); } } } if ( options.min_grid_width == 0 || options.min_grid_width == 'undefined' ) { //fallback width so it's never sized to zero when render timing collides options.min_grid_width = $( 'body' ).innerWidth(); } //Adjust for the vertical scrollbar offset that can occur when the items per page always exceeds the screen height. if ( Global.isVerticalScrollBarRequired( this.grid.parents( '.ui-jqgrid-bdiv' )[0] ) ) { options.min_grid_width -= Global.getScrollbarWidth(); } if ( !options.max_grid_width ) { options.max_grid_width = null; //No maximum. } if ( options.max_grid_width && options.max_grid_width < options.min_grid_width ) { options.min_grid_width = options.max_grid_width; } Debug.Text( 'Target Grid: ' + this.setup.container_selector + ' Target Div: ' + this.grid.parents( this.setup.container_selector ).id + ' Width: Min: ' + options.min_grid_width + ' Max: ' + options.max_grid_width + ' Scrollbar Offset: ' + Global.getScrollbarWidth() + ' Parent Width: ' + $( this.grid.parents( '.edit-view-tab, body' )[0] ).width(), 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); if ( column_model ) { var total_column_width = 0; for ( var i = 0; i < column_model.length; i++ ) { var field = column_model[i].name; var width = column_model[i].width ? column_model[i].width : column_model.widthOrg; total_column_width += width; //Don't change the width of columns if they are already the same size. This may help avoid minor changes in the table caused by simple redraws. if ( this.grid.getColProp( field ).width != width ) { this.grid.setColWidth( field, width ); } } return total_column_width; } var column_model = this.getColumnModel(); //Possible exception //Error: Uncaught TypeError: Cannot read property 'length' of undefined in /interface/html5/#!m=TimeSheet&date=20141102&user_id=53130 line 4288 if ( !column_model ) { Debug.Text( 'ERROR: column_model is null or undefined', 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); return; } function longer( champ, contender ) { return ( contender.length > champ.length ) ? contender : champ; } function longestWord( str ) { if ( str && typeof str === 'string' ) { var words = str.split( ' ' ); return words.reduce( longer ); } else { return ''; } } var grid_data = this.getData(); this.grid_total_width = 0; var cb_column_width = 0; //checkbox column width, usually 22, but only set once we know there is a checkbox column. var cb_column_count_adjustment = 0; //If the checkbox column exists or not. var longest_field_width = 0; var last_column = null; var column_widths = {}; for ( var i = 0; i < column_model.length; i++ ) { var col = column_model[i]; last_column = col; var field = col.name; var longest_cell_content = longestWord( col.label ); // allow extra space for sort order UI hint var header_is_longest = true; if ( field == 'cb' ) { //hard coded override on CB column, so we don't try to check the data in each row for it. cb_column_count_adjustment = 1; cb_column_width = 22; width = cb_column_width; } else { if ( options.column_width_override && options.column_width_override[field] ) { width = options.column_width_override[field]; } else { for ( var j = 0; j < grid_data.length; j++ ) { var row_data = grid_data[j]; if ( !row_data.hasOwnProperty( field ) ) { break; } var current_cell_content = row_data[field]; if ( !current_cell_content ) { current_cell_content = ''; } if ( !longest_cell_content ) { longest_cell_content = current_cell_content.toString(); header_is_longest = false; } else { if ( current_cell_content && current_cell_content.toString().length > longest_cell_content.length ) { longest_cell_content = current_cell_content.toString(); header_is_longest = false; } } } var calculate_text_width_options; if ( longest_cell_content == field ) { calculate_text_width_options = { fontSize: '11px', fontWeight: 'bolder' }; } else { calculate_text_width_options = { fontSize: '11px', fontWeight: 'normal' }; } var width = Global.calculateTextWidth( longest_cell_content, calculate_text_width_options ); // + 40; // 8 is drag handle width +2 for borders +20 for sort order ui hint (17 for actual hint,13 for header padding on Firefox under windows if ( header_is_longest === true ) { width += 40; } else { width += 12; } } } if ( width > longest_field_width ) { longest_field_width = width; } //Debug.Text( ' Column: '+ field +' Width: '+ width +' Content: \''+ longest_cell_content +'\' Longest Column Width: '+ longest_field_width +' Header is Longest: '+ header_is_longest, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); column_widths[field] = width; this.grid_total_width += width; } //If the longest column width can be used for all columns, size them all equally so they don't change sizes between pages. if ( ( longest_field_width * column_model.length ) <= options.min_grid_width ) { var equal_column_width = Math.floor( ( options.min_grid_width - cb_column_width ) / ( column_model.length - cb_column_count_adjustment ) ); var equal_column_width_remainder = ( ( options.min_grid_width - cb_column_width ) % ( column_model.length - cb_column_count_adjustment ) ); //Eliminate partial pixel adjustments. Debug.Text( ' Grid columns CAN fit with equal sizes... Grid Width: ' + options.min_grid_width + ' Optimal Grid Width: ' + this.grid_total_width + ' Equal Size Width: ' + equal_column_width + ' Remainder: ' + equal_column_width_remainder, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); var adjusted_grid_width = 0; var x = 0; for ( var tmp_column_name in column_widths ) { if ( tmp_column_name == 'cb' ) { this.grid.setColWidth( 'cb', cb_column_width ); adjusted_grid_width += cb_column_width; } else { var tmp_column_width = equal_column_width; if ( x == 1 ) { tmp_column_width += equal_column_width_remainder; } //Debug.Text( ' Adjusted Column: '+ tmp_column_name +' Width: Old: '+ column_widths[tmp_column_name] +' New: '+ tmp_column_width, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); if ( this.grid.getColProp( tmp_column_name ).width != tmp_column_width ) { this.grid.setColWidth( tmp_column_name, tmp_column_width ); } adjusted_grid_width += tmp_column_width; } this.grid.setColProp( tmp_column_name, 'fixed', true ); x++; } Debug.Text( ' Adjusted Grid Width: ' + adjusted_grid_width + ' Adjusted Column Remainder: ' + equal_column_width_remainder, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); this.grid_total_width = options.min_grid_width; } else { Debug.Text( ' Optimal Grid Width: ' + this.grid_total_width + ' Equal Size Width: ' + ( longest_field_width * column_model.length ), 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); var body_width_difference = 0; var column_width_adjustment = 0; var columns_to_adjust_width = 0; var column_width_adjustment_remainder = 0; //If the optimal column widths are wider than the max grid width, shrink them to fit. if ( ( options.max_grid_width > 0 && this.grid_total_width > options.max_grid_width ) || ( options.min_grid_width > 0 && this.grid_total_width < options.min_grid_width ) ) { //When columns are too small to fit on the screen and need to be stretched, ignore the overridden column. if ( ( options.max_grid_width > 0 && this.grid_total_width > options.max_grid_width ) ) { body_width_difference = this.grid_total_width - options.max_grid_width; //Should be a negative difference to shrink columns to fit max width. this.grid_total_width = options.max_grid_width; } else { body_width_difference = options.min_grid_width - this.grid_total_width; //Should be a positive difference to grow columns to fit min width. this.grid_total_width = options.min_grid_width; } if ( body_width_difference != 0 ) { if ( options.column_width_override ) { columns_to_adjust_width = ( column_model.length - options.column_width_override.length() ); } else { columns_to_adjust_width = ( column_model.length - cb_column_count_adjustment ); } column_width_adjustment = Math.floor( body_width_difference / columns_to_adjust_width ); column_width_adjustment_remainder = ( body_width_difference % columns_to_adjust_width ); //Eliminate partial pixel adjustments. } } var adjusted_grid_width = 0; var x = 0; for ( var tmp_column_name in column_widths ) { if ( tmp_column_name == 'cb' ) { this.grid.setColWidth( 'cb', cb_column_width ); adjusted_grid_width += cb_column_width; } else { var tmp_column_width; if ( options.column_width_override && options.column_width_override[n] ) { tmp_column_width = column_widths[tmp_column_name]; } else { tmp_column_width = column_widths[tmp_column_name] + column_width_adjustment; } if ( x == 1 ) { //First column after the CB. tmp_column_width += column_width_adjustment_remainder; } Debug.Text( ' Adjusted Column: ' + tmp_column_name + ' Width: Old: ' + column_widths[tmp_column_name] + ' New: ' + tmp_column_width + ' Adjustment: ' + column_width_adjustment + ' Body Difference: ' + body_width_difference + ' Columns Adjusted: ' + columns_to_adjust_width, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); if ( this.grid.getColProp( tmp_column_name ).width != tmp_column_width ) { this.grid.setColWidth( tmp_column_name, tmp_column_width ); } adjusted_grid_width += tmp_column_width; } this.grid.setColProp( tmp_column_name, 'fixed', true ); x++; } //Debug.Text( ' Adjusted Grid Width: '+ adjusted_grid_width +' Adjusted Column Remainder: '+ column_width_adjustment_remainder, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); } //this.setGridWidth( this.grid_total_width, this.grid_total_width ); //Causes columns with to flucuate, specifically in schedule day mode. Debug.Text( ' FINAL Grid width: ' + this.grid_total_width + ' Body Width: ' + Global.bodyWidth() + ' Total Rows: ' + grid_data.length, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); return this.grid_total_width; }; //resize event. $( window ).off( 'resize.grids' ).on( 'resize.grids', Global.debounce( ( e ) => this.TTGridResizeEvent( e ), 500 ) ); this.TTGridResizeEvent = function( e ) { e.stopPropagation(); Debug.Text( ' Window resize event hit by TTGrid. Target: ' + e.target, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); if ( LocalCacheData.resizeable_grids.length > 0 ) { //remove the nulls var grids = LocalCacheData.resizeable_grids.filter( function( t ) { return t != null; } ); LocalCacheData.resizeable_grids = grids; for ( var i in LocalCacheData.resizeable_grids ) { var ttgrid = LocalCacheData.resizeable_grids[i]; if ( !ttgrid || ( typeof ttgrid.getTableIDElement === 'function' && ttgrid.getTableIDElement().length === 0 ) || !ttgrid.grid || ttgrid.setup.onResizeGrid === false ) { Debug.Arr( LocalCacheData.resizeable_grids, ' Grid ignored ' + i, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); LocalCacheData.resizeable_grids[i] = null; continue; } //Try to only resize visible grids. ie: Switch from Audit tab to primary tab, wait until resize event is triggered, then switch back, triggering "flashing" of scroll bars appearing/disappearing // Only happens on edit views with double row fields (ie: Note fields), like Edit Punch or Edit Schedule. if ( ttgrid.getTableIDElement().is( ':visible' ) ) { Debug.Text( ' Processing: ' + ttgrid.ui_id, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); if ( ttgrid.setup.onResizeGrid && typeof ttgrid.setup.onResizeGrid == 'function' ) { Debug.Text( ' TTGrid invoked setup defined onResizeGrid() for ' + ttgrid.ui_id, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); ttgrid.setup.onResizeGrid(); } else { if ( ttgrid.grid.length == 1 ) { Debug.Text( ' TTGrid invoked TTgrid::setGridColumnsWidth() for ' + ttgrid.ui_id, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); ttgrid.setGridColumnsWidth(); } } } else { Debug.Text( ' Skipping grid that is not visible: ' + ttgrid.ui_id, 'TTGrid.js', 'TTGrid', 'setGridColumnsWidth', 10 ); } } //Usually we will want to make double sure that visible search grids resize. //Have to check for grid though because dashboard has no "search grid" if ( LocalCacheData.current_open_primary_controller && LocalCacheData.current_open_primary_controller.grid ) { LocalCacheData.current_open_primary_controller.setGridColumnsWidth(); //Be sure to call the setGridColumnsWidth() from current_open_primary_controller in case its overridden. LocalCacheData.current_open_primary_controller.setGridSize(); } //Also resize grids inside Edit Views. if ( LocalCacheData.current_open_sub_controller && LocalCacheData.current_open_sub_controller.grid ) { LocalCacheData.current_open_sub_controller.setGridColumnsWidth(); //Be sure to call the setGridColumnsWidth() from current_open_sub_controller in case its overridden. LocalCacheData.current_open_sub_controller.setGridSize(); } } } };