Source: components/FeaturesTable.js

  1. /**
  2. * @module components/FeaturesTable.js
  3. * @name FeaturesTable
  4. * @copyright 2024 3Liz
  5. * @author DOUCHIN Michaƫl
  6. * @license MPL-2.0
  7. */
  8. import { html, render } from 'lit-html';
  9. import { mainLizmap, mainEventDispatcher } from '../modules/Globals.js';
  10. import GeoJSON from 'ol/format/GeoJSON.js';
  11. /**
  12. * @class
  13. * @name FeaturesTable
  14. * @summary Allows to display a compact list of vector layer features labels
  15. * @augments HTMLElement
  16. * @description lizmap-features-table
  17. * @fires FeaturesTable#features.table.item.highlighted
  18. * @fires FeaturesTable#features.table.item.dragged
  19. * @fires FeaturesTable#features.table.rendered
  20. * @example <caption>Example of use</caption>
  21. * <lizmap-features-table draggable="yes" sortingOrder="asc" sortingField="libsquart"
  22. * withGeometry="1" expressionFilter="quartmno = 'HO'"
  23. * uniqueField="id" layerId="subdistrict_24ceec66_e7fe_46a2_b57a_af5c50389649"
  24. * layerTitle="child sub-districts"
  25. * (optional) data-show-highlighted-feature-geometry="true"
  26. * (optional) data-center-to-highlighted-feature-geometry="true"
  27. * (optional) data-max-features="100"
  28. * (optional) data-active-item-feature-id="5"
  29. * >
  30. * <lizmap-field data-alias="District's name" data-description="Label of district's name">
  31. * "libsquart"
  32. * </lizmap-field>
  33. * </lizmap-features-table>
  34. */
  35. export default class FeaturesTable extends HTMLElement {
  36. constructor() {
  37. super();
  38. // Random element id
  39. if (window.isSecureContext) {
  40. this.id = window.crypto.randomUUID();
  41. } else {
  42. this.id = btoa(String.fromCharCode(...new Uint8Array( Array(30).fill().map(() => Math.round(Math.random() * 30)) )));
  43. }
  44. // Layer name
  45. this.layerTitle = this.getAttribute('layerTitle') || 'Features table: error';
  46. // Layer id
  47. this.layerId = this.getAttribute('layerId');
  48. // Error text
  49. this.error = null;
  50. // Get the layer name & configuration
  51. this.layerConfig = null;
  52. if (mainLizmap.initialConfig.layers.layerIds.includes(this.layerId)) {
  53. this.layerConfig = mainLizmap.initialConfig.layers.getLayerConfigByLayerId(this.layerId);
  54. }
  55. // Primary key field
  56. this.uniqueField = this.getAttribute('uniqueField');
  57. // Expression filter
  58. this.expressionFilter = this.getAttribute('expressionFilter');
  59. // Get the geometry or NetworkError
  60. this.withGeometry = this.hasAttribute('withGeometry');
  61. // Sorting attribute
  62. this.sortingField = this.getAttribute('sortingField');
  63. // Sorting order
  64. const sortingOrder = this.getAttribute('sortingOrder');
  65. this.sortingOrder = (sortingOrder !== null && ['asc', 'desc'].includes(sortingOrder.toLowerCase())) ? sortingOrder : 'asc';
  66. // open popup ?
  67. this.openPopup = (this.layerConfig && this.layerConfig.popup);
  68. // Add drag&drop capability ?
  69. const draggable = this.getAttribute('draggable');
  70. this.itemsDraggable = (draggable !== null && ['yes', 'no'].includes(draggable.toLowerCase())) ? draggable : 'no';
  71. // Features
  72. this.features = [];
  73. // Additional Fields JSON
  74. this.additionalFields = {fields:[]};
  75. // Clicked item line number
  76. this.activeItemLineNumber = null;
  77. // Maximum number of features
  78. this.maxFeatures = (this.dataset.maxFeatures > 0) ? this.dataset.maxFeatures : 1000;
  79. }
  80. /**
  81. * Load features from the layer and configured filter
  82. */
  83. async load() {
  84. if (this.dataset.showHighlightedFeatureGeometry === 'true') {
  85. // Remove the highlight on the map
  86. mainLizmap.map.clearHighlightFeatures();
  87. }
  88. // Build needed fields
  89. let fields = `${this.uniqueField}`;
  90. if (this.sortingField) {
  91. fields += ',' + this.sortingField;
  92. }
  93. let uniqueAdditionalFields = [];
  94. // Create a unique JSON object for PHP request
  95. if (!this.isAdditionalFieldsEmpty()) {
  96. uniqueAdditionalFields = {};
  97. this.additionalFields.fields.forEach(field => {
  98. uniqueAdditionalFields[field.alias] = field.expression;
  99. });
  100. }
  101. // Get the features corresponding to the given parameters from attributes
  102. mainLizmap.featuresTable.getFeatures(this.layerId, this.expressionFilter, this.withGeometry, fields, uniqueAdditionalFields, this.maxFeatures, this.sortingField, this.sortingOrder)
  103. .then(displayExpressions => {
  104. // Check for errors
  105. if (!('status' in displayExpressions)) return;
  106. if (displayExpressions.status != 'success') {
  107. console.error(displayExpressions.error);
  108. } else {
  109. // Set component data property
  110. this.features = displayExpressions.data;
  111. }
  112. // Render
  113. this.render();
  114. // If an error occurred, replace empty content with error
  115. if (displayExpressions.status != 'success') {
  116. this.querySelector('table.lizmap-features-table-container').innerHTML = `<p style="padding: 3px;">
  117. ${displayExpressions.error}
  118. </p>`;
  119. }
  120. })
  121. .catch(err => {
  122. // Display an error message
  123. console.warn(err.message);
  124. this.innerHTML = `<p style="padding: 3px;">${err.message}</p>`;
  125. })
  126. }
  127. /**
  128. * Render component from the template using Lit
  129. */
  130. render() {
  131. // Render with lit-html
  132. render(this._template(), this);
  133. // If there is not features, add empty content in the container
  134. if (this.features.length === 0) {
  135. this.querySelector('table.lizmap-features-table-container').innerHTML = '&nbsp;';
  136. } else {
  137. // Add drag & drop capabilities if option is set
  138. if (this.itemsDraggable == 'yes') {
  139. this.addDragAndDropCapabilities();
  140. }
  141. // Toggle the feature detail
  142. this.toggleFeatureDetail();
  143. }
  144. /**
  145. * When the table has been successfully displayed. The event carries the lizmap-features-table HTML element ID
  146. * @event features.table.rendered
  147. * @property {string} elementId HTML element ID
  148. */
  149. mainEventDispatcher.dispatch({
  150. type: 'features.table.rendered',
  151. elementId: this.id
  152. });
  153. }
  154. /**
  155. * Get the feature corresponding to the given feature ID
  156. * @param {number} featureId WFS Feature ID
  157. * @returns {object | null} WFS Feature
  158. */
  159. getFeatureById(featureId) {
  160. if (this.features.length === 0) {
  161. return null;
  162. }
  163. return this.features.find(feature => feature.properties.feature_id == featureId);
  164. }
  165. /**
  166. * Toggle the display of the active feature details
  167. *
  168. */
  169. toggleFeatureDetail() {
  170. // Do nothing if features have not been fetched yet
  171. // This is important to be able to create a new element
  172. // with its attribute data-active-item-feature-id set to a feature ID
  173. if (!this.features.length) return;
  174. // Get the value store in the dataset attribute
  175. const activeItemFeatureId = this.dataset.activeItemFeatureId;
  176. // console.log(`Toggle. activeItemFeatureId = ${activeItemFeatureId}`);
  177. // We should hide or display the features details
  178. // depending on the attribute value
  179. // If it is empty or a string 'null', hide the popup and remove the geometry
  180. // It is set to an existing feature ID, display the popup and the feature geometry
  181. if (!activeItemFeatureId || activeItemFeatureId === 'null') {
  182. // activeItemFeatureId empty or a string 'null'
  183. // deactivate the display of feature details (popup, geometry, etc.)
  184. if (this.openPopup) {
  185. // Hide the popup and display the list of features
  186. const item = this.querySelector('tr.lizmap-features-table-item.popup-displayed');
  187. if (item) item.classList.remove('popup-displayed');
  188. // Set the tr title back to original
  189. this.querySelectorAll('tr.lizmap-features-table-item').forEach(tr => {
  190. tr.setAttribute(
  191. 'title',
  192. `${lizDict['featuresTable.item.hover'] + '.'} ${this.itemsDraggable == 'yes' ? lizDict['featuresTable.item.draggable.hover'] + '.' : ''}`
  193. );
  194. });
  195. // Also remove the popup-displayed class from the div
  196. const div = this.querySelector('div.lizmap-features-table.popup-displayed');
  197. if (div) div.classList.remove('popup-displayed');
  198. }
  199. // Remove the highlight on the map
  200. if (this.dataset.showHighlightedFeatureGeometry === 'true') {
  201. mainLizmap.map.clearHighlightFeatures();
  202. }
  203. // Reset the active line number
  204. this.activeItemLineNumber = null;
  205. } else {
  206. // activeItemFeatureId is set
  207. // activate the display of feature details (popup, geometry, etc.)
  208. // Set the active line number
  209. const lineTr = this.querySelector(`tr.lizmap-features-table-item[data-feature-id="${activeItemFeatureId}"]`);
  210. if (lineTr) {
  211. this.activeItemLineNumber = parseInt(lineTr.dataset.lineId);
  212. }
  213. // Get the WFS feature
  214. const activeFeature = this.getFeatureById(activeItemFeatureId);
  215. // If no feature corresponds, we should deactivate the features detail
  216. if (!activeFeature) {
  217. this.setAttribute("data-active-item-feature-id", "");
  218. return;
  219. }
  220. // Display the feature popup
  221. if (this.openPopup) {
  222. this.displayFeaturePopup(activeFeature);
  223. }
  224. // Center the map on the clicked element if the feature has a geometry
  225. if (activeFeature.geometry && this.dataset.centerToHighlightedFeatureGeometry === 'true') {
  226. const geom = (new GeoJSON()).readGeometry(activeFeature.geometry, {
  227. dataProjection: 'EPSG:4326',
  228. featureProjection: lizMap.mainLizmap.projection
  229. });
  230. mainLizmap.map.zoomToGeometryOrExtent(geom, { duration: 150 });
  231. }
  232. // Highlight the clicked element on the map
  233. if (this.dataset.showHighlightedFeatureGeometry === 'true') {
  234. mainLizmap.map.setHighlightFeatures(
  235. activeFeature,
  236. "geojson",
  237. "EPSG:4326",
  238. );
  239. }
  240. // Dispatch event
  241. /**
  242. * When the user has selected an item and highlighted it
  243. * @event features.table.item.highlighted
  244. * @property {string} itemFeatureId The feature ID of the selected item
  245. */
  246. mainEventDispatcher.dispatch({
  247. type: 'features.table.item.highlighted',
  248. itemFeatureId: activeItemFeatureId,
  249. });
  250. }
  251. }
  252. /**
  253. * Get the feature popup HTML content
  254. * and display it
  255. * @param {object} feature WFS feature
  256. */
  257. displayFeaturePopup(feature) {
  258. // Get the default title for the table lines (tr)
  259. const defaultItemTitle = `${lizDict['featuresTable.item.hover']}. ${this.itemsDraggable == 'yes' ? lizDict['featuresTable.item.draggable.hover'] + '.' : ''}`;
  260. // Fetch the feature popup HTML and use it to display the popup
  261. mainLizmap.featuresTable.openPopup(
  262. this.layerId,
  263. feature,
  264. this.uniqueField,
  265. this.querySelector('div.lizmap-features-table-item-popup'),
  266. function(aLayerId, aFeature, aTarget) {
  267. // Add bootstrap classes to the popup tables
  268. const popupTable = aTarget.querySelector('table.lizmapPopupTable');
  269. if (popupTable) {
  270. popupTable.classList.add('table', 'table-condensed', 'table-sm', 'table-bordered', 'table-striped');
  271. }
  272. // Show popup and hide other children
  273. const featuresTableDiv = aTarget.parentElement;
  274. if (featuresTableDiv) {
  275. // Add class to the parent
  276. featuresTableDiv.classList.add('popup-displayed');
  277. // Remove popup-displayed for all other items
  278. // And restore previous title
  279. var items = featuresTableDiv.querySelectorAll('table.lizmap-features-table-container tr.lizmap-features-table-item.popup-displayed');
  280. Array.from(items).forEach(item => {
  281. item.classList.remove('popup-displayed');
  282. item.setAttribute('title', defaultItemTitle);
  283. });
  284. // Add class to the active item
  285. const childSelector = `tr.lizmap-features-table-item[data-feature-id="${feature.properties.feature_id}"]`;
  286. const activeItem = featuresTableDiv.querySelector(childSelector);
  287. if (activeItem) activeItem.classList.add('popup-displayed');
  288. // Change title
  289. activeItem.setAttribute(
  290. 'title',
  291. lizDict['featuresTable.item.active.hover']
  292. );
  293. // Toggle previous/next buttons depending on active line id
  294. const previousButton = featuresTableDiv.querySelector('div.lizmap-features-table-toolbar button.previous-popup');
  295. const nextButton = featuresTableDiv.querySelector('div.lizmap-features-table-toolbar button.next-popup');
  296. previousButton.style.display = (activeItem.dataset.lineId == 1) ? 'none' : 'initial';
  297. nextButton.style.display = (activeItem.dataset.lineId == featuresTableDiv.dataset.featuresCount) ? 'none' : 'initial';
  298. }
  299. }
  300. );
  301. }
  302. /**
  303. * Display a popup when a feature item is clicked
  304. * @param {Event} event Click event on a feature item
  305. * @param {number} featureId WFS feature ID
  306. */
  307. onItemClick(event, featureId) {
  308. // console.log('onItemClick - top');
  309. // Check if the item was active
  310. const itemWasActive = (this.dataset.activeItemFeatureId == featureId);
  311. // Set the features table properties
  312. if (!itemWasActive) {
  313. this.setAttribute('data-active-item-feature-id', featureId);
  314. } else {
  315. this.setAttribute('data-active-item-feature-id', "");
  316. this.activeItemLineNumber = null;
  317. }
  318. }
  319. /**
  320. * Add drag&drop capabilities to the lizmap-features-table element
  321. *
  322. * A request is sent when the order changes
  323. */
  324. addDragAndDropCapabilities() {
  325. // Add drag and drop events to table items
  326. const items = this.querySelectorAll('table.lizmap-features-table-container tr.lizmap-features-table-item');
  327. if (!items) return;
  328. Array.from(items).forEach(item => {
  329. item.setAttribute('draggable', 'true');
  330. item.addEventListener('dragstart', onDragStart)
  331. item.addEventListener('drop', OnDropped)
  332. item.addEventListener('dragenter', onDragEnter)
  333. item.addEventListener('dragover', onDragOver)
  334. item.addEventListener('dragleave', onDragLeave)
  335. item.addEventListener('dragend', onDragEnd)
  336. });
  337. // Utility functions for drag & drop capability
  338. /**
  339. * Action when an item is dragged
  340. * @param {Event} e Drag event
  341. */
  342. function onDragStart (e) {
  343. const index = [].indexOf.call(e.target.parentElement.children, e.target);
  344. e.dataTransfer.setData('text/plain', index)
  345. }
  346. /**
  347. * Action on drag enter
  348. * @param {Event} e Drag event
  349. */
  350. function onDragEnter (e) {
  351. cancelDefault(e);
  352. }
  353. /**
  354. * Action when an item is dragged over another
  355. * @param {Event} e Drag event
  356. */
  357. function onDragOver (e) {
  358. // Change the target element's style to signify a drag over event
  359. // has occurred
  360. e.currentTarget.style.background = "lightblue";
  361. cancelDefault(e);
  362. }
  363. /**
  364. * Action when an item is dragged out of another
  365. * @param {Event} e Drag event
  366. */
  367. function onDragLeave (e) {
  368. // Change the target element's style back to default
  369. e.currentTarget.style.background = "";
  370. cancelDefault(e);
  371. }
  372. /**
  373. * Wait for a delay
  374. * @param {number} delay Delay in milliseconds
  375. * @returns {Promise} Promise that resolves after the delay
  376. */
  377. function waitForIt(delay) {
  378. return new Promise((resolve) => setTimeout(resolve, delay))
  379. }
  380. /**
  381. * Action when an item is dropped
  382. * @param {Event} e Drag event
  383. */
  384. function OnDropped (e) {
  385. cancelDefault(e)
  386. // Change the target element's style back to default
  387. // Celui sur lequel on a lĆ¢chĆ© l'item dĆ©placĆ©
  388. e.currentTarget.style.background = "";
  389. // Get item
  390. const item = e.currentTarget;
  391. // Get dragged item old and new index
  392. const oldIndex = e.dataTransfer.getData('text/plain');
  393. // Get the dropped item
  394. const dropped = item.parentElement.children[oldIndex];
  395. // Emphasize the element
  396. // So that the user sees it well after drop
  397. dropped.style.border = "2px solid var(--color-contrasted-elements)";
  398. // Move the dropped items at new place
  399. item.before(dropped);
  400. // Set the new line number to the items
  401. let i = 1;
  402. for (const child of item.parentElement.children) {
  403. if (!child.classList.contains('lizmap-features-table-item')) {
  404. continue;
  405. }
  406. const lineId = i;
  407. child.dataset.lineId = lineId;
  408. i++;
  409. }
  410. // Send event
  411. const movedFeatureId = dropped.dataset.featureId;
  412. const newItem = item.parentElement.querySelector(`tr.lizmap-features-table-item[data-feature-id="${movedFeatureId}"]`);
  413. // Set the new active line number
  414. this.activeItemLineNumber = parseInt(newItem.dataset.lineId);
  415. /**
  416. * When the user has dropped an item in a new position
  417. * @event features.table.item.dragged
  418. * @property {string} itemFeatureId The vector feature ID
  419. * @property {string} itemOldLineId The original line ID before dropping the item
  420. * @property {string} itemNewLineId The new line ID after dropping the item in a new position
  421. */
  422. mainEventDispatcher.dispatch({
  423. type: 'features.table.item.dragged',
  424. itemFeatureId: movedFeatureId,
  425. itemOldLineId: dropped.dataset.lineId,
  426. itemNewLineId: newItem.dataset.lineId
  427. });
  428. }
  429. /**
  430. * Action when an item has finished being dragged
  431. * @param {Event} e Drag event
  432. */
  433. async function onDragEnd (e) {
  434. // Restore style after some time
  435. await waitForIt(3000);
  436. e.target.style.border = "";
  437. // e.target.style.backgroundColor = "";
  438. cancelDefault(e);
  439. }
  440. /**
  441. * Prevent the default behavior of an event
  442. * @param {Event} e event
  443. * @returns {boolean} False
  444. */
  445. function cancelDefault (e) {
  446. e.preventDefault();
  447. e.stopPropagation();
  448. return false;
  449. }
  450. }
  451. connectedCallback() {
  452. // console.log('connectedCallback - top');
  453. if (this.querySelector("lizmap-field")) {
  454. const listField = this.querySelectorAll("lizmap-field");
  455. const verifiedFields = this.verifyFields(listField);
  456. verifiedFields.forEach((field) => {
  457. const fieldExpression = field.innerText;
  458. const fieldDescription = field.dataset.description;
  459. let fieldAlias = field.dataset.alias;
  460. if (!fieldAlias) fieldAlias = fieldExpression.replaceAll('"', '');
  461. // Prevent all fields goes on one tab instead of the other when multiple layers are clicked on
  462. if (btoa(fieldAlias) in this.additionalFields.fields) {
  463. field.remove();
  464. return;
  465. }
  466. this.additionalFields.fields.push({
  467. 'alias': btoa(fieldAlias),
  468. 'expression': fieldExpression,
  469. 'description': fieldDescription
  470. });
  471. field.remove();
  472. });
  473. }
  474. // Template
  475. this._template = () => html`
  476. <div class="lizmap-features-table" data-features-count="${this.features.length}"
  477. title="${lizDict['bob']}">
  478. <h4>${this.layerTitle}</h4>
  479. <div class="lizmap-features-table-toolbar">
  480. <button class="btn btn-mini previous-popup"
  481. title="${lizDict['featuresTable.toolbar.previous']}"
  482. @click=${() => {
  483. // Click on the previous item
  484. const lineNumber = this.activeItemLineNumber - 1;
  485. const featureDiv = this.querySelector(`tr.lizmap-features-table-item[data-line-id="${lineNumber}"]`);
  486. if (featureDiv) featureDiv.click();
  487. }}></button>
  488. <button class="btn btn-mini next-popup"
  489. title="${lizDict['featuresTable.toolbar.next']}"
  490. @click=${() => {
  491. // Click on the next item
  492. const lineNumber = this.activeItemLineNumber + 1;
  493. const featureDiv = this.querySelector(`tr.lizmap-features-table-item[data-line-id="${lineNumber}"]`);
  494. if (featureDiv) featureDiv.click();
  495. }}></button>
  496. <button class="btn btn-mini close-popup"
  497. title="${lizDict['featuresTable.toolbar.close']}"
  498. @click=${() => {
  499. const checkFeatureId = (this.dataset.activeItemFeatureId) ?? "";
  500. if (!checkFeatureId) return;
  501. this.dataset.activeItemFeatureId = ""
  502. }}></button>
  503. </div>
  504. <table class="table table-sm table-bordered table-condensed lizmap-features-table-container">
  505. ${this.buildLabels()}
  506. <tbody>
  507. ${this.features.map((feature, idx) =>
  508. html`
  509. <tr
  510. class="lizmap-features-table-item ${this.openPopup ? 'has-action' : ''}"
  511. data-layer-id="${this.layerId}"
  512. data-feature-id="${feature.properties.feature_id}"
  513. data-line-id="${idx+1}"
  514. title="${this.openPopup ? lizDict['featuresTable.item.hover'] + '.': ''} ${this.itemsDraggable == 'yes' ? lizDict['featuresTable.item.draggable.hover'] + '.' : ''}"
  515. @click=${event => {
  516. this.onItemClick(event, feature.properties.feature_id);
  517. }}
  518. >
  519. ${this.buildColumns(feature.properties)}
  520. </tr>
  521. `
  522. )}
  523. </tbody>
  524. </table>
  525. <div class="lizmap-features-table-item-popup"></div>
  526. </div>
  527. `;
  528. // Load
  529. this.load();
  530. }
  531. /**
  532. * Build the columns of the table
  533. * @param {object} properties - Object containing the properties of the feature
  534. * @returns {TemplateResult} The columns of the table
  535. */
  536. buildColumns(properties) {
  537. let result = html`
  538. ${this.buildDisplayExpressionColumn(properties)}
  539. `;
  540. if (!this.isAdditionalFieldsEmpty()) {
  541. this.additionalFields.fields.forEach(field => {
  542. let td = html`
  543. <td
  544. class="lizmap-features-table-item"
  545. >
  546. ${properties[field.alias]}
  547. </td>
  548. `;
  549. result = html`
  550. ${result}
  551. ${td}
  552. `;
  553. });
  554. }
  555. return result;
  556. }
  557. /**
  558. * Initialize tab with the first column "display_expression"
  559. * @param {object} properties - Object containing the properties of the feature
  560. * @returns {TemplateResult} The first column of the table
  561. */
  562. buildDisplayExpressionColumn(properties) {
  563. if (this.isGeneralLabelExisting()) {
  564. return html`
  565. <td class="lizmap-features-table-item">
  566. ${properties.display_expression}
  567. </td>
  568. `;
  569. } else {
  570. return html``;
  571. }
  572. }
  573. /**
  574. * Initialize the labels of the table
  575. * @returns {TemplateResult} The labels of the table
  576. */
  577. buildLabels() {
  578. if (this.isAdditionalFieldsEmpty()) {
  579. return html``;
  580. }
  581. let result;
  582. this.additionalFields.fields.forEach(field => {
  583. let th = html`
  584. <th
  585. class="border lizmap-features-table-item"
  586. title="${(field.description) ? field.description : ''}"
  587. >
  588. ${atob(field.alias)}
  589. </th>
  590. `;
  591. result = html`
  592. ${result}
  593. ${th}
  594. `;
  595. });
  596. if (this.isGeneralLabelExisting()) {
  597. // First th to create an empty column for "display_expression"
  598. return html`
  599. <thead>
  600. <tr class="border-0">
  601. <th class="border-0 lizmap-features-table-item-empty"></th>
  602. ${result}
  603. </tr>
  604. </thead>
  605. `;
  606. } else {
  607. return html`
  608. <thead>
  609. <tr>
  610. ${result}
  611. </tr>
  612. </thead>
  613. `;
  614. }
  615. }
  616. /**
  617. * Check if the additionalFields property is empty
  618. * @returns {boolean} True if the additionalFields property is empty
  619. */
  620. isAdditionalFieldsEmpty() {
  621. return this.additionalFields.fields.length === 0;
  622. }
  623. /**
  624. * Check if the general label "display_expression" is existing
  625. * @returns {boolean} True if the general label "display_expression" is existing
  626. */
  627. isGeneralLabelExisting() {
  628. return this.features[0].properties.hasOwnProperty('display_expression');
  629. }
  630. /**
  631. * Verify if there's no fields with the same alias or expression
  632. * @param {Array.<object>} listField - List of fields
  633. * @returns {Array.<object>} - List of verified fields
  634. */
  635. verifyFields(listField) {
  636. let verifiedFields = [listField[0]];
  637. for (let i = 1; i < listField.length; i++) {
  638. const fieldAlias = listField[i].dataset.alias;
  639. const fieldExpression = listField[i].innerText;
  640. let isValid = true;
  641. verifiedFields.forEach(field => {
  642. if (field.innerText === fieldExpression) {
  643. listField[i].remove();
  644. isValid = false;
  645. } else if (field.dataset.alias === fieldAlias && field.dataset.alias !== "") {
  646. // Remove the field if the alias is already used but not when they are both empty because fields will be automatically different
  647. listField[i].remove();
  648. isValid = false;
  649. }
  650. })
  651. if (isValid) {
  652. verifiedFields.push(listField[i]);
  653. }
  654. }
  655. return verifiedFields;
  656. }
  657. static get observedAttributes() { return ['updated','expressionfilter', 'data-active-item-feature-id']; }
  658. attributeChangedCallback(name, oldValue, newValue) {
  659. // Listen to the change of the updated attribute
  660. // This will trigger the load (refresh the content)
  661. // Be aware that the name returned here is always lowercase
  662. if (name === 'updated') {
  663. // console.log('Reload features table');
  664. this.load();
  665. }
  666. // Also reload when the expressionFilter has changed
  667. if (name === 'expressionfilter') {
  668. // Prevent features table to load two time at its creation
  669. if (oldValue && newValue && oldValue != newValue) {
  670. // console.log('Reload the table with the new expressionFilter');
  671. this.expressionFilter = newValue;
  672. this.load();
  673. }
  674. }
  675. // Check for the data-active-item-feature-id
  676. if (name === 'data-active-item-feature-id') {
  677. if (oldValue != newValue) {
  678. // console.log(`Attribute data-active-item-feature-id changed, OLD = ${oldValue}, NEW = ${newValue}`);
  679. this.toggleFeatureDetail();
  680. }
  681. }
  682. }
  683. disconnectedCallback() {
  684. }
  685. }