diff --git a/assets/src/components/FeaturesTable.js b/assets/src/components/FeaturesTable.js
index c6e7f694b5..25d5df64ef 100644
--- a/assets/src/components/FeaturesTable.js
+++ b/assets/src/components/FeaturesTable.js
@@ -17,6 +17,15 @@ import { mainLizmap, mainEventDispatcher } from '../modules/Globals.js';
* @element lizmap-features-table
* @fires features.table.item.dragged
* @fires features.table.rendered
+ * @example
Example of use
+ *
+ *
+ * "libsquart"
+ *
+ *
*/
export default class FeaturesTable extends HTMLElement {
@@ -53,7 +62,7 @@ export default class FeaturesTable extends HTMLElement {
// Sorting attribute and direction
this.sortingField = this.getAttribute('sortingField');
const sortingOrder = this.getAttribute('sortingOrder');
- this.sortingOrder = (sortingOrder !== null && ['asc', 'desc'].includes(sortingOrder)) ? this.sortingField : 'asc';
+ this.sortingOrder = (sortingOrder !== null && ['asc', 'desc'].includes(sortingOrder)) ? sortingOrder : 'asc';
// open popup ?
this.openPopup = (this.layerConfig && this.layerConfig.popup);
@@ -65,6 +74,9 @@ export default class FeaturesTable extends HTMLElement {
// Features
this.features = [];
+ // Additional Fields JSON
+ this.additionalFields = {field:[]};
+
// Clicked item feature ID
this.activeItemFeatureId = null;
@@ -82,8 +94,18 @@ export default class FeaturesTable extends HTMLElement {
fields += ',' + this.sortingField;
}
+ let uniqueAdditionalFields = [];
+
+ // Create a unique JSON object for PHP request
+ if (!this.isAdditionalFieldsEmpty()) {
+ uniqueAdditionalFields = {};
+
+ this.additionalFields.field.forEach(field => {
+ uniqueAdditionalFields[field.alias] = field.expression;
+ });
+ }
// Get the features corresponding to the given parameters from attributes
- mainLizmap.featuresTable.getFeatures(this.layerId, this.expressionFilter, this.withGeometry, fields)
+ mainLizmap.featuresTable.getFeatures(this.layerId, this.expressionFilter, this.withGeometry, fields, uniqueAdditionalFields)
.then(displayExpressions => {
// Check for errors
if (!('status' in displayExpressions)) return;
@@ -106,7 +128,7 @@ export default class FeaturesTable extends HTMLElement {
// If an error occurred, replace empty content with error
if (displayExpressions.status != 'success') {
- this.querySelector('div.lizmap-features-table-container').innerHTML = `
+ this.querySelector('table.lizmap-features-table-container').innerHTML = `
${displayExpressions.error}
`;
}
@@ -176,7 +198,7 @@ export default class FeaturesTable extends HTMLElement {
// If there is not features, add empty content in the container
if (this.features.length === 0) {
- this.querySelector('div.lizmap-features-table-container').innerHTML = ' ';
+ this.querySelector('table.lizmap-features-table-container').innerHTML = ' ';
}
// Add drag & drop capabilities if option is set
@@ -204,6 +226,7 @@ export default class FeaturesTable extends HTMLElement {
* @param {number} lineId Line number of the item in the features table
*/
onItemClick(event, feature) {
+
if (!this.openPopup) {return true;}
// Check if the item was active
@@ -213,19 +236,22 @@ export default class FeaturesTable extends HTMLElement {
const activeItemTitle = `${this.openPopup ? lizDict['featuresTable.item.active.hover']: ''}`;
const defaultItemTitle = `${this.openPopup ? lizDict['featuresTable.item.hover'] + '.': ''} ${this.itemsDraggable == 'yes' ? lizDict['featuresTable.item.draggable.hover'] + '.' : ''}`;
+ // Fix event.target depending on which HTML tag we click on
+ const eventTarget = event.currentTarget;
+
if (!itemWasActive) {
// Set the features table properties
- const lineId = parseInt(event.target.dataset.lineId);
+ const lineId = parseInt(eventTarget.dataset.lineId);
this.activeItemFeatureId = feature.properties.feature_id;
this.activeItemLineNumber = lineId;
// Get popup data and display it
mainLizmap.featuresTable.openPopup(
- event.target.dataset.layerId,
+ eventTarget.dataset.layerId,
feature,
this.uniqueField,
- event.target.parentElement.parentElement.querySelector('div.lizmap-features-table-item-popup'),
+ eventTarget.parentElement.parentElement.parentElement.querySelector('div.lizmap-features-table-item-popup'),
function(aLayerId, aFeature, aTarget) {
// Add bootstrap classes to the popup tables
const popupTable = aTarget.querySelector('table.lizmapPopupTable');
@@ -241,14 +267,14 @@ export default class FeaturesTable extends HTMLElement {
// Remove popup-displayed for all other items
// And restore previous title
- var items = featuresTableDiv.querySelectorAll('div.lizmap-features-table-container div.lizmap-features-table-item.popup-displayed');
+ var items = featuresTableDiv.querySelectorAll('table.lizmap-features-table-container tr.lizmap-features-table-item.popup-displayed');
Array.from(items).forEach(item => {
item.classList.remove('popup-displayed');
item.setAttribute('title', defaultItemTitle);
});
// Add class to the active item
- const childSelector = `div.lizmap-features-table-item[data-feature-id="${feature.properties.feature_id}"]`;
+ const childSelector = `tr.lizmap-features-table-item[data-feature-id="${feature.properties.feature_id}"]`;
const activeItem = featuresTableDiv.querySelector(childSelector);
if (activeItem) activeItem.classList.add('popup-displayed');
@@ -268,9 +294,9 @@ export default class FeaturesTable extends HTMLElement {
this.activeItemFeatureId = null;
this.activeItemLineNumber = null;
- event.target.classList.remove('popup-displayed');
- event.target.setAttribute('title', defaultItemTitle);
- event.target.closest('div.lizmap-features-table').classList.remove('popup-displayed');
+ eventTarget.classList.remove('popup-displayed');
+ eventTarget.setAttribute('title', defaultItemTitle);
+ eventTarget.closest('div.lizmap-features-table').classList.remove('popup-displayed');
}
}
@@ -282,7 +308,7 @@ export default class FeaturesTable extends HTMLElement {
*/
addDragAndDropCapabilities() {
// Add drag and drop events to table items
- const items = this.querySelectorAll('div.lizmap-features-table-container div.lizmap-features-table-item');
+ const items = this.querySelectorAll('table.lizmap-features-table-container tr.lizmap-features-table-item');
if (!items) return;
Array.from(items).forEach(item => {
@@ -359,7 +385,7 @@ export default class FeaturesTable extends HTMLElement {
// Send event
const movedFeatureId = dropped.dataset.featureId;
- const newItem = item.parentElement.querySelector(`div.lizmap-features-table-item[data-feature-id="${movedFeatureId}"]`);
+ const newItem = item.parentElement.querySelector(`tr.lizmap-features-table-item[data-feature-id="${movedFeatureId}"]`);
/**
* When the user has dropped an item in a new position
* @event features.table.item.dragged
@@ -396,6 +422,25 @@ export default class FeaturesTable extends HTMLElement {
connectedCallback() {
+ if (document.querySelector("lizmap-field")) {
+ const listField = document.querySelectorAll("lizmap-field");
+
+ listField.forEach((field) => {
+ // Prevent all fields goes on one tab instead of the other when multiple layers are clicked on
+ if (JSON.stringify(this.additionalFields.field).includes(btoa(field.getAttribute('alias')))) {
+ return;
+ }
+
+ this.additionalFields.field.push({
+ 'alias': btoa(field.getAttribute('alias')),
+ 'expression': field.innerText,
+ 'description': field.getAttribute('description')
+ });
+
+ field.remove();
+ });
+ }
+
// Template
this._template = () => html`
{
// Click on the previous item
const lineNumber = this.activeItemLineNumber - 1;
- const featureDiv = this.querySelector(`div.lizmap-features-table-item[data-line-id="${lineNumber}"]`);
+ const featureDiv = this.querySelector(`tr.lizmap-features-table-item[data-line-id="${lineNumber}"]`);
if (featureDiv) featureDiv.click();
}}>
-
- ${this.features.map((feature, idx) =>
- html`
-
- `
- )}
-
+
+ ${this.buildLabels()}
+
+ ${this.features.map((feature, idx) =>
+ html`
+
+ `
+ )}
+
+
`;
@@ -452,6 +501,118 @@ export default class FeaturesTable extends HTMLElement {
this.load();
}
+ /**
+ * Build the columns of the table
+ * @param properties - Object containing the properties of the feature
+ * @returns {TemplateResult<1>} The columns of the table
+ */
+ buildColumns(properties) {
+
+ let result = html`
+ ${this.buildDisplayExpressionColumn(properties)}
+ `;
+
+ if (!this.isAdditionalFieldsEmpty()) {
+ this.additionalFields.field.forEach(field => {
+ let td = html`
+
+ ${properties[field.alias]}
+ |
+ `;
+ result = html`
+ ${result}
+ ${td}
+ `;
+ });
+ }
+
+ return result;
+ }
+
+ /**
+ * Initialize tab with the first column "display_expression"
+ * @param {object} properties - Object containing the properties of the feature
+ * @returns {TemplateResult<1>} The first column of the table
+ */
+ buildDisplayExpressionColumn(properties) {
+ if (this.isGeneralLabelExisting()) {
+ return html`
+
+ ${properties.display_expression}
+ |
+ `;
+ } else {
+ return html``;
+ }
+ }
+
+ /**
+ * Initialize the labels of the table
+ * @returns {TemplateResult<1>} The labels of the table
+ */
+ buildLabels() {
+ if (this.isAdditionalFieldsEmpty()) {
+ return html``;
+ }
+
+ let result;
+
+ this.additionalFields.field.forEach(field => {
+ let th = html`
+
+ ${atob(field.alias)}
+ |
+ `;
+ result = html`
+ ${result}
+ ${th}
+ `;
+ });
+
+ if (this.isGeneralLabelExisting()) {
+ // First th to create an empty column for "display_expression"
+ return html`
+
+
+ |
+ ${result}
+
+
+ `;
+ } else {
+ return html`
+
+
+ ${result}
+
+
+ `;
+ }
+
+
+ }
+
+ /**
+ * Check if the additionalFields property is empty
+ * @returns {boolean} True if the additionalFields property is empty
+ */
+ isAdditionalFieldsEmpty() {
+ return this.additionalFields.field.length === 0;
+ }
+
+ /**
+ * Check if the general label "display_expression" is existing
+ * @returns {boolean} True if the general label "display_expression" is existing
+ */
+ isGeneralLabelExisting() {
+ return this.features[0].properties.hasOwnProperty('display_expression');
+ }
+
static get observedAttributes() { return ['updated']; }
attributeChangedCallback(name, oldValue, newValue) {
diff --git a/assets/src/modules/FeaturesTable.js b/assets/src/modules/FeaturesTable.js
index f481d3e266..b206bc9f19 100644
--- a/assets/src/modules/FeaturesTable.js
+++ b/assets/src/modules/FeaturesTable.js
@@ -32,13 +32,14 @@ export default class FeaturesTable {
* @param {string|null} filter An QGIS expression filter
* @param {boolean} withGeometry If we need to get the geometry
* @param {string|null} fields List of field names separated by comma
+ * @param {object|array} additionalFields JSON object with the field names and expressions
*
* @returns — A Promise that resolves with the result of parsing the response body text as JSON.
* @throws — {ResponseError} In case of invalid content type (not application/json or application/vnd.geo+json) or Invalid JSON
* @throws — {HttpError} In case of not successful response (status not in the range 200 – 299)
* @throws — {NetworkError} In case of catch exceptions
*/
- getFeatures(layerId, filter = null, withGeometry = false, fields = 'null') {
+ getFeatures(layerId, filter = null, withGeometry = false, fields = 'null', additionalFields = []) {
// Build URL
const url = `${lizUrls.service.replace('service?','features/displayExpression?')}&`;
@@ -49,7 +50,7 @@ export default class FeaturesTable {
formData.append('exp_filter', filter);
formData.append('with_geometry', withGeometry.toString());
formData.append('fields', fields);
-
+ formData.append('additionalFields',JSON.stringify(additionalFields));
// Return promise
return Utils.fetchJSON(url, {
method: "POST",
diff --git a/lizmap/modules/lizmap/controllers/features.classic.php b/lizmap/modules/lizmap/controllers/features.classic.php
index 53effbc933..9669c53cc8 100644
--- a/lizmap/modules/lizmap/controllers/features.classic.php
+++ b/lizmap/modules/lizmap/controllers/features.classic.php
@@ -154,6 +154,22 @@ public function displayExpression()
// Filter
$exp_filter = trim($this->param('exp_filter', 'FALSE'));
+ // AdditionalFields
+ try {
+ $additionalFields = json_decode($this->param('additionalFields', '[]'), true);
+ } catch (\Exception $e) {
+ $content['error'] = 'An error occurred while replacing the expression text !';
+ $rep->data = $content;
+
+ return $rep;
+ }
+
+ foreach ($additionalFields as $key => $value) {
+ $additionalFields[$key] = $this->regexCheck($value);
+ }
+
+ $expressions = array_merge($expressions, $additionalFields);
+
// Get the evaluated features for the given layer and parameters
$getDisplayExpressions = qgisExpressionUtils::virtualFields(
$qgisLayer,
@@ -182,4 +198,27 @@ public function displayExpression()
return $rep;
}
+
+ /**
+ * Check if the expression is valid.
+ *
+ * @param $expression string The expression to check
+ *
+ * @return string The expression if it is valid, 'INVALID EXPRESSION' otherwise
+ */
+ private function regexCheck($expression): string
+ {
+ // Allowing only fields like "libsquart" and not expressions like rand(10,34)
+ $regex = '/^[a-zA-Z0-9_\\- ]+$/';
+
+ $expression = str_replace('"', '', $expression);
+
+ preg_match($regex, $expression, $matches);
+
+ if (count($matches) == 0) {
+ return "'INVALID EXPRESSION'";
+ }
+
+ return '"'.$expression.'"';
+ }
}
diff --git a/lizmap/www/assets/css/map.css b/lizmap/www/assets/css/map.css
index 4bfe844397..bb5874a4ea 100644
--- a/lizmap/www/assets/css/map.css
+++ b/lizmap/www/assets/css/map.css
@@ -3603,8 +3603,7 @@ lizmap-features-table h4 {
div.lizmap-features-table {
position: relative;
}
-div.lizmap-features-table-container {
- border: 1px solid var(--color-contrasted-elements);
+table.lizmap-features-table-container {
font-size: 0.9em;
margin: 0px 10px 0px 10px;
position: relative;
@@ -3612,35 +3611,20 @@ div.lizmap-features-table-container {
overflow: auto;
}
-/* Feature item */
-div.lizmap-features-table-container div.lizmap-features-table-item {
- padding: 5px;
- border-top: 1px solid var(--color-contrasted-elements);
- position: relative;
- /* Invert colors on hover & focus */
- &:hover, &:focus, &:active, &.popup-displayed {
- background-color: var(--color-contrasted-elements);
- color: var(--color-contrasted-text);
- }
- /* Do not render the top border for the first item (parent already has a border) */
- &:first-child {
- border-top: none;
- }
-}
/* Pointer cursor */
-div.lizmap-features-table-container div.lizmap-features-table-item.has-action {
+table.lizmap-features-table-container tr.lizmap-features-table-item.has-action {
cursor: pointer;
}
+
/* Hide other children if only one child feature is active */
-div.lizmap-features-table.popup-displayed div.lizmap-features-table-container div.lizmap-features-table-item {
+div.lizmap-features-table.popup-displayed table.lizmap-features-table-container tr.lizmap-features-table-item {
display: none;
}
/* In this context, display only the active child item */
-div.lizmap-features-table.popup-displayed div.lizmap-features-table-container div.lizmap-features-table-item.popup-displayed {
- display: block;
-}
-/* Toolbar visible when popup is visible */
+div.lizmap-features-table.popup-displayed table.lizmap-features-table-container tr.lizmap-features-table-item.popup-displayed {
+ display: revert;
+}/* Toolbar visible when popup is visible */
div.lizmap-features-table div.lizmap-features-table-toolbar {
display: none;
}
@@ -3686,17 +3670,17 @@ div.lizmap-features-table-toolbar button.next-popup {
/* Popup div, visible only if popup-dsplayed class is present for it preceeding sibling */
-div.lizmap-features-table div.lizmap-features-table-container + div.lizmap-features-table-item-popup {
+div.lizmap-features-table table.lizmap-features-table-container + div.lizmap-features-table-item-popup {
display: none;
min-height: 200px;
min-width: 200px;
overflow: auto;
}
-div.lizmap-features-table.popup-displayed div.lizmap-features-table-container + div.lizmap-features-table-item-popup {
+div.lizmap-features-table.popup-displayed table.lizmap-features-table-container + div.lizmap-features-table-item-popup {
display: block;
}
/* Hide title of the popup, since it is already visible above */
-div.lizmap-features-table.popup-displayed div.lizmap-features-table-container + div.lizmap-features-table-item-popup h4.lizmapPopupTitle {
+div.lizmap-features-table.popup-displayed table.lizmap-features-table-container + div.lizmap-features-table-item-popup h4.lizmapPopupTitle {
display: none;
}
.hide {
diff --git a/tests/end2end/playwright/lizmap-features-table.spec.js b/tests/end2end/playwright/lizmap-features-table.spec.js
index e01e5c27eb..15812d166a 100644
--- a/tests/end2end/playwright/lizmap-features-table.spec.js
+++ b/tests/end2end/playwright/lizmap-features-table.spec.js
@@ -34,12 +34,12 @@ test.describe('Display lizmap-features-table component in popup from QGIS toolti
await expect(lizmapFeaturesTable.locator("h4")).toHaveText("child sub-districts");
// Check items count
- await expect(lizmapFeaturesTable.locator("div.lizmap-features-table-container div.lizmap-features-table-item")).toHaveCount(10);
+ await expect(lizmapFeaturesTable.locator("table.lizmap-features-table-container tr.lizmap-features-table-item")).toHaveCount(10);
// Get first item and check it
- let firstItem = lizmapFeaturesTable.locator("div.lizmap-features-table-container div.lizmap-features-table-item").first();
+ let firstItem = lizmapFeaturesTable.locator("table.lizmap-features-table-container tr.lizmap-features-table-item").first();
await expect(firstItem).toHaveAttribute('data-line-id', '1');
- await expect(firstItem).toHaveAttribute('data-feature-id', '10');
+ await expect(firstItem).toHaveAttribute('data-feature-id', '17');
// Click on first item and check sub-popup
firstItem.click();
@@ -47,13 +47,13 @@ test.describe('Display lizmap-features-table component in popup from QGIS toolti
await expect(firstItem).toHaveClass(/popup-displayed/);
let popupContainer = lizmapFeaturesTable.locator('div.lizmap-features-table-item-popup');
await expect(popupContainer).toBeVisible();
- await expect(popupContainer.locator('table.lizmapPopupTable tbody tr:first-child td')).toHaveText('10');
+ await expect(popupContainer.locator('table.lizmapPopupTable tbody tr:first-child td')).toHaveText('17');
// Next item
let nextItemButton = lizmapFeaturesTable.locator('div.lizmap-features-table-toolbar button.next-popup');
nextItemButton.click();
await expect(popupContainer).toBeVisible();
- await expect(popupContainer.locator('table.lizmapPopupTable tbody tr:first-child td')).toHaveText('8');
+ await expect(popupContainer.locator('table.lizmapPopupTable tbody tr:first-child td')).toHaveText('9');
// Close Item
let closeItemButton = lizmapFeaturesTable.locator('div.lizmap-features-table-toolbar button.close-popup');
@@ -62,6 +62,15 @@ test.describe('Display lizmap-features-table component in popup from QGIS toolti
await expect(lizmapFeaturesTable.locator('div.lizmap-features-table')).not.toHaveClass(/popup-displayed/);
await expect(firstItem).not.toHaveClass(/popup-displayed/);
+ // Drag and Drop Item
+ await page.locator('.lizmap-features-table-container > tbody > tr:nth-child(2)').dragTo(page.locator('.lizmap-features-table-container > tbody > tr:first-child'));
+
+ await expect(firstItem).toHaveAttribute('data-line-id', '1');
+ await expect(firstItem).toHaveAttribute('data-feature-id', '9');
+
+ let secondItem = lizmapFeaturesTable.locator(".lizmap-features-table-container > tbody > tr:nth-child(2)");
+ await expect(secondItem).toHaveAttribute('data-line-id', '2');
+ await expect(secondItem).toHaveAttribute('data-feature-id', '17');
//clear screen