import {pushData} from "../session"; const tpl = `
⌛️ Loading Datatable...
Selected 0 row
Unselect
` function path2field(path: string[]) { return [ path.join(''), path.map((p) => p.length).join('_'), path.length ].join('_'); } function field2path(field: string) { let parts = field.split('_'); let level = parseInt(parts[parts.length - 1]); let path = []; let start = 0; for (let i = 0; i < level; i++) { let len = parseInt(parts[parts.length - 1 - level + i]); path.push(field.substring(start, start + len)); start += len; } return path; } function flatten_row_and_extract_column( row: { [field: string]: any }, // origin row current_columns: { [field: string]: any }, // used to receive column struct row_data: { [field: string]: any }, // used to receive flatten row path: string[] ) { if (!row) return; let keys: string[] = []; try { keys = Object.keys(row); } catch (e) { } keys.forEach((key: any) => { let val = row[key]; path.push(key); if (!(key in current_columns)) current_columns[key] = {}; if (val && typeof val == "object" && !Array.isArray(val)) { flatten_row_and_extract_column(val, current_columns[key], row_data, path); } else { row_data[path2field(path)] = val; } path.pop(); }); } function flatten_row(row: { [field: string]: any }) { let current_columns = {}, row_data = {}, path: string[] = []; flatten_row_and_extract_column(row, current_columns, row_data, path); return row_data; } /* * field_args: key -> column_def * path_args: [(path, column_def), ...] * */ function row_data_and_column_def( data: any[], field_args: { [field: string]: any }, path_args: any[][], column_order: { [field: string]: any }, ) { function capitalizeFirstLetter(s: string) { return s.charAt(0).toUpperCase() + s.slice(1); } function gen_columns_def( current_columns: { [field: string]: any }, // all leaf node is {} path: string[], field_args: { [field: string]: any }, path_field_args: { [field: string]: any }, args_from_parent: { [field: string]: any } ) { let column_def: any[] = []; Object.keys(current_columns).forEach((key) => { let val = current_columns[key]; path.push(key); let path_field = path2field(path); if (Object.keys(val).length > 0) { let extra_args = { ...args_from_parent, ...(path_field_args[path_field] || {}), }; column_def.push({ headerName: capitalizeFirstLetter(key.replace(/_/g, " ")), children: gen_columns_def(val, path, field_args, path_field_args, extra_args) }); } else { let column = { headerName: capitalizeFirstLetter(key.replace(/_/g, " ")), field: path_field, ...args_from_parent, ...(field_args[key] || {}), ...(path_field_args[path_field] || {}), }; column_def.push(column); } path.pop(); }) return column_def; } let columns = {}; let rows = []; for (let row of data) { let row_data = {}; flatten_row_and_extract_column(row, columns, row_data, []); rows.push(row_data); } let path_field_args: { [field: string]: any } = {}; path_args.map(([path, column_def]) => { path_field_args[path2field(path)] = column_def }) if (column_order) { // replace all leaf node in column_order to {} columns = JSON.parse( JSON.stringify(column_order), (key, val) => (val && typeof val == "object" && !Array.isArray(val)) ? val : {} ); } let column_defs = gen_columns_def(columns, [], field_args, path_field_args, {}); return { rowData: rows, columnDefs: column_defs, } } function parse_js_func(object: any, js_func_key: string) { return JSON.parse(JSON.stringify(object), (key, value) => { if ( typeof value === 'object' && value.__pywebio_js_function__ === js_func_key && 'params' in value && 'body' in value ) { try { return new Function(...value.params, value.body); } catch (e) { console.error("Parse js function error: %s", e); return null; } } return value; }) } function safe_run(func_name: string, func: any, ...args: any[]) { try { if (typeof func === 'function') func.bind(this)(...args); } catch (e) { console.error("Error on %s function:\n", func_name, e); } } const gridDefaultOptions = { //https://www.ag-grid.com/javascript-data-grid/row-selection/ rowMultiSelectWithClick: true, groupSelectsChildren: true, groupSelectsFiltered: true, // https://www.ag-grid.com/javascript-data-grid/selection-overview/ enableCellTextSelection: true, ensureDomOrder: true, autoGroupColumnDef: { pinned: 'left',//force pinned left. Does not work in columnDef }, // some enterprise config enableCharts: true, enableRangeSelection: true, // animateRows: true, // have rows animate to new positions when sorted sideBar: { toolPanels: [ { id: 'columns', labelDefault: 'Columns', labelKey: 'columns', iconKey: 'columns', toolPanel: 'agColumnsToolPanel', minWidth: 225, width: 290, maxWidth: 400, }, { id: 'filters', labelDefault: 'Filters', labelKey: 'filters', iconKey: 'filter', toolPanel: 'agFiltersToolPanel', minWidth: 180, maxWidth: 400, width: 250, }, ], position: 'right', }, }; const gridDefaultColDef = { //https://www.ag-grid.com/javascript-data-grid/row-height/#text-wrapping //wrapText: true, // <-- HERE //autoHeight: true, // <-- & HERE // suppressMenu: true, wrapHeaderText: true, autoHeaderHeight: true, sortable: true, filter: true, // flex: 1, // minWidth: 90, resizable: true, // allow every column to be aggregated enableValue: true, // allow every column to be grouped enableRowGroup: true, // allow every column to be pivoted enablePivot: true, // sizeColumnsToFit:true, defaultAggFunc: 'avg', } export let Datatable = { handle_type: 'datatable', get_element: function (spec: any): JQuery { let html = Mustache.render(tpl, spec); let elem = $(html); spec.field_args = parse_js_func(spec.field_args, spec.js_func_key); spec.path_args = parse_js_func(spec.path_args, spec.js_func_key); spec.grid_args = parse_js_func(spec.grid_args, spec.js_func_key); let auto_height = spec.height == 'auto'; let options = row_data_and_column_def(spec.records, spec.field_args, spec.path_args, spec.column_order); if (spec.actions.length === 0) { elem.find('.ag-grid-tools').hide(); } else { // not show actions at beginning elem.find('.ag-grid-tools .grid-unselect, .ag-grid-tools .grid-actions').hide(); } let getRowId = undefined; if (spec.id_field) { getRowId = (params: any) => params.data[path2field(spec.id_field)] } let grid_resolve: (opts: any) => void = null; let gridPromise = new Promise((resolve, reject) => { grid_resolve = resolve; }); if (spec.instance_id) // @ts-ignore window[`ag_grid_${spec.instance_id}_promise`] = gridPromise; const gridOptions: any = { ...gridDefaultOptions, ...spec.grid_args, path2field, field2path, spec, flatten_row, // https://www.ag-grid.com/javascript-data-grid/row-ids/ getRowId: getRowId, rowData: options.rowData, columnDefs: options.columnDefs, //https://www.ag-grid.com/javascript-data-grid/row-selection/ rowSelection: (spec.actions.length > 0) && (spec.multiple_select ? 'multiple' : 'single'), defaultColDef: { ...gridDefaultColDef, ...(spec.grid_args.defaultColDef || {}), }, getSelectedRowIDs: function () { const selectedRows = gridOptions.api.getSelectedNodes(); let selected_row_ids = []; for (let r of selectedRows) { if (!r.group) selected_row_ids.push(r.id); } if (!spec.id_field) selected_row_ids = selected_row_ids.map((rid: any) => parseInt(rid)); return selected_row_ids; }, onGridReady: (param: any) => { grid_resolve(gridOptions); if (auto_height) { gridOptions.api.setDomLayout('autoHeight'); } let grid_elem = elem.find(".ag-grid")[0]; let on_grid_show = Promise.resolve(); if (grid_elem.clientWidth === 0) { // the grid is hidden via `display: none`, wait for it to show on_grid_show = new Promise((resolve) => { // @ts-ignore let observer = new ResizeObserver((entries, observer) => { if (grid_elem.clientWidth > 0) { observer.disconnect(); resolve(); } }); observer.observe(grid_elem); }); } on_grid_show.then(() => { gridOptions.columnApi.autoSizeAllColumns(); let content_width = 0; gridOptions.columnApi.getColumns().forEach((column: any) => { if (!column.getColDef().hide) content_width += column.getActualWidth(); }); if (content_width < grid_elem.clientWidth) { // the content is smaller than the grid, so we set columns to adjust in size to fit the grid horizontally gridOptions.api.sizeColumnsToFit(); } }) if (spec.actions.length > 0) { elem.find('.ag-grid-tools').css('opacity', 1); } elem.find('.grid-unselect .act-btn').on('click', () => gridOptions.api.deselectAll()); for (let btn_idx in spec.actions) { let label = spec.actions[btn_idx]; if (label === null) { elem.find('.grid-actions').append('
'); } else { let btn = $(`
${label}
`); btn.on('click', () => { pushData({ btn: parseInt(btn_idx), rows: gridOptions.getSelectedRowIDs() }, spec.callback_id) }); elem.find('.grid-actions').append(btn); } } safe_run('agGrid.onGridReady()', spec.grid_args.onGridReady, param); }, onCellFocused: (params: any) => { var row = gridOptions.api.getDisplayedRowAtIndex(params.rowIndex); var cellValue = gridOptions.api.getValue(params.column, row) if (cellValue === undefined) cellValue = '' document.querySelector('.ag-grid-cell-bar').innerHTML = cellValue; if (spec.cell_content_bar) { let bar = elem.find('.ag-grid-cell-bar'); bar.show(); } safe_run('agGrid.onCellFocused()', spec.grid_args.onCellFocused, params); }, onSelectionChanged: (param: any) => { const selectedRows = gridOptions.getSelectedRowIDs(); if (spec.on_select && selectedRows.length > 0) { pushData({ rows: selectedRows }, spec.callback_id) } elem.find(".ag-grid-row-count").text(selectedRows.length); elem.find(".ag-grid-row-unit").text(selectedRows.length > 1 ? 'rows' : 'row'); if (selectedRows.length === 0) { elem.find('.ag-grid-tools .grid-unselect, .ag-grid-tools .grid-actions').hide(); } if (selectedRows.length >= 1) { elem.find('.ag-grid-tools .grid-unselect, .ag-grid-tools .grid-actions').fadeIn(200); } safe_run('agGrid.onSelectionChanged()', spec.grid_args.onSelectionChanged, param); } }; let ag_version = spec.enterprise_key ? 'ag-grid-enterprise' : 'ag-grid'; // @ts-ignore requirejs([ag_version], function (agGrid) { elem.find('.grid-loading').remove(); new agGrid.Grid(elem.find(".ag-grid")[0], gridOptions); if (spec.instance_id) { // @ts-ignore window[`ag_grid_${spec.instance_id}`] = gridOptions; } }); return elem; } };