From bd59d0dbf18b1612f8cb11caefde8474622a73a6 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 18 Mar 2025 14:56:43 +0100 Subject: [PATCH 01/49] Migrate datatables from client to server side --- assets/src/legacy/attributeTable.js | 389 +++++++++--------- .../lizmap/controllers/datatables.classic.php | 88 ++++ 2 files changed, 273 insertions(+), 204 deletions(-) create mode 100644 lizmap/modules/lizmap/controllers/datatables.classic.php diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 7dc9a5255b..9b0c9daa9b 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1324,25 +1324,23 @@ var lizAttributeTable = function() { if( cFeatures && cFeatures.length > 0 ){ // Format features for datatable - formatDatatableFeatures( + var ff = formatDatatableFeatures( cFeatures, isChild, hiddenFields, lConfig['selectedFeatures'], lConfig['id'], - parentLayerID - ).then((ff) => { - var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); - } - lConfig['features'] = foundFeatures; - }); + parentLayerID); + var foundFeatures = ff.foundFeatures; + var dataSet = ff.dataSet; + + // Datatable configuration + if ( $.fn.dataTable.isDataTable( aTable ) ) { + var oTable = $( aTable ).dataTable(); + oTable.fnClearTable(); + oTable.fnAddData( dataSet ); + } + lConfig['features'] = foundFeatures; } if ( !cFeatures || cFeatures.length == 0 ){ @@ -1493,150 +1491,152 @@ var lizAttributeTable = function() { var firstDisplayedColIndex = cdc.firstDisplayedColIndex; // Format features for datatable - formatDatatableFeatures( + var ff = formatDatatableFeatures( atFeatures, isChild, hiddenFields, lConfig['selectedFeatures'], lConfig['id'], parentLayerID, - pivotReference - ).then((ff) => { - var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Fill in the features object - // only when necessary : object is empty or is not child or (is child and no full features list in the object) - var refillFeatures = false; - var dLen = lConfig['features'] ? Object.keys(lConfig['features']).length : 0; - if( dLen == 0 ){ - refillFeatures = true; - if( !isChild ){ - lConfig['featuresFullSet'] = true; - } + pivotReference); + var foundFeatures = ff.foundFeatures; + var dataSet = ff.dataSet; + + // Fill in the features object + // only when necessary : object is empty or is not child or (is child and no full features list in the object) + var refillFeatures = false; + var dLen = lConfig['features'] ? Object.keys(lConfig['features']).length : 0; + if( dLen == 0 ){ + refillFeatures = true; + if( !isChild ){ + lConfig['featuresFullSet'] = true; } - else{ - if( isChild ){ - if( !lConfig['featuresFullSet'] ){ - refillFeatures = true; - } - }else{ - lConfig['featuresFullSet'] = true; + } + else{ + if( isChild ){ + if( !lConfig['featuresFullSet'] ){ refillFeatures = true; } + }else{ + lConfig['featuresFullSet'] = true; + refillFeatures = true; } - if( refillFeatures ) { - lConfig['features'] = foundFeatures; - } + } + if( refillFeatures ) { + lConfig['features'] = foundFeatures; + } - lConfig['alias'] = cAliases; - // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); + lConfig['alias'] = cAliases; + // Datatable configuration + if ( $.fn.dataTable.isDataTable( aTable ) ) { + var oTable = $( aTable ).dataTable(); + oTable.fnClearTable(); + oTable.fnAddData( dataSet ); + } + else { + // Search while typing in text input + // Deactivate if too many items + var searchWhileTyping = true; + if( dataLength > 500000 ){ + searchWhileTyping = false; } - else { - // Search while typing in text input - // Deactivate if too many items - var searchWhileTyping = true; - if( dataLength > 500000 ){ - searchWhileTyping = false; - } - var myDom = '<ipl>'; - if( searchWhileTyping ) { - $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ - var searchVal = this.value; - lizdelay(function(){ - oTable.fnFilter( searchVal ); - }, 500 ); - }); - }else{ - myDom = '<ipl>'; - } + var myDom = '<ipl>'; + if( searchWhileTyping ) { + $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ + var searchVal = this.value; + lizdelay(function(){ + oTable.fnFilter( searchVal ); + }, 500 ); + }); + }else{ + myDom = '<ipl>'; + } - $( aTable ).dataTable( { - data: dataSet - ,columns: columns - ,initComplete: function(settings, json) { - const api = new $.fn.dataTable.Api(settings); - const tableId = api.table().node().id; - const featureType = tableId.split('attribute-layer-table-')[1]; - - // Trigger event telling attribute table is ready - lizMap.events.triggerEvent("attributeLayerContentReady", - { - 'featureType': featureType - } - ); - } - ,order: [[ firstDisplayedColIndex, "asc" ]] - ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } - ,deferRender: true - ,createdRow: function ( row, data, dataIndex ) { - if ( $.inArray( data.DT_RowId.toString(), lConfig['selectedFeatures'] ) != -1 - ) { - $(row).addClass('selected'); - data.lizSelected = 'a'; + const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); + const params = globalThis['lizUrls'].params; + params['layerId'] = lConfig.id; + + $( aTable ).dataTable( { + serverSide: true + ,ajax: datatablesUrl + '?' + new URLSearchParams(params).toString() + ,columns: columns + ,initComplete: function(settings, json) { + const api = new $.fn.dataTable.Api(settings); + const tableId = api.table().node().id; + const featureType = tableId.split('attribute-layer-table-')[1]; + + // Trigger event telling attribute table is ready + lizMap.events.triggerEvent("attributeLayerContentReady", + { + 'featureType': featureType } + ); + } + ,order: [[ firstDisplayedColIndex, "asc" ]] + ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } + ,deferRender: true + ,createdRow: function ( row, data, dataIndex ) { + if ( $.inArray( data.DT_RowId.toString(), lConfig['selectedFeatures'] ) != -1 + ) { + $(row).addClass('selected'); + data.lizSelected = 'a'; } - ,drawCallback: function (settings) { - // rendering ok, find img with data-attr-thumbnail - const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); - for(let thumbnail of thumbnailColl) { - thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); - } + } + ,drawCallback: function (settings) { + // rendering ok, find img with data-attr-thumbnail + const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); + for(let thumbnail of thumbnailColl) { + thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); } - ,dom: myDom - ,pageLength: 50 - ,scrollY: '95%' - ,scrollX: '100%' - - } ); + } + ,dom: myDom + ,pageLength: 10 + ,scrollY: '95%' + ,scrollX: '100%' - var oTable = $( aTable ).dataTable(); + } ); - if( !searchWhileTyping ) - $('#attribute-layer-search-' + cleanName).hide(); + var oTable = $( aTable ).dataTable(); - // Bind button which clears top-left search input content - $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ - $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); - }); + if( !searchWhileTyping ) + $('#attribute-layer-search-' + cleanName).hide(); - // Unbind previous events on page - $( aTable ).on( 'page.dt', function() { - // unbind previous events - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); - }); + // Bind button which clears top-left search input content + $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ + $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); + }); - // Bind events when drawing table - $( aTable ).on( 'draw.dt', function() { + // Unbind previous events on page + $( aTable ).on( 'page.dt', function() { + // unbind previous events + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); + }); - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); + // Bind events when drawing table + $( aTable ).on( 'draw.dt', function() { - // Bind event when users click anywhere on the table line to highlight - bindTableLineClick(aName, aTable); + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); - // Refresh size - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); + // Bind event when users click anywhere on the table line to highlight + bindTableLineClick(aName, aTable); - refreshDatatableSize('#' + mycontainerId); + // Refresh size + var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - return false; + refreshDatatableSize('#' + mycontainerId); - }); - } + return false; - // Check editable features - if (canEdit || canDelete) { - lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id],[exp_f]); - } + }); + } - }); + // Check editable features + if (canEdit || canDelete) { + lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id],[exp_f]); + } } if ( !cFeatures || cFeatures.length == 0 ){ @@ -1675,10 +1675,33 @@ var lizAttributeTable = function() { const columns = []; let firstDisplayedColIndex = 0; // Column with selected status - columns.push( {"data": "lizSelected", "width": "25px", "searchable": false, "sortable": true, "visible": false} ); + columns.push({ + data: "lizSelected", + width: "25px", + searchable: false, + sortable: true, + visible: false + }); firstDisplayedColIndex+=1; - columns.push({ "data": "featureToolbar", "width": "25px", "searchable": false, "sortable": false}); + columns.push({ + data: "featureToolbar", + width: "25px", + searchable: false, + sortable: false, + render: (data, type, row, meta) => { + const layerId = config.layers[aName].id; + const fid = row['DT_RowId']; + + // TODO: handle pivotId, parentLayerID and isChild + let pivotId; + let isChild; + let parentLayerID; + return ` + + `; + } + }); firstDisplayedColIndex += 1; // Add column for each field @@ -1898,86 +1921,46 @@ var lizAttributeTable = function() { * @param layerId * @param parentLayerID * @param pivotId - * - * @returns {Promise} */ function formatDatatableFeatures(atFeatures, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId = null){ - const waitForIt = (delay) => { - return new Promise((resolve) => setTimeout(resolve, delay)); - } - const mapFeatures = (features, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId = null) => { - return new Promise((resolve) => { - const dataSet = []; - const foundFeatures = {}; + var dataSet = []; + var foundFeatures = {}; + atFeatures.forEach(function(feat) { + var line = {}; - features.forEach(function(feat) { - const line = {}; + // add feature to layer global data + var fid = feat.id.split('.')[1]; + foundFeatures[fid] = feat; - // add feature to layer global data - const fid = feat.id.split('.')[1]; - foundFeatures[fid] = feat; + // Add row ID + line['DT_RowId'] = fid; + line['lizSelected'] = 'z'; - // Add row ID - line['DT_RowId'] = fid; - line['lizSelected'] = 'z'; - - if( selectedFeatures && selectedFeatures.indexOf(fid) != -1 ) - line.lizSelected = 'a'; - line['featureToolbar'] = - `` + - ``; - - // Build table lines - for (var idx in feat.properties){ - if( (hiddenFields.indexOf(idx) > -1) ) - continue; - var prop = feat.properties[idx]; - if (typeof prop == 'string') { - prop = DOMPurify.sanitize(prop, { - ADD_ATTR: ['target'] - }); - } - line[idx] = prop; - } + if( selectedFeatures && $.inArray( fid, selectedFeatures ) != -1 ) + line.lizSelected = 'a'; + line['featureToolbar'] = ``; - dataSet.push( line ); - }); - - resolve({ - 'dataSet': dataSet, - 'foundFeatures': foundFeatures - }); - }); - } - return new Promise(async (resolve) => { - const step = 500; - let start = 0; - let end = start+step; - let found = -1; - const timeout = 10; - var dataSet = []; - var foundFeatures = {}; - do { - const features = atFeatures.slice(start, end); - found = features.length; - if (found != 0) { - const result = await mapFeatures(features, isChild, hiddenFields, selectedFeatures, layerId, parentLayerID, pivotId); - Object.assign(foundFeatures, result.foundFeatures); - dataSet = dataSet.concat(result.dataSet); - await waitForIt(timeout); - start = end; - end = start+step; + // Build table lines + for (var idx in feat.properties){ + if( ($.inArray(idx, hiddenFields) > -1) ) + continue; + var prop = feat.properties[idx]; + if (typeof prop == 'string') { + prop = DOMPurify.sanitize(prop, { + ADD_ATTR: ['target'] + }); } - } while (found !== 0); - resolve({ - 'dataSet': dataSet, - 'foundFeatures': foundFeatures - }); + line[idx] = prop; + } + + + dataSet.push( line ); }); + return { + 'dataSet': dataSet, + 'foundFeatures': foundFeatures + }; } /** @@ -3304,9 +3287,7 @@ var lizAttributeTable = function() { if(refAttributeLayerConf && refAttributeLayerConf[1]?.pivot == 'True'){ // check if pivot is in relations for both layers const validRelation = [nlayerId,mLayerId].every((layerId)=>{ - return config.relations[layerId] && config.relations[layerId].some((rlayer)=>{ - return rlayer.referencingLayer == pivotId - }) + return config.relations[layerId] && config.relations[layerId].filter((rlayer)=>{ return rlayer.referencingLayer == pivotId}).length == 1 }) if (validRelation) return pivotId; diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php new file mode 100644 index 0000000000..ed6806c955 --- /dev/null +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -0,0 +1,88 @@ +getResponse('binary'); + $rep->outputFileName = 'datatables.json'; + $rep->mimeType = 'application/json'; + + // Lizmap parameters + $repository = $this->param('repository'); + $project = $this->param('project'); + $layerId = $this->param('layerId'); + + // DataTables parameters + $DTStart = $this->param('start'); + $DTLength = $this->param('length'); + + $DTOrder = $this->param('order'); + $DTColumns = $this->param('columns'); + $DTOrderColumnIndex = $DTOrder[0]['column']; + $DTOrderColumnDirection = $DTOrder[0]['dir'] == 'desc' ? 'd' : ''; + $DTOrderColumnName = $DTColumns[$DTOrderColumnIndex]['data']; + + $DTSearch = $this->param('search'); + + $lproj = lizmap::getProject($repository.'~'.$project); + $layer = $lproj->getLayer($layerId); + $typeName = $layer->getWfsTypeName(); + + $wfsparams = array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'TYPENAME' => $typeName, + 'OUTPUTFORMAT' => 'GeoJSON', + 'GEOMETRYNAME' => 'none', + 'MAXFEATURES' => $DTLength, + 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, + 'STARTINDEX' => $DTStart, + ); + + $wfsrequest = new WFSRequest($lproj, $wfsparams, lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $featureData = $wfsresponse->getBodyAsString(); + $jsonFeatures = json_decode($featureData)->features; + $data = array(); + foreach ($jsonFeatures as $key => $feature) { + $dataObject = array_merge( + array( + 'DT_RowId' => (int) explode('.', $feature->id)[1], + 'lizSelected' => '', + 'featureToolbar' => '', + ), + (array) $feature->properties + ); + $data[] = $dataObject; + } + // jLog::log(var_export(, true), 'error'); + + $returnedData = array( + 'draw' => (int) $this->param('draw'), + 'recordsTotal' => 31, // TODO: get the total number of features + 'recordsFiltered' => 31, + 'data' => $data, + // 'error' => 'error', + ); + + $rep->content = json_encode($returnedData); + // $rep->content = $wfsresponse; + + return $rep; + } +} From 89892b0671715df53949860c88a5194d64a183e4 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 20 Mar 2025 17:26:57 +0100 Subject: [PATCH 02/49] DT: handle `selected` class for lines in `createdRow` callback --- assets/src/legacy/attributeTable.js | 75 +++-------------------------- 1 file changed, 7 insertions(+), 68 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 9b0c9daa9b..78642be7d5 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1576,10 +1576,9 @@ var lizAttributeTable = function() { ,order: [[ firstDisplayedColIndex, "asc" ]] ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } ,deferRender: true - ,createdRow: function ( row, data, dataIndex ) { - if ( $.inArray( data.DT_RowId.toString(), lConfig['selectedFeatures'] ) != -1 - ) { - $(row).addClass('selected'); + , createdRow: (row, data) => { + if ((lConfig['selectedFeatures'].includes(data.DT_RowId.toString()))) { + row.classList.add('selected'); data.lizSelected = 'a'; } } @@ -3028,68 +3027,6 @@ var lizAttributeTable = function() { } - /** - * - * @param featureType - * @param featureIds - */ - function redrawAttributeTableContent( featureType, featureIds ){ - // Loop through all datatables to get the one concerning this featureType - $('.attribute-table-table[id]').each(function(){ - var tableId = $(this).attr('id'); - var tableLayerName = $(this).parents('div.dataTables_wrapper:first').prev('input.attribute-table-hidden-layer').val() - - if ( tableLayerName - && $.fn.dataTable.isDataTable( $(this) ) - && lizMap.cleanName( featureType ) == tableLayerName - ){ - // Get selected feature ids if not given - if( !featureIds ){ - // Assure selectedFeatures property exists for the layer - if( !config.layers[featureType]['selectedFeatures'] ) - config.layers[featureType]['selectedFeatures'] = []; - var featureIds = config.layers[featureType]['selectedFeatures']; - } - - // Get Datatable api - var rTable = $(this).DataTable(); - var dTable = $(this).dataTable(); - - // Remove class selected for all the lines - rTable - .rows( $(this).find('tr.selected') ) - .every(function(){ - dTable.fnUpdate( 'z', this, 0, false, false ); - }) - //~ .draw() - .nodes() - .to$() - .removeClass( 'selected' ) - ; - - // Add class selected from featureIds - // And change lizSelected column value to a - if( featureIds.length > 0 ){ - - var rTable = $(this).DataTable(); - rTable.data().each( function(d){ - if( $.inArray( d.DT_RowId.toString(), featureIds ) != -1 ) - d.lizSelected = 'a'; - }); - rTable - .rows( function ( idx, data, node ) { - return data.lizSelected == 'a' ? true : false; - }) - .nodes() - .to$() - .addClass( 'selected' ) - ; - } - } - - }); - } - /** * * @param featureType @@ -3397,8 +3334,10 @@ var lizAttributeTable = function() { // Update attribute table tools updateAttributeTableTools( e.featureType ); - // Redraw attribute table content ( = add "selected" classes) - redrawAttributeTableContent( e.featureType, e.featureIds ); + // Redraw attribute table content + // `createdRow` callback handles "selected" classes + const table = new DataTable('table[id^=attribute-layer-table-]'); + table.draw('page'); // Update openlayers layer drawing if( e.updateDrawing ) From 6e0c2cf6b6a74734f942409bb335082cf47b74da Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 21 Mar 2025 19:10:43 +0100 Subject: [PATCH 03/49] Attribute table: handle moveSelectedToTop w/ serverSide --- assets/src/legacy/attributeTable.js | 45 ++++++++++--------- .../lizmap/controllers/datatables.classic.php | 41 ++++++++++++++--- 2 files changed, 58 insertions(+), 28 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 78642be7d5..4b45afb007 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -45,6 +45,7 @@ var lizAttributeTable = function() { var wfsTypenameMap = {}; var mediaLinkPrefix = globalThis['lizUrls'].media + '?' + new URLSearchParams(globalThis['lizUrls'].params); var startupFilter = false; + let moveSelectedToTop = false; if( !( typeof lizLayerFilter === 'undefined' ) ){ startupFilter = true; lizMap.lizmapLayerFilterActive = true; @@ -775,27 +776,19 @@ var lizAttributeTable = function() { ); // Bind click on "move selected to top" button - $('#attribute-layer-'+ cleanName + ' button.btn-moveselectedtotop-attributeTable') - .click(function(){ - var aTable = '#attribute-layer-table-' + $(this).val(); - var dTable = $( aTable ).DataTable(); - var previousOrder = dTable.order(); - previousOrder = $.grep(previousOrder, function(o){ - return o[0] != 0; - }); - var selectedOrder = [ [0, 'asc'] ]; - var newOrder = selectedOrder.concat(previousOrder); - dTable.order( newOrder ).draw(); - - // Scroll to top - $(aTable).parents('div.attribute-layer-content').scrollTop(0); - - return false; - }) - .hover( - function(){ $(this).addClass('btn-primary'); }, - function(){ $(this).removeClass('btn-primary'); } - ); + const moveSelectedToTopSelector = '#attribute-layer-' + cleanName + ' button.btn-moveselectedtotop-attributeTable'; + document.querySelector(moveSelectedToTopSelector).addEventListener('click', (e) => { + const dTableSelector = '#attribute-layer-table-' + e.currentTarget.value; + const dTable = new DataTable(dTableSelector); + moveSelectedToTop = true; + dTable.draw(); + moveSelectedToTop = false; + + // Scroll to top + document.querySelector(dTableSelector).parentElement.scroll({ + top: 0 + }) ; + }); // Bind click on filter button @@ -1559,7 +1552,15 @@ var lizAttributeTable = function() { $( aTable ).dataTable( { serverSide: true - ,ajax: datatablesUrl + '?' + new URLSearchParams(params).toString() + ,ajax: { + url: datatablesUrl + '?' + new URLSearchParams(params).toString(), + data: (d) => { + if (moveSelectedToTop) { + d.moveselectedtotop = true; + d.selectedfeatureids = lConfig['selectedFeatures'].join(); + } + } + } ,columns: columns ,initComplete: function(settings, json) { const api = new $.fn.dataTable.Api(settings); diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index ed6806c955..1de7b83ee9 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -25,6 +25,8 @@ public function index() $repository = $this->param('repository'); $project = $this->param('project'); $layerId = $this->param('layerId'); + $moveSelectedToTop = $this->param('moveselectedtotop'); + $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); // DataTables parameters $DTStart = $this->param('start'); @@ -42,7 +44,9 @@ public function index() $layer = $lproj->getLayer($layerId); $typeName = $layer->getWfsTypeName(); - $wfsparams = array( + $jsonFeatures = array(); + + $wfsParamsData = array( 'SERVICE' => 'WFS', 'VERSION' => '1.0.0', 'REQUEST' => 'GetFeature', @@ -54,10 +58,38 @@ public function index() 'STARTINDEX' => $DTStart, ); - $wfsrequest = new WFSRequest($lproj, $wfsparams, lizmap::getServices()); + if ($moveSelectedToTop == 'true') { + $featureIds = array(); + foreach ($selectedFeatureIDs as $id) { + $featureIds[] = $typeName.'.'.$id; + } + + $wfsrequest = new WFSRequest( + $lproj, + array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'OUTPUTFORMAT' => 'GeoJSON', + 'GEOMETRYNAME' => 'none', + 'FEATUREID' => implode(',', $featureIds), + ), + lizmap::getServices() + ); + $wfsresponse = $wfsrequest->process(); + $featureData = $wfsresponse->getBodyAsString(); + $jsonFeatures = json_decode($featureData)->features; + + // Remove selected features from the list of features to get + $DTLength = $DTLength - count($jsonFeatures); + $wfsParamsData['MAXFEATURES'] = $DTLength; + $wfsParamsData['EXP_FILTER'] = '$id NOT IN ('.implode(' , ', $selectedFeatureIDs).')'; + } + + $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); - $jsonFeatures = json_decode($featureData)->features; + $jsonFeatures = array_merge($jsonFeatures, json_decode($featureData)->features); $data = array(); foreach ($jsonFeatures as $key => $feature) { $dataObject = array_merge( @@ -70,18 +102,15 @@ public function index() ); $data[] = $dataObject; } - // jLog::log(var_export(, true), 'error'); $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => 31, // TODO: get the total number of features 'recordsFiltered' => 31, 'data' => $data, - // 'error' => 'error', ); $rep->content = json_encode($returnedData); - // $rep->content = $wfsresponse; return $rep; } From 77481315c3ca49e23c73345b63a2b65127c28e53 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 25 Mar 2025 18:24:19 +0100 Subject: [PATCH 04/49] Attribute table: handle filter w/ serverSide --- assets/src/legacy/attributeTable.js | 46 ++++++++----------- .../lizmap/controllers/datatables.classic.php | 10 ++++ 2 files changed, 28 insertions(+), 28 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 4b45afb007..ebeafdca08 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1325,22 +1325,10 @@ var lizAttributeTable = function() { lConfig['id'], parentLayerID); var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); - } lConfig['features'] = foundFeatures; } if ( !cFeatures || cFeatures.length == 0 ){ - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - } $(aTable).hide(); $('#attribute-layer-'+ cleanName +' span.attribute-layer-msg').html( @@ -1521,12 +1509,7 @@ var lizAttributeTable = function() { lConfig['alias'] = cAliases; // Datatable configuration - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - oTable.fnAddData( dataSet ); - } - else { + if ( !$.fn.dataTable.isDataTable( aTable ) ) { // Search while typing in text input // Deactivate if too many items var searchWhileTyping = true; @@ -1555,10 +1538,23 @@ var lizAttributeTable = function() { ,ajax: { url: datatablesUrl + '?' + new URLSearchParams(params).toString(), data: (d) => { + // Handle selected features moved to top if (moveSelectedToTop) { d.moveselectedtotop = true; d.selectedfeatureids = lConfig['selectedFeatures'].join(); } + + // Handle filtered features + const filteredFeaturesIds = lConfig.filteredFeatures; + if (filteredFeaturesIds && filteredFeaturesIds.length > 0) { + d.filteredfeatureids = filteredFeaturesIds.join(); + } + + // Handle features filtered by their parent + const exp_filter = lConfig.request_params.exp_filter; + if (exp_filter) { + d.exp_filter = exp_filter; + } } } ,columns: columns @@ -1640,10 +1636,6 @@ var lizAttributeTable = function() { } if ( !cFeatures || cFeatures.length == 0 ){ - if ( $.fn.dataTable.isDataTable( aTable ) ) { - var oTable = $( aTable ).dataTable(); - oTable.fnClearTable(); - } $(aTable).hide(); $('#attribute-layer-'+ cleanName +' span.attribute-layer-msg').html( @@ -2335,6 +2327,10 @@ var lizAttributeTable = function() { applyEmptyLayerFilter( typeName, typeNamePile, typeNameFilter, typeNameDone, cascade ); } + // Refresh attributeTable + const table = new DataTable('#attribute-layer-table-' + lizMap.cleanName(typeName)); + table.draw(); + $('#layerActionUnfilter').toggle((lizMap.lizmapLayerFilterActive !== null)); } @@ -2729,12 +2725,6 @@ var lizAttributeTable = function() { lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerByName(layerConfig.name).expressionFilter = null; } - // Refresh attributeTable - var opTable = '#attribute-layer-table-'+lizMap.cleanName( typeName ); - if( $( opTable ).length ){ - refreshLayerAttributeDatatable(typeName, opTable, cFeatures); - } - // And send event so that getFeatureInfo and getPrint use the updated layer filters lizMap.events.triggerEvent("layerFilterParamChanged", { diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 1de7b83ee9..44fd3a11f9 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -27,6 +27,8 @@ public function index() $layerId = $this->param('layerId'); $moveSelectedToTop = $this->param('moveselectedtotop'); $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); + $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); + $expFilter = $this->param('exp_filter'); // DataTables parameters $DTStart = $this->param('start'); @@ -86,6 +88,14 @@ public function index() $wfsParamsData['EXP_FILTER'] = '$id NOT IN ('.implode(' , ', $selectedFeatureIDs).')'; } + if (count($filteredFeatureIDs) > 0) { + $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; + } + + if ($expFilter) { + $wfsParamsData['EXP_FILTER'] = $expFilter; + } + $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); From 513dc57648477a88287d42a92f84bae6cc1b1cfb Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 27 Mar 2025 18:11:40 +0100 Subject: [PATCH 05/49] AttributeTable: add `data-layerid` in s attributes to ease selection of them --- assets/src/legacy/attributeTable.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index ebeafdca08..4dc9264c23 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -644,7 +644,8 @@ var lizAttributeTable = function() { } html+= '
'; html+= ' '; - html+= '
'; + const classes = 'attribute-table-table table table-hover table-condensed table-striped order-column cell-border'; + html+= '
'; html+= ''; // attribute-layer-content @@ -1119,9 +1120,10 @@ var lizAttributeTable = function() { var cDiv = '
'; var tId = 'attribute-layer-table-' + lizMap.cleanName(parentLayerName) + '-' + lizMap.cleanName(childLayerName); var tClass = 'attribute-table-table table table-hover table-condensed table-striped cell-border child-of-' + lizMap.cleanName(parentLayerName); + const dataLayerId = relation.referencingLayer; cDiv+= ' '; cDiv+= ' '; - cDiv+= '
'; + cDiv+= '
'; cDiv+= '
'; childDiv.push(cDiv); From 8a6e2a47d21cf0db70296ac12281d0535b0c5ad1 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 27 Mar 2025 18:14:27 +0100 Subject: [PATCH 06/49] Handle 'selected' state for rows without querying server --- assets/src/legacy/attributeTable.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 4dc9264c23..6f1dd570de 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -3327,10 +3327,21 @@ var lizAttributeTable = function() { // Update attribute table tools updateAttributeTableTools( e.featureType ); - // Redraw attribute table content - // `createdRow` callback handles "selected" classes - const table = new DataTable('table[id^=attribute-layer-table-]'); - table.draw('page'); + // Update selected features in the table + const layerId = config.attributeLayers[e.featureType].layerId; + const selectedFeatures = config.layers[e.featureType].selectedFeatures; + const table = new DataTable('table[data-layerid=' + layerId + ']'); + + table.rows().every(function (rowIdx) { + var data = this.data(); + if ((selectedFeatures.includes(data.DT_RowId.toString()))) { + this.row(rowIdx).node().classList.add('selected'); + data.lizSelected = 'a'; + } else { + this.row(rowIdx).node().classList.remove('selected'); + data.lizSelected = 'z'; + } + }); // Update openlayers layer drawing if( e.updateDrawing ) From 70ea075d879ae0b1a36512904bfa65fe023135a0 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 28 Mar 2025 15:29:18 +0100 Subject: [PATCH 07/49] Get total number of records with WFS `RESULTTYPE=hits` --- .../lizmap/controllers/datatables.classic.php | 31 ++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 44fd3a11f9..108d1cf8a9 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -26,8 +26,14 @@ public function index() $project = $this->param('project'); $layerId = $this->param('layerId'); $moveSelectedToTop = $this->param('moveselectedtotop'); - $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); - $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); + $selectedFeatureIDs = array(); + if ($this->param('selectedfeatureids')) { + $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); + } + $filteredFeatureIDs = array(); + if ($this->param('filteredfeatureids')) { + $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); + } $expFilter = $this->param('exp_filter'); // DataTables parameters @@ -48,6 +54,23 @@ public function index() $jsonFeatures = array(); + // Get total number of features + $hits = 0; + $wfsParamsHits = array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'TYPENAME' => $typeName, + 'RESULTTYPE' => 'hits', + ); + + $wfsrequest = new WFSRequest($lproj, $wfsParamsHits, lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $hitsData = $wfsresponse->getBodyAsString(); + preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches); + $hits = $matches[1]; + + // Get features $wfsParamsData = array( 'SERVICE' => 'WFS', 'VERSION' => '1.0.0', @@ -115,8 +138,8 @@ public function index() $returnedData = array( 'draw' => (int) $this->param('draw'), - 'recordsTotal' => 31, // TODO: get the total number of features - 'recordsFiltered' => 31, + 'recordsTotal' => $hits, + 'recordsFiltered' => $hits, // TODO: implement filtering 'data' => $data, ); From 98fdcd25c01ec72b293b43129643e4e53ee5e472 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 28 Mar 2025 15:31:06 +0100 Subject: [PATCH 08/49] Don't request WFS GetFeature at startup to get data as DT does --- assets/src/legacy/attributeTable.js | 322 +++++++++++----------------- 1 file changed, 125 insertions(+), 197 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 6f1dd570de..6b1c2a5779 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -335,11 +335,8 @@ var lizAttributeTable = function() { wfsParams['SRSNAME'] = 'EPSG:4326'; } - const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); - - let fetchRequests = [getFeatureRequest]; - let namedRequests = {'getFeature': fetchRequests.length-1}; - + let fetchRequests = []; + let namedRequests = []; if (!(layerConfig?.['alias'] && layerConfig?.['types'])) { const describeFeatureTypeRequest = lizMap.mainLizmap.wfs.describeFeatureType({ @@ -351,7 +348,7 @@ var lizAttributeTable = function() { const allColumnsKeyValues = {}; - // Indexes 0 and 1 are use for getFeature and describeFeature requests + // Index 0 is used for describeFeature requests namedRequests['keyValues'] = fetchRequests.length+0; let responseOrder = fetchRequests.length+0; for (const fieldName in lizMap.keyValueConfig?.[layerName]) { @@ -378,7 +375,7 @@ var lizAttributeTable = function() { })); } } - if (forceEmptyTable) return buildLayerAttributeDatatable(layerName, tableSelector, [], layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); + // if (forceEmptyTable) return buildLayerAttributeDatatable(layerName, tableSelector, [], layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); document.body.style.cursor = 'progress'; Promise.all(fetchRequests).then(responses => { @@ -398,14 +395,14 @@ var lizAttributeTable = function() { } layerConfig['featureCrs'] = 'EPSG:4326'; - if (namedRequests?.['describeFeatureType']) { + if (namedRequests.hasOwnProperty('describeFeatureType') ) { const describeFeatureTypeResponse = responses[namedRequests['describeFeatureType']]; layerConfig['aliases'] = describeFeatureTypeResponse.aliases; layerConfig['types'] = describeFeatureTypeResponse.types; layerConfig['columns'] = describeFeatureTypeResponse.columns; } - buildLayerAttributeDatatable(layerName, tableSelector, responses[0].features, layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); + buildLayerAttributeDatatable(layerName, tableSelector, layerConfig.aliases, layerConfig.types, allColumnsKeyValues, callBack); document.body.style.cursor = 'default'; }).catch(() => { @@ -1350,13 +1347,12 @@ var lizAttributeTable = function() { * * @param aName * @param aTable - * @param cFeatures * @param cAliases * @param cTypes * @param allColumnsKeyValues * @param aCallback */ - function buildLayerAttributeDatatable(aName, aTable, cFeatures, cAliases, cTypes, allColumnsKeyValues, aCallback ) { + function buildLayerAttributeDatatable(aName, aTable, cAliases, cTypes, allColumnsKeyValues, aCallback ) { // Get config var lConfig = config.layers[aName]; @@ -1445,211 +1441,137 @@ var lizAttributeTable = function() { canDelete = true; } - cFeatures = typeof cFeatures !== 'undefined' ? cFeatures : null; - if( !cFeatures ){ - // features is an object, let's transform it to an array - // XXX IE compat: Object.values is not available on IE... - var features = config.layers[aName]['features']; - cFeatures = Object.keys(features).map(function (key) { - return features[key]; - }); - } - - var atFeatures = cFeatures; - var dataLength = atFeatures.length; - - if( cFeatures && cFeatures.length > 0 ){ - let keys = [], exp_f; - let pkey = config.attributeLayers[aName]['primaryKey'] || null; - if (pkey) { - keys = cFeatures.map((f) => - `'${f.id.split(".")[1]}'` - ) - exp_f = `"${pkey}" IN ( ${keys.join(' , ')} )`; - } - - // Create columns for datatable - var cdc = createDatatableColumns(aName, atFeatures, hiddenFields, cAliases, cTypes, allColumnsKeyValues); - var columns = cdc.columns; - var firstDisplayedColIndex = cdc.firstDisplayedColIndex; - - // Format features for datatable - var ff = formatDatatableFeatures( - atFeatures, - isChild, - hiddenFields, - lConfig['selectedFeatures'], - lConfig['id'], - parentLayerID, - pivotReference); - var foundFeatures = ff.foundFeatures; - var dataSet = ff.dataSet; - - // Fill in the features object - // only when necessary : object is empty or is not child or (is child and no full features list in the object) - var refillFeatures = false; - var dLen = lConfig['features'] ? Object.keys(lConfig['features']).length : 0; - if( dLen == 0 ){ - refillFeatures = true; - if( !isChild ){ - lConfig['featuresFullSet'] = true; - } - } - else{ - if( isChild ){ - if( !lConfig['featuresFullSet'] ){ - refillFeatures = true; - } - }else{ - lConfig['featuresFullSet'] = true; - refillFeatures = true; - } - } - if( refillFeatures ) { - lConfig['features'] = foundFeatures; - } - - lConfig['alias'] = cAliases; - // Datatable configuration - if ( !$.fn.dataTable.isDataTable( aTable ) ) { - // Search while typing in text input - // Deactivate if too many items - var searchWhileTyping = true; - if( dataLength > 500000 ){ - searchWhileTyping = false; - } - - var myDom = '<ipl>'; - if( searchWhileTyping ) { - $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ - var searchVal = this.value; - lizdelay(function(){ - oTable.fnFilter( searchVal ); - }, 500 ); - }); - }else{ - myDom = '<ipl>'; - } - - const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); - const params = globalThis['lizUrls'].params; - params['layerId'] = lConfig.id; - - $( aTable ).dataTable( { - serverSide: true - ,ajax: { - url: datatablesUrl + '?' + new URLSearchParams(params).toString(), - data: (d) => { - // Handle selected features moved to top - if (moveSelectedToTop) { - d.moveselectedtotop = true; - d.selectedfeatureids = lConfig['selectedFeatures'].join(); - } + // Create columns for datatable + var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues); + var columns = cdc.columns; + var firstDisplayedColIndex = cdc.firstDisplayedColIndex; + + lConfig['alias'] = cAliases; + // Datatable configuration + if ( !$.fn.dataTable.isDataTable( aTable ) ) { + // Search while typing in text input + // Deactivate if too many items + var searchWhileTyping = true; + + var myDom = '<ipl>'; + if( searchWhileTyping ) { + $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ + var searchVal = this.value; + lizdelay(function(){ + oTable.fnFilter( searchVal ); + }, 500 ); + }); + }else{ + myDom = '<ipl>'; + } + + const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); + const params = globalThis['lizUrls'].params; + params['layerId'] = lConfig.id; + + $( aTable ).dataTable( { + serverSide: true + ,ajax: { + url: datatablesUrl + '?' + new URLSearchParams(params).toString(), + type: 'POST', + data: (d) => { + // Handle selected features moved to top + if (moveSelectedToTop) { + d.moveselectedtotop = true; + d.selectedfeatureids = lConfig['selectedFeatures'].join(); + } - // Handle filtered features - const filteredFeaturesIds = lConfig.filteredFeatures; - if (filteredFeaturesIds && filteredFeaturesIds.length > 0) { - d.filteredfeatureids = filteredFeaturesIds.join(); - } + // Handle filtered features + const filteredFeaturesIds = lConfig.filteredFeatures; + if (filteredFeaturesIds && filteredFeaturesIds.length > 0) { + d.filteredfeatureids = filteredFeaturesIds.join(); + } - // Handle features filtered by their parent - const exp_filter = lConfig.request_params.exp_filter; - if (exp_filter) { - d.exp_filter = exp_filter; - } + // Handle features filtered by their parent + const exp_filter = lConfig.request_params.exp_filter; + if (exp_filter) { + d.exp_filter = exp_filter; } } - ,columns: columns - ,initComplete: function(settings, json) { - const api = new $.fn.dataTable.Api(settings); - const tableId = api.table().node().id; - const featureType = tableId.split('attribute-layer-table-')[1]; - - // Trigger event telling attribute table is ready - lizMap.events.triggerEvent("attributeLayerContentReady", - { - 'featureType': featureType - } - ); - } - ,order: [[ firstDisplayedColIndex, "asc" ]] - ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } - ,deferRender: true - , createdRow: (row, data) => { - if ((lConfig['selectedFeatures'].includes(data.DT_RowId.toString()))) { - row.classList.add('selected'); - data.lizSelected = 'a'; + } + ,columns: columns + ,initComplete: function(settings, json) { + const api = new $.fn.dataTable.Api(settings); + const tableId = api.table().node().id; + const featureType = tableId.split('attribute-layer-table-')[1]; + + // Trigger event telling attribute table is ready + lizMap.events.triggerEvent("attributeLayerContentReady", + { + 'featureType': featureType } + ); + } + ,order: [[ firstDisplayedColIndex, "asc" ]] + ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } + ,deferRender: true + , createdRow: (row, data) => { + if ((lConfig['selectedFeatures'].includes(data.DT_RowId.toString()))) { + row.classList.add('selected'); + data.lizSelected = 'a'; } - ,drawCallback: function (settings) { - // rendering ok, find img with data-attr-thumbnail - const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); - for(let thumbnail of thumbnailColl) { - thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); - } + } + ,drawCallback: function (settings) { + // rendering ok, find img with data-attr-thumbnail + const thumbnailColl = document.getElementsByClassName('data-attr-thumbnail'); + for(let thumbnail of thumbnailColl) { + thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); } - ,dom: myDom - ,pageLength: 10 - ,scrollY: '95%' - ,scrollX: '100%' - - } ); - - var oTable = $( aTable ).dataTable(); + } + ,dom: myDom + ,pageLength: 50 + ,scrollY: '95%' + ,scrollX: '100%' + } ); - if( !searchWhileTyping ) - $('#attribute-layer-search-' + cleanName).hide(); + var oTable = $( aTable ).dataTable(); - // Bind button which clears top-left search input content - $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ - $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); - }); + if( !searchWhileTyping ) + $('#attribute-layer-search-' + cleanName).hide(); - // Unbind previous events on page - $( aTable ).on( 'page.dt', function() { - // unbind previous events - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); - }); + // Bind button which clears top-left search input content + $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ + $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); + }); - // Bind events when drawing table - $( aTable ).on( 'draw.dt', function() { + // Unbind previous events on page + $( aTable ).on( 'page.dt', function() { + // unbind previous events + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); + }); - $(aTable +' tr').unbind('click'); - $(aTable +' tr td button').unbind('click'); + // Bind events when drawing table + $( aTable ).on( 'draw.dt', function() { - // Bind event when users click anywhere on the table line to highlight - bindTableLineClick(aName, aTable); + $(aTable +' tr').unbind('click'); + $(aTable +' tr td button').unbind('click'); - // Refresh size - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); + // Bind event when users click anywhere on the table line to highlight + bindTableLineClick(aName, aTable); - refreshDatatableSize('#' + mycontainerId); + // Refresh size + var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - return false; + refreshDatatableSize('#' + mycontainerId); - }); - } + return false; - // Check editable features - if (canEdit || canDelete) { - lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id],[exp_f]); - } + }); } - if ( !cFeatures || cFeatures.length == 0 ){ - $(aTable).hide(); - - $('#attribute-layer-'+ cleanName +' span.attribute-layer-msg').html( - lizDict['attributeLayers.toolbar.msg.data.nodata'] + ' ' + lizDict['attributeLayers.toolbar.msg.data.extent'] - ).addClass('failure'); - - } else { - $(aTable).show(); - refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')) - + // Check editable features + if (canEdit || canDelete) { + lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id],[exp_f]); } + refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')) + if (aCallback) aCallback(aName,aTable); @@ -1665,7 +1587,7 @@ var lizAttributeTable = function() { * @param cTypes * @param allColumnsKeyValues */ - function createDatatableColumns(aName, atFeatures, hiddenFields, cAliases, cTypes, allColumnsKeyValues){ + function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues){ const columns = []; let firstDisplayedColIndex = 0; // Column with selected status @@ -1699,7 +1621,7 @@ var lizAttributeTable = function() { firstDisplayedColIndex += 1; // Add column for each field - for (var columnName in atFeatures[0].properties){ + for (var columnName in cAliases){ // Do not add hidden fields if (hiddenFields.includes(columnName)){ continue; @@ -1745,8 +1667,14 @@ var lizAttributeTable = function() { let davConf = globalThis['lizUrls'].webDavUrl && globalThis['lizUrls']?.resourceUrlReplacement?.webdav && config.layers[aName]?.webDavFields && Array.isArray(config.layers[aName].webDavFields) && config.layers[aName].webDavFields.includes(columnName); colConf['render'] = function (data, type, row, meta) { // Replace media and URL with links - if (!data || !(typeof data === 'string')) + if (!data || !(typeof data === 'string')){ return data; + } + // Sanitize 'string' data + data = DOMPurify.sanitize(data, { + ADD_ATTR: ['target'] + }); + if (davConf) { // replace the root of the url if(data.startsWith(globalThis['lizUrls'].webDavUrl)){ From 9d10f2389bd76bd207c7f132ccc49447b32358c8 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Mon, 31 Mar 2025 16:31:30 +0200 Subject: [PATCH 09/49] Handle children tables --- assets/src/legacy/attributeTable.js | 39 ++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 9 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 6b1c2a5779..d97a5c764c 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -121,6 +121,9 @@ var lizAttributeTable = function() { 'exp_filter': null, 'selection': null }; + // This parameter is used when a parent feature is highlighted + // It only filters children tables + config.layers[configLayerName]['line_filter'] = null; // Get existing filter if exists (via permalink) const layer = lizMap.mainLizmap.state.layersAndGroupsCollection.getLayerByName(layername); @@ -319,10 +322,6 @@ var lizAttributeTable = function() { GEOMETRYNAME: 'extent' }; - if(filter){ - wfsParams['EXP_FILTER'] = filter; - } - // Calculate bbox from map extent if needed if (config.options?.limitDataToBbox == 'True') { const mapExtent = lizMap.mainLizmap.map.getView().calculateExtent(); @@ -1247,7 +1246,16 @@ var lizAttributeTable = function() { if( relation.referencingLayer == childLayerConfig.id ){ filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'"; } - getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false); + + // Refresh datatable if it is already created + // Create if it is not + childLayerConfig.line_filter = filter; + if (DataTable.isDataTable(childTableSelector)) { + const childTable = new DataTable(childTableSelector); + childTable.draw(); + } else { + getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false); + } } } } @@ -1469,7 +1477,7 @@ var lizAttributeTable = function() { const params = globalThis['lizUrls'].params; params['layerId'] = lConfig.id; - $( aTable ).dataTable( { + $( aTable ).dataTable({ serverSide: true ,ajax: { url: datatablesUrl + '?' + new URLSearchParams(params).toString(), @@ -1488,10 +1496,23 @@ var lizAttributeTable = function() { } // Handle features filtered by their parent - const exp_filter = lConfig.request_params.exp_filter; - if (exp_filter) { - d.exp_filter = exp_filter; + if(isChild) { + if(lConfig.line_filter) { + d.exp_filter = lConfig.line_filter; + } + } else { + const exp_filter = lConfig.request_params.exp_filter; + if (exp_filter) { + d.exp_filter = exp_filter; + } + } + }, + dataSrc: function(json) { + // Copy received features to config + for (const feature of json.data) { + config.layers[aName]['features'][feature.DT_RowId] = {'properties' : feature}; } + return json.data; } } ,columns: columns From fce61a82d7c85913025b15bf0212eecfd2c7a805 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Mon, 31 Mar 2025 21:56:05 +0200 Subject: [PATCH 10/49] Return JSON features as they are cached in `lizMap.config` --- assets/src/legacy/attributeTable.js | 19 ++++++++++++++----- .../lizmap/controllers/datatables.classic.php | 16 +--------------- 2 files changed, 15 insertions(+), 20 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index d97a5c764c..7fe0d0bd12 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1507,12 +1507,21 @@ var lizAttributeTable = function() { } } }, - dataSrc: function(json) { - // Copy received features to config - for (const feature of json.data) { - config.layers[aName]['features'][feature.DT_RowId] = {'properties' : feature}; + dataSrc: (json) => { + // Format data for DataTables + let formatedData = []; + for (const feature of json.data.features) { + const featID = feature.id.split('.').pop(); + formatedData.push(Object.assign({ + 'DT_RowId': featID, + 'lizSelected': '', + 'featureToolbar': '', + }, feature.properties)); + + // Copy received features to config + config.layers[aName]['features'][featID] = feature; } - return json.data; + return formatedData; } } ,columns: columns diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 108d1cf8a9..722477fdc8 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -77,7 +77,6 @@ public function index() 'REQUEST' => 'GetFeature', 'TYPENAME' => $typeName, 'OUTPUTFORMAT' => 'GeoJSON', - 'GEOMETRYNAME' => 'none', 'MAXFEATURES' => $DTLength, 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, 'STARTINDEX' => $DTStart, @@ -122,25 +121,12 @@ public function index() $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); - $jsonFeatures = array_merge($jsonFeatures, json_decode($featureData)->features); - $data = array(); - foreach ($jsonFeatures as $key => $feature) { - $dataObject = array_merge( - array( - 'DT_RowId' => (int) explode('.', $feature->id)[1], - 'lizSelected' => '', - 'featureToolbar' => '', - ), - (array) $feature->properties - ); - $data[] = $dataObject; - } $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => $hits, 'recordsFiltered' => $hits, // TODO: implement filtering - 'data' => $data, + 'data' => json_decode($featureData), ); $rep->content = json_encode($returnedData); From 36c43f95c1232c3d64341cbf16b3daf2885acc6b Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 1 Apr 2025 11:07:51 +0200 Subject: [PATCH 11/49] Handle link/unlink in featureToolbars in attribute table --- assets/src/legacy/attributeTable.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 7fe0d0bd12..7183d6e8bc 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1400,6 +1400,7 @@ var lizAttributeTable = function() { // Pivot table ? var isPivot = false; + let pivotId; if( isChild && 'pivot' in config.attributeLayers[aName] && config.attributeLayers[aName]['pivot'] == 'True' @@ -1421,7 +1422,7 @@ var lizAttributeTable = function() { } if (parentLayerConfig && parentLayerConfig[1] && parentLayerConfig[1].cleanname && highlightedFeature) { var childLayerId = lConfig.id; - var pivotId = getPivotIdFromRelatedLayers(parentLayerID, childLayerId); + pivotId = getPivotIdFromRelatedLayers(parentLayerID, childLayerId); if (pivotId) { pivotReference = pivotId + ":" + highlightedFeature; } @@ -1450,7 +1451,7 @@ var lizAttributeTable = function() { } // Create columns for datatable - var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues); + var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID); var columns = cdc.columns; var firstDisplayedColIndex = cdc.firstDisplayedColIndex; @@ -1617,7 +1618,7 @@ var lizAttributeTable = function() { * @param cTypes * @param allColumnsKeyValues */ - function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues){ + function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID){ const columns = []; let firstDisplayedColIndex = 0; // Column with selected status @@ -1638,11 +1639,6 @@ var lizAttributeTable = function() { render: (data, type, row, meta) => { const layerId = config.layers[aName].id; const fid = row['DT_RowId']; - - // TODO: handle pivotId, parentLayerID and isChild - let pivotId; - let isChild; - let parentLayerID; return ` `; From 8c04889537fbe734bfc01f3bbc92b1ffd147c210 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 1 Apr 2025 11:09:52 +0200 Subject: [PATCH 12/49] Return `recordsFiltered` for Datatables --- lizmap/modules/lizmap/controllers/datatables.classic.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 722477fdc8..f94860d51e 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -69,6 +69,10 @@ public function index() $hitsData = $wfsresponse->getBodyAsString(); preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches); $hits = $matches[1]; + $recordsFiltered = $hits; + if (count($filteredFeatureIDs) > 0) { + $recordsFiltered = count($filteredFeatureIDs); + } // Get features $wfsParamsData = array( @@ -125,7 +129,7 @@ public function index() $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => $hits, - 'recordsFiltered' => $hits, // TODO: implement filtering + 'recordsFiltered' => $recordsFiltered, 'data' => json_decode($featureData), ); From 701c5591b506e078b2bd5060ffcc66b60714aff5 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 1 Apr 2025 18:22:43 +0200 Subject: [PATCH 13/49] Redraw table if it already exists --- assets/src/legacy/attributeTable.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 7183d6e8bc..15a1e0bceb 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1594,6 +1594,10 @@ var lizAttributeTable = function() { return false; }); + } else { + // Table already created, just redraw it + const table = new DataTable(aTable); + table.draw(); } // Check editable features From 8c58a195e4094d2a5200d5e76c0b65f70f2d29c9 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Wed, 2 Apr 2025 11:50:14 +0200 Subject: [PATCH 14/49] Handle children tables follow up --- assets/src/legacy/attributeTable.js | 40 ++++++++++++++++------------- 1 file changed, 22 insertions(+), 18 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 15a1e0bceb..ddd180f129 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -220,7 +220,7 @@ var lizAttributeTable = function() { const tableSelector = '#attribute-layer-table-' + cleanName; // Get data and fill attribute table - getDataAndFillAttributeTable(layerName, layerFilter, tableSelector, false); + getDataAndFillAttributeTable(layerName, layerFilter, false, tableSelector, false); const tabElement = document.getElementById('nav-tab-attribute-layer-' + cleanName); bootstrap.Tab.getOrCreateInstance(tabElement).show(); @@ -308,15 +308,27 @@ var lizAttributeTable = function() { * * @param layerName * @param filter + * @param isLinefilter * @param tableSelector * @param forceEmptyTable * @param callBack */ - function getDataAndFillAttributeTable(layerName, filter, tableSelector, forceEmptyTable, callBack){ + function getDataAndFillAttributeTable(layerName, filter, isLinefilter = false, tableSelector, forceEmptyTable, callBack){ let layerConfig = lizMap.config.layers[layerName]; const typeName = layerConfig?.shortname || layerConfig?.typename || layerConfig?.name; + if(filter){ + if (isLinefilter) { + layerConfig['line_filter'] = filter; + } else { + layerConfig['request_params']['filter'] = filter; + } + } else { + layerConfig['line_filter'] = '$id = -99999999'; + layerConfig['request_params']['filter'] = undefined; + } + const wfsParams = { TYPENAME: typeName, GEOMETRYNAME: 'extent' @@ -717,7 +729,7 @@ var lizAttributeTable = function() { const tableSelector = '#attribute-layer-table-'+cleanName; $('#attribute-layer-main-'+cleanName+' > div.attribute-layer-content').hide(); - getDataAndFillAttributeTable(lname, null, tableSelector, false, () => { + getDataAndFillAttributeTable(lname, null, false, tableSelector, false, () => { $('#attribute-layer-main-' + cleanName + ' > div.attribute-layer-content').show(); refreshDatatableSize('#attribute-layer-main-' + cleanName); }); @@ -1247,15 +1259,7 @@ var lizAttributeTable = function() { filter = '"' + relation.referencingField + '" = ' + "'" + fp[relation.referencedField] + "'"; } - // Refresh datatable if it is already created - // Create if it is not - childLayerConfig.line_filter = filter; - if (DataTable.isDataTable(childTableSelector)) { - const childTable = new DataTable(childTableSelector); - childTable.draw(); - } else { - getDataAndFillAttributeTable(childLayerName, filter, childTableSelector, false); - } + getDataAndFillAttributeTable(childLayerName, filter, true, childTableSelector, false); } } } @@ -1407,7 +1411,7 @@ var lizAttributeTable = function() { ){ isPivot = true; } - var pivotReference = null; + let pivotReference = null; // checks if the parent and child are related via pivot if (parentLayerID) { // means that the table is displayed as a child @@ -1451,7 +1455,7 @@ var lizAttributeTable = function() { } // Create columns for datatable - var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID); + var cdc = createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotReference, parentLayerID); var columns = cdc.columns; var firstDisplayedColIndex = cdc.firstDisplayedColIndex; @@ -1622,7 +1626,7 @@ var lizAttributeTable = function() { * @param cTypes * @param allColumnsKeyValues */ - function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotId, parentLayerID){ + function createDatatableColumns(aName, hiddenFields, cAliases, cTypes, allColumnsKeyValues, isChild, pivotReference, parentLayerID){ const columns = []; let firstDisplayedColIndex = 0; // Column with selected status @@ -1644,7 +1648,7 @@ var lizAttributeTable = function() { const layerId = config.layers[aName].id; const fid = row['DT_RowId']; return ` - + `; } }); @@ -1977,7 +1981,7 @@ var lizAttributeTable = function() { * @param forceEmptyTable */ function getEditionChildData( childLayerName, filter, childTable, forceEmptyTable = false ){ - getDataAndFillAttributeTable(childLayerName, filter, childTable, forceEmptyTable, () => { + getDataAndFillAttributeTable(childLayerName, filter, true, childTable, forceEmptyTable, () => { // Check edition capabilities var canCreateChildren = false; var canEdit = false; @@ -3080,7 +3084,7 @@ var lizAttributeTable = function() { // Else refresh main table with no filter else{ // If not pivot - getDataAndFillAttributeTable(featureType, null, zTable, false); + getDataAndFillAttributeTable(featureType, null, false, zTable, false); } } }); From 970792625320e9701a7805a37582f0bfdae875ef Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 8 Apr 2025 15:22:30 +0200 Subject: [PATCH 15/49] Remove legacy code linked to `limitDataToBbox` parameter This parameter was set in the plugin by the `Limit fetched data to the current map extent and layer visibility` checkbox --- assets/src/legacy/attributeTable.js | 98 +---------------------------- assets/src/legacy/map.js | 5 -- assets/src/modules/SelectionTool.js | 6 -- 3 files changed, 2 insertions(+), 107 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index ddd180f129..3db43b1d43 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -51,11 +51,6 @@ var lizAttributeTable = function() { lizMap.lizmapLayerFilterActive = true; } - var limitDataToBbox = false; - if ( 'limitDataToBbox' in config.options && config.options.limitDataToBbox == 'True'){ - limitDataToBbox = true; - } - if (!('attributeLayers' in config)) return -1; @@ -198,16 +193,6 @@ var lizAttributeTable = function() { .click(function(){ var cleanName = $(this).val(); - // Disable attribute table if limitDataToBbox and layer not visible in map - if(limitDataToBbox){ - let layer = lizMap.mainLizmap.map.getLayerByName(lizMap.getLayerNameByCleanName(cleanName)); - if( layer ) { - if(warnResolution(layer)){ - return false; - } - } - } - // Add Div if not already there const layerName = attributeLayersDic[cleanName]; if( !$('#nav-tab-attribute-layer-' + cleanName ).length ){ @@ -334,18 +319,6 @@ var lizAttributeTable = function() { GEOMETRYNAME: 'extent' }; - // Calculate bbox from map extent if needed - if (config.options?.limitDataToBbox == 'True') { - const mapExtent = lizMap.mainLizmap.map.getView().calculateExtent(); - const mapExtent4326 = lizMap.mainLizmap.transformExtent( - mapExtent, - lizMap.mainLizmap.map.getView().getProjection().getCode(), - 'EPSG:4326' - ); - wfsParams['BBOX'] = mapExtent4326; - wfsParams['SRSNAME'] = 'EPSG:4326'; - } - let fetchRequests = []; let namedRequests = []; @@ -442,18 +415,6 @@ var lizAttributeTable = function() { wfsParams['EXP_FILTER'] = filter; } - // Calculate bbox from map extent if needed - if (config.options?.limitDataToBbox == 'True') { - const mapExtent = lizMap.mainLizmap.map.getView().calculateExtent(); - const mapExtent4326 = lizMap.mainLizmap.transformExtent( - mapExtent, - lizMap.mainLizmap.map.getView().getProjection().getCode(), - 'EPSG:4326' - ); - wfsParams['BBOX'] = mapExtent4326; - wfsParams['SRSNAME'] = 'EPSG:4326'; - } - const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); Promise.all([getFeatureRequest]).then(responses => { refreshLayerAttributeDatatable(layerName, tableSelector, responses[0].features); @@ -597,16 +558,6 @@ var lizAttributeTable = function() { html+= ' '; } - // Refresh button (if limitDataToBbox is true) - if( limitDataToBbox - && config.layers[lname]['geometryType'] != 'none' - && config.layers[lname]['geometryType'] != 'unknown' - ){ - // Add button to refresh table - html+= ''; - - } - // Get children content var childHtml = getChildrenHtmlContent( lname ); var alc=''; @@ -705,43 +656,6 @@ var lizAttributeTable = function() { }); } - if(limitDataToBbox){ - $('#attribute-layer-'+ cleanName + ' button.btn-refresh-table') - .click(function(){ - // Reset button tooltip & style - $(this) - .attr('data-bs-toggle', 'tooltip') - .attr('data-bs-title', lizDict['attributeLayers.toolbar.btn.refresh.table.tooltip']) - .removeClass('btn-warning'); - - // Disable if the layer is not visible - let layer = lizMap.mainLizmap.map.getLayerByName(lizMap.getLayerNameByCleanName(cleanName)); - if( layer ) { - if(warnResolution(layer)){ - return false; - } - }else{ - // do nothing if no layer found - return false; - } - - // Refresh table - const tableSelector = '#attribute-layer-table-'+cleanName; - $('#attribute-layer-main-'+cleanName+' > div.attribute-layer-content').hide(); - - getDataAndFillAttributeTable(lname, null, false, tableSelector, false, () => { - $('#attribute-layer-main-' + cleanName + ' > div.attribute-layer-content').show(); - refreshDatatableSize('#attribute-layer-main-' + cleanName); - }); - - return false; - }) - .hover( - function(){ $(this).addClass('btn-primary'); }, - function(){ $(this).removeClass('btn-primary'); } - ); - } - if( childHtml ){ $('#attribute-layer-'+ cleanName + ' button.btn-toggle-children') .click(function(){ @@ -828,7 +742,7 @@ var lizAttributeTable = function() { eFormat = 'GML3'; var cleanName = $(this).parents('div.attribute-layer-main:first').attr('id').replace('attribute-layer-main-', ''); var eName = attributeLayersDic[ cleanName ]; - lizMap.exportVectorLayer( eName, eFormat, limitDataToBbox ); + lizMap.exportVectorLayer( eName, eFormat ); }); // Bind click on createFeature button @@ -2075,11 +1989,7 @@ var lizAttributeTable = function() { if( aName in config.attributeLayers ) atConfig = config.attributeLayers[aName]; - var limitDataToBbox = false; - if ( 'limitDataToBbox' in config.options && config.options.limitDataToBbox == 'True'){ - limitDataToBbox = true; - } - lizMap.getFeatureData(aName, aFilter, aFeatureID, aGeometryName, limitDataToBbox, null, null, aCallBack); + lizMap.getFeatureData(aName, aFilter, aFeatureID, aGeometryName, null, null, null, aCallBack); return true; } @@ -3218,10 +3128,6 @@ var lizAttributeTable = function() { GEOMETRYNAME: 'extent' }; wfsParams['EXP_FILTER'] = '"' + referencedPivotField + '" = ' + "'" + referencedFieldValue + "'"; - if (config.options?.limitDataToBbox == 'True') { - wfsParams['BBOX'] = lizMap.mainLizmap.map.getView().calculateExtent(); - wfsParams['SRSNAME'] = lizMap.mainLizmap.map.getView().getProjection().getCode(); - } const getFeatureRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); diff --git a/assets/src/legacy/map.js b/assets/src/legacy/map.js index 5c600ddfcb..dfdd2cc3e5 100644 --- a/assets/src/legacy/map.js +++ b/assets/src/legacy/map.js @@ -1111,11 +1111,6 @@ window.lizMap = function() { }; wfsParams['EXP_FILTER'] = '"' + config.relations.pivot[rLayerId][layerId] + '" = ' + "'" + feat.properties[relation.referencedField] + "'"; - // Calculate bbox - if (config.options?.limitDataToBbox == 'True') { - wfsParams['BBOX'] = lizMap.mainLizmap.map.getView().calculateExtent(); - wfsParams['SRSNAME'] = lizMap.mainLizmap.map.getView().getProjection().getCode(); - } preProcessRequest = lizMap.mainLizmap.wfs.getFeature(wfsParams); let ut = { diff --git a/assets/src/modules/SelectionTool.js b/assets/src/modules/SelectionTool.js index ca9d82c70a..4d410b58dd 100644 --- a/assets/src/modules/SelectionTool.js +++ b/assets/src/modules/SelectionTool.js @@ -492,12 +492,6 @@ export default class SelectionTool { EXP_FILTER: spatialFilter }; - // Apply limit to bounding box config - if (this._lizmap3.config?.limitDataToBbox === 'True') { - wfsParams['BBOX'] = this._map.getView().calculateExtent(); - wfsParams['SRSNAME'] = this._map.getView().getProjection().getCode(); - } - // Restrict to current geometry extent for performance // But not with 'disjoint' to get features if (this._geomOperator !== 'disjoint') { From b765c7b66f7aad30dc05ded3c6a9ffe01c44793e Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 8 Apr 2025 15:27:07 +0200 Subject: [PATCH 16/49] Attribute table: add a toggle button to filter features in the current extent --- assets/src/images/svg/filter-square.svg | 4 ++ assets/src/legacy/attributeTable.js | 55 +++++++++++++------ .../lizmap/controllers/datatables.classic.php | 31 +++++++++++ .../en_US/dictionnary.UTF-8.properties | 4 +- lizmap/www/assets/css/map.css | 5 ++ 5 files changed, 80 insertions(+), 19 deletions(-) create mode 100644 assets/src/images/svg/filter-square.svg diff --git a/assets/src/images/svg/filter-square.svg b/assets/src/images/svg/filter-square.svg new file mode 100644 index 0000000000..3b58be62da --- /dev/null +++ b/assets/src/images/svg/filter-square.svg @@ -0,0 +1,4 @@ + + + + diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 3db43b1d43..023f79e686 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -6,6 +6,7 @@ */ import DOMPurify from 'dompurify'; +import '../images/svg/filter-square.svg'; var lizAttributeTable = function() { @@ -248,18 +249,14 @@ var lizAttributeTable = function() { {'layers': attributeLayersDic} ); - // Map events - if (limitDataToBbox) { - lizMap.mainLizmap.map.on('moveend', () => { - let btitle = lizDict['attributeLayers.toolbar.btn.refresh.table.tooltip.changed']; - btitle += ' ' + lizDict['attributeLayers.toolbar.btn.refresh.table.tooltip']; - $('button.btn-refresh-table') - .attr('data-bs-toggle', 'tooltip') - .attr('data-bs-title', btitle) - .addClass('btn-warning') - .tooltip(); + // Redraw datatables when map is moved and their filter by extent button is active + lizMap.mainLizmap.map.on('moveend', () => { + document.querySelectorAll('.btn-filterbyextent-attributeTable.active').forEach((btn) => { + const layerId = btn.getAttribute('data-layerid'); + const dTable = new DataTable('table[data-layerid=' + layerId + ']'); + dTable.draw(); }); - } + }); // Bind click on tabs to resize datatable tables $('#attributeLayers-tabs li').click(function(){ @@ -525,6 +522,14 @@ var lizAttributeTable = function() { html+= ' '; } + // Filter data by extent button + html+= ` + `; + // Invert selection html += '' @@ -936,11 +941,17 @@ var lizAttributeTable = function() { { 'featureType': aName, 'updateDrawing': true} ); return false; - }) - .hover( - function(){ $(this).addClass('btn-primary'); }, - function(){ $(this).removeClass('btn-primary'); } - ); + }).hover( + function(){ $(this).addClass('btn-primary'); }, + function(){ $(this).removeClass('btn-primary'); } + ); + + // Bind click on btn-filterbyextent button + document.querySelector('#attribute-layer-'+ cleanName + ' button.btn-filterbyextent-attributeTable').addEventListener('click', (e) => { + const layerId = e.currentTarget.getAttribute('data-layerid'); + const dTable = new DataTable('table[data-layerid=' + layerId + ']'); + dTable.draw(); + }); } /** @@ -1425,10 +1436,22 @@ var lizAttributeTable = function() { d.exp_filter = exp_filter; } } + // Handle features filtered by extent + if (document.querySelector('.btn-filterbyextent-attributeTable.active[value="' + cleanName + '"]')) { + const olView = lizMap.mainLizmap.map.getView(); + const bbox = olView.calculateExtent().join(','); + d.bbox = bbox; + const projCode = olView.getProjection().getCode(); + d.srsname = projCode; + } }, dataSrc: (json) => { // Format data for DataTables let formatedData = []; + if (!json.data) { + return formatedData; + } + for (const feature of json.data.features) { const featID = feature.id.split('.').pop(); formatedData.push(Object.assign({ diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index f94860d51e..af03707974 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -36,6 +36,12 @@ public function index() } $expFilter = $this->param('exp_filter'); + $bbox = array(); + $srsName = $this->param('srsname'); + if ($this->param('bbox') && $srsName) { + $bbox = explode(',', $this->param('bbox')); + } + // DataTables parameters $DTStart = $this->param('start'); $DTLength = $this->param('length'); @@ -122,6 +128,31 @@ public function index() $wfsParamsData['EXP_FILTER'] = $expFilter; } + // Handle filter by extent + if (count($bbox) == 4) { + // Add parameters to get features in the bounding box (paginated) + $bboxString = implode(',', $bbox); + $wfsParamsData['BBOX'] = $bboxString; + $wfsParamsData['SRSNAME'] = $srsName; + + // Get total number of features in the bounding box + $wfsParamsFilterByExtentHits = array( + 'SERVICE' => 'WFS', + 'VERSION' => '1.0.0', + 'REQUEST' => 'GetFeature', + 'TYPENAME' => $typeName, + 'RESULTTYPE' => 'hits', + 'BBOX' => $bboxString, + 'SRSNAME' => $srsName, + ); + + $wfsrequest = new WFSRequest($lproj, $wfsParamsFilterByExtentHits, lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $filterByExtentHitsData = $wfsresponse->getBodyAsString(); + preg_match('/numberOfFeatures="([0-9]+)"/', $filterByExtentHitsData, $matches); + $recordsFiltered = $matches[1]; + } + $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $featureData = $wfsresponse->getBodyAsString(); diff --git a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties index 57dc404faf..28601005a8 100644 --- a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties +++ b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties @@ -111,9 +111,7 @@ attributeLayers.toolbar.btn.toggle.children.title=Toggle children tables attributeLayers.toolbar.btn.data.linkFeatures.title=Link selected features attributeLayers.toolbar.btn.data.hide.title=Hide attributeLayers.toolbar.btn.data.show.title=Show -attributeLayers.toolbar.btn.refresh.table.title=Refresh -attributeLayers.toolbar.btn.refresh.table.tooltip=Refresh the table data. -attributeLayers.toolbar.btn.refresh.table.tooltip.changed=The map extent has changed since the last time data were fetched. +attributeLayers.toolbar.btn.filterByExtent.title=Filter the table with the features in the current map extent attributeLayers.toolbar.msg.data.lines=lines returned attributeLayers.toolbar.msg.data.nodata=No result for this layer attributeLayers.toolbar.msg.data.extent=in the current extent diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css index a2762ac13b..3c573823df 100644 --- a/lizmap/www/assets/css/map.css +++ b/lizmap/www/assets/css/map.css @@ -1395,6 +1395,11 @@ SubDock overflow: auto; } +.btn-filterbyextent-attributeTable svg { + height: 14px; + width: 14px; +} + .attribute-layer-content { overflow:auto; height: 90%; From 8626e72a96675426688d7ed53cf5258cd8dbb48d Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 8 Apr 2025 17:07:42 +0200 Subject: [PATCH 17/49] e2e: test data filtered by extent in attribute table TODO: handle export --- .../playwright/attribute-table.spec.js | 50 ++++++++++++------- .../playwright/fixtures/expect-response.js | 16 +++--- tests/end2end/playwright/pages/project.js | 12 +++++ 3 files changed, 54 insertions(+), 24 deletions(-) diff --git a/tests/end2end/playwright/attribute-table.spec.js b/tests/end2end/playwright/attribute-table.spec.js index da7c53ba80..364d51441c 100644 --- a/tests/end2end/playwright/attribute-table.spec.js +++ b/tests/end2end/playwright/attribute-table.spec.js @@ -677,18 +677,10 @@ test.describe('Attribute table @readonly', () => { await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) .toContainText('Showing 651 to 700 of 700 entries'); }); -}); test.describe('Attribute table data restricted to map extent @readonly', () => { - test('Data restriction and refresh button behaviour', async ({ page }) => { - // Update config to update limitDataToBbox - await page.route('**/service/getProjectConfig*', async route => { - const response = await route.fetch(); - const json = await response.json(); - json.options['limitDataToBbox'] = 'True'; - await route.fulfill({ response, json }); - }); + test('Data filtered by extent', async ({ page }) => { const project = new ProjectPage(page, 'attribute_table'); // Catch default GetMap @@ -716,22 +708,36 @@ test.describe('Attribute table data restricted to map extent @readonly', () => { requestExpect(getMapRequest).toContainParametersInUrl(getMapExpectedParameters); await getMapRequest.response(); - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(tableName); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); + let tableHtml = project.attributeTableHtml(tableName); // Check table lines await expect(tableHtml.locator('tbody tr')).toHaveCount(7); + // Check filter by extent button + await expect(page.locator('.btn-filterbyextent-table')).not.toHaveClass(/active/); + + // Activate filter by extent + datatablesRequestPromise = project.waitForDatatablesRequest(); + await page.locator('.btn-filterbyextent-table').click(); + datatablesRequest = await datatablesRequestPromise; + datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); - await expect(page.locator('.btn-refresh-table')).not.toHaveClass(/btn-warning/); + // Check table lines + await expect(tableHtml.locator('tbody tr')).toHaveCount(7); + // Check filter by extent button + await expect(page.locator('.btn-filterbyextent-table')).toHaveClass(/active/); // Use the first line let firstTr = tableHtml.locator('tbody tr').first(); await expect(firstTr.locator('lizmap-feature-toolbar .feature-zoom')).toBeVisible(); + // Zoom to feature of the first line getMapRequestPromise = project.waitForGetMapRequest(); await firstTr.locator('lizmap-feature-toolbar .feature-zoom').click(); @@ -741,12 +747,20 @@ test.describe('Attribute table data restricted to map extent @readonly', () => { requestExpect(getMapRequest).toContainParametersInUrl(getMapExpectedParameters); await getMapRequest.response(); - await expect(page.locator('.btn-refresh-table')).toHaveClass(/btn-warning/); + // Check table lines + await expect(tableHtml.locator('tbody tr')).toHaveCount(5); - // Refresh - await page.locator('.btn-refresh-table').click(); + // Unactivate filter by extent and assert all features are in the table + datatablesRequestPromise = project.waitForDatatablesRequest(); + await page.locator('.btn-filterbyextent-table').click(); + datatablesRequest = await datatablesRequestPromise; + datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); - await expect(tableHtml.locator('tbody tr')).toHaveCount(5); + // Check table lines + await expect(tableHtml.locator('tbody tr')).toHaveCount(7); + // Check filter by extent button + await expect(page.locator('.btn-filterbyextent-table')).not.toHaveClass(/active/); }); }); diff --git a/tests/end2end/playwright/fixtures/expect-response.js b/tests/end2end/playwright/fixtures/expect-response.js index d50a4ee836..be4e5a15d3 100644 --- a/tests/end2end/playwright/fixtures/expect-response.js +++ b/tests/end2end/playwright/fixtures/expect-response.js @@ -106,7 +106,7 @@ export const expect = baseExpect.extend({ /** * Expecting the response is a valid JSON - * @param {APIResponse} response the response to test + * @param {APIResponse|Response|null} response the response to test * * @returns {MatcherReturnType} the result */ @@ -115,10 +115,10 @@ export const expect = baseExpect.extend({ let pass = true; try { // check response status - expect(response.ok()).toBeTruthy(); - expect(response.status()).toBe(200); + expect(response?.ok()).toBeTruthy(); + expect(response?.status()).toBe(200); // check content-type header - expect(response.headers()['content-type']).toContain('application/json'); + expect(response?.headers()['content-type']).toContain('application/json'); } catch { pass = false; } @@ -127,13 +127,17 @@ export const expect = baseExpect.extend({ pass =!pass; } + const received = (response !== null ? `${response?.status()} ${response?.headers()['content-type']}` : 'null'); + const message = pass ? () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) + '\n\n' + - `Response is JSON: ${response.status()} ${response.headers()['content-type']}` + 'Response is JSON\n'+ + `Received: ${received}` : () => this.utils.matcherHint(assertionName, undefined, undefined, { isNot: this.isNot }) + '\n\n' + - `Response is not JSON: ${response.status()} ${response.headers()['content-type']}`; + 'Response is not JSON\n'+ + `Received: ${received}`; return { message, diff --git a/tests/end2end/playwright/pages/project.js b/tests/end2end/playwright/pages/project.js index 2216aa3f25..66634220d9 100644 --- a/tests/end2end/playwright/pages/project.js +++ b/tests/end2end/playwright/pages/project.js @@ -339,6 +339,18 @@ export class ProjectPage extends BasePage { ); } + /** + * Waits for a datatables request + * @returns {Promise} The datatables request + */ + async waitForDatatablesRequest() { + return this.page.waitForRequest( + request => request.method() === 'POST' && + request.url().includes('datatables') === true && + request.postData()?.includes('draw') === true + ); + } + /** * open function * Open the URL for the given project and repository From 7d11ae8b8f62195161628c18e94a8e38419402bf Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 11 Apr 2025 16:14:09 +0200 Subject: [PATCH 18/49] Attribute table: install Datatables v 2.2.2 --- assets/src/legacy/attributeTable.js | 1 + .../app/responses/myHtmlMapResponse.class.php | 3 +- lizmap/www/assets/css/datatables.min.css | 19 ++++++++++++ package-lock.json | 29 +++++++++++++++++++ package.json | 1 + stylelint.config.mjs | 1 + 6 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 lizmap/www/assets/css/datatables.min.css diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 023f79e686..320f7e85b0 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -5,6 +5,7 @@ * @license MPL-2.0 */ +import DataTable from 'datatables.net-bs5'; import DOMPurify from 'dompurify'; import '../images/svg/filter-square.svg'; diff --git a/lizmap/app/responses/myHtmlMapResponse.class.php b/lizmap/app/responses/myHtmlMapResponse.class.php index 6189cbadba..183ce247e1 100644 --- a/lizmap/app/responses/myHtmlMapResponse.class.php +++ b/lizmap/app/responses/myHtmlMapResponse.class.php @@ -35,9 +35,10 @@ public function __construct() $this->addAssets('jquery_ui'); $this->addAssets('bootstrap'); - $this->addAssets('datatables'); $this->addAssets('map'); + $this->addCSSLink($bp.'assets/css/datatables.min.css'); + $this->setBodyAttributes(array('data-proj4js-lib-path' => $bp.'assets/js/Proj4js/')); } diff --git a/lizmap/www/assets/css/datatables.min.css b/lizmap/www/assets/css/datatables.min.css new file mode 100644 index 0000000000..095585d8bb --- /dev/null +++ b/lizmap/www/assets/css/datatables.min.css @@ -0,0 +1,19 @@ +/* + * This combined file was created by the DataTables downloader builder: + * https://datatables.net/download + * + * To rebuild or modify this file with the latest versions of the included + * software please visit: + * https://datatables.net/download/#bs5/dt-2.2.2 + * + * Included libraries: + * DataTables 2.2.2 + */ + +:root{--dt-row-selected: 13, 110, 253;--dt-row-selected-text: 255, 255, 255;--dt-row-selected-link: 9, 10, 11;--dt-row-stripe: 0, 0, 0;--dt-row-hover: 0, 0, 0;--dt-column-ordering: 0, 0, 0;--dt-html-background: white}:root.dark{--dt-html-background: rgb(33, 37, 41)}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{display:inline-block;box-sizing:border-box;content:"";border-top:5px solid transparent;border-left:10px solid rgba(0, 0, 0, 0.5);border-bottom:5px solid transparent;border-right:0px solid transparent}table.dataTable tr.dt-hasChild td.dt-control:before{border-top:10px solid rgba(0, 0, 0, 0.5);border-left:5px solid transparent;border-bottom:0px solid transparent;border-right:5px solid transparent}table.dataTable tfoot:empty{display:none}html.dark table.dataTable td.dt-control:before,:root[data-bs-theme=dark] table.dataTable td.dt-control:before,:root[data-theme=dark] table.dataTable td.dt-control:before{border-left-color:rgba(255, 255, 255, 0.5)}html.dark table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before{border-top-color:rgba(255, 255, 255, 0.5);border-left-color:transparent}div.dt-scroll{width:100%}div.dt-scroll-body thead tr,div.dt-scroll-body tfoot tr{height:0}div.dt-scroll-body thead tr th,div.dt-scroll-body thead tr td,div.dt-scroll-body tfoot tr th,div.dt-scroll-body tfoot tr td{height:0 !important;padding-top:0px !important;padding-bottom:0px !important;border-top-width:0px !important;border-bottom-width:0px !important}div.dt-scroll-body thead tr th div.dt-scroll-sizing,div.dt-scroll-body thead tr td div.dt-scroll-sizing,div.dt-scroll-body tfoot tr th div.dt-scroll-sizing,div.dt-scroll-body tfoot tr td div.dt-scroll-sizing{height:0 !important;overflow:hidden !important}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before{position:absolute;display:block;bottom:50%;content:"▲";content:"▲"/""}table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{position:absolute;display:block;top:50%;content:"▼";content:"▼"/""}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>th.dt-ordering-asc,table.dataTable thead>tr>th.dt-ordering-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc,table.dataTable thead>tr>td.dt-ordering-asc,table.dataTable thead>tr>td.dt-ordering-desc{position:relative;padding-right:30px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order{position:absolute;right:12px;top:0;bottom:0;width:12px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{left:0;opacity:.125;line-height:9px;font-size:.8em}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc{cursor:pointer}table.dataTable thead>tr>th.dt-orderable-asc:hover,table.dataTable thead>tr>th.dt-orderable-desc:hover,table.dataTable thead>tr>td.dt-orderable-asc:hover,table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(0, 0, 0, 0.05);outline-offset:-2px}table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{opacity:.6}table.dataTable thead>tr>th.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>th.sorting_asc_disabled span.dt-column-order:before,table.dataTable thead>tr>td.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>td.sorting_asc_disabled span.dt-column-order:before{display:none}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}div.dt-scroll-body>table.dataTable>thead>tr>th,div.dt-scroll-body>table.dataTable>thead>tr>td{overflow:hidden}:root.dark table.dataTable thead>tr>th.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>th.dt-orderable-desc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(255, 255, 255, 0.05)}div.dt-processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-22px;text-align:center;padding:2px;z-index:10}div.dt-processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dt-processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:rgb(13, 110, 253);background:rgb(var(--dt-row-selected));animation-timing-function:cubic-bezier(0, 1, 1, 0)}div.dt-processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dt-processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0, 0)}100%{transform:translate(24px, 0)}}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable th,table.dataTable td{box-sizing:border-box}table.dataTable th.dt-type-numeric,table.dataTable th.dt-type-date,table.dataTable td.dt-type-numeric,table.dataTable td.dt-type-date{text-align:right}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable th.dt-empty,table.dataTable td.dt-empty{text-align:center;vertical-align:top}table.dataTable thead th,table.dataTable thead td,table.dataTable tfoot th,table.dataTable tfoot td{text-align:left}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}/*! Bootstrap 5 integration for DataTables + * + * ©2020 SpryMedia Ltd, all rights reserved. + * License: MIT datatables.net/license/mit + */table.table.dataTable{clear:both;margin-bottom:0;max-width:none;border-spacing:0}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:none}table.table.dataTable>:not(caption)>*>*{background-color:var(--bs-table-bg)}table.table.dataTable>tbody>tr{background-color:transparent}table.table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px rgb(13, 110, 253);box-shadow:inset 0 0 0 9999px rgb(var(--dt-row-selected));color:rgb(255, 255, 255);color:rgb(var(--dt-row-selected-text))}table.table.dataTable>tbody>tr.selected a{color:rgb(9, 10, 11);color:rgb(var(--dt-row-selected-link))}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05)}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1).selected>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.95);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95)}table.table.dataTable.table-hover>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075)}table.table.dataTable.table-hover>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.975);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975)}div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:1em}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:1em}div.dt-container div.dt-layout-full{width:100%}div.dt-container div.dt-layout-full>*:only-child{margin-left:auto;margin-right:auto}div.dt-container div.dt-layout-table>div{display:block !important}@media screen and (max-width: 767px){div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:0}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:0}}div.dt-container div.dt-length label{font-weight:normal;text-align:left;white-space:nowrap}div.dt-container div.dt-length select{width:auto;display:inline-block;margin-right:.5em}div.dt-container div.dt-search{text-align:right}div.dt-container div.dt-search label{font-weight:normal;white-space:nowrap;text-align:left}div.dt-container div.dt-search input{margin-left:.5em;display:inline-block;width:auto}div.dt-container div.dt-paging{margin:0}div.dt-container div.dt-paging ul.pagination{margin:2px 0;flex-wrap:wrap}div.dt-container div.dt-row{position:relative}div.dt-scroll-head table.dataTable{margin-bottom:0 !important}div.dt-scroll-body{border-bottom-color:var(--bs-border-color);border-bottom-width:var(--bs-border-width);border-bottom-style:solid}div.dt-scroll-body>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dt-scroll-body>table>tbody>tr:first-child{border-top-width:0}div.dt-scroll-body>table>thead>tr{border-width:0 !important}div.dt-scroll-body>table>tbody>tr:last-child>*{border-bottom:none}div.dt-scroll-foot>.dt-scroll-footInner{box-sizing:content-box}div.dt-scroll-foot>.dt-scroll-footInner>table{margin-top:0 !important;border-top:none}div.dt-scroll-foot>.dt-scroll-footInner>table>tfoot>tr:first-child{border-top-width:0 !important}@media screen and (max-width: 767px){div.dt-container div.dt-length,div.dt-container div.dt-search,div.dt-container div.dt-info,div.dt-container div.dt-paging{text-align:center}div.dt-container .row{--bs-gutter-y: 0.5rem}div.dt-container div.dt-paging ul.pagination{justify-content:center !important}}table.dataTable.table-sm>thead>tr th.dt-orderable-asc,table.dataTable.table-sm>thead>tr th.dt-orderable-desc,table.dataTable.table-sm>thead>tr th.dt-ordering-asc,table.dataTable.table-sm>thead>tr th.dt-ordering-desc,table.dataTable.table-sm>thead>tr td.dt-orderable-asc,table.dataTable.table-sm>thead>tr td.dt-orderable-desc,table.dataTable.table-sm>thead>tr td.dt-ordering-asc,table.dataTable.table-sm>thead>tr td.dt-ordering-desc{padding-right:20px}table.dataTable.table-sm>thead>tr th.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-desc span.dt-column-order{right:5px}div.dt-scroll-head table.table-bordered{border-bottom-width:0}div.table-responsive>div.dt-container>div.row{margin:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:last-child{padding-right:0}:root[data-bs-theme=dark]{--dt-row-hover: 255, 255, 255;--dt-row-stripe: 255, 255, 255;--dt-column-ordering: 255, 255, 255} + + diff --git a/package-lock.json b/package-lock.json index 363c70997f..3d8373fd8e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -26,6 +26,7 @@ "chai": "^6.0.x", "cypress": "<10.0.0", "cypress-file-upload": "^5.0.x", + "datatables.net-bs5": "^2.2.2", "dompurify": "^3.2.x", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "^4.3.x", @@ -3888,6 +3889,27 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/datatables.net": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/datatables.net/-/datatables.net-2.3.4.tgz", + "integrity": "sha512-fKuRlrBIdpAl2uIFgl9enKecHB41QmFd/2nN9LBbOvItV/JalAxLcyqdZXex7wX4ZXjnJQEnv6xeS9veOpKzSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-bs5": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/datatables.net-bs5/-/datatables.net-bs5-2.3.4.tgz", + "integrity": "sha512-OSoPWhNfiU71VjNP604uTmFRxiX32U7SCW0KRZ2X6z3ZYbIwjjoWcMEjjPWOH3uOqaI0OTDBgOgOs5G28VaJog==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net": "2.3.4", + "jquery": ">=1.7" + } + }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", @@ -7149,6 +7171,13 @@ "node": ">= 10.13.0" } }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/js-base64": { "version": "2.6.4", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.6.4.tgz", diff --git a/package.json b/package.json index d92369d775..2f825f012c 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "cypress": "<10.0.0", "cypress-file-upload": "^5.0.x", "dompurify": "^3.2.x", + "datatables.net-bs5": "^2.2.2", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "^4.3.x", "jsts": "^2.11.x", diff --git a/stylelint.config.mjs b/stylelint.config.mjs index ca0f28030c..d21e1bed4b 100644 --- a/stylelint.config.mjs +++ b/stylelint.config.mjs @@ -24,6 +24,7 @@ export default { "lizmap/www/assets/css/dataTables.bootstrap.min.css", "lizmap/www/assets/css/jquery.dataTables.min.css", "lizmap/www/assets/css/responsive.dataTables.min.css", + "lizmap/www/assets/css/datatables.min.css", "lizmap/www/assets/jelix/**/*.css", "lizmap/www/assets/js/jquery/**/*.css", "tests/units/vendor/**/*.css", From 12268e92ca224f0b6e2f9cd63977b40cbc52bf28 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 11 Apr 2025 16:15:25 +0200 Subject: [PATCH 19/49] Attribute table: update code for Datatables v2.2.2 Legacy datatables is kept for admin part of Lizmap --- assets/src/legacy/attributeTable.js | 143 ++++++---------------------- assets/src/legacy/map.js | 4 +- lizmap/www/assets/css/map.css | 83 ---------------- 3 files changed, 34 insertions(+), 196 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 320f7e85b0..a76553c907 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -258,13 +258,6 @@ var lizAttributeTable = function() { dTable.draw(); }); }); - - // Bind click on tabs to resize datatable tables - $('#attributeLayers-tabs li').click(function(){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - }); - } else { // Hide navbar menu $('#mapmenu li.attributeLayers').hide(); @@ -501,12 +494,6 @@ var lizAttributeTable = function() { // Action bar specific to the tab html+= '
'; - // Search input - html+= '
'; - html+= ' '; - html+= ' '; - html+= '
'; - // Selected searched lines button html+= ''; @@ -652,22 +639,12 @@ var lizAttributeTable = function() { $(tabContentId).remove(); //remove respective tab content }); - if( childHtml ){ - - // Bind adjust child columns when children tab visibility change - $('#attribute-layer-' + cleanName + ' div.attribute-layer-child-content ul li a[data-toggle="tab"]').on('shown.bs.tab', function (e) { - var target = $(e.target).attr("href") // activated tab - var dtable = $(target).find('table.dataTable'); - dtable.DataTable().tables().columns.adjust(); - }); - } - if( childHtml ){ $('#attribute-layer-'+ cleanName + ' button.btn-toggle-children') .click(function(){ var parentDir = $(this).parents('div.attribute-layer-main'); parentDir.find('div.attribute-layer-content').toggleClass('showChildren'); - parentDir.find('div.tabbable.attribute-layer-child-content').toggle(); + parentDir.find('div.attribute-layer-child-content').toggle(); // Refresh parent table size refreshDatatableSize('#attribute-layer-main-'+ cleanName); return false; @@ -683,8 +660,6 @@ var lizAttributeTable = function() { $('#attribute-layer-main-' + cleanName).toggleClass('reduced', !$(this).hasClass('btn-primary')); $('#attribute-table-panel-' + cleanName).toggleClass('visible', !$(this).hasClass('btn-primary')); $(this).toggleClass('btn-primary'); - - refreshDatatableSize('#attribute-layer-main-'+ cleanName); return false; }); } @@ -1387,28 +1362,12 @@ var lizAttributeTable = function() { lConfig['alias'] = cAliases; // Datatable configuration - if ( !$.fn.dataTable.isDataTable( aTable ) ) { - // Search while typing in text input - // Deactivate if too many items - var searchWhileTyping = true; - - var myDom = '<ipl>'; - if( searchWhileTyping ) { - $('#attribute-layer-search-' + cleanName).on( 'keyup', function (e){ - var searchVal = this.value; - lizdelay(function(){ - oTable.fnFilter( searchVal ); - }, 500 ); - }); - }else{ - myDom = '<ipl>'; - } - + if ( !DataTable.isDataTable( aTable ) ) { const datatablesUrl = globalThis['lizUrls'].wms.replace('service', 'datatables'); const params = globalThis['lizUrls'].params; params['layerId'] = lConfig.id; - $( aTable ).dataTable({ + const oTable = new DataTable(aTable, { serverSide: true ,ajax: { url: datatablesUrl + '?' + new URLSearchParams(params).toString(), @@ -1468,17 +1427,17 @@ var lizAttributeTable = function() { } } ,columns: columns - ,initComplete: function(settings, json) { - const api = new $.fn.dataTable.Api(settings); - const tableId = api.table().node().id; - const featureType = tableId.split('attribute-layer-table-')[1]; + ,initComplete: function(settings) { + // Refresh size of datatable after data has been loaded + refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')); // Trigger event telling attribute table is ready - lizMap.events.triggerEvent("attributeLayerContentReady", - { - 'featureType': featureType - } - ); + const tableId = settings.api.table().node().id; + const featureType = tableId.split('attribute-layer-table-')[1]; + + lizMap.events.triggerEvent("attributeLayerContentReady",{ + 'featureType': featureType, + }); } ,order: [[ firstDisplayedColIndex, "asc" ]] ,language: { url:globalThis['lizUrls']["dataTableLanguage"] } @@ -1496,43 +1455,35 @@ var lizAttributeTable = function() { thumbnail.setAttribute('src', lizUrls.media+'?repository='+lizUrls.params.repository+'&project='+lizUrls.params.project+'&path='+thumbnail.dataset.src); } } - ,dom: myDom ,pageLength: 50 - ,scrollY: '95%' - ,scrollX: '100%' - } ); - - var oTable = $( aTable ).dataTable(); - - if( !searchWhileTyping ) - $('#attribute-layer-search-' + cleanName).hide(); - - // Bind button which clears top-left search input content - $('#attribute-layer-search-' + cleanName).next('.clear-layer-search').click(function(){ - $('#attribute-layer-search-' + cleanName).val('').focus().keyup(); + ,scrollY: '95%' // Used to init but refreshDatatableSize() does the job of setting the height + ,layout: { + topStart: null, + topEnd: null, + bottomStart: ['info', 'pageLength'], + bottomEnd: { + paging: { + firstLast: false + } + }, + } }); // Unbind previous events on page - $( aTable ).on( 'page.dt', function() { + oTable.on( 'page', function() { // unbind previous events $(aTable +' tr').unbind('click'); $(aTable +' tr td button').unbind('click'); }); // Bind events when drawing table - $( aTable ).on( 'draw.dt', function() { + oTable.on( 'draw', function() { $(aTable +' tr').unbind('click'); $(aTable +' tr td button').unbind('click'); // Bind event when users click anywhere on the table line to highlight bindTableLineClick(aName, aTable); - - // Refresh size - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - - refreshDatatableSize('#' + mycontainerId); - return false; }); @@ -1547,8 +1498,6 @@ var lizAttributeTable = function() { lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id],[exp_f]); } - refreshDatatableSize('#'+$('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id')) - if (aCallback) aCallback(aName,aTable); @@ -2069,7 +2018,7 @@ var lizAttributeTable = function() { var tableLayerName = $(this).parents('div.dataTables_wrapper:first').prev('input.attribute-table-hidden-layer').val() // Get parent table for the feature type if ( tableLayerName - && $.fn.dataTable.isDataTable( $(this) ) + && DataTable.isDataTable( $(this) ) && lizMap.cleanName( featureType ) == tableLayerName ){ @@ -2932,7 +2881,7 @@ var lizAttributeTable = function() { var tableLayerName = $(this).parents('div.dataTables_wrapper:first').prev('input.attribute-table-hidden-layer').val() if ( tableLayerName - && $.fn.dataTable.isDataTable( $(this) ) + && DataTable.isDataTable( $(this) ) && lizMap.cleanName( featureType ) == tableLayerName ){ var zTable = '#' + tableId; @@ -3039,14 +2988,10 @@ var lizAttributeTable = function() { var h = $(container + ' div.attribute-layer-content').height() ? $(container + ' div.attribute-layer-content').height() : 0; h -= $(container + ' thead').height() ? $(container + ' thead').height() : 0; - h -= $(container + ' div.dataTables_paginate').height() ? $(container + ' div.dataTables_paginate').height() : 0; - h -= $(container + ' div.dataTables_filter').height() ? $(container + ' div.dataTables_filter').height() : 0; - h -= 20; - - dtable.parent('div.dataTables_scrollBody').height(h); + h -= $(container + ' div.dt-paging').height() ? $(container + ' div.dt-paging').height() : 0; + h -= 25; - // Width : adapt columns size - dtable.DataTable().tables().columns.adjust(); + dtable.parent('div.dt-scroll-body').height(h); } /** @@ -3525,35 +3470,10 @@ var lizAttributeTable = function() { } }, - bottomdocksizechanged: function(evt) { + bottomdocksizechanged: function() { var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); refreshDatatableSize('#'+mycontainerId); - }, - dockopened: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } - }, - dockclosed: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } - }, - rightdockopened: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } - }, - rightdockclosed: function(evt) { - if($('#mapmenu li.attributeLayers').hasClass('active')){ - var mycontainerId = $('#bottom-dock div.bottom-content.active div.attribute-layer-main').attr('id'); - refreshDatatableSize('#'+mycontainerId); - } } - }); // lizMap.events.on end // Extend lizMap API @@ -3561,5 +3481,4 @@ var lizAttributeTable = function() { } // uicreated }); - }(); diff --git a/assets/src/legacy/map.js b/assets/src/legacy/map.js index dfdd2cc3e5..0410e0e439 100644 --- a/assets/src/legacy/map.js +++ b/assets/src/legacy/map.js @@ -15,6 +15,8 @@ import WFS from '../modules/WFS.js'; import WMS from '../modules/WMS.js'; import { Utils } from '../modules/Utils.js'; +import DataTable from 'datatables.net-bs5'; + window.lizMap = function() { /** * PRIVATE Property: config @@ -1331,7 +1333,7 @@ window.lizMap = function() { } // Handle compact-tables/explode-tables behaviour - parentDiv.find('.lizmapPopupChildren .popupAllFeaturesCompact table').DataTable({ + new DataTable(parentDiv.find('.lizmapPopupChildren .popupAllFeaturesCompact table'),{ order: [[1, 'asc']], language: { url:globalThis['lizUrls']["dataTableLanguage"] } }); diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css index 3c573823df..c4e5c961a6 100644 --- a/lizmap/www/assets/css/map.css +++ b/lizmap/www/assets/css/map.css @@ -1289,88 +1289,6 @@ SubDock font-size: 1em; } -#attributeLayers .tab-content .attribute-content div.dataTables_wrapper div.dataTables_paginate ul.pagination { - margin: 2px 0; - white-space: nowrap; -} - -#attributeLayers .tab-content .attribute-content .pagination { - display: inline-block; - padding-left: 0; - margin: 20px 0; - border-radius: 4px; -} - -#attributeLayers .tab-content .attribute-content .pagination > li { - display: inline; - box-sizing: border-box; - padding:0; - margin:0; - border:0; - border-radius:0; -} - -#attributeLayers .tab-content .attribute-content .pagination > li > a, -#attributeLayers .tab-content .attribute-content .pagination > li > span { - position: relative; - float: left; - padding: 6px 12px; - margin-left: -1px; - line-height: 1.4286; - color: #337ab7; - text-decoration: none; - background-color: #fff; - border: 1px solid #ddd; -} - -#attributeLayers .tab-content .attribute-content .pagination > li > a:focus, -#attributeLayers .tab-content .attribute-content .pagination > li > a:hover, -#attributeLayers .tab-content .attribute-content .pagination > li > span:focus, -#attributeLayers .tab-content .attribute-content .pagination > li > span:hover { - z-index: 3; - color: #23527c; - background-color: #eee; - border-color: #ddd; -} - -#attributeLayers .tab-content .attribute-content .pagination > .active > a, -#attributeLayers .tab-content .attribute-content .pagination > .active > a:focus, -#attributeLayers .tab-content .attribute-content .pagination > .active > a:hover, -#attributeLayers .tab-content .attribute-content .pagination > .active > span, -#attributeLayers .tab-content .attribute-content .pagination > .active > span:focus, -#attributeLayers .tab-content .attribute-content .pagination > .active > span:hover { - z-index: 2; - color: #fff; - cursor: default; - background-color: #337ab7; - border-color: #337ab7; -} - -#attributeLayers .tab-content .attribute-content .pagination > .disabled > a, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > a:focus, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > a:hover, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > span, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > span:focus, -#attributeLayers .tab-content .attribute-content .pagination > .disabled > span:hover { - color: #777; - cursor: not-allowed; - background-color: #fff; - border-color: #ddd; -} - -#attributeLayers .tab-content .attribute-content .pagination > li:first-child > a, -#attributeLayers .tab-content .attribute-content .pagination > li:first-child > span { - margin-left: 0; - border-top-left-radius: 4px; - border-bottom-left-radius: 4px; -} - -#attributeLayers .tab-content .attribute-content .pagination > li:last-child > a, -#attributeLayers .tab-content .attribute-content .pagination > li:last-child > span { - border-top-right-radius: 4px; - border-bottom-right-radius: 4px; -} - #attribute-summary.attribute-content.bottom-content{ overflow:auto; } @@ -1401,7 +1319,6 @@ SubDock } .attribute-layer-content { - overflow:auto; height: 90%; clear:right; } From 855d7fec0d92f3259b01e5874c6d0dec251b585c Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 15 Apr 2025 10:31:49 +0200 Subject: [PATCH 20/49] Add Datataables searchBuilder --- assets/src/legacy/attributeTable.js | 27 +++++++++++- lizmap/www/assets/css/datatables.min.css | 13 +++++- lizmap/www/assets/css/map.css | 10 ++++- package-lock.json | 56 ++++++++++++++++++++++++ package.json | 3 ++ 5 files changed, 105 insertions(+), 4 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index a76553c907..2b17003d00 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -6,6 +6,9 @@ */ import DataTable from 'datatables.net-bs5'; +import 'datatables.net-buttons-bs5'; +import DateTime from 'datatables.net-datetime'; +import 'datatables.net-searchbuilder-bs5'; import DOMPurify from 'dompurify'; import '../images/svg/filter-square.svg'; @@ -1469,6 +1472,28 @@ var lizAttributeTable = function() { } }); + // Add searchBuilder button + // Disable live search to avoid searching on each keystroke + // Only display columns that are sortable for search + const searchBuilderButton = new DataTable.Buttons(oTable, { + buttons: [ + { + extend: 'searchBuilder', + config: { + liveSearch: false, + columns: '.dt-orderable-asc' + } + } + ] + }); + + // Attach searchBuilder button to attribute-layer-action-bar + const actionBar = document.querySelector(aTable) + .closest('.attribute-layer-content') + .previousElementSibling; + + actionBar.insertAdjacentElement('afterbegin', searchBuilderButton.container()[0]); + // Unbind previous events on page oTable.on( 'page', function() { // unbind previous events @@ -1521,7 +1546,7 @@ var lizAttributeTable = function() { data: "lizSelected", width: "25px", searchable: false, - sortable: true, + sortable: false, visible: false }); firstDisplayedColIndex+=1; diff --git a/lizmap/www/assets/css/datatables.min.css b/lizmap/www/assets/css/datatables.min.css index 095585d8bb..721e5366a6 100644 --- a/lizmap/www/assets/css/datatables.min.css +++ b/lizmap/www/assets/css/datatables.min.css @@ -4,10 +4,10 @@ * * To rebuild or modify this file with the latest versions of the included * software please visit: - * https://datatables.net/download/#bs5/dt-2.2.2 + * https://datatables.net/download/#bs5/dt-2.2.2/b-3.2.2/date-1.5.5/sb-1.8.2 * * Included libraries: - * DataTables 2.2.2 + * DataTables 2.2.2, Buttons 3.2.2, DateTime 1.5.5, SearchBuilder 1.8.2 */ :root{--dt-row-selected: 13, 110, 253;--dt-row-selected-text: 255, 255, 255;--dt-row-selected-link: 9, 10, 11;--dt-row-stripe: 0, 0, 0;--dt-row-hover: 0, 0, 0;--dt-column-ordering: 0, 0, 0;--dt-html-background: white}:root.dark{--dt-html-background: rgb(33, 37, 41)}table.dataTable td.dt-control{text-align:center;cursor:pointer}table.dataTable td.dt-control:before{display:inline-block;box-sizing:border-box;content:"";border-top:5px solid transparent;border-left:10px solid rgba(0, 0, 0, 0.5);border-bottom:5px solid transparent;border-right:0px solid transparent}table.dataTable tr.dt-hasChild td.dt-control:before{border-top:10px solid rgba(0, 0, 0, 0.5);border-left:5px solid transparent;border-bottom:0px solid transparent;border-right:5px solid transparent}table.dataTable tfoot:empty{display:none}html.dark table.dataTable td.dt-control:before,:root[data-bs-theme=dark] table.dataTable td.dt-control:before,:root[data-theme=dark] table.dataTable td.dt-control:before{border-left-color:rgba(255, 255, 255, 0.5)}html.dark table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-bs-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before,:root[data-theme=dark] table.dataTable tr.dt-hasChild td.dt-control:before{border-top-color:rgba(255, 255, 255, 0.5);border-left-color:transparent}div.dt-scroll{width:100%}div.dt-scroll-body thead tr,div.dt-scroll-body tfoot tr{height:0}div.dt-scroll-body thead tr th,div.dt-scroll-body thead tr td,div.dt-scroll-body tfoot tr th,div.dt-scroll-body tfoot tr td{height:0 !important;padding-top:0px !important;padding-bottom:0px !important;border-top-width:0px !important;border-bottom-width:0px !important}div.dt-scroll-body thead tr th div.dt-scroll-sizing,div.dt-scroll-body thead tr td div.dt-scroll-sizing,div.dt-scroll-body tfoot tr th div.dt-scroll-sizing,div.dt-scroll-body tfoot tr td div.dt-scroll-sizing{height:0 !important;overflow:hidden !important}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before{position:absolute;display:block;bottom:50%;content:"▲";content:"▲"/""}table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{position:absolute;display:block;top:50%;content:"▼";content:"▼"/""}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>th.dt-ordering-asc,table.dataTable thead>tr>th.dt-ordering-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc,table.dataTable thead>tr>td.dt-ordering-asc,table.dataTable thead>tr>td.dt-ordering-desc{position:relative;padding-right:30px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order{position:absolute;right:12px;top:0;bottom:0;width:12px}table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-orderable-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{left:0;opacity:.125;line-height:9px;font-size:.8em}table.dataTable thead>tr>th.dt-orderable-asc,table.dataTable thead>tr>th.dt-orderable-desc,table.dataTable thead>tr>td.dt-orderable-asc,table.dataTable thead>tr>td.dt-orderable-desc{cursor:pointer}table.dataTable thead>tr>th.dt-orderable-asc:hover,table.dataTable thead>tr>th.dt-orderable-desc:hover,table.dataTable thead>tr>td.dt-orderable-asc:hover,table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(0, 0, 0, 0.05);outline-offset:-2px}table.dataTable thead>tr>th.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>th.dt-ordering-desc span.dt-column-order:after,table.dataTable thead>tr>td.dt-ordering-asc span.dt-column-order:before,table.dataTable thead>tr>td.dt-ordering-desc span.dt-column-order:after{opacity:.6}table.dataTable thead>tr>th.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>th.sorting_asc_disabled span.dt-column-order:before,table.dataTable thead>tr>td.sorting_desc_disabled span.dt-column-order:after,table.dataTable thead>tr>td.sorting_asc_disabled span.dt-column-order:before{display:none}table.dataTable thead>tr>th:active,table.dataTable thead>tr>td:active{outline:none}div.dt-scroll-body>table.dataTable>thead>tr>th,div.dt-scroll-body>table.dataTable>thead>tr>td{overflow:hidden}:root.dark table.dataTable thead>tr>th.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>th.dt-orderable-desc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-asc:hover,:root.dark table.dataTable thead>tr>td.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>th.dt-orderable-desc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-asc:hover,:root[data-bs-theme=dark] table.dataTable thead>tr>td.dt-orderable-desc:hover{outline:2px solid rgba(255, 255, 255, 0.05)}div.dt-processing{position:absolute;top:50%;left:50%;width:200px;margin-left:-100px;margin-top:-22px;text-align:center;padding:2px;z-index:10}div.dt-processing>div:last-child{position:relative;width:80px;height:15px;margin:1em auto}div.dt-processing>div:last-child>div{position:absolute;top:0;width:13px;height:13px;border-radius:50%;background:rgb(13, 110, 253);background:rgb(var(--dt-row-selected));animation-timing-function:cubic-bezier(0, 1, 1, 0)}div.dt-processing>div:last-child>div:nth-child(1){left:8px;animation:datatables-loader-1 .6s infinite}div.dt-processing>div:last-child>div:nth-child(2){left:8px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(3){left:32px;animation:datatables-loader-2 .6s infinite}div.dt-processing>div:last-child>div:nth-child(4){left:56px;animation:datatables-loader-3 .6s infinite}@keyframes datatables-loader-1{0%{transform:scale(0)}100%{transform:scale(1)}}@keyframes datatables-loader-3{0%{transform:scale(1)}100%{transform:scale(0)}}@keyframes datatables-loader-2{0%{transform:translate(0, 0)}100%{transform:translate(24px, 0)}}table.dataTable.nowrap th,table.dataTable.nowrap td{white-space:nowrap}table.dataTable th,table.dataTable td{box-sizing:border-box}table.dataTable th.dt-type-numeric,table.dataTable th.dt-type-date,table.dataTable td.dt-type-numeric,table.dataTable td.dt-type-date{text-align:right}table.dataTable th.dt-left,table.dataTable td.dt-left{text-align:left}table.dataTable th.dt-center,table.dataTable td.dt-center{text-align:center}table.dataTable th.dt-right,table.dataTable td.dt-right{text-align:right}table.dataTable th.dt-justify,table.dataTable td.dt-justify{text-align:justify}table.dataTable th.dt-nowrap,table.dataTable td.dt-nowrap{white-space:nowrap}table.dataTable th.dt-empty,table.dataTable td.dt-empty{text-align:center;vertical-align:top}table.dataTable thead th,table.dataTable thead td,table.dataTable tfoot th,table.dataTable tfoot td{text-align:left}table.dataTable thead th.dt-head-left,table.dataTable thead td.dt-head-left,table.dataTable tfoot th.dt-head-left,table.dataTable tfoot td.dt-head-left{text-align:left}table.dataTable thead th.dt-head-center,table.dataTable thead td.dt-head-center,table.dataTable tfoot th.dt-head-center,table.dataTable tfoot td.dt-head-center{text-align:center}table.dataTable thead th.dt-head-right,table.dataTable thead td.dt-head-right,table.dataTable tfoot th.dt-head-right,table.dataTable tfoot td.dt-head-right{text-align:right}table.dataTable thead th.dt-head-justify,table.dataTable thead td.dt-head-justify,table.dataTable tfoot th.dt-head-justify,table.dataTable tfoot td.dt-head-justify{text-align:justify}table.dataTable thead th.dt-head-nowrap,table.dataTable thead td.dt-head-nowrap,table.dataTable tfoot th.dt-head-nowrap,table.dataTable tfoot td.dt-head-nowrap{white-space:nowrap}table.dataTable tbody th.dt-body-left,table.dataTable tbody td.dt-body-left{text-align:left}table.dataTable tbody th.dt-body-center,table.dataTable tbody td.dt-body-center{text-align:center}table.dataTable tbody th.dt-body-right,table.dataTable tbody td.dt-body-right{text-align:right}table.dataTable tbody th.dt-body-justify,table.dataTable tbody td.dt-body-justify{text-align:justify}table.dataTable tbody th.dt-body-nowrap,table.dataTable tbody td.dt-body-nowrap{white-space:nowrap}/*! Bootstrap 5 integration for DataTables @@ -17,3 +17,12 @@ */table.table.dataTable{clear:both;margin-bottom:0;max-width:none;border-spacing:0}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:none}table.table.dataTable>:not(caption)>*>*{background-color:var(--bs-table-bg)}table.table.dataTable>tbody>tr{background-color:transparent}table.table.dataTable>tbody>tr.selected>*{box-shadow:inset 0 0 0 9999px rgb(13, 110, 253);box-shadow:inset 0 0 0 9999px rgb(var(--dt-row-selected));color:rgb(255, 255, 255);color:rgb(var(--dt-row-selected-text))}table.table.dataTable>tbody>tr.selected a{color:rgb(9, 10, 11);color:rgb(var(--dt-row-selected-link))}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1)>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-stripe), 0.05)}table.table.dataTable.table-striped>tbody>tr:nth-of-type(2n+1).selected>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.95);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.95)}table.table.dataTable.table-hover>tbody>tr:hover>*{box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-hover), 0.075)}table.table.dataTable.table-hover>tbody>tr.selected:hover>*{box-shadow:inset 0 0 0 9999px rgba(13, 110, 253, 0.975);box-shadow:inset 0 0 0 9999px rgba(var(--dt-row-selected), 0.975)}div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:1em}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:1em}div.dt-container div.dt-layout-full{width:100%}div.dt-container div.dt-layout-full>*:only-child{margin-left:auto;margin-right:auto}div.dt-container div.dt-layout-table>div{display:block !important}@media screen and (max-width: 767px){div.dt-container div.dt-layout-start>*:not(:last-child){margin-right:0}div.dt-container div.dt-layout-end>*:not(:first-child){margin-left:0}}div.dt-container div.dt-length label{font-weight:normal;text-align:left;white-space:nowrap}div.dt-container div.dt-length select{width:auto;display:inline-block;margin-right:.5em}div.dt-container div.dt-search{text-align:right}div.dt-container div.dt-search label{font-weight:normal;white-space:nowrap;text-align:left}div.dt-container div.dt-search input{margin-left:.5em;display:inline-block;width:auto}div.dt-container div.dt-paging{margin:0}div.dt-container div.dt-paging ul.pagination{margin:2px 0;flex-wrap:wrap}div.dt-container div.dt-row{position:relative}div.dt-scroll-head table.dataTable{margin-bottom:0 !important}div.dt-scroll-body{border-bottom-color:var(--bs-border-color);border-bottom-width:var(--bs-border-width);border-bottom-style:solid}div.dt-scroll-body>table{border-top:none;margin-top:0 !important;margin-bottom:0 !important}div.dt-scroll-body>table>tbody>tr:first-child{border-top-width:0}div.dt-scroll-body>table>thead>tr{border-width:0 !important}div.dt-scroll-body>table>tbody>tr:last-child>*{border-bottom:none}div.dt-scroll-foot>.dt-scroll-footInner{box-sizing:content-box}div.dt-scroll-foot>.dt-scroll-footInner>table{margin-top:0 !important;border-top:none}div.dt-scroll-foot>.dt-scroll-footInner>table>tfoot>tr:first-child{border-top-width:0 !important}@media screen and (max-width: 767px){div.dt-container div.dt-length,div.dt-container div.dt-search,div.dt-container div.dt-info,div.dt-container div.dt-paging{text-align:center}div.dt-container .row{--bs-gutter-y: 0.5rem}div.dt-container div.dt-paging ul.pagination{justify-content:center !important}}table.dataTable.table-sm>thead>tr th.dt-orderable-asc,table.dataTable.table-sm>thead>tr th.dt-orderable-desc,table.dataTable.table-sm>thead>tr th.dt-ordering-asc,table.dataTable.table-sm>thead>tr th.dt-ordering-desc,table.dataTable.table-sm>thead>tr td.dt-orderable-asc,table.dataTable.table-sm>thead>tr td.dt-orderable-desc,table.dataTable.table-sm>thead>tr td.dt-ordering-asc,table.dataTable.table-sm>thead>tr td.dt-ordering-desc{padding-right:20px}table.dataTable.table-sm>thead>tr th.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr th.dt-ordering-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-orderable-desc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-asc span.dt-column-order,table.dataTable.table-sm>thead>tr td.dt-ordering-desc span.dt-column-order{right:5px}div.dt-scroll-head table.table-bordered{border-bottom-width:0}div.table-responsive>div.dt-container>div.row{margin:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:first-child{padding-left:0}div.table-responsive>div.dt-container>div.row>div[class^=col-]:last-child{padding-right:0}:root[data-bs-theme=dark]{--dt-row-hover: 255, 255, 255;--dt-row-stripe: 255, 255, 255;--dt-column-ordering: 255, 255, 255} +@keyframes dtb-spinner{100%{transform:rotate(360deg)}}@-o-keyframes dtb-spinner{100%{-o-transform:rotate(360deg);transform:rotate(360deg)}}@-ms-keyframes dtb-spinner{100%{-ms-transform:rotate(360deg);transform:rotate(360deg)}}@-webkit-keyframes dtb-spinner{100%{-webkit-transform:rotate(360deg);transform:rotate(360deg)}}@-moz-keyframes dtb-spinner{100%{-moz-transform:rotate(360deg);transform:rotate(360deg)}}div.dataTables_wrapper{position:relative}div.dt-buttons{position:initial}div.dt-buttons .dt-button{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}div.dt-button-info{position:fixed;top:50%;left:50%;width:400px;margin-top:-100px;margin-left:-200px;background-color:white;border-radius:.75em;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.8);text-align:center;z-index:2003;overflow:hidden}div.dt-button-info h2{padding:2rem 2rem 1rem 2rem;margin:0;font-weight:normal}div.dt-button-info>div{padding:1em 2em 2em 2em}div.dtb-popover-close{position:absolute;top:6px;right:6px;width:22px;height:22px;text-align:center;border-radius:3px;cursor:pointer;z-index:2003}button.dtb-hide-drop{display:none !important}div.dt-button-collection-title{text-align:center;padding:.3em .5em .5em;margin-left:.5em;margin-right:.5em;font-size:.9em;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}div.dt-button-collection-title:empty{display:none}span.dt-button-spacer{display:inline-block;margin:.5em;white-space:nowrap}span.dt-button-spacer.bar{border-left:1px solid rgba(0, 0, 0, 0.3);vertical-align:middle;padding-left:.5em}span.dt-button-spacer.bar:empty{height:1em;width:1px;padding-left:0}div.dt-button-collection .dt-button-active{padding-right:3em}div.dt-button-collection .dt-button-active:after{position:absolute;top:50%;margin-top:-10px;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection .dt-button-active.dt-button-split{padding-right:0}div.dt-button-collection .dt-button-active.dt-button-split:after{display:none}div.dt-button-collection .dt-button-active.dt-button-split>*:first-child{padding-right:3em}div.dt-button-collection .dt-button-active.dt-button-split>*:first-child:after{position:absolute;top:50%;margin-top:-10px;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection .dt-button-active-a a{padding-right:3em}div.dt-button-collection .dt-button-active-a a:after{position:absolute;right:1em;display:inline-block;content:"✓";color:inherit}div.dt-button-collection span.dt-button-spacer{width:100%;font-size:.9em;text-align:center;margin:.5em 0}div.dt-button-collection span.dt-button-spacer:empty{height:0;width:100%}div.dt-button-collection span.dt-button-spacer.bar{border-left:none;border-bottom:1px solid rgba(0, 0, 0, 0.1);padding-left:0}@media print{table.dataTable tr>*{box-shadow:none !important}}div.dt-buttons div.btn-group{position:initial}div.dt-buttons span.dt-button-spacer.empty{margin:1px}div.dt-buttons span.dt-button-spacer.bar:empty{height:inherit}div.dt-buttons .btn.processing{color:rgba(0, 0, 0, 0.2)}div.dt-buttons .btn.processing:after{position:absolute;top:50%;left:50%;width:16px;height:16px;margin:-8px 0 0 -8px;box-sizing:border-box;display:block;content:" ";border:2px solid rgb(40, 40, 40);border-radius:50%;border-left-color:transparent;border-right-color:transparent;animation:dtb-spinner 1500ms infinite linear;-o-animation:dtb-spinner 1500ms infinite linear;-ms-animation:dtb-spinner 1500ms infinite linear;-webkit-animation:dtb-spinner 1500ms infinite linear;-moz-animation:dtb-spinner 1500ms infinite linear}div.dropdown-menu.dt-button-collection{margin-top:4px;width:200px}div.dropdown-menu.dt-button-collection .dt-button{position:relative}div.dropdown-menu.dt-button-collection .dt-button.dropdown-toggle::after{position:absolute;right:12px;top:14px}div.dropdown-menu.dt-button-collection div.dt-button-split{display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:stretch}div.dropdown-menu.dt-button-collection div.dt-button-split a:first-child{min-width:auto;flex:1 0 50px;padding-right:0}div.dropdown-menu.dt-button-collection div.dt-button-split button:last-child{min-width:33px;flex:0;background:transparent;border:none;line-height:1rem;color:var(--bs-dropdown-link-color);padding:var(--bs-dropdown-item-padding-y) var(--bs-dropdown-item-padding-x);overflow:visible}div.dropdown-menu.dt-button-collection div.dt-button-split button:last-child:hover{color:var(--bs-dropdown-link-hover-color);background-color:var(--bs-dropdown-link-hover-bg)}div.dropdown-menu.dt-button-collection.fixed{position:fixed;display:block;top:50%;left:50%;margin-left:-75px;border-radius:5px;background-color:white;padding:.5em}div.dropdown-menu.dt-button-collection.fixed.two-column{margin-left:-200px}div.dropdown-menu.dt-button-collection.fixed.three-column{margin-left:-225px}div.dropdown-menu.dt-button-collection.fixed.four-column{margin-left:-300px}div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-409px}@media screen and (max-width: 1024px){div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-308px}}@media screen and (max-width: 640px){div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-203px}}@media screen and (max-width: 460px){div.dropdown-menu.dt-button-collection.fixed.columns{margin-left:-100px}}div.dropdown-menu.dt-button-collection.fixed>:last-child{max-height:100vh;overflow:auto}div.dropdown-menu.dt-button-collection.two-column>:last-child,div.dropdown-menu.dt-button-collection.three-column>:last-child,div.dropdown-menu.dt-button-collection.four-column>:last-child{display:block !important;column-gap:8px}div.dropdown-menu.dt-button-collection.two-column>:last-child>*,div.dropdown-menu.dt-button-collection.three-column>:last-child>*,div.dropdown-menu.dt-button-collection.four-column>:last-child>*{-webkit-column-break-inside:avoid;break-inside:avoid}div.dropdown-menu.dt-button-collection.two-column{width:400px}div.dropdown-menu.dt-button-collection.two-column>:last-child{padding-bottom:1px;column-count:2}div.dropdown-menu.dt-button-collection.three-column{width:450px}div.dropdown-menu.dt-button-collection.three-column>:last-child{padding-bottom:1px;column-count:3}div.dropdown-menu.dt-button-collection.four-column{width:600px}div.dropdown-menu.dt-button-collection.four-column>:last-child{padding-bottom:1px;column-count:4}div.dropdown-menu.dt-button-collection .dt-button{border-radius:0}div.dropdown-menu.dt-button-collection.columns{width:auto}div.dropdown-menu.dt-button-collection.columns>:last-child{display:flex;flex-wrap:wrap;justify-content:flex-start;align-items:center;gap:6px;width:818px;padding-bottom:1px}div.dropdown-menu.dt-button-collection.columns>:last-child .dt-button{min-width:200px;flex:0 1;margin:0}div.dropdown-menu.dt-button-collection.columns.dtb-b3>:last-child,div.dropdown-menu.dt-button-collection.columns.dtb-b2>:last-child,div.dropdown-menu.dt-button-collection.columns.dtb-b1>:last-child{justify-content:space-between}div.dropdown-menu.dt-button-collection.columns.dtb-b3 .dt-button{flex:1 1 32%}div.dropdown-menu.dt-button-collection.columns.dtb-b2 .dt-button{flex:1 1 48%}div.dropdown-menu.dt-button-collection.columns.dtb-b1 .dt-button{flex:1 1 100%}@media screen and (max-width: 1024px){div.dropdown-menu.dt-button-collection.columns>:last-child{width:612px}}@media screen and (max-width: 640px){div.dropdown-menu.dt-button-collection.columns>:last-child{width:406px}div.dropdown-menu.dt-button-collection.columns.dtb-b3 .dt-button{flex:0 1 32%}}@media screen and (max-width: 460px){div.dropdown-menu.dt-button-collection.columns>:last-child{width:200px}}div.dt-button-background{position:fixed;top:0;left:0;width:100%;height:100%;z-index:999}@media screen and (max-width: 767px){div.dt-buttons{float:none;width:100%;text-align:center;margin-bottom:.5em}div.dt-buttons a.btn{float:none}}div.dt-button-info{background-color:var(--bs-body-bg);border:1px solid var(--bs-border-color-translucent)}:root[data-bs-theme=dark] div.dropdown-menu.dt-button-collection.fixed{background-color:var(--bs-body-bg);border:1px solid var(--bs-border-color-translucent)} + + +div.dt-datetime{position:absolute;background-color:white;z-index:2050;border:1px solid #ccc;box-shadow:0 5px 15px -5px rgba(0, 0, 0, 0.5);padding:6px 20px;width:275px;border-radius:5px}div.dt-datetime.inline{position:relative;box-shadow:none}div.dt-datetime div.dt-datetime-title{text-align:center;padding:5px 0px 3px}div.dt-datetime div.dt-datetime-buttons{text-align:center}div.dt-datetime div.dt-datetime-buttons a{display:inline-block;padding:0 .5em .5em .5em;margin:0;font-size:.9em}div.dt-datetime div.dt-datetime-buttons a:hover{text-decoration:underline}div.dt-datetime table{border-spacing:0;margin:12px 0;width:100%}div.dt-datetime table.dt-datetime-table-nospace{margin-top:-12px}div.dt-datetime table th{font-size:.8em;color:#777;font-weight:normal;width:14.285714286%;padding:0 0 4px 0;text-align:center}div.dt-datetime table td{font-size:.9em;color:#444;padding:0}div.dt-datetime table td.selectable{text-align:center;background:#f5f5f5}div.dt-datetime table td.selectable.disabled{color:#aaa;background:white}div.dt-datetime table td.selectable.disabled button:hover{color:#aaa;background:white}div.dt-datetime table td.selectable.now{background-color:#ddd}div.dt-datetime table td.selectable.now button{font-weight:bold}div.dt-datetime table td.selectable.selected button{background:#4e6ca3;color:white;border-radius:2px}div.dt-datetime table td.selectable button:hover{background:#ff8000;color:white;border-radius:2px}div.dt-datetime table td.dt-datetime-week{font-size:.7em}div.dt-datetime table button{width:100%;box-sizing:border-box;border:none;background:transparent;font-size:inherit;color:inherit;text-align:center;padding:4px 0;cursor:pointer;margin:0}div.dt-datetime table button span{display:inline-block;min-width:14px;text-align:right}div.dt-datetime table.weekNumber th{width:12.5%}div.dt-datetime div.dt-datetime-calendar table{margin-top:0}div.dt-datetime div.dt-datetime-label{position:relative;display:inline-block;height:30px;padding:5px 6px;border:1px solid transparent;box-sizing:border-box;cursor:pointer}div.dt-datetime div.dt-datetime-label:hover{border:1px solid #ddd;border-radius:2px;background-color:#f5f5f5}div.dt-datetime div.dt-datetime-label select{position:absolute;top:6px;left:0;cursor:pointer;opacity:0}div.dt-datetime.horizontal{width:550px}div.dt-datetime.horizontal div.dt-datetime-date,div.dt-datetime.horizontal div.dt-datetime-time{width:48%}div.dt-datetime.horizontal div.dt-datetime-time{margin-left:4%}div.dt-datetime div.dt-datetime-date{position:relative;float:left;width:100%}div.dt-datetime div.dt-datetime-time{position:relative;float:left;width:100%;text-align:center}div.dt-datetime div.dt-datetime-time>span{vertical-align:middle}div.dt-datetime div.dt-datetime-time th{text-align:left}div.dt-datetime div.dt-datetime-time div.dt-datetime-timeblock{display:inline-block;vertical-align:middle}div.dt-datetime div.dt-datetime-iconLeft,div.dt-datetime div.dt-datetime-iconRight{width:30px;height:30px;background-position:center;background-repeat:no-repeat;opacity:.3;overflow:hidden;box-sizing:border-box;border:1px solid transparent}div.dt-datetime div.dt-datetime-iconLeft:hover,div.dt-datetime div.dt-datetime-iconRight:hover{border:1px solid #ccc;border-radius:2px;background-color:#f0f0f0;opacity:.6}div.dt-datetime div.dt-datetime-iconLeft button,div.dt-datetime div.dt-datetime-iconRight button{border:none;background:transparent;text-indent:30px;height:100%;width:100%;cursor:pointer}div.dt-datetime div.dt-datetime-iconLeft{position:absolute;top:5px;left:5px}div.dt-datetime div.dt-datetime-iconLeft button{position:relative;z-index:1}div.dt-datetime div.dt-datetime-iconLeft:after{position:absolute;top:7px;left:10px;display:block;content:"";border-top:7px solid transparent;border-right:7px solid black;border-bottom:7px solid transparent}div.dt-datetime div.dt-datetime-iconRight{position:absolute;top:5px;right:5px}div.dt-datetime div.dt-datetime-iconRight button{position:relative;z-index:1}div.dt-datetime div.dt-datetime-iconRight:after{position:absolute;top:7px;left:12px;display:block;content:"";border-top:7px solid transparent;border-left:7px solid black;border-bottom:7px solid transparent}div.dt-datetime-error{clear:both;padding:0 1em;max-width:240px;font-size:11px;line-height:1.25em;text-align:center;color:#b11f1f}html.dark input.dt-datetime,:root[data-theme=dark] input.dt-datetime,:root[data-bs-theme=dark] input.dt-datetime{color-scheme:dark}html.dark div.dt-datetime,:root[data-theme=dark] div.dt-datetime,:root[data-bs-theme=dark] div.dt-datetime{border:1px solid #595b5e;background-color:#212529;box-shadow:3px 4px 10px 1px rgba(0, 0, 0, 0.8)}html.dark div.dt-datetime table th,:root[data-theme=dark] div.dt-datetime table th,:root[data-bs-theme=dark] div.dt-datetime table th{color:#ccc}html.dark div.dt-datetime table td,:root[data-theme=dark] div.dt-datetime table td,:root[data-bs-theme=dark] div.dt-datetime table td{color:#eee}html.dark div.dt-datetime table td.selectable,:root[data-theme=dark] div.dt-datetime table td.selectable,:root[data-bs-theme=dark] div.dt-datetime table td.selectable{background:#373c41}html.dark div.dt-datetime table td.selectable.disabled,:root[data-theme=dark] div.dt-datetime table td.selectable.disabled,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.disabled{color:#aaa;background:#171b1f}html.dark div.dt-datetime table td.selectable.disabled button:hover,:root[data-theme=dark] div.dt-datetime table td.selectable.disabled button:hover,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.disabled button:hover{color:#aaa;background:#171b1f}html.dark div.dt-datetime table td.selectable.now,:root[data-theme=dark] div.dt-datetime table td.selectable.now,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.now{background:#4b5055}html.dark div.dt-datetime table td.selectable.selected button,:root[data-theme=dark] div.dt-datetime table td.selectable.selected button,:root[data-bs-theme=dark] div.dt-datetime table td.selectable.selected button{background:#6ea8fe;color:black}html.dark div.dt-datetime table td.selectable button:hover,:root[data-theme=dark] div.dt-datetime table td.selectable button:hover,:root[data-bs-theme=dark] div.dt-datetime table td.selectable button:hover{background:#ff8000;color:black}html.dark div.dt-datetime div.dt-datetime-label:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-label:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-label:hover{border:1px solid transparent;background-color:rgba(255, 255, 255, 0.1)}html.dark div.dt-datetime div.dt-datetime-iconLeft:hover,html.dark div.dt-datetime div.dt-datetime-iconRight:hover,html.dark div.dt-datetime div.dt-datetime-iconUp:hover,html.dark div.dt-datetime div.dt-datetime-iconDown:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconRight:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconUp:hover,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconDown:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconRight:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconUp:hover,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconDown:hover{border:1px solid transparent;background-color:rgba(255, 255, 255, 0.1)}html.dark div.dt-datetime div.dt-datetime-iconLeft:after,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:after,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconLeft:after{border-right-color:white}html.dark div.dt-datetime div.dt-datetime-iconRight:after,:root[data-theme=dark] div.dt-datetime div.dt-datetime-iconRight:after,:root[data-bs-theme=dark] div.dt-datetime div.dt-datetime-iconRight:after{border-left-color:white}html.dark div.dt-datetime select,:root[data-theme=dark] div.dt-datetime select,:root[data-bs-theme=dark] div.dt-datetime select{color-scheme:dark}html.dark div.dt-datetime-error,:root[data-theme=dark] div.dt-datetime-error,:root[data-bs-theme=dark] div.dt-datetime-error{color:#b11f1f} + + +div.dt-button-collection{overflow:visible !important;z-index:2002 !important}div.dt-button-collection div.dtsb-searchBuilder{box-sizing:border-box;padding-left:1em !important;padding-right:1em !important}div.dt-button-collection.dtb-collection-closeable div.dtsb-titleRow{padding-right:40px}.dtsb-greyscale{border:1px solid #cecece !important}div.dtsb-logicContainer .dtsb-greyscale{border:none !important}div.dtsb-searchBuilder{justify-content:space-evenly;cursor:default;margin-bottom:1em;text-align:left;width:100%}div.dtsb-searchBuilder button.dtsb-button,div.dtsb-searchBuilder select{font-size:1em}div.dtsb-searchBuilder div.dtsb-titleRow{justify-content:space-evenly;margin-bottom:.5em}div.dtsb-searchBuilder div.dtsb-titleRow div.dtsb-title{display:inline-block;padding-top:14px}div.dtsb-searchBuilder div.dtsb-titleRow div.dtsb-title:empty{display:inline}div.dtsb-searchBuilder div.dtsb-titleRow button.dtsb-clearAll{float:right;margin-bottom:.8em}div.dtsb-searchBuilder div.dtsb-vertical .dtsb-value,div.dtsb-searchBuilder div.dtsb-vertical .dtsb-data,div.dtsb-searchBuilder div.dtsb-vertical .dtsb-condition{display:block}div.dtsb-searchBuilder div.dtsb-group{position:relative;clear:both;margin-bottom:.8em}div.dtsb-searchBuilder div.dtsb-group button.dtsb-search{float:right}div.dtsb-searchBuilder div.dtsb-group button.dtsb-clearGroup{margin:2px;text-align:center;padding:0}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer{-webkit-transform:rotate(90deg);-moz-transform:rotate(90deg);-o-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg);position:absolute;margin-top:.8em;margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria{margin-bottom:.8em;display:flex;justify-content:start;flex-flow:row wrap}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-dropDown,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-input{padding:.4em;margin-right:.8em;min-width:5em;max-width:20em;color:inherit;font-size:1em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-dropDown option.dtsb-notItalic,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-input option.dtsb-notItalic{font-style:normal}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-italic{font-style:italic}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont{flex:1;white-space:nowrap}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont span.dtsb-joiner{margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont input.dtsb-value{width:33%}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont select,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont input{height:100%;box-sizing:border-box}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer{margin-left:auto;display:inline-block}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-delete,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-right,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-left{margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-delete:last-child,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-right:last-child,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button.dtsb-left:last-child{margin-right:0}@media screen and (max-width: 550px){div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria{display:flex;flex-flow:none;flex-direction:column;justify-content:start;padding-right:calc(35px + .8em);margin-bottom:0px}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:not(:first-child),div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:not(:nth-child(2)),div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:not(:last-child){padding-top:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:first-child,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:nth-child(2),div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria:last-child{padding-top:0em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-dropDown,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-input{max-width:none;width:100%;margin-bottom:.8em;margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-inputCont{margin-right:.8em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer{position:absolute;width:35px;display:flex;flex-wrap:wrap-reverse;right:0}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria div.dtsb-buttonContainer button{margin-right:0px !important}}div.dtsb-searchBuilder div.dtsb-titleRow{height:40px}div.dtsb-searchBuilder div.dtsb-titleRow div.dtsb-title{padding-top:10px}div.dtsb-searchBuilder div.dtsb-group button.dtsb-clearGroup{margin-right:8px}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria .form-select{width:auto;display:inline-block;padding-right:30px !important}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-condition{border-color:#28a745}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-data{border-color:#dc3545}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria select.dtsb-value,div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria input.dtsb-value{border-color:#007bff}div.dtsb-searchBuilder div.dtsb-group div.dtsb-criteria .form-control{display:inline-block;font-size:1em}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer{border-radius:4px;display:flex;flex-direction:row;flex-wrap:wrap;justify-content:flex-start;align-content:flex-start;align-items:flex-start;margin-top:10px;overflow:hidden}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer button.dtsb-logic{border:none;border-radius:0px;flex-grow:1;flex-shrink:0;flex-basis:3em;margin:0px;padding:.375rem .7rem}div.dtsb-searchBuilder div.dtsb-group div.dtsb-logicContainer button.dtsb-clearGroup{border:none;border-radius:0px;width:2em;margin:0px}div.dt-button-collection div.dtsb-searchBuilder{padding-left:10px;padding-right:10px} + + diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css index c4e5c961a6..47beba33cc 100644 --- a/lizmap/www/assets/css/map.css +++ b/lizmap/www/assets/css/map.css @@ -1304,7 +1304,7 @@ SubDock } .attribute-layer-action-bar{ - height:30px; + height:33px; border-bottom: 1px solid lightgrey; } @@ -1313,6 +1313,14 @@ SubDock overflow: auto; } +.attribute-layer-action-bar .dt-buttons{ + margin-right: 4px; +} + +.attribute-layer-action-bar .dt-buttons > .btn { + padding: 2px 6px; +} + .btn-filterbyextent-attributeTable svg { height: 14px; width: 14px; diff --git a/package-lock.json b/package-lock.json index 3d8373fd8e..9da16e847a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,9 @@ "cypress": "<10.0.0", "cypress-file-upload": "^5.0.x", "datatables.net-bs5": "^2.2.2", + "datatables.net-buttons-bs5": "^3.2.2", + "datatables.net-datetime": "^1.5.5", + "datatables.net-searchbuilder-bs5": "^1.8.2", "dompurify": "^3.2.x", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "^4.3.x", @@ -3910,6 +3913,59 @@ "jquery": ">=1.7" } }, + "node_modules/datatables.net-buttons": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/datatables.net-buttons/-/datatables.net-buttons-3.2.5.tgz", + "integrity": "sha512-OSTl7evbfe0SMee11lyzu5iv/z8Yp05eh3s1QBte/FNqHcoXN8hlAVSSGpYgk5pj8zwHPYIu6fHeMEue4ARUNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net": "^2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-buttons-bs5": { + "version": "3.2.5", + "resolved": "https://registry.npmjs.org/datatables.net-buttons-bs5/-/datatables.net-buttons-bs5-3.2.5.tgz", + "integrity": "sha512-3eT/Sd90x7imq9MRcKP9X3j70qg/u+OvtZSNWJEihRf1Mb/Sr8NexQw/Bag/ui6GJHa5dhUeFrOgBSKtEW70iA==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net-bs5": "^2", + "datatables.net-buttons": "3.2.5", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-datetime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/datatables.net-datetime/-/datatables.net-datetime-1.6.0.tgz", + "integrity": "sha512-iOCaUv8MEuRMizBoCQlIWmI8xlNW5rJeohpxdE4RBd6oYz5Z01m4bca4zd26+EY4/4ONnVPPAPht40LKgNSkgA==", + "dev": true, + "license": "MIT" + }, + "node_modules/datatables.net-searchbuilder": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder/-/datatables.net-searchbuilder-1.8.4.tgz", + "integrity": "sha512-rK+5sZU5wwphVRGqFB0ahJ75+yL5jBznnLrRKnGZ03fBDlhQo9TM9k8FKU1vK6NyWHj2fPes+mS/0s7LkP8cMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net": "^2", + "jquery": ">=1.7" + } + }, + "node_modules/datatables.net-searchbuilder-bs5": { + "version": "1.8.4", + "resolved": "https://registry.npmjs.org/datatables.net-searchbuilder-bs5/-/datatables.net-searchbuilder-bs5-1.8.4.tgz", + "integrity": "sha512-9adKB0QZCl4rZ0TluwJxYWxqKCWBndL/ie7I2LGKTT4Qq02uYjkQi/xqueczGPf3HChohuSHARJO6wbtYcNxyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "datatables.net-bs5": "^2", + "datatables.net-searchbuilder": "1.8.4", + "jquery": ">=1.7" + } + }, "node_modules/dayjs": { "version": "1.11.18", "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.18.tgz", diff --git a/package.json b/package.json index 2f825f012c..0e7ee672cf 100644 --- a/package.json +++ b/package.json @@ -38,6 +38,9 @@ "cypress-file-upload": "^5.0.x", "dompurify": "^3.2.x", "datatables.net-bs5": "^2.2.2", + "datatables.net-buttons-bs5": "^3.2.2", + "datatables.net-datetime": "^1.5.5", + "datatables.net-searchbuilder-bs5": "^1.8.2", "eslint-plugin-jsdoc": "^60.2.x", "flatgeobuf": "^4.3.x", "jsts": "^2.11.x", From 7bef459b77a07cf2c264815d69bb5774d1745b11 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 15 Apr 2025 17:21:21 +0200 Subject: [PATCH 21/49] Add Datatables searchBuilder backend logic --- assets/src/legacy/attributeTable.js | 3 +- .../lizmap/controllers/datatables.classic.php | 160 ++++++++++++++---- 2 files changed, 130 insertions(+), 33 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 2b17003d00..6a202d23c6 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1481,7 +1481,8 @@ var lizAttributeTable = function() { extend: 'searchBuilder', config: { liveSearch: false, - columns: '.dt-orderable-asc' + columns: '.dt-orderable-asc', + depthLimit: 1 } } ] diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index af03707974..2881e34a96 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -52,7 +52,10 @@ public function index() $DTOrderColumnDirection = $DTOrder[0]['dir'] == 'desc' ? 'd' : ''; $DTOrderColumnName = $DTColumns[$DTOrderColumnIndex]['data']; - $DTSearch = $this->param('search'); + $DTSearchBuilder = ''; + if ($this->param('searchBuilder')) { + $DTSearchBuilder = $this->param('searchBuilder'); + } $lproj = lizmap::getProject($repository.'~'.$project); $layer = $lproj->getLayer($layerId); @@ -60,17 +63,20 @@ public function index() $jsonFeatures = array(); - // Get total number of features - $hits = 0; - $wfsParamsHits = array( + $wfsParamsData = array( 'SERVICE' => 'WFS', 'VERSION' => '1.0.0', 'REQUEST' => 'GetFeature', 'TYPENAME' => $typeName, + ); + + // Get total number of features + $hits = 0; + $wfsParamsHits = array( 'RESULTTYPE' => 'hits', ); - $wfsrequest = new WFSRequest($lproj, $wfsParamsHits, lizmap::getServices()); + $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $hitsData = $wfsresponse->getBodyAsString(); preg_match('/numberOfFeatures="([0-9]+)"/', $hitsData, $matches); @@ -80,18 +86,6 @@ public function index() $recordsFiltered = count($filteredFeatureIDs); } - // Get features - $wfsParamsData = array( - 'SERVICE' => 'WFS', - 'VERSION' => '1.0.0', - 'REQUEST' => 'GetFeature', - 'TYPENAME' => $typeName, - 'OUTPUTFORMAT' => 'GeoJSON', - 'MAXFEATURES' => $DTLength, - 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, - 'STARTINDEX' => $DTStart, - ); - if ($moveSelectedToTop == 'true') { $featureIds = array(); foreach ($selectedFeatureIDs as $id) { @@ -124,6 +118,108 @@ public function index() $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; } + // Handle search made by searchBuilder + if ($DTSearchBuilder) { + foreach ($DTSearchBuilder['criteria'] as $criteria) { + $column = $criteria['data']; + $condition = $criteria['condition']; + $value = ''; + $value1 = isset($criteria['value1']) ? addslashes($criteria['value1']) : ''; + $value2 = isset($criteria['value2']) ? addslashes($criteria['value2']) : ''; + + // Map DataTables operators to QGIS Server operators + switch ($condition) { + case '=': + case '!=': + case '<': + case '<=': + case '>': + case '>=': + $qgisOperator = $condition; + if ($criteria['type'] == 'num') { + $value = $value1; + } else { + $value = '\''.$value1.'\''; + } + + break; + + case 'starts': + $qgisOperator = 'ILIKE'; + $value = '\''.$value1.'%\''; + + break; + + case '!starts': + $qgisOperator = 'NOT ILIKE'; + $value = '\''.$value1.'%\''; + + break; + + case 'contains': + $qgisOperator = 'ILIKE'; + $value = '\'%'.$value1.'%\''; + + break; + + case '!contains': + $qgisOperator = 'NOT ILIKE'; + $value = '\'%'.$value1.'%\''; + + break; + + case 'ends': + $qgisOperator = 'ILIKE'; + $value = '\'%'.$value1.'\''; + + break; + + case '!ends': + $qgisOperator = 'NOT ILIKE'; + $value = '\'%'.$value1.'\''; + + break; + + case 'null': + $qgisOperator = 'IS NULL'; + + break; + + case '!null': + $qgisOperator = 'IS NOT NULL'; + + break; + + case 'between': + $qgisOperator = 'BETWEEN'; + if ($criteria['type'] == 'num') { + $value = $value1.' AND '.$value2; + } else { + $value = '\''.$value1.'\' AND \''.$value2.'\''; + } + + break; + + case '!between': + $qgisOperator = 'NOT BETWEEN'; + if ($criteria['type'] == 'num') { + $value = $value1.' AND '.$value2; + } else { + $value = '\''.$value1.'\' AND \''.$value2.'\''; + } + + break; + } + // Append the filter to the exp_filter string + if (!empty($expFilter)) { + $logic = isset($DTSearchBuilder['logic']) ? $DTSearchBuilder['logic'] : 'AND'; + $expFilter .= " {$logic} "; + } + + $expFilter .= "{$column} {$qgisOperator} {$value}"; + } + } + if ($expFilter) { $wfsParamsData['EXP_FILTER'] = $expFilter; } @@ -134,29 +230,29 @@ public function index() $bboxString = implode(',', $bbox); $wfsParamsData['BBOX'] = $bboxString; $wfsParamsData['SRSNAME'] = $srsName; + } - // Get total number of features in the bounding box - $wfsParamsFilterByExtentHits = array( - 'SERVICE' => 'WFS', - 'VERSION' => '1.0.0', - 'REQUEST' => 'GetFeature', - 'TYPENAME' => $typeName, - 'RESULTTYPE' => 'hits', - 'BBOX' => $bboxString, - 'SRSNAME' => $srsName, - ); + $wfsParamsPaginated = array( + 'OUTPUTFORMAT' => 'GeoJSON', + 'MAXFEATURES' => $DTLength, + 'STARTINDEX' => $DTStart, + 'SORTBY' => $DTOrderColumnName.' '.$DTOrderColumnDirection, + ); - $wfsrequest = new WFSRequest($lproj, $wfsParamsFilterByExtentHits, lizmap::getServices()); + $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsPaginated), lizmap::getServices()); + $wfsresponse = $wfsrequest->process(); + $featureData = $wfsresponse->getBodyAsString(); + + // Get hits when data is filtered + if ($expFilter || count($bbox) == 4) { + + $wfsrequest = new WFSRequest($lproj, array_merge($wfsParamsData, $wfsParamsHits), lizmap::getServices()); $wfsresponse = $wfsrequest->process(); $filterByExtentHitsData = $wfsresponse->getBodyAsString(); preg_match('/numberOfFeatures="([0-9]+)"/', $filterByExtentHitsData, $matches); $recordsFiltered = $matches[1]; } - $wfsrequest = new WFSRequest($lproj, $wfsParamsData, lizmap::getServices()); - $wfsresponse = $wfsrequest->process(); - $featureData = $wfsresponse->getBodyAsString(); - $returnedData = array( 'draw' => (int) $this->param('draw'), 'recordsTotal' => $hits, From 5fcf493b241b4ab412b6e2c95cf2532d2811d086 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Fri, 25 Apr 2025 17:05:20 +0200 Subject: [PATCH 22/49] Returns editableFeatures in datatables request --- assets/src/legacy/attributeTable.js | 85 ++++++++++--------- .../lizmap/classes/qgisVectorLayer.class.php | 19 ++++- .../lizmap/controllers/datatables.classic.php | 11 +++ 3 files changed, 73 insertions(+), 42 deletions(-) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 6a202d23c6..ae0d9d669d 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1415,12 +1415,24 @@ var lizAttributeTable = function() { return formatedData; } + // Get editable features + const editableFeatures = json.editableFeatures; + for (const feature of json.data.features) { - const featID = feature.id.split('.').pop(); + const featID = parseInt(feature.id.split('.').pop()); + let editionRestricted = ''; + if (editableFeatures.status === 'restricted') { + editionRestricted = 'edition-restricted="true"'; + if (editableFeatures.featuresids.includes(featID)) { + editionRestricted = 'edition-restricted="false"'; + } + } + const ftb = ``; + formatedData.push(Object.assign({ 'DT_RowId': featID, 'lizSelected': '', - 'featureToolbar': '', + 'featureToolbar': ftb, }, feature.properties)); // Copy received features to config @@ -1472,28 +1484,29 @@ var lizAttributeTable = function() { } }); - // Add searchBuilder button - // Disable live search to avoid searching on each keystroke - // Only display columns that are sortable for search - const searchBuilderButton = new DataTable.Buttons(oTable, { - buttons: [ - { - extend: 'searchBuilder', - config: { - liveSearch: false, - columns: '.dt-orderable-asc', - depthLimit: 1 - } - } - ] - }); - - // Attach searchBuilder button to attribute-layer-action-bar + // Attach searchBuilder button to attribute-layer-action-bar if exists const actionBar = document.querySelector(aTable) - .closest('.attribute-layer-content') - .previousElementSibling; - - actionBar.insertAdjacentElement('afterbegin', searchBuilderButton.container()[0]); + ?.closest('.attribute-layer-content') + ?.previousElementSibling; + + if (actionBar) { + // Add searchBuilder button + // Disable live search to avoid searching on each keystroke + // Only display columns that are sortable for search + const searchBuilderButton = new DataTable.Buttons(oTable, { + buttons: [ + { + extend: 'searchBuilder', + config: { + liveSearch: false, + columns: '.dt-orderable-asc', + depthLimit: 1 + } + } + ] + }); + actionBar.insertAdjacentElement('afterbegin', searchBuilderButton.container()[0]); + } // Unbind previous events on page oTable.on( 'page', function() { @@ -1519,11 +1532,6 @@ var lizAttributeTable = function() { table.draw(); } - // Check editable features - if (canEdit || canDelete) { - lizMap.mainLizmap.edition.fetchEditableFeatures([lConfig.id],[exp_f]); - } - if (aCallback) aCallback(aName,aTable); @@ -1557,13 +1565,13 @@ var lizAttributeTable = function() { width: "25px", searchable: false, sortable: false, - render: (data, type, row, meta) => { - const layerId = config.layers[aName].id; - const fid = row['DT_RowId']; - return ` - - `; - } + // render: (data, type, row, meta) => { + // const layerId = config.layers[aName].id; + // const fid = row['DT_RowId']; + // return ` + // + // `; + // } }); firstDisplayedColIndex += 1; @@ -1909,19 +1917,20 @@ var lizAttributeTable = function() { } // Bind events when drawing table - $( childTable ).one( 'draw.dt', function() { + const DTchildTable = new DataTable(childTable); + DTchildTable.one('draw', function() { if( canEdit ) { // Add property on lizmap-feature-toolbar to edit children feature linked to a parent feature const parentFeatId = $(childTable).parents('div.tab-pane.attribute-layer-child-content') .find('input.attribute-table-hidden-parent-feature-id').val(); - $(childTable).DataTable().cells().nodes() + DTchildTable.cells().nodes() .to$().children('lizmap-feature-toolbar').attr('parent-feature-id', parentFeatId); } if ( canCreateChildren ) { // Button to create feature linked to parent - const createHeader = $($(childTable).DataTable().column(1).header()); + const createHeader = $(DTchildTable.column(1).header()); if ( createHeader.find('button.attribute-layer-feature-create').length == 0 ) { createHeader .append(` '; // Unselect button - html+= ' '; + html+= ''; // 'Move selected to top' button - html+= ' '; + html+= ` + `; // Filter button : only if no filter applied at startup if( !startupFilter - && ( !lizMap.lizmapLayerFilterActive || lizMap.lizmapLayerFilterActive == lname ) - ){ - html+= ' '; + && ( !lizMap.lizmapLayerFilterActive || lizMap.lizmapLayerFilterActive == lname )){ + html+= ''; } // Filter data by extent button @@ -687,9 +692,7 @@ var lizAttributeTable = function() { document.querySelector(moveSelectedToTopSelector).addEventListener('click', (e) => { const dTableSelector = '#attribute-layer-table-' + e.currentTarget.value; const dTable = new DataTable(dTableSelector); - moveSelectedToTop = true; dTable.draw(); - moveSelectedToTop = false; // Scroll to top document.querySelector(dTableSelector).parentElement.scroll({ @@ -1379,9 +1382,8 @@ var lizAttributeTable = function() { type: 'POST', data: (d) => { // Handle selected features moved to top - if (moveSelectedToTop) { - d.moveselectedtotop = true; - d.selectedfeatureids = lConfig['selectedFeatures'].join(); + if (document.querySelector('.btn-moveselectedtotop-attributeTable.active[data-layerid="' + lConfig.id + '"]')) { + d.filteredfeatureids = lConfig['selectedFeatures'].join(); } // Handle filtered features @@ -3097,20 +3099,25 @@ var lizAttributeTable = function() { const selectedFeatures = config.layers[e.featureType].selectedFeatures; const table = new DataTable('table[data-layerid=' + layerId + ']'); - table.rows().every(function (rowIdx) { - var data = this.data(); - if ((selectedFeatures.includes(data.DT_RowId.toString()))) { - this.row(rowIdx).node().classList.add('selected'); - data.lizSelected = 'a'; - } else { - this.row(rowIdx).node().classList.remove('selected'); - data.lizSelected = 'z'; - } - }); + if (document.querySelector('.btn-moveselectedtotop-attributeTable.active[data-layerid="' + layerId + '"]')) { + table.draw(); + } else { + table.rows().every(function (rowIdx) { + var data = this.data(); + if ((selectedFeatures.includes(data.DT_RowId.toString()))) { + this.row(rowIdx).node().classList.add('selected'); + data.lizSelected = 'a'; + } else { + this.row(rowIdx).node().classList.remove('selected'); + data.lizSelected = 'z'; + } + }); + } // Update openlayers layer drawing - if( e.updateDrawing ) + if( e.updateDrawing ){ updateMapLayerSelection( e.featureType ); + } }, layerFilteredFeaturesChanged: function(e) { diff --git a/lizmap/modules/lizmap/controllers/datatables.classic.php b/lizmap/modules/lizmap/controllers/datatables.classic.php index 9c27acee99..88d204e8c5 100644 --- a/lizmap/modules/lizmap/controllers/datatables.classic.php +++ b/lizmap/modules/lizmap/controllers/datatables.classic.php @@ -24,11 +24,6 @@ public function index() $repository = $this->param('repository'); $project = $this->param('project'); $layerId = $this->param('layerId'); - $moveSelectedToTop = $this->param('moveselectedtotop'); - $selectedFeatureIDs = array(); - if ($this->param('selectedfeatureids')) { - $selectedFeatureIDs = explode(',', $this->param('selectedfeatureids')); - } $filteredFeatureIDs = array(); if ($this->param('filteredfeatureids')) { $filteredFeatureIDs = explode(',', $this->param('filteredfeatureids')); @@ -87,34 +82,6 @@ public function index() $recordsFiltered = count($filteredFeatureIDs); } - if ($moveSelectedToTop == 'true') { - $featureIds = array(); - foreach ($selectedFeatureIDs as $id) { - $featureIds[] = $typeName.'.'.$id; - } - - $wfsrequest = new WFSRequest( - $lproj, - array( - 'SERVICE' => 'WFS', - 'VERSION' => '1.0.0', - 'REQUEST' => 'GetFeature', - 'OUTPUTFORMAT' => 'GeoJSON', - 'GEOMETRYNAME' => 'none', - 'FEATUREID' => implode(',', $featureIds), - ), - lizmap::getServices() - ); - $wfsresponse = $wfsrequest->process(); - $featureData = $wfsresponse->getBodyAsString(); - $jsonFeatures = json_decode($featureData)->features; - - // Remove selected features from the list of features to get - $DTLength = $DTLength - count($jsonFeatures); - $wfsParamsData['MAXFEATURES'] = $DTLength; - $wfsParamsData['EXP_FILTER'] = '$id NOT IN ('.implode(' , ', $selectedFeatureIDs).')'; - } - if (count($filteredFeatureIDs) > 0) { $wfsParamsData['EXP_FILTER'] = '$id IN ('.implode(' , ', $filteredFeatureIDs).')'; } diff --git a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties index 28601005a8..b49a63e0ec 100644 --- a/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties +++ b/lizmap/modules/view/locales/en_US/dictionnary.UTF-8.properties @@ -101,7 +101,7 @@ attributeLayers.toolbar.cb.data.detail.title=Display info attributeLayers.toolbar.input.search.title=Search attributeLayers.toolbar.btn.select.searched.title=Select searched line attributeLayers.toolbar.btn.data.unselect.title=Unselect all -attributeLayers.toolbar.btn.data.moveselectedtotop.title=Move selected to top +attributeLayers.toolbar.btn.data.moveselectedtotop.title=Display selection only attributeLayers.toolbar.btn.data.filter.title=Filter attributeLayers.toolbar.btn.data.export.title=Export attributeLayers.toolbar.btn.data.createFeature.title=Create feature From 99f6a49a13b42a237440d25281fd760adc4f1c02 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Thu, 15 May 2025 11:52:09 +0200 Subject: [PATCH 33/49] DT: set `orderSequence` to have DT 1.x behaviour https://datatables.net/reference/option/columns.orderSequence --- assets/src/legacy/attributeTable.js | 1 + 1 file changed, 1 insertion(+) diff --git a/assets/src/legacy/attributeTable.js b/assets/src/legacy/attributeTable.js index 87416861ed..620820ac6b 100644 --- a/assets/src/legacy/attributeTable.js +++ b/assets/src/legacy/attributeTable.js @@ -1375,6 +1375,7 @@ var lizAttributeTable = function() { const params = globalThis['lizUrls'].params; params['layerId'] = lConfig.id; + DataTable.defaults.column.orderSequence = ['asc', 'desc']; const oTable = new DataTable(aTable, { serverSide: true ,ajax: { From 4269d6ce5df35e54707d2b0d8ece26850d7c0fc0 Mon Sep 17 00:00:00 2001 From: nboisteault Date: Tue, 26 Aug 2025 15:31:03 +0200 Subject: [PATCH 34/49] e2e: fix some tests --- .../playwright/attribute-table.spec.js | 71 +++++++++---------- 1 file changed, 34 insertions(+), 37 deletions(-) diff --git a/tests/end2end/playwright/attribute-table.spec.js b/tests/end2end/playwright/attribute-table.spec.js index 6bbafd8669..72cdb0db3d 100644 --- a/tests/end2end/playwright/attribute-table.spec.js +++ b/tests/end2end/playwright/attribute-table.spec.js @@ -50,11 +50,11 @@ test.describe('Attribute table @readonly', () => { await project.open(); let layerName = 'quartiers_shp'; - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(layerName); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); let tableWrapper = project.attributeTableWrapper(layerName); await expect(tableWrapper.locator('div.dt-scroll-head th')) .toHaveCount(6); @@ -73,11 +73,11 @@ test.describe('Attribute table @readonly', () => { await project.closeAttributeTable(); layerName = 'Les_quartiers_a_Montpellier'; - getFeatureRequestPromise = project.waitForGetFeatureRequest(); + datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(layerName); - getFeatureRequest = await getFeatureRequestPromise; - getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); + datatablesRequest = await datatablesRequestPromise; + datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); tableWrapper = project.attributeTableWrapper(layerName); await expect(tableWrapper.locator('div.dt-scroll-head th')) .toHaveCount(7); @@ -148,11 +148,11 @@ test.describe('Attribute table @readonly', () => { expect(defaultByteLength).toBeGreaterThan(8000); // 8667 expect(defaultByteLength).toBeLessThan(10000); // 8667 - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(tableName); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); let tableHtml = project.attributeTableHtml(tableName); // Check table lines @@ -439,11 +439,11 @@ test.describe('Attribute table @readonly', () => { const typeName = 'quartiers_shp'; const layerName = 'quartiers_shp'; - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(tableName); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); let tableHtml = project.attributeTableHtml(tableName); // Check table lines @@ -633,12 +633,12 @@ test.describe('Attribute table @readonly', () => { await project.open(); const layerName = 'Les_quartiers_a_Montpellier'; - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(layerName); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); - await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); + await expect(project.attributeTableWrapper(layerName).locator('div.dt-info')) .toContainText('Showing 1 to 7 of 7 entries'); await expect(project.attributeTableHtml(layerName).locator('tbody tr')) .toHaveCount(7); @@ -660,26 +660,23 @@ test.describe('Attribute table @readonly', () => { await project.closeLeftDock(); const layerName = 'random_points'; - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(layerName); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); - await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); + await expect(project.attributeTableWrapper(layerName).locator('div.dt-info')) .toContainText('Showing 1 to 50 of 700 entries'); await expect(project.attributeTableHtml(layerName).locator('tbody tr')) .toHaveCount(50); - await expect(project.attributeTableWrapper(layerName).locator('ul.pagination > li.paginate_button')) + await expect(project.attributeTableWrapper(layerName).locator('ul.pagination > li.dt-paging-button')) .toHaveCount(9); - // click on last page which is the previous last paginate_button + // click on last page which is the previous last dt-paging-button await project.attributeTableWrapper(layerName).hover(); - project.attributeTableWrapper(layerName).locator('ul.pagination > li.paginate_button:nth-last-child(-0n+2)').dispatchEvent('click'); - await expect(project.attributeTableWrapper(layerName).locator('div.dataTables_info')) + project.attributeTableWrapper(layerName).locator('ul.pagination > li.dt-paging-button:nth-last-child(-0n+2) > button').dispatchEvent('click'); + await expect(project.attributeTableWrapper(layerName).locator('div.dt-info')) .toContainText('Showing 651 to 700 of 700 entries'); }); -}); - -test.describe('Attribute table data restricted to map extent @readonly', () => { test('Data filtered by extent', async ({ page }) => { @@ -865,11 +862,11 @@ test.describe('Layer export permissions ACL', () => { // check layer export capabilities for logged in user for(const layerObj of expected){ - let getFeatureRequestPromise = project.waitForGetFeatureRequest(); + let datatablesRequestPromise = project.waitForDatatablesRequest(); await project.openAttributeTable(layerObj.layer); - let getFeatureRequest = await getFeatureRequestPromise; - let getFeatureResponse = await getFeatureRequest.response(); - responseExpect(getFeatureResponse).toBeGeoJson(); + let datatablesRequest = await datatablesRequestPromise; + let datatablesResponse = await datatablesRequest.response(); + responseExpect(datatablesResponse).toBeJson(); await expect(userPage.locator('.attribute-layer-action-bar .export-formats')).toHaveCount(layerObj.onPage); await project.closeAttributeTable(); } From b466b0bb78b52395348a8126892c8733228ff708 Mon Sep 17 00:00:00 2001 From: rldhont Date: Wed, 10 Sep 2025 12:10:32 +0200 Subject: [PATCH 35/49] Use `active` with `btn-primary` class for filter and select buttons in FeatureToolbar --- assets/src/components/FeatureToolbar.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/assets/src/components/FeatureToolbar.js b/assets/src/components/FeatureToolbar.js index 6a336efb3e..373e93670c 100644 --- a/assets/src/components/FeatureToolbar.js +++ b/assets/src/components/FeatureToolbar.js @@ -50,7 +50,7 @@ export default class FeatureToolbar extends HTMLElement {