From 19814d6d967a286eceb47643abcd292a5eba4106 Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 6 Oct 2025 11:49:58 +0200 Subject: [PATCH 01/73] ADD SearchEngineBase simple search engine + autocomplete --- .eslintrc | 2 +- build/webpack/controls.webpack.config.js | 1 + build/webpack/modules.webpack.config.js | 1 + ...s-ol-searchenginebase-modules-default.html | 61 +++ src/index.js | 1 + .../Controls/SearchEngine/GPFsearchEngine.css | 26 ++ .../Controls/SearchEngine/SearchEngineBase.js | 378 ++++++++++++++++++ src/packages/bundle.js | 3 + 8 files changed, 472 insertions(+), 1 deletion(-) create mode 100644 samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html create mode 100644 src/packages/Controls/SearchEngine/SearchEngineBase.js diff --git a/.eslintrc b/.eslintrc index 27a0c22c4..1d6921bbb 100644 --- a/.eslintrc +++ b/.eslintrc @@ -47,7 +47,7 @@ "no-prototype-builtins": "off", "linebreak-style": [ "error", - "unix" + "windows" ], "padded-blocks": [ "error", diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 99544a143..35cb02ca7 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -74,6 +74,7 @@ module.exports = (env, argv) => { // formats break; case "SearchEngine": + case "SearchEngineBase": // crs break; case "MeasureArea": diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 0cc800678..96658711f 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -45,6 +45,7 @@ module.exports = (env, argv) => { "GpfExtOlReverseGeocode" : path.join(rootdir, "src", "packages", "Controls/ReverseGeocode", "ReverseGeocode.js"), "GpfExtOlRoute" : path.join(rootdir, "src", "packages", "Controls/Route", "Route.js"), "GpfExtOlSearchEngine" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngine.js"), + "GpfExtOlSearchEngineBase" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineBase.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), "GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"), "GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html new file mode 100644 index 000000000..6dcc7dfd6 --- /dev/null +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html @@ -0,0 +1,61 @@ +{{#extend "ol-sample-modules-layout"}} + +{{#content "vendor"}} + + + +{{/content}} + +{{#content "head"}} + Sample openlayers SearchEngine +{{/content}} + +{{#content "style"}} + +{{/content}} + +{{#content "body"}} +

Ajout du moteur de recherche avec les options par défaut

+ +
+
+{{/content}} + +{{#content "js"}} + +{{/content}} + +{{/extend}} diff --git a/src/index.js b/src/index.js index 0c45507be..71a798674 100644 --- a/src/index.js +++ b/src/index.js @@ -24,6 +24,7 @@ export { default as LayerMapBox } from "./packages/Layers/LayerMapBox"; export { default as LayerSwitcher } from "./packages/Controls/LayerSwitcher/LayerSwitcher"; export { default as GetFeatureInfo } from "./packages/Controls/GetFeatureInfo/GetFeatureInfo"; export { default as SearchEngine } from "./packages/Controls/SearchEngine/SearchEngine"; +export { default as SearchEngineBase } from "./packages/Controls/SearchEngine/SearchEngineBase"; export { default as MousePosition } from "./packages/Controls/MousePosition/MousePosition"; export { default as Drawing } from "./packages/Controls/Drawing/Drawing"; export { default as Route } from "./packages/Controls/Route/Route"; diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index eb32455c5..5a94a08ee 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -187,3 +187,29 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } } + +[id^="GPsearchEngine"] ul { + position: absolute; + background-color: white; + width: 100%; + margin: 3px 0; + box-shadow: 0 0 6px #000; + list-style: none; + padding: 0; +} +[id^="GPsearchEngine"] ul li { + padding: 6px 10px; + color: #5E5E5E; +} +[id^="GPsearchEngine"] ul li.active, +[id^="GPsearchEngine"] ul li:hover { + color: #000000; + background-color: #CEDBEF; +} + +[id^=GPsearchEngine-].ol-collapsed form[id^=GPsearchInput-Base-] { + overflow: hidden; +} +[id^=GPsearchEngine-] form[id^=GPsearchInput-Base-] { + overflow: visible; +} diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js new file mode 100644 index 000000000..70521c71f --- /dev/null +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -0,0 +1,378 @@ +// import CSS +import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; +// import "../../CSS/Controls/SearchEngine/GPFsearchEngineStyle.css"; +// import OpenLayers +// import Control from "ol/control/Control"; +import Control from "../Control"; +import Widget from "../Widget"; +import Map from "ol/Map"; +import Overlay from "ol/Overlay"; +import { + transform as olProjTransform, + get as olProjGet, + transformExtent as olProjTransformExtent +} from "ol/proj"; +import GeoJSON from "ol/format/GeoJSON"; +// import geoportal library access +import Gp from "geoportal-access-lib"; +// import local +import Config from "../../Utils/Config"; +import Logger from "../../Utils/LoggerByDefault"; +import Utils from "../../Utils/Helper"; +import Markers from "../Utils/Markers"; +import Interactions from "../Utils/Interactions"; +import SelectorID from "../../Utils/SelectorID"; +import MathUtils from "../../Utils/MathUtils"; +import SearchEngineUtils from "../../Utils/SearchEngineUtils"; +import GeocodeUtils from "../../Utils/GeocodeUtils"; +import CRS from "../../CRS/CRS"; +// import local des layers +import GeoportalWMS from "../../Layers/LayerWMS"; +import GeoportalWMTS from "../../Layers/LayerWMTS"; +import GeoportalWFS from "../../Layers/LayerWFS"; +import GeoportalMapBox from "../../Layers/LayerMapBox"; +// Service +import Search from "../../Services/Search"; +// DOM +import SearchEngineDOM from "./SearchEngineDOM"; +import checkDsfr from "../Utils/CheckDsfr"; +import { getUid } from "ol"; + +var logger = Logger.getLogger("searchengine"); + +/** + * @typedef {Object} SearchEngineOptions + * @property {number} [id] - Identifiant du widget (option avancée) + * @property {string} [apiKey] - Clé API. "calcul" par défaut. + * @property {boolean} [ssl=true] - Utilisation du protocole https (true par défaut) + * @property {boolean} [collapsed=true] - Mode réduit (true par défaut) + * @property {boolean} [collapsible=true] - Contrôle pliable ou non (true par défaut) + * @property {string} [direction="start"] - Position du picto (loupe), "start" par défaut + * @property {string} [placeholder="Rechercher un lieu, une adresse"] - Placeholder de la barre de recherche + * @property {boolean} [displayMarker=true] - Afficher un marqueur sur le résultat (true par défaut) + * @property {string} [markerStyle="lightOrange"] - Style du marqueur ("lightOrange", "darkOrange", "red", "turquoiseBlue") + * @property {string} [markerUrl=""] - URL du marqueur (prioritaire sur markerStyle) + * @property {boolean} [splitResults=false] - Désactiver la recherche par couches (false par défaut) + * @property {boolean} [displayButtonAdvancedSearch=false] - Afficher le bouton de recherche avancée (false par défaut) + * @property {boolean} [displayButtonGeolocate=false] - Afficher le bouton de géolocalisation (false par défaut) + * @property {boolean} [displayButtonCoordinateSearch=false] - Afficher le bouton de recherche par coordonnées (false par défaut) + * @property {boolean} [coordinateSearchInAdvancedSearch=false] - Afficher la recherche par coordonnées dans la recherche avancée + * @property {boolean} [displayButtonClose=true] - Afficher le bouton de fermeture (true par défaut) + * @property {Object} [coordinateSearch] - Options de recherche par coordonnées + * @property {HTMLElement} [coordinateSearch.target=null] - Cible d'affichage des résultats + * @property {Array} [coordinateSearch.units] - Unités de coordonnées à afficher ("DEC", "DMS", "M", "KM") + * Values may be "DEC" (decimal degrees), "DMS" (sexagecimal) for geographical coordinates, + * and "M" or "KM" for metric coordinates + * @property {Array} [coordinateSearch.systems] - Systèmes de projection à afficher (objet avec crs, label, type) + * @property {Object} [advancedSearch] - Options de recherche avancée (voir geocodeOptions.filterOptions) + * @property {HTMLElement} [advancedSearch.target=null] - Cible d'affichage des résultats + * @property {Object} [resources] - Ressources utilisées par les services + * @property {string|string[]} [resources.geocode="location"] - Ressources de géocodage + * @property {string[]} [resources.autocomplete] - Ressources d'autocomplétion + * @property {boolean} [resources.search=false] - Activer le service de recherche (false par défaut) + * @property {Object} [searchOptions={}] - Options du service de recherche + * @property {boolean} [searchOptions.addToMap=true] - Ajouter la couche automatiquement à la carte + * @property {string[]} [searchOptions.filterServices] - Filtrer sur une liste de services ("WMTS,TMS" par défaut) + * @property {string[]} [searchOptions.filterWMTSPriority] - Filtrer sur les couches WMTS prioritaires + * @property {string[]} [searchOptions.filterProjections] - Filtrer sur une liste de projections + * @property {boolean} [searchOptions.filterLayersPriority=false] - Filtrer sur les couches prioritaires + * @property {boolean} [searchOptions.filterLayers=true] - Activer le filtrage automatique des couches + * @property {Object} [searchOptions.filterLayersList] - Liste des couches à filtrer {"layerName": "service"} + * @property {boolean} [searchOptions.filterTMS=true] - Garder les TMS avec style dans les métadonnées + * @property {Object} [searchOptions.serviceOptions] - Options du service de recherche + * @property {string} [searchOptions.serviceOptions.url] - URL du service + * @property {string} [searchOptions.serviceOptions.index="standard"] - Index de recherche + * @property {string[]} [searchOptions.serviceOptions.fields=["title","layer_name"]] - Champs de recherche + * @property {number} [searchOptions.serviceOptions.size=1000] - Nombre de réponses du service + * @property {number} [searchOptions.serviceOptions.maximumResponses=10] - Nombre de résultats à afficher + * @property {number} [searchOptions.maximumEntries] - Nombre maximum de résultats à afficher + * @property {Object} [geocodeOptions={}] - Options du service de géocodage (voir Gp.Services.geocode {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~geocode Gp.Services.geocode})) + * @property {Object} [geocodeOptions.serviceOptions] - Options du service de géocodage + * @property {Object} [autocompleteOptions={}] - Options du service d'autocomplétion (voir Gp.Services.autoComplete {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~autoComplete Gp.Services.autoComplete}) + * @property {Object} [autocompleteOptions.serviceOptions] - Options du service d'autocomplétion + * @property {boolean} [autocompleteOptions.triggerGeocode=false] - Déclencher une requête de géocodage si aucune suggestion + * @property {number} [autocompleteOptions.triggerDelay=1000] - Délai avant la requête de géocodage (ms) + * @property {number} [autocompleteOptions.maximumEntries] - Nombre maximum de résultats d'autocomplétion à afficher + * @property {boolean} [autocompleteOptions.prettifyResults=false] - Nettoyer/embellir les résultats d'autocomplétion + * @property {string|number|Function} [zoomTo] - Niveau de zoom à appliquer sur le résultat ("auto", niveau, ou fonction) + * Value possible : auto or zoom level. + * Possible to overload it with a function : + * zoomTo : function (info) { + * // do some stuff... + * return zoom; + * } + */ + +/** + * @classdesc + * SearchEngine Base control + * + * @alias ol.control.SearchEngineBase + * @module SearchEngine +*/ +class SearchEngineBase extends Control { + + constructor (options) { + options = options || {}; + // call ol.control.Control constructor + super(options); + + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "SearchEngineBase"; + + // initialisation du composant + this.initialize(options); + + // Widget main DOM container + this._initContainer(options); + + this._initEvents(options); + + // Get historic in localStorage + this._historic = false; + this._historicName = "GPsearch-" + (typeof options.historic === "string" ? options.historic : this.CLASSNAME); + if (options.historic !== false) { + this._historic = []; + try { + const stor = window.localStorage.getItem(this._historicName); + if (stor) { + this._historic = JSON.parse(stor); + } + } catch (e) { + // logger.warn("LocalStorage not available"); + } + } + this.showHistoric(); + + return this; + } + /** + * Initialize SearchEngine control (called by SearchEngine constructor) + * + * @param {Object} options - constructor options + * @private + */ + initialize (options) { + + } + /** Add event listeners + * @param {Object} options - constructor options + * @private + */ + _initEvents (options) { + this.input.addEventListener("input", function (e) { + }.bind(this)); + this.input.addEventListener("keydown", function (e) { + if (/ArrowDown|ArrowUp/.test(e.key)) { + e.preventDefault(); + } + }.bind(this)); + this.input.addEventListener("keyup", function (e) { + if (/ArrowDown|ArrowUp/.test(e.key)) { + e.preventDefault(); + // Navigation in autocomplete list + const list = Array.from(this.autocompleteList.querySelectorAll("li")); + if (list.length === 0) { + return; + } + let idx = list.findIndex(li => li.classList.contains("active")); + list.forEach(li => li.classList.remove("active")); + if (e.key === "ArrowDown") { + idx++; + if (idx >= list.length) { + idx = 0; + } + } else if (e.key === "ArrowUp") { + idx--; + if (idx < 0) { + idx = list.length - 1; + } + } + // Set active + const current = list[idx]; + current.classList.add("active"); + this.input.value = current.innerText; + this.input.setAttribute("aria-activedescendant", current.id); + this.input.setAttribute("data-active-option", current.id); + } else if ( + (e.target.value.length && e.target.value.length >= (options.minChars || 0)) + || (e.key === "Enter") + ) { + // Autocomplete + this.autocomplete(e.target.value, e.key === "Enter"); + } else { + // Show historic + this.showHistoric(); + } + }.bind(this), false); + } + /** + * + * @param {*} options + */ + _initContainer (options) { + const element = this.element = document.createElement("div"); + element.className = "GPwidget gpf-widget ol-collapsed"; + element.id = "GPsearchEngine-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + // Add button if no target + if (!options.target) { + this.button = document.createElement("button"); + this.button.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn gpf-btn-icon-search fr-btn"; + this.button.setAttribute("aria-pressed", "false"); + this.button.setAttribute("type", "button"); + this.button.setAttribute("title", options.title || options.label || "Search"); + this.button.addEventListener("click", function () { + element.classList.toggle("ol-collapsed"); + const pressed = this.button.getAttribute("aria-pressed") === "true"; + this.button.setAttribute("aria-pressed", !pressed); + if (!pressed) { + input.focus(); + } else { + input.blur(); + } + }.bind(this)); + element.appendChild(this.button); + } + const container = document.createElement("form"); + container.className = "gpf-panel__content fr-modal__content"; + container.id = "GPsearchInput-Base-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + container.addEventListener("submit", function (e) { + e.preventDefault(); + return false; + }); + element.appendChild(container); + + + // Input + const input = this.input = document.createElement("input"); + input.type = "text"; + input.className = "GPsearchInputText gpf-input fr-input"; + input.id = "GPsearchInputText-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + input.placeholder = options.placeholder || "Rechercher..."; + input.autocomplete = "off"; + input.setAttribute("aria-label", options.ariaLabel || "Rechercher"); + container.appendChild(input); + + // Autocomplete container + const autocompleteList = this.autocompleteList = document.createElement("ul"); + autocompleteList.className = "GPautoCompleteList GPelementHidden gpf-panel fr-modal gpf-hidden"; + autocompleteList.id = "GPautocompleteList-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + autocompleteList.setAttribute("role", "listbox"); + autocompleteList.setAttribute("tabindex", "-1"); + autocompleteList.setAttribute("aria-label", "Propositions"); + container.appendChild(autocompleteList); + + // Input controller for accessibility + input.setAttribute("role", "combobox"); + input.setAttribute("aria-controls", autocompleteList.id); + input.setAttribute("aria-expanded", "false"); + input.setAttribute("aria-autocomplete", "list"); + input.setAttribute("aria-haspopup", "listbox"); + + input.addEventListener("focus", function () { + input.setAttribute("aria-expanded", "true"); + autocompleteList.classList.add("gpf-visible"); + autocompleteList.classList.remove("gpf-hidden"); + autocompleteList.classList.add("GPelementVisible"); + autocompleteList.classList.remove("GPelementHidden"); + }.bind(this)); + input.addEventListener("focusout", function () { + setTimeout(function () { + input.setAttribute("aria-expanded", "false"); + autocompleteList.classList.remove("gpf-visible"); + autocompleteList.classList.add("gpf-hidden"); + autocompleteList.classList.remove("GPelementVisible"); + autocompleteList.classList.add("GPelementHidden"); + }, 100); + }.bind(this)); + } + /** Autocomplete and update list + * @param {string} [value] input value + * @param {boolean} [force=false] force to add in historic + * @api + */ + autocomplete (value, force) { + if (force && value) { + this._updateHistoric(value); + } + this._updateList(); + } + /** + * Show historic list + * @api + */ + showHistoric () { + if (this._historic) { + this._updateList(this._historic.length ? this._historic : []); + } + } + /** + * Update autocomplete list + * @param {Array<*>} tab list of autocomplete items + */ + _updateList (tab) { + tab = tab || []; + // Accessibility + this.autocompleteList.querySelectorAll("li").forEach(li => li.classList.remove("active")); + this.input.setAttribute("aria-activedescendant", ""); + this.input.setAttribute("data-active-option", ""); + // Update list + this.autocompleteList.innerHTML = ""; + tab.forEach((item, idx) => { + const li = document.createElement("li"); + li.id = "GPsearchHistoric-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)) + "-" + idx; + li.className = "GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine"; + li.setAttribute("role", "option"); + li.setAttribute("data-idx", idx); + li.innerHTML = this.getItemTitle(item); + this.autocompleteList.appendChild(li); + li.addEventListener("click", function (e) { + const idx = Number(e.target.getAttribute("data-idx")); + this.input.value = this.getItemTitle(tab[idx]); + this._updateHistoric(this.input.value); + this._updateList(); + }.bind(this)); + }); + } + /** + * * @param {*} item + * @returns + */ + getItemTitle (item) { + return item; + } + /** + * Add or replace value in historic list + * @param {*} value + */ + _updateHistoric (value) { + if (this._historic) { + // Update historic + const idx = this._historic.indexOf(value); + if (idx !== -1) { + this._historic.splice(idx, 1); + } + this._historic.unshift(value); + if (this._historic.length > 10) { + this._historic.pop(); + } + localStorage.setItem(this._historicName, JSON.stringify(this._historic)); + } + } + +} + +export default SearchEngineBase; + +// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +if (window.ol && window.ol.control) { + /**/ + window.ol.control.SearchEngine = SearchEngineBase; + /*/ + window.ol.control.SearchEngine = SearchEngine; + /**/ +} diff --git a/src/packages/bundle.js b/src/packages/bundle.js index 5e0524109..f9dfab380 100644 --- a/src/packages/bundle.js +++ b/src/packages/bundle.js @@ -79,6 +79,7 @@ import LayerMapBox from "./Layers/LayerMapBox"; import LayerSwitcher from "./Controls/LayerSwitcher/LayerSwitcher"; import GetFeatureInfo from "./Controls/GetFeatureInfo/GetFeatureInfo"; import SearchEngine from "./Controls/SearchEngine/SearchEngine"; +import SearchEngineBase from "./Controls/SearchEngine/SearchEngineBase"; import MousePosition from "./Controls/MousePosition/MousePosition"; import Drawing from "./Controls/Drawing/Drawing"; import Route from "./Controls/Route/Route"; @@ -268,6 +269,7 @@ Ol.control.LayerSwitcher = LayerSwitcher; Ol.control.GeoportalAttribution = GeoportalAttribution; Ol.control.GetFeatureInfo = GetFeatureInfo; Ol.control.SearchEngine = SearchEngine; +Ol.control.SearchEngineBase = SearchEngineBase; Ol.control.Route = Route; Ol.control.Isocurve = Isocurve; Ol.control.GeoportalMousePosition = MousePosition; @@ -298,6 +300,7 @@ export { * @see ol.control.GeoportalAttribution * @see ol.control.GetFeatureInfo * @see ol.control.SearchEngine + * @see ol.control.SearchEngineBase * @see ol.control.Route * @see ol.control.Isocurve * @see ol.control.GeoportalMousePosition From da490c962d5e8659aceff9484e01eb82bd40a8ef Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 6 Oct 2025 16:47:17 +0200 Subject: [PATCH 02/73] Add Search service / default base class --- ...s-ol-searchenginebase-modules-default.html | 25 +++++- .../Controls/SearchEngine/GPFsearchEngine.css | 2 + .../Controls/SearchEngine/SearchEngineBase.js | 89 ++++++++++++++----- src/packages/Services/SearchServiceBase.js | 67 ++++++++++++++ 4 files changed, 161 insertions(+), 22 deletions(-) create mode 100644 src/packages/Services/SearchServiceBase.js diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html index 6dcc7dfd6..0c2ad8062 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html @@ -24,6 +24,7 @@

Ajout du moteur de recherche avec les options par défaut

+

Dernière sélection :

{{/content}} {{#content "js"}} @@ -50,10 +51,32 @@

Ajout du moteur de recherche avec les options par défaut

}); // 2. Appel du SearchEngine - var search = new ol.control.SearchEngine({}); + var search = new ol.control.SearchEngineBase({ + searchService: new ol.service.SearchServiceBase({ + searchTab : [ + "Paris", "Joinville-le-Pont", + "Créteil", "Maisons-Alfort", "Alfortville", "Ivry-sur-Seine", "Charenton-le-Pont", + "Saint-Maurice", "Nogent-sur-Marne", "Le Perreux-sur-Marne", "Neuilly-Plaisance", + "Bry-sur-Marne", "Villiers-sur-Marne", "Champigny-sur-Marne", "Chennevières-sur-Marne", + "Ormesson-sur-Marne", "Santeny", "Limeil-Brévannes", "Valenton", "Villecresnes", + "Marolles-en-Brie", "La Queue-en-Brie", "Boissy-Saint-Léger", "Sucy-en-Brie", + "Bonneuil-sur-Marne", "Crécy-la-Chapelle", "Chelles", "Gournay-sur-Marne", + "Le Raincy", "Clichy-sous-Bois", "Montfermeil", "Vaujours", "Tremblay-en-France", + "Coubron", "Aulnay-sous-Bois", "Sevran", "Livry-Gargan", "Clichy-sous-Bois", + "Montfermeil", "Gagny", "Neuilly-sur-Marne", "Noisy-le-Grand", "Noisy-le-Sec", + "Romainville", "Les Lilas", "Bagnolet", "Montreuil", "Vincennes", "Fontenay-sous-Bois" + ] + }) + }); // 3. Ajout du SearchEngine à la carte map.addControl(search); + + // Get search info + search.on('select', e => { + console.log("Select", e) + document.getElementById("selection").innerHTML = e.title; + }) }; {{/content}} diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 5a94a08ee..b664382c5 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -196,10 +196,12 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ box-shadow: 0 0 6px #000; list-style: none; padding: 0; + overflow: hidden; } [id^="GPsearchEngine"] ul li { padding: 6px 10px; color: #5E5E5E; + white-space: nowrap; } [id^="GPsearchEngine"] ul li.active, [id^="GPsearchEngine"] ul li:hover { diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 70521c71f..b7b89169d 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -3,6 +3,7 @@ import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; // import "../../CSS/Controls/SearchEngine/GPFsearchEngineStyle.css"; // import OpenLayers // import Control from "ol/control/Control"; +import OlObject from "ol/Object"; import Control from "../Control"; import Widget from "../Widget"; import Map from "ol/Map"; @@ -33,11 +34,13 @@ import GeoportalWFS from "../../Layers/LayerWFS"; import GeoportalMapBox from "../../Layers/LayerMapBox"; // Service import Search from "../../Services/Search"; +import DefaultSearchService from "../../Services/SearchServiceBase"; // DOM import SearchEngineDOM from "./SearchEngineDOM"; import checkDsfr from "../Utils/CheckDsfr"; import { getUid } from "ol"; + var logger = Logger.getLogger("searchengine"); /** @@ -103,6 +106,7 @@ var logger = Logger.getLogger("searchengine"); * } */ + /** * @classdesc * SearchEngine Base control @@ -123,6 +127,11 @@ class SearchEngineBase extends Control { */ this.CLASSNAME = "SearchEngineBase"; + this.searchService = options.searchService || new DefaultSearchService(); + this.searchService.on("search", function (e) { + this.onSearch(e); + }.bind(this)); + // initialisation du composant this.initialize(options); @@ -171,14 +180,16 @@ class SearchEngineBase extends Control { } }.bind(this)); this.input.addEventListener("keyup", function (e) { + // autocomplete list + const list = Array.from(this.autocompleteList.querySelectorAll("li")); + let idx = list.findIndex(li => li.classList.contains("active")); + // Handle key events if (/ArrowDown|ArrowUp/.test(e.key)) { e.preventDefault(); // Navigation in autocomplete list - const list = Array.from(this.autocompleteList.querySelectorAll("li")); if (list.length === 0) { return; } - let idx = list.findIndex(li => li.classList.contains("active")); list.forEach(li => li.classList.remove("active")); if (e.key === "ArrowDown") { idx++; @@ -201,12 +212,20 @@ class SearchEngineBase extends Control { (e.target.value.length && e.target.value.length >= (options.minChars || 0)) || (e.key === "Enter") ) { - // Autocomplete - this.autocomplete(e.target.value, e.key === "Enter"); + if (idx >= 0) { + // An item has been selected + list[idx].click(); + } else { + // Autocomplete + if (e.target.value !== this._currentValue) { + this.autocomplete(e.target.value, e.key === "Enter"); + } + } } else { // Show historic this.showHistoric(); } + this._currentValue = e.target.value; }.bind(this), false); } /** @@ -296,16 +315,46 @@ class SearchEngineBase extends Control { * @api */ autocomplete (value, force) { - if (force && value) { - this._updateHistoric(value); - } + clearTimeout(this._completeDelay); + this._completeDelay = setTimeout(function () { + this.searchService.autocomplete(value, { force : force }); + }.bind(this), this.get("triggerDelay") || 300); + } + /** Do something on search ready + * @param {Object} e event + * @param {string} e.search search string + * @param {Object|boolean} e.options options given to autocomplete + * @param {Array<*>} e.result result of autocomplete + * @api + */ + onSearch (e) { + clearTimeout(this._completeDelay); + // Update list} + this._updateList(e.result); + } + /** An item has been selected + * @param {*} item selected item + * @api + */ + select (item) { + clearTimeout(this._completeDelay); + const title = this.getItemTitle(item); + this.input.value = title; + this._currentValue = title; + this._updateHistoric(title); this._updateList(); + this.dispatchEvent({ + type : "select", + title : this.getItemTitle(item), + item : item + }); } /** * Show historic list * @api */ showHistoric () { + clearTimeout(this._completeDelay); if (this._historic) { this._updateList(this._historic.length ? this._historic : []); } @@ -315,7 +364,7 @@ class SearchEngineBase extends Control { * @param {Array<*>} tab list of autocomplete items */ _updateList (tab) { - tab = tab || []; + tab = (tab || []).slice(0, this.get("maximumEntries") || 10); // Accessibility this.autocompleteList.querySelectorAll("li").forEach(li => li.classList.remove("active")); this.input.setAttribute("aria-activedescendant", ""); @@ -332,18 +381,17 @@ class SearchEngineBase extends Control { this.autocompleteList.appendChild(li); li.addEventListener("click", function (e) { const idx = Number(e.target.getAttribute("data-idx")); - this.input.value = this.getItemTitle(tab[idx]); - this._updateHistoric(this.input.value); - this._updateList(); + this.select(tab[idx]); }.bind(this)); }); } - /** - * * @param {*} item - * @returns + /** Get item title given an item object + * @param {*} item + * @returns {string} title + * @api */ getItemTitle (item) { - return item; + return this.searchService.getItemTitle(item); } /** * Add or replace value in historic list @@ -357,9 +405,12 @@ class SearchEngineBase extends Control { this._historic.splice(idx, 1); } this._historic.unshift(value); - if (this._historic.length > 10) { + // Remove last if > 10 + if (this._historic.length > (this.get("maximumEntries") || 10)) { this._historic.pop(); } + + // Save in localStorage localStorage.setItem(this._historicName, JSON.stringify(this._historic)); } } @@ -370,9 +421,5 @@ export default SearchEngineBase; // Expose SearchEngine as ol.control.SearchEngine (for a build bundle) if (window.ol && window.ol.control) { - /**/ - window.ol.control.SearchEngine = SearchEngineBase; - /*/ - window.ol.control.SearchEngine = SearchEngine; - /**/ + window.ol.control.SearchEngineBase = SearchEngineBase; } diff --git a/src/packages/Services/SearchServiceBase.js b/src/packages/Services/SearchServiceBase.js new file mode 100644 index 000000000..c2d9eac52 --- /dev/null +++ b/src/packages/Services/SearchServiceBase.js @@ -0,0 +1,67 @@ +import OlObject from "ol/Object"; + +/** Base class for search services + * + */ +class SearchServiceBase extends OlObject { + + constructor (options) { + super(); + options = options || {}; + if (options.searchTab) { + this._searchTab = options.searchTab || []; + }; + } + + /** Autocomplete function + * Dispatchs "searchstart" event when search starts + * Dispatchs "search" event when search is finished + * @param {string} search + * @param {Object} [options] + * @param {string} options.force force search even if search string is less than minChars / enter is pressed + * @api + */ + autocomplete (search, options) { + // Search has started + this.dispatchEvent({ + type : "searchstart", + search : search, + options : options, + }); + // Simulate asynchronous behavior + setTimeout(function () { + const result = []; + const rex = new RegExp(search, "i"); + (this._searchTab || []).forEach((city) => { + if (rex.test(city.toLowerCase())) { + result.push(city); + } + }); + // When search is finished + this.dispatchEvent({ + type : "search", + search : search, + options : options, + result : result + }); + }.bind(this), 200); + } + /** Get title of an item + * @param {*} item + * @returns {string} title + */ + getItemTitle (item) { + return item; + } + +} + +export default SearchServiceBase; + +// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +if (window.ol) { + if (!window.ol.service) { + window.ol.service = {}; + } + window.ol.service.SearchServiceBase = SearchServiceBase; +} From 0482fac5a33b8035d8a85a779fbb96adeaf9c59f Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 8 Oct 2025 16:10:41 +0200 Subject: [PATCH 03/73] =?UTF-8?q?feat(search):=20Ajout=20service=20g=C3=A9?= =?UTF-8?q?ocodage=20IGN=20+=20contr=C3=B4le=20li=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .eslintrc | 5 +- build/webpack/controls.webpack.config.js | 1 + build/webpack/modules.webpack.config.js | 1 + ...rchenginebase-modules-dsfr-geocodeIGN.html | 79 ++ ...l-searchenginebase-modules-geocodeIGN.html | 83 +++ .../Controls/SearchEngine/SearchEngineBase.js | 46 +- .../SearchEngine/SearchEngineGeocodeIGN.js | 241 ++++++ src/packages/Controls/SearchEngine/Service.js | 699 ++++++++++++++++++ .../Controls/SearchEngine/map-pin-2-fill.svg | 1 + src/packages/Services/SearchServiceBase.js | 6 +- 10 files changed, 1144 insertions(+), 18 deletions(-) create mode 100644 samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html create mode 100644 samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html create mode 100644 src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js create mode 100644 src/packages/Controls/SearchEngine/Service.js create mode 100644 src/packages/Controls/SearchEngine/map-pin-2-fill.svg diff --git a/.eslintrc b/.eslintrc index 1d6921bbb..783ae46d9 100644 --- a/.eslintrc +++ b/.eslintrc @@ -45,10 +45,7 @@ "no-console": "off", "no-proto": "off", "no-prototype-builtins": "off", - "linebreak-style": [ - "error", - "windows" - ], + "linebreak-style": "off", "padded-blocks": [ "error", { diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 35cb02ca7..5126d532b 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -75,6 +75,7 @@ module.exports = (env, argv) => { break; case "SearchEngine": case "SearchEngineBase": + case "SearchEngineGeocodeIGN": // crs break; case "MeasureArea": diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 96658711f..809dfaa64 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -46,6 +46,7 @@ module.exports = (env, argv) => { "GpfExtOlRoute" : path.join(rootdir, "src", "packages", "Controls/Route", "Route.js"), "GpfExtOlSearchEngine" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngine.js"), "GpfExtOlSearchEngineBase" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineBase.js"), + "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), "GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"), "GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html new file mode 100644 index 000000000..53f08fe21 --- /dev/null +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeIGN.html @@ -0,0 +1,79 @@ +{{#extend "ol-sample-modules-dsfr-layout"}} + +{{#content "vendor"}} + + + +{{/content}} + +{{#content "head"}} + Sample openlayers SearchEngine +{{/content}} + +{{#content "style"}} + +{{/content}} + +{{#content "body"}} +

Ajout du moteur de recherche avec les options par défaut

+ +
+
+

Dernière sélection :

+{{/content}} + +{{#content "js"}} + +{{/content}} + +{{/extend}} diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html new file mode 100644 index 000000000..8fa33d5a8 --- /dev/null +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html @@ -0,0 +1,83 @@ +{{#extend "ol-sample-modules-layout"}} + +{{#content "vendor"}} + + + +{{/content}} + +{{#content "head"}} + Sample openlayers SearchEngine +{{/content}} + +{{#content "style"}} + +{{/content}} + +{{#content "body"}} +

Ajout du moteur de recherche avec les options par défaut

+ +
+
+

Dernière sélection :

+{{/content}} + +{{#content "js"}} + +{{/content}} + +{{/extend}} diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index b7b89169d..8e7b2e1d2 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -128,6 +128,10 @@ class SearchEngineBase extends Control { this.CLASSNAME = "SearchEngineBase"; this.searchService = options.searchService || new DefaultSearchService(); + this.searchService.on("autocomplete", function (e) { + this.onAutocomplete(e); + }.bind(this)); + this.searchService.on("search", function (e) { this.onSearch(e); }.bind(this)); @@ -162,14 +166,14 @@ class SearchEngineBase extends Control { * Initialize SearchEngine control (called by SearchEngine constructor) * * @param {Object} options - constructor options - * @private + * @protected */ initialize (options) { } /** Add event listeners * @param {Object} options - constructor options - * @private + * @protected */ _initEvents (options) { this.input.addEventListener("input", function (e) { @@ -213,7 +217,7 @@ class SearchEngineBase extends Control { || (e.key === "Enter") ) { if (idx >= 0) { - // An item has been selected + // An item has been selected list[idx].click(); } else { // Autocomplete @@ -310,27 +314,45 @@ class SearchEngineBase extends Control { }.bind(this)); } /** Autocomplete and update list - * @param {string} [value] input value - * @param {boolean} [force=false] force to add in historic + * @param {String} [value] input value + * @param {Boolean} [force=false] force to add in historic * @api */ autocomplete (value, force) { clearTimeout(this._completeDelay); this._completeDelay = setTimeout(function () { this.searchService.autocomplete(value, { force : force }); - }.bind(this), this.get("triggerDelay") || 300); + }.bind(this), this.get("triggerDelay") || 100); + } + + onAutocomplete (e) { + clearTimeout(this._completeDelay); + // Update list} + this._updateList(e.result); + } + + /** Effectue la recherche de géocodage + * @param {String} [value] input value + * @api + */ + search (idx) { + clearTimeout(this._completeDelay); + console.log(idx); + this._completeDelay = setTimeout(function () { + this.searchService.search(idx); + }.bind(this), this.get("triggerDelay") || 100); } /** Do something on search ready * @param {Object} e event - * @param {string} e.search search string - * @param {Object|boolean} e.options options given to autocomplete + * @param {String} e.search search string + * @param {Object|Boolean} e.options options given to autocomplete * @param {Array<*>} e.result result of autocomplete * @api */ onSearch (e) { clearTimeout(this._completeDelay); // Update list} - this._updateList(e.result); + this.dispatchEvent(e); } /** An item has been selected * @param {*} item selected item @@ -341,7 +363,7 @@ class SearchEngineBase extends Control { const title = this.getItemTitle(item); this.input.value = title; this._currentValue = title; - this._updateHistoric(title); + this._updateHistoric(item); this._updateList(); this.dispatchEvent({ type : "select", @@ -356,6 +378,7 @@ class SearchEngineBase extends Control { showHistoric () { clearTimeout(this._completeDelay); if (this._historic) { + console.log(this._historic); this._updateList(this._historic.length ? this._historic : []); } } @@ -382,12 +405,13 @@ class SearchEngineBase extends Control { li.addEventListener("click", function (e) { const idx = Number(e.target.getAttribute("data-idx")); this.select(tab[idx]); + this.search(idx); }.bind(this)); }); } /** Get item title given an item object * @param {*} item - * @returns {string} title + * @returns {String} title * @api */ getItemTitle (item) { diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js new file mode 100644 index 000000000..2b0893aad --- /dev/null +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -0,0 +1,241 @@ +// import CSS +import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; +// import "../../CSS/Controls/SearchEngine/GPFsearchEngineStyle.css"; +// import OpenLayers +// import Control from "ol/control/Control"; +import OlObject from "ol/Object"; +import Control from "../Control"; +import Widget from "../Widget"; +import Map from "ol/Map"; +import Overlay from "ol/Overlay"; +import { + transform as olProjTransform, + get as olProjGet, + transformExtent as olProjTransformExtent +} from "ol/proj"; +import GeoJSON from "ol/format/GeoJSON"; +// import geoportal library access +import Gp from "geoportal-access-lib"; +// import local +import Config from "../../Utils/Config"; +import Logger from "../../Utils/LoggerByDefault"; +import Utils from "../../Utils/Helper"; +import Markers from "../Utils/Markers"; +import Interactions from "../Utils/Interactions"; +import SelectorID from "../../Utils/SelectorID"; +import MathUtils from "../../Utils/MathUtils"; +import SearchEngineUtils from "../../Utils/SearchEngineUtils"; +import GeocodeUtils from "../../Utils/GeocodeUtils"; +import CRS from "../../CRS/CRS"; +// import local des layers +import GeoportalWMS from "../../Layers/LayerWMS"; +import GeoportalWMTS from "../../Layers/LayerWMTS"; +import GeoportalWFS from "../../Layers/LayerWFS"; +import GeoportalMapBox from "../../Layers/LayerMapBox"; +// Service +import Search from "../../Services/Search"; +import DefaultSearchService from "../../Services/SearchServiceBase"; +import SearchEngineBase from "./SearchEngineBase"; +import { AbstractService, GeocodeIGNService } from "./Service"; +// DOM +import SearchEngineDOM from "./SearchEngineDOM"; +import checkDsfr from "../Utils/CheckDsfr"; +import { getUid } from "ol"; +import { Layer, Vector } from "ol/layer"; +import VectorSource from "ol/source/Vector"; + +import { Style, Icon, Stroke, Fill, Circle as CircleStyle } from "ol/style"; +const color = "#000091"; + +const createStyle = (feature) => { + const geometryType = feature.getGeometry().getType(); + + switch (geometryType) { + case "Point": + case "MultiPoint": + return new Style({ + image : new Icon({ + src : "/src/packages/Controls/SearchEngine/map-pin-2-fill.svg", + scale : 0.2, + // color : color, + }), + }); + + case "LineString": + case "MultiLineString": + return new Style({ + stroke : new Stroke({ + color : color, + width : 3, + }), + }); + + case "Polygon": + case "MultiPolygon": + return new Style({ + stroke : new Stroke({ + color : color, + lineDash : [8, 8], + width : 2, + }), + fill : new Fill({ + color : "rgba(0, 0, 0, 0.1)", + opacity : 0.8 + }), + }); + + default: + return new Style(); + } +}; + +var logger = Logger.getLogger("searchengine"); + +/** + * @typedef {Object} SearchEngineOptions + * @property {number} [id] - Identifiant du widget (option avancée) + * @property {string} [apiKey] - Clé API. "calcul" par défaut. + * @property {boolean} [ssl=true] - Utilisation du protocole https (true par défaut) + * @property {boolean} [collapsed=true] - Mode réduit (true par défaut) + * @property {boolean} [collapsible=true] - Contrôle pliable ou non (true par défaut) + * @property {string} [direction="start"] - Position du picto (loupe), "start" par défaut + * @property {string} [placeholder="Rechercher un lieu, une adresse"] - Placeholder de la barre de recherche + * @property {boolean} [displayMarker=true] - Afficher un marqueur sur le résultat (true par défaut) + * @property {string} [markerStyle="lightOrange"] - Style du marqueur ("lightOrange", "darkOrange", "red", "turquoiseBlue") + * @property {string} [markerUrl=""] - URL du marqueur (prioritaire sur markerStyle) + * @property {boolean} [splitResults=false] - Désactiver la recherche par couches (false par défaut) + * @property {boolean} [displayButtonAdvancedSearch=false] - Afficher le bouton de recherche avancée (false par défaut) + * @property {boolean} [displayButtonGeolocate=false] - Afficher le bouton de géolocalisation (false par défaut) + * @property {boolean} [displayButtonCoordinateSearch=false] - Afficher le bouton de recherche par coordonnées (false par défaut) + * @property {boolean} [coordinateSearchInAdvancedSearch=false] - Afficher la recherche par coordonnées dans la recherche avancée + * @property {boolean} [displayButtonClose=true] - Afficher le bouton de fermeture (true par défaut) + * @property {Object} [coordinateSearch] - Options de recherche par coordonnées + * @property {HTMLElement} [coordinateSearch.target=null] - Cible d'affichage des résultats + * @property {Array} [coordinateSearch.units] - Unités de coordonnées à afficher ("DEC", "DMS", "M", "KM") + * Values may be "DEC" (decimal degrees), "DMS" (sexagecimal) for geographical coordinates, + * and "M" or "KM" for metric coordinates + * @property {Array} [coordinateSearch.systems] - Systèmes de projection à afficher (objet avec crs, label, type) + * @property {Object} [advancedSearch] - Options de recherche avancée (voir geocodeOptions.filterOptions) + * @property {HTMLElement} [advancedSearch.target=null] - Cible d'affichage des résultats + * @property {Object} [resources] - Ressources utilisées par les services + * @property {string|string[]} [resources.geocode="location"] - Ressources de géocodage + * @property {string[]} [resources.autocomplete] - Ressources d'autocomplétion + * @property {boolean} [resources.search=false] - Activer le service de recherche (false par défaut) + * @property {Object} [searchOptions={}] - Options du service de recherche + * @property {boolean} [searchOptions.addToMap=true] - Ajouter la couche automatiquement à la carte + * @property {string[]} [searchOptions.filterServices] - Filtrer sur une liste de services ("WMTS,TMS" par défaut) + * @property {string[]} [searchOptions.filterWMTSPriority] - Filtrer sur les couches WMTS prioritaires + * @property {string[]} [searchOptions.filterProjections] - Filtrer sur une liste de projections + * @property {boolean} [searchOptions.filterLayersPriority=false] - Filtrer sur les couches prioritaires + * @property {boolean} [searchOptions.filterLayers=true] - Activer le filtrage automatique des couches + * @property {Object} [searchOptions.filterLayersList] - Liste des couches à filtrer {"layerName": "service"} + * @property {boolean} [searchOptions.filterTMS=true] - Garder les TMS avec style dans les métadonnées + * @property {Object} [searchOptions.serviceOptions] - Options du service de recherche + * @property {string} [searchOptions.serviceOptions.url] - URL du service + * @property {string} [searchOptions.serviceOptions.index="standard"] - Index de recherche + * @property {string[]} [searchOptions.serviceOptions.fields=["title","layer_name"]] - Champs de recherche + * @property {number} [searchOptions.serviceOptions.size=1000] - Nombre de réponses du service + * @property {number} [searchOptions.serviceOptions.maximumResponses=10] - Nombre de résultats à afficher + * @property {number} [searchOptions.maximumEntries] - Nombre maximum de résultats à afficher + * @property {Object} [geocodeOptions={}] - Options du service de géocodage (voir Gp.Services.geocode {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~geocode Gp.Services.geocode})) + * @property {Object} [geocodeOptions.serviceOptions] - Options du service de géocodage + * @property {Object} [autocompleteOptions={}] - Options du service d'autocomplétion (voir Gp.Services.autoComplete {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~autoComplete Gp.Services.autoComplete}) + * @property {Object} [autocompleteOptions.serviceOptions] - Options du service d'autocomplétion + * @property {boolean} [autocompleteOptions.triggerGeocode=false] - Déclencher une requête de géocodage si aucune suggestion + * @property {number} [autocompleteOptions.triggerDelay=1000] - Délai avant la requête de géocodage (ms) + * @property {number} [autocompleteOptions.maximumEntries] - Nombre maximum de résultats d'autocomplétion à afficher + * @property {boolean} [autocompleteOptions.prettifyResults=false] - Nettoyer/embellir les résultats d'autocomplétion + * @property {string|number|Function} [zoomTo] - Niveau de zoom à appliquer sur le résultat ("auto", niveau, ou fonction) + * Value possible : auto or zoom level. + * Possible to overload it with a function : + * zoomTo : function (info) { + * // do some stuff... + * return zoom; + * } + */ + + +/** + * @classdesc + * SearchEngine Base control + * + * @alias ol.control.SearchEngineGeocodeIGN + * @module SearchEngine +*/ +class SearchEngineGeocodeIGN extends SearchEngineBase { + + constructor (options) { + options = options || {}; + + // Gère le service + if (!options.searchService || !(options.searchService instanceof AbstractService)) { + options.searchService = new GeocodeIGNService(options.serviceOptions); + } + + // call ol.control.Control constructor + super(options); + + this.layer = new Vector({ + source : new VectorSource({}), + zIndex : Infinity, + style : createStyle, + }); + this.extent = new Vector({ + source : new VectorSource({}), + zIndex : Infinity, + style : createStyle, + }); + + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "SearchEngineGeocodeIGN"; + + return this; + } + + setMap (map) { + super.setMap(map); + if (map) { + map.addLayer(this.extent); + map.addLayer(this.layer); + } + } + + _initEvents (options) { + super._initEvents(options); + this.on("search", this.addResultToMap); + } + + addResultToMap (e) { + let view = this.getMap().getView(); + + this.layer.getSource().clear(); + this.extent.getSource().clear(); + let extent, zoom; + if (e.result !== null) { + this.layer.getSource().addFeature(e.result); + extent = e.result.getGeometry().getExtent(); + zoom = 15; + } + if (e.extent !== null) { + this.extent.getSource().addFeature(e.extent); + extent = e.extent.getGeometry().getExtent(); + } + if (extent) { + view.fit(extent); + if (zoom) { + view.setZoom(zoom); + } + } + } + +} + +export default SearchEngineGeocodeIGN; + +// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.SearchEngineGeocodeIGN = SearchEngineGeocodeIGN; +} diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js new file mode 100644 index 000000000..49273d82d --- /dev/null +++ b/src/packages/Controls/SearchEngine/Service.js @@ -0,0 +1,699 @@ +// import CSS +import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; +import GeoJSON from "ol/format/GeoJSON"; +import Logger from "../../Utils/LoggerByDefault"; +// import geoportal library access +import Gp from "geoportal-access-lib"; +// import local +import Utils from "../../Utils/Helper"; +import GeocodeUtils from "../../Utils/GeocodeUtils"; +// Service +import Search from "../../Services/Search"; +import Feature from "ol/Feature.js"; +import BaseObject from "ol/Object"; +import Point from "ol/geom/Point.js"; + +var logger = Logger.getLogger("searchengine"); + +/** + * Options de construction d'un service + * @typedef AbstractServiceOptions + */ + +/** + * Options pour l'autocomplétion + * @typedef AutocompleteOptions + */ + +/** + * Options pour la recherche + * @typedef SearchOptions + */ + +/** + * Options pour l'autocomplétion + * @typedef AutocompleteResult + */ + +/** + * Options pour la recherche + * @typedef SearchResult + */ + + +/** + * @classdesc + * SearchEngine control + * + * @alias ol.control.SearchEngine + * @abstract + * @module SearchEngine +*/ +class AbstractService extends BaseObject { + + /** + * @constructor + * @param {AbstractServiceOptions} options + */ + constructor (options) { + options = options || {}; + + // call ol.control.Control constructor + super(options); + + if ((this.constructor == AbstractService)) { + throw new TypeError("AbstractService cannot be instantiate"); + } + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "AbstractService"; + + // initialisation du composant + this.initialize(options); + + return this; + } + + /**= + * @param {AbstractServiceOptions} options + */ + initialize (options) { + this.AUTOCOMPLETE_EVENT = "autocomplete"; + this.SEARCH_EVENT = "search"; + + this._autocompleteLocations = []; + this._locations = []; + } + + /** + * Récupère le résultat d'une recherche d'autocomplétion. + * @param {Number} [index] Optionnel. Index du résultat. Si nul, renvoie tous les résultats + * @returns {Array|AutocompleteResult} + */ + getAutocompleteLocations (index) { + if (index === undefined) { + return this._autocompleteLocations; + } else { + return this._autocompleteLocations[index]; + } + } + + /** + * Récupère le résultat d'une recherche de kieu (recherche finale). + * @param {Number} [index] Optionnel. Index du résultat. Si nul, renvoie tous les résultats + * @returns {Array|SearchResult} + */ + getResult (index) { + if (index === undefined) { + return this._locations; + } else { + return this._locations[index]; + } + } + + + /** + * @param {AutocompleteOptions} obj + * @abstract + */ + autocomplete (obj) { } + + + /** + * @param {SearchOptions} obj + * @abstract + */ + search (obj) { } + + + /** + * @param {SearchOptions} obj + * @abstract + */ + getItemTitle (obj) { + return obj; + } + +} + + +/** + * @classdesc + * SearchEngine control + * + * @alias ol.control.SearchEngine + * @abstract + * @module SearchEngine +*/ +class GeocodeIGNService extends AbstractService { + + /** + * @constructor + * @param {AbstractServiceOptions} options + */ + constructor (options) { + options = options || {}; + + // call ol.control.Control constructor + super(options); + + if (!(this instanceof GeocodeIGNService)) { + throw new TypeError("ERROR CLASS_CONSTRUCTOR"); + } + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "GeocodeIGNService"; + + return this; + } + + initialize (options) { + super.initialize(options); + + // define default options + this.options = { + searchOptions : { + maximumEntries : 5, + serviceOptions : { + maximumResponses : 10, + }, + filterLayers : true + }, + geocodeOptions : { + serviceOptions : {} + }, + autocompleteOptions : { + serviceOptions : { + maximumResponses : 5, + }, + triggerGeocode : false, + triggerDelay : 1000, + prettifyResults : false + }, + }; + + Utils.mergeParams(this.options, options); + + // configuration avec gestion des options surchargées du service + if (this.options.searchOptions) { + if (this.options.searchOptions.serviceOptions) { + if (this.options.searchOptions.serviceOptions.url) { + Search.setUrl(this.options.searchOptions.serviceOptions.url); + } + if (this.options.searchOptions.serviceOptions.fields) { + Search.setFields(this.options.searchOptions.serviceOptions.fields); + } + if (this.options.searchOptions.serviceOptions.index) { + Search.setIndex(this.options.searchOptions.serviceOptions.index); + } + if (this.options.searchOptions.serviceOptions.size) { + Search.setSize(this.options.searchOptions.serviceOptions.size); + } + if (this.options.searchOptions.serviceOptions.maximumResponses) { + Search.setMaximumResponses(this.options.searchOptions.serviceOptions.maximumResponses); + } + } + if (this.options.searchOptions.filterServices) { + Search.setFiltersByService(this.options.searchOptions.filterServices); + } + if (this.options.searchOptions.filterLayersPriority) { + Search.setFiltersByLayerPriority(this.options.searchOptions.filterLayersPriority); + } + if (this.options.searchOptions.filterWMTSPriority) { + Search.setFilterWMTSPriority(this.options.searchOptions.filterWMTSPriority); + } + if (this.options.searchOptions.filterTMS === false) { + Search.setFilterTMS(this.options.searchOptions.filterTMS); + } + if (this.options.searchOptions.filterProjections) { + Search.setFiltersByProjection(this.options.searchOptions.filterProjections); + } + } + // abonnement au service + Search.target.addEventListener("suggest", (e) => { + logger.debug(e); + let suggestResults = e.detail; + // filtre des suggestions selon la configuration ou l'option filterLayersList + suggestResults = this._filterResultsFromConfigLayers(suggestResults); + + this._fillSearchedSuggestListContainer(suggestResults); + }); + + this._currentGeocodingLocation = null; + this._suggestedLocations = []; + } + + + /** + * @param {AutocompleteOptions} obj + * @param {String} obj.value Valeur de l'autocomplete + * @abstract + */ + autocomplete (value, obj) { + if (!value) { + return; + } + + // on sauvegarde le localisant + this._currentGeocodingLocation = value; + + // on limite les requêtes à partir de 3 car. saisie ! + if (value.length < 3) { + this._clearResults(); + return; + } + + // INFORMATION + // on effectue la requête au service d'autocompletion. + // on met en place des callbacks afin de recuperer les resultats ou + // les messages d'erreurs du service. + // les resultats sont affichés dans une liste deroulante. + this._requestAutoComplete({ + text : value, + // callback onSuccess + onSuccess : this._onSuccessAutoComplete.bind(this), + // callback onFailure + onFailure : this._onFailureAutoComplete.bind(this) + }); + } + + getItemTitle (obj) { + return obj.fullText; + } + + /** + * Éxécute une requête au service. + * + * @param {Object} settings - service settings + * @param {String} settings.text - text + * @param {Function} settings.onSuccess - callback + * @param {Function} settings.onFailure - callback + * @private + */ + _requestAutoComplete (settings) { + // on ne fait pas de requête si on n'a pas renseigné de parametres ! + if (!settings || (typeof settings === "object" && Object.keys(settings).length === 0)) { + return; + } + + // on ne fait pas de requête si la parametre 'text' est vide ! + if (!settings.text) { + return; + } + + logger.log(settings); + + let options = {}; + // on recupere les options du service + Utils.assign(options, this.options.autocompleteOptions.serviceOptions); + // ainsi que la recherche et les callbacks + Utils.assign(options, settings); + + // cas où la clef API n'est pas renseignée dans les options du service, + // on utilise celle renseignée au niveau du controle ou la clé "calcul" par défaut. + options.apiKey = options.apiKey || this.options.apiKey; + + // si l'utilisateur a spécifié le paramètre ssl au niveau du control, on s'en sert + // true par défaut (https) + if (typeof options.ssl !== "boolean") { + if (typeof this.options.ssl === "boolean") { + options.ssl = this.options.ssl; + } else { + options.ssl = true; + } + } + logger.log(options); + + Gp.Services.autoComplete(options); + } + + _onSuccessAutoComplete (results) { + console.log("_onSuccessAutoComplete"); + let _maximumEntries = this.options.autocompleteOptions.maximumEntries; + let _prettifyResults = this.options.autocompleteOptions.prettifyResults; + + logger.log("request from AutoComplete", results); + if (results) { + // on sauvegarde l'etat des résultats + this._suggestedLocations = results.suggestedLocations; + this._autocompleteLocations = []; + // on vérifie qu'on n'a pas récupéré des coordonnées nulles (par ex recherche par code postal) + for (let i = 0; i < this._suggestedLocations.length; i++) { + let ilocation = this._suggestedLocations[i]; + if (ilocation.position && ilocation.position.x === 0 && ilocation.position.y === 0 && ilocation.fullText) { + // si les coordonnées sont nulles, il faut relancer une requête de géocodage avec l'attribut "fullText" récupéré + this._getGeocodeCoordinatesFromFullText(ilocation, i); + } else { + // sinon on peut afficher normalement le résultat dans la liste + this._autocompleteLocations.push(ilocation); + } + }; + // on filtre et enjolive éventuellement les résultats + if (_prettifyResults === true) { + this._prettifyAutocompleteResults(this._autocompleteLocations); + } + // on ne garde que le nombre de résultats que l'on veut afficher + if (_maximumEntries) { + this._autocompleteLocations = this._autocompleteLocations.slice(0, _maximumEntries); + } + + // on annule eventuellement une requete de geocodage en cours car on obtient des + // de nouveau des resultats d'autocompletion... + if (this._triggerHandler) { + clearTimeout(this._triggerHandler); + this._triggerHandler = null; + logger.warn("Cancel a geocode request !"); + } + this.dispatchEvent({ + type : this.AUTOCOMPLETE_EVENT, + result : this._autocompleteLocations, + }); + } + } + + _onFailureAutoComplete (error) { + console.log("_onFailureAutoComplete"); + let _triggerGeocode = this.options.autocompleteOptions.triggerGeocode; + let _triggerDelay = this.options.autocompleteOptions.triggerDelay; + + let onSuccess = function (results) { + logger.log("request from Geocoding", results); + if (results) { + this._clearResults(); + // on modifie la structure des reponses pour être + // compatible avec l'autocompletion ! + let locations = results.locations; + for (let i = 0; i < locations.length; i++) { + let location = locations[i]; + location.fullText = GeocodeUtils.getGeocodedLocationFreeform(location); + location.position = { + x : location.position.lon, + y : location.position.lat + }; + this._autocompleteLocations.push(location); + } + } + this.dispatchEvent({ + type : this.AUTOCOMPLETE_EVENT, + result : this._autocompleteLocations, + }); + }; + + // FIXME + // où affiche t on les messages : ex. 'No suggestion matching the search' ? + this._clearResults(); + logger.log(error.message); + // on envoie une requete de geocodage si aucun resultat d'autocompletion + // n'a été trouvé ! Et on n'oublie pas d'annuler celle qui est en cours ! + if (error.message === "No suggestion matching the search" && _triggerGeocode /* && value.length === 5 */) { + if (this._triggerHandler) { + clearTimeout(this._triggerHandler); + logger.warn("Cancel the last geocode request !"); + } + this._triggerHandler = setTimeout( + function () { + logger.warn("Launch a geocode request !"); + this._requestGeocoding({ + location : value, + // callback onSuccess + onSuccess : onSuccess.bind(this), + // callback onFailure + onFailure : function (error) { + logger.log(error.message); + } + }); + }, _triggerDelay + ); + } + } + + /** + * this method is called by this.onAutoCompleteSearchText() + * and it clears suggested location from duplicate entries and improve unprecise fulltext entries. + * + * @param {Array} autocompleteResults - Array of autocompleteResults to display + * @private + */ + _prettifyAutocompleteResults (autocompleteResults) { + for (var i = autocompleteResults.length - 1; i >= 0; i--) { + var autocompleteResult = autocompleteResults[i]; + if ((autocompleteResult.type === "StreetAddress" && autocompleteResult.kind === "municipality") || + autocompleteResult.type === "PositionOfInterest" && autocompleteResult.poiType[0] === "lieu-dit habité" && autocompleteResult.poiType[1] === "zone d'habitation") { + // on retire les éléments streetAdress - municipality car déjà pris en compte par POI + autocompleteResults.splice(i, 1); + } + // on précise le type dans le fulltext au POI des types département et région + if ((autocompleteResult.type === "PositionOfInterest" && autocompleteResult.poiType[0] === "administratif" && + (autocompleteResult.poiType[1] === "département" || autocompleteResult.poiType[1] === "région"))) { + autocompleteResult.fullText = autocompleteResult.fullText + ", " + autocompleteResult.poiType[1]; + } + }; + } + + + /** + * this method is called by Gp.Services.autoComplete callback in case of success + * (cf. this.onAutoCompleteSearchText), for suggested locations with null coordinates + * (case of postalCode research for instance). + * Send a geocode request with suggested location 'fullText' attribute, to get its coordinates and display it in autocomplete results list container. + * + * @param {Gp.Services.AutoCompleteResponse.SuggestedLocation} suggestedLocation - autocompletion result (with null coordinates) to be geocoded + * @param {Number} i - suggestedLocation position in Gp.Services.AutoCompleteResponse.suggestedLocations autocomplete results list + * @private + */ + _getGeocodeCoordinatesFromFullText (suggestedLocation, i) { + var context = this; + Gp.Services.geocode({ + apiKey : this.options.apiKey, + ssl : this.options.ssl, + q : GeocodeUtils.getSuggestedLocationFreeform(suggestedLocation), + index : suggestedLocation.type, + // callback onSuccess + onSuccess : function (response) { + logger.log("request from Geocoding (coordinates null)", response); + if (response.locations && response.locations.length !== 0 && response.locations[0].position) { + // on modifie les coordonnées du résultat en EPSG:4326 donc lat,lon + /// \TODO verifier si l'inversion des coordonnees est necessaire + if (context._suggestedLocations && context._suggestedLocations[i]) { + context._suggestedLocations[i].position = { + lon : response.locations[0].position.y, + lat : response.locations[0].position.x + }; + // et on l'affiche dans la liste + context._locationsToBeDisplayed.unshift(context._suggestedLocations[i]); + context._fillAutoCompletedLocationListContainer(context._locationsToBeDisplayed); + } + } + }, + // callback onFailure + onFailure : function () { + // si on n'a pas réussi à récupérer les coordonnées, on affiche quand même le résultat + if (context._suggestedLocations && context._suggestedLocations[i]) { + context._createAutoCompletedLocationElement(context._suggestedLocations[i], i); + } + } + }); + } + + _clearResults () { + this._autocompleteLocations = []; + this._locations = []; + } + + + + /** + * this method is called by event 'click' on 'GPautoCompleteResultsList' tag div + * (cf. this._createAutoCompleteListElement), and it selects the location. + * this location displays a marker on the map. + * @param {SearchOptions} obj + * @abstract + */ + search (idx) { + // TODO on souhaite un comportement different pour la selection des reponses + // de l'autocompletion : + // - liste deroulante des reponses, + // - puis possibilité de cliquer sur une suggestion + // - mais aussi de la choisir avec le clavier (arrow up/down), puis valider + // par un return + // cette selection avec les fleches doit mettre à jour le input ! + // (comme un moteur de recherche de navigateur) + + // var idx = SelectorID.index(e.target.id); + + const location = this._autocompleteLocations[idx]; + + if (idx === undefined) { + return; + } + + // on ajoute le texte de l'autocomplétion dans l'input + let label = GeocodeUtils.getSuggestedLocationFreeform(location); + + // on sauvegarde le localisant + this._currentGeocodingLocation = label; + + // on centre la vue et positionne le marker, à la position reprojetée dans la projection de la carte + this._requestGeocoding({ + index : "address,poi", + limit : 1, + returnTrueGeometry : true, + location : label, + onSuccess : this._onSuccessSearch.bind(this), + onFailure : this._onFailureSearch.bind(this, location), + }); + } + + + /** + * this method is called by this.onAutoCompleteSearch() + * and executes a request to the service. + * + * @param {Object} settings - service settings + * @param {String} settings.location - text + * @param {Function} settings.onSuccess - callback + * @param {Function} settings.onFailure - callback + * @private + */ + _requestGeocoding (settings) { + // on ne fait pas de requête si on n'a pas renseigné de parametres ! + if (!settings || (typeof settings === "object" && Object.keys(settings).length === 0)) { + return; + } + + logger.log(settings); + + var options = {}; + // on recupere les options du service + Utils.assign(options, this.options.geocodeOptions.serviceOptions); + // ainsi que la recherche et les callbacks + Utils.assign(options, settings); + // on redefinie les callbacks si les callbacks de service existent + var bOnSuccess = !!(this.options.geocodeOptions.serviceOptions.onSuccess !== null && typeof this.options.geocodeOptions.serviceOptions.onSuccess === "function"); + if (bOnSuccess) { + console.log("bonsuccess"); + var cbOnSuccess = function (e) { + settings.onSuccess.bind(this, e); + this.options.geocodeOptions.serviceOptions.onSuccess.bind(this, e); + }; + options.onSuccess = cbOnSuccess.bind(this); + } + + var bOnFailure = !!(this.options.geocodeOptions.serviceOptions.onFailure !== null && typeof this.options.geocodeOptions.serviceOptions.onFailure === "function"); + if (bOnFailure) { + console.log("bonFailrure"); + var cbOnFailure = function (e) { + settings.onFailure.bind(this, e); + this.options.geocodeOptions.serviceOptions.onFailure.bind(this, e); + }; + options.onFailure = cbOnFailure.bind(this); + } + + // cas où la clef API n'est pas renseignée dans les options du service, + // on utilise celle renseignée au niveau du controle ou la clé "calcul" par défaut + options.apiKey = options.apiKey || this.options.apiKey; + + // si l'utilisateur a spécifié le paramètre ssl au niveau du control, on s'en sert + // true par défaut (https) + if (typeof options.ssl !== "boolean") { + if (typeof this.options.ssl === "boolean") { + options.ssl = this.options.ssl; + } else { + options.ssl = true; + } + } + + logger.log(options); + + Gp.Services.geocode(options); + } + + _onSuccessSearch (results) { + let position = [ + results.locations[0].position.lon, + results.locations[0].position.lat + ]; + let f, extent; + if (results.locations[0].placeAttributes.truegeometry) { + let geom = JSON.parse(results.locations[0].placeAttributes.truegeometry); + + let format = new GeoJSON(); + let geometry = format.readGeometry(geom, { + dataProjection : "EPSG:4326", // incoming data + featureProjection : "EPSG:3857" // map projection + }); + if (geom.type !== "Point" && geom.type !== "MultiPoint") { + extent = new Feature({ geometry : geometry }); + // Point au milieu + const geom = new Point(position); + geom.transform("EPSG:4326", "EPSG:3857"); + f = new Feature({ geometry : geom }); + } else { + extent = null; + f = new Feature({ geometry : geometry }); + } + } else { + + } + + /** + * event triggered when an element of the results is clicked for autocompletion + * + * @event searchengine:autocomplete:click + * @property {Object} type - event + * @property {Object} feature - feature renvoyé par le géocodage + * @property {Object} target - instance SearchEngine + * @example + * SearchEngine.on("searchengine:autocomplete:click", function (e) { + * console.log(e.location); + * }) + */ + this.dispatchEvent({ + type : this.SEARCH_EVENT, + result : f, + extent : extent, + }); + } + + _onFailureSearch (location, error) { + let position = [ + location.position.x, + location.position.y + ]; + + logger.warn(error); + + /** + * event triggered when an element of the results is clicked for autocompletion + * + * @event searchengine:autocomplete:click + * @property {Object} type - event + * @property {Object} location - location + * @property {Object} target - instance SearchEngine + * @example + * SearchEngine.on("searchengine:autocomplete:click", function (e) { + * console.log(e.location); + * }) + */ + + const geom = new Point(position); + let f = new Feature({ geometry : geom }); + + this.dispatchEvent({ + type : this.SEARCH_EVENT, + result : f + }); + } + +} + +export { AbstractService, GeocodeIGNService }; +// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +if (window.ol) { + window.ol.service = window.ol.service ? window.ol.service : {}; + window.ol.service.GeocodeIGNService = GeocodeIGNService; +} \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/map-pin-2-fill.svg b/src/packages/Controls/SearchEngine/map-pin-2-fill.svg new file mode 100644 index 000000000..e4b3668af --- /dev/null +++ b/src/packages/Controls/SearchEngine/map-pin-2-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/packages/Services/SearchServiceBase.js b/src/packages/Services/SearchServiceBase.js index c2d9eac52..805658994 100644 --- a/src/packages/Services/SearchServiceBase.js +++ b/src/packages/Services/SearchServiceBase.js @@ -16,9 +16,9 @@ class SearchServiceBase extends OlObject { /** Autocomplete function * Dispatchs "searchstart" event when search starts * Dispatchs "search" event when search is finished - * @param {string} search + * @param {String} search * @param {Object} [options] - * @param {string} options.force force search even if search string is less than minChars / enter is pressed + * @param {String} options.force force search even if search string is less than minChars / enter is pressed * @api */ autocomplete (search, options) { @@ -48,7 +48,7 @@ class SearchServiceBase extends OlObject { } /** Get title of an item * @param {*} item - * @returns {string} title + * @returns {String} title */ getItemTitle (item) { return item; From bbfa3765cbce9f78d22ab9f9509c566d7fe3f000 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 9 Oct 2025 11:31:59 +0200 Subject: [PATCH 04/73] fix(search): Gestion raccourcis clavier --- .../Controls/SearchEngine/SearchEngineBase.js | 115 ++++++++++-------- .../SearchEngine/SearchEngineGeocodeIGN.js | 1 + src/packages/Controls/SearchEngine/Service.js | 8 +- 3 files changed, 66 insertions(+), 58 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 8e7b2e1d2..ac1638906 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -169,15 +169,13 @@ class SearchEngineBase extends Control { * @protected */ initialize (options) { - + options.minChars = options.minChars ? options.minChars : 0; } /** Add event listeners * @param {Object} options - constructor options * @protected */ _initEvents (options) { - this.input.addEventListener("input", function (e) { - }.bind(this)); this.input.addEventListener("keydown", function (e) { if (/ArrowDown|ArrowUp/.test(e.key)) { e.preventDefault(); @@ -187,47 +185,61 @@ class SearchEngineBase extends Control { // autocomplete list const list = Array.from(this.autocompleteList.querySelectorAll("li")); let idx = list.findIndex(li => li.classList.contains("active")); - // Handle key events - if (/ArrowDown|ArrowUp/.test(e.key)) { - e.preventDefault(); - // Navigation in autocomplete list - if (list.length === 0) { - return; - } - list.forEach(li => li.classList.remove("active")); - if (e.key === "ArrowDown") { - idx++; - if (idx >= list.length) { - idx = 0; + if (idx === -1) { + // Ancienne valeur + this._previousValue = e.target.value; + } + switch (e.key) { + case "ArrowDown": + case "ArrowUp": + e.preventDefault(); + // Navigation in autocomplete list + if (list.length === 0) { + return; } - } else if (e.key === "ArrowUp") { - idx--; + list.forEach(li => li.classList.remove("active")); + if (e.key === "ArrowDown") { + idx++; + if (idx >= list.length) { + idx = -1; + } + } else if (e.key === "ArrowUp") { + idx--; + if (idx < -1) { + idx = list.length - 1; + } + } + if (idx !== -1) { + // Set active + const current = list[idx]; + current.classList.add("active"); + this.input.value = current.innerText; + this.input.setAttribute("aria-activedescendant", current.id); + this.input.setAttribute("data-active-option", current.id); + } else { + // Réaffiche la valeur précédente de l'utilisateur + e.target.value = this._previousValue; + } + break; + case "Enter": + // Lance la recherche + let item = list[idx]; if (idx < 0) { - idx = list.length - 1; + // Pas d'item sélectionné : on prend le premier de la liste + item = list[0]; } - } - // Set active - const current = list[idx]; - current.classList.add("active"); - this.input.value = current.innerText; - this.input.setAttribute("aria-activedescendant", current.id); - this.input.setAttribute("data-active-option", current.id); - } else if ( - (e.target.value.length && e.target.value.length >= (options.minChars || 0)) - || (e.key === "Enter") - ) { - if (idx >= 0) { - // An item has been selected - list[idx].click(); - } else { - // Autocomplete - if (e.target.value !== this._currentValue) { + if (item) { + item.click(); + } + break; + default: + if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { this.autocomplete(e.target.value, e.key === "Enter"); + } else if (e.target.value.length === 0) { + // Show historic + this.showHistoric(); } - } - } else { - // Show historic - this.showHistoric(); + break; } this._currentValue = e.target.value; }.bind(this), false); @@ -272,7 +284,7 @@ class SearchEngineBase extends Control { // Input const input = this.input = document.createElement("input"); - input.type = "text"; + input.type = "search"; input.className = "GPsearchInputText gpf-input fr-input"; input.id = "GPsearchInputText-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); input.placeholder = options.placeholder || "Rechercher..."; @@ -303,14 +315,12 @@ class SearchEngineBase extends Control { autocompleteList.classList.add("GPelementVisible"); autocompleteList.classList.remove("GPelementHidden"); }.bind(this)); - input.addEventListener("focusout", function () { - setTimeout(function () { - input.setAttribute("aria-expanded", "false"); - autocompleteList.classList.remove("gpf-visible"); - autocompleteList.classList.add("gpf-hidden"); - autocompleteList.classList.remove("GPelementVisible"); - autocompleteList.classList.add("GPelementHidden"); - }, 100); + input.addEventListener("blur", function () { + input.setAttribute("aria-expanded", "false"); + autocompleteList.classList.remove("gpf-visible"); + autocompleteList.classList.add("gpf-hidden"); + autocompleteList.classList.remove("GPelementVisible"); + autocompleteList.classList.add("GPelementHidden"); }.bind(this)); } /** Autocomplete and update list @@ -335,11 +345,10 @@ class SearchEngineBase extends Control { * @param {String} [value] input value * @api */ - search (idx) { + search (item) { clearTimeout(this._completeDelay); - console.log(idx); this._completeDelay = setTimeout(function () { - this.searchService.search(idx); + this.searchService.search(item); }.bind(this), this.get("triggerDelay") || 100); } /** Do something on search ready @@ -378,7 +387,6 @@ class SearchEngineBase extends Control { showHistoric () { clearTimeout(this._completeDelay); if (this._historic) { - console.log(this._historic); this._updateList(this._historic.length ? this._historic : []); } } @@ -403,9 +411,10 @@ class SearchEngineBase extends Control { li.innerHTML = this.getItemTitle(item); this.autocompleteList.appendChild(li); li.addEventListener("click", function (e) { + console.log("click", e); const idx = Number(e.target.getAttribute("data-idx")); this.select(tab[idx]); - this.search(idx); + this.search(tab[idx], idx); }.bind(this)); }); } diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 2b0893aad..1ab2b8086 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -222,6 +222,7 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { if (e.extent !== null) { this.extent.getSource().addFeature(e.extent); extent = e.extent.getGeometry().getExtent(); + zoom = null; } if (extent) { view.fit(extent); diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 49273d82d..3a13b2385 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -510,10 +510,10 @@ class GeocodeIGNService extends AbstractService { * this method is called by event 'click' on 'GPautoCompleteResultsList' tag div * (cf. this._createAutoCompleteListElement), and it selects the location. * this location displays a marker on the map. - * @param {SearchOptions} obj + * @param {Object} location Objet de la recherche * @abstract */ - search (idx) { + search (location) { // TODO on souhaite un comportement different pour la selection des reponses // de l'autocompletion : // - liste deroulante des reponses, @@ -525,9 +525,7 @@ class GeocodeIGNService extends AbstractService { // var idx = SelectorID.index(e.target.id); - const location = this._autocompleteLocations[idx]; - - if (idx === undefined) { + if (location === undefined) { return; } From dd29ef3688ad6b2a14c34f693230ef2b39086809 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 9 Oct 2025 13:53:27 +0200 Subject: [PATCH 05/73] =?UTF-8?q?fix(search):=20Fix=20focus=20=C3=A9l?= =?UTF-8?q?=C3=A9ment=20autocomplete?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/SearchEngine/SearchEngineBase.js | 28 +++++++++++-------- .../SearchEngine/SearchEngineGeocodeIGN.js | 6 ++-- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index ac1638906..da438c62b 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -308,20 +308,27 @@ class SearchEngineBase extends Control { input.setAttribute("aria-autocomplete", "list"); input.setAttribute("aria-haspopup", "listbox"); - input.addEventListener("focus", function () { + input.addEventListener("focus", () => { input.setAttribute("aria-expanded", "true"); autocompleteList.classList.add("gpf-visible"); autocompleteList.classList.remove("gpf-hidden"); autocompleteList.classList.add("GPelementVisible"); autocompleteList.classList.remove("GPelementHidden"); - }.bind(this)); - input.addEventListener("blur", function () { - input.setAttribute("aria-expanded", "false"); - autocompleteList.classList.remove("gpf-visible"); - autocompleteList.classList.add("gpf-hidden"); - autocompleteList.classList.remove("GPelementVisible"); - autocompleteList.classList.add("GPelementHidden"); - }.bind(this)); + }); + input.addEventListener("blur", (e) => { + // N'agit que si le focus est hors de l'élément + if (e.relatedTarget && autocompleteList.contains(e.relatedTarget)) { + input.focus(); + } else { + setTimeout(() => { + input.setAttribute("aria-expanded", "false"); + autocompleteList.classList.remove("gpf-visible"); + autocompleteList.classList.add("gpf-hidden"); + autocompleteList.classList.remove("GPelementVisible"); + autocompleteList.classList.add("GPelementHidden"); + }, 100); + } + }); } /** Autocomplete and update list * @param {String} [value] input value @@ -410,8 +417,7 @@ class SearchEngineBase extends Control { li.setAttribute("data-idx", idx); li.innerHTML = this.getItemTitle(item); this.autocompleteList.appendChild(li); - li.addEventListener("click", function (e) { - console.log("click", e); + li.addEventListener("mouseup", function (e) { const idx = Number(e.target.getAttribute("data-idx")); this.select(tab[idx]); this.search(tab[idx], idx); diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 1ab2b8086..081b346a8 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -222,12 +222,12 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { if (e.extent !== null) { this.extent.getSource().addFeature(e.extent); extent = e.extent.getGeometry().getExtent(); - zoom = null; } if (extent) { view.fit(extent); - if (zoom) { - view.setZoom(zoom); + view.getZoom(); + if (view.getZoom() > 15) { + view.setZoom(15); } } } From 1504bd4d5dd768944f12de48ab86721e46e3f725 Mon Sep 17 00:00:00 2001 From: viglino Date: Thu, 9 Oct 2025 17:37:20 +0200 Subject: [PATCH 06/73] UPD Gestion de la croix + enter Service de base pour les tests --- .../Controls/SearchEngine/SearchEngineBase.js | 16 +++++++++++----- src/packages/Services/SearchServiceBase.js | 13 ++++++++++--- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index da438c62b..393bd389a 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -176,11 +176,19 @@ class SearchEngineBase extends Control { * @protected */ _initEvents (options) { + // Empty input + this.input.addEventListener("input", function (e) { + if (!e.target.value) { + this.showHistoric(); + } + }.bind(this)); + // Prevent cursor to go to the end of input on keydown this.input.addEventListener("keydown", function (e) { if (/ArrowDown|ArrowUp/.test(e.key)) { e.preventDefault(); } }.bind(this)); + // Keyboard navigation this.input.addEventListener("keyup", function (e) { // autocomplete list const list = Array.from(this.autocompleteList.querySelectorAll("li")); @@ -229,16 +237,14 @@ class SearchEngineBase extends Control { item = list[0]; } if (item) { - item.click(); + // Simule un clic sur l'élément sélectionné + item.dispatchEvent(new Event("mouseup")); } break; default: if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { this.autocomplete(e.target.value, e.key === "Enter"); - } else if (e.target.value.length === 0) { - // Show historic - this.showHistoric(); - } + } break; } this._currentValue = e.target.value; diff --git a/src/packages/Services/SearchServiceBase.js b/src/packages/Services/SearchServiceBase.js index 805658994..ec58c376b 100644 --- a/src/packages/Services/SearchServiceBase.js +++ b/src/packages/Services/SearchServiceBase.js @@ -15,13 +15,13 @@ class SearchServiceBase extends OlObject { /** Autocomplete function * Dispatchs "searchstart" event when search starts - * Dispatchs "search" event when search is finished + * Dispatchs "autocomplete" event when finished * @param {String} search * @param {Object} [options] * @param {String} options.force force search even if search string is less than minChars / enter is pressed * @api */ - autocomplete (search, options) { + _search (search, options, what) { // Search has started this.dispatchEvent({ type : "searchstart", @@ -39,13 +39,20 @@ class SearchServiceBase extends OlObject { }); // When search is finished this.dispatchEvent({ - type : "search", + type : what, search : search, options : options, result : result }); }.bind(this), 200); } + autocomplete (search, options) { + this._search(search, options, "autocomplete"); + } + search (search, options) { + this._search(search, options, "search"); + } + /** Get title of an item * @param {*} item * @returns {String} title From b16cf43f598f010b90702cb662bcb81ad2d28700 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 9 Oct 2025 19:12:04 +0200 Subject: [PATCH 07/73] =?UTF-8?q?feat(search):=20Ajout=20DSFR=20et=20modif?= =?UTF-8?q?=20ic=C3=B4ne?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...s-ol-searchenginebase-modules-default.html | 12 +- .../SearchEngine/DSFRsearchEngineStyle.css | 59 +++- .../Controls/SearchEngine/GPFsearchEngine.css | 15 +- .../SearchEngine/GPFsearchEngineStyle.css | 17 ++ .../SearchEngine/img/dsfr/history-line.svg | 1 + .../Controls/SearchEngine/SearchEngineBase.js | 263 ++++++++---------- .../SearchEngine/SearchEngineGeocodeIGN.js | 150 ++-------- src/packages/Controls/SearchEngine/Service.js | 112 ++++++-- .../Controls/SearchEngine/map-pin-2-fill.svg | 2 +- 9 files changed, 321 insertions(+), 310 deletions(-) create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html index 0c2ad8062..95a2dacc5 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html @@ -52,7 +52,7 @@

Ajout du moteur de recherche avec les options par défaut

// 2. Appel du SearchEngine var search = new ol.control.SearchEngineBase({ - searchService: new ol.service.SearchServiceBase({ + searchService: new ol.service.DefaultSearchService({ searchTab : [ "Paris", "Joinville-le-Pont", "Créteil", "Maisons-Alfort", "Alfortville", "Ivry-sur-Seine", "Charenton-le-Pont", @@ -73,9 +73,13 @@

Ajout du moteur de recherche avec les options par défaut

map.addControl(search); // Get search info - search.on('select', e => { - console.log("Select", e) - document.getElementById("selection").innerHTML = e.title; + search.on('search', e => { + console.log("search", e); + document.getElementById("selection").innerHTML = e.result; + }) + // Get search info + search.on('autocomplete', e => { + console.log("autocomplete", e); }) }; diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index 51af56796..fd5d0bbff 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -127,15 +127,18 @@ div.GPbuttonsContainer > button { } form[id^=GPsearchInput-] { - margin-left: 8px; - width: 300px; + width: 312px; } button[id^="GPshowSearchEnginePicto-"][aria-pressed="false"] + form[id^=GPsearchInput-] { - max-width: 300px; + max-width: 0px; border: none; } +button[id^="GPshowSearchEnginePicto-"][aria-pressed="true"] + form[id^=GPsearchInput-] { + max-width: 312px; +} + button[id^="GPshowSearchEnginePicto-"] { border-radius: 0 0.25rem 0 0; } @@ -263,3 +266,53 @@ div[id^=GPgeocodeResults-] { max-height: 4rem; } } + +[id^="GPsearchEngine"] { + display: flex; + flex-direction: row-reverse; + max-width: 360px; + padding: 0; +} + +[id^="GPsearchEngine"] ul { + padding: 12px; + padding-bottom: 0px; + margin: 0; + /* Temporaire */ + width: calc(100% + 48px); +} + +[id^="GPsearchEngine"] ul:empty { + padding: unset; +} + +[id^="GPsearchEngine"] ul li { + padding: 0.75rem 0.5rem; + color: var(--text-action-high-grey); +} + +[id^="GPsearchEngine"] ul li::before { + margin-right: 0.5rem; +} + +[id^="GPsearchEngine"] ul li.active, +[id^="GPsearchEngine"] ul li:hover { + color: #000000; + background-color: var(--background-default-grey-hover); +} + +[id^="GPsearchEngine"] ul li:active { + color: #000000; + background-color: var(--background-default-grey-active); +} + +.fr-icon-history-line::before, +.fr-icon-history-line::after { + -webkit-mask-image: url("./img/dsfr/history-line.svg"); + mask-image: url("./img/dsfr/history-line.svg"); +} + +form[id^=GPsearchInput] > input[id^=GPsearchInputText] { + height: 3rem; + max-height: 3rem; +} \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index b664382c5..c9d2be4ac 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -164,6 +164,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^="GPautoCompleteList"] { margin-left: 33px; + z-index: 2; } [id^="GPautoCompleteList"] { @@ -191,23 +192,15 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^="GPsearchEngine"] ul { position: absolute; background-color: white; - width: 100%; - margin: 3px 0; - box-shadow: 0 0 6px #000; + box-shadow: 0 2px 6px 0 rgba(0, 0, 18, 0.16); list-style: none; - padding: 0; overflow: hidden; } [id^="GPsearchEngine"] ul li { - padding: 6px 10px; - color: #5E5E5E; + overflow: hidden; + text-overflow: ellipsis; white-space: nowrap; } -[id^="GPsearchEngine"] ul li.active, -[id^="GPsearchEngine"] ul li:hover { - color: #000000; - background-color: #CEDBEF; -} [id^=GPsearchEngine-].ol-collapsed form[id^=GPsearchInput-Base-] { overflow: hidden; diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css index eefaa8fed..6fa65f153 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css @@ -213,3 +213,20 @@ div[id^=GPgeocodeResults-] { [id^="GPautocompleteResultsLocation"] { padding: 0; } + +[id^="GPsearchEngine"] ul { + margin: 3px 0; + width: 100%; + padding: 0; +} + +[id^="GPsearchEngine"] ul li { + color: #5E5E5E; + padding: 6px 10px; +} + +[id^="GPsearchEngine"] ul li.active, +[id^="GPsearchEngine"] ul li:hover { + color: #000000; + background-color: #CEDBEF; +} \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg new file mode 100644 index 000000000..91df49d3d --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/history-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 393bd389a..147ba2683 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -1,109 +1,34 @@ // import CSS import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; -// import "../../CSS/Controls/SearchEngine/GPFsearchEngineStyle.css"; -// import OpenLayers -// import Control from "ol/control/Control"; -import OlObject from "ol/Object"; import Control from "../Control"; -import Widget from "../Widget"; -import Map from "ol/Map"; -import Overlay from "ol/Overlay"; -import { - transform as olProjTransform, - get as olProjGet, - transformExtent as olProjTransformExtent -} from "ol/proj"; -import GeoJSON from "ol/format/GeoJSON"; -// import geoportal library access -import Gp from "geoportal-access-lib"; -// import local -import Config from "../../Utils/Config"; import Logger from "../../Utils/LoggerByDefault"; -import Utils from "../../Utils/Helper"; -import Markers from "../Utils/Markers"; -import Interactions from "../Utils/Interactions"; -import SelectorID from "../../Utils/SelectorID"; -import MathUtils from "../../Utils/MathUtils"; -import SearchEngineUtils from "../../Utils/SearchEngineUtils"; -import GeocodeUtils from "../../Utils/GeocodeUtils"; -import CRS from "../../CRS/CRS"; -// import local des layers -import GeoportalWMS from "../../Layers/LayerWMS"; -import GeoportalWMTS from "../../Layers/LayerWMTS"; -import GeoportalWFS from "../../Layers/LayerWFS"; -import GeoportalMapBox from "../../Layers/LayerMapBox"; -// Service -import Search from "../../Services/Search"; -import DefaultSearchService from "../../Services/SearchServiceBase"; -// DOM -import SearchEngineDOM from "./SearchEngineDOM"; -import checkDsfr from "../Utils/CheckDsfr"; +import { DefaultSearchService } from "./Service"; import { getUid } from "ol"; +const typeClasses = { + "history" : "fr-icon-history-line", + "search" : "fr-icon-map-pin-2-line", +}; var logger = Logger.getLogger("searchengine"); - /** - * @typedef {Object} SearchEngineOptions - * @property {number} [id] - Identifiant du widget (option avancée) - * @property {string} [apiKey] - Clé API. "calcul" par défaut. - * @property {boolean} [ssl=true] - Utilisation du protocole https (true par défaut) - * @property {boolean} [collapsed=true] - Mode réduit (true par défaut) - * @property {boolean} [collapsible=true] - Contrôle pliable ou non (true par défaut) - * @property {string} [direction="start"] - Position du picto (loupe), "start" par défaut - * @property {string} [placeholder="Rechercher un lieu, une adresse"] - Placeholder de la barre de recherche - * @property {boolean} [displayMarker=true] - Afficher un marqueur sur le résultat (true par défaut) - * @property {string} [markerStyle="lightOrange"] - Style du marqueur ("lightOrange", "darkOrange", "red", "turquoiseBlue") - * @property {string} [markerUrl=""] - URL du marqueur (prioritaire sur markerStyle) - * @property {boolean} [splitResults=false] - Désactiver la recherche par couches (false par défaut) - * @property {boolean} [displayButtonAdvancedSearch=false] - Afficher le bouton de recherche avancée (false par défaut) - * @property {boolean} [displayButtonGeolocate=false] - Afficher le bouton de géolocalisation (false par défaut) - * @property {boolean} [displayButtonCoordinateSearch=false] - Afficher le bouton de recherche par coordonnées (false par défaut) - * @property {boolean} [coordinateSearchInAdvancedSearch=false] - Afficher la recherche par coordonnées dans la recherche avancée - * @property {boolean} [displayButtonClose=true] - Afficher le bouton de fermeture (true par défaut) - * @property {Object} [coordinateSearch] - Options de recherche par coordonnées - * @property {HTMLElement} [coordinateSearch.target=null] - Cible d'affichage des résultats - * @property {Array} [coordinateSearch.units] - Unités de coordonnées à afficher ("DEC", "DMS", "M", "KM") - * Values may be "DEC" (decimal degrees), "DMS" (sexagecimal) for geographical coordinates, - * and "M" or "KM" for metric coordinates - * @property {Array} [coordinateSearch.systems] - Systèmes de projection à afficher (objet avec crs, label, type) - * @property {Object} [advancedSearch] - Options de recherche avancée (voir geocodeOptions.filterOptions) - * @property {HTMLElement} [advancedSearch.target=null] - Cible d'affichage des résultats - * @property {Object} [resources] - Ressources utilisées par les services - * @property {string|string[]} [resources.geocode="location"] - Ressources de géocodage - * @property {string[]} [resources.autocomplete] - Ressources d'autocomplétion - * @property {boolean} [resources.search=false] - Activer le service de recherche (false par défaut) - * @property {Object} [searchOptions={}] - Options du service de recherche - * @property {boolean} [searchOptions.addToMap=true] - Ajouter la couche automatiquement à la carte - * @property {string[]} [searchOptions.filterServices] - Filtrer sur une liste de services ("WMTS,TMS" par défaut) - * @property {string[]} [searchOptions.filterWMTSPriority] - Filtrer sur les couches WMTS prioritaires - * @property {string[]} [searchOptions.filterProjections] - Filtrer sur une liste de projections - * @property {boolean} [searchOptions.filterLayersPriority=false] - Filtrer sur les couches prioritaires - * @property {boolean} [searchOptions.filterLayers=true] - Activer le filtrage automatique des couches - * @property {Object} [searchOptions.filterLayersList] - Liste des couches à filtrer {"layerName": "service"} - * @property {boolean} [searchOptions.filterTMS=true] - Garder les TMS avec style dans les métadonnées - * @property {Object} [searchOptions.serviceOptions] - Options du service de recherche - * @property {string} [searchOptions.serviceOptions.url] - URL du service - * @property {string} [searchOptions.serviceOptions.index="standard"] - Index de recherche - * @property {string[]} [searchOptions.serviceOptions.fields=["title","layer_name"]] - Champs de recherche - * @property {number} [searchOptions.serviceOptions.size=1000] - Nombre de réponses du service - * @property {number} [searchOptions.serviceOptions.maximumResponses=10] - Nombre de résultats à afficher - * @property {number} [searchOptions.maximumEntries] - Nombre maximum de résultats à afficher - * @property {Object} [geocodeOptions={}] - Options du service de géocodage (voir Gp.Services.geocode {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~geocode Gp.Services.geocode})) - * @property {Object} [geocodeOptions.serviceOptions] - Options du service de géocodage - * @property {Object} [autocompleteOptions={}] - Options du service d'autocomplétion (voir Gp.Services.autoComplete {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~autoComplete Gp.Services.autoComplete}) - * @property {Object} [autocompleteOptions.serviceOptions] - Options du service d'autocomplétion - * @property {boolean} [autocompleteOptions.triggerGeocode=false] - Déclencher une requête de géocodage si aucune suggestion - * @property {number} [autocompleteOptions.triggerDelay=1000] - Délai avant la requête de géocodage (ms) - * @property {number} [autocompleteOptions.maximumEntries] - Nombre maximum de résultats d'autocomplétion à afficher - * @property {boolean} [autocompleteOptions.prettifyResults=false] - Nettoyer/embellir les résultats d'autocomplétion - * @property {string|number|Function} [zoomTo] - Niveau de zoom à appliquer sur le résultat ("auto", niveau, ou fonction) - * Value possible : auto or zoom level. - * Possible to overload it with a function : - * zoomTo : function (info) { - * // do some stuff... - * return zoom; - * } + * @typedef {Object} SearchEngineBaseOptions Options du constructeur pour le contrôle de recherche. + * + * @property {HTMLElement|string} [target] - Élément DOM ou sélecteur dans lequel insérer le contrôle. + * Si non défini, le contrôle crée un bouton permettant d’ouvrir/fermer le champ de recherche. + * @property {string} [title="Rechercher"] - Texte du titre (attribut `title`) du bouton principal. + * @property {string} [collapsible=false] - Si vrai, permet de fermer le contrôle. + * @property {string} [ariaLabel="Rechercher"] - Libellé accessible (ARIA) pour le champ de recherche. + * @property {string} [placeholder=""] - Texte d’indication affiché dans le champ de saisie. + * @property {number} [minChars=0] - Nombre minimum de caractères à saisir avant de lancer l’autocomplétion. + * @property {number} [maximumEntries=5] - Nombre maximum d’entrées affichées dans la liste d’autocomplétion. + * @property {number} [triggerDelay=100] - Délai (en millisecondes) avant le déclenchement de l’autocomplétion + * après la saisie de l’utilisateur. + * @property {boolean|string} [historic=true] - Active ou non l’historique local des recherches. Valeur acceptées : + * - `false` : désactive complètement l’historique ; + * - `true` : active l’historique sous le nom par défaut `GPsearch-SearchEngineBase` ; + * - `string` : active l’historique sous un nom personnalisé (ex. `"monHistoriquePerso"`). + * @property {import("./Service.js").AbstractSearchService} [searchService] - Service de recherche à utiliser. Créera un service par défaut si non donné. */ @@ -116,6 +41,24 @@ var logger = Logger.getLogger("searchengine"); */ class SearchEngineBase extends Control { + /** + * @constructor + * @param {SearchEngineBaseOptions} options Options du constructeur + * @fires autocomplete + * @fires search + * @fires select + * + * @example + * const search = new ol.control.SearchEngineBase({ + * placeholder: "Rechercher une adresse...", + * minChars: 3, + * maximumEntries: 10, + * historic: "mesRecherches", + * searchService: new CustomSearchService() + * }); + * + * map.addControl(search) + */ constructor (options) { options = options || {}; // call ol.control.Control constructor @@ -127,7 +70,10 @@ class SearchEngineBase extends Control { */ this.CLASSNAME = "SearchEngineBase"; - this.searchService = options.searchService || new DefaultSearchService(); + // initialisation du composant + this.initialize(options); + + this.searchService = options.searchService; this.searchService.on("autocomplete", function (e) { this.onAutocomplete(e); }.bind(this)); @@ -136,9 +82,6 @@ class SearchEngineBase extends Control { this.onSearch(e); }.bind(this)); - // initialisation du composant - this.initialize(options); - // Widget main DOM container this._initContainer(options); @@ -146,7 +89,7 @@ class SearchEngineBase extends Control { // Get historic in localStorage this._historic = false; - this._historicName = "GPsearch-" + (typeof options.historic === "string" ? options.historic : this.CLASSNAME); + this._historicName = "GPsearch-" + options.historic; if (options.historic !== false) { this._historic = []; try { @@ -159,20 +102,28 @@ class SearchEngineBase extends Control { } } this.showHistoric(); - - return this; } /** * Initialize SearchEngine control (called by SearchEngine constructor) * - * @param {Object} options - constructor options + * @param {SearchEngineBaseOptions} options - constructor options * @protected */ initialize (options) { - options.minChars = options.minChars ? options.minChars : 0; + // Valeurs par défaut des options + options.minChars = options.minChars ? options.minChars : 3; + options.maximumEntries = options.maximumEntries ? options.maximumEntries : 5; + options.historic = (typeof options.historic === "string" ? options.historic : this.CLASSNAME); + options.title = options.title ? options.title : "Rechercher"; + options.ariaLabel = options.ariaLabel ? options.ariaLabel : "Rechercher"; + options.placeholder = options.placeholder ? options.placeholder : ""; + options.searchService = options.searchService ? options.searchService : new DefaultSearchService(); + options.collapsible = options.collapsible === true ? true : false; + + this.set("maximumEntries", options.maximumEntries); } /** Add event listeners - * @param {Object} options - constructor options + * @param {SearchEngineBaseOptions} options - constructor options * @protected */ _initEvents (options) { @@ -249,6 +200,30 @@ class SearchEngineBase extends Control { } this._currentValue = e.target.value; }.bind(this), false); + + // Événement d'envoi du formulaire + this.container.addEventListener("submit", function (e) { + e.preventDefault(); + const list = Array.from(this.autocompleteList.querySelectorAll("li")); + + if (e.submitter && e.submitter.type === "submit") { + // Si on appuie sur le bouton, on vérifie que l'input ne soit pas vide + let input = e.target.querySelector("input"); + const value = input.value; + if (value.length < options.minChars) { + return false; + } + } + let idx = list.findIndex(li => li.classList.contains("active")); + let item = list[idx]; + if (idx < 0) { + // Pas d'item sélectionné : on prend le premier de la liste + item = list[0]; + } + if (item) { + item.click(); + } + }.bind(this)); } /** * @@ -256,52 +231,53 @@ class SearchEngineBase extends Control { */ _initContainer (options) { const element = this.element = document.createElement("div"); - element.className = "GPwidget gpf-widget ol-collapsed"; + element.className = "GPwidget gpf-widget"; element.id = "GPsearchEngine-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); - // Add button if no target + const container = this.container = document.createElement("form"); + container.className = "fr-search-bar"; + container.id = "GPsearchInput-Base-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + + // Création du bouton if (!options.target) { this.button = document.createElement("button"); this.button.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); - this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn gpf-btn-icon-search fr-btn"; - this.button.setAttribute("aria-pressed", "false"); - this.button.setAttribute("type", "button"); - this.button.setAttribute("title", options.title || options.label || "Search"); - this.button.addEventListener("click", function () { - element.classList.toggle("ol-collapsed"); - const pressed = this.button.getAttribute("aria-pressed") === "true"; - this.button.setAttribute("aria-pressed", !pressed); - if (!pressed) { - input.focus(); - } else { - input.blur(); - } - }.bind(this)); + this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn gpf-btn-icon-search fr-btn fr-btn--lg"; + this.button.setAttribute("aria-pressed", "true"); + this.button.setAttribute("type", "submit"); + this.button.setAttribute("form", container.id); + if (options.title) { + this.button.setAttribute("title", options.title); + } + if (options.collapsible) { + this.button.addEventListener("click", function () { + element.classList.toggle("ol-collapsed"); + const pressed = this.button.getAttribute("aria-pressed") === "true"; + this.button.setAttribute("aria-pressed", !pressed); + if (!pressed) { + input.focus(); + } else { + input.blur(); + } + }.bind(this)); + } element.appendChild(this.button); } - const container = document.createElement("form"); - container.className = "gpf-panel__content fr-modal__content"; - container.id = "GPsearchInput-Base-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); - container.addEventListener("submit", function (e) { - e.preventDefault(); - return false; - }); + element.appendChild(container); - - // Input const input = this.input = document.createElement("input"); - input.type = "search"; - input.className = "GPsearchInputText gpf-input fr-input"; + input.type = "text"; + input.className = "GPsearchInputText fr-input"; input.id = "GPsearchInputText-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); - input.placeholder = options.placeholder || "Rechercher..."; + input.placeholder = options.placeholder; input.autocomplete = "off"; - input.setAttribute("aria-label", options.ariaLabel || "Rechercher"); + input.setAttribute("aria-label", options.ariaLabel); container.appendChild(input); // Autocomplete container const autocompleteList = this.autocompleteList = document.createElement("ul"); - autocompleteList.className = "GPautoCompleteList GPelementHidden gpf-panel fr-modal gpf-hidden"; - autocompleteList.id = "GPautocompleteList-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + autocompleteList.className = "GPautoCompleteList GPelementHidden gpf-hidden"; + autocompleteList.id = "GPautoCompleteList-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); autocompleteList.setAttribute("role", "listbox"); autocompleteList.setAttribute("tabindex", "-1"); autocompleteList.setAttribute("aria-label", "Propositions"); @@ -352,6 +328,7 @@ class SearchEngineBase extends Control { clearTimeout(this._completeDelay); // Update list} this._updateList(e.result); + this.dispatchEvent(e); } /** Effectue la recherche de géocodage @@ -400,14 +377,15 @@ class SearchEngineBase extends Control { showHistoric () { clearTimeout(this._completeDelay); if (this._historic) { - this._updateList(this._historic.length ? this._historic : []); + this._updateList(this._historic.length ? this._historic : [], "history"); } } /** * Update autocomplete list * @param {Array<*>} tab list of autocomplete items + * @param {string} [type="search"] Optionnel. Type à inclure. Valeur autorisée : "history", "search" */ - _updateList (tab) { + _updateList (tab, type = "search") { tab = (tab || []).slice(0, this.get("maximumEntries") || 10); // Accessibility this.autocompleteList.querySelectorAll("li").forEach(li => li.classList.remove("active")); @@ -415,15 +393,16 @@ class SearchEngineBase extends Control { this.input.setAttribute("data-active-option", ""); // Update list this.autocompleteList.innerHTML = ""; + const iconClass = typeClasses[type] || typeClasses["search"]; tab.forEach((item, idx) => { const li = document.createElement("li"); li.id = "GPsearchHistoric-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)) + "-" + idx; - li.className = "GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine"; + li.className = `GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; li.setAttribute("role", "option"); li.setAttribute("data-idx", idx); li.innerHTML = this.getItemTitle(item); this.autocompleteList.appendChild(li); - li.addEventListener("mouseup", function (e) { + li.addEventListener("click", function (e) { const idx = Number(e.target.getAttribute("data-idx")); this.select(tab[idx]); this.search(tab[idx], idx); diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 081b346a8..6e1abd965 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -1,51 +1,13 @@ // import CSS import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; -// import "../../CSS/Controls/SearchEngine/GPFsearchEngineStyle.css"; -// import OpenLayers -// import Control from "ol/control/Control"; -import OlObject from "ol/Object"; -import Control from "../Control"; -import Widget from "../Widget"; -import Map from "ol/Map"; -import Overlay from "ol/Overlay"; -import { - transform as olProjTransform, - get as olProjGet, - transformExtent as olProjTransformExtent -} from "ol/proj"; -import GeoJSON from "ol/format/GeoJSON"; -// import geoportal library access -import Gp from "geoportal-access-lib"; -// import local -import Config from "../../Utils/Config"; import Logger from "../../Utils/LoggerByDefault"; -import Utils from "../../Utils/Helper"; -import Markers from "../Utils/Markers"; -import Interactions from "../Utils/Interactions"; -import SelectorID from "../../Utils/SelectorID"; -import MathUtils from "../../Utils/MathUtils"; -import SearchEngineUtils from "../../Utils/SearchEngineUtils"; -import GeocodeUtils from "../../Utils/GeocodeUtils"; -import CRS from "../../CRS/CRS"; -// import local des layers -import GeoportalWMS from "../../Layers/LayerWMS"; -import GeoportalWMTS from "../../Layers/LayerWMTS"; -import GeoportalWFS from "../../Layers/LayerWFS"; -import GeoportalMapBox from "../../Layers/LayerMapBox"; -// Service -import Search from "../../Services/Search"; -import DefaultSearchService from "../../Services/SearchServiceBase"; import SearchEngineBase from "./SearchEngineBase"; -import { AbstractService, GeocodeIGNService } from "./Service"; -// DOM -import SearchEngineDOM from "./SearchEngineDOM"; -import checkDsfr from "../Utils/CheckDsfr"; -import { getUid } from "ol"; -import { Layer, Vector } from "ol/layer"; +import { AbstractSearchService, IGNSearchService } from "./Service"; +import { Vector } from "ol/layer"; import VectorSource from "ol/source/Vector"; -import { Style, Icon, Stroke, Fill, Circle as CircleStyle } from "ol/style"; -const color = "#000091"; +import { Style, Icon, Stroke, Fill } from "ol/style"; +const color = "rgba(0, 0, 145, 1)"; const createStyle = (feature) => { const geometryType = feature.getGeometry().getType(); @@ -56,8 +18,7 @@ const createStyle = (feature) => { return new Style({ image : new Icon({ src : "/src/packages/Controls/SearchEngine/map-pin-2-fill.svg", - scale : 0.2, - // color : color, + color : [0, 0, 145, 1], }), }); @@ -91,69 +52,6 @@ const createStyle = (feature) => { var logger = Logger.getLogger("searchengine"); -/** - * @typedef {Object} SearchEngineOptions - * @property {number} [id] - Identifiant du widget (option avancée) - * @property {string} [apiKey] - Clé API. "calcul" par défaut. - * @property {boolean} [ssl=true] - Utilisation du protocole https (true par défaut) - * @property {boolean} [collapsed=true] - Mode réduit (true par défaut) - * @property {boolean} [collapsible=true] - Contrôle pliable ou non (true par défaut) - * @property {string} [direction="start"] - Position du picto (loupe), "start" par défaut - * @property {string} [placeholder="Rechercher un lieu, une adresse"] - Placeholder de la barre de recherche - * @property {boolean} [displayMarker=true] - Afficher un marqueur sur le résultat (true par défaut) - * @property {string} [markerStyle="lightOrange"] - Style du marqueur ("lightOrange", "darkOrange", "red", "turquoiseBlue") - * @property {string} [markerUrl=""] - URL du marqueur (prioritaire sur markerStyle) - * @property {boolean} [splitResults=false] - Désactiver la recherche par couches (false par défaut) - * @property {boolean} [displayButtonAdvancedSearch=false] - Afficher le bouton de recherche avancée (false par défaut) - * @property {boolean} [displayButtonGeolocate=false] - Afficher le bouton de géolocalisation (false par défaut) - * @property {boolean} [displayButtonCoordinateSearch=false] - Afficher le bouton de recherche par coordonnées (false par défaut) - * @property {boolean} [coordinateSearchInAdvancedSearch=false] - Afficher la recherche par coordonnées dans la recherche avancée - * @property {boolean} [displayButtonClose=true] - Afficher le bouton de fermeture (true par défaut) - * @property {Object} [coordinateSearch] - Options de recherche par coordonnées - * @property {HTMLElement} [coordinateSearch.target=null] - Cible d'affichage des résultats - * @property {Array} [coordinateSearch.units] - Unités de coordonnées à afficher ("DEC", "DMS", "M", "KM") - * Values may be "DEC" (decimal degrees), "DMS" (sexagecimal) for geographical coordinates, - * and "M" or "KM" for metric coordinates - * @property {Array} [coordinateSearch.systems] - Systèmes de projection à afficher (objet avec crs, label, type) - * @property {Object} [advancedSearch] - Options de recherche avancée (voir geocodeOptions.filterOptions) - * @property {HTMLElement} [advancedSearch.target=null] - Cible d'affichage des résultats - * @property {Object} [resources] - Ressources utilisées par les services - * @property {string|string[]} [resources.geocode="location"] - Ressources de géocodage - * @property {string[]} [resources.autocomplete] - Ressources d'autocomplétion - * @property {boolean} [resources.search=false] - Activer le service de recherche (false par défaut) - * @property {Object} [searchOptions={}] - Options du service de recherche - * @property {boolean} [searchOptions.addToMap=true] - Ajouter la couche automatiquement à la carte - * @property {string[]} [searchOptions.filterServices] - Filtrer sur une liste de services ("WMTS,TMS" par défaut) - * @property {string[]} [searchOptions.filterWMTSPriority] - Filtrer sur les couches WMTS prioritaires - * @property {string[]} [searchOptions.filterProjections] - Filtrer sur une liste de projections - * @property {boolean} [searchOptions.filterLayersPriority=false] - Filtrer sur les couches prioritaires - * @property {boolean} [searchOptions.filterLayers=true] - Activer le filtrage automatique des couches - * @property {Object} [searchOptions.filterLayersList] - Liste des couches à filtrer {"layerName": "service"} - * @property {boolean} [searchOptions.filterTMS=true] - Garder les TMS avec style dans les métadonnées - * @property {Object} [searchOptions.serviceOptions] - Options du service de recherche - * @property {string} [searchOptions.serviceOptions.url] - URL du service - * @property {string} [searchOptions.serviceOptions.index="standard"] - Index de recherche - * @property {string[]} [searchOptions.serviceOptions.fields=["title","layer_name"]] - Champs de recherche - * @property {number} [searchOptions.serviceOptions.size=1000] - Nombre de réponses du service - * @property {number} [searchOptions.serviceOptions.maximumResponses=10] - Nombre de résultats à afficher - * @property {number} [searchOptions.maximumEntries] - Nombre maximum de résultats à afficher - * @property {Object} [geocodeOptions={}] - Options du service de géocodage (voir Gp.Services.geocode {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~geocode Gp.Services.geocode})) - * @property {Object} [geocodeOptions.serviceOptions] - Options du service de géocodage - * @property {Object} [autocompleteOptions={}] - Options du service d'autocomplétion (voir Gp.Services.autoComplete {@link http://ignf.github.io/geoportal-access-lib/latest/jsdoc/module-Services.html#~autoComplete Gp.Services.autoComplete}) - * @property {Object} [autocompleteOptions.serviceOptions] - Options du service d'autocomplétion - * @property {boolean} [autocompleteOptions.triggerGeocode=false] - Déclencher une requête de géocodage si aucune suggestion - * @property {number} [autocompleteOptions.triggerDelay=1000] - Délai avant la requête de géocodage (ms) - * @property {number} [autocompleteOptions.maximumEntries] - Nombre maximum de résultats d'autocomplétion à afficher - * @property {boolean} [autocompleteOptions.prettifyResults=false] - Nettoyer/embellir les résultats d'autocomplétion - * @property {string|number|Function} [zoomTo] - Niveau de zoom à appliquer sur le résultat ("auto", niveau, ou fonction) - * Value possible : auto or zoom level. - * Possible to overload it with a function : - * zoomTo : function (info) { - * // do some stuff... - * return zoom; - * } - */ - /** * @classdesc @@ -168,8 +66,8 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { options = options || {}; // Gère le service - if (!options.searchService || !(options.searchService instanceof AbstractService)) { - options.searchService = new GeocodeIGNService(options.serviceOptions); + if (!options.searchService || !(options.searchService instanceof AbstractSearchService)) { + options.searchService = new IGNSearchService(options.serviceOptions); } // call ol.control.Control constructor @@ -186,12 +84,6 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { style : createStyle, }); - /** - * Nom de la classe (heritage) - * @private - */ - this.CLASSNAME = "SearchEngineGeocodeIGN"; - return this; } @@ -203,18 +95,29 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { } } + initialize (options) { + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "SearchEngineGeocodeIGN"; + super.initialize(options); + } + _initEvents (options) { super._initEvents(options); this.on("search", this.addResultToMap); } addResultToMap (e) { - let view = this.getMap().getView(); - this.layer.getSource().clear(); this.extent.getSource().clear(); let extent, zoom; if (e.result !== null) { + window.layer = this.layer; + window.feature = e.result; + window.featureImage = this.layer.getStyle()(e.result); + console.log(e.result, e.result.getGeometry(), e.result.getGeometry().getCoordinates()); this.layer.getSource().addFeature(e.result); extent = e.result.getGeometry().getExtent(); zoom = 15; @@ -223,11 +126,14 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { this.extent.getSource().addFeature(e.extent); extent = e.extent.getGeometry().getExtent(); } - if (extent) { - view.fit(extent); - view.getZoom(); - if (view.getZoom() > 15) { - view.setZoom(15); + if (this.getMap()) { + let view = this.getMap().getView(); + if (extent) { + view.fit(extent); + view.getZoom(); + if (view.getZoom() > 15) { + view.setZoom(15); + } } } } diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 3a13b2385..e91a41924 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -17,7 +17,7 @@ var logger = Logger.getLogger("searchengine"); /** * Options de construction d'un service - * @typedef AbstractServiceOptions + * @typedef AbstractSearchServiceOptions */ /** @@ -43,17 +43,17 @@ var logger = Logger.getLogger("searchengine"); /** * @classdesc - * SearchEngine control + * AbstractSearchService control * - * @alias ol.control.SearchEngine + * @alias ol.control.AbstractSearchService * @abstract - * @module SearchEngine + * @module SearchService */ -class AbstractService extends BaseObject { +class AbstractSearchService extends BaseObject { /** * @constructor - * @param {AbstractServiceOptions} options + * @param {AbstractSearchServiceOptions} options */ constructor (options) { options = options || {}; @@ -61,14 +61,14 @@ class AbstractService extends BaseObject { // call ol.control.Control constructor super(options); - if ((this.constructor == AbstractService)) { - throw new TypeError("AbstractService cannot be instantiate"); + if ((this.constructor == AbstractSearchService)) { + throw new TypeError("AbstractSearchService cannot be instantiate"); } /** * Nom de la classe (heritage) * @private */ - this.CLASSNAME = "AbstractService"; + this.CLASSNAME = "AbstractSearchService"; // initialisation du composant this.initialize(options); @@ -77,7 +77,7 @@ class AbstractService extends BaseObject { } /**= - * @param {AbstractServiceOptions} options + * @param {AbstractSearchServiceOptions} options */ initialize (options) { this.AUTOCOMPLETE_EVENT = "autocomplete"; @@ -141,17 +141,70 @@ class AbstractService extends BaseObject { /** * @classdesc - * SearchEngine control + * DefaultSearchService control * - * @alias ol.control.SearchEngine - * @abstract - * @module SearchEngine + * @alias ol.control.DefaultSearchService + * @module SearchService +*/ +class DefaultSearchService extends AbstractSearchService { + + constructor (options) { + super(); + options = options || {}; + if (options.searchTab) { + this._searchTab = options.searchTab || []; + }; + } + + /** Autocomplete function + * Dispatchs "searchstart" event when search starts + * Dispatchs "search" event when search is finished + * @param {String} search + * @param {Object} [options] + * @param {String} options.force force search even if search string is less than minChars / enter is pressed + * @api + */ + autocomplete (value) { + // Simulate asynchronous behavior + this._autocompleteLocations = []; + const rex = new RegExp(value, "i"); + (this._searchTab || []).forEach((city) => { + if (rex.test(city.toLowerCase())) { + this._autocompleteLocations.push(city); + } + }); + // When search is finished + this.dispatchEvent({ + type : this.AUTOCOMPLETE_EVENT, + result : this._autocompleteLocations + }); + } + + /** + * @param {SearchOptions} obj + */ + search (obj) { + this.dispatchEvent({ + type : this.SEARCH_EVENT, + result : obj + }); + } + +} + + +/** + * @classdesc + * IGNSearchService control + * + * @alias ol.control.IGNSearchService + * @module SearchService */ -class GeocodeIGNService extends AbstractService { +class IGNSearchService extends AbstractSearchService { /** * @constructor - * @param {AbstractServiceOptions} options + * @param {AbstractSearchServiceOptions} options */ constructor (options) { options = options || {}; @@ -159,14 +212,14 @@ class GeocodeIGNService extends AbstractService { // call ol.control.Control constructor super(options); - if (!(this instanceof GeocodeIGNService)) { + if (!(this instanceof IGNSearchService)) { throw new TypeError("ERROR CLASS_CONSTRUCTOR"); } /** * Nom de la classe (heritage) * @private */ - this.CLASSNAME = "GeocodeIGNService"; + this.CLASSNAME = "IGNSearchService"; return this; } @@ -261,11 +314,11 @@ class GeocodeIGNService extends AbstractService { // on sauvegarde le localisant this._currentGeocodingLocation = value; - // on limite les requêtes à partir de 3 car. saisie ! - if (value.length < 3) { - this._clearResults(); - return; - } + // // on limite les requêtes à partir de 3 car. saisie ! + // if (value.length < 3) { + // this._clearResults(); + // return; + // } // INFORMATION // on effectue la requête au service d'autocompletion. @@ -689,9 +742,14 @@ class GeocodeIGNService extends AbstractService { } -export { AbstractService, GeocodeIGNService }; +export { AbstractSearchService, DefaultSearchService, IGNSearchService }; + // Expose SearchEngine as ol.control.SearchEngine (for a build bundle) if (window.ol) { - window.ol.service = window.ol.service ? window.ol.service : {}; - window.ol.service.GeocodeIGNService = GeocodeIGNService; -} \ No newline at end of file + if (!window.ol.service) { + window.ol.service = {}; + } + window.ol.service.AbstractSearchService = AbstractSearchService; + window.ol.service.DefaultSearchService = DefaultSearchService; + window.ol.service.IGNSearchService = IGNSearchService; +} diff --git a/src/packages/Controls/SearchEngine/map-pin-2-fill.svg b/src/packages/Controls/SearchEngine/map-pin-2-fill.svg index e4b3668af..427babc8e 100644 --- a/src/packages/Controls/SearchEngine/map-pin-2-fill.svg +++ b/src/packages/Controls/SearchEngine/map-pin-2-fill.svg @@ -1 +1 @@ - \ No newline at end of file + From 2dceda30cdb06d3dd617e7a70508875a1e5cdb45 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 10 Oct 2025 10:03:24 +0200 Subject: [PATCH 08/73] fix(search): Change dispatch event en click() --- src/packages/Controls/SearchEngine/SearchEngineBase.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 147ba2683..1749c4762 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -189,7 +189,7 @@ class SearchEngineBase extends Control { } if (item) { // Simule un clic sur l'élément sélectionné - item.dispatchEvent(new Event("mouseup")); + item.click(); } break; default: From 061224623f109d37624ccca2255bd8e8aae5e32f Mon Sep 17 00:00:00 2001 From: viglino Date: Fri, 10 Oct 2025 13:10:07 +0200 Subject: [PATCH 09/73] Update searchengine --- ...s-ol-searchenginebase-modules-default.html | 4 +- .../SearchEngine/DSFRsearchEngineStyle.css | 10 +---- .../Controls/SearchEngine/GPFsearchEngine.css | 25 +++++++++++ .../Controls/SearchEngine/SearchEngineBase.js | 44 ++++++++++++++++--- 4 files changed, 67 insertions(+), 16 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html index 95a2dacc5..9fe1e54a5 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-default.html @@ -65,8 +65,8 @@

Ajout du moteur de recherche avec les options par défaut

"Coubron", "Aulnay-sous-Bois", "Sevran", "Livry-Gargan", "Clichy-sous-Bois", "Montfermeil", "Gagny", "Neuilly-sur-Marne", "Noisy-le-Grand", "Noisy-le-Sec", "Romainville", "Les Lilas", "Bagnolet", "Montreuil", "Vincennes", "Fontenay-sous-Bois" - ] - }) + ]}), + collapsible: true }); // 3. Ajout du SearchEngine à la carte diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index fd5d0bbff..fd435a754 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -267,19 +267,11 @@ div[id^=GPgeocodeResults-] { } } -[id^="GPsearchEngine"] { - display: flex; - flex-direction: row-reverse; - max-width: 360px; - padding: 0; -} - [id^="GPsearchEngine"] ul { padding: 12px; padding-bottom: 0px; margin: 0; - /* Temporaire */ - width: calc(100% + 48px); + width: 100%; } [id^="GPsearchEngine"] ul:empty { diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index c9d2be4ac..ff5a25584 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -208,3 +208,28 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^=GPsearchEngine-] form[id^=GPsearchInput-Base-] { overflow: visible; } + +[id^="GPsearchEngine"] { + display: flex; + flex-direction: row; + max-width: 360px; + padding: 0; + max-height: 32px; +} +[id^="GPsearchEngine"] form { + display: flex; + flex-direction: column; +} +[id^="GPsearchEngine"] form > * { + display: flex; + flex-direction: row; +} +[id^="GPsearchEngine"] form .GPInputGroup { + padding: 0; +} +[id^="GPsearchEngine"] form .GPOptionsContainer { + display: flex; + height: calc(100% - 2px); +} + + diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 1749c4762..fe46a0a51 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -233,18 +233,19 @@ class SearchEngineBase extends Control { const element = this.element = document.createElement("div"); element.className = "GPwidget gpf-widget"; element.id = "GPsearchEngine-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + // Main container const container = this.container = document.createElement("form"); container.className = "fr-search-bar"; container.id = "GPsearchInput-Base-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); // Création du bouton - if (!options.target) { + if (!options.target && options.collapsible) { this.button = document.createElement("button"); this.button.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn gpf-btn-icon-search fr-btn fr-btn--lg"; this.button.setAttribute("aria-pressed", "true"); - this.button.setAttribute("type", "submit"); - this.button.setAttribute("form", container.id); + // this.button.setAttribute("type", "submit"); + // this.button.setAttribute("form", container.id); if (options.title) { this.button.setAttribute("title", options.title); } @@ -264,6 +265,11 @@ class SearchEngineBase extends Control { } element.appendChild(container); + + const search = document.createElement("div"); + search.className = "GPInputGroup fr-input"; + container.appendChild(search); + // Input const input = this.input = document.createElement("input"); input.type = "text"; @@ -272,16 +278,44 @@ class SearchEngineBase extends Control { input.placeholder = options.placeholder; input.autocomplete = "off"; input.setAttribute("aria-label", options.ariaLabel); - container.appendChild(input); + search.appendChild(input); + + // Options container + this.optionscontainer = document.createElement("div"); + this.optionscontainer.className = "GPOptionsContainer"; + search.appendChild(this.optionscontainer); + + // Submit button + const submit = document.createElement("button"); + submit.className = "GPsearchInputSubmit gpf-btn gpf-btn-icon-search fr-btn"; + submit.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + submit.type = "submit"; + if (options.title) { + submit.setAttribute("title", options.title); + } + search.appendChild(submit); // Autocomplete container + const acContainer = document.createElement("div"); + acContainer.className = "GPautoCompleteContainer"; + container.appendChild(acContainer); + + // Autocomplete list + const autocompleteHeader = this.autocompleteHeader = document.createElement("div"); + autocompleteHeader.className = "GPautoCompleteHeader"; + acContainer.appendChild(autocompleteHeader); + const autocompleteList = this.autocompleteList = document.createElement("ul"); autocompleteList.className = "GPautoCompleteList GPelementHidden gpf-hidden"; autocompleteList.id = "GPautoCompleteList-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); autocompleteList.setAttribute("role", "listbox"); autocompleteList.setAttribute("tabindex", "-1"); autocompleteList.setAttribute("aria-label", "Propositions"); - container.appendChild(autocompleteList); + acContainer.appendChild(autocompleteList); + + const autocompleteFooter = this.autocompleteFooter = document.createElement("div"); + autocompleteFooter.className = "GPautoCompleteFooter"; + acContainer.appendChild(autocompleteFooter); // Input controller for accessibility input.setAttribute("role", "combobox"); From 59a5b1265987645b660a320b250a0102ec905309 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 10 Oct 2025 15:03:55 +0200 Subject: [PATCH 10/73] feat(search): Ajout popup recherche --- ...l-searchenginebase-modules-geocodeIGN.html | 5 +- .../SearchEngine/DSFRsearchEngineStyle.css | 51 +++++++ .../Controls/SearchEngine/SearchEngineBase.js | 7 +- .../SearchEngine/SearchEngineGeocodeIGN.js | 128 +++++++++++++++++- src/packages/Controls/SearchEngine/Service.js | 6 +- 5 files changed, 188 insertions(+), 9 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html index 8fa33d5a8..a86fc6c0e 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-geocodeIGN.html @@ -57,7 +57,10 @@

Ajout du moteur de recherche avec les options par défaut

}); // 2. Appel du SearchEngine - var search = new ol.control.SearchEngineGeocodeIGN(); + var search = new ol.control.SearchEngineGeocodeIGN({ + + collapsible : true, + }); // 3. Ajout du SearchEngine à la carte map.addControl(search); diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index fd435a754..cd3ee94a2 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -307,4 +307,55 @@ div[id^=GPgeocodeResults-] { form[id^=GPsearchInput] > input[id^=GPsearchInputText] { height: 3rem; max-height: 3rem; +} + +.GPSearchPopup { + display: flex; + position: relative; + align-items: center; + justify-content: space-between; + flex-wrap: nowrap; + gap: 12px; + width: 288px; + + bottom: 17px; + padding: 0.75rem; + border: 1px solid var(--border-default-grey); + box-shadow: var(--overlap-shadow); + background-color: var(--background-default-grey); + font-size: 0.75em; +} +/* Border layer (slightly larger triangle) */ +.GPSearchPopup::before { + content: ""; + position: absolute; + border-top: 16px solid var(--border-default-grey); /* Border color */ + border-right: 15px solid transparent; + border-left: 15px solid transparent; + bottom: -17px; + margin-left: -15px; + left: 50%; +} + +/* Inner layer (actual background triangle) */ +.GPSearchPopup::after { + content: ""; + position: absolute; + border-top: 15px solid var(--background-default-grey); /* Same as popup bg */ + border-right: 14px solid transparent; + border-left: 14px solid transparent; + bottom: -15px; + margin-left: -14px; + left: 50%; +} + +.GPPopupContent { + line-height: 1.25rem; +} + +.GPButtonGroups { + display: flex; + flex-direction: row-reverse; + flex: 0 0 auto; + align-items: center; } \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index fe46a0a51..5094302c5 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -205,7 +205,7 @@ class SearchEngineBase extends Control { this.container.addEventListener("submit", function (e) { e.preventDefault(); const list = Array.from(this.autocompleteList.querySelectorAll("li")); - + if (e.submitter && e.submitter.type === "submit") { // Si on appuie sur le bouton, on vérifie que l'input ne soit pas vide let input = e.target.querySelector("input"); @@ -242,7 +242,7 @@ class SearchEngineBase extends Control { if (!options.target && options.collapsible) { this.button = document.createElement("button"); this.button.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); - this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn gpf-btn-icon-search fr-btn fr-btn--lg"; + this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn fr-icon-search-line fr-btn fr-btn--lg"; this.button.setAttribute("aria-pressed", "true"); // this.button.setAttribute("type", "submit"); // this.button.setAttribute("form", container.id); @@ -287,10 +287,11 @@ class SearchEngineBase extends Control { // Submit button const submit = document.createElement("button"); - submit.className = "GPsearchInputSubmit gpf-btn gpf-btn-icon-search fr-btn"; + submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; submit.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); submit.type = "submit"; if (options.title) { + submit.textContent = options.title; submit.setAttribute("title", options.title); } search.appendChild(submit); diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 6e1abd965..b1add401a 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -1,4 +1,5 @@ // import CSS +import { Select } from "ol/interaction"; import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import Logger from "../../Utils/LoggerByDefault"; import SearchEngineBase from "./SearchEngineBase"; @@ -7,6 +8,8 @@ import { Vector } from "ol/layer"; import VectorSource from "ol/source/Vector"; import { Style, Icon, Stroke, Fill } from "ol/style"; +import Overlay from "ol/Overlay.js"; +import checkDsfr from "../Utils/CheckDsfr"; const color = "rgba(0, 0, 145, 1)"; const createStyle = (feature) => { @@ -84,14 +87,31 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { style : createStyle, }); + this.selectInteraction = new Select({ + layers : [this.layer, this.extent], + style : createStyle, + }); + + this.selectInteraction.on("select", this._onSelectElement.bind(this)); + + this.popup = this._createPopup(); + return this; } + /** + * Fonction appellée lors de l'ajout du contrôle à une carte + * @override + * @param {import("ol/Map.js").default|null} map Map + */ setMap (map) { super.setMap(map); if (map) { map.addLayer(this.extent); map.addLayer(this.layer); + + map.addInteraction(this.selectInteraction); + map.addOverlay(this.popup); } } @@ -102,6 +122,7 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { */ this.CLASSNAME = "SearchEngineGeocodeIGN"; super.initialize(options); + this.REMOVE_FEATURE_EVENT = "remove:feature"; } _initEvents (options) { @@ -114,10 +135,6 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { this.extent.getSource().clear(); let extent, zoom; if (e.result !== null) { - window.layer = this.layer; - window.feature = e.result; - window.featureImage = this.layer.getStyle()(e.result); - console.log(e.result, e.result.getGeometry(), e.result.getGeometry().getCoordinates()); this.layer.getSource().addFeature(e.result); extent = e.result.getGeometry().getExtent(); zoom = 15; @@ -138,6 +155,109 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { } } + + /** + * + * @param {import("ol/interaction/Select").SelectEvent} e Événement de séléction + */ + _onSelectElement (e) { + const position = e.mapBrowserEvent.coordinate; + if (e.selected.length) { + // Ajoute le popup + const feature = e.selected[0]; + this.popup.setPosition(position); + this.setPopupContent(feature.get("infoPopup")); + this.popup.set("feature", feature); + this.popup.set("layer", e.target.getLayer(feature)); + } else { + this.popup.setPosition(undefined); + this.setPopupContent(""); + this.popup.unset("feature"); + this.popup.unset("layer"); + } + } + + _createPopup () { + // Popup global + let element = this._popupDiv = document.createElement("div"); + // TODO : ajouter gp-feature-info-div lorsque les deux seront pareils + element.className = "GPSearchPopup"; + + // Contenu du popup + let popupContent = this._popupContent = document.createElement("div"); + popupContent.className = "GPPopupContent"; + + // Groupe de boutons + let popupBtns = this._popupBtns = document.createElement("div"); + popupBtns.className = "GPButtonGroups gpf-btns-group"; + + popupBtns.appendChild(this._addCloseButton()); + popupBtns.appendChild(this._addRemoveButton()); + + element.appendChild(popupContent); + element.appendChild(popupBtns); + + const overlay = new Overlay({ + element : element, + positioning : "bottom-center", + }); + + return overlay; + } + + setPopupContent (content) { + this._popupContent.innerHTML = content; + } + + _addCloseButton () { + let closer = document.createElement("button"); + closer.title = closer.ariaLabel = "Fermer la pop-up"; + closer.textContent = "Fermer"; + closer.className = "GPButton gpf-btn fr-icon-close-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline"; + + // Ferme le popup + closer.onclick = this._closePopup.bind(this); + return closer; + } + + _closePopup () { + this.selectInteraction.getFeatures().clear(); + if (this.popup != null) { + this.popup.setPosition(undefined); + } + return false; + } + + _addRemoveButton () { + let remove = document.createElement("button"); + remove.title = remove.ariaLabel = "Supprimer le marqueur"; + remove.textContent = "Supprimer"; + remove.className = "GPButton gpf-btn fr-icon-delete-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline"; + + // Supprime la feature + remove.onclick = this._removeFeature.bind(this); + + return remove; + } + + _removeFeature () { + const f = this.popup.get("feature"); + const layer = this.popup.get("layer"); + // Supprime la feature + if (layer && f) { + layer.getSource().removeFeature(f); + + this.dispatchEvent({ + type : this.REMOVE_FEATURE_EVENT, + feature : f, + layer : layer, + }); + + // Ferme le popup + this._closePopup(); + } + } + } export default SearchEngineGeocodeIGN; diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index e91a41924..6f60965fa 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -687,9 +687,12 @@ class IGNSearchService extends AbstractSearchService { extent = null; f = new Feature({ geometry : geometry }); } - } else { + } + if (extent) { + extent.set("infoPopup", this._currentGeocodingLocation); } + f.set("infoPopup", this._currentGeocodingLocation); /** * event triggered when an element of the results is clicked for autocompletion @@ -733,6 +736,7 @@ class IGNSearchService extends AbstractSearchService { const geom = new Point(position); let f = new Feature({ geometry : geom }); + f.set("infoPopup", this._currentGeocodingLocation); this.dispatchEvent({ type : this.SEARCH_EVENT, From f53c94ba9a5e5e67f46d02f33dd8a532012e37a8 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 10 Oct 2025 17:46:00 +0200 Subject: [PATCH 11/73] fix(search): Centre popup sur le point + ajout doc --- .../SearchEngine/SearchEngineGeocodeIGN.js | 50 +++++++++++++++---- src/packages/Controls/SearchEngine/Service.js | 50 +++++++++++++++---- 2 files changed, 82 insertions(+), 18 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index b1add401a..785b03148 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -55,6 +55,17 @@ const createStyle = (feature) => { var logger = Logger.getLogger("searchengine"); +/** + * Options spécifiques au contrôle IGN + * + * Cette définition combine (hérite) de SearchEngineBaseOptions + * et ajoute une propriété `serviceOptions` qui contient + * les options propres au service (IGNSearchService). + * + * @typedef {import("./SearchEngineBase.js").SearchEngineBaseOptions & { + * serviceOptions: import("./Service.js").AbstractSearchServiceOptions + * }} SearchEngineGeocodeIGNOptions + */ /** * @classdesc @@ -65,14 +76,13 @@ var logger = Logger.getLogger("searchengine"); */ class SearchEngineGeocodeIGN extends SearchEngineBase { + /** + * @constructor + * @param {SearchEngineGeocodeIGNOptions} options Options du constructeur + */ constructor (options) { options = options || {}; - // Gère le service - if (!options.searchService || !(options.searchService instanceof AbstractSearchService)) { - options.searchService = new IGNSearchService(options.serviceOptions); - } - // call ol.control.Control constructor super(options); @@ -100,9 +110,9 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { } /** - * Fonction appellée lors de l'ajout du contrôle à une carte + * Fonction d'ajout du contrôle. * @override - * @param {import("ol/Map.js").default|null} map Map + * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. */ setMap (map) { super.setMap(map); @@ -115,16 +125,34 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { } } + /** + * Initialise les options du contrôle. + * + * @override + * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. + */ initialize (options) { /** * Nom de la classe (heritage) * @private */ this.CLASSNAME = "SearchEngineGeocodeIGN"; - super.initialize(options); this.REMOVE_FEATURE_EVENT = "remove:feature"; + + // Créé le serbice de géocodage IGN + if (!options.searchService || !(options.searchService instanceof AbstractSearchService)) { + options.searchService = new IGNSearchService(options.serviceOptions); + } + + super.initialize(options); } + /** + * Initialise les événements du contrôle. + * + * @override + * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. + */ _initEvents (options) { super._initEvents(options); this.on("search", this.addResultToMap); @@ -161,10 +189,14 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { * @param {import("ol/interaction/Select").SelectEvent} e Événement de séléction */ _onSelectElement (e) { - const position = e.mapBrowserEvent.coordinate; + let position = e.mapBrowserEvent.coordinate; if (e.selected.length) { // Ajoute le popup const feature = e.selected[0]; + if (feature.getGeometry().getType() === "Point") { + // Place le popup sur le point + position = feature.getGeometry().getCoordinates(); + } this.popup.setPosition(position); this.setPopupContent(feature.get("infoPopup")); this.popup.set("feature", feature); diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 6f60965fa..746aab571 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -14,30 +14,62 @@ import BaseObject from "ol/Object"; import Point from "ol/geom/Point.js"; var logger = Logger.getLogger("searchengine"); - /** * Options de construction d'un service - * @typedef AbstractSearchServiceOptions + * @typedef {Object} AbstractSearchServiceOptions + * @property {String} [apiKey] - Clé API utilisée pour les requêtes vers les services IGN + * @property {Boolean} [ssl=true] - Force l'utilisation du protocole HTTPS si défini à true + * @property {AutocompleteOptions} [autocompleteOptions] - Options spécifiques à l'autocomplétion + * @property {SearchOptions} [searchOptions] - Options spécifiques à la recherche finale + * @property {GeocodeOptions} [geocodeOptions] - Options spécifiques au géocodage */ /** * Options pour l'autocomplétion - * @typedef AutocompleteOptions + * @typedef {Object} AutocompleteOptions + * @property {Object} [serviceOptions] - Options passées à Gp.Services.autoComplete + * @property {Number} [maximumResponses] - Nombre maximal de réponses retournées + * @property {Boolean} [triggerGeocode=false] - Si vrai, déclenche une requête de géocodage lorsque l'autocomplétion échoue + * @property {Number} [triggerDelay=1000] - Délai (ms) avant déclenchement du géocodage automatique + * @property {Boolean} [prettifyResults=false] - Si vrai, embellit/filtre les résultats */ /** - * Options pour la recherche - * @typedef SearchOptions + * Options pour la recherche finale (géocodage) + * @typedef {Object} SearchOptions + * @property {Object} [serviceOptions] - Options passées à Gp.Services.geocode + * @property {Number} [maximumResponses] - Nombre maximal de réponses + * @property {Boolean} [filterLayers] - Active le filtrage des résultats par couche + * @property {String|Array} [index] - Indexs utilisés (e.g. "address,poi") + * @property {Number} [limit] - Limite de résultats */ /** - * Options pour l'autocomplétion - * @typedef AutocompleteResult + * Options pour le géocodage (appel manuel de coordonnées via texte) + * @typedef {Object} GeocodeOptions + * @property {Object} [serviceOptions] - Options passées à Gp.Services.geocode + * @property {String} [location] - Texte à géocoder + * @property {Function} [onSuccess] - Callback exécuté en cas de succès + * @property {Function} [onFailure] - Callback exécuté en cas d'échec + */ + +/** + * Résultat d'une autocomplétion + * @typedef {Object} AutocompleteResult + * @property {String} fullText - Libellé affichable du lieu + * @property {Object} position - Coordonnées + * @property {Number} position.x - Longitude + * @property {Number} position.y - Latitude + * @property {String} [type] - Type de résultat (e.g. "StreetAddress", "PositionOfInterest") + * @property {Array} [poiType] - Types détaillés (e.g. ["administratif","région"]) */ /** - * Options pour la recherche - * @typedef SearchResult + * Résultat d'une recherche (géocodage final) + * @typedef {Object} SearchResult + * @property {import("ol/Feature").default} feature - Feature OL contenant la géométrie + * @property {import("ol/Feature").default|undefined} [extent] - Étendue si zone géographique + * @property {String} [infoPopup] - Texte à afficher dans un popup */ From 3e1346bb98d75141ac46928225786329f7896950 Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 13 Oct 2025 13:37:44 +0200 Subject: [PATCH 12/73] ADD advanced search engine --- build/webpack/controls.webpack.config.js | 1 + build/webpack/modules.webpack.config.js | 1 + ...ginebase-modules-dsfr-geocodeAdvanced.html | 79 ++++++++++++ .../Controls/SearchEngine/GPFsearchEngine.css | 66 +++++++++- .../SearchEngine/SearchEngineAdvanced.js | 119 ++++++++++++++++++ .../Controls/SearchEngine/SearchEngineBase.js | 49 +++++--- .../SearchEngine/SearchEngineGeocodeIGN.js | 9 ++ src/packages/Controls/SearchEngine/Service.js | 12 +- src/packages/Utils/Helper.js | 20 +++ 9 files changed, 329 insertions(+), 27 deletions(-) create mode 100644 samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html create mode 100644 src/packages/Controls/SearchEngine/SearchEngineAdvanced.js diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 5126d532b..de19729ad 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -76,6 +76,7 @@ module.exports = (env, argv) => { case "SearchEngine": case "SearchEngineBase": case "SearchEngineGeocodeIGN": + case "SearchEngineAdvanced": // crs break; case "MeasureArea": diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 809dfaa64..fdea276fd 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -47,6 +47,7 @@ module.exports = (env, argv) => { "GpfExtOlSearchEngine" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngine.js"), "GpfExtOlSearchEngineBase" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineBase.js"), "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"), + "GpfExtOlSearchEngineAdvanced" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineAdvanced.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), "GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"), "GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html new file mode 100644 index 000000000..57453cf74 --- /dev/null +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -0,0 +1,79 @@ +{{#extend "ol-sample-modules-dsfr-layout"}} + +{{#content "vendor"}} + + + +{{/content}} + +{{#content "head"}} + Sample openlayers SearchEngine +{{/content}} + +{{#content "style"}} + +{{/content}} + +{{#content "body"}} +

Ajout du moteur de recherche avec les options par défaut

+ +
+
+

Dernière sélection :

+{{/content}} + +{{#content "js"}} + +{{/content}} + +{{/extend}} diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index ff5a25584..67a2941ea 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -189,12 +189,13 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } -[id^="GPsearchEngine"] ul { +[id^="GPsearchEngine"] .GPautoCompleteContainer { position: absolute; background-color: white; box-shadow: 0 2px 6px 0 rgba(0, 0, 18, 0.16); list-style: none; overflow: hidden; + flex-direction: column; } [id^="GPsearchEngine"] ul li { overflow: hidden; @@ -216,10 +217,12 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ padding: 0; max-height: 32px; } +/* [id^="GPsearchEngine"] form { display: flex; flex-direction: column; } +*/ [id^="GPsearchEngine"] form > * { display: flex; flex-direction: row; @@ -232,4 +235,65 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ height: calc(100% - 2px); } +[id^="GPsearchEngine"] form .GPOptionsContainer button { + max-width: unset; + min-height: 1rem; + line-height: 1rem; + margin: 0.25rem 0.5rem; + border-radius: 0; +} +[id^="GPsearchEngine"] form .GPOptionsContainer button:before { + display: none; +} + +[id^="GPsearchEngine"] form .GPautoCompleteHeader button { + margin: 12px 12px 0; + max-width: unset; +} +[id^="GPsearchEngine"] form .GPautoCompleteFooter button { + margin: 0 12px 12px; + max-width: unset; +} + +[id^="GPsearchEngine"] .GPautoCompleteHeader .GPSearchEngine-locate { + display: none; +} +[id^="GPsearchEngine"] .GPautoCompleteContainer[data-type="history"] .GPSearchEngine-locate { + display: block; +} + +/* Advanced search panel */ +[id^="GPsearchEngine-Advanced"] { + display: flex; + flex-direction: column; +} +[id^="GPsearchEngine-Advanced"] [id^="GPsearchEngine-"] { + position: static; +} +[id^="GPsearchEngine-Advanced"] > div { + display: flex; + flex-direction: column; + width: 100%; +} +[id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer { + display: none; + padding: 1rem; + background-color: var(--background-default-grey); + box-sizing: content-box; + width: calc(100% - 2rem); +} +[id^="GPsearchEngine-Advanced"]:has(.GPSearchEngine-advanced-btn[aria-expanded="true"]) > .GPAdvancedContainer { + display: flex; +} +[id^="GPsearchEngine-Advanced"] .GPSearchEngine-advanced-btn[aria-expanded="false"]::after { + transform: rotate(180deg); +} +[id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled { + background-color: var(--background-action-high-blue-france); +} +[id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled, +[id^="GPsearchEngine-Advanced"] .GPsearchInputText:disabled { + cursor: default; + user-select: none; +} diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js new file mode 100644 index 000000000..a62a51471 --- /dev/null +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -0,0 +1,119 @@ +import Control from "ol/control/Control"; +import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; +import Helper from "../../Utils/Helper"; + +/** + * Classe représentant un moteur de recherche avancée utilisant le service de géocodage de l'IGN. + * + * @extends {SearchEngineGeocodeIGN} + * @example + * import SearchEngineAdvanced from "geopf-controls/Controls/SearchEngine/SearchEngineAdvanced"; + */ +class SearchEngineAdvanced extends Control { + + /** + * Constructeur. + * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. + */ + constructor (options) { + options = options || {}; + // call ol.control.Control constructor + + super(options); + this.initialize(options); + this._initContainer(options); + // this._initEvents(options); + } + + /** + * Initialise les options du contrôle. + * + * @override + * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. + */ + initialize (options) { + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "SearchEngineAdvanced"; + } + + /** + * Fonction d'ajout du contrôle. + * @override + * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. + */ + setMap (map) { + if (this.getMap() && this.baseSearchEngine) { + this.getMap().removeControl(this.baseSearchEngine); + } + super.setMap(map); + if (this.baseSearchEngine) { + this.baseSearchEngine.setMap(map); + } + } + + /** + * Ajoute le contrôle à la carte. + * @override + * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. + */ + _initContainer (options) { + // Gestion de l'affichage des options avancées + const element = this.element = document.createElement("div"); + element.className = "GPwidget gpf-widget"; + element.id = Helper.getUid("GPsearchEngine-Advanced-"); + + // Default base search engine + const baseContainer = this.advancedContainer = document.createElement("div"); + this.element.appendChild(baseContainer); + options.target = baseContainer; + this.baseSearchEngine = new SearchEngineGeocodeIGN(options); + this.baseSearchEngine.on(["select", "search", "autocomplete"], (e) => { + this.dispatchEvent(e); + }); + + // Geolocation + const locationBtn = document.createElement("button"); + locationBtn.innerText = "Me géolocaliser"; + locationBtn.className = "GPSearchEngine-locate fr-btn fr-icon-arrow-up-s-line fr-btn--icon-left fr-btn--tertiary-no-outline"; + this.baseSearchEngine.autocompleteHeader.appendChild(locationBtn); + + // Ajout des options avancées + const advancedBtn = document.createElement("button"); + advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; + advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); + advancedBtn.type = "button"; + advancedBtn.title = "Avancée"; + advancedBtn.innerHTML = "Avancée"; + advancedBtn.setAttribute("aria-label", "Afficher les options avancées"); + advancedBtn.setAttribute("aria-expanded", "false"); + this.baseSearchEngine.optionscontainer.appendChild(advancedBtn); + + // Gestion de l'affichage des options avancées + const advancedContainer = this.advancedContainer = document.createElement("div"); + advancedContainer.className = "GPAdvancedContainer"; + advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); + advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); + this.element.appendChild(advancedContainer); + this.advancedContainer.innerHTML = "Formulaires spécifiques"; + + // Gestion du bouton avancé + advancedBtn.setAttribute("aria-controls", advancedContainer.id); + advancedBtn.addEventListener("click", (e) => { + e.preventDefault(); + const isHidden = advancedBtn.getAttribute("aria-expanded") === "false"; + advancedBtn.setAttribute("aria-expanded", isHidden); + this.baseSearchEngine.setActive(isHidden); + }); + } + +} + +export default SearchEngineAdvanced; + +// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.SearchEngineAdvanced = SearchEngineAdvanced; +} \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 5094302c5..d81d13a32 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -3,7 +3,7 @@ import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import Control from "../Control"; import Logger from "../../Utils/LoggerByDefault"; import { DefaultSearchService } from "./Service"; -import { getUid } from "ol"; +import Helper from "../../Utils/Helper"; const typeClasses = { "history" : "fr-icon-history-line", @@ -11,6 +11,7 @@ const typeClasses = { }; var logger = Logger.getLogger("searchengine"); + /** * @typedef {Object} SearchEngineBaseOptions Options du constructeur pour le contrôle de recherche. * @@ -232,16 +233,16 @@ class SearchEngineBase extends Control { _initContainer (options) { const element = this.element = document.createElement("div"); element.className = "GPwidget gpf-widget"; - element.id = "GPsearchEngine-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + element.id = Helper.getUid("GPsearchEngine-"); // Main container const container = this.container = document.createElement("form"); container.className = "fr-search-bar"; - container.id = "GPsearchInput-Base-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + container.id = Helper.getUid("GPsearchInput-Base-"); // Création du bouton if (!options.target && options.collapsible) { this.button = document.createElement("button"); - this.button.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + this.button.id = Helper.getUid("GPshowSearchEnginePicto-"); this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn fr-icon-search-line fr-btn fr-btn--lg"; this.button.setAttribute("aria-pressed", "true"); // this.button.setAttribute("type", "submit"); @@ -274,7 +275,7 @@ class SearchEngineBase extends Control { const input = this.input = document.createElement("input"); input.type = "text"; input.className = "GPsearchInputText fr-input"; - input.id = "GPsearchInputText-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + input.id = Helper.getUid("GPsearchInputText-"); input.placeholder = options.placeholder; input.autocomplete = "off"; input.setAttribute("aria-label", options.ariaLabel); @@ -286,9 +287,9 @@ class SearchEngineBase extends Control { search.appendChild(this.optionscontainer); // Submit button - const submit = document.createElement("button"); + const submit = this.subimtBt = document.createElement("button"); submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; - submit.id = "GPshowSearchEnginePicto-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + submit.id = Helper.getUid("GPshowSearchEnginePicto-"); submit.type = "submit"; if (options.title) { submit.textContent = options.title; @@ -298,7 +299,7 @@ class SearchEngineBase extends Control { // Autocomplete container const acContainer = document.createElement("div"); - acContainer.className = "GPautoCompleteContainer"; + acContainer.className = "GPautoCompleteContainer GPelementHidden gpf-hidden"; container.appendChild(acContainer); // Autocomplete list @@ -307,8 +308,8 @@ class SearchEngineBase extends Control { acContainer.appendChild(autocompleteHeader); const autocompleteList = this.autocompleteList = document.createElement("ul"); - autocompleteList.className = "GPautoCompleteList GPelementHidden gpf-hidden"; - autocompleteList.id = "GPautoCompleteList-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)); + autocompleteList.className = "GPautoCompleteList"; + autocompleteList.id = Helper.getUid("GPautoCompleteList-"); autocompleteList.setAttribute("role", "listbox"); autocompleteList.setAttribute("tabindex", "-1"); autocompleteList.setAttribute("aria-label", "Propositions"); @@ -320,17 +321,17 @@ class SearchEngineBase extends Control { // Input controller for accessibility input.setAttribute("role", "combobox"); - input.setAttribute("aria-controls", autocompleteList.id); + input.setAttribute("aria-controls", acContainer.id); input.setAttribute("aria-expanded", "false"); input.setAttribute("aria-autocomplete", "list"); input.setAttribute("aria-haspopup", "listbox"); input.addEventListener("focus", () => { input.setAttribute("aria-expanded", "true"); - autocompleteList.classList.add("gpf-visible"); - autocompleteList.classList.remove("gpf-hidden"); - autocompleteList.classList.add("GPelementVisible"); - autocompleteList.classList.remove("GPelementHidden"); + acContainer.classList.add("gpf-visible"); + acContainer.classList.remove("gpf-hidden"); + acContainer.classList.add("GPelementVisible"); + acContainer.classList.remove("GPelementHidden"); }); input.addEventListener("blur", (e) => { // N'agit que si le focus est hors de l'élément @@ -339,14 +340,20 @@ class SearchEngineBase extends Control { } else { setTimeout(() => { input.setAttribute("aria-expanded", "false"); - autocompleteList.classList.remove("gpf-visible"); - autocompleteList.classList.add("gpf-hidden"); - autocompleteList.classList.remove("GPelementVisible"); - autocompleteList.classList.add("GPelementHidden"); + acContainer.classList.remove("gpf-visible"); + acContainer.classList.add("gpf-hidden"); + acContainer.classList.remove("GPelementVisible"); + acContainer.classList.add("GPelementHidden"); }, 100); } }); } + + setActive (active) { + this.input.disabled = !!active; + this.subimtBt.disabled = !!active; + } + /** Autocomplete and update list * @param {String} [value] input value * @param {Boolean} [force=false] force to add in historic @@ -421,6 +428,8 @@ class SearchEngineBase extends Control { * @param {string} [type="search"] Optionnel. Type à inclure. Valeur autorisée : "history", "search" */ _updateList (tab, type = "search") { + this.autocompleteList.parentNode.dataset.type = type; + // tab = (tab || []).slice(0, this.get("maximumEntries") || 10); // Accessibility this.autocompleteList.querySelectorAll("li").forEach(li => li.classList.remove("active")); @@ -431,7 +440,7 @@ class SearchEngineBase extends Control { const iconClass = typeClasses[type] || typeClasses["search"]; tab.forEach((item, idx) => { const li = document.createElement("li"); - li.id = "GPsearchHistoric-" + (window.ol.getUid ? window.ol.getUid(this) : getUid(this)) + "-" + idx; + li.id = Helper.getUid("GPsearchHistoric-"); li.className = `GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; li.setAttribute("role", "option"); li.setAttribute("data-idx", idx); diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 785b03148..852e63a9f 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -115,7 +115,16 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. */ setMap (map) { + // Remove controls from the current map + if (this.getMap()) { + this.getMap().removeLayer(this.extent); + this.getMap().removeLayer(this.layer); + this.getMap().removeInteraction(this.selectInteraction); + this.getMap().removeOverlay(this.popup); + } + // Init map super.setMap(map); + // Add the control to the new map if (map) { map.addLayer(this.extent); map.addLayer(this.layer); diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 746aab571..7b0121401 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -163,6 +163,7 @@ class AbstractSearchService extends BaseObject { /** * @param {SearchOptions} obj * @abstract + * @returns {String} */ getItemTitle (obj) { return obj; @@ -191,7 +192,7 @@ class DefaultSearchService extends AbstractSearchService { /** Autocomplete function * Dispatchs "searchstart" event when search starts * Dispatchs "search" event when search is finished - * @param {String} search + * @param {String} value Valeur de l'autocomplete * @param {Object} [options] * @param {String} options.force force search even if search string is less than minChars / enter is pressed * @api @@ -213,7 +214,7 @@ class DefaultSearchService extends AbstractSearchService { } /** - * @param {SearchOptions} obj + * @param {SearchOptions} obj Search options */ search (obj) { this.dispatchEvent({ @@ -236,7 +237,7 @@ class IGNSearchService extends AbstractSearchService { /** * @constructor - * @param {AbstractSearchServiceOptions} options + * @param {AbstractSearchServiceOptions} options options */ constructor (options) { options = options || {}; @@ -334,11 +335,10 @@ class IGNSearchService extends AbstractSearchService { /** - * @param {AutocompleteOptions} obj - * @param {String} obj.value Valeur de l'autocomplete + * @param {String} value Valeur de l'autocomplete * @abstract */ - autocomplete (value, obj) { + autocomplete (value) { if (!value) { return; } diff --git a/src/packages/Utils/Helper.js b/src/packages/Utils/Helper.js index 067b49a82..8c4504c81 100644 --- a/src/packages/Utils/Helper.js +++ b/src/packages/Utils/Helper.js @@ -1,3 +1,5 @@ +let uidCounter_ = 0; + /** * @module Helper * @alias module:~utils/HelperUtils @@ -99,6 +101,24 @@ var Helper = { } } } + }, + /** + * Gets a unique ID for an object. This mutates the object so that further calls + * with the same object as a parameter returns the same value. Unique IDs are generated + * as a strictly increasing sequence. Adapted from goog.getUid. + * + * @param {String} prefix - prefix for the unique ID + * @param {Object} [obj] The object to get the unique ID for. + * @return {String} The unique ID for the object. + * @api + */ + getUid : function (prefix, obj) { + if (obj) { + if (obj.id) { + return obj.id; + } + } + return prefix + (++uidCounter_); } }; From 28decb15046b51b055ba681d5d595d7f3550261b Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 13 Oct 2025 15:18:36 +0200 Subject: [PATCH 13/73] FIX Me localiser + ajout sur la carte --- .../Controls/SearchEngine/GPFsearchEngine.css | 1 + .../SearchEngine/SearchEngineAdvanced.js | 55 ++++++++++++++++++- .../Controls/SearchEngine/SearchEngineBase.js | 2 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 67a2941ea..112022f90 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -196,6 +196,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ list-style: none; overflow: hidden; flex-direction: column; + width: 100%; } [id^="GPsearchEngine"] ul li { overflow: hidden; diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index a62a51471..3f6d7359d 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -1,4 +1,7 @@ import Control from "ol/control/Control"; +import Geolocation from "ol/Geolocation"; +import OlFeature from "ol/Feature"; +import Point from "ol/geom/Point"; import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; import Helper from "../../Utils/Helper"; @@ -20,9 +23,20 @@ class SearchEngineAdvanced extends Control { // call ol.control.Control constructor super(options); + + // Geolocation + this.geolocation = new Geolocation({ + // enableHighAccuracy must be set to true to have the heading value. + trackingOptions : { + enableHighAccuracy : true, + }, + projection : "EPSG:4326", + }); + + // Initialize this.initialize(options); this._initContainer(options); - // this._initEvents(options); + this._initEvents(options); } /** @@ -54,6 +68,41 @@ class SearchEngineAdvanced extends Control { } } + _initEvents (options) { + this.geolocation.on("change:position", () => { + const pt = new Point(this.geolocation.getPosition()); + pt.transform("EPSG:4326", this.getMap().getView().getProjection()); + const evt = this.addResultToMap (pt, "Ma localisation"); + this.dispatchEvent(evt); + this.geolocation.setTracking(false); + }); + } + + /** Display result on map + * @param {Object|Point|OlFeature} e objet a afficher + * @param {String} [info] Popup info + */ + addResultToMap (obj, info) { + let evt = obj; + if (obj instanceof OlFeature) { + evt = { + result : obj, + extent : null + }; + } else if (obj instanceof Point) { + evt = { + result : new OlFeature(obj), + extent : null + }; + } + if (info) { + evt.result.set(info); + } + evt.type = "search"; + this.baseSearchEngine.addResultToMap(evt); + return evt; + } + /** * Ajoute le contrôle à la carte. * @override @@ -79,6 +128,10 @@ class SearchEngineAdvanced extends Control { locationBtn.innerText = "Me géolocaliser"; locationBtn.className = "GPSearchEngine-locate fr-btn fr-icon-arrow-up-s-line fr-btn--icon-left fr-btn--tertiary-no-outline"; this.baseSearchEngine.autocompleteHeader.appendChild(locationBtn); + locationBtn.addEventListener("click", () => { + this.geolocation.setTracking(true); + console.log("tracking", this.geolocation); + }); // Ajout des options avancées const advancedBtn = document.createElement("button"); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index d81d13a32..dd73fde27 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -335,7 +335,7 @@ class SearchEngineBase extends Control { }); input.addEventListener("blur", (e) => { // N'agit que si le focus est hors de l'élément - if (e.relatedTarget && autocompleteList.contains(e.relatedTarget)) { + if (e.relatedTarget && acContainer.contains(e.relatedTarget)) { input.focus(); } else { setTimeout(() => { From 73280067184935df8cc0df4b27c920fc4d525b2b Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 13 Oct 2025 17:38:47 +0200 Subject: [PATCH 14/73] ADD advanced form list --- .../Controls/SearchEngine/GPFsearchEngine.css | 18 +++-- .../SearchEngine/SearchEngineAdvanced.js | 76 ++++++++++++++++--- 2 files changed, 76 insertions(+), 18 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 112022f90..60a1fe45b 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -218,12 +218,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ padding: 0; max-height: 32px; } -/* -[id^="GPsearchEngine"] form { - display: flex; - flex-direction: column; -} -*/ + [id^="GPsearchEngine"] form > * { display: flex; flex-direction: row; @@ -278,10 +273,13 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } [id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer { display: none; - padding: 1rem; + margin-top: 0.5rem; background-color: var(--background-default-grey); box-sizing: content-box; - width: calc(100% - 2rem); + max-width: 100%; + max-height: unset; + padding: 1rem; + box-sizing: border-box; } [id^="GPsearchEngine-Advanced"]:has(.GPSearchEngine-advanced-btn[aria-expanded="true"]) > .GPAdvancedContainer { display: flex; @@ -298,3 +296,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ cursor: default; user-select: none; } + +[id^="GPsearchEngine-Advanced"] .GPAdvancedContainer[data-open="true"] > button { + display: none; +} \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 3f6d7359d..65bf6acf6 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -33,6 +33,8 @@ class SearchEngineAdvanced extends Control { projection : "EPSG:4326", }); + this._searchForms = ["form1", "form2", "form3"]; + // Initialize this.initialize(options); this._initContainer(options); @@ -96,7 +98,7 @@ class SearchEngineAdvanced extends Control { }; } if (info) { - evt.result.set(info); + evt.result.set("infoPopup", info); } evt.type = "search"; this.baseSearchEngine.addResultToMap(evt); @@ -124,14 +126,7 @@ class SearchEngineAdvanced extends Control { }); // Geolocation - const locationBtn = document.createElement("button"); - locationBtn.innerText = "Me géolocaliser"; - locationBtn.className = "GPSearchEngine-locate fr-btn fr-icon-arrow-up-s-line fr-btn--icon-left fr-btn--tertiary-no-outline"; - this.baseSearchEngine.autocompleteHeader.appendChild(locationBtn); - locationBtn.addEventListener("click", () => { - this.geolocation.setTracking(true); - console.log("tracking", this.geolocation); - }); + this.baseSearchEngine.autocompleteHeader.appendChild(this._getGeolocButton()); // Ajout des options avancées const advancedBtn = document.createElement("button"); @@ -150,7 +145,57 @@ class SearchEngineAdvanced extends Control { advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); this.element.appendChild(advancedContainer); - this.advancedContainer.innerHTML = "Formulaires spécifiques"; + + // Geolocation + advancedContainer.appendChild(this._getGeolocButton()); + + // Formulaires specifiques + this._searchForms.forEach(form => { + const section = document.createElement("section"); + section.className = "fr-accordion"; + advancedContainer.appendChild(section); + const title = document.createElement("h3"); + title.className = "fr-accordion__title"; + section.appendChild(title); + const button = document.createElement("button"); + button.type = "button"; + button.className = "fr-accordion__btn"; + button.setAttribute("aria-expanded", "false"); + button.innerText = form; + section.appendChild(button); + // Accordion + const accordion = document.createElement("div"); + accordion.className = "fr-collapse"; + accordion.id = Helper.getUid("accordion-"); + button.setAttribute("aria-controls", accordion.id); + section.appendChild(accordion); + // Content + const atitle = document.createElement("h4"); + atitle.className = "fr-h4"; + accordion.appendChild(atitle); + const aform = document.createElement("p"); + aform.innerText = "Lorem ipsum dolor sit amet"; + accordion.appendChild(aform); + // Handle collapsing + button.addEventListener("click", () => { + const expanded = button.getAttribute("aria-expanded") === "true"; + advancedContainer.querySelectorAll("section").forEach(sec => { + sec.querySelector(".fr-collapse").classList.remove("fr-collapse--expanded"); + sec.querySelector("button").setAttribute("aria-expanded", "false"); + advancedContainer.dataset.open = !expanded; + if (!expanded) { + sec.classList.add("fr-hidden"); + } else { + sec.classList.remove("fr-hidden"); + } + }); + if (!expanded) { + button.setAttribute("aria-expanded", "true"); + accordion.classList.add("fr-collapse--expanded"); + section.classList.remove("fr-hidden"); + } + }); + }); // Gestion du bouton avancé advancedBtn.setAttribute("aria-controls", advancedContainer.id); @@ -162,6 +207,17 @@ class SearchEngineAdvanced extends Control { }); } + _getGeolocButton () { + const locationBtn = document.createElement("button"); + locationBtn.innerText = "Me géolocaliser"; + locationBtn.className = "GPSearchEngine-locate fr-btn fr-btn--sm fr-btn--icon-left fr-btn--tertiary-no-outline gpf-btn-icon-search-geolocate"; + locationBtn.addEventListener("click", () => { + this.geolocation.setTracking(true); + console.log("tracking", this.geolocation); + }); + return locationBtn; + } + } export default SearchEngineAdvanced; From b8b0a8c27d679677d19186abd4c3dc24c3fdfe31 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 14 Oct 2025 15:43:22 +0200 Subject: [PATCH 15/73] feat(advanced-search): Ajout AbstractResearch + ajout dans le build webpack --- build/webpack/controls.webpack.config.js | 2 + build/webpack/extend.themes.webpack.js | 2 + build/webpack/modules.webpack.config.js | 2 + demos/angular-project/angular.json | 1 + ...ginebase-modules-dsfr-geocodeAdvanced.html | 13 +- .../DSFRadvancedSearchEngineStyle.css | 5 + .../SearchEngine/DSFRsearchEngineStyle.css | 8 +- .../SearchEngine/GPFadvancedSearchEngine.css | 6 + .../GPFadvancedSearchEngineStyle.css | 1 + .../Controls/SearchEngine/GPFsearchEngine.css | 15 +- .../SearchEngine/GPFsearchEngineStyle.css | 4 + .../SearchEngine/AbstractAdvancedResearch.js | 171 ++++++++++++++++++ .../SearchEngine/InseeAdvancedResearch.js | 62 +++++++ .../SearchEngine/SearchEngineAdvanced.js | 24 ++- 14 files changed, 298 insertions(+), 18 deletions(-) create mode 100644 src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css create mode 100644 src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css create mode 100644 src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css create mode 100644 src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js create mode 100644 src/packages/Controls/SearchEngine/InseeAdvancedResearch.js diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index de19729ad..65c47239c 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -77,6 +77,8 @@ module.exports = (env, argv) => { case "SearchEngineBase": case "SearchEngineGeocodeIGN": case "SearchEngineAdvanced": + case "AbstractAdvancedResearch": + case "InseeAdvancedResearch": // crs break; case "MeasureArea": diff --git a/build/webpack/extend.themes.webpack.js b/build/webpack/extend.themes.webpack.js index 0a50b5c1a..e981cc481 100644 --- a/build/webpack/extend.themes.webpack.js +++ b/build/webpack/extend.themes.webpack.js @@ -26,6 +26,7 @@ module.exports = (env, argv) => { path.join(rootdir, "src", "packages", "CSS", "Controls/ReverseGeocoding", "GPFreverseGeocodingStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/Route", "GPFrouteStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "GPFsearchEngineStyle.css"), + path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "GPFadvancedSearchEngineStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/ToolBoxMeasure", "GPFtoolBoxMeasureStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/Zoom", "GPFzoomStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/FullScreen", "GPFfullScreenStyle.css"), @@ -61,6 +62,7 @@ module.exports = (env, argv) => { path.join(rootdir, "src", "packages", "CSS", "Controls/ReverseGeocoding", "DSFRreverseGeocodingStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/Route", "DSFRrouteStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "DSFRsearchEngineStyle.css"), + path.join(rootdir, "src", "packages", "CSS", "Controls/SearchEngine", "DSFRadvancedSearchEngineStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/ToolBoxMeasure", "DSFRtoolBoxMeasureStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/Zoom", "DSFRzoomStyle.css"), path.join(rootdir, "src", "packages", "CSS", "Controls/FullScreen", "DSFRfullScreenStyle.css"), diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index fdea276fd..b68945841 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -48,6 +48,8 @@ module.exports = (env, argv) => { "GpfExtOlSearchEngineBase" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineBase.js"), "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"), "GpfExtOlSearchEngineAdvanced" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineAdvanced.js"), + "GpfExtOlInseeAdvancedResearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedResearch.js"), + "GpfExtOlAbstractAdvancedResearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedResearch.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), "GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"), "GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"), diff --git a/demos/angular-project/angular.json b/demos/angular-project/angular.json index fc1bbbf42..26faa96e5 100644 --- a/demos/angular-project/angular.json +++ b/demos/angular-project/angular.json @@ -38,6 +38,7 @@ "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/MousePosition/GPFmousePosition.css", "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/Export/GPFexport.css", "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css", + "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css", "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/Editor/GPFeditor.css", "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/ReverseGeocoding/GPFreverseGeocoding.css", "node_modules/geopf-extensions-openlayers/src/packages/CSS/Controls/Drawing/GPFdrawing.css", diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 57453cf74..8fc9ceb7b 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -3,7 +3,9 @@ {{#content "vendor"}} + + {{/content}} {{#content "head"}} @@ -56,8 +58,17 @@

Ajout du moteur de recherche avec les options par défaut

] }); + var insee = new ol.control.InseeAdvancedResearch({ + name : "Code insee", + }); + var insee2 = new ol.control.InseeAdvancedResearch({ + name : "Code insee 2", + }); + // 2. Appel du SearchEngine - var search = new ol.control.SearchEngineAdvanced(); + var search = new ol.control.SearchEngineAdvanced({ + advancedResearch : [insee, insee2] + }); // 3. Ajout du SearchEngine à la carte map.addControl(search); diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css new file mode 100644 index 000000000..9b9580d0a --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -0,0 +1,5 @@ +/** STYLE DSFR RECHERCHE AVANCEE **/ + +form[id^=GPAdvancedForm-] > .GPFormFooter { + justify-content: center; +} \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index cd3ee94a2..ce9557791 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -267,6 +267,10 @@ div[id^=GPgeocodeResults-] { } } +[id^="GPsearchEngine"] .GPautoCompleteContainer { + background-color: var(--background-default-grey); +} + [id^="GPsearchEngine"] ul { padding: 12px; padding-bottom: 0px; @@ -289,12 +293,12 @@ div[id^=GPgeocodeResults-] { [id^="GPsearchEngine"] ul li.active, [id^="GPsearchEngine"] ul li:hover { - color: #000000; + /* color: #000000; */ background-color: var(--background-default-grey-hover); } [id^="GPsearchEngine"] ul li:active { - color: #000000; + /* color: #000000; */ background-color: var(--background-default-grey-active); } diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css new file mode 100644 index 000000000..a6c6c0fe4 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -0,0 +1,6 @@ +/** STYLE COMMUN RECHERCHE AVANCEE **/ + +form[id^=GPAdvancedForm-] > .GPFormFooter { + display: flex; + flex-wrap: wrap; +} \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css new file mode 100644 index 000000000..69223b815 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css @@ -0,0 +1 @@ +/** STYLE NON DSFR RECHERCHE AVANCEE **/ diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 60a1fe45b..7d4da0adc 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -191,7 +191,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^="GPsearchEngine"] .GPautoCompleteContainer { position: absolute; - background-color: white; box-shadow: 0 2px 6px 0 rgba(0, 0, 18, 0.16); list-style: none; overflow: hidden; @@ -219,34 +218,34 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ max-height: 32px; } -[id^="GPsearchEngine"] form > * { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] > * { display: flex; flex-direction: row; } -[id^="GPsearchEngine"] form .GPInputGroup { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPInputGroup { padding: 0; } -[id^="GPsearchEngine"] form .GPOptionsContainer { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer { display: flex; height: calc(100% - 2px); } -[id^="GPsearchEngine"] form .GPOptionsContainer button { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer button { max-width: unset; min-height: 1rem; line-height: 1rem; margin: 0.25rem 0.5rem; border-radius: 0; } -[id^="GPsearchEngine"] form .GPOptionsContainer button:before { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer button:before { display: none; } -[id^="GPsearchEngine"] form .GPautoCompleteHeader button { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPautoCompleteHeader button { margin: 12px 12px 0; max-width: unset; } -[id^="GPsearchEngine"] form .GPautoCompleteFooter button { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPautoCompleteFooter button { margin: 0 12px 12px; max-width: unset; } diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css index 6fa65f153..c3c78eb7b 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css @@ -214,6 +214,10 @@ div[id^=GPgeocodeResults-] { padding: 0; } +[id^="GPsearchEngine"] .GPautoCompleteContainer { + background-color: white; +} + [id^="GPsearchEngine"] ul { margin: 3px 0; width: 100%; diff --git a/src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js b/src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js new file mode 100644 index 000000000..09ec965cd --- /dev/null +++ b/src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js @@ -0,0 +1,171 @@ +// import CSS +import "../../CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css"; +import Control from "../Control"; +import Logger from "../../Utils/LoggerByDefault"; +import { Collection } from "ol"; +import Helper from "../../Utils/Helper"; + +var logger = Logger.getLogger("abstractAdvancedResearch"); + +/** + * @typedef {Object} AbstractAdvancedResearchOptions Options du constructeur pour le contrôle de recherche. + * + * @property {string} name - Nom de la recherche avancée. + */ + +/** + * @classdesc + * AbstractAdvancedResearch Base control + * + * @alias ol.control.AbstractAdvancedResearch + * @module AbstractAdvancedResearch +*/ +class AbstractAdvancedResearch extends Control { + + /** + * @constructor + * @param {AbstractAdvancedResearchOptions} options Options du constructeur + * + * @example + */ + constructor (options) { + options = options || {}; + // call ol.control.Control constructor + super(options); + + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "AbstractAdvancedResearch"; + + // initialisation du composant + this.initialize(options); + + this.element = this._initContainer(options); + this._initEvents(options); + } + /** + * Initialize SearchEngine control (called by SearchEngine constructor) + * + * @param {AbstractAdvancedResearchOptions} options - constructor options + * @protected + */ + initialize (options) { + if (!options.name) { + throw new SyntaxError("`name` is mandatory"); + } else { + this.name = options.name; + } + + this.inputs = []; + } + + getName () { + return this.name; + } + + getContent () { + return this.containerthis.name; + } + + /** + * + * @param {AbstractAdvancedResearchOptions} options + * @returns {HTMLFormElement} Élément du formulaire + * @protected + */ + _initContainer (options) { + let element = document.createElement("form"); + element.className = "GPForm gpf-advanced-search-form"; + element.id = Helper.getUid("GPAdvancedForm-"); + + this.addInputs(); + this.inputs.forEach((elem) => { + element.appendChild(elem); + }); + + const btnGroup = document.createElement("div"); + btnGroup.className = "GPFormFooter"; + + const eraseBtn = this.eraseBtn = document.createElement("button"); + eraseBtn.className = "GPBtn gpf-btn fr-btn fr-btn--tertiary"; + eraseBtn.id = Helper.getUid("GPEraseBtn-"); + eraseBtn.textContent = "Effacer"; + + const searchBtn = this.searchBtn = document.createElement("button"); + searchBtn.className = "GPBtn gpf-btn fr-btn"; + searchBtn.id = Helper.getUid("GPSearchBtn-"); + searchBtn.type = "submit"; + searchBtn.textContent = "Rechercher"; + searchBtn.setAttribute("form", element.id); + + btnGroup.appendChild(eraseBtn); + btnGroup.appendChild(searchBtn); + + element.appendChild(btnGroup); + + return element; + } + + /** + * Ajoute des éléments d'input dans la collection `this.inputs`; + * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * @protected + * @abstract + */ + addInputs () { + + } + + + /** Add event listeners + * @param {AbstractAdvancedResearchOptions} options - constructor options + * @protected + */ + _initEvents (options) { + this.eraseBtn.onclick = this._onErase.bind(this); + /** + * Fonction de recherche + * @param {PointerEvent} e + */ + const onSearch = function (e) { + e.preventDefault(); + this._onSearch(e); + // TODO : AJOUTER ÉVÉNEMENT ONSEARCH ? + }; + this.searchBtn.onclick = onSearch.bind(this); + } + + /** + * + * @param {PointerEvent} e + * @protected + */ + _onErase (e) { + this.inputs.forEach(input => { + if (input.value !== undefined) { + input.value = null; + } + }); + } + + + /** + * + * @param {PointerEvent} e + * @abstract + * @protected + */ + _onSearch (e) { + + } + +} + +export default AbstractAdvancedResearch; + +// Expose AbstractAdvancedResearch as ol.control.AbstractAdvancedResearch (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.AbstractAdvancedResearch = AbstractAdvancedResearch; +} diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedResearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedResearch.js new file mode 100644 index 000000000..dfa367ade --- /dev/null +++ b/src/packages/Controls/SearchEngine/InseeAdvancedResearch.js @@ -0,0 +1,62 @@ +// import CSS +import AbstractAdvancedResearch from "./AbstractAdvancedResearch"; +import Logger from "../../Utils/LoggerByDefault"; +import Collection from "ol/Collection"; +import Helper from "../../Utils/Helper"; + +var logger = Logger.getLogger("abstractAdvancedResearch"); + + +/** + * @classdesc + * AbstractAdvancedResearch Base control + * + * @alias ol.control.InseeAdvancedResearch + * @module InseeAdvancedResearch +*/ +class InseeAdvancedResearch extends AbstractAdvancedResearch { + + /** + * @constructor + * @example + */ + constructor (options) { + super(options); + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "InseeAdvancedResearch"; + } + + /** + * Ajoute des éléments d'input dans la collection `this.inputs`; + * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * @protected + * @abstract + */ + addInputs () { + super.addInputs(); + const inseeInput = document.createElement("input"); + inseeInput.type = "text"; + this.inputs.push(inseeInput); + } + + /** + * + * @param {PointerEvent} e + * @abstract + * @protected + */ + _onSearch (e) { + console.log(e); + } + +} + +export default InseeAdvancedResearch; + +// Expose InseeAdvancedResearch as ol.control.InseeAdvancedResearch (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.InseeAdvancedResearch = InseeAdvancedResearch; +} diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 65bf6acf6..b5d7c9009 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -33,7 +33,6 @@ class SearchEngineAdvanced extends Control { projection : "EPSG:4326", }); - this._searchForms = ["form1", "form2", "form3"]; // Initialize this.initialize(options); @@ -53,6 +52,13 @@ class SearchEngineAdvanced extends Control { * @private */ this.CLASSNAME = "SearchEngineAdvanced"; + + if (options.advancedResearch && options.advancedResearch instanceof Array) { + this._searchForms = options.advancedResearch; + } else { + this._searchForms = []; + } + console.log(this._searchForms); } /** @@ -61,6 +67,7 @@ class SearchEngineAdvanced extends Control { * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. */ setMap (map) { + console.log("set map"); if (this.getMap() && this.baseSearchEngine) { this.getMap().removeControl(this.baseSearchEngine); } @@ -68,6 +75,9 @@ class SearchEngineAdvanced extends Control { if (this.baseSearchEngine) { this.baseSearchEngine.setMap(map); } + this._searchForms.forEach(research => { + research.setMap(map); + }); } _initEvents (options) { @@ -150,7 +160,7 @@ class SearchEngineAdvanced extends Control { advancedContainer.appendChild(this._getGeolocButton()); // Formulaires specifiques - this._searchForms.forEach(form => { + this._searchForms.forEach(research => { const section = document.createElement("section"); section.className = "fr-accordion"; advancedContainer.appendChild(section); @@ -161,7 +171,7 @@ class SearchEngineAdvanced extends Control { button.type = "button"; button.className = "fr-accordion__btn"; button.setAttribute("aria-expanded", "false"); - button.innerText = form; + button.innerText = research.getName(); section.appendChild(button); // Accordion const accordion = document.createElement("div"); @@ -173,10 +183,10 @@ class SearchEngineAdvanced extends Control { const atitle = document.createElement("h4"); atitle.className = "fr-h4"; accordion.appendChild(atitle); - const aform = document.createElement("p"); - aform.innerText = "Lorem ipsum dolor sit amet"; - accordion.appendChild(aform); - // Handle collapsing + + // Contenu recherche avancée + research.setTarget(accordion); + button.addEventListener("click", () => { const expanded = button.getAttribute("aria-expanded") === "true"; advancedContainer.querySelectorAll("section").forEach(sec => { From 19a8f724107994f79238e83f03000fb01ff6941d Mon Sep 17 00:00:00 2001 From: viglino Date: Thu, 16 Oct 2025 12:14:35 +0200 Subject: [PATCH 16/73] ADD LocationAdvancedSearch --- build/webpack/controls.webpack.config.js | 1 + build/webpack/modules.webpack.config.js | 1 + ...ginebase-modules-dsfr-geocodeAdvanced.html | 5 +- .../Controls/SearchEngine/GPFsearchEngine.css | 39 ++++- .../SearchEngine/LocationAdvancedSearch.js | 160 ++++++++++++++++++ .../SearchEngine/SearchEngineAdvanced.js | 1 - .../Controls/SearchEngine/SearchEngineBase.js | 6 +- 7 files changed, 202 insertions(+), 11 deletions(-) create mode 100644 src/packages/Controls/SearchEngine/LocationAdvancedSearch.js diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 65c47239c..9ad3a3f3f 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -77,6 +77,7 @@ module.exports = (env, argv) => { case "SearchEngineBase": case "SearchEngineGeocodeIGN": case "SearchEngineAdvanced": + case "LocationAdvancedSearch": case "AbstractAdvancedResearch": case "InseeAdvancedResearch": // crs diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index b68945841..2b43ef0a8 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -48,6 +48,7 @@ module.exports = (env, argv) => { "GpfExtOlSearchEngineBase" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineBase.js"), "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"), "GpfExtOlSearchEngineAdvanced" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineAdvanced.js"), + "GpfExtOlLocationAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "LocationAdvancedSearch.js"), "GpfExtOlInseeAdvancedResearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedResearch.js"), "GpfExtOlAbstractAdvancedResearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedResearch.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 8fc9ceb7b..6e86c040b 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -6,6 +6,7 @@ + {{/content}} {{#content "head"}} @@ -64,10 +65,12 @@

Ajout du moteur de recherche avec les options par défaut

var insee2 = new ol.control.InseeAdvancedResearch({ name : "Code insee 2", }); + var location = new ol.control.LocationAdvancedSearch({ + }) // 2. Appel du SearchEngine var search = new ol.control.SearchEngineAdvanced({ - advancedResearch : [insee, insee2] + advancedResearch : [insee, insee2, location] }); // 3. Ajout du SearchEngine à la carte diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 7d4da0adc..3f3858966 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -196,6 +196,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ overflow: hidden; flex-direction: column; width: 100%; + z-index: 1; } [id^="GPsearchEngine"] ul li { overflow: hidden; @@ -261,7 +262,13 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^="GPsearchEngine-Advanced"] { display: flex; flex-direction: column; + max-height: calc(100% - 2rem); + overflow: hidden; } +[id^="GPsearchEngine-Advanced"] [id^="GPsearchEngine-"]{ + max-height: unset; +} + [id^="GPsearchEngine-Advanced"] [id^="GPsearchEngine-"] { position: static; } @@ -272,7 +279,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } [id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer { display: none; - margin-top: 0.5rem; background-color: var(--background-default-grey); box-sizing: content-box; max-width: 100%; @@ -288,14 +294,35 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } [id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled { - background-color: var(--background-action-high-blue-france); + background-color: var(--background-action-high-blue-france); } [id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled, [id^="GPsearchEngine-Advanced"] .GPsearchInputText:disabled { - cursor: default; - user-select: none; + cursor: default; + user-select: none; } [id^="GPsearchEngine-Advanced"] .GPAdvancedContainer[data-open="true"] > button { - display: none; -} \ No newline at end of file + display: none; +} + +[id^="GPsearchEngine-Advanced"] section { + overflow: hidden auto; +} +[id^="GPsearchEngine-Advanced"] [data-open="true"] section:before { + display: none; +} + +[id^="GPsearchEngine-Advanced"] section .fr-collapse { + box-sizing: border-box; +} + +[id^="GPsearchEngine-Advanced"] [id^="GPsearchInput-Base-"] .GPsearchInputText { + border-radius: 0.25rem 0.25rem 0 0; +} +[id^="GPsearchEngine-Advanced"] section [id^="GPsearchInput-Base-"] .GPsearchInputSubmit { + display: none; +} +[id^="GPsearchEngine-Advanced"] section [id^="GPsearchEngine-"] { + margin-top: 0.5rem; +} diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js new file mode 100644 index 000000000..2b352ce35 --- /dev/null +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -0,0 +1,160 @@ +import Helper from "../../Utils/Helper"; +import AbstractAdvancedResearch from "./AbstractAdvancedResearch"; +import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; + +class LocationAdvancedSearch extends AbstractAdvancedResearch { + + /** + * @constructor + * @param {AbstractAdvancedResearchOptions} options Options du constructeur + * + * @example + */ + constructor (options) { + options = options || {}; + + options.name = options.name || "Lieux et toponymes"; + + // call ol.control.Control constructor + super(options); + + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "LocationAdvancedSearch"; + + this.element.addEventListener("submit", e => { + e.preventDefault(); + console.log(e); + }); + } + + setMap (map) { + super.setMap(map); + this.search.setMap(map); + } + _getLabelContainer (text, type, input) { + const container = document.createElement("div"); + container.className = type; + this.inputs.push(container); + const label = document.createElement("label"); + label.className = "fr-label"; + label.innerText = text; + container.appendChild(label); + if (input) { + label.setAttribute("for", input.id); + container.appendChild(input); + } + return container; + } + /** Add inputs + * + */ + addInputs () { + super.addInputs(); + // Type + const typeSelect = document.createElement("select"); + typeSelect.className = "fr-select"; + typeSelect.id = typeSelect.name = Helper.getUid("LocationAdvancedSearch-type-"); + this._getLabelContainer("Type", "fr-select-group", typeSelect); + /* Liste des types + fetch("https://data.geopf.fr/geocodage/getCapabilities").then(response => { + return response.json(); + }).then (json => { + console.log(json); + }).catch(() => { + console.log("error"); + }); + */ + const typeList = [ "Administratif", "Aérodrome", "Cimetière", "Construction ponctuelle", "Construction surfacique", "Cours d'eau" ]; + typeList.forEach(k => { + const typeOption = document.createElement("option"); + typeOption.value = k.toLowerCase(); + typeOption.innerText = k; + typeSelect.appendChild(typeOption); + }); + typeSelect.addEventListener("change", (e) => { + this.filter.category = typeSelect.value; + }); + + // Search input + const searchContainer = this._getLabelContainer("Renseigner un lieu", "fr-input-group"); + this.search = new SearchEngineGeocodeIGN({ + target : searchContainer, + historic : "GPAdvancedLocation", + maximumEntries : 0 + }); + this.search.container.addEventListener("submit", e => { + e.stopPropagation(); + e.preventDefault(); + this._onSearch(); + }, true); + + // Code postal + const postalInput = document.createElement("input"); + postalInput.className = "fr-input"; + postalInput.type = "text"; + postalInput.id = Helper.getUid("LocationAdvancedSearch-postal-"); + this._getLabelContainer("Code postal", "fr-input-group", postalInput); + postalInput.addEventListener("change", () => { + this.filter.postcode = postalInput.value; + }); + + // Code INSEE + const inseeInput = document.createElement("input"); + inseeInput.className = "fr-input"; + inseeInput.type = "text"; + inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); + this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput); + inseeInput.addEventListener("change", () => { + this.filter.citycode = inseeInput.value; + }); + + this.filter = { + category : typeSelect.value, + postcode : postalInput.value, + citycode : inseeInput.value + }; + } + _onErase (e) { + this.element.querySelectorAll("select").forEach(input => { + input.value = ""; + }); + this.element.querySelectorAll("input").forEach(input => { + input.value = ""; + }); + this.filters = { + category : "", + postcode : "", + citycode : "" + }; + } + /** Lancer une recheche + * + */ + _onSearch () { + console.log("search", this); + const value = this.search.input.value; + if (value) { + this.search.searchService._requestGeocoding({ + "index" : "poi", + "limit" : 1, + maximumResponses : 1, + filters : this.filter, + "returnTrueGeometry" : true, + "location" : value, + onSuccess : e => this.search.searchService._onSuccessSearch(e), + onFailure : e => console.log("ERROR") + }); + } + } + +} + +export default LocationAdvancedSearch; + +// Expose LocationAdvancedSearch as ol.control.LocationAdvancedSearch (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.LocationAdvancedSearch = LocationAdvancedSearch; +} diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index b5d7c9009..e2ab0a764 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -67,7 +67,6 @@ class SearchEngineAdvanced extends Control { * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. */ setMap (map) { - console.log("set map"); if (this.getMap() && this.baseSearchEngine) { this.getMap().removeControl(this.baseSearchEngine); } diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index dd73fde27..35a54d421 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -113,7 +113,7 @@ class SearchEngineBase extends Control { initialize (options) { // Valeurs par défaut des options options.minChars = options.minChars ? options.minChars : 3; - options.maximumEntries = options.maximumEntries ? options.maximumEntries : 5; + options.maximumEntries = (typeof options.maximumEntries === "number") ? options.maximumEntries : 5; options.historic = (typeof options.historic === "string" ? options.historic : this.CLASSNAME); options.title = options.title ? options.title : "Rechercher"; options.ariaLabel = options.ariaLabel ? options.ariaLabel : "Rechercher"; @@ -207,7 +207,7 @@ class SearchEngineBase extends Control { e.preventDefault(); const list = Array.from(this.autocompleteList.querySelectorAll("li")); - if (e.submitter && e.submitter.type === "submit") { + if (e && e.submitter && e.submitter.type === "submit") { // Si on appuie sur le bouton, on vérifie que l'input ne soit pas vide let input = e.target.querySelector("input"); const value = input.value; @@ -430,7 +430,7 @@ class SearchEngineBase extends Control { _updateList (tab, type = "search") { this.autocompleteList.parentNode.dataset.type = type; // - tab = (tab || []).slice(0, this.get("maximumEntries") || 10); + tab = (tab || []).slice(0, this.get("maximumEntries")); // Accessibility this.autocompleteList.querySelectorAll("li").forEach(li => li.classList.remove("active")); this.input.setAttribute("aria-activedescendant", ""); From b1cc79e79394212e9953e1d6f11e164fe1549b2f Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 16 Oct 2025 17:00:14 +0200 Subject: [PATCH 17/73] =?UTF-8?q?feat(search):=20Ajout=20location=20avanc?= =?UTF-8?q?=C3=A9e=20code=20INSEE=20+=20merge=20commits?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/webpack/controls.webpack.config.js | 4 +- build/webpack/modules.webpack.config.js | 4 +- ...ginebase-modules-dsfr-geocodeAdvanced.html | 15 +- .../DSFRadvancedSearchEngineStyle.css | 15 +- .../SearchEngine/DSFRsearchEngineStyle.css | 4 +- .../SearchEngine/GPFadvancedSearchEngine.css | 28 ++ .../Controls/SearchEngine/GPFsearchEngine.css | 39 +-- ...dResearch.js => AbstractAdvancedSearch.js} | 87 ++--- .../SearchEngine/InseeAdvancedResearch.js | 62 ---- .../SearchEngine/InseeAdvancedSearch.js | 111 +++++++ .../SearchEngine/LocationAdvancedSearch.js | 35 +- .../SearchEngine/SearchEngineAdvanced.js | 266 +++++++++++++-- .../Controls/SearchEngine/SearchEngineBase.js | 306 +++++++++++------- .../SearchEngine/SearchEngineGeocodeIGN.js | 235 +------------- src/packages/Controls/SearchEngine/Service.js | 117 ++++++- 15 files changed, 805 insertions(+), 523 deletions(-) rename src/packages/Controls/SearchEngine/{AbstractAdvancedResearch.js => AbstractAdvancedSearch.js} (58%) delete mode 100644 src/packages/Controls/SearchEngine/InseeAdvancedResearch.js create mode 100644 src/packages/Controls/SearchEngine/InseeAdvancedSearch.js diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 9ad3a3f3f..8c1f7d938 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -78,8 +78,8 @@ module.exports = (env, argv) => { case "SearchEngineGeocodeIGN": case "SearchEngineAdvanced": case "LocationAdvancedSearch": - case "AbstractAdvancedResearch": - case "InseeAdvancedResearch": + case "AbstractAdvancedSearch": + case "InseeAdvancedSearch": // crs break; case "MeasureArea": diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 2b43ef0a8..296846674 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -49,8 +49,8 @@ module.exports = (env, argv) => { "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"), "GpfExtOlSearchEngineAdvanced" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineAdvanced.js"), "GpfExtOlLocationAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "LocationAdvancedSearch.js"), - "GpfExtOlInseeAdvancedResearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedResearch.js"), - "GpfExtOlAbstractAdvancedResearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedResearch.js"), + "GpfExtOlInseeAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedSearch.js"), + "GpfExtOlAbstractAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedSearch.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), "GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"), "GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 6e86c040b..a1e1e95f0 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -3,9 +3,9 @@ {{#content "vendor"}} - + - + {{/content}} @@ -59,18 +59,15 @@

Ajout du moteur de recherche avec les options par défaut

] }); - var insee = new ol.control.InseeAdvancedResearch({ - name : "Code insee", - }); - var insee2 = new ol.control.InseeAdvancedResearch({ - name : "Code insee 2", - }); + var insee = new ol.control.InseeAdvancedSearch({ + }) + var location = new ol.control.LocationAdvancedSearch({ }) // 2. Appel du SearchEngine var search = new ol.control.SearchEngineAdvanced({ - advancedResearch : [insee, insee2, location] + advancedSearch : [insee, location] }); // 3. Ajout du SearchEngine à la carte diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index 9b9580d0a..a1d09dca2 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -1,5 +1,16 @@ /** STYLE DSFR RECHERCHE AVANCEE **/ -form[id^=GPAdvancedForm-] > .GPFormFooter { - justify-content: center; +div[data-open="true"] > section.fr-accordion::before { + box-shadow: inset 0 1px 0 0 var(--border-default-grey); +} + +div[id^=GPsearchEngine-AdvancedContainer] > button.GPSearchEngine-locate { + margin-bottom : 0.75rem; +} + +[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"] .GPOptionsContainer button { + font-size: 0.875rem; + line-height: 1.5rem; + min-height: 2rem; + padding: 0.25rem 0.75rem; } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index ce9557791..2d204e6e0 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -126,9 +126,9 @@ div.GPbuttonsContainer > button { width: 40px; } -form[id^=GPsearchInput-] { +/* form[id^=GPsearchInput-] { width: 312px; -} +} */ button[id^="GPshowSearchEnginePicto-"][aria-pressed="false"] + form[id^=GPsearchInput-] { max-width: 0px; diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index a6c6c0fe4..80ab30b0d 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -1,6 +1,34 @@ /** STYLE COMMUN RECHERCHE AVANCEE **/ +div[id^=GPsearchEngine-Advanced] { + width: 312px; +} + +div[id^=GPsearchEngine-Advanced] div[id^=GPsearchEngine-] { + width: 100%; +} + +form[id^=GPAdvancedForm-], form[id^=GPAdvancedForm-] * { + height : 100%; + width: 100%; +} + form[id^=GPAdvancedForm-] > .GPFormFooter { + margin-top: 0.75rem; display: flex; + justify-content: space-between; flex-wrap: wrap; + gap: 16px; +} + +[id^="GPsearchEngine-Advanced"] form[id^="GPsearchInput-Base-"]:first-child { + display: flex; + flex-direction: row; +} + +form[id^=GPAdvancedForm-] > .GPFormFooter > button { + flex: 1 1 0; + display: flex; + justify-content: center; + align-items: center; } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 3f3858966..08632b730 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -62,6 +62,7 @@ form[id^=GPsearchInput-] { display: inline-block; position: relative; overflow: hidden; + width: 100%; transition: max-width 0.5s ease-out 0s; } @@ -134,9 +135,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ /* Simple search input */ -[id^="GPsearchInput"] {} - - [id^="GPshowSearchDiv"]{ display: flex; } @@ -190,7 +188,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } [id^="GPsearchEngine"] .GPautoCompleteContainer { - position: absolute; + /* position: absolute; */ box-shadow: 0 2px 6px 0 rgba(0, 0, 18, 0.16); list-style: none; overflow: hidden; @@ -213,19 +211,18 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^="GPsearchEngine"] { display: flex; - flex-direction: row; - max-width: 360px; + flex-direction: column; padding: 0; - max-height: 32px; } -[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] > * { +[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-].GPSearchBar > :first-child { display: flex; flex-direction: row; -} -[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPInputGroup { padding: 0; } +/* [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPInputGroup { + padding: 0; +} */ [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer { display: flex; height: calc(100% - 2px); @@ -242,8 +239,12 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ display: none; } +.GPautoCompleteHeader { + padding: 0.75rem 0.75rem 0; +} + [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPautoCompleteHeader button { - margin: 12px 12px 0; + /* margin: 12px 12px 0; */ max-width: unset; } [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPautoCompleteFooter button { @@ -251,10 +252,10 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ max-width: unset; } -[id^="GPsearchEngine"] .GPautoCompleteHeader .GPSearchEngine-locate { +[id^="GPsearchEngine"] .GPautoCompleteContainer .GPautoCompleteHeader { display: none; } -[id^="GPsearchEngine"] .GPautoCompleteContainer[data-type="history"] .GPSearchEngine-locate { +[id^="GPsearchEngine"] .GPautoCompleteContainer[data-type="history"] .GPautoCompleteHeader { display: block; } @@ -283,7 +284,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ box-sizing: content-box; max-width: 100%; max-height: unset; - padding: 1rem; + padding: 0.75rem 0.75rem 1px; box-sizing: border-box; } [id^="GPsearchEngine-Advanced"]:has(.GPSearchEngine-advanced-btn[aria-expanded="true"]) > .GPAdvancedContainer { @@ -302,16 +303,16 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ user-select: none; } -[id^="GPsearchEngine-Advanced"] .GPAdvancedContainer[data-open="true"] > button { +/* [id^="GPsearchEngine-Advanced"] .GPAdvancedContainer[data-open="true"] > button { display: none; -} +} */ -[id^="GPsearchEngine-Advanced"] section { +[id^="GPsearchEngine-Advanced"][data-open="true"] section { overflow: hidden auto; } -[id^="GPsearchEngine-Advanced"] [data-open="true"] section:before { +/* [id^="GPsearchEngine-Advanced"] [data-open="true"] section::before { display: none; -} +} */ [id^="GPsearchEngine-Advanced"] section .fr-collapse { box-sizing: border-box; diff --git a/src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js similarity index 58% rename from src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js rename to src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js index 09ec965cd..5dd8aed17 100644 --- a/src/packages/Controls/SearchEngine/AbstractAdvancedResearch.js +++ b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js @@ -4,27 +4,26 @@ import Control from "../Control"; import Logger from "../../Utils/LoggerByDefault"; import { Collection } from "ol"; import Helper from "../../Utils/Helper"; - -var logger = Logger.getLogger("abstractAdvancedResearch"); +var logger = Logger.getLogger("abstractAdvancedSearch"); /** - * @typedef {Object} AbstractAdvancedResearchOptions Options du constructeur pour le contrôle de recherche. + * @typedef {Object} AbstractAdvancedSearchOptions Options du constructeur pour le contrôle de recherche. * * @property {string} name - Nom de la recherche avancée. */ /** * @classdesc - * AbstractAdvancedResearch Base control + * AbstractAdvancedSearch Base control * - * @alias ol.control.AbstractAdvancedResearch - * @module AbstractAdvancedResearch + * @alias ol.control.AbstractAdvancedSearch + * @module AbstractAdvancedSearch */ -class AbstractAdvancedResearch extends Control { +class AbstractAdvancedSearch extends Control { /** * @constructor - * @param {AbstractAdvancedResearchOptions} options Options du constructeur + * @param {AbstractAdvancedSearchOptions} options Options du constructeur * * @example */ @@ -37,7 +36,7 @@ class AbstractAdvancedResearch extends Control { * Nom de la classe (heritage) * @private */ - this.CLASSNAME = "AbstractAdvancedResearch"; + this.CLASSNAME = "AbstractAdvancedSearch"; // initialisation du composant this.initialize(options); @@ -45,10 +44,22 @@ class AbstractAdvancedResearch extends Control { this.element = this._initContainer(options); this._initEvents(options); } + + setMap (map) { + super.setMap(map); + this.inputs.forEach(input => { + if (input.setMap) { + input.setMap(map); + } + }); + // Replace les boutons à la fin + this.element.appendChild(this.btnGroup); + } + /** * Initialize SearchEngine control (called by SearchEngine constructor) * - * @param {AbstractAdvancedResearchOptions} options - constructor options + * @param {AbstractAdvancedSearchOptions} options - constructor options * @protected */ initialize (options) { @@ -66,36 +77,47 @@ class AbstractAdvancedResearch extends Control { } getContent () { - return this.containerthis.name; + return this.element; } /** * - * @param {AbstractAdvancedResearchOptions} options + * @param {AbstractAdvancedSearchOptions} options * @returns {HTMLFormElement} Élément du formulaire * @protected */ _initContainer (options) { let element = document.createElement("form"); element.className = "GPForm gpf-advanced-search-form"; - element.id = Helper.getUid("GPAdvancedForm-"); + element.id = helper.getUid("GPAdvancedForm-" + this.CLASSNAME + "-"); + + let fieldset = document.createElement("fieldset"); + fieldset.className = "GPFieldset fr-fieldset gpf-advanced-search-fieldset"; + fieldset.id = helper.getUid("GPAdvancedFieldset-"); this.addInputs(); this.inputs.forEach((elem) => { - element.appendChild(elem); + if (elem instanceof Control) { + elem.setTarget(element); + } else { + element.appendChild(elem); + } }); - const btnGroup = document.createElement("div"); + // element.appendChild(fieldset); + + const btnGroup = this.btnGroup = document.createElement("div"); btnGroup.className = "GPFormFooter"; const eraseBtn = this.eraseBtn = document.createElement("button"); + eraseBtn.type = "reset"; eraseBtn.className = "GPBtn gpf-btn fr-btn fr-btn--tertiary"; - eraseBtn.id = Helper.getUid("GPEraseBtn-"); + eraseBtn.id = helper.getUid("GPEraseBtn-"); eraseBtn.textContent = "Effacer"; const searchBtn = this.searchBtn = document.createElement("button"); searchBtn.className = "GPBtn gpf-btn fr-btn"; - searchBtn.id = Helper.getUid("GPSearchBtn-"); + searchBtn.id = helper.getUid("GPSearchBtn-"); searchBtn.type = "submit"; searchBtn.textContent = "Rechercher"; searchBtn.setAttribute("form", element.id); @@ -120,21 +142,12 @@ class AbstractAdvancedResearch extends Control { /** Add event listeners - * @param {AbstractAdvancedResearchOptions} options - constructor options + * @param {AbstractAdvancedSearchOptions} options - constructor options * @protected */ _initEvents (options) { this.eraseBtn.onclick = this._onErase.bind(this); - /** - * Fonction de recherche - * @param {PointerEvent} e - */ - const onSearch = function (e) { - e.preventDefault(); - this._onSearch(e); - // TODO : AJOUTER ÉVÉNEMENT ONSEARCH ? - }; - this.searchBtn.onclick = onSearch.bind(this); + this.element.onsubmit = this._onSearch.bind(this); } /** @@ -143,29 +156,27 @@ class AbstractAdvancedResearch extends Control { * @protected */ _onErase (e) { - this.inputs.forEach(input => { - if (input.value !== undefined) { - input.value = null; - } + e.preventDefault(); + this.getContent().querySelectorAll("input").forEach(input => { + input.value = ""; }); } - /** * - * @param {PointerEvent} e + * @param {SubmitEvent} e * @abstract * @protected */ _onSearch (e) { - + e.preventDefault(); } } -export default AbstractAdvancedResearch; +export default AbstractAdvancedSearch; -// Expose AbstractAdvancedResearch as ol.control.AbstractAdvancedResearch (for a build bundle) +// Expose AbstractAdvancedSearch as ol.control.AbstractAdvancedSearch (for a build bundle) if (window.ol && window.ol.control) { - window.ol.control.AbstractAdvancedResearch = AbstractAdvancedResearch; + window.ol.control.AbstractAdvancedSearch = AbstractAdvancedSearch; } diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedResearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedResearch.js deleted file mode 100644 index dfa367ade..000000000 --- a/src/packages/Controls/SearchEngine/InseeAdvancedResearch.js +++ /dev/null @@ -1,62 +0,0 @@ -// import CSS -import AbstractAdvancedResearch from "./AbstractAdvancedResearch"; -import Logger from "../../Utils/LoggerByDefault"; -import Collection from "ol/Collection"; -import Helper from "../../Utils/Helper"; - -var logger = Logger.getLogger("abstractAdvancedResearch"); - - -/** - * @classdesc - * AbstractAdvancedResearch Base control - * - * @alias ol.control.InseeAdvancedResearch - * @module InseeAdvancedResearch -*/ -class InseeAdvancedResearch extends AbstractAdvancedResearch { - - /** - * @constructor - * @example - */ - constructor (options) { - super(options); - /** - * Nom de la classe (heritage) - * @private - */ - this.CLASSNAME = "InseeAdvancedResearch"; - } - - /** - * Ajoute des éléments d'input dans la collection `this.inputs`; - * Cette méthode est abstraite et doit être surchargée dans les autres classes. - * @protected - * @abstract - */ - addInputs () { - super.addInputs(); - const inseeInput = document.createElement("input"); - inseeInput.type = "text"; - this.inputs.push(inseeInput); - } - - /** - * - * @param {PointerEvent} e - * @abstract - * @protected - */ - _onSearch (e) { - console.log(e); - } - -} - -export default InseeAdvancedResearch; - -// Expose InseeAdvancedResearch as ol.control.InseeAdvancedResearch (for a build bundle) -if (window.ol && window.ol.control) { - window.ol.control.InseeAdvancedResearch = InseeAdvancedResearch; -} diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js new file mode 100644 index 000000000..80f302cc7 --- /dev/null +++ b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js @@ -0,0 +1,111 @@ +// import CSS +import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; +import Logger from "../../Utils/LoggerByDefault"; +import Collection from "ol/Collection"; +import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; +import { InseeSearchService } from "./Service"; + +var logger = Logger.getLogger("abstractAdvancedSearch"); + + +/** + * @classdesc + * AbstractAdvancedSearch Base control + * + * @alias ol.control.InseeAdvancedSearch + * @module InseeAdvancedSearch +*/ +class InseeAdvancedSearch extends AbstractAdvancedSearch { + + /** + * @constructor + * @example + */ + constructor (options) { + super(options); + + this.inseeInput.on("search", function (e) { + this.dispatchEvent(e); + }.bind(this)); + } + + initialize (options) { + if (!options.name) { + options.name = "Code INSEE"; + } + super.initialize(options); + + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "InseeAdvancedSearch"; + } + + /** + * Ajoute des éléments d'input dans la collection `this.inputs`; + * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * @protected + * @abstract + */ + addInputs () { + super.addInputs(); + + let inseeInput = this.inseeInput = new SearchEngineGeocodeIGN({ + label : "Code INSEE", + hint : "Format attendu INSEE : 5 chiffres, selon le code officiel géographique (COG)", + searchService : new InseeSearchService({ + autocomplete : false, + searchOptions : { + serviceOptions : { + fields : ["postcode"], + }, + }, + }) + }); + + this.inputs.push(inseeInput); + } + + _initEvents (options) { + super._initEvents(options); + + this.inseeInput.input.onkeydown = function (e) { + if (e.key === "Enter") { + this.element.requestSubmit(this.searchBtn); + } + }.bind(this); + } + + /** + * + * @param {PointerEvent} e + * @protected + */ + _onSearch (e) { + super._onSearch(e); + const insee = this.inseeInput.input.value; + + const count = insee.length; + const number = parseInt(insee, 10); + if (!isNaN(number) && 0 <= number <= 99999 && count === 5) { + this.inseeInput.removeMessages(); + this.inseeInput.search({ + location : insee, + filters : { + cityCode : insee, + } + }); + } else { + this.inseeInput.addMessage("Le champs INSEE doit être un texte de 5 chiffres exactement"); + } + } + +} + +export default InseeAdvancedSearch; + +// Expose InseeAdvancedSearch as ol.control.InseeAdvancedSearch (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.InseeAdvancedSearch = InseeAdvancedSearch; +} diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 2b352ce35..87a5e6f4b 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -1,12 +1,12 @@ import Helper from "../../Utils/Helper"; -import AbstractAdvancedResearch from "./AbstractAdvancedResearch"; +import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; -class LocationAdvancedSearch extends AbstractAdvancedResearch { +class LocationAdvancedSearch extends AbstractAdvancedSearch { /** * @constructor - * @param {AbstractAdvancedResearchOptions} options Options du constructeur + * @param {AbstractAdvancedSearchOptions} options Options du constructeur * * @example */ @@ -18,16 +18,18 @@ class LocationAdvancedSearch extends AbstractAdvancedResearch { // call ol.control.Control constructor super(options); + this.search.on("search", function (e) { + this.dispatchEvent(e); + }.bind(this)); + } + + initialize (options) { + super.initialize(options); /** * Nom de la classe (heritage) * @private */ this.CLASSNAME = "LocationAdvancedSearch"; - - this.element.addEventListener("submit", e => { - e.preventDefault(); - console.log(e); - }); } setMap (map) { @@ -74,27 +76,24 @@ class LocationAdvancedSearch extends AbstractAdvancedResearch { typeOption.innerText = k; typeSelect.appendChild(typeOption); }); - typeSelect.addEventListener("change", (e) => { + typeSelect.addEventListener("change", () => { this.filter.category = typeSelect.value; }); // Search input const searchContainer = this._getLabelContainer("Renseigner un lieu", "fr-input-group"); this.search = new SearchEngineGeocodeIGN({ + autocomplete : false, target : searchContainer, historic : "GPAdvancedLocation", maximumEntries : 0 }); - this.search.container.addEventListener("submit", e => { - e.stopPropagation(); - e.preventDefault(); - this._onSearch(); - }, true); // Code postal const postalInput = document.createElement("input"); postalInput.className = "fr-input"; postalInput.type = "text"; + postalInput.name = "postalCode"; postalInput.id = Helper.getUid("LocationAdvancedSearch-postal-"); this._getLabelContainer("Code postal", "fr-input-group", postalInput); postalInput.addEventListener("change", () => { @@ -104,6 +103,7 @@ class LocationAdvancedSearch extends AbstractAdvancedResearch { // Code INSEE const inseeInput = document.createElement("input"); inseeInput.className = "fr-input"; + inseeInput.name = "cityCode"; inseeInput.type = "text"; inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput); @@ -118,6 +118,7 @@ class LocationAdvancedSearch extends AbstractAdvancedResearch { }; } _onErase (e) { + super._onErase(e); this.element.querySelectorAll("select").forEach(input => { input.value = ""; }); @@ -133,8 +134,8 @@ class LocationAdvancedSearch extends AbstractAdvancedResearch { /** Lancer une recheche * */ - _onSearch () { - console.log("search", this); + _onSearch (e) { + super._onSearch(e); const value = this.search.input.value; if (value) { this.search.searchService._requestGeocoding({ @@ -145,7 +146,7 @@ class LocationAdvancedSearch extends AbstractAdvancedResearch { "returnTrueGeometry" : true, "location" : value, onSuccess : e => this.search.searchService._onSuccessSearch(e), - onFailure : e => console.log("ERROR") + onFailure : e => console.log("ERROR", e) }); } } diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index e2ab0a764..4b97391f2 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -4,6 +4,57 @@ import OlFeature from "ol/Feature"; import Point from "ol/geom/Point"; import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; import Helper from "../../Utils/Helper"; +import { Select } from "ol/interaction"; + +import { Vector } from "ol/layer"; +import VectorSource from "ol/source/Vector"; + +import Overlay from "ol/Overlay.js"; + +import { Style, Icon, Stroke, Fill } from "ol/style"; +import Feature from "ol/Feature"; +const color = "rgba(0, 0, 145, 1)"; + +const createStyle = (feature) => { + const geometryType = feature.getGeometry().getType(); + + switch (geometryType) { + case "Point": + case "MultiPoint": + return new Style({ + image : new Icon({ + src : "/src/packages/Controls/SearchEngine/map-pin-2-fill.svg", + color : [0, 0, 145, 1], + }), + }); + + case "LineString": + case "MultiLineString": + return new Style({ + stroke : new Stroke({ + color : color, + width : 3, + }), + }); + + case "Polygon": + case "MultiPolygon": + return new Style({ + stroke : new Stroke({ + color : color, + lineDash : [8, 8], + width : 2, + }), + fill : new Fill({ + color : "rgba(0, 0, 0, 0.1)", + opacity : 0.8 + }), + }); + + default: + return new Style(); + } +}; /** * Classe représentant un moteur de recherche avancée utilisant le service de géocodage de l'IGN. @@ -34,6 +85,27 @@ class SearchEngineAdvanced extends Control { }); + this.layer = new Vector({ + source : new VectorSource({}), + zIndex : Infinity, + style : createStyle, + }); + this.extent = new Vector({ + source : new VectorSource({}), + zIndex : Infinity, + style : createStyle, + }); + + this.selectInteraction = new Select({ + layers : [this.layer, this.extent], + style : createStyle, + }); + + this.selectInteraction.on("select", this._onSelectElement.bind(this)); + + this.popup = this._createPopup(); + + // Initialize this.initialize(options); this._initContainer(options); @@ -53,12 +125,15 @@ class SearchEngineAdvanced extends Control { */ this.CLASSNAME = "SearchEngineAdvanced"; - if (options.advancedResearch && options.advancedResearch instanceof Array) { - this._searchForms = options.advancedResearch; + if (options.advancedSearch && options.advancedSearch instanceof Array) { + this._searchForms = options.advancedSearch; } else { this._searchForms = []; } - console.log(this._searchForms); + + this._searchForms.forEach(search => { + search.on("search", this.onAdvancedSearchResult.bind(this)); + }); } /** @@ -74,26 +149,34 @@ class SearchEngineAdvanced extends Control { if (this.baseSearchEngine) { this.baseSearchEngine.setMap(map); } - this._searchForms.forEach(research => { - research.setMap(map); + this._searchForms.forEach(search => { + search.setMap(map); }); + + this.extent.setMap(map); + this.layer.setMap(map); + this.selectInteraction.setMap(map); + this.popup.setMap(map); } _initEvents (options) { this.geolocation.on("change:position", () => { const pt = new Point(this.geolocation.getPosition()); pt.transform("EPSG:4326", this.getMap().getView().getProjection()); - const evt = this.addResultToMap (pt, "Ma localisation"); + const evt = this.createEvent(pt, "Ma localisation"); + this.addResultToMap(evt); this.dispatchEvent(evt); this.geolocation.setTracking(false); }); + + this.on("search", this.addResultToMap.bind(this)); } /** Display result on map - * @param {Object|Point|OlFeature} e objet a afficher + * @param {Object|Point|OlFeature} obj objet a afficher * @param {String} [info] Popup info */ - addResultToMap (obj, info) { + createEvent (obj, info) { let evt = obj; if (obj instanceof OlFeature) { evt = { @@ -110,10 +193,142 @@ class SearchEngineAdvanced extends Control { evt.result.set("infoPopup", info); } evt.type = "search"; - this.baseSearchEngine.addResultToMap(evt); return evt; } + + addResultToMap (e) { + this.layer.getSource().clear(); + this.extent.getSource().clear(); + let extent, zoom; + if (e.result !== null) { + this.layer.getSource().addFeature(e.result); + extent = e.result.getGeometry().getExtent(); + zoom = 15; + } + if (e.extent !== null) { + this.extent.getSource().addFeature(e.extent); + extent = e.extent.getGeometry().getExtent(); + } + if (this.getMap()) { + let view = this.getMap().getView(); + if (extent) { + view.fit(extent); + view.getZoom(); + if (view.getZoom() > 15) { + view.setZoom(15); + } + } + } + } + + + /** + * + * @param {import("ol/interaction/Select").SelectEvent} e Événement de séléction + */ + _onSelectElement (e) { + let position = e.mapBrowserEvent.coordinate; + if (e.selected.length) { + // Ajoute le popup + const feature = e.selected[0]; + if (feature.getGeometry().getType() === "Point") { + // Place le popup sur le point + position = feature.getGeometry().getCoordinates(); + } + this.popup.setPosition(position); + this.setPopupContent(feature.get("infoPopup")); + this.popup.set("feature", feature); + this.popup.set("layer", e.target.getLayer(feature)); + } else { + this.popup.setPosition(undefined); + this.setPopupContent(""); + this.popup.unset("feature"); + this.popup.unset("layer"); + } + } + + _createPopup () { + // Popup global + let element = this._popupDiv = document.createElement("div"); + // TODO : ajouter gp-feature-info-div lorsque les deux seront pareils + element.className = "GPSearchPopup"; + + // Contenu du popup + let popupContent = this._popupContent = document.createElement("div"); + popupContent.className = "GPPopupContent"; + + // Groupe de boutons + let popupBtns = this._popupBtns = document.createElement("div"); + popupBtns.className = "GPButtonGroups gpf-btns-group"; + + popupBtns.appendChild(this._addCloseButton()); + popupBtns.appendChild(this._addRemoveButton()); + + element.appendChild(popupContent); + element.appendChild(popupBtns); + + const overlay = new Overlay({ + element : element, + positioning : "bottom-center", + }); + + return overlay; + } + + setPopupContent (content) { + this._popupContent.innerHTML = content; + } + + _addCloseButton () { + let closer = document.createElement("button"); + closer.title = closer.ariaLabel = "Fermer la pop-up"; + closer.textContent = "Fermer"; + closer.className = "GPButton gpf-btn fr-icon-close-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline"; + + // Ferme le popup + closer.onclick = this._closePopup.bind(this); + return closer; + } + + _closePopup () { + this.selectInteraction.getFeatures().clear(); + if (this.popup != null) { + this.popup.setPosition(undefined); + } + return false; + } + + _addRemoveButton () { + let remove = document.createElement("button"); + remove.title = remove.ariaLabel = "Supprimer le marqueur"; + remove.textContent = "Supprimer"; + remove.className = "GPButton gpf-btn fr-icon-delete-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline"; + + // Supprime la feature + remove.onclick = this._removeFeature.bind(this); + + return remove; + } + + _removeFeature () { + const f = this.popup.get("feature"); + const layer = this.popup.get("layer"); + // Supprime la feature + if (layer && f) { + layer.getSource().removeFeature(f); + + this.dispatchEvent({ + type : this.REMOVE_FEATURE_EVENT, + feature : f, + layer : layer, + }); + + // Ferme le popup + this._closePopup(); + } + } + /** * Ajoute le contrôle à la carte. * @override @@ -123,12 +338,14 @@ class SearchEngineAdvanced extends Control { // Gestion de l'affichage des options avancées const element = this.element = document.createElement("div"); element.className = "GPwidget gpf-widget"; - element.id = Helper.getUid("GPsearchEngine-Advanced-"); + element.id = helper.getUid("GPsearchEngine-Advanced-"); // Default base search engine const baseContainer = this.advancedContainer = document.createElement("div"); this.element.appendChild(baseContainer); options.target = baseContainer; + options.searchButton = true; + options.search = true; this.baseSearchEngine = new SearchEngineGeocodeIGN(options); this.baseSearchEngine.on(["select", "search", "autocomplete"], (e) => { this.dispatchEvent(e); @@ -140,7 +357,7 @@ class SearchEngineAdvanced extends Control { // Ajout des options avancées const advancedBtn = document.createElement("button"); advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; - advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); + advancedBtn.id = helper.getUid("GPSearchEngine-advanced-btn-"); advancedBtn.type = "button"; advancedBtn.title = "Avancée"; advancedBtn.innerHTML = "Avancée"; @@ -151,7 +368,7 @@ class SearchEngineAdvanced extends Control { // Gestion de l'affichage des options avancées const advancedContainer = this.advancedContainer = document.createElement("div"); advancedContainer.className = "GPAdvancedContainer"; - advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); + advancedContainer.id = helper.getUid("GPsearchEngine-AdvancedContainer-"); advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); this.element.appendChild(advancedContainer); @@ -159,7 +376,7 @@ class SearchEngineAdvanced extends Control { advancedContainer.appendChild(this._getGeolocButton()); // Formulaires specifiques - this._searchForms.forEach(research => { + this._searchForms.forEach(Search => { const section = document.createElement("section"); section.className = "fr-accordion"; advancedContainer.appendChild(section); @@ -170,21 +387,17 @@ class SearchEngineAdvanced extends Control { button.type = "button"; button.className = "fr-accordion__btn"; button.setAttribute("aria-expanded", "false"); - button.innerText = research.getName(); - section.appendChild(button); + button.innerText = Search.getName(); + title.appendChild(button); // Accordion const accordion = document.createElement("div"); accordion.className = "fr-collapse"; - accordion.id = Helper.getUid("accordion-"); + accordion.id = helper.getUid("accordion-"); button.setAttribute("aria-controls", accordion.id); section.appendChild(accordion); - // Content - const atitle = document.createElement("h4"); - atitle.className = "fr-h4"; - accordion.appendChild(atitle); // Contenu recherche avancée - research.setTarget(accordion); + Search.setTarget(accordion); button.addEventListener("click", () => { const expanded = button.getAttribute("aria-expanded") === "true"; @@ -219,7 +432,7 @@ class SearchEngineAdvanced extends Control { _getGeolocButton () { const locationBtn = document.createElement("button"); locationBtn.innerText = "Me géolocaliser"; - locationBtn.className = "GPSearchEngine-locate fr-btn fr-btn--sm fr-btn--icon-left fr-btn--tertiary-no-outline gpf-btn-icon-search-geolocate"; + locationBtn.className = "GPSearchEngine-locate fr-btn fr-btn--sm fr-icon-crosshair-2-line fr-btn--icon-left fr-btn--tertiary-no-outline"; locationBtn.addEventListener("click", () => { this.geolocation.setTracking(true); console.log("tracking", this.geolocation); @@ -227,6 +440,15 @@ class SearchEngineAdvanced extends Control { return locationBtn; } + onAdvancedSearchResult (e) { + console.log(e); + if (e.result instanceof Array) { + // TODO : GÉRER MULTIPLE RÉSULTATS + } else if (e.result instanceof Feature) { + this.addResultToMap(e); + } + } + } export default SearchEngineAdvanced; diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 35a54d421..28f0f5f58 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -18,11 +18,15 @@ var logger = Logger.getLogger("searchengine"); * @property {HTMLElement|string} [target] - Élément DOM ou sélecteur dans lequel insérer le contrôle. * Si non défini, le contrôle crée un bouton permettant d’ouvrir/fermer le champ de recherche. * @property {string} [title="Rechercher"] - Texte du titre (attribut `title`) du bouton principal. + * @property {string} [label=""] - Label à ajouter. Aucun par défaut. + * @property {string} [hint=""] - Texte additionnel à ajouter sous le label. Aucun par défaut. + * @property {Boolean} [search=false] - Si vrai, définit le composant comme une barre de recherche (classes CSS et attributs HTML). * @property {string} [collapsible=false] - Si vrai, permet de fermer le contrôle. * @property {string} [ariaLabel="Rechercher"] - Libellé accessible (ARIA) pour le champ de recherche. * @property {string} [placeholder=""] - Texte d’indication affiché dans le champ de saisie. * @property {number} [minChars=0] - Nombre minimum de caractères à saisir avant de lancer l’autocomplétion. * @property {number} [maximumEntries=5] - Nombre maximum d’entrées affichées dans la liste d’autocomplétion. + * @property {number} [searchButton=false] - Affiche un bouton de recherche. Faux par défaut. * @property {number} [triggerDelay=100] - Délai (en millisecondes) avant le déclenchement de l’autocomplétion * après la saisie de l’utilisateur. * @property {boolean|string} [historic=true] - Active ou non l’historique local des recherches. Valeur acceptées : @@ -75,9 +79,12 @@ class SearchEngineBase extends Control { this.initialize(options); this.searchService = options.searchService; - this.searchService.on("autocomplete", function (e) { - this.onAutocomplete(e); - }.bind(this)); + // Permet l'autocomplétion + if (this.searchService.get("autocomplete") !== false) { + this.searchService.on("autocomplete", function (e) { + this.onAutocomplete(e); + }.bind(this)); + } this.searchService.on("search", function (e) { this.onSearch(e); @@ -91,16 +98,16 @@ class SearchEngineBase extends Control { // Get historic in localStorage this._historic = false; this._historicName = "GPsearch-" + options.historic; - if (options.historic !== false) { + if (options.historic !== false && this.searchService.get("autocomplete") !== false) { this._historic = []; - try { + try { const stor = window.localStorage.getItem(this._historicName); if (stor) { this._historic = JSON.parse(stor); - } + } } catch (e) { // logger.warn("LocalStorage not available"); - } + } } this.showHistoric(); } @@ -119,88 +126,95 @@ class SearchEngineBase extends Control { options.ariaLabel = options.ariaLabel ? options.ariaLabel : "Rechercher"; options.placeholder = options.placeholder ? options.placeholder : ""; options.searchService = options.searchService ? options.searchService : new DefaultSearchService(); + options.label = options.label ? options.label : ""; + options.hint = options.hint ? options.hint : ""; + options.search = options.search === true ? true : false; + options.searchButton = options.searchButton === true ? true : false; options.collapsible = options.collapsible === true ? true : false; this.set("maximumEntries", options.maximumEntries); } + /** Add event listeners * @param {SearchEngineBaseOptions} options - constructor options * @protected */ _initEvents (options) { - // Empty input - this.input.addEventListener("input", function (e) { - if (!e.target.value) { - this.showHistoric(); - } - }.bind(this)); - // Prevent cursor to go to the end of input on keydown - this.input.addEventListener("keydown", function (e) { - if (/ArrowDown|ArrowUp/.test(e.key)) { - e.preventDefault(); - } - }.bind(this)); - // Keyboard navigation - this.input.addEventListener("keyup", function (e) { - // autocomplete list - const list = Array.from(this.autocompleteList.querySelectorAll("li")); - let idx = list.findIndex(li => li.classList.contains("active")); - if (idx === -1) { - // Ancienne valeur - this._previousValue = e.target.value; - } - switch (e.key) { - case "ArrowDown": - case "ArrowUp": + if (this.searchService.get("autocomplete") !== false) { + // Empty input + this.input.addEventListener("input", function (e) { + if (!e.target.value) { + this.showHistoric(); + } + }.bind(this)); + // Prevent cursor to go to the end of input on keydown + this.input.addEventListener("keydown", function (e) { + if (/ArrowDown|ArrowUp/.test(e.key)) { e.preventDefault(); - // Navigation in autocomplete list - if (list.length === 0) { - return; - } - list.forEach(li => li.classList.remove("active")); - if (e.key === "ArrowDown") { - idx++; - if (idx >= list.length) { - idx = -1; + } + }.bind(this)); + // Keyboard navigation + this.input.addEventListener("keyup", function (e) { + // autocomplete list + const list = Array.from(this.autocompleteList.querySelectorAll("li")); + let idx = list.findIndex(li => li.classList.contains("active")); + if (idx === -1) { + // Ancienne valeur + this._previousValue = e.target.value; + } + switch (e.key) { + case "ArrowDown": + case "ArrowUp": + e.preventDefault(); + // Navigation in autocomplete list + if (list.length === 0) { + return; } - } else if (e.key === "ArrowUp") { - idx--; - if (idx < -1) { - idx = list.length - 1; + list.forEach(li => li.classList.remove("active")); + if (e.key === "ArrowDown") { + idx++; + if (idx >= list.length) { + idx = -1; + } + } else if (e.key === "ArrowUp") { + idx--; + if (idx < -1) { + idx = list.length - 1; + } } - } - if (idx !== -1) { - // Set active - const current = list[idx]; - current.classList.add("active"); - this.input.value = current.innerText; - this.input.setAttribute("aria-activedescendant", current.id); - this.input.setAttribute("data-active-option", current.id); - } else { - // Réaffiche la valeur précédente de l'utilisateur - e.target.value = this._previousValue; - } - break; - case "Enter": - // Lance la recherche - let item = list[idx]; - if (idx < 0) { - // Pas d'item sélectionné : on prend le premier de la liste - item = list[0]; - } - if (item) { - // Simule un clic sur l'élément sélectionné - item.click(); - } - break; - default: - if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { - this.autocomplete(e.target.value, e.key === "Enter"); - } - break; - } - this._currentValue = e.target.value; - }.bind(this), false); + if (idx !== -1) { + // Set active + const current = list[idx]; + current.classList.add("active"); + this.input.value = current.innerText; + this.input.setAttribute("aria-activedescendant", current.id); + this.input.setAttribute("data-active-option", current.id); + } else { + // Réaffiche la valeur précédente de l'utilisateur + e.target.value = this._previousValue; + } + break; + case "Enter": + // Lance la recherche + let item = list[idx]; + if (idx < 0) { + // Pas d'item sélectionné : on prend le premier de la liste + item = list[0]; + } + if (item) { + // Simule un clic sur l'élément sélectionné + item.click(); + } + break; + default: + if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { + this.autocomplete(e.target.value, e.key === "Enter"); + } + break; + } + this._currentValue = e.target.value; + }.bind(this), false); + } // Événement d'envoi du formulaire this.container.addEventListener("submit", function (e) { @@ -233,16 +247,17 @@ class SearchEngineBase extends Control { _initContainer (options) { const element = this.element = document.createElement("div"); element.className = "GPwidget gpf-widget"; - element.id = Helper.getUid("GPsearchEngine-"); + element.id = helper.getUid("GPsearchEngine-"); // Main container const container = this.container = document.createElement("form"); - container.className = "fr-search-bar"; - container.id = Helper.getUid("GPsearchInput-Base-"); + container.className = options.search ? "GPSearchBar fr-search-bar" : ""; + // container.className = "fr-search-bar"; + container.id = helper.getUid("GPsearchInput-Base-"); // Création du bouton if (!options.target && options.collapsible) { this.button = document.createElement("button"); - this.button.id = Helper.getUid("GPshowSearchEnginePicto-"); + this.button.id = helper.getUid("GPshowSearchEnginePicto-"); this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn fr-icon-search-line fr-btn fr-btn--lg"; this.button.setAttribute("aria-pressed", "true"); // this.button.setAttribute("type", "submit"); @@ -268,39 +283,65 @@ class SearchEngineBase extends Control { element.appendChild(container); const search = document.createElement("div"); - search.className = "GPInputGroup fr-input"; + // search.className = "GPInputGroup fr-input"; + search.className = "GPInputGroup"; + search.classList.add(options.search ? "fr-input" : "fr-input-group"); container.appendChild(search); + // Input const input = this.input = document.createElement("input"); input.type = "text"; input.className = "GPsearchInputText fr-input"; - input.id = Helper.getUid("GPsearchInputText-"); + input.id = helper.getUid("GPsearchInputText-"); input.placeholder = options.placeholder; input.autocomplete = "off"; input.setAttribute("aria-label", options.ariaLabel); + + if (options.label) { + const label = document.createElement("label"); + label.className = "GPLabel fr-label"; + label.textContent = options.label; + label.htmlFor = input.id; + if (options.hint) { + const hint = document.createElement("span"); + hint.className = "GPLabelHint fr-hint-text"; + hint.textContent = options.hint; + label.appendChild(hint); + } + search.appendChild(label); + } search.appendChild(input); + const messages = document.createElement("div"); + messages.className = "GPMessagesGroup fr-messages-group"; + messages.ariaLive = "polite"; + messages.id = helper.getUid("GPMessagesGroup-"); + input.setAttribute("aria-describedby", messages.id); + search.appendChild(messages); + // Options container this.optionscontainer = document.createElement("div"); this.optionscontainer.className = "GPOptionsContainer"; search.appendChild(this.optionscontainer); // Submit button - const submit = this.subimtBt = document.createElement("button"); - submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; - submit.id = Helper.getUid("GPshowSearchEnginePicto-"); - submit.type = "submit"; - if (options.title) { - submit.textContent = options.title; - submit.setAttribute("title", options.title); + if (options.searchButton) { + const submit = this.subimtBt = document.createElement("button"); + submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; + submit.id = helper.getUid("GPshowSearchEnginePicto-"); + submit.type = "submit"; + if (options.title) { + submit.setAttribute("title", options.title); + } + container.appendChild(submit); } - search.appendChild(submit); // Autocomplete container const acContainer = document.createElement("div"); acContainer.className = "GPautoCompleteContainer GPelementHidden gpf-hidden"; - container.appendChild(acContainer); + element.appendChild(acContainer); + // element.appendChild(acContainer); // Autocomplete list const autocompleteHeader = this.autocompleteHeader = document.createElement("div"); @@ -309,7 +350,7 @@ class SearchEngineBase extends Control { const autocompleteList = this.autocompleteList = document.createElement("ul"); autocompleteList.className = "GPautoCompleteList"; - autocompleteList.id = Helper.getUid("GPautoCompleteList-"); + autocompleteList.id = helper.getUid("GPautoCompleteList-"); autocompleteList.setAttribute("role", "listbox"); autocompleteList.setAttribute("tabindex", "-1"); autocompleteList.setAttribute("aria-label", "Propositions"); @@ -326,27 +367,29 @@ class SearchEngineBase extends Control { input.setAttribute("aria-autocomplete", "list"); input.setAttribute("aria-haspopup", "listbox"); - input.addEventListener("focus", () => { - input.setAttribute("aria-expanded", "true"); - acContainer.classList.add("gpf-visible"); - acContainer.classList.remove("gpf-hidden"); - acContainer.classList.add("GPelementVisible"); - acContainer.classList.remove("GPelementHidden"); - }); - input.addEventListener("blur", (e) => { - // N'agit que si le focus est hors de l'élément - if (e.relatedTarget && acContainer.contains(e.relatedTarget)) { - input.focus(); - } else { - setTimeout(() => { - input.setAttribute("aria-expanded", "false"); - acContainer.classList.remove("gpf-visible"); - acContainer.classList.add("gpf-hidden"); - acContainer.classList.remove("GPelementVisible"); - acContainer.classList.add("GPelementHidden"); - }, 100); - } - }); + if (this.searchService.get("autocomplete") !== false) { + input.addEventListener("focus", () => { + input.setAttribute("aria-expanded", "true"); + acContainer.classList.add("gpf-visible"); + acContainer.classList.remove("gpf-hidden"); + acContainer.classList.add("GPelementVisible"); + acContainer.classList.remove("GPelementHidden"); + }); + input.addEventListener("blur", (e) => { + // N'agit que si le focus est hors de l'élément + if (e.relatedTarget && acContainer.contains(e.relatedTarget)) { + input.focus(); + } else { + setTimeout(() => { + input.setAttribute("aria-expanded", "false"); + acContainer.classList.remove("gpf-visible"); + acContainer.classList.add("gpf-hidden"); + acContainer.classList.remove("GPelementVisible"); + acContainer.classList.add("GPelementHidden"); + }, 100); + } + }); + } } setActive (active) { @@ -378,6 +421,7 @@ class SearchEngineBase extends Control { * @api */ search (item) { + console.log(item); clearTimeout(this._completeDelay); this._completeDelay = setTimeout(function () { this.searchService.search(item); @@ -440,7 +484,7 @@ class SearchEngineBase extends Control { const iconClass = typeClasses[type] || typeClasses["search"]; tab.forEach((item, idx) => { const li = document.createElement("li"); - li.id = Helper.getUid("GPsearchHistoric-"); + li.id = helper.getUid("GPsearchHistoric-"); li.className = `GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; li.setAttribute("role", "option"); li.setAttribute("data-idx", idx); @@ -483,6 +527,38 @@ class SearchEngineBase extends Control { } } + /** + * Ajoute un message à un champs de saisie + * @param {HTMLInputElement|HTMLSelectElement} input Champs de saisie + * @param {String} message Message à afficher + * @param {String} [type="error"] Type du message. Message d'erreur par défaut + * @api + */ + addMessage (message, type = "error") { + let messageElement = this.input.ariaDescribedByElements[0]; + if (messageElement) { + const p = document.createElement("p"); + const messageType = type === "error" ? "error" : "valid"; + p.className = `GPMessage GPMessage--${messageType} fr-message fr-message--${messageType}`; + p.id = helper.getUid("GPMessage-"); + p.textContent = message; + + messageElement.replaceChildren(p); + } + } + + /** + * Enlève un message d'erreur + * @param {HTMLInputElement|HTMLSelectElement} input Champs de saisie + * @api + */ + removeMessages () { + let messageElement = this.input.ariaDescribedByElements[0]; + if (messageElement) { + messageElement.replaceChildren(); + } + } + } export default SearchEngineBase; diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 852e63a9f..a572dfbe3 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -1,57 +1,8 @@ // import CSS -import { Select } from "ol/interaction"; import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import Logger from "../../Utils/LoggerByDefault"; import SearchEngineBase from "./SearchEngineBase"; import { AbstractSearchService, IGNSearchService } from "./Service"; -import { Vector } from "ol/layer"; -import VectorSource from "ol/source/Vector"; - -import { Style, Icon, Stroke, Fill } from "ol/style"; -import Overlay from "ol/Overlay.js"; -import checkDsfr from "../Utils/CheckDsfr"; -const color = "rgba(0, 0, 145, 1)"; - -const createStyle = (feature) => { - const geometryType = feature.getGeometry().getType(); - - switch (geometryType) { - case "Point": - case "MultiPoint": - return new Style({ - image : new Icon({ - src : "/src/packages/Controls/SearchEngine/map-pin-2-fill.svg", - color : [0, 0, 145, 1], - }), - }); - - case "LineString": - case "MultiLineString": - return new Style({ - stroke : new Stroke({ - color : color, - width : 3, - }), - }); - - case "Polygon": - case "MultiPolygon": - return new Style({ - stroke : new Stroke({ - color : color, - lineDash : [8, 8], - width : 2, - }), - fill : new Fill({ - color : "rgba(0, 0, 0, 0.1)", - opacity : 0.8 - }), - }); - - default: - return new Style(); - } -}; var logger = Logger.getLogger("searchengine"); @@ -86,54 +37,9 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { // call ol.control.Control constructor super(options); - this.layer = new Vector({ - source : new VectorSource({}), - zIndex : Infinity, - style : createStyle, - }); - this.extent = new Vector({ - source : new VectorSource({}), - zIndex : Infinity, - style : createStyle, - }); - - this.selectInteraction = new Select({ - layers : [this.layer, this.extent], - style : createStyle, - }); - - this.selectInteraction.on("select", this._onSelectElement.bind(this)); - - this.popup = this._createPopup(); - return this; } - /** - * Fonction d'ajout du contrôle. - * @override - * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. - */ - setMap (map) { - // Remove controls from the current map - if (this.getMap()) { - this.getMap().removeLayer(this.extent); - this.getMap().removeLayer(this.layer); - this.getMap().removeInteraction(this.selectInteraction); - this.getMap().removeOverlay(this.popup); - } - // Init map - super.setMap(map); - // Add the control to the new map - if (map) { - map.addLayer(this.extent); - map.addLayer(this.layer); - - map.addInteraction(this.selectInteraction); - map.addOverlay(this.popup); - } - } - /** * Initialise les options du contrôle. * @@ -148,11 +54,18 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { this.CLASSNAME = "SearchEngineGeocodeIGN"; this.REMOVE_FEATURE_EVENT = "remove:feature"; + if (options.autocomplete === false) { + this.set("autocomplete", false); + options.serviceOptions = options.serviceOptions ? options.serviceOptions : {}; + options.serviceOptions.autocomplete = false; + } + // Créé le serbice de géocodage IGN if (!options.searchService || !(options.searchService instanceof AbstractSearchService)) { options.searchService = new IGNSearchService(options.serviceOptions); } + super.initialize(options); } @@ -164,139 +77,7 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { */ _initEvents (options) { super._initEvents(options); - this.on("search", this.addResultToMap); - } - - addResultToMap (e) { - this.layer.getSource().clear(); - this.extent.getSource().clear(); - let extent, zoom; - if (e.result !== null) { - this.layer.getSource().addFeature(e.result); - extent = e.result.getGeometry().getExtent(); - zoom = 15; - } - if (e.extent !== null) { - this.extent.getSource().addFeature(e.extent); - extent = e.extent.getGeometry().getExtent(); - } - if (this.getMap()) { - let view = this.getMap().getView(); - if (extent) { - view.fit(extent); - view.getZoom(); - if (view.getZoom() > 15) { - view.setZoom(15); - } - } - } - } - - - /** - * - * @param {import("ol/interaction/Select").SelectEvent} e Événement de séléction - */ - _onSelectElement (e) { - let position = e.mapBrowserEvent.coordinate; - if (e.selected.length) { - // Ajoute le popup - const feature = e.selected[0]; - if (feature.getGeometry().getType() === "Point") { - // Place le popup sur le point - position = feature.getGeometry().getCoordinates(); - } - this.popup.setPosition(position); - this.setPopupContent(feature.get("infoPopup")); - this.popup.set("feature", feature); - this.popup.set("layer", e.target.getLayer(feature)); - } else { - this.popup.setPosition(undefined); - this.setPopupContent(""); - this.popup.unset("feature"); - this.popup.unset("layer"); - } - } - - _createPopup () { - // Popup global - let element = this._popupDiv = document.createElement("div"); - // TODO : ajouter gp-feature-info-div lorsque les deux seront pareils - element.className = "GPSearchPopup"; - - // Contenu du popup - let popupContent = this._popupContent = document.createElement("div"); - popupContent.className = "GPPopupContent"; - - // Groupe de boutons - let popupBtns = this._popupBtns = document.createElement("div"); - popupBtns.className = "GPButtonGroups gpf-btns-group"; - - popupBtns.appendChild(this._addCloseButton()); - popupBtns.appendChild(this._addRemoveButton()); - - element.appendChild(popupContent); - element.appendChild(popupBtns); - - const overlay = new Overlay({ - element : element, - positioning : "bottom-center", - }); - - return overlay; - } - - setPopupContent (content) { - this._popupContent.innerHTML = content; - } - - _addCloseButton () { - let closer = document.createElement("button"); - closer.title = closer.ariaLabel = "Fermer la pop-up"; - closer.textContent = "Fermer"; - closer.className = "GPButton gpf-btn fr-icon-close-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline"; - - // Ferme le popup - closer.onclick = this._closePopup.bind(this); - return closer; - } - - _closePopup () { - this.selectInteraction.getFeatures().clear(); - if (this.popup != null) { - this.popup.setPosition(undefined); - } - return false; - } - - _addRemoveButton () { - let remove = document.createElement("button"); - remove.title = remove.ariaLabel = "Supprimer le marqueur"; - remove.textContent = "Supprimer"; - remove.className = "GPButton gpf-btn fr-icon-delete-line fr-btn fr-btn--sm gpf-btn--tertiary fr-btn--tertiary-no-outline"; - - // Supprime la feature - remove.onclick = this._removeFeature.bind(this); - - return remove; - } - - _removeFeature () { - const f = this.popup.get("feature"); - const layer = this.popup.get("layer"); - // Supprime la feature - if (layer && f) { - layer.getSource().removeFeature(f); - - this.dispatchEvent({ - type : this.REMOVE_FEATURE_EVENT, - feature : f, - layer : layer, - }); - - // Ferme le popup - this._closePopup(); - } + // this.on("search", this.addResultToMap); } } diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 7b0121401..68eec0ab4 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -117,6 +117,10 @@ class AbstractSearchService extends BaseObject { this._autocompleteLocations = []; this._locations = []; + + if (options.autocomplete === false) { + this.set("autocomplete", false); + } } /** @@ -182,8 +186,13 @@ class AbstractSearchService extends BaseObject { class DefaultSearchService extends AbstractSearchService { constructor (options) { - super(); options = options || {}; + super(options); + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "DefaultSearchService"; if (options.searchTab) { this._searchTab = options.searchTab || []; }; @@ -226,6 +235,99 @@ class DefaultSearchService extends AbstractSearchService { } +/** + * @classdesc + * DefaultSearchService control + * + * @alias ol.control.DefaultSearchService + * @module SearchService +*/ +class InseeSearchService extends AbstractSearchService { + + constructor (options) { + options = options || {}; + // Aucune autocomplétion + options.autocomplete = false; + super(options); + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "InseeSearchService"; + + this.ignService = new IGNSearchService({ + autocomplete : false, + }); + + this.ignService.on(this.SEARCH_EVENT, this._onSearch.bind(this)); + } + + /** + * Pas de service d'autocomplétion pour l'API géo + */ + autocomplete () { + return; + } + + /** + * @param {Object} object Code insee + * @param {String} object.location Code insee + */ + search (object) { + const insee = object.location; + // Envoi la requête si le chiffre est compris entre 0 et 99999 + const response = this._requestGeoAPI({ value : insee }); + response.then(r => { + if (r instanceof Array && r.length) { + const result = r[0]; + + let location = { + fullText : result.nom, + }; + + let filters = { + citycode : result.code + }; + + this.ignService.search(location, filters); + } + }); + } + + _onSearch (e) { + this.dispatchEvent(e); + } + + /** + * + * @param {Object} settings + */ + async _requestGeoAPI (settings) { + const baseURL = "https://geo.api.gouv.fr/communes"; + const format = "json"; + const fields = ["nom", "code"]; + const url = `${baseURL}?code=${settings.value}&format=${format}&fields=${fields}`; + + try { + const response = await fetch(url, { + headers : { + "Content-Type" : "application/json", + }, + }); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const result = await response.json(); + return result; + } catch (error) { + console.error(error.message); + } + } + +} + + /** * @classdesc * IGNSearchService control @@ -272,6 +374,7 @@ class IGNSearchService extends AbstractSearchService { geocodeOptions : { serviceOptions : {} }, + autocomplete : true, autocompleteOptions : { serviceOptions : { maximumResponses : 5, @@ -331,6 +434,8 @@ class IGNSearchService extends AbstractSearchService { this._currentGeocodingLocation = null; this._suggestedLocations = []; + + console.log(this.options); } @@ -338,7 +443,7 @@ class IGNSearchService extends AbstractSearchService { * @param {String} value Valeur de l'autocomplete * @abstract */ - autocomplete (value) { + autocomplete (value) { if (!value) { return; } @@ -588,8 +693,6 @@ class IGNSearchService extends AbstractSearchService { this._autocompleteLocations = []; this._locations = []; } - - /** * this method is called by event 'click' on 'GPautoCompleteResultsList' tag div @@ -598,7 +701,7 @@ class IGNSearchService extends AbstractSearchService { * @param {Object} location Objet de la recherche * @abstract */ - search (location) { + search (location, filters = {}) { // TODO on souhaite un comportement different pour la selection des reponses // de l'autocompletion : // - liste deroulante des reponses, @@ -626,6 +729,7 @@ class IGNSearchService extends AbstractSearchService { limit : 1, returnTrueGeometry : true, location : label, + filters : filters, onSuccess : this._onSuccessSearch.bind(this), onFailure : this._onFailureSearch.bind(this, location), }); @@ -778,7 +882,7 @@ class IGNSearchService extends AbstractSearchService { } -export { AbstractSearchService, DefaultSearchService, IGNSearchService }; +export { AbstractSearchService, DefaultSearchService, InseeSearchService, IGNSearchService }; // Expose SearchEngine as ol.control.SearchEngine (for a build bundle) if (window.ol) { @@ -787,5 +891,6 @@ if (window.ol) { } window.ol.service.AbstractSearchService = AbstractSearchService; window.ol.service.DefaultSearchService = DefaultSearchService; + window.ol.service.InseeSearchService = InseeSearchService; window.ol.service.IGNSearchService = IGNSearchService; } From fe9f2db89ee3fdd522817385749d722d2c3cda71 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 16 Oct 2025 17:14:08 +0200 Subject: [PATCH 18/73] fix(search): Fix type helper --- .../SearchEngine/AbstractAdvancedSearch.js | 8 ++++---- .../SearchEngine/SearchEngineAdvanced.js | 8 ++++---- .../Controls/SearchEngine/SearchEngineBase.js | 18 +++++++++--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js index 5dd8aed17..4c618d9c5 100644 --- a/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js @@ -89,11 +89,11 @@ class AbstractAdvancedSearch extends Control { _initContainer (options) { let element = document.createElement("form"); element.className = "GPForm gpf-advanced-search-form"; - element.id = helper.getUid("GPAdvancedForm-" + this.CLASSNAME + "-"); + element.id = Helper.getUid("GPAdvancedForm-" + this.CLASSNAME + "-"); let fieldset = document.createElement("fieldset"); fieldset.className = "GPFieldset fr-fieldset gpf-advanced-search-fieldset"; - fieldset.id = helper.getUid("GPAdvancedFieldset-"); + fieldset.id = Helper.getUid("GPAdvancedFieldset-"); this.addInputs(); this.inputs.forEach((elem) => { @@ -112,12 +112,12 @@ class AbstractAdvancedSearch extends Control { const eraseBtn = this.eraseBtn = document.createElement("button"); eraseBtn.type = "reset"; eraseBtn.className = "GPBtn gpf-btn fr-btn fr-btn--tertiary"; - eraseBtn.id = helper.getUid("GPEraseBtn-"); + eraseBtn.id = Helper.getUid("GPEraseBtn-"); eraseBtn.textContent = "Effacer"; const searchBtn = this.searchBtn = document.createElement("button"); searchBtn.className = "GPBtn gpf-btn fr-btn"; - searchBtn.id = helper.getUid("GPSearchBtn-"); + searchBtn.id = Helper.getUid("GPSearchBtn-"); searchBtn.type = "submit"; searchBtn.textContent = "Rechercher"; searchBtn.setAttribute("form", element.id); diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 4b97391f2..12fb73043 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -338,7 +338,7 @@ class SearchEngineAdvanced extends Control { // Gestion de l'affichage des options avancées const element = this.element = document.createElement("div"); element.className = "GPwidget gpf-widget"; - element.id = helper.getUid("GPsearchEngine-Advanced-"); + element.id = Helper.getUid("GPsearchEngine-Advanced-"); // Default base search engine const baseContainer = this.advancedContainer = document.createElement("div"); @@ -357,7 +357,7 @@ class SearchEngineAdvanced extends Control { // Ajout des options avancées const advancedBtn = document.createElement("button"); advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; - advancedBtn.id = helper.getUid("GPSearchEngine-advanced-btn-"); + advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); advancedBtn.type = "button"; advancedBtn.title = "Avancée"; advancedBtn.innerHTML = "Avancée"; @@ -368,7 +368,7 @@ class SearchEngineAdvanced extends Control { // Gestion de l'affichage des options avancées const advancedContainer = this.advancedContainer = document.createElement("div"); advancedContainer.className = "GPAdvancedContainer"; - advancedContainer.id = helper.getUid("GPsearchEngine-AdvancedContainer-"); + advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); this.element.appendChild(advancedContainer); @@ -392,7 +392,7 @@ class SearchEngineAdvanced extends Control { // Accordion const accordion = document.createElement("div"); accordion.className = "fr-collapse"; - accordion.id = helper.getUid("accordion-"); + accordion.id = Helper.getUid("accordion-"); button.setAttribute("aria-controls", accordion.id); section.appendChild(accordion); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 28f0f5f58..366cdeaa3 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -247,17 +247,17 @@ class SearchEngineBase extends Control { _initContainer (options) { const element = this.element = document.createElement("div"); element.className = "GPwidget gpf-widget"; - element.id = helper.getUid("GPsearchEngine-"); + element.id = Helper.getUid("GPsearchEngine-"); // Main container const container = this.container = document.createElement("form"); container.className = options.search ? "GPSearchBar fr-search-bar" : ""; // container.className = "fr-search-bar"; - container.id = helper.getUid("GPsearchInput-Base-"); + container.id = Helper.getUid("GPsearchInput-Base-"); // Création du bouton if (!options.target && options.collapsible) { this.button = document.createElement("button"); - this.button.id = helper.getUid("GPshowSearchEnginePicto-"); + this.button.id = Helper.getUid("GPshowSearchEnginePicto-"); this.button.className = "GPshowOpen GPshowAdvancedToolPicto GPshowSearchEnginePicto gpf-btn fr-icon-search-line fr-btn fr-btn--lg"; this.button.setAttribute("aria-pressed", "true"); // this.button.setAttribute("type", "submit"); @@ -293,7 +293,7 @@ class SearchEngineBase extends Control { const input = this.input = document.createElement("input"); input.type = "text"; input.className = "GPsearchInputText fr-input"; - input.id = helper.getUid("GPsearchInputText-"); + input.id = Helper.getUid("GPsearchInputText-"); input.placeholder = options.placeholder; input.autocomplete = "off"; input.setAttribute("aria-label", options.ariaLabel); @@ -316,7 +316,7 @@ class SearchEngineBase extends Control { const messages = document.createElement("div"); messages.className = "GPMessagesGroup fr-messages-group"; messages.ariaLive = "polite"; - messages.id = helper.getUid("GPMessagesGroup-"); + messages.id = Helper.getUid("GPMessagesGroup-"); input.setAttribute("aria-describedby", messages.id); search.appendChild(messages); @@ -329,7 +329,7 @@ class SearchEngineBase extends Control { if (options.searchButton) { const submit = this.subimtBt = document.createElement("button"); submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; - submit.id = helper.getUid("GPshowSearchEnginePicto-"); + submit.id = Helper.getUid("GPshowSearchEnginePicto-"); submit.type = "submit"; if (options.title) { submit.setAttribute("title", options.title); @@ -350,7 +350,7 @@ class SearchEngineBase extends Control { const autocompleteList = this.autocompleteList = document.createElement("ul"); autocompleteList.className = "GPautoCompleteList"; - autocompleteList.id = helper.getUid("GPautoCompleteList-"); + autocompleteList.id = Helper.getUid("GPautoCompleteList-"); autocompleteList.setAttribute("role", "listbox"); autocompleteList.setAttribute("tabindex", "-1"); autocompleteList.setAttribute("aria-label", "Propositions"); @@ -484,7 +484,7 @@ class SearchEngineBase extends Control { const iconClass = typeClasses[type] || typeClasses["search"]; tab.forEach((item, idx) => { const li = document.createElement("li"); - li.id = helper.getUid("GPsearchHistoric-"); + li.id = Helper.getUid("GPsearchHistoric-"); li.className = `GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; li.setAttribute("role", "option"); li.setAttribute("data-idx", idx); @@ -540,7 +540,7 @@ class SearchEngineBase extends Control { const p = document.createElement("p"); const messageType = type === "error" ? "error" : "valid"; p.className = `GPMessage GPMessage--${messageType} fr-message fr-message--${messageType}`; - p.id = helper.getUid("GPMessage-"); + p.id = Helper.getUid("GPMessage-"); p.textContent = message; messageElement.replaceChildren(p); From 2cdc7c323b52c0e9ad56240901d84992b7db2a7e Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 17 Oct 2025 17:26:53 +0200 Subject: [PATCH 19/73] =?UTF-8?q?feat(search):=20Ajout=20recherche=20avanc?= =?UTF-8?q?=C3=A9e=20coordonn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build/webpack/controls.webpack.config.js | 1 + build/webpack/modules.webpack.config.js | 1 + ...ginebase-modules-dsfr-geocodeAdvanced.html | 6 +- .../DSFRadvancedSearchEngineStyle.css | 54 ++ .../SearchEngine/CoordinateAdvancedSearch.js | 557 ++++++++++++++++++ .../SearchEngine/SearchEngineAdvanced.js | 5 +- .../SearchEngine/SearchEngineGeocodeIGN.js | 1 - 7 files changed, 620 insertions(+), 5 deletions(-) create mode 100644 src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 8c1f7d938..742f119c7 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -80,6 +80,7 @@ module.exports = (env, argv) => { case "LocationAdvancedSearch": case "AbstractAdvancedSearch": case "InseeAdvancedSearch": + case "CoordinateAdancedSearch": // crs break; case "MeasureArea": diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 296846674..6e87c4eac 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -51,6 +51,7 @@ module.exports = (env, argv) => { "GpfExtOlLocationAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "LocationAdvancedSearch.js"), "GpfExtOlInseeAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedSearch.js"), "GpfExtOlAbstractAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedSearch.js"), + "GpfExtOlCoordinateAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "CoordinateAdvancedSearch.js"), "GpfExtOlExport" : path.join(rootdir, "src", "packages", "Controls/Export", "Export.js"), "GpfExtOlMeasureArea" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureArea.js"), "GpfExtOlMeasureAzimuth" : path.join(rootdir, "src", "packages", "Controls", "Measures", "MeasureAzimuth.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index a1e1e95f0..dae3e1bcf 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -7,6 +7,7 @@ + {{/content}} {{#content "head"}} @@ -63,11 +64,14 @@

Ajout du moteur de recherche avec les options par défaut

}) var location = new ol.control.LocationAdvancedSearch({ + }) + + var coordinates = new ol.control.CoordinateAdvancedSearch({ }) // 2. Appel du SearchEngine var search = new ol.control.SearchEngineAdvanced({ - advancedSearch : [insee, location] + advancedSearch : [insee, location, coordinates] }); // 3. Ajout du SearchEngine à la carte diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index a1d09dca2..aac96acbb 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -13,4 +13,58 @@ div[id^=GPsearchEngine-AdvancedContainer] > button.GPSearchEngine-locate { line-height: 1.5rem; min-height: 2rem; padding: 0.25rem 0.75rem; +} + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs { + display: flex; + flex-direction: row; + gap: 1rem; +} + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs { + flex-direction: column; +} + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .display-mask { + display: none; +} + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs .display-mask { + display: unset; + position: absolute; + top: 0.5rem; + left: 1rem; + pointer-events: none; + font-family: inherit; +} + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs input[id^=CoordinateAdvancedSearch-] { + color: transparent; + caret-color: var(--text-default-grey); + letter-spacing: 2px; +} + + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs select { + display: none; +} + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatInputs select { + display: unset; +} + + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .GPCoordinateInputs { + display: flex; + flex-direction: row; + gap: 8px; +} + + +form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .GPCoordinateInputs select { + width:auto; +} + +.GPCoordinateInputs > div { + position: relative; } \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js new file mode 100644 index 000000000..ae91612fc --- /dev/null +++ b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js @@ -0,0 +1,557 @@ +// import CSS +import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; +import Logger from "../../Utils/LoggerByDefault"; +import CRS from "../../CRS/CRS"; +import Helper from "../../Utils/Helper"; +import { + transform as olProjTransform, + get as olProjGet +} from "ol/proj"; +import MathUtils from "../../Utils/MathUtils"; +import Feature from "ol/Feature.js"; +import Point from "ol/geom/Point.js"; + +let logger = Logger.getLogger("abstractAdvancedSearch"); + +/** + * @classdesc + * CoordinateAdvancedSearch Base control + * + * @alias ol.control.CoordinateAdvancedSearch + * @module CoordinateAdvancedSearch +*/ +class CoordinateAdvancedSearch extends AbstractAdvancedSearch { + + /** + * @constructor + * @example + */ + constructor (options) { + super(options); + + this.element.dataset.unitType = this.get("unitType"); + this.element.dataset.unit = this.unit.value; + } + + initialize (options) { + if (!options.name) { + options.name = "Coordonnées"; + } + super.initialize(options); + + this._initCoordinateSearchSystems(options); + this._initCoordinateSearchUnits(options); + + this._currentCoordinateSystem = this._coordinateSearchSystems[0]; + this._currentUnit = this._coordinateSearchUnits[this._currentCoordinateSystem.type]; + + this.set("unitType", this._currentCoordinateSystem.type); + this.set("unit", this._currentUnit[0].code); + + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "CoordinateAdvancedSearch"; + } + + + + /** + * this method is called by the constructor and initialize the projection + * systems. + * getting coordinates in the requested projection : + * see this.onCoordinateSearchSystemChange() + * + * @private + */ + _initCoordinateSearchSystems (options) { + console.log("init coord"); + this._coordinateSearchSystems = []; + // on donne la possibilité à l'utilisateur de modifier + // la liste des systèmes à afficher + // Ex. this.options.coordinateSearch.systems + + // systemes de projection disponible par defaut + let projectionSystemsByDefault = [{ + label : "G\u00e9ographique", + crs : "EPSG:4326", + type : "Geographical" + }, { + label : "Web Mercator", + crs : "EPSG:3857", + type : "Metric" + }, { + label : "Lambert 93", + crs : "EPSG:2154", + type : "Metric" + }]; + + if (options && options.coordinateSearch && options.coordinateSearch.systems) { + let systems = options.coordinateSearch.systems; + // on ajoute les definitions d'un systeme de reference fournies par l'utilisateur + for (let i = 0; i < systems.length; i++) { + let sys = systems[i]; + this._setSystem(sys); + } + } + + // on ajoute les systèmes de projections par défaut + if (this._coordinateSearchSystems.length === 0) { + for (let j = 0; j < projectionSystemsByDefault.length; j++) { + this._setSystem(projectionSystemsByDefault[j]); + } + } + } + + /** + * this method is called by the constructor and initialize the units. + * getting coordinates in the requested units : + * see this.onCoordinateSearchUnitsChange() + * + * @private + */ + _initCoordinateSearchUnits (options) { + console.log("init search units"); + this._coordinateSearchUnits = []; + // on donne la possibilité à l'utilisateur de modifier + // la liste des unités à afficher + // Ex. + // this.options.units : ["DEC", "DMS"] + + // unités disponible par defaut + let projectionUnitsByDefault = { + Geographical : [{ + code : "DEC", + label : "Degrés décimaux", + format : MathUtils.coordinateToDecimal + }, { + code : "DMS", + label : "Degrés sexagésimaux", + format : MathUtils.coordinateToDMS + }], + Metric : [{ + code : "M", + label : "Mètres", + format : MathUtils.coordinateToMeter + }, { + code : "KM", + label : "Kilomètres", + format : MathUtils.coordinateToKMeter + }] + }; + + if (options && options.coordinateSearch && options.coordinateSearch.units) { + let units = options.coordinateSearch.units; + for (let type in projectionUnitsByDefault) { + if (projectionUnitsByDefault.hasOwnProperty(type)) { + let found = false; + for (let j = 0; j < projectionUnitsByDefault[type].length; j++) { + let obj = projectionUnitsByDefault[type][j]; + for (let i = 0; i < units.length; i++) { + let unit = units[i]; + if (obj.code === unit) { + found = true; + if (!this._coordinateSearchUnits[type]) { + this._coordinateSearchUnits[type] = []; + } + this._coordinateSearchUnits[type].push(obj); + } + } + } + if (!found) { + this._coordinateSearchUnits[type] = projectionUnitsByDefault[type]; + } + } + } + } + + // au cas où... + if (typeof this._coordinateSearchUnits === "object" && Object.keys(this._coordinateSearchUnits).length === 0) { + this._coordinateSearchUnits = projectionUnitsByDefault; + } + } + + + /** + * Set additional projection system + * + * @param {Object} system - projection system + * @param {String} system.crs - Proj4 crs alias (from proj4 defs) e.g. "EPSG:4326" + * @param {String} [system.label] - CRS label to be displayed in control. Default is system.crs alias + * @param {String} [system.type] - CRS units type for coordinates conversion (one of control options.units). Default is "Metric" + * @private + */ + /** + * Définit un système de projection supplémentaire + * + * @param {Object} system - système de projection + * @param {String} system.crs - Alias CRS Proj4 (depuis les définitions proj4), ex. "EPSG:4326" + * @param {String} [system.label] - Libellé du CRS affiché dans le contrôle. Par défaut, l’alias system.crs + * @param {String} [system.type] - Type d’unités du CRS pour la conversion des coordonnées. Par défaut : "Metric" + * @private + */ + _setSystem (system) { + if (typeof system !== "object") { + logger.log("[ERROR] CoordinateAdvancedSearch:_setSystem - system parameter should be an object"); + return; + } + if (!system.crs) { + logger.error("crs not defined !"); + return; + } + if (!system.label) { + logger.warn("crs label not defined, use crs code by default."); + system.label = system.crs; + } + if (!system.type) { + logger.warn("type srs not defined, use 'Metric' by default."); + system.type = "Metric"; + } + + // chargement de la definition de la projection + // même si déjà chargé... + CRS.loadByName(system.crs); + + if (!olProjGet(system.crs)) { + logger.error("crs '{}' not available into proj4 definitions !", system.crs); + return; + } + + // add system to control systems + for (var j = 0; j < this._coordinateSearchSystems.length; j++) { + var obj = this._coordinateSearchSystems[j]; + if (system.crs === obj.crs) { + // warn user + logger.info("crs '{}' already configured", obj.crs); + } + } + system.code = this._coordinateSearchSystems.length; + this._coordinateSearchSystems.push(system); + } + + + _getLabelContainer (text, type, input, mandatory = false) { + const container = document.createElement("div"); + container.className = type; + const label = this._createLabel(text, mandatory); + container.appendChild(label); + if (input) { + label.setAttribute("for", input.id); + container.appendChild(input); + } + return container; + } + + _createLabel (text, mandatory = false) { + const label = document.createElement("label"); + label.className = "fr-label"; + const star = mandatory ? "*" : ""; + label.textContent = text + star; + return label; + } + + + _updateLabel (text, container, mandatory = false) { + const label = container.querySelector("label"); + const star = mandatory ? "*" : ""; + label.innerText = text + star; + } + + /** + * Ajoute des éléments d'input dans la collection `this.inputs`; + * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * @protected + * @abstract + */ + addInputs () { + super.addInputs(); + console.log("add inputs"); + + // Indication champs obligatoire + const div = document.createElement("div"); + const mandatory = document.createElement("span"); + mandatory.className = "GPLabelHint fr-hint-text"; + mandatory.textContent = "* Tous les champs sont obligatoires"; + div.appendChild(mandatory); + this.inputs.push(div); + + // Système de référence + const systemSelect = this.system = document.createElement("select"); + systemSelect.className = "fr-select"; + systemSelect.id = systemSelect.name = Helper.getUid("CoordinateAdvancedSearch-system-"); + const systemContainer = this._getLabelContainer("Système de référence", "fr-select-group", systemSelect, true); + this.inputs.push(systemContainer); + + this._coordinateSearchSystems.forEach((elem, index) => { + console.log(elem); + const option = document.createElement("option"); + option.value = index; + option.selected = !index; + option.innerText = elem.label; + systemSelect.appendChild(option); + }); + + // Unité correspondantes + const unitSelect = this.unit = document.createElement("select"); + unitSelect.className = "fr-select"; + unitSelect.id = unitSelect.name = Helper.getUid("CoordinateAdvancedSearch-unit-"); + const unitContainer = this._getLabelContainer("Unité", "fr-select-group", unitSelect, true); + this.inputs.push(unitContainer); + + for (let j = 0; j < this._currentUnit.length; j++) { + const unit = this._currentUnit[j]; + const option = document.createElement("option"); + option.value = (unit.code) ? unit.code : j;; + option.textContent = unit.label || j; + unitSelect.appendChild(option); + } + + // Longitude et Latitude / X et Y + const values = this.lonLatInputs = document.createElement("div"); + values.className = "GPLonLatInputs"; + + // Longitude / X + const lonWrapper = this.lon = this.createCoordinateInput("lon"); + values.appendChild(lonWrapper); + + // Latitude / Y + const latWrapper = this.lat = this.createCoordinateInput("lat"); + values.appendChild(latWrapper); + + this.inputs.push(values); + } + + createCoordinateInput (type) { + // type = "lon" or "lat" + const wrapper = document.createElement("div"); + wrapper.className = "GPCoordinateWrapper"; + + // Label + const labelText = type === "lon" ? "Longitude" : "Latitude"; + const label = this._createLabel(labelText, true); + wrapper.appendChild(label); + + // Input container + const inputContainer = document.createElement("div"); + inputContainer.className = "GPCoordinateInputs"; + wrapper.appendChild(inputContainer); + + // Input + const div = document.createElement("div"); + const input = document.createElement("input"); + input.className = "GPCoordinateInput fr-input"; + input.type = "text"; + input.name = type; + input.inputMode = "numeric"; + input.autocomplete = "off"; + input.required = true; + input.id = Helper.getUid(`CoordinateAdvancedSearch-${type}-`); + div.appendChild(input); + inputContainer.appendChild(div); + + // Lie le label à l'input + label.setAttribute("for", input.id); + + // Mask + const mask = document.createElement("div"); + mask.className = "display-mask"; + mask.textContent = "__°__'__\""; + // inputContainer.appendChild(mask); + div.appendChild(mask); + + // Cardinal points + inputContainer.appendChild(this._createSelectCardinals(type)); + + return wrapper; + } + + + _createSelectCardinals (baseName) { + const options = { + lon : ["O", "E"], + lat : ["N", "S"], + }; + + if (!options[baseName]) { + return; + } else { + const cardinals = document.createElement("select"); + cardinals.className = "fr-select"; + cardinals.id = cardinals.name = Helper.getUid("CoordinateAdvancedSearch-cardinal-"); + + options[baseName].forEach(value => { + const option = document.createElement("option"); + option.value = option.textContent = value; + cardinals.appendChild(option); + }); + + return cardinals; + } + } + + _initEvents (options) { + super._initEvents(options); + + this.system.addEventListener("change", this._updateSystem.bind(this)); + + this.unit.addEventListener("change", this._updateUnits.bind(this)); + + console.log(this); + + this.on("change:unitType", this._updateInputsLabel.bind(this)); + + this.on("change:unit", this._updateInputs.bind(this)); + } + + _updateSystem (e) { + const crs = this._coordinateSearchSystems[e.target.value].crs; + if (crs !== this._currentCoordinateSystem.crs) { + this._currentCoordinateSystem = this._coordinateSearchSystems[e.target.value]; + this._currentUnit = this._coordinateSearchUnits[this._currentCoordinateSystem.type]; + this.unit.replaceChildren(); + for (let j = 0; j < this._currentUnit.length; j++) { + const unit = this._currentUnit[j]; + const option = document.createElement("option"); + option.value = (unit.code) ? unit.code : j; + option.textContent = unit.label || j; + this.unit.appendChild(option); + } + e.target.closest("form").dataset.unit = this.unit.value; + this.set("unit", this.unit.value); + e.target.closest("form").dataset.unitType = this._currentCoordinateSystem.type; + this.set("unitType", this._currentCoordinateSystem.type); + } + } + + _updateUnits (e) { + const value = e.target.value; + if (this.get("unit") !== value) { + e.target.closest("form").dataset.unit = value; + this.set("unit", value); + } + } + + _updateInputsLabel () { + const unitType = this.get("unitType"); + const degree = unitType === "Geographical"; + const labels = { + "lon" : degree ? "Longitude" : "X", + "lat" : degree ? "Latitude" : "Y", + }; + this._updateLabel(labels.lon, this.lon, true); + this._updateLabel(labels.lat, this.lat, true); + } + + _updateInputs () { + console.log("update inputs"); + if (this.get("unit") === "DMS") { + this.lonLatInputs.querySelectorAll("input").forEach(input => { + input.value = ""; + input.minLength = "6"; + input.maxLength = "6"; + input.addEventListener("beforeinput", this._onlonLatBeforeInput.bind(this)); + input.addEventListener("input", this._onlonLatInput.bind(this)); + }); + } else { + this.lonLatInputs.querySelectorAll("input").forEach(input => { + input.value = ""; + delete input.minLength; + delete input.maxLength; + input.removeEventListener("beforeinput", this._onlonLatBeforeInput); + input.removeEventListener("input", this._onlonLatInput); + }); + } + } + + /** + * + * @param {InputEvent} e + * @returns + */ + _onlonLatBeforeInput (e) { + // const regex = /^(?:\d{2}°\d{2}'\d{2}(?:"|''))$|^\d{6}$/; + const regex = /^\d+$/; + // Vérifie si c'est un chiffre + if (e.inputType.startsWith("insert") && !regex.test(e.data)) { + e.preventDefault(); + } + } + + _format (value) { + const v = value.padEnd(6, "_"); + return `${v.slice(0,2)}°${v.slice(2,4)}'${v.slice(4,6)}"`; + } + + /** + * + * @param {InputEvent} e + */ + _onlonLatInput (e) { + const value = e.target.value; + const mask = e.target.parentElement.querySelector(".display-mask"); + mask.textContent = this._format(value); + } + + /** + * + * @param {PointerEvent} e + * @protected + */ + _onSearch (e) { + super._onSearch(e); + // Récupère les valeurs des inputs + let lon = this.lon.querySelector("input").value; + let lat = this.lat.querySelector("input").value; + const hemispheres = this.lon.querySelector("select").value + this.lat.querySelector("select").value; + console.log(lon, lat, hemispheres); + if (this.get("unit") === "DMS") { + // Transforme les DMS en degrés décimaux + lon = MathUtils.dmsToDecimal( + parseInt(lon.substring(0, 2)), + parseInt(lon.substring(2, 4)), + parseInt(lon.substring(4, 6)), + hemispheres + ); + lat = MathUtils.dmsToDecimal( + parseInt(lat.substring(0, 2)), + parseInt(lat.substring(2, 4)), + parseInt(lat.substring(4, 6)), + hemispheres + ); + } else if (this.get("unit") === "KM") { + // Transforme les kilomètres en mètre + lon = parseFloat(lon) * 1000; + lat = parseFloat(lat) * 1000; + } else { + // Récupère les valeurs en float + lon = parseFloat(lon); + lat = parseFloat(lat); + } + + // Projette les coordonnées dans les coordonnées de la carte + let coords = [lon, lat]; + const mapProj = this.getMap().getView().getProjection().getCode(); + console.log(this._currentCoordinateSystem); + const currentProj = this._currentCoordinateSystem.crs; + if (mapProj !== currentProj) { + coords = olProjTransform(coords, currentProj, mapProj); + } + + const geom = new Point(coords); + const f = new Feature({ geometry : geom }); + + this.dispatchEvent({ + type : "search", + result : f, + }); + } + +} + +export default CoordinateAdvancedSearch; + +// Expose CoordinateAdvancedSearch as ol.control.CoordinateAdvancedSearch (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.CoordinateAdvancedSearch = CoordinateAdvancedSearch; +} diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 12fb73043..56bd254d6 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -201,12 +201,12 @@ class SearchEngineAdvanced extends Control { this.layer.getSource().clear(); this.extent.getSource().clear(); let extent, zoom; - if (e.result !== null) { + if (!!e.result) { this.layer.getSource().addFeature(e.result); extent = e.result.getGeometry().getExtent(); zoom = 15; } - if (e.extent !== null) { + if (!!e.extent) { this.extent.getSource().addFeature(e.extent); extent = e.extent.getGeometry().getExtent(); } @@ -214,7 +214,6 @@ class SearchEngineAdvanced extends Control { let view = this.getMap().getView(); if (extent) { view.fit(extent); - view.getZoom(); if (view.getZoom() > 15) { view.setZoom(15); } diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index a572dfbe3..6d34c8cab 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -65,7 +65,6 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { options.searchService = new IGNSearchService(options.serviceOptions); } - super.initialize(options); } From 9982469cfc1808ea7bf9fe9616f26ea4e05f6f41 Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 20 Oct 2025 17:23:38 +0200 Subject: [PATCH 20/73] FIX input outline UPD Search service options --- .../SearchEngine/GPFadvancedSearchEngine.css | 3 ++ .../SearchEngine/LocationAdvancedSearch.js | 46 ++++++++++--------- .../SearchEngine/SearchEngineGeocodeIGN.js | 1 + src/packages/Controls/SearchEngine/Service.js | 21 +++++++-- 4 files changed, 44 insertions(+), 27 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 80ab30b0d..478c3b958 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -2,10 +2,13 @@ div[id^=GPsearchEngine-Advanced] { width: 312px; + padding: 5px; + margin: -5px; } div[id^=GPsearchEngine-Advanced] div[id^=GPsearchEngine-] { width: 100%; + margin: 0 5px; } form[id^=GPAdvancedForm-], form[id^=GPAdvancedForm-] * { diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 87a5e6f4b..91ba01a24 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -1,6 +1,6 @@ import Helper from "../../Utils/Helper"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; -import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; +import { IGNSearchService } from "./Service"; class LocationAdvancedSearch extends AbstractAdvancedSearch { @@ -18,7 +18,14 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // call ol.control.Control constructor super(options); - this.search.on("search", function (e) { + // Search service + this.searchService = new IGNSearchService({ + index : "poi", + limit : 1, + returnTrueGeometry : true + }); + // Do something on search + this.searchService.on("search", function (e) { this.dispatchEvent(e); }.bind(this)); } @@ -31,11 +38,12 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { */ this.CLASSNAME = "LocationAdvancedSearch"; } - + /* setMap (map) { super.setMap(map); this.search.setMap(map); } + */ _getLabelContainer (text, type, input) { const container = document.createElement("div"); container.className = type; @@ -81,19 +89,20 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { }); // Search input - const searchContainer = this._getLabelContainer("Renseigner un lieu", "fr-input-group"); - this.search = new SearchEngineGeocodeIGN({ - autocomplete : false, - target : searchContainer, - historic : "GPAdvancedLocation", - maximumEntries : 0 - }); + const searchInput = this.searchInput = document.createElement("input"); + searchInput.className = "fr-input"; + searchInput.type = "text"; + searchInput.name = "search"; + searchInput.id = Helper.getUid("LocationAdvancedSearch-search-"); + this._getLabelContainer("Renseigner un lieu", "fr-input-group", searchInput); // Code postal const postalInput = document.createElement("input"); postalInput.className = "fr-input"; postalInput.type = "text"; postalInput.name = "postalCode"; + postalInput.pattern = "(\\d{5}"; + postalInput.title = "Code postal à 5 chiffres"; postalInput.id = Helper.getUid("LocationAdvancedSearch-postal-"); this._getLabelContainer("Code postal", "fr-input-group", postalInput); postalInput.addEventListener("change", () => { @@ -105,6 +114,8 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { inseeInput.className = "fr-input"; inseeInput.name = "cityCode"; inseeInput.type = "text"; + postalInput.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; + postalInput.title = "Code INSEE sur 5 caractères"; inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput); inseeInput.addEventListener("change", () => { @@ -125,7 +136,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { this.element.querySelectorAll("input").forEach(input => { input.value = ""; }); - this.filters = { + this.filter = { category : "", postcode : "", citycode : "" @@ -136,18 +147,9 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { */ _onSearch (e) { super._onSearch(e); - const value = this.search.input.value; + const value = this.searchInput.value; if (value) { - this.search.searchService._requestGeocoding({ - "index" : "poi", - "limit" : 1, - maximumResponses : 1, - filters : this.filter, - "returnTrueGeometry" : true, - "location" : value, - onSuccess : e => this.search.searchService._onSuccessSearch(e), - onFailure : e => console.log("ERROR", e) - }); + this.searchService.search(value, this.filter); } } diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 6d34c8cab..871156693 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -59,6 +59,7 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { options.serviceOptions = options.serviceOptions ? options.serviceOptions : {}; options.serviceOptions.autocomplete = false; } + options.returnTrueGeometry = true; // Créé le serbice de géocodage IGN if (!options.searchService || !(options.searchService instanceof AbstractSearchService)) { diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 68eec0ab4..33dd8a662 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -22,6 +22,10 @@ var logger = Logger.getLogger("searchengine"); * @property {AutocompleteOptions} [autocompleteOptions] - Options spécifiques à l'autocomplétion * @property {SearchOptions} [searchOptions] - Options spécifiques à la recherche finale * @property {GeocodeOptions} [geocodeOptions] - Options spécifiques au géocodage + * @property {boolean} [autocomplete=true] + * @property {String} [index="address,poi"] + * @property {Number} [limit=1] + * @property {boolean} [returnTrueGeometry=false] */ /** @@ -121,6 +125,9 @@ class AbstractSearchService extends BaseObject { if (options.autocomplete === false) { this.set("autocomplete", false); } + this.set("index", options.index || "address,poi"); + this.set("limit", typeof options.limit === "number" ? options.limit : 1); + this.set("returnTrueGeometry", !!options.returnTrueGeometry); } /** @@ -716,18 +723,22 @@ class IGNSearchService extends AbstractSearchService { if (location === undefined) { return; } - // on ajoute le texte de l'autocomplétion dans l'input - let label = GeocodeUtils.getSuggestedLocationFreeform(location); + let label; + if (typeof location === "string") { + label = location; + } else { + label = GeocodeUtils.getSuggestedLocationFreeform(location); + } // on sauvegarde le localisant this._currentGeocodingLocation = label; // on centre la vue et positionne le marker, à la position reprojetée dans la projection de la carte this._requestGeocoding({ - index : "address,poi", - limit : 1, - returnTrueGeometry : true, + index : this.get("index") || "address,poi", + limit : this.get("limit") || 1, + returnTrueGeometry : this.get("returnTrueGeometry"), location : label, filters : filters, onSuccess : this._onSuccessSearch.bind(this), From 3ce587150496bb9ecab4bdbf8c8d997777ad2210 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 10:35:03 +0200 Subject: [PATCH 21/73] =?UTF-8?q?fix(search):=20Fix=20popup=20+=20chgt=20r?= =?UTF-8?q?ech.=20avanc=C3=A9e=20coordonn=C3=A9es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchEngine/CoordinateAdvancedSearch.js | 63 +++-- .../SearchEngine/SearchEngineAdvanced.js | 226 +++++++++--------- src/packages/Controls/SearchEngine/Service.js | 4 + 3 files changed, 165 insertions(+), 128 deletions(-) diff --git a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js index ae91612fc..e4a896b27 100644 --- a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js @@ -66,7 +66,6 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { * @private */ _initCoordinateSearchSystems (options) { - console.log("init coord"); this._coordinateSearchSystems = []; // on donne la possibilité à l'utilisateur de modifier // la liste des systèmes à afficher @@ -112,7 +111,6 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { * @private */ _initCoordinateSearchUnits (options) { - console.log("init search units"); this._coordinateSearchUnits = []; // on donne la possibilité à l'utilisateur de modifier // la liste des unités à afficher @@ -266,7 +264,6 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { */ addInputs () { super.addInputs(); - console.log("add inputs"); // Indication champs obligatoire const div = document.createElement("div"); @@ -284,7 +281,6 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.inputs.push(systemContainer); this._coordinateSearchSystems.forEach((elem, index) => { - console.log(elem); const option = document.createElement("option"); option.value = index; option.selected = !index; @@ -397,8 +393,6 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.unit.addEventListener("change", this._updateUnits.bind(this)); - console.log(this); - this.on("change:unitType", this._updateInputsLabel.bind(this)); this.on("change:unit", this._updateInputs.bind(this)); @@ -441,11 +435,19 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { }; this._updateLabel(labels.lon, this.lon, true); this._updateLabel(labels.lat, this.lat, true); + + // Reset la valeur + this.lon.querySelector("input").value = ""; + this.lat.querySelector("input").value = ""; } _updateInputs () { - console.log("update inputs"); - if (this.get("unit") === "DMS") { + const unit = this.get("unit"); + let factor = 1; + if (this.get("unitType") === "Metric") { + factor = unit === "KM" ? 0.001 : 1000; + } + if (unit === "DMS") { this.lonLatInputs.querySelectorAll("input").forEach(input => { input.value = ""; input.minLength = "6"; @@ -455,9 +457,9 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { }); } else { this.lonLatInputs.querySelectorAll("input").forEach(input => { - input.value = ""; - delete input.minLength; - delete input.maxLength; + input.value = input.value === "" || isNaN(input.value) ? "" : parseFloat(input.value) * factor; + input.removeAttribute("minLength"); + input.removeAttribute("maxLength"); input.removeEventListener("beforeinput", this._onlonLatBeforeInput); input.removeEventListener("input", this._onlonLatInput); }); @@ -503,21 +505,19 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { // Récupère les valeurs des inputs let lon = this.lon.querySelector("input").value; let lat = this.lat.querySelector("input").value; - const hemispheres = this.lon.querySelector("select").value + this.lat.querySelector("select").value; - console.log(lon, lat, hemispheres); if (this.get("unit") === "DMS") { // Transforme les DMS en degrés décimaux lon = MathUtils.dmsToDecimal( parseInt(lon.substring(0, 2)), parseInt(lon.substring(2, 4)), parseInt(lon.substring(4, 6)), - hemispheres + this.lon.querySelector("select").value ); lat = MathUtils.dmsToDecimal( parseInt(lat.substring(0, 2)), parseInt(lat.substring(2, 4)), parseInt(lat.substring(4, 6)), - hemispheres + this.lat.querySelector("select").value ); } else if (this.get("unit") === "KM") { // Transforme les kilomètres en mètre @@ -532,7 +532,6 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { // Projette les coordonnées dans les coordonnées de la carte let coords = [lon, lat]; const mapProj = this.getMap().getView().getProjection().getCode(); - console.log(this._currentCoordinateSystem); const currentProj = this._currentCoordinateSystem.crs; if (mapProj !== currentProj) { coords = olProjTransform(coords, currentProj, mapProj); @@ -540,13 +539,43 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { const geom = new Point(coords); const f = new Feature({ geometry : geom }); - + // Ajout des coordonnées pour le popup + f.set("infoPopup", this._createInfoPopup(lon, lat)); + this.dispatchEvent({ type : "search", result : f, }); } + _createInfoPopup (lon, lat) { + let x, y, valueX, valueY; + if (this.get("unitType") === "Geographical") { + x = "Longitude"; + y = "Latitude"; + + if (this.get("unit") === "DMS") { + const lon = this.lon.querySelector("input").value; + const lonCardinal = this.lon.querySelector("select").value; + valueX = `${parseInt(lon.substring(0, 2))}°${parseInt(lon.substring(2, 4))}'${parseInt(lon.substring(4, 6))}" ${lonCardinal}`; + + const lat = this.lat.querySelector("input").value; + const latCardinal = this.lat.querySelector("select").value; + valueY = `${parseInt(lat.substring(0, 2))}°${parseInt(lat.substring(2, 4))}'${parseInt(lat.substring(4, 6))}" ${latCardinal}`; + } else { + valueX = `${lon} °`; + valueY = `${lat} °`; + } + } else { + x = "X"; + y = "Y"; + valueX = `${lon} ${this.get("unit").toLowerCase()}`; + valueY = `${lat} ${this.get("unit").toLowerCase()}`; + } + const infoPopup = `${x} : ${valueX}
${y} : ${valueY}`; + return infoPopup; + } + } export default CoordinateAdvancedSearch; diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 56bd254d6..3718e937d 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -4,9 +4,9 @@ import OlFeature from "ol/Feature"; import Point from "ol/geom/Point"; import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; import Helper from "../../Utils/Helper"; -import { Select } from "ol/interaction"; +import Select from "ol/interaction/Select"; -import { Vector } from "ol/layer"; +import Vector from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; import Overlay from "ol/Overlay.js"; @@ -153,10 +153,12 @@ class SearchEngineAdvanced extends Control { search.setMap(map); }); - this.extent.setMap(map); - this.layer.setMap(map); - this.selectInteraction.setMap(map); - this.popup.setMap(map); + if (map) { + map.addLayer(this.extent); + map.addLayer(this.layer); + map.addInteraction(this.selectInteraction); + map.addOverlay(this.popup); + } } _initEvents (options) { @@ -197,14 +199,115 @@ class SearchEngineAdvanced extends Control { } + /** + * Créé le conteneur + * + * @param {Object} options Options du constructeur + */ + _initContainer (options) { + // Gestion de l'affichage des options avancées + const element = this.element = document.createElement("div"); + element.className = "GPwidget gpf-widget"; + element.id = Helper.getUid("GPsearchEngine-Advanced-"); + + // Default base search engine + const baseContainer = this.advancedContainer = document.createElement("div"); + this.element.appendChild(baseContainer); + options.target = baseContainer; + options.searchButton = true; + options.search = true; + this.baseSearchEngine = new SearchEngineGeocodeIGN(options); + this.baseSearchEngine.on(["select", "search", "autocomplete"], (e) => { + this.dispatchEvent(e); + }); + + // Geolocation + this.baseSearchEngine.autocompleteHeader.appendChild(this._getGeolocButton()); + + // Ajout des options avancées + const advancedBtn = document.createElement("button"); + advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; + advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); + advancedBtn.type = "button"; + advancedBtn.title = "Avancée"; + advancedBtn.innerHTML = "Avancée"; + advancedBtn.setAttribute("aria-label", "Afficher les options avancées"); + advancedBtn.setAttribute("aria-expanded", "false"); + this.baseSearchEngine.optionscontainer.appendChild(advancedBtn); + + // Gestion de l'affichage des options avancées + const advancedContainer = this.advancedContainer = document.createElement("div"); + advancedContainer.className = "GPAdvancedContainer"; + advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); + advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); + this.element.appendChild(advancedContainer); + + // Geolocation + advancedContainer.appendChild(this._getGeolocButton()); + + // Formulaires specifiques + this._searchForms.forEach(Search => { + const section = document.createElement("section"); + section.className = "fr-accordion"; + advancedContainer.appendChild(section); + const title = document.createElement("h3"); + title.className = "fr-accordion__title"; + section.appendChild(title); + const button = document.createElement("button"); + button.type = "button"; + button.className = "fr-accordion__btn"; + button.setAttribute("aria-expanded", "false"); + button.innerText = Search.getName(); + title.appendChild(button); + // Accordion + const accordion = document.createElement("div"); + accordion.className = "fr-collapse"; + accordion.id = Helper.getUid("accordion-"); + button.setAttribute("aria-controls", accordion.id); + section.appendChild(accordion); + + // Contenu recherche avancée + Search.setTarget(accordion); + + button.addEventListener("click", () => { + const expanded = button.getAttribute("aria-expanded") === "true"; + advancedContainer.querySelectorAll("section").forEach(sec => { + sec.querySelector(".fr-collapse").classList.remove("fr-collapse--expanded"); + sec.querySelector("button").setAttribute("aria-expanded", "false"); + advancedContainer.dataset.open = !expanded; + if (!expanded) { + sec.classList.add("fr-hidden"); + } else { + sec.classList.remove("fr-hidden"); + } + }); + if (!expanded) { + button.setAttribute("aria-expanded", "true"); + accordion.classList.add("fr-collapse--expanded"); + section.classList.remove("fr-hidden"); + } + }); + }); + + // Gestion du bouton avancé + advancedBtn.setAttribute("aria-controls", advancedContainer.id); + advancedBtn.addEventListener("click", (e) => { + e.preventDefault(); + const isHidden = advancedBtn.getAttribute("aria-expanded") === "false"; + advancedBtn.setAttribute("aria-expanded", isHidden); + this.baseSearchEngine.setActive(isHidden); + }); + } + + addResultToMap (e) { + this._closePopup(); this.layer.getSource().clear(); this.extent.getSource().clear(); - let extent, zoom; + let extent; if (!!e.result) { this.layer.getSource().addFeature(e.result); extent = e.result.getGeometry().getExtent(); - zoom = 15; } if (!!e.extent) { this.extent.getSource().addFeature(e.extent); @@ -229,6 +332,8 @@ class SearchEngineAdvanced extends Control { _onSelectElement (e) { let position = e.mapBrowserEvent.coordinate; if (e.selected.length) { + // Ferme l'ancien popup + this.popup.setPosition(undefined); // Ajoute le popup const feature = e.selected[0]; if (feature.getGeometry().getType() === "Point") { @@ -236,7 +341,7 @@ class SearchEngineAdvanced extends Control { position = feature.getGeometry().getCoordinates(); } this.popup.setPosition(position); - this.setPopupContent(feature.get("infoPopup")); + this.setPopupContent(feature.get("infoPopup") || ""); this.popup.set("feature", feature); this.popup.set("layer", e.target.getLayer(feature)); } else { @@ -292,7 +397,7 @@ class SearchEngineAdvanced extends Control { _closePopup () { this.selectInteraction.getFeatures().clear(); - if (this.popup != null) { + if (this.popup !== null) { this.popup.setPosition(undefined); } return false; @@ -328,106 +433,6 @@ class SearchEngineAdvanced extends Control { } } - /** - * Ajoute le contrôle à la carte. - * @override - * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. - */ - _initContainer (options) { - // Gestion de l'affichage des options avancées - const element = this.element = document.createElement("div"); - element.className = "GPwidget gpf-widget"; - element.id = Helper.getUid("GPsearchEngine-Advanced-"); - - // Default base search engine - const baseContainer = this.advancedContainer = document.createElement("div"); - this.element.appendChild(baseContainer); - options.target = baseContainer; - options.searchButton = true; - options.search = true; - this.baseSearchEngine = new SearchEngineGeocodeIGN(options); - this.baseSearchEngine.on(["select", "search", "autocomplete"], (e) => { - this.dispatchEvent(e); - }); - - // Geolocation - this.baseSearchEngine.autocompleteHeader.appendChild(this._getGeolocButton()); - - // Ajout des options avancées - const advancedBtn = document.createElement("button"); - advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; - advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); - advancedBtn.type = "button"; - advancedBtn.title = "Avancée"; - advancedBtn.innerHTML = "Avancée"; - advancedBtn.setAttribute("aria-label", "Afficher les options avancées"); - advancedBtn.setAttribute("aria-expanded", "false"); - this.baseSearchEngine.optionscontainer.appendChild(advancedBtn); - - // Gestion de l'affichage des options avancées - const advancedContainer = this.advancedContainer = document.createElement("div"); - advancedContainer.className = "GPAdvancedContainer"; - advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); - advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); - this.element.appendChild(advancedContainer); - - // Geolocation - advancedContainer.appendChild(this._getGeolocButton()); - - // Formulaires specifiques - this._searchForms.forEach(Search => { - const section = document.createElement("section"); - section.className = "fr-accordion"; - advancedContainer.appendChild(section); - const title = document.createElement("h3"); - title.className = "fr-accordion__title"; - section.appendChild(title); - const button = document.createElement("button"); - button.type = "button"; - button.className = "fr-accordion__btn"; - button.setAttribute("aria-expanded", "false"); - button.innerText = Search.getName(); - title.appendChild(button); - // Accordion - const accordion = document.createElement("div"); - accordion.className = "fr-collapse"; - accordion.id = Helper.getUid("accordion-"); - button.setAttribute("aria-controls", accordion.id); - section.appendChild(accordion); - - // Contenu recherche avancée - Search.setTarget(accordion); - - button.addEventListener("click", () => { - const expanded = button.getAttribute("aria-expanded") === "true"; - advancedContainer.querySelectorAll("section").forEach(sec => { - sec.querySelector(".fr-collapse").classList.remove("fr-collapse--expanded"); - sec.querySelector("button").setAttribute("aria-expanded", "false"); - advancedContainer.dataset.open = !expanded; - if (!expanded) { - sec.classList.add("fr-hidden"); - } else { - sec.classList.remove("fr-hidden"); - } - }); - if (!expanded) { - button.setAttribute("aria-expanded", "true"); - accordion.classList.add("fr-collapse--expanded"); - section.classList.remove("fr-hidden"); - } - }); - }); - - // Gestion du bouton avancé - advancedBtn.setAttribute("aria-controls", advancedContainer.id); - advancedBtn.addEventListener("click", (e) => { - e.preventDefault(); - const isHidden = advancedBtn.getAttribute("aria-expanded") === "false"; - advancedBtn.setAttribute("aria-expanded", isHidden); - this.baseSearchEngine.setActive(isHidden); - }); - } - _getGeolocButton () { const locationBtn = document.createElement("button"); locationBtn.innerText = "Me géolocaliser"; @@ -440,7 +445,6 @@ class SearchEngineAdvanced extends Control { } onAdvancedSearchResult (e) { - console.log(e); if (e.result instanceof Array) { // TODO : GÉRER MULTIPLE RÉSULTATS } else if (e.result instanceof Feature) { diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 33dd8a662..ddd62dcb0 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -834,6 +834,10 @@ class IGNSearchService extends AbstractSearchService { extent = null; f = new Feature({ geometry : geometry }); } + } else { + const geom = new Point(position); + geom.transform("EPSG:4326", "EPSG:3857"); + f = new Feature({ geometry : geom }); } if (extent) { From 2f3f76dce5d53f746efa92611f4df7778454cda9 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 10:54:03 +0200 Subject: [PATCH 22/73] fix(search): Fix historique (mauvaise comparaison) --- .../Controls/SearchEngine/SearchEngineBase.js | 42 ++++++++++++++----- 1 file changed, 31 insertions(+), 11 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 366cdeaa3..b41f7290b 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -511,22 +511,42 @@ class SearchEngineBase extends Control { */ _updateHistoric (value) { if (this._historic) { - // Update historic - const idx = this._historic.indexOf(value); - if (idx !== -1) { - this._historic.splice(idx, 1); - } - this._historic.unshift(value); - // Remove last if > 10 - if (this._historic.length > (this.get("maximumEntries") || 10)) { - this._historic.pop(); + let isPresent = false; + for (let i = 0; i < this._historic.length; i++) { + const elem = this._historic[i]; + if (this._isEqual(elem, value)) { + // L'élément est déjà dans l'historique + isPresent = true; + break; + } } + if (!isPresent) { + const length = this._historic.unshift(value); + // Retire le dernier élément si le nombre maximal est atteint + if (length > (this.get("maximumEntries"))) { + this._historic.pop(); + } - // Save in localStorage - localStorage.setItem(this._historicName, JSON.stringify(this._historic)); + // Save in localStorage + localStorage.setItem(this._historicName, JSON.stringify(this._historic)); + } } } + /** + * Vérifie si deux éléments (objets) sont égaux. + * + * @param {Array} a Premier objet + * @param {Object} b Objet de comparaison + */ + _isEqual (a, b) { + // TODO : Améliorer comparaison ? + const jsonA = JSON.stringify(a); + const jsonB = JSON.stringify(b); + + return jsonA === jsonB; + } + /** * Ajoute un message à un champs de saisie * @param {HTMLInputElement|HTMLSelectElement} input Champs de saisie From 76de403a1c21a472ae95779bd2cb02e902035f6c Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 14:16:38 +0200 Subject: [PATCH 23/73] =?UTF-8?q?fix(search):=20Fix=20CSS=20mobile=20et=20?= =?UTF-8?q?=C3=A9l=C3=A9ments=20DOM?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ginebase-modules-dsfr-geocodeAdvanced.html | 3 ++ .../DSFRadvancedSearchEngineStyle.css | 28 ++++++++++++- .../SearchEngine/DSFRsearchEngineStyle.css | 42 ++++++++++++++++--- .../SearchEngine/GPFadvancedSearchEngine.css | 1 - .../GPFadvancedSearchEngineStyle.css | 4 ++ .../Controls/SearchEngine/GPFsearchEngine.css | 17 +------- .../SearchEngine/GPFsearchEngineStyle.css | 8 +++- .../SearchEngine/SearchEngineAdvanced.js | 10 +++-- .../Controls/SearchEngine/SearchEngineBase.js | 25 ++++++----- 9 files changed, 99 insertions(+), 39 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index dae3e1bcf..7990e3651 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -20,6 +20,9 @@ width: 100%; height: 500px; } + [id^="GPsearchEngine-"] { + top: 8px; + } {{/content}} diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index aac96acbb..a115013ad 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -4,6 +4,17 @@ div[data-open="true"] > section.fr-accordion::before { box-shadow: inset 0 1px 0 0 var(--border-default-grey); } + +div[id^=GPsearchEngine-Advanced] { + width: 100%; +} + +@media (min-width: 569px) { + div[id^=GPsearchEngine-Advanced] { + width: 360px; + } +} + div[id^=GPsearchEngine-AdvancedContainer] > button.GPSearchEngine-locate { margin-bottom : 0.75rem; } @@ -53,7 +64,6 @@ form[id^=GPAdvancedForm-CoordinateAdvancedSearch][data-unit="DMS"] > .GPLonLatIn display: unset; } - form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .GPCoordinateInputs { display: flex; flex-direction: row; @@ -67,4 +77,20 @@ form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs .GPCoordinat .GPCoordinateInputs > div { position: relative; +} + + +[id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer { + background-color: var(--background-default-grey); +} + +[id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled { + background-color: var(--background-action-high-blue-france); +} + +/* Surcharge DSFR */ +div[id^=GPsearchEngine-AdvancedContainer-] .fr-accordion .fr-collapse { + margin: 0 -0.25rem; + padding-left: 1rem; + padding-right: 1rem; } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index 2d204e6e0..f461f8fbf 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -1,3 +1,12 @@ + +/* Mode Mobile */ +@media (min-width: 576px) { + [id^="GPsearchEngine-"] { + top: 8px; + left: 46px; + } +} + .gpf-widget-padding { padding-top: 5px; padding-bottom: 5px; @@ -272,33 +281,46 @@ div[id^=GPgeocodeResults-] { } [id^="GPsearchEngine"] ul { - padding: 12px; - padding-bottom: 0px; - margin: 0; + padding: 0 12px; + margin: 0.75rem 0 0; width: 100%; } +[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"] .GPOptionsContainer button { + font-size: 0.875rem; + line-height: 1.5rem; + min-height: 2rem; + padding: 0.25rem 0.75rem; +} + [id^="GPsearchEngine"] ul:empty { padding: unset; + margin: unset; } [id^="GPsearchEngine"] ul li { padding: 0.75rem 0.5rem; color: var(--text-action-high-grey); + box-shadow: 0 -1px 0 0 var(--border-default-grey); +} + +[id^="GPsearchEngine"] ul li:first-child { + padding: 0.75rem 0.5rem; + color: var(--text-action-high-grey); + box-shadow: none; } + [id^="GPsearchEngine"] ul li::before { margin-right: 0.5rem; } [id^="GPsearchEngine"] ul li.active, [id^="GPsearchEngine"] ul li:hover { - /* color: #000000; */ background-color: var(--background-default-grey-hover); } [id^="GPsearchEngine"] ul li:active { - /* color: #000000; */ background-color: var(--background-default-grey-active); } @@ -362,4 +384,14 @@ form[id^=GPsearchInput] > input[id^=GPsearchInputText] { flex-direction: row-reverse; flex: 0 0 auto; align-items: center; +} + +form.GPSearchBar > .GPInputGroup,form.GPSearchBar > button[id^=GPshowSearchEnginePicto-].fr-btn, form.GPSearchBar > .GPInputGroup > input { + height: 48px; + max-height: unset; +} + +form.GPSearchBar > button[id^=GPshowSearchEnginePicto-].fr-btn { + width: 48px; + max-width: unset; } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 478c3b958..6a14c0af2 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -1,7 +1,6 @@ /** STYLE COMMUN RECHERCHE AVANCEE **/ div[id^=GPsearchEngine-Advanced] { - width: 312px; padding: 5px; margin: -5px; } diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css index 69223b815..96ff1301e 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngineStyle.css @@ -1 +1,5 @@ /** STYLE NON DSFR RECHERCHE AVANCEE **/ + +div[id^=GPsearchEngine-Advanced] { + width:312px; +} \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 08632b730..11c4a06eb 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -1,10 +1,5 @@ /* SEARCH ENGINE */ -[id^="GPsearchEngine-"] { - top: 8px; - left: 46px; -} - [id^="GPsearchEngine-"] { /* dsfr */ display: flex; @@ -231,7 +226,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer button { max-width: unset; min-height: 1rem; - line-height: 1rem; margin: 0.25rem 0.5rem; border-radius: 0; } @@ -240,6 +234,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } .GPautoCompleteHeader { + margin-bottom: 0.75rem; padding: 0.75rem 0.75rem 0; } @@ -280,7 +275,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } [id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer { display: none; - background-color: var(--background-default-grey); box-sizing: content-box; max-width: 100%; max-height: unset; @@ -294,15 +288,6 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ transform: rotate(180deg); } -[id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled { - background-color: var(--background-action-high-blue-france); -} -[id^="GPsearchEngine-Advanced"] .GPsearchInputSubmit:disabled, -[id^="GPsearchEngine-Advanced"] .GPsearchInputText:disabled { - cursor: default; - user-select: none; -} - /* [id^="GPsearchEngine-Advanced"] .GPAdvancedContainer[data-open="true"] > button { display: none; } */ diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css index c3c78eb7b..7b0e974d5 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngineStyle.css @@ -3,6 +3,8 @@ [id^="GPsearchEngine-"] { flex-direction: row; align-items: center; + top: 8px; + left: 46px; } .position-container-top-left [id^="GPsearchEngine-"], @@ -159,10 +161,14 @@ dialog[id^=GPgeocodeResultsList] { width: 280px; position: absolute; max-height: 140px; - background-color: var(--background-default-grey); + background-color: #ffffff; } +[id^="GPsearchEngine-Advanced"] > .GPAdvancedContainer { + background-color: #ffffff; +} + div[id^=GPautoCompleteList] { top: 35px; } diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 3718e937d..8962b5f41 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -153,6 +153,8 @@ class SearchEngineAdvanced extends Control { search.setMap(map); }); + this.element.appendChild(this.advancedContainer); + if (map) { map.addLayer(this.extent); map.addLayer(this.layer); @@ -211,9 +213,9 @@ class SearchEngineAdvanced extends Control { element.id = Helper.getUid("GPsearchEngine-Advanced-"); // Default base search engine - const baseContainer = this.advancedContainer = document.createElement("div"); - this.element.appendChild(baseContainer); - options.target = baseContainer; + // const baseContainer = this.baseContainer = document.createElement("div"); + // this.element.appendChild(baseContainer); + options.target = this.element; options.searchButton = true; options.search = true; this.baseSearchEngine = new SearchEngineGeocodeIGN(options); @@ -240,7 +242,7 @@ class SearchEngineAdvanced extends Control { advancedContainer.className = "GPAdvancedContainer"; advancedContainer.id = Helper.getUid("GPsearchEngine-AdvancedContainer-"); advancedContainer.setAttribute("aria-labelledby", advancedBtn.id); - this.element.appendChild(advancedContainer); + // baseContainer.appendChild(advancedContainer); // Geolocation advancedContainer.appendChild(this._getGeolocButton()); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index b41f7290b..c19dbc055 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -328,7 +328,7 @@ class SearchEngineBase extends Control { // Submit button if (options.searchButton) { const submit = this.subimtBt = document.createElement("button"); - submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; + submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn fr-btn--lg"; submit.id = Helper.getUid("GPshowSearchEnginePicto-"); submit.type = "submit"; if (options.title) { @@ -511,25 +511,28 @@ class SearchEngineBase extends Control { */ _updateHistoric (value) { if (this._historic) { - let isPresent = false; + let index = -1; for (let i = 0; i < this._historic.length; i++) { const elem = this._historic[i]; if (this._isEqual(elem, value)) { // L'élément est déjà dans l'historique - isPresent = true; + index = i; break; } } - if (!isPresent) { - const length = this._historic.unshift(value); - // Retire le dernier élément si le nombre maximal est atteint - if (length > (this.get("maximumEntries"))) { - this._historic.pop(); - } + if (index !== -1) { + // Enlève de l'historique pour le remettre en première position; + this._historic.splice(index, 1); + } - // Save in localStorage - localStorage.setItem(this._historicName, JSON.stringify(this._historic)); + const length = this._historic.unshift(value); + // Retire le dernier élément si le nombre maximal est atteint + if (length > (this.get("maximumEntries"))) { + this._historic.pop(); } + + // Enregistre dans le localStorage + localStorage.setItem(this._historicName, JSON.stringify(this._historic)); } } From 7932023721e5aa5f47b01fee5813ae6fda1c148e Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 14:43:36 +0200 Subject: [PATCH 24/73] feat(search): Ajout search dans index.js + fix css --- src/index.js | 7 +++++++ .../SearchEngine/DSFRadvancedSearchEngineStyle.css | 7 ------- src/packages/Controls/SearchEngine/Service.js | 4 +++- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/index.js b/src/index.js index 71a798674..dc71a2843 100644 --- a/src/index.js +++ b/src/index.js @@ -25,6 +25,13 @@ export { default as LayerSwitcher } from "./packages/Controls/LayerSwitcher/Laye export { default as GetFeatureInfo } from "./packages/Controls/GetFeatureInfo/GetFeatureInfo"; export { default as SearchEngine } from "./packages/Controls/SearchEngine/SearchEngine"; export { default as SearchEngineBase } from "./packages/Controls/SearchEngine/SearchEngineBase"; +export { default as SearchEngineAdvanced } from "./packages/Controls/SearchEngine/SearchEngineAdvanced"; +export { default as SearchEngineGeocodeIGN } from "./packages/Controls/SearchEngine/SearchEngineGeocodeIGN"; +export { default as AbstractAdvancedSearch } from "./packages/Controls/SearchEngine/AbstractAdvancedSearch"; +export { default as CoordinateAdvancedSearch } from "./packages/Controls/SearchEngine/CoordinateAdvancedSearch"; +export { default as InseeAdvancedSearch } from "./packages/Controls/SearchEngine/InseeAdvancedSearch"; +export { default as LocationAdvancedSearch } from "./packages/Controls/SearchEngine/LocationAdvancedSearch"; +export { default as IGNSearchService } from "./packages/Controls/SearchEngine/Service"; export { default as MousePosition } from "./packages/Controls/MousePosition/MousePosition"; export { default as Drawing } from "./packages/Controls/Drawing/Drawing"; export { default as Route } from "./packages/Controls/Route/Route"; diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index a115013ad..cd572996e 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -19,13 +19,6 @@ div[id^=GPsearchEngine-AdvancedContainer] > button.GPSearchEngine-locate { margin-bottom : 0.75rem; } -[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"] .GPOptionsContainer button { - font-size: 0.875rem; - line-height: 1.5rem; - min-height: 2rem; - padding: 0.25rem 0.75rem; -} - form[id^=GPAdvancedForm-CoordinateAdvancedSearch] > .GPLonLatInputs { display: flex; flex-direction: row; diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index ddd62dcb0..07af82bc9 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -897,7 +897,9 @@ class IGNSearchService extends AbstractSearchService { } -export { AbstractSearchService, DefaultSearchService, InseeSearchService, IGNSearchService }; +export { AbstractSearchService, DefaultSearchService, InseeSearchService }; + +export default IGNSearchService; // Expose SearchEngine as ol.control.SearchEngine (for a build bundle) if (window.ol) { From 00245e8abab5649902ddf9ad40ccbcb9268a41f2 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 14:52:03 +0200 Subject: [PATCH 25/73] fix(search): Fix import service --- src/packages/Controls/SearchEngine/LocationAdvancedSearch.js | 2 +- src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 91ba01a24..408da98d4 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -1,6 +1,6 @@ import Helper from "../../Utils/Helper"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; -import { IGNSearchService } from "./Service"; +import IGNSearchService from "./Service"; class LocationAdvancedSearch extends AbstractAdvancedSearch { diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index 871156693..a407e948e 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -2,7 +2,8 @@ import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import Logger from "../../Utils/LoggerByDefault"; import SearchEngineBase from "./SearchEngineBase"; -import { AbstractSearchService, IGNSearchService } from "./Service"; +import { AbstractSearchService } from "./Service"; +import IGNSearchService from "./Service"; var logger = Logger.getLogger("searchengine"); From 7a97b2b5b6f4dca50037a9c3da1385154efdbe1f Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 16:22:03 +0200 Subject: [PATCH 26/73] =?UTF-8?q?fix(search):=20fix=20param=C3=A8tre=20Ret?= =?UTF-8?q?urnTrueGeometry?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ges-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html | 3 ++- src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js | 5 +++-- src/packages/Controls/SearchEngine/Service.js | 1 + 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 7990e3651..3bdcfc31b 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -74,7 +74,8 @@

Ajout du moteur de recherche avec les options par défaut

// 2. Appel du SearchEngine var search = new ol.control.SearchEngineAdvanced({ - advancedSearch : [insee, location, coordinates] + advancedSearch : [insee, location, coordinates], + returnTrueGeometry : true, }); // 3. Ajout du SearchEngine à la carte diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index a407e948e..f055b18cb 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -55,12 +55,13 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { this.CLASSNAME = "SearchEngineGeocodeIGN"; this.REMOVE_FEATURE_EVENT = "remove:feature"; + options.serviceOptions = options.serviceOptions ? options.serviceOptions : {}; if (options.autocomplete === false) { this.set("autocomplete", false); - options.serviceOptions = options.serviceOptions ? options.serviceOptions : {}; options.serviceOptions.autocomplete = false; } - options.returnTrueGeometry = true; + + options.serviceOptions.returnTrueGeometry = !!options.returnTrueGeometry; // Créé le serbice de géocodage IGN if (!options.searchService || !(options.searchService instanceof AbstractSearchService)) { diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 07af82bc9..976200130 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -264,6 +264,7 @@ class InseeSearchService extends AbstractSearchService { this.ignService = new IGNSearchService({ autocomplete : false, + returnTrueGeometry : true, }); this.ignService.on(this.SEARCH_EVENT, this._onSearch.bind(this)); From e10677837ccf3a5fd007f2e62e46a20bbc2de33f Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 16:48:09 +0200 Subject: [PATCH 27/73] fix(search): Fix icone point sur la carte --- src/packages/Controls/SearchEngine/SearchEngineAdvanced.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 8962b5f41..666f63eb0 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -12,6 +12,7 @@ import VectorSource from "ol/source/Vector"; import Overlay from "ol/Overlay.js"; import { Style, Icon, Stroke, Fill } from "ol/style"; +import mapPinIcon from "./map-pin-2-fill.svg"; import Feature from "ol/Feature"; const color = "rgba(0, 0, 145, 1)"; @@ -23,7 +24,7 @@ const createStyle = (feature) => { case "MultiPoint": return new Style({ image : new Icon({ - src : "/src/packages/Controls/SearchEngine/map-pin-2-fill.svg", + src : mapPinIcon, color : [0, 0, 145, 1], }), }); From 2b4321ea5156be5e94c5ef2052e9eb4a2de5ed5e Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 21 Oct 2025 16:55:05 +0200 Subject: [PATCH 28/73] =?UTF-8?q?fix(search):=20Ajout=20z-index=20recherch?= =?UTF-8?q?e=20avanc=C3=A9e?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index 11c4a06eb..ecba395ce 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -283,6 +283,7 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ } [id^="GPsearchEngine-Advanced"]:has(.GPSearchEngine-advanced-btn[aria-expanded="true"]) > .GPAdvancedContainer { display: flex; + z-index: 1; } [id^="GPsearchEngine-Advanced"] .GPSearchEngine-advanced-btn[aria-expanded="false"]::after { transform: rotate(180deg); From 09eaa7a119ef6bd497d7f7349e34b02c93ce2535 Mon Sep 17 00:00:00 2001 From: viglino Date: Tue, 21 Oct 2025 17:33:34 +0200 Subject: [PATCH 29/73] UPD add number of search / getResultFeatures --- .../SearchEngine/GPFadvancedSearchEngine.css | 4 ++ .../SearchEngine/LocationAdvancedSearch.js | 32 ++++++++++++-- src/packages/Controls/SearchEngine/Service.js | 42 ++++++++++++++----- 3 files changed, 64 insertions(+), 14 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 478c3b958..7a0e3e928 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -34,4 +34,8 @@ form[id^=GPAdvancedForm-] > .GPFormFooter > button { display: flex; justify-content: center; align-items: center; +} + +form[id^=GPAdvancedForm-] input:invalid { + border-color: #d93025; } \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 91ba01a24..cb098dd44 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -21,12 +21,20 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // Search service this.searchService = new IGNSearchService({ index : "poi", - limit : 1, + limit : 5, returnTrueGeometry : true }); // Do something on search this.searchService.on("search", function (e) { - this.dispatchEvent(e); + // Format output + if (e.nbResults === 0) { + console.log("No result"); + } else if (e.nbResults === 1) { + console.log(e); + this.dispatchEvent(e); + } else { + this.handleMultipleResults(e); + } }.bind(this)); } @@ -44,6 +52,14 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { this.search.setMap(map); } */ + /** Handle multiresults: display list for selct/search + * @param {Object} e Event + */ + handleMultipleResults (e) { + // Par defaut on selectionne le premier resultat + this.dispatchEvent(e); + } + _getLabelContainer (text, type, input) { const container = document.createElement("div"); container.className = type; @@ -63,6 +79,14 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { */ addInputs () { super.addInputs(); + + // Legend + const legend = document.createElement("legend"); + legend.className = "fr-fieldset__legend"; + legend.id = Helper.getUid("LocationAdvancedSearch-legend-"); + legend.innerText = "* champs obligatoires"; + this.inputs.push(legend); + // Type const typeSelect = document.createElement("select"); typeSelect.className = "fr-select"; @@ -93,8 +117,10 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { searchInput.className = "fr-input"; searchInput.type = "text"; searchInput.name = "search"; + searchInput.setAttribute("minlength", "3"); + searchInput.required = true; searchInput.id = Helper.getUid("LocationAdvancedSearch-search-"); - this._getLabelContainer("Renseigner un lieu", "fr-input-group", searchInput); + this._getLabelContainer("Renseigner un lieu *", "fr-input-group", searchInput); // Code postal const postalInput = document.createElement("input"); diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index ddd62dcb0..48a5ff932 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -770,10 +770,11 @@ class IGNSearchService extends AbstractSearchService { Utils.assign(options, this.options.geocodeOptions.serviceOptions); // ainsi que la recherche et les callbacks Utils.assign(options, settings); + options.maximumResponses = settings.limit; // on redefinie les callbacks si les callbacks de service existent var bOnSuccess = !!(this.options.geocodeOptions.serviceOptions.onSuccess !== null && typeof this.options.geocodeOptions.serviceOptions.onSuccess === "function"); if (bOnSuccess) { - console.log("bonsuccess"); + console.log("bonSuccess"); var cbOnSuccess = function (e) { settings.onSuccess.bind(this, e); this.options.geocodeOptions.serviceOptions.onSuccess.bind(this, e); @@ -783,7 +784,7 @@ class IGNSearchService extends AbstractSearchService { var bOnFailure = !!(this.options.geocodeOptions.serviceOptions.onFailure !== null && typeof this.options.geocodeOptions.serviceOptions.onFailure === "function"); if (bOnFailure) { - console.log("bonFailrure"); + console.log("bonFailure"); var cbOnFailure = function (e) { settings.onFailure.bind(this, e); this.options.geocodeOptions.serviceOptions.onFailure.bind(this, e); @@ -810,14 +811,22 @@ class IGNSearchService extends AbstractSearchService { Gp.Services.geocode(options); } - _onSuccessSearch (results) { + /** Get features based on current search result + * @param {Number} index Index of the result + * @returns {Object} Object containing feature and extent (if any) + */ + getResultFeatures (index) { + let location = this.getResult(index); + if (!location) { + return { featureFilter : null, extent : null }; + } let position = [ - results.locations[0].position.lon, - results.locations[0].position.lat + location.position.lon, + location.position.lat ]; let f, extent; - if (results.locations[0].placeAttributes.truegeometry) { - let geom = JSON.parse(results.locations[0].placeAttributes.truegeometry); + if (location.placeAttributes.truegeometry) { + let geom = JSON.parse(location.placeAttributes.truegeometry); let format = new GeoJSON(); let geometry = format.readGeometry(geom, { @@ -844,6 +853,16 @@ class IGNSearchService extends AbstractSearchService { extent.set("infoPopup", this._currentGeocodingLocation); } f.set("infoPopup", this._currentGeocodingLocation); + return { feature : f, extent : extent }; + } + + /** Do something on search + * @private + */ + _onSuccessSearch (results) { + this._locations = results.locations; + + const features = this.getResultFeatures(0); /** * event triggered when an element of the results is clicked for autocompletion @@ -859,19 +878,20 @@ class IGNSearchService extends AbstractSearchService { */ this.dispatchEvent({ type : this.SEARCH_EVENT, - result : f, - extent : extent, + result : features.feature, + extent : features.extent, + nbResults : results.locations.length, }); } _onFailureSearch (location, error) { + logger.warn(error); + let position = [ location.position.x, location.position.y ]; - logger.warn(error); - /** * event triggered when an element of the results is clicked for autocompletion * From 949894d28b20f1e02c7ebbc15b52ba1dec4c947a Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 22 Oct 2025 11:14:28 +0200 Subject: [PATCH 30/73] fix(search): fix width DSFR SM --- .../CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index cd572996e..f086038a3 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -9,7 +9,7 @@ div[id^=GPsearchEngine-Advanced] { width: 100%; } -@media (min-width: 569px) { +@media (min-width: 36em) { div[id^=GPsearchEngine-Advanced] { width: 360px; } From 3fc2e66816c100ab90cc8069493798be5509a101 Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 22 Oct 2025 12:58:46 +0200 Subject: [PATCH 31/73] ADD multisearch list + format tooltips --- .../SearchEngine/GPFadvancedSearchEngine.css | 27 ++++- .../SearchEngine/LocationAdvancedSearch.js | 99 +++++++++++++++++-- 2 files changed, 116 insertions(+), 10 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index a0eb394c6..0780e6523 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -37,4 +37,29 @@ form[id^=GPAdvancedForm-] > .GPFormFooter > button { form[id^=GPAdvancedForm-] input:invalid { border-color: #d93025; -} \ No newline at end of file +} + +form[id^=GPAdvancedForm-] ul.search-result { + list-style-type: none; + padding: 0; + margin: 0; +} +form[id^=GPAdvancedForm-] ul.search-result .hidden { + display: none; +} +form[id^=GPAdvancedForm-] ul.search-result button { + flex: 1 1 0; + display: flex; + justify-content: center; + align-items: center; +} +form[id^=GPAdvancedForm-] ul.search-result .search-result-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + flex-direction: column; + text-align: center; +} +form[id^=GPAdvancedForm-] ul.search-result .search-result-actions:hover { + background-color: transparent; +} diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 0dda793fc..9e888083a 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -21,19 +21,49 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // Search service this.searchService = new IGNSearchService({ index : "poi", - limit : 5, + limit : 10, returnTrueGeometry : true }); // Do something on search this.searchService.on("search", function (e) { + if (!e.multi) { + this.searchResult.innerHTML = ""; + } // Format output - if (e.nbResults === 0) { - console.log("No result"); - } else if (e.nbResults === 1) { - console.log(e); + if (e.nbResults === 1) { + const attr = e.attr || this.searchService.getResult(0).placeAttributes; + ["postcode","citycode","city","category"].forEach(field => { + attr[field] = attr[field] || []; + }); + const into = { + infoPopup : "" + attr.toponym + "
" + + (attr.category ? ("" + (attr.category || []).join(", ") + "
") : "") + + (attr.postcode ? ("Code postal : " + (attr.postcode || []).join(", ") + "
") : "") , + toponyme : attr.toponym, + postcode : attr.postcode[0], + postcodes : attr.postcode.join(" - "), + insee : attr.citycode[0], + citycodes : attr.citycode.join(" - "), + city : attr.city[0], + citys : attr.city.join(" - "), + category : attr.category[0], + categories : attr.category.join(" - ") + }; + + if (e.result) { + e.result.setProperties(into); + } + if (e.extent) { + e.extent.setProperties(into); + } this.dispatchEvent(e); } else { - this.handleMultipleResults(e); + this.element.parentElement.parentElement.scrollTop = 0; + if (e.nbResults === 0) { + this.searchResult.innerHTML = "
  • Aucun résultat
  • " ; + } else { + this.handleMultipleResults(e); + } } }.bind(this)); } @@ -56,8 +86,54 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { * @param {Object} e Event */ handleMultipleResults (e) { + const results = this.searchService.getResult(); + results.forEach((result, i) => { + const attr = result.placeAttributes; + const li = document.createElement("li"); + li.className = "search-result-item" + (i>=5 ? " hidden" : ""); + li.title = li.innerText = attr.toponym + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); + this.searchResult.appendChild(li); + li.addEventListener("click", () => { + const features = this.searchService.getResultFeatures(i); + const event = { + type : "search", + multi : true, + attr : attr, + extent : features.extent, + result : features.feature, + nbResults : 1 + }; + this.searchService.dispatchEvent(event); + }); + }); + // Actions + const li = document.createElement("li"); + this.searchResult.appendChild(li); + // more options + if (results.length > 5) { + const plusBtn = document.createElement("button"); + plusBtn.className = "fr-btn fr-btn--sm fr-btn--tertiary"; + plusBtn.innerText = "Afficher plus de résultats"; + plusBtn.addEventListener("click", () => { + plusBtn.remove(); + this.searchResult.querySelectorAll(".hidden").forEach(el => { + el.classList.remove("hidden"); + }); + }); + li.appendChild(plusBtn); + } + // clear button + li.className = "search-result-actions"; + const okBtn = document.createElement("button"); + okBtn.className = "fr-btn fr-btn--sm fr-btn--tertiary"; + okBtn.innerText = "OK"; + okBtn.addEventListener("click", () => { + this.searchResult.innerHTML = ""; + }); + li.appendChild(okBtn); + // Par defaut on selectionne le premier resultat - this.dispatchEvent(e); + // this.dispatchEvent(e); } _getLabelContainer (text, type, input) { @@ -80,6 +156,10 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { addInputs () { super.addInputs(); + this.searchResult = document.createElement("ul"); + this.searchResult.className = "search-result"; + this.inputs.push(this.searchResult); + // Legend const legend = document.createElement("legend"); legend.className = "fr-fieldset__legend"; @@ -140,8 +220,8 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { inseeInput.className = "fr-input"; inseeInput.name = "cityCode"; inseeInput.type = "text"; - postalInput.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; - postalInput.title = "Code INSEE sur 5 caractères"; + inseeInput.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; + inseeInput.title = "Code INSEE sur 5 caractères"; inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput); inseeInput.addEventListener("change", () => { @@ -162,6 +242,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { this.element.querySelectorAll("input").forEach(input => { input.value = ""; }); + this.searchResult.innerHTML = ""; this.filter = { category : "", postcode : "", From e8adf0b4408149b4792840e7aecaf54d254a60cb Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 22 Oct 2025 13:20:08 +0200 Subject: [PATCH 32/73] ADD dispatch expand event on advanced search elemnts --- .../SearchEngine/LocationAdvancedSearch.js | 88 +++++++++++-------- .../SearchEngine/SearchEngineAdvanced.js | 1 + 2 files changed, 52 insertions(+), 37 deletions(-) diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 9e888083a..bafe1b185 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -24,48 +24,62 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { limit : 10, returnTrueGeometry : true }); - // Do something on search - this.searchService.on("search", function (e) { - if (!e.multi) { - this.searchResult.innerHTML = ""; + } + + _initEvents (options) { + super._initEvents(options); + + this.on("expand", e => { + if (e.expanded) { + setTimeout(() => this.searchInput.focus(), 300); } - // Format output - if (e.nbResults === 1) { - const attr = e.attr || this.searchService.getResult(0).placeAttributes; - ["postcode","citycode","city","category"].forEach(field => { - attr[field] = attr[field] || []; - }); - const into = { - infoPopup : "" + attr.toponym + "
    " + - (attr.category ? ("" + (attr.category || []).join(", ") + "
    ") : "") + - (attr.postcode ? ("Code postal : " + (attr.postcode || []).join(", ") + "
    ") : "") , - toponyme : attr.toponym, - postcode : attr.postcode[0], - postcodes : attr.postcode.join(" - "), - insee : attr.citycode[0], - citycodes : attr.citycode.join(" - "), - city : attr.city[0], - citys : attr.city.join(" - "), - category : attr.category[0], - categories : attr.category.join(" - ") - }; + this.searchResult.innerHTML = ""; + }); - if (e.result) { - e.result.setProperties(into); - } - if (e.extent) { - e.extent.setProperties(into); + // Do something on search (when ready) + setTimeout(() => { + this.searchService.on("search", e => { + if (!e.multi) { + this.searchResult.innerHTML = ""; } - this.dispatchEvent(e); - } else { - this.element.parentElement.parentElement.scrollTop = 0; - if (e.nbResults === 0) { - this.searchResult.innerHTML = "
  • Aucun résultat
  • " ; + // Format output + if (e.nbResults === 1) { + const attr = e.attr || this.searchService.getResult(0).placeAttributes; + ["postcode","citycode","city","category"].forEach(field => { + attr[field] = attr[field] || []; + }); + const into = { + infoPopup : "" + attr.toponym + "
    " + + (attr.category ? ("" + (attr.category || []).join(", ") + "
    ") : "") + + (attr.postcode ? ("Code postal : " + (attr.postcode || []).join(", ") + "
    ") : "") , + toponyme : attr.toponym, + postcode : attr.postcode[0], + postcodes : attr.postcode.join(" - "), + insee : attr.citycode[0], + citycodes : attr.citycode.join(" - "), + city : attr.city[0], + citys : attr.city.join(" - "), + category : attr.category[0], + categories : attr.category.join(" - ") + }; + + if (e.result) { + e.result.setProperties(into); + } + if (e.extent) { + e.extent.setProperties(into); + } + this.dispatchEvent(e); } else { - this.handleMultipleResults(e); + this.element.parentElement.parentElement.scrollTop = 0; + if (e.nbResults === 0) { + this.searchResult.innerHTML = "
  • Aucun résultat
  • " ; + } else { + this.handleMultipleResults(e); + } } - } - }.bind(this)); + }); + }); } initialize (options) { diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 666f63eb0..9a0b12d61 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -289,6 +289,7 @@ class SearchEngineAdvanced extends Control { accordion.classList.add("fr-collapse--expanded"); section.classList.remove("fr-hidden"); } + Search.dispatchEvent({ type : "expand", expanded : !expanded }); }); }); From 88d2293cec25b20572bc0c777a4f7128202fa178 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 22 Oct 2025 14:36:19 +0200 Subject: [PATCH 33/73] fix(search): Ajout layer en layer.setMap (et pas map.addLayer) Permet aux couches de ne pas s'ajouter dans le gestionnaire de couche --- src/packages/Controls/SearchEngine/SearchEngineAdvanced.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 9a0b12d61..be8b879aa 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -156,9 +156,10 @@ class SearchEngineAdvanced extends Control { this.element.appendChild(this.advancedContainer); - if (map) { - map.addLayer(this.extent); - map.addLayer(this.layer); + if (map) { + // Place les couches au dessus des autres + this.extent.setMap(map); + this.layer.setMap(map); map.addInteraction(this.selectInteraction); map.addOverlay(this.popup); } From 4ccd1eec0b6d91cb552dc04bea55978f0576c5b4 Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 22 Oct 2025 14:45:39 +0200 Subject: [PATCH 34/73] FIX accessibility --- .../SearchEngine/GPFadvancedSearchEngine.css | 68 +++++++++++-------- .../SearchEngine/LocationAdvancedSearch.js | 12 +++- 2 files changed, 50 insertions(+), 30 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 0780e6523..26ebe8938 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -1,51 +1,63 @@ /** STYLE COMMUN RECHERCHE AVANCEE **/ div[id^=GPsearchEngine-Advanced] { - padding: 5px; - margin: -5px; + padding: 5px; + margin: -5px; } div[id^=GPsearchEngine-Advanced] div[id^=GPsearchEngine-] { - width: 100%; - margin: 0 5px; + width: 100%; + margin: 0 5px; } form[id^=GPAdvancedForm-], form[id^=GPAdvancedForm-] * { - height : 100%; - width: 100%; + height : 100%; + width: 100%; } form[id^=GPAdvancedForm-] > .GPFormFooter { - margin-top: 0.75rem; - display: flex; - justify-content: space-between; - flex-wrap: wrap; - gap: 16px; + margin-top: 0.75rem; + display: flex; + justify-content: space-between; + flex-wrap: wrap; + gap: 16px; } [id^="GPsearchEngine-Advanced"] form[id^="GPsearchInput-Base-"]:first-child { - display: flex; - flex-direction: row; + display: flex; + flex-direction: row; } form[id^=GPAdvancedForm-] > .GPFormFooter > button { - flex: 1 1 0; - display: flex; - justify-content: center; - align-items: center; + flex: 1 1 0; + display: flex; + justify-content: center; + align-items: center; } form[id^=GPAdvancedForm-] input:invalid { - border-color: #d93025; + border-color: #d93025; } form[id^=GPAdvancedForm-] ul.search-result { - list-style-type: none; - padding: 0; - margin: 0; + list-style-type: none; + padding: 0; + margin: -0.5em 0 0; +} +form[id^=GPAdvancedForm-] ul.search-result a { + text-decoration: none; + overflow: hidden; + text-overflow: ellipsis; + margin: 0; + padding: 0; + background: none; + display: block; +} +form[id^=GPAdvancedForm-] ul.search-result a:before { + margin-right: 0.5em; } form[id^=GPAdvancedForm-] ul.search-result .hidden { - display: none; + display: none; } form[id^=GPAdvancedForm-] ul.search-result button { flex: 1 1 0; @@ -54,12 +66,12 @@ form[id^=GPAdvancedForm-] ul.search-result button { align-items: center; } form[id^=GPAdvancedForm-] ul.search-result .search-result-actions { - display: flex; - justify-content: flex-end; - gap: 8px; - flex-direction: column; - text-align: center; + display: flex; + justify-content: flex-end; + gap: 8px; + flex-direction: column; + text-align: center; } form[id^=GPAdvancedForm-] ul.search-result .search-result-actions:hover { - background-color: transparent; + background-color: transparent; } diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index bafe1b185..d8b817d83 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -105,9 +105,16 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { const attr = result.placeAttributes; const li = document.createElement("li"); li.className = "search-result-item" + (i>=5 ? " hidden" : ""); - li.title = li.innerText = attr.toponym + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); this.searchResult.appendChild(li); - li.addEventListener("click", () => { + // link for accessibility + const a = document.createElement("a"); + li.appendChild(a); + a.className = "fr-icon-map-pin-2-line"; + a.href = "#"; + a.title = a.innerText = attr.toponym + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); + li.addEventListener("click", () => a.click()); + a.addEventListener("click", e => { + e.preventDefault(); const features = this.searchService.getResultFeatures(i); const event = { type : "search", @@ -143,6 +150,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { okBtn.innerText = "OK"; okBtn.addEventListener("click", () => { this.searchResult.innerHTML = ""; + this.element.parentElement.parentElement.scrollTop = 0; }); li.appendChild(okBtn); From e399ade7c8c7641affa84056926d6bff4d169e04 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 22 Oct 2025 15:38:25 +0200 Subject: [PATCH 35/73] fix(search): Fix suppr feature dans le popup --- ...chenginebase-modules-dsfr-geocodeAdvanced.html | 8 +------- .../Controls/SearchEngine/SearchEngineAdvanced.js | 15 +++++++++++++-- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 3bdcfc31b..b1cca91f5 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -41,11 +41,6 @@

    Ajout du moteur de recherche avec les options par défaut

    // on cache l'image de chargement du Géoportail. document.getElementById('map').style.backgroundImage = 'none'; - let source = new ol.source.Vector({}) - let layer = new ol.layer.Vector({ - source : source, - }) - // 1. Création de la map map = new ol.Map({ target : "map", @@ -58,8 +53,7 @@

    Ajout du moteur de recherche avec les options par défaut

    source: new ol.source.OSM(), // zIndex : 4, opacity: 0.5 - }), - layer + }) ] }); diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index be8b879aa..6709dc924 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -14,6 +14,7 @@ import Overlay from "ol/Overlay.js"; import { Style, Icon, Stroke, Fill } from "ol/style"; import mapPinIcon from "./map-pin-2-fill.svg"; import Feature from "ol/Feature"; +import { Layer } from "ol/layer"; const color = "rgba(0, 0, 145, 1)"; const createStyle = (feature) => { @@ -103,7 +104,6 @@ class SearchEngineAdvanced extends Control { }); this.selectInteraction.on("select", this._onSelectElement.bind(this)); - this.popup = this._createPopup(); @@ -126,6 +126,11 @@ class SearchEngineAdvanced extends Control { */ this.CLASSNAME = "SearchEngineAdvanced"; + /** + * @type {Object} + */ + this._layerFeatureAssociation = {}; + if (options.advancedSearch && options.advancedSearch instanceof Array) { this._searchForms = options.advancedSearch; } else { @@ -312,10 +317,14 @@ class SearchEngineAdvanced extends Control { let extent; if (!!e.result) { this.layer.getSource().addFeature(e.result); + // Ajout de la couche pour la retrouver plus tard + this._layerFeatureAssociation[e.result.ol_uid] = this.layer; extent = e.result.getGeometry().getExtent(); } if (!!e.extent) { this.extent.getSource().addFeature(e.extent); + // Ajout de la couche pour la retrouver plus tard + this._layerFeatureAssociation[e.extent.ol_uid] = this.extent; extent = e.extent.getGeometry().getExtent(); } if (this.getMap()) { @@ -348,7 +357,9 @@ class SearchEngineAdvanced extends Control { this.popup.setPosition(position); this.setPopupContent(feature.get("infoPopup") || ""); this.popup.set("feature", feature); - this.popup.set("layer", e.target.getLayer(feature)); + // Récupère la couche liée; + const layer = this._layerFeatureAssociation[feature.ol_uid]; + this.popup.set("layer", layer); } else { this.popup.setPosition(undefined); this.setPopupContent(""); From 63b7ed51912fe7fcf9bedda31831c97dfbb4f1e2 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 23 Oct 2025 10:46:17 +0200 Subject: [PATCH 36/73] =?UTF-8?q?fix(search):=20G=C3=A8re=20focus=20/=20ta?= =?UTF-8?q?b=20pour=20btn=20g=C3=A9oloc=20(autocomplete)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchEngine/SearchEngineAdvanced.js | 25 +++++++++++++++++-- .../Controls/SearchEngine/SearchEngineBase.js | 22 +++++++++++++++- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 6709dc924..44b426b3c 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -180,6 +180,26 @@ class SearchEngineAdvanced extends Control { this.geolocation.setTracking(false); }); + this.baseSearchEngine.input.addEventListener("keydown", function (/** @type {KeyboardEvent} */ e) { + if (e.key === "Tab" && !e.shiftKey && this.locationBtn.checkVisibility()) { + e.preventDefault(); + this.locationBtn.focus(); + } + }.bind(this)); + + this.locationBtn.addEventListener("keydown", function (/** @type {KeyboardEvent} */ e) { + if (e.key === "Tab") { + e.preventDefault(); + if (e.shiftKey) { + // Retourne sur l'input + this.baseSearchEngine.input.focus(); + } else { + // Focus sur le bouton de recherche avancée + this.advancedBtn.focus(); + } + } + }.bind(this)); + this.on("search", this.addResultToMap.bind(this)); } @@ -231,10 +251,11 @@ class SearchEngineAdvanced extends Control { }); // Geolocation - this.baseSearchEngine.autocompleteHeader.appendChild(this._getGeolocButton()); + this.locationBtn = this._getGeolocButton(); + this.baseSearchEngine.autocompleteHeader.appendChild(this.locationBtn); // Ajout des options avancées - const advancedBtn = document.createElement("button"); + const advancedBtn = this.advancedBtn = document.createElement("button"); advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); advancedBtn.type = "button"; diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index c19dbc055..dfaff7a5a 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -375,10 +375,30 @@ class SearchEngineBase extends Control { acContainer.classList.add("GPelementVisible"); acContainer.classList.remove("GPelementHidden"); }); + + // Gère le focus pour la sélection d'éléments dans la liste input.addEventListener("blur", (e) => { // N'agit que si le focus est hors de l'élément if (e.relatedTarget && acContainer.contains(e.relatedTarget)) { - input.focus(); + // N'empêche pas le focus sur un bouton + if (!(e.relatedTarget.tagName === "BUTTON")) { + input.focus(); + } else { + // Ajout d'un event listener pour retourner sur l'input en cas de besoin + e.relatedTarget.addEventListener("blur", (e) => { + if (e.relatedTarget && acContainer.contains(e.relatedTarget) || e.relatedTarget === input) { + input.focus(); + } else { + setTimeout(() => { + input.setAttribute("aria-expanded", "false"); + acContainer.classList.remove("gpf-visible"); + acContainer.classList.add("gpf-hidden"); + acContainer.classList.remove("GPelementVisible"); + acContainer.classList.add("GPelementHidden"); + }, 100); + } + }, { once : true }); + } } else { setTimeout(() => { input.setAttribute("aria-expanded", "false"); From 7b833ab0ff3e64476067e032c77a36f14a84ea05 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 23 Oct 2025 14:39:08 +0200 Subject: [PATCH 37/73] fix(search): Modif rech. av. lieux + code insee --- .../SearchEngine/InseeAdvancedSearch.js | 10 +++++---- .../SearchEngine/LocationAdvancedSearch.js | 21 +++++++++++++------ .../Controls/SearchEngine/SearchEngineBase.js | 2 +- src/packages/Controls/SearchEngine/Service.js | 14 ++++++++----- 4 files changed, 31 insertions(+), 16 deletions(-) diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js index 80f302cc7..c2782b322 100644 --- a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js @@ -64,6 +64,9 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { }) }); + this.inseeInput.input.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; + this.inseeInput.input.title = "Code INSEE sur 5 caractères"; + this.inputs.push(inseeInput); } @@ -84,11 +87,10 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { */ _onSearch (e) { super._onSearch(e); + const pattern = this.inseeInput.input.pattern; const insee = this.inseeInput.input.value; - const count = insee.length; - const number = parseInt(insee, 10); - if (!isNaN(number) && 0 <= number <= 99999 && count === 5) { + if (RegExp(pattern).test(insee)) { this.inseeInput.removeMessages(); this.inseeInput.search({ location : insee, @@ -97,7 +99,7 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { } }); } else { - this.inseeInput.addMessage("Le champs INSEE doit être un texte de 5 chiffres exactement"); + this.inseeInput.addMessage("Le champs INSEE n'est pas valide (texte de 5 chiffres)."); } } diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index d8b817d83..50c871ad8 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -158,13 +158,17 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // this.dispatchEvent(e); } - _getLabelContainer (text, type, input) { + _getLabelContainer (text, type, input, hint = "") { const container = document.createElement("div"); container.className = type; this.inputs.push(container); const label = document.createElement("label"); label.className = "fr-label"; - label.innerText = text; + let labelText = text; + if (hint) { + labelText = `${text} ${hint}`; + } + label.innerHTML = labelText; container.appendChild(label); if (input) { label.setAttribute("for", input.id); @@ -184,11 +188,16 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // Legend const legend = document.createElement("legend"); - legend.className = "fr-fieldset__legend"; + legend.className = "fr-fieldset__legend fr-fieldset__legend--regular"; legend.id = Helper.getUid("LocationAdvancedSearch-legend-"); - legend.innerText = "* champs obligatoires"; + const hint = document.createElement("span"); + hint.className = "fr-hint-text"; + hint.textContent = "* Champs obligatoires"; + legend.appendChild(hint); this.inputs.push(legend); + // ^pkp^*k + // Type const typeSelect = document.createElement("select"); typeSelect.className = "fr-select"; @@ -229,7 +238,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { postalInput.className = "fr-input"; postalInput.type = "text"; postalInput.name = "postalCode"; - postalInput.pattern = "(\\d{5}"; + postalInput.pattern = "(\\d{5})"; postalInput.title = "Code postal à 5 chiffres"; postalInput.id = Helper.getUid("LocationAdvancedSearch-postal-"); this._getLabelContainer("Code postal", "fr-input-group", postalInput); @@ -245,7 +254,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { inseeInput.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; inseeInput.title = "Code INSEE sur 5 caractères"; inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); - this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput); + this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput, "Format attendu INSEE : 5 chiffres, selon le code officiel géographique (COG)"); inseeInput.addEventListener("change", () => { this.filter.citycode = inseeInput.value; }); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index dfaff7a5a..78fa1691a 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -508,7 +508,7 @@ class SearchEngineBase extends Control { li.className = `GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; li.setAttribute("role", "option"); li.setAttribute("data-idx", idx); - li.innerHTML = this.getItemTitle(item); + li.innerHTML = li.title = this.getItemTitle(item); this.autocompleteList.appendChild(li); li.addEventListener("click", function (e) { const idx = Number(e.target.getAttribute("data-idx")); diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Controls/SearchEngine/Service.js index 7591fbdbe..7dfe9adde 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Controls/SearchEngine/Service.js @@ -265,6 +265,7 @@ class InseeSearchService extends AbstractSearchService { this.ignService = new IGNSearchService({ autocomplete : false, returnTrueGeometry : true, + index : "poi", }); this.ignService.on(this.SEARCH_EVENT, this._onSearch.bind(this)); @@ -289,11 +290,14 @@ class InseeSearchService extends AbstractSearchService { if (r instanceof Array && r.length) { const result = r[0]; - let location = { - fullText : result.nom, - }; + let location = result.nom; + // Sinon la requête ne se lancera pas + if (result.nom.length < 3) { + location = `${result.nom}, ${result.codesPostaux[0]}`; + } - let filters = { + let filters = { + category : "administratif", citycode : result.code }; @@ -313,7 +317,7 @@ class InseeSearchService extends AbstractSearchService { async _requestGeoAPI (settings) { const baseURL = "https://geo.api.gouv.fr/communes"; const format = "json"; - const fields = ["nom", "code"]; + const fields = ["nom", "code", "codesPostaux"]; const url = `${baseURL}?code=${settings.value}&format=${format}&fields=${fields}`; try { From adf2a9aac2e32ae4a0bd92218f3541ba929d8d4e Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 24 Oct 2025 11:51:41 +0200 Subject: [PATCH 38/73] docs(search): Ajout doc + ajout services dans Services/ --- .../SearchEngine/AbstractAdvancedSearch.js | 49 +- .../SearchEngine/CoordinateAdvancedSearch.js | 155 ++++-- .../SearchEngine/InseeAdvancedSearch.js | 38 +- .../SearchEngine/LocationAdvancedSearch.js | 77 ++- .../SearchEngine/SearchEngineAdvanced.js | 101 +++- .../Controls/SearchEngine/SearchEngineBase.js | 168 +++---- .../SearchEngine/SearchEngineGeocodeIGN.js | 34 +- .../Controls/SearchEngine/typedefs.js | 129 +++++ .../Services/AbstractSearchService.js | 128 +++++ src/packages/Services/DefaultSearchService.js | 78 +++ .../IGNSearchService.js} | 473 +++--------------- src/packages/Services/InseeSearchService.js | 141 ++++++ src/packages/Services/SearchServiceBase.js | 74 --- src/packages/Services/typedefs.js | 118 +++++ 14 files changed, 1067 insertions(+), 696 deletions(-) create mode 100644 src/packages/Controls/SearchEngine/typedefs.js create mode 100644 src/packages/Services/AbstractSearchService.js create mode 100644 src/packages/Services/DefaultSearchService.js rename src/packages/{Controls/SearchEngine/Service.js => Services/IGNSearchService.js} (62%) create mode 100644 src/packages/Services/InseeSearchService.js delete mode 100644 src/packages/Services/SearchServiceBase.js create mode 100644 src/packages/Services/typedefs.js diff --git a/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js index 4c618d9c5..010d37ac1 100644 --- a/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/AbstractAdvancedSearch.js @@ -2,19 +2,19 @@ import "../../CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css"; import Control from "../Control"; import Logger from "../../Utils/LoggerByDefault"; -import { Collection } from "ol"; import Helper from "../../Utils/Helper"; +import Map from "ol/Map"; var logger = Logger.getLogger("abstractAdvancedSearch"); -/** - * @typedef {Object} AbstractAdvancedSearchOptions Options du constructeur pour le contrôle de recherche. - * - * @property {string} name - Nom de la recherche avancée. - */ +// Typedefs partagés disponibles dans ./typedefs.js (AbstractAdvancedSearchOptions, ...) -/** +/** * @classdesc - * AbstractAdvancedSearch Base control + * Contrôle de base pour les recherches avancées (formulaires spécialisés). + * + * Fournit la structure HTML du formulaire, la gestion des inputs et + * les événements d'effacement et de soumission. Les contrôles spécialisés + * doivent surcharger addInputs() et _onSearch(). * * @alias ol.control.AbstractAdvancedSearch * @module AbstractAdvancedSearch @@ -25,7 +25,6 @@ class AbstractAdvancedSearch extends Control { * @constructor * @param {AbstractAdvancedSearchOptions} options Options du constructeur * - * @example */ constructor (options) { options = options || {}; @@ -57,9 +56,8 @@ class AbstractAdvancedSearch extends Control { } /** - * Initialize SearchEngine control (called by SearchEngine constructor) - * - * @param {AbstractAdvancedSearchOptions} options - constructor options + * Initialise le contrôle (appelé par le constructeur). + * @param {AbstractAdvancedSearchOptions} options Options d'initialisation * @protected */ initialize (options) { @@ -72,17 +70,26 @@ class AbstractAdvancedSearch extends Control { this.inputs = []; } + /** + * Retourne le nom du la recherche avancée + * @returns {String} Nom de la recherche avancée + */ getName () { return this.name; } + /** + * Retourne le formulaire de la recherche avancée + * @returns {HTMLFormElement} Formulaire de la recherche + */ getContent () { return this.element; } /** - * - * @param {AbstractAdvancedSearchOptions} options + * Crée le conteneur DOM du formulaire. + * + * @param {AbstractAdvancedSearchOptions} options Options d'initialisation * @returns {HTMLFormElement} Élément du formulaire * @protected */ @@ -131,8 +138,8 @@ class AbstractAdvancedSearch extends Control { } /** - * Ajoute des éléments d'input dans la collection `this.inputs`; - * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * Ajoute des éléments d'input dans la collection `this.inputs`. + * Cette méthode est abstraite et doit être surchargée par les implémentations spécifiques. * @protected * @abstract */ @@ -151,8 +158,8 @@ class AbstractAdvancedSearch extends Control { } /** - * - * @param {PointerEvent} e + * Réinitialise les champs du formulaire. + * @param {PointerEvent} e Événement d'effacement * @protected */ _onErase (e) { @@ -163,9 +170,9 @@ class AbstractAdvancedSearch extends Control { } /** - * - * @param {SubmitEvent} e - * @abstract + * Traitement lors de la soumission du formulaire (recherche). + * Doit être surchargé par les contrôles spécifiques pour lancer la recherche. + * @param {SubmitEvent|PointerEvent} e Évènement de soumission / recherche * @protected */ _onSearch (e) { diff --git a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js index e4a896b27..b3425720e 100644 --- a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js @@ -15,17 +15,20 @@ let logger = Logger.getLogger("abstractAdvancedSearch"); /** * @classdesc - * CoordinateAdvancedSearch Base control + * Contrôle de recherche avancée par coordonnées + * (saisie pour différents systèmes de coordonnées et unités correspondantes). * * @alias ol.control.CoordinateAdvancedSearch * @module CoordinateAdvancedSearch -*/ + */ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { /** - * @constructor - * @example - */ + * Constructeur. + * + * @constructor + * @param {CoordinateAdvancedSearchOptions} [options] Options du contrôle + */ constructor (options) { super(options); @@ -33,6 +36,11 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.element.dataset.unit = this.unit.value; } + /** + * @override + * @protected + * @param {CoordinateAdvancedSearchOptions} options Options d'initialisation + */ initialize (options) { if (!options.name) { options.name = "Coordonnées"; @@ -55,15 +63,9 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.CLASSNAME = "CoordinateAdvancedSearch"; } - - /** - * this method is called by the constructor and initialize the projection - * systems. - * getting coordinates in the requested projection : - * see this.onCoordinateSearchSystemChange() - * * @private + * @param {CoordinateAdvancedSearchOptions} [options] Options d'initialisation possibles (options.coordinateSearch.systems) */ _initCoordinateSearchSystems (options) { this._coordinateSearchSystems = []; @@ -104,11 +106,8 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } /** - * this method is called by the constructor and initialize the units. - * getting coordinates in the requested units : - * see this.onCoordinateSearchUnitsChange() - * * @private + * @param {CoordinateAdvancedSearchOptions} [options] Options d'initialisation possibles (options.coordinateSearch.units) */ _initCoordinateSearchUnits (options) { this._coordinateSearchUnits = []; @@ -170,24 +169,12 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } } - - /** - * Set additional projection system - * - * @param {Object} system - projection system - * @param {String} system.crs - Proj4 crs alias (from proj4 defs) e.g. "EPSG:4326" - * @param {String} [system.label] - CRS label to be displayed in control. Default is system.crs alias - * @param {String} [system.type] - CRS units type for coordinates conversion (one of control options.units). Default is "Metric" - * @private - */ /** - * Définit un système de projection supplémentaire + * Définit un système de projection supplémentaire et charge sa définition CRS si nécessaire. * - * @param {Object} system - système de projection - * @param {String} system.crs - Alias CRS Proj4 (depuis les définitions proj4), ex. "EPSG:4326" - * @param {String} [system.label] - Libellé du CRS affiché dans le contrôle. Par défaut, l’alias system.crs - * @param {String} [system.type] - Type d’unités du CRS pour la conversion des coordonnées. Par défaut : "Metric" * @private + * @param {CoordinateSearchSystem} system Description du système de projection + * @returns {void} */ _setSystem (system) { if (typeof system !== "object") { @@ -228,7 +215,16 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this._coordinateSearchSystems.push(system); } - + /** + * Crée un conteneur d'étiquette pour un élément d'input. + * + * @private + * @param {String} text Texte de l'étiquette + * @param {String} type Classe CSS du conteneur + * @param {HTMLElement} [input] Élément input à rattacher + * @param {Boolean} [mandatory=false] Indique si le champ est obligatoire + * @returns {HTMLElement} Élément conteneur (div) + */ _getLabelContainer (text, type, input, mandatory = false) { const container = document.createElement("div"); container.className = type; @@ -241,6 +237,14 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { return container; } + /** + * Crée un élément label. + * + * @private + * @param {String} text Texte du label + * @param {Boolean} [mandatory=false] Ajoute un astérisque si vrai + * @returns {HTMLLabelElement} Élément label créé + */ _createLabel (text, mandatory = false) { const label = document.createElement("label"); label.className = "fr-label"; @@ -249,7 +253,14 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { return label; } - + /** + * Met à jour le texte d'un label existant. + * + * @private + * @param {String} text Nouveau texte + * @param {HTMLElement} container Conteneur contenant le label à mettre à jour + * @param {Boolean} [mandatory=false] Indique si le champ est obligatoire + */ _updateLabel (text, container, mandatory = false) { const label = container.querySelector("label"); const star = mandatory ? "*" : ""; @@ -257,10 +268,8 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } /** - * Ajoute des éléments d'input dans la collection `this.inputs`; - * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * @override * @protected - * @abstract */ addInputs () { super.addInputs(); @@ -318,6 +327,13 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.inputs.push(values); } + /** + * Crée le wrapper (label + input + masques) pour une composante de coordonnées (lon/lat). + * + * @private + * @param {"lon"|"lat"} type Type de composante ("lon" ou "lat") + * @returns {HTMLElement} Wrapper contenant l'input et éléments associés + */ createCoordinateInput (type) { // type = "lon" or "lat" const wrapper = document.createElement("div"); @@ -362,7 +378,13 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { return wrapper; } - + /** + * Crée la liste des points cardinaux (N/S ou O/E) pour un champ de coordonnées. + * + * @private + * @param {String} baseName "lon" ou "lat" + * @returns {HTMLSelectElement|undefined} Élément select des cardinaux ou undefined si non applicable + */ _createSelectCardinals (baseName) { const options = { lon : ["O", "E"], @@ -386,6 +408,11 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } } + /** + * @override + * @protected + * @param {CoordinateAdvancedSearchOptions} options Options d'initialisation + */ _initEvents (options) { super._initEvents(options); @@ -398,6 +425,12 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.on("change:unit", this._updateInputs.bind(this)); } + /** + * Met à jour le système de référence sélectionné et réinitialise les unités disponibles. + * + * @private + * @param {Event} e Événement change provenant du select système + */ _updateSystem (e) { const crs = this._coordinateSearchSystems[e.target.value].crs; if (crs !== this._currentCoordinateSystem.crs) { @@ -418,6 +451,12 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } } + /** + * Met à jour l'unité sélectionnée et stocke la valeur dans le dataset du formulaire. + * + * @private + * @param {Event} e Événement change provenant du select unité + */ _updateUnits (e) { const value = e.target.value; if (this.get("unit") !== value) { @@ -426,6 +465,11 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } } + /** + * Met à jour les labels des inputs en fonction du type d'unités (géographique vs métrique). + * + * @private + */ _updateInputsLabel () { const unitType = this.get("unitType"); const degree = unitType === "Geographical"; @@ -441,6 +485,11 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this.lat.querySelector("input").value = ""; } + /** + * Met à jour les inputs lorsque l'unité change (conversion ou activation du masquage pour DMS). + * + * @private + */ _updateInputs () { const unit = this.get("unit"); let factor = 1; @@ -467,9 +516,10 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } /** - * - * @param {InputEvent} e - * @returns + * Validation avant saisie pour les formats DMS : n'autorise que les chiffres (évite les lettres). + * + * @private + * @param {InputEvent} e Événement beforeinput */ _onlonLatBeforeInput (e) { // const regex = /^(?:\d{2}°\d{2}'\d{2}(?:"|''))$|^\d{6}$/; @@ -480,14 +530,23 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } } + /** + * Formate une chaîne de 6 caractères en DMS (DD°MM'SS"). + * + * @private + * @param {String} value Valeur brute (6 caractères numériques) + * @returns {String} Chaîne formatée (ex. "12°34'56\"") + */ _format (value) { const v = value.padEnd(6, "_"); return `${v.slice(0,2)}°${v.slice(2,4)}'${v.slice(4,6)}"`; } /** - * - * @param {InputEvent} e + * Met à jour l'affichage du masque pendant la saisie DMS. + * + * @private + * @param {InputEvent} e Événement input */ _onlonLatInput (e) { const value = e.target.value; @@ -496,9 +555,9 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { } /** - * - * @param {PointerEvent} e + * @override * @protected + * @param {PointerEvent} e Événement de soumission */ _onSearch (e) { super._onSearch(e); @@ -548,6 +607,14 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { }); } + /** + * Construit le contenu HTML à afficher dans la popup à partir des coordonnées fournies. + * + * @private + * @param {Number|String} lon Valeur X / longitude + * @param {Number|String} lat Valeur Y / latitude + * @returns {String} Contenu HTML (string) à injecter dans la popup + */ _createInfoPopup (lon, lat) { let x, y, valueX, valueY; if (this.get("unitType") === "Geographical") { diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js index c2782b322..ee70e5938 100644 --- a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js @@ -1,26 +1,27 @@ // import CSS import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; import Logger from "../../Utils/LoggerByDefault"; -import Collection from "ol/Collection"; import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; -import { InseeSearchService } from "./Service"; +import InseeSearchService from "../../Services/InseeSearchService"; var logger = Logger.getLogger("abstractAdvancedSearch"); +// Voir les typedefs réutilisables (types partagés) dans le dossier SearchEngine si nécessaire. /** * @classdesc - * AbstractAdvancedSearch Base control + * Contrôle de recherche avancée pour les codes INSEE. * * @alias ol.control.InseeAdvancedSearch * @module InseeAdvancedSearch -*/ + * @extends AbstractAdvancedSearch + */ class InseeAdvancedSearch extends AbstractAdvancedSearch { /** - * @constructor - * @example - */ + * @constructor + * @param {AbstractAdvancedSearchOptions} [options] - Options du contrôle. + */ constructor (options) { super(options); @@ -29,6 +30,12 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { }.bind(this)); } + /** + * + * @param {AbstractAdvancedSearchOptions} options Options de recherche avancée + * @override + * @protected + */ initialize (options) { if (!options.name) { options.name = "Code INSEE"; @@ -43,10 +50,10 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { } /** - * Ajoute des éléments d'input dans la collection `this.inputs`; - * Cette méthode est abstraite et doit être surchargée dans les autres classes. + * Ajoute les inputs spécifiques au contrôle (surcouche du parent). + * Crée et configure l'input INSEE. * @protected - * @abstract + * @returns {void} */ addInputs () { super.addInputs(); @@ -70,6 +77,12 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { this.inputs.push(inseeInput); } + /** + * Initialise les événements DOM spécifiques. + * @protected + * @param {AbstractAdvancedSearchOptions} options - Options d'initialisation (transmises depuis le parent). + * @override + */ _initEvents (options) { super._initEvents(options); @@ -81,9 +94,10 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { } /** - * - * @param {PointerEvent} e + * Handler de la recherche déclenchée (bouton / submit). + * Valide le code INSEE puis lance la recherche via le SearchEngine associé. * @protected + * @param {PointerEvent|Event} e - Événement de recherche. */ _onSearch (e) { super._onSearch(e); diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 50c871ad8..d215a33d6 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -1,15 +1,21 @@ import Helper from "../../Utils/Helper"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; -import IGNSearchService from "./Service"; +import IGNSearchService from "../../Services/IGNSearchService"; +/** + * @classdesc + * Recherche avancée par lieu / toponyme (formulaire pour filtrer par type, code postal, INSEE). + * + * @alias ol.control.LocationAdvancedSearch + * @module LocationAdvancedSearch + */ class LocationAdvancedSearch extends AbstractAdvancedSearch { /** - * @constructor - * @param {AbstractAdvancedSearchOptions} options Options du constructeur - * - * @example - */ + * Constructeur du contrôle LocationAdvancedSearch. + * @constructor + * @param {AbstractAdvancedSearchOptions} options Options du constructeur + */ constructor (options) { options = options || {}; @@ -26,6 +32,11 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { }); } + /** + * @override + * @protected + * @param {AbstractAdvancedSearchOptions} options Options du constructeur + */ _initEvents (options) { super._initEvents(options); @@ -82,6 +93,11 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { }); } + /** + * @override + * @protected + * @param {AbstractAdvancedSearchOptions} options Options du constructeur + */ initialize (options) { super.initialize(options); /** @@ -90,14 +106,11 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { */ this.CLASSNAME = "LocationAdvancedSearch"; } - /* - setMap (map) { - super.setMap(map); - this.search.setMap(map); - } - */ - /** Handle multiresults: display list for selct/search - * @param {Object} e Event + + /** + * Affiche la liste des résultats multiples et permet la sélection. + * @private + * @param {Event} e Événement de recherche contenant les résultats */ handleMultipleResults (e) { const results = this.searchService.getResult(); @@ -158,6 +171,15 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // this.dispatchEvent(e); } + /** + * Crée un conteneur label + input pour le formulaire. + * @private + * @param {String} text Texte du label + * @param {String} type Classe CSS du conteneur + * @param {HTMLElement} input Élément input à rattacher + * @param {String} [hint] Texte d'aide optionnel + * @returns {HTMLElement} Conteneur HTML + */ _getLabelContainer (text, type, input, hint = "") { const container = document.createElement("div"); container.className = type; @@ -176,8 +198,10 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { } return container; } - /** Add inputs - * + + /** + * @override + * @protected */ addInputs () { super.addInputs(); @@ -196,8 +220,6 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { legend.appendChild(hint); this.inputs.push(legend); - // ^pkp^*k - // Type const typeSelect = document.createElement("select"); typeSelect.className = "fr-select"; @@ -265,6 +287,12 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { citycode : inseeInput.value }; } + + /** + * @protected + * @override + * @param {PointerEvent} e Événement d'effacement + */ _onErase (e) { super._onErase(e); this.element.querySelectorAll("select").forEach(input => { @@ -280,14 +308,21 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { citycode : "" }; } - /** Lancer une recheche - * + + /** + * @protected + * @override + * @param {PointerEvent} e Événement de soumission */ _onSearch (e) { super._onSearch(e); const value = this.searchInput.value; if (value) { - this.searchService.search(value, this.filter); + const obj = { + location : value, + filters : this.filter + }; + this.searchService.search(obj); } } diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 44b426b3c..6e0c0690d 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -1,10 +1,11 @@ -import Control from "ol/control/Control"; +import Control from "../Control"; import Geolocation from "ol/Geolocation"; import OlFeature from "ol/Feature"; import Point from "ol/geom/Point"; import SearchEngineGeocodeIGN from "./SearchEngineGeocodeIGN"; import Helper from "../../Utils/Helper"; -import Select from "ol/interaction/Select"; +import Select, { SelectEvent } from "ol/interaction/Select"; +import Map from "ol/Map"; import Vector from "ol/layer/Vector"; import VectorSource from "ol/source/Vector"; @@ -15,6 +16,7 @@ import { Style, Icon, Stroke, Fill } from "ol/style"; import mapPinIcon from "./map-pin-2-fill.svg"; import Feature from "ol/Feature"; import { Layer } from "ol/layer"; +import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; const color = "rgba(0, 0, 145, 1)"; const createStyle = (feature) => { @@ -59,17 +61,19 @@ const createStyle = (feature) => { }; /** - * Classe représentant un moteur de recherche avancée utilisant le service de géocodage de l'IGN. - * - * @extends {SearchEngineGeocodeIGN} - * @example - * import SearchEngineAdvanced from "geopf-controls/Controls/SearchEngine/SearchEngineAdvanced"; + * @classdesc + * Contrôle de recherche avancée permettant de rechercher via d'autres manières. + * Gère aussi l'ajout des élements sur la carte etc. + * + * @extends {Control} + * @module SearchEngineAdvanced */ class SearchEngineAdvanced extends Control { /** - * Constructeur. + * Constructeur du contrôle de recherche avancée. * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. + * @param {AbstractAdvancedSearch[]} options.advancedSearch - Recherches avancées. */ constructor (options) { options = options || {}; @@ -115,9 +119,9 @@ class SearchEngineAdvanced extends Control { /** * Initialise les options du contrôle. - * - * @override * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. + * @param {AbstractAdvancedSearch[]} options.advancedSearch - Recherches avancées. + * @private */ initialize (options) { /** @@ -131,6 +135,11 @@ class SearchEngineAdvanced extends Control { */ this._layerFeatureAssociation = {}; + /** + * @type {Array} + */ + this._searchForms; + if (options.advancedSearch && options.advancedSearch instanceof Array) { this._searchForms = options.advancedSearch; } else { @@ -143,9 +152,8 @@ class SearchEngineAdvanced extends Control { } /** - * Fonction d'ajout du contrôle. * @override - * @param {import("ol/Map.js").default|null} map - Carte à laquelle ajouter le contrôle. + * @param {Map|null} map Carte cible */ setMap (map) { if (this.getMap() && this.baseSearchEngine) { @@ -170,6 +178,11 @@ class SearchEngineAdvanced extends Control { } } + /** + * Initialise les événements du contrôle (géolocalisation, navigation clavier, recherche). + * @param {SearchEngineGeocodeIGNOptions} options Options du constructeur. + * @private + */ _initEvents (options) { this.geolocation.on("change:position", () => { const pt = new Point(this.geolocation.getPosition()); @@ -203,9 +216,11 @@ class SearchEngineAdvanced extends Control { this.on("search", this.addResultToMap.bind(this)); } - /** Display result on map - * @param {Object|Point|OlFeature} obj objet a afficher - * @param {String} [info] Popup info + /** + * Crée un événement de recherche à partir d'un objet (Feature ou Point). + * @param {Object|Point|OlFeature} obj Objet à afficher (Feature ou Point) + * @param {String} [info] Texte affiché dans la popup + * @returns {Object} Événement normalisé de type "search" */ createEvent (obj, info) { let evt = obj; @@ -227,11 +242,10 @@ class SearchEngineAdvanced extends Control { return evt; } - /** - * Créé le conteneur - * - * @param {Object} options Options du constructeur + * Initialise le conteneur principal du contrôle et les sous-composants. + * @param {SearchEngineGeocodeIGNOptions} options Options du constructeur + * @private */ _initContainer (options) { // Gestion de l'affichage des options avancées @@ -330,7 +344,10 @@ class SearchEngineAdvanced extends Control { }); } - + /** + * Ajoute les résultats (features) sur la carte et ajuste la vue. + * @param {Object} e Événement de recherche contenant result/extent + */ addResultToMap (e) { this._closePopup(); this.layer.getSource().clear(); @@ -359,10 +376,10 @@ class SearchEngineAdvanced extends Control { } } - /** - * - * @param {import("ol/interaction/Select").SelectEvent} e Événement de séléction + * Callback lors de la sélection d'une feature (affiche le popup). + * @param {SelectEvent} e Événement de sélection + * @private */ _onSelectElement (e) { let position = e.mapBrowserEvent.coordinate; @@ -389,6 +406,11 @@ class SearchEngineAdvanced extends Control { } } + /** + * Crée et retourne l'overlay popup pour afficher les infos de feature. + * @private + * @returns {Overlay} Overlay du popups + */ _createPopup () { // Popup global let element = this._popupDiv = document.createElement("div"); @@ -417,10 +439,19 @@ class SearchEngineAdvanced extends Control { return overlay; } + /** + * Définit le contenu HTML du popup. + * @param {String} content Contenu HTML à afficher + */ setPopupContent (content) { this._popupContent.innerHTML = content; } + /** + * Crée le bouton de fermeture du popup. + * @returns {HTMLButtonElement} Bouton de fermeture + * @private + */ _addCloseButton () { let closer = document.createElement("button"); closer.title = closer.ariaLabel = "Fermer la pop-up"; @@ -432,6 +463,11 @@ class SearchEngineAdvanced extends Control { return closer; } + /** + * Ferme le popup et désélectionne la feature. + * @returns {Boolean} false + * @private + */ _closePopup () { this.selectInteraction.getFeatures().clear(); if (this.popup !== null) { @@ -440,6 +476,11 @@ class SearchEngineAdvanced extends Control { return false; } + /** + * Crée le bouton de suppression du marqueur. + * @returns {HTMLButtonElement} Bouton de suppression + * @private + */ _addRemoveButton () { let remove = document.createElement("button"); remove.title = remove.ariaLabel = "Supprimer le marqueur"; @@ -452,6 +493,10 @@ class SearchEngineAdvanced extends Control { return remove; } + /** + * Supprime la feature sélectionnée de la couche et ferme le popup. + * @private + */ _removeFeature () { const f = this.popup.get("feature"); const layer = this.popup.get("layer"); @@ -470,6 +515,11 @@ class SearchEngineAdvanced extends Control { } } + /** + * Crée le bouton de géolocalisation. + * @returns {HTMLButtonElement} Bouton de géolocalisation + * @private + */ _getGeolocButton () { const locationBtn = document.createElement("button"); locationBtn.innerText = "Me géolocaliser"; @@ -481,6 +531,11 @@ class SearchEngineAdvanced extends Control { return locationBtn; } + /** + * Callback lors d'un résultat de recherche avancée. + * @param {Object} e Événement de recherche avancée + * @private + */ onAdvancedSearchResult (e) { if (e.result instanceof Array) { // TODO : GÉRER MULTIPLE RÉSULTATS diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 78fa1691a..88ad1062b 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -2,9 +2,11 @@ import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import Control from "../Control"; import Logger from "../../Utils/LoggerByDefault"; -import { DefaultSearchService } from "./Service"; +import DefaultSearchService from "../../Services/DefaultSearchService"; import Helper from "../../Utils/Helper"; +// Voir les typedefs partagés dans ./typedefs.js (SearchEngineBaseOptions, SearchServiceOptions, ...) + const typeClasses = { "history" : "fr-icon-history-line", "search" : "fr-icon-map-pin-2-line", @@ -12,58 +14,32 @@ const typeClasses = { var logger = Logger.getLogger("searchengine"); -/** - * @typedef {Object} SearchEngineBaseOptions Options du constructeur pour le contrôle de recherche. - * - * @property {HTMLElement|string} [target] - Élément DOM ou sélecteur dans lequel insérer le contrôle. - * Si non défini, le contrôle crée un bouton permettant d’ouvrir/fermer le champ de recherche. - * @property {string} [title="Rechercher"] - Texte du titre (attribut `title`) du bouton principal. - * @property {string} [label=""] - Label à ajouter. Aucun par défaut. - * @property {string} [hint=""] - Texte additionnel à ajouter sous le label. Aucun par défaut. - * @property {Boolean} [search=false] - Si vrai, définit le composant comme une barre de recherche (classes CSS et attributs HTML). - * @property {string} [collapsible=false] - Si vrai, permet de fermer le contrôle. - * @property {string} [ariaLabel="Rechercher"] - Libellé accessible (ARIA) pour le champ de recherche. - * @property {string} [placeholder=""] - Texte d’indication affiché dans le champ de saisie. - * @property {number} [minChars=0] - Nombre minimum de caractères à saisir avant de lancer l’autocomplétion. - * @property {number} [maximumEntries=5] - Nombre maximum d’entrées affichées dans la liste d’autocomplétion. - * @property {number} [searchButton=false] - Affiche un bouton de recherche. Faux par défaut. - * @property {number} [triggerDelay=100] - Délai (en millisecondes) avant le déclenchement de l’autocomplétion - * après la saisie de l’utilisateur. - * @property {boolean|string} [historic=true] - Active ou non l’historique local des recherches. Valeur acceptées : - * - `false` : désactive complètement l’historique ; - * - `true` : active l’historique sous le nom par défaut `GPsearch-SearchEngineBase` ; - * - `string` : active l’historique sous un nom personnalisé (ex. `"monHistoriquePerso"`). - * @property {import("./Service.js").AbstractSearchService} [searchService] - Service de recherche à utiliser. Créera un service par défaut si non donné. - */ - - /** * @classdesc - * SearchEngine Base control + * Contrôle de base pour la recherche (barre de recherche, autocomplétion, historique). * * @alias ol.control.SearchEngineBase * @module SearchEngine -*/ + */ class SearchEngineBase extends Control { /** - * @constructor - * @param {SearchEngineBaseOptions} options Options du constructeur - * @fires autocomplete - * @fires search - * @fires select - * - * @example - * const search = new ol.control.SearchEngineBase({ - * placeholder: "Rechercher une adresse...", - * minChars: 3, - * maximumEntries: 10, - * historic: "mesRecherches", - * searchService: new CustomSearchService() - * }); - * - * map.addControl(search) - */ + * Constructeur du contrôle SearchEngineBase. + * @constructor + * @param {SearchEngineBaseOptions} options Options du constructeur + * @fires autocomplete + * @fires search + * @fires select + * @example + * const search = new ol.control.SearchEngineBase({ + * placeholder: "Rechercher une adresse...", + * minChars: 3, + * maximumEntries: 10, + * historic: "mesRecherches", + * searchService: new CustomSearchService() + * }); + * map.addControl(search) + */ constructor (options) { options = options || {}; // call ol.control.Control constructor @@ -112,10 +88,9 @@ class SearchEngineBase extends Control { this.showHistoric(); } /** - * Initialize SearchEngine control (called by SearchEngine constructor) - * - * @param {SearchEngineBaseOptions} options - constructor options + * Initialise le contrôle SearchEngineBase (appelé par le constructeur). * @protected + * @param {SearchEngineBaseOptions} options Options du constructeur */ initialize (options) { // Valeurs par défaut des options @@ -135,9 +110,10 @@ class SearchEngineBase extends Control { this.set("maximumEntries", options.maximumEntries); } - /** Add event listeners - * @param {SearchEngineBaseOptions} options - constructor options + /** + * Ajoute les écouteurs d'événements sur les éléments du contrôle. * @protected + * @param {SearchEngineBaseOptions} options Options du constructeur */ _initEvents (options) { if (this.searchService.get("autocomplete") !== false) { @@ -208,7 +184,7 @@ class SearchEngineBase extends Control { break; default: if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { - this.autocomplete(e.target.value, e.key === "Enter"); + this.autocomplete(e.target.value); } break; } @@ -241,8 +217,10 @@ class SearchEngineBase extends Control { }.bind(this)); } /** - * - * @param {*} options + * Initialise le conteneur DOM principal du contrôle. + * @private + * @param {SearchEngineBaseOptions} options Options du constructeur + * @returns {void} */ _initContainer (options) { const element = this.element = document.createElement("div"); @@ -412,23 +390,33 @@ class SearchEngineBase extends Control { } } + /** + * Active ou désactive le contrôle (désactive l'input / bouton). + * @param {Boolean} active Indique si le contrôle doit être désactivé + * @returns {void} + */ setActive (active) { this.input.disabled = !!active; this.subimtBt.disabled = !!active; } - /** Autocomplete and update list - * @param {String} [value] input value - * @param {Boolean} [force=false] force to add in historic + /** + * Lance l'autocomplétion et met à jour la liste. + * @param {String} [value] Valeur de l'input * @api */ - autocomplete (value, force) { + autocomplete (value) { clearTimeout(this._completeDelay); this._completeDelay = setTimeout(function () { - this.searchService.autocomplete(value, { force : force }); + this.searchService.autocomplete(value); }.bind(this), this.get("triggerDelay") || 100); } + /** + * Callback sur événement d'autocomplétion. + * @param {Object} e Événement d'autocomplétion + * @private + */ onAutocomplete (e) { clearTimeout(this._completeDelay); // Update list} @@ -436,22 +424,21 @@ class SearchEngineBase extends Control { this.dispatchEvent(e); } - /** Effectue la recherche de géocodage - * @param {String} [value] input value + /** + * Lance la recherche de géocodage. + * @param {IGNSearchObject} item Valeur ou objet à rechercher * @api */ search (item) { - console.log(item); clearTimeout(this._completeDelay); this._completeDelay = setTimeout(function () { this.searchService.search(item); }.bind(this), this.get("triggerDelay") || 100); } - /** Do something on search ready - * @param {Object} e event - * @param {String} e.search search string - * @param {Object|Boolean} e.options options given to autocomplete - * @param {Array<*>} e.result result of autocomplete + + /** + * Callback sur événement de recherche. + * @param {Object} e Événement de recherche * @api */ onSearch (e) { @@ -459,8 +446,10 @@ class SearchEngineBase extends Control { // Update list} this.dispatchEvent(e); } - /** An item has been selected - * @param {*} item selected item + + /** + * Callback sur sélection d'un item. + * @param {Object} item Élément sélectionné * @api */ select (item) { @@ -476,8 +465,9 @@ class SearchEngineBase extends Control { item : item }); } + /** - * Show historic list + * Affiche la liste de l'historique. * @api */ showHistoric () { @@ -486,10 +476,12 @@ class SearchEngineBase extends Control { this._updateList(this._historic.length ? this._historic : [], "history"); } } + /** - * Update autocomplete list - * @param {Array<*>} tab list of autocomplete items - * @param {string} [type="search"] Optionnel. Type à inclure. Valeur autorisée : "history", "search" + * Met à jour la liste d'autocomplétion. + * @private + * @param {Array} tab Liste des items d'autocomplétion + * @param {String} [type="search"] Type d'affichage ("history" ou "search") */ _updateList (tab, type = "search") { this.autocompleteList.parentNode.dataset.type = type; @@ -513,21 +505,27 @@ class SearchEngineBase extends Control { li.addEventListener("click", function (e) { const idx = Number(e.target.getAttribute("data-idx")); this.select(tab[idx]); - this.search(tab[idx], idx); + this.search({ + location : tab[idx] + }); }.bind(this)); }); } - /** Get item title given an item object - * @param {*} item - * @returns {String} title + + /** + * Retourne le titre à afficher pour un item. + * @param {Object} item Élément à afficher + * @returns {String} Titre * @api */ getItemTitle (item) { return this.searchService.getItemTitle(item); } + /** - * Add or replace value in historic list - * @param {*} value + * Ajoute ou remplace une valeur dans l'historique. + * @private + * @param {Object} value Valeur à ajouter */ _updateHistoric (value) { if (this._historic) { @@ -558,9 +556,10 @@ class SearchEngineBase extends Control { /** * Vérifie si deux éléments (objets) sont égaux. - * - * @param {Array} a Premier objet + * @private + * @param {Object} a Premier objet * @param {Object} b Objet de comparaison + * @returns {Boolean} true si égal, false sinon */ _isEqual (a, b) { // TODO : Améliorer comparaison ? @@ -571,10 +570,9 @@ class SearchEngineBase extends Control { } /** - * Ajoute un message à un champs de saisie - * @param {HTMLInputElement|HTMLSelectElement} input Champs de saisie + * Ajoute un message à un champ de saisie. * @param {String} message Message à afficher - * @param {String} [type="error"] Type du message. Message d'erreur par défaut + * @param {String} [type="error"] Type du message ("error" ou "valid") * @api */ addMessage (message, type = "error") { @@ -591,8 +589,8 @@ class SearchEngineBase extends Control { } /** - * Enlève un message d'erreur - * @param {HTMLInputElement|HTMLSelectElement} input Champs de saisie + * Enlève les messages d'erreur du champ de saisie. + * @param {HTMLInputElement|HTMLSelectElement} input Champ de saisie * @api */ removeMessages () { diff --git a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js index f055b18cb..0e6916aa4 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js +++ b/src/packages/Controls/SearchEngine/SearchEngineGeocodeIGN.js @@ -1,36 +1,41 @@ // import CSS -import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import Logger from "../../Utils/LoggerByDefault"; import SearchEngineBase from "./SearchEngineBase"; -import { AbstractSearchService } from "./Service"; -import IGNSearchService from "./Service"; +import AbstractSearchService from "../../Services/AbstractSearchService"; +import IGNSearchService from "../../Services/IGNSearchService"; var logger = Logger.getLogger("searchengine"); /** - * Options spécifiques au contrôle IGN + * @file + * Contrôle de recherche spécialisé pour le géocodage IGN. + * Ce contrôle hérite de SearchEngineBase et ajoute la gestion des options spécifiques IGN via la propriété `serviceOptions`. + * Utilisez le typedef {@link SearchEngineGeocodeIGNOptions} pour bénéficier de l'autocomplétion et de la documentation dans VSCode/TypeDoc. * - * Cette définition combine (hérite) de SearchEngineBaseOptions - * et ajoute une propriété `serviceOptions` qui contient - * les options propres au service (IGNSearchService). - * - * @typedef {import("./SearchEngineBase.js").SearchEngineBaseOptions & { - * serviceOptions: import("./Service.js").AbstractSearchServiceOptions - * }} SearchEngineGeocodeIGNOptions + * @see SearchEngineBaseOptions + * @see AbstractSearchServiceOptions */ /** * @classdesc - * SearchEngine Base control + * Moteur de recherche spécialisé pour le géocodage IGN (implémentation de SearchEngineBase). * * @alias ol.control.SearchEngineGeocodeIGN * @module SearchEngine -*/ + * @extends SearchEngineBase + */ class SearchEngineGeocodeIGN extends SearchEngineBase { /** + * Constructeur du contrôle SearchEngineGeocodeIGN. + * * @constructor - * @param {SearchEngineGeocodeIGNOptions} options Options du constructeur + * @param {SearchEngineGeocodeIGNOptions} [options] Options du contrôle. + * @example + * const ctrl = new SearchEngineGeocodeIGN({ + * placeholder: "Rechercher...", + * serviceOptions: { apiKey: "votre-cle", returnTrueGeometry: true } + * }); */ constructor (options) { options = options || {}; @@ -79,7 +84,6 @@ class SearchEngineGeocodeIGN extends SearchEngineBase { */ _initEvents (options) { super._initEvents(options); - // this.on("search", this.addResultToMap); } } diff --git a/src/packages/Controls/SearchEngine/typedefs.js b/src/packages/Controls/SearchEngine/typedefs.js new file mode 100644 index 000000000..9bb446790 --- /dev/null +++ b/src/packages/Controls/SearchEngine/typedefs.js @@ -0,0 +1,129 @@ +/** + * Typedefs partagés pour les contrôles SearchEngine. + * Ce fichier centralise les types réutilisables afin que typedoc génère des docs cohérentes. + */ + +/** + * @typedef {Object} SearchServiceOptions + * @property {Boolean} [autocomplete] - Active/désactive l'autocomplétion côté service. + * @property {Object} [searchOptions] - Options passées au service de recherche. + * @property {Object} [searchOptions.serviceOptions] - Options spécifiques au service. + * @property {string[]} [searchOptions.serviceOptions.fields] - Liste de champs à demander au service. + */ + +/** + * @typedef {Object} SearchEngineOptions + * @property {String} [label] - Libellé associé à l'input. + * @property {String} [hint] - Texte d'aide / placeholder. + * @property {SearchServiceOptions|Object} [searchService] - Configuration du service de recherche. + */ + +/** + * @typedef {Object} TextInputElement + * @property {String} [pattern] - Pattern HTML attendu (chaîne regex). + * @property {String} [title] - Titre / info du champ. + * @property {String} [value] - Valeur courante du champ. + * @property {(e:KeyboardEvent)=>void} [onkeydown] - Gestionnaire keydown. + * @property {(type:string, listener:Function)=>void} [addEventListener] - Ajout d'écouteur. + */ + +/** + * Résultat d'une autocomplétion (voir {@link https://ignf.github.io/geoportal-access-lib/latest/jsdoc/Gp.Services.AutoComplete.SuggestedLocation.html}) + * @typedef {Object} AutocompleteResult + * @property {"StreetAddress"|"PositionOfInterest"} type - Type de suggestion. + * @property {Position} position - Coordonnées du point, dans le système de coordonnées spécifiées. + * @property {String} commune - Nom de la commune. + * @property {String} fullText - Texte complet représentant la suggestion. + * @property {String} postalCode - Code postal de la suggestion. + * @property {Number} classification - Nombre utilisé pour classigier l'importance de l'endroit suggéré de 1 (plus important) à 7 (moins important) + * @property {Array} [poiType] - Types POI détaillés. + * @property {String} [street] - Nom de la rue (types "StreetAddress" seulement). + * @property {String} [kind] - Nature du point d'intérêt, e.g. "prefecture", "municipality"... (types "PositionOfInterest" seulement). + */ + +/** + * Position dans un système de coordonnées. + * @typedef {Object} Position + * @property {Number} x - Longitude. + * @property {Number} y - Latitude. + */ + + +/** + * Erreur du service (voir {@link https://ignf.github.io/geoportal-access-lib/latest/jsdoc/Gp.Error.html}) + * @typedef {Object} ErrorService + * @property {string} message - Message d'erreur retourné par le service. + * @property {number} status - Code de statut (-1 si inconnu). + * @property {string} type - Type d'erreur (ex. "UNKNOWN_ERROR"). + * @property {string} name - Nom de l'erreur (ex. "ErrorService"). + * @property {string} [stack] - Stack trace de l'erreur, si disponible. + */ + + +/** + * Résultat d'une recherche (géocodage final). + * @typedef {Object} SearchResult + * @property {import("ol/Feature").default} feature - Feature OL contenant la géométrie. + * @property {import("ol/Feature").default|undefined} [extent] - Étendue si zone géographique. + * @property {String} [infoPopup] - Texte à afficher dans un popup. + */ + +/** + * Options pour le contrôle SearchEngineBase. + * @typedef {Object} SearchEngineBaseOptions + * @property {HTMLElement|string} [target] - Élément DOM ou sélecteur cible. + * @property {String} [title="Rechercher"] - Texte du titre du bouton. + * @property {String} [label=""] - Label affiché. + * @property {String} [hint=""] - Texte d'aide. + * @property {Boolean} [search=false] - Comportement en tant que barre de recherche. + * @property {String} [collapsible=false] - Si vrai, le contrôle est repliable. + * @property {String} [ariaLabel="Rechercher"] - Libellé ARIA. + * @property {String} [placeholder=""] - Placeholder de l'input. + * @property {Number} [minChars=0] - Nombre minimal de caractères pour autocomplétion. + * @property {Number} [maximumEntries=5] - Nombre maximal d'entrées affichées. + * @property {boolean|string} [historic=true] - Gestion historique local (false|true|string). + * @property {import("../../Services/AbstractSearchService").default} [searchService] - Service de recherche. + */ + +/** + * Options pour AbstractAdvancedSearch (formulaires avancés). + * @typedef {Object} AbstractAdvancedSearchOptions + * @property {String} name - Nom du formulaire de recherche avancée. + */ + +/** + * Événement de recherche générique. + * @typedef {Object} SearchEvent + * @property {String} type - Type d'événement (ex. "search"). + * @property {Object} [detail] - Payload détaillé. + */ + +/** + * Description d'un système de projection utilisable par le contrôle CoordinateAdvancedSearch. + * @typedef {Object} CoordinateSearchSystem + * @property {String} crs - Alias CRS (ex. "EPSG:4326"). + * @property {String} [label] - Libellé affiché pour le système. + * @property {String} [type] - Type d'unités ("Geographical"|"Metric"). + */ + +/** + * Options spécifiques à la recherche par coordonnées. + * @typedef {Object} CoordinateSearchOptions + * @property {CoordinateSearchSystem[]} [systems] - Liste de systèmes de projection personnalisés. + * @property {string[]} [units] - Liste de codes d'unités à afficher (ex. ["DEC","DMS","M","KM"]). + */ + +/** + * Options pour le contrôle SearchEngineGeocodeIGN. + * Étend SearchEngineBaseOptions et ajoute les options spécifiques du service IGN. + * @typedef {SearchEngineBaseOptions & { + * serviceOptions?: AbstractSearchServiceOptions + * }} SearchEngineGeocodeIGNOptions + */ + + +/** + * Options pour le contrôle SearchEngineGeocodeIGN. + * Étend SearchEngineBaseOptions et ajoute les options spécifiques du service IGN. + * @typedef {AbstractAdvancedSearchOptions & {coordinateSearch?: CoordinateSearchOptions}} CoordinateAdvancedSearchOptions + */ diff --git a/src/packages/Services/AbstractSearchService.js b/src/packages/Services/AbstractSearchService.js new file mode 100644 index 000000000..8ef148687 --- /dev/null +++ b/src/packages/Services/AbstractSearchService.js @@ -0,0 +1,128 @@ +import Logger from "../Utils/LoggerByDefault"; +import BaseObject from "ol/Object"; + +var logger = Logger.getLogger("searchengine"); + +/** + * @classdesc + * Service de recherche abstrait : base commune pour les services d'autocomplétion et de géocodage. + * À surcharger pour chaque type de service (IGN, INSEE, etc.). + * + * @alias ol.service.AbstractSearchService + * @abstract + * @module SearchService + */ +class AbstractSearchService extends BaseObject { + + /** + * Constructeur du service abstrait. + * @constructor + * @param {AbstractSearchServiceOptions} options Options du service + */ + constructor (options) { + options = options || {}; + + // call ol.control.Control constructor + super(options); + + if ((this.constructor == AbstractSearchService)) { + throw new TypeError("AbstractSearchService cannot be instantiate"); + } + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "AbstractSearchService"; + + // initialisation du composant + this.initialize(options); + + return this; + } + + /** + * Initialise le service avec les options fournies. + * @protected + * @param {AbstractSearchServiceOptions} options Options de configuration du service + */ + initialize (options) { + this.AUTOCOMPLETE_EVENT = "autocomplete"; + this.SEARCH_EVENT = "search"; + + this._autocompleteLocations = []; + this._locations = []; + + if (options.autocomplete === false) { + this.set("autocomplete", false); + } + this.set("index", options.index || "address,poi"); + this.set("limit", typeof options.limit === "number" ? options.limit : 1); + this.set("returnTrueGeometry", !!options.returnTrueGeometry); + } + + /** + * Récupère la liste des résultats d'autocomplétion. + * @param {Number} [index] Index du résultat (optionnel). Si non fourni, retourne tous les résultats. + * @returns {Array|AutocompleteResult} Un tableau de résultats ou un résultat unique + */ + getAutocompleteLocations (index) { + if (index === undefined) { + return this._autocompleteLocations; + } else { + return this._autocompleteLocations[index]; + } + } + + /** + * Récupère la liste des résultats de recherche finale. + * @param {Number} [index] Index du résultat (optionnel). Si non fourni, retourne tous les résultats. + * @returns {Array|SearchResult} Un tableau de résultats ou un résultat unique + */ + getResult (index) { + if (index === undefined) { + return this._locations; + } else { + return this._locations[index]; + } + } + + /** + * Lance une autocomplétion. + * Méthode à surcharger dans les services concrets. + * @param {String} text Texte d'autocomplétion + * @abstract + */ + autocomplete (text) { } + + + /** + * Lance une recherche. + * Méthode à surcharger dans les services concrets. + * @param {Object} obj Options de recherche (dépend du service) + * @abstract + */ + search (obj) { } + + + /** + * Retourne le titre à afficher pour un résultat. + * Méthode à surcharger dans les services concrets. + * @param {AutocompleteResult} obj Objet dont le titre dérive + * @abstract + * @returns {String} Titre du résultat + */ + getItemTitle (obj) { + return obj; + } + +} + +export default AbstractSearchService; + +// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +if (window.ol) { + if (!window.ol.service) { + window.ol.service = {}; + } + window.ol.service.AbstractSearchService = AbstractSearchService; +} diff --git a/src/packages/Services/DefaultSearchService.js b/src/packages/Services/DefaultSearchService.js new file mode 100644 index 000000000..bad617675 --- /dev/null +++ b/src/packages/Services/DefaultSearchService.js @@ -0,0 +1,78 @@ +import Logger from "../Utils/LoggerByDefault"; +import AbstractSearchService from "./AbstractSearchService"; + +var logger = Logger.getLogger("searchengine"); + +/** + * @classdesc + * Service de recherche par défaut (implémentation simple pour tests ou données locales). + * + * @alias ol.service.DefaultSearchService + * @module SearchService + * @extends AbstractSearchService + */ +class DefaultSearchService extends AbstractSearchService { + + /** + * Constructeur du service par défaut. + * @constructor + * @param {AbstractSearchServiceOptions} options Options du service (ex: {searchTab: [...]}) + * @param {Array} options.searchTab Tableau d'élément dans lequel rechercher + */ + constructor (options) { + options = options || {}; + super(options); + /** + * Nom de la classe (héritage) + * @private + */ + this.CLASSNAME = "DefaultSearchService"; + if (options.searchTab) { + this._searchTab = options.searchTab || []; + } + } + + /** + * Lance une autocomplétion sur la liste locale. + * @override + * @param {String} value Valeur à compléter + */ + autocomplete (value) { + // Simulate asynchronous behavior + this._autocompleteLocations = []; + const rex = new RegExp(value, "i"); + (this._searchTab || []).forEach((city) => { + if (rex.test(city.toLowerCase())) { + this._autocompleteLocations.push(city); + } + }); + // When search is finished + this.dispatchEvent({ + type : this.AUTOCOMPLETE_EVENT, + result : this._autocompleteLocations + }); + } + + /** + * Lance une recherche (simulée). + * @override + * @param {Object} obj Options de recherche + */ + search (obj) { + this.dispatchEvent({ + type : this.SEARCH_EVENT, + result : obj + }); + } + +} + +export default DefaultSearchService; + +// Expose DefaultSearchService as ol.service.DefaultSearchService (for a build bundle) +if (window.ol) { + if (!window.ol.service) { + window.ol.service = {}; + } + window.ol.service.DefaultSearchService = DefaultSearchService; +} diff --git a/src/packages/Controls/SearchEngine/Service.js b/src/packages/Services/IGNSearchService.js similarity index 62% rename from src/packages/Controls/SearchEngine/Service.js rename to src/packages/Services/IGNSearchService.js index 7dfe9adde..927e05f42 100644 --- a/src/packages/Controls/SearchEngine/Service.js +++ b/src/packages/Services/IGNSearchService.js @@ -1,357 +1,34 @@ -// import CSS -import "../../CSS/Controls/SearchEngine/GPFsearchEngine.css"; import GeoJSON from "ol/format/GeoJSON"; -import Logger from "../../Utils/LoggerByDefault"; +import Logger from "../Utils/LoggerByDefault"; +import AbstractSearchService from "./AbstractSearchService"; // import geoportal library access import Gp from "geoportal-access-lib"; // import local -import Utils from "../../Utils/Helper"; -import GeocodeUtils from "../../Utils/GeocodeUtils"; +import Utils from "../Utils/Helper"; +import GeocodeUtils from "../Utils/GeocodeUtils"; // Service -import Search from "../../Services/Search"; +import Search from "./Search"; import Feature from "ol/Feature.js"; -import BaseObject from "ol/Object"; import Point from "ol/geom/Point.js"; +import { canvasPool } from "ol/renderer/canvas/Layer"; var logger = Logger.getLogger("searchengine"); -/** - * Options de construction d'un service - * @typedef {Object} AbstractSearchServiceOptions - * @property {String} [apiKey] - Clé API utilisée pour les requêtes vers les services IGN - * @property {Boolean} [ssl=true] - Force l'utilisation du protocole HTTPS si défini à true - * @property {AutocompleteOptions} [autocompleteOptions] - Options spécifiques à l'autocomplétion - * @property {SearchOptions} [searchOptions] - Options spécifiques à la recherche finale - * @property {GeocodeOptions} [geocodeOptions] - Options spécifiques au géocodage - * @property {boolean} [autocomplete=true] - * @property {String} [index="address,poi"] - * @property {Number} [limit=1] - * @property {boolean} [returnTrueGeometry=false] - */ - -/** - * Options pour l'autocomplétion - * @typedef {Object} AutocompleteOptions - * @property {Object} [serviceOptions] - Options passées à Gp.Services.autoComplete - * @property {Number} [maximumResponses] - Nombre maximal de réponses retournées - * @property {Boolean} [triggerGeocode=false] - Si vrai, déclenche une requête de géocodage lorsque l'autocomplétion échoue - * @property {Number} [triggerDelay=1000] - Délai (ms) avant déclenchement du géocodage automatique - * @property {Boolean} [prettifyResults=false] - Si vrai, embellit/filtre les résultats - */ - -/** - * Options pour la recherche finale (géocodage) - * @typedef {Object} SearchOptions - * @property {Object} [serviceOptions] - Options passées à Gp.Services.geocode - * @property {Number} [maximumResponses] - Nombre maximal de réponses - * @property {Boolean} [filterLayers] - Active le filtrage des résultats par couche - * @property {String|Array} [index] - Indexs utilisés (e.g. "address,poi") - * @property {Number} [limit] - Limite de résultats - */ - -/** - * Options pour le géocodage (appel manuel de coordonnées via texte) - * @typedef {Object} GeocodeOptions - * @property {Object} [serviceOptions] - Options passées à Gp.Services.geocode - * @property {String} [location] - Texte à géocoder - * @property {Function} [onSuccess] - Callback exécuté en cas de succès - * @property {Function} [onFailure] - Callback exécuté en cas d'échec - */ - -/** - * Résultat d'une autocomplétion - * @typedef {Object} AutocompleteResult - * @property {String} fullText - Libellé affichable du lieu - * @property {Object} position - Coordonnées - * @property {Number} position.x - Longitude - * @property {Number} position.y - Latitude - * @property {String} [type] - Type de résultat (e.g. "StreetAddress", "PositionOfInterest") - * @property {Array} [poiType] - Types détaillés (e.g. ["administratif","région"]) - */ - -/** - * Résultat d'une recherche (géocodage final) - * @typedef {Object} SearchResult - * @property {import("ol/Feature").default} feature - Feature OL contenant la géométrie - * @property {import("ol/Feature").default|undefined} [extent] - Étendue si zone géographique - * @property {String} [infoPopup] - Texte à afficher dans un popup - */ - - -/** - * @classdesc - * AbstractSearchService control - * - * @alias ol.control.AbstractSearchService - * @abstract - * @module SearchService -*/ -class AbstractSearchService extends BaseObject { - - /** - * @constructor - * @param {AbstractSearchServiceOptions} options - */ - constructor (options) { - options = options || {}; - - // call ol.control.Control constructor - super(options); - - if ((this.constructor == AbstractSearchService)) { - throw new TypeError("AbstractSearchService cannot be instantiate"); - } - /** - * Nom de la classe (heritage) - * @private - */ - this.CLASSNAME = "AbstractSearchService"; - - // initialisation du composant - this.initialize(options); - - return this; - } - - /**= - * @param {AbstractSearchServiceOptions} options - */ - initialize (options) { - this.AUTOCOMPLETE_EVENT = "autocomplete"; - this.SEARCH_EVENT = "search"; - - this._autocompleteLocations = []; - this._locations = []; - - if (options.autocomplete === false) { - this.set("autocomplete", false); - } - this.set("index", options.index || "address,poi"); - this.set("limit", typeof options.limit === "number" ? options.limit : 1); - this.set("returnTrueGeometry", !!options.returnTrueGeometry); - } - - /** - * Récupère le résultat d'une recherche d'autocomplétion. - * @param {Number} [index] Optionnel. Index du résultat. Si nul, renvoie tous les résultats - * @returns {Array|AutocompleteResult} - */ - getAutocompleteLocations (index) { - if (index === undefined) { - return this._autocompleteLocations; - } else { - return this._autocompleteLocations[index]; - } - } - - /** - * Récupère le résultat d'une recherche de kieu (recherche finale). - * @param {Number} [index] Optionnel. Index du résultat. Si nul, renvoie tous les résultats - * @returns {Array|SearchResult} - */ - getResult (index) { - if (index === undefined) { - return this._locations; - } else { - return this._locations[index]; - } - } - - - /** - * @param {AutocompleteOptions} obj - * @abstract - */ - autocomplete (obj) { } - - - /** - * @param {SearchOptions} obj - * @abstract - */ - search (obj) { } - - - /** - * @param {SearchOptions} obj - * @abstract - * @returns {String} - */ - getItemTitle (obj) { - return obj; - } - -} - - -/** - * @classdesc - * DefaultSearchService control - * - * @alias ol.control.DefaultSearchService - * @module SearchService -*/ -class DefaultSearchService extends AbstractSearchService { - - constructor (options) { - options = options || {}; - super(options); - /** - * Nom de la classe (heritage) - * @private - */ - this.CLASSNAME = "DefaultSearchService"; - if (options.searchTab) { - this._searchTab = options.searchTab || []; - }; - } - - /** Autocomplete function - * Dispatchs "searchstart" event when search starts - * Dispatchs "search" event when search is finished - * @param {String} value Valeur de l'autocomplete - * @param {Object} [options] - * @param {String} options.force force search even if search string is less than minChars / enter is pressed - * @api - */ - autocomplete (value) { - // Simulate asynchronous behavior - this._autocompleteLocations = []; - const rex = new RegExp(value, "i"); - (this._searchTab || []).forEach((city) => { - if (rex.test(city.toLowerCase())) { - this._autocompleteLocations.push(city); - } - }); - // When search is finished - this.dispatchEvent({ - type : this.AUTOCOMPLETE_EVENT, - result : this._autocompleteLocations - }); - } - - /** - * @param {SearchOptions} obj Search options - */ - search (obj) { - this.dispatchEvent({ - type : this.SEARCH_EVENT, - result : obj - }); - } - -} - - -/** - * @classdesc - * DefaultSearchService control - * - * @alias ol.control.DefaultSearchService - * @module SearchService -*/ -class InseeSearchService extends AbstractSearchService { - - constructor (options) { - options = options || {}; - // Aucune autocomplétion - options.autocomplete = false; - super(options); - /** - * Nom de la classe (heritage) - * @private - */ - this.CLASSNAME = "InseeSearchService"; - - this.ignService = new IGNSearchService({ - autocomplete : false, - returnTrueGeometry : true, - index : "poi", - }); - - this.ignService.on(this.SEARCH_EVENT, this._onSearch.bind(this)); - } - - /** - * Pas de service d'autocomplétion pour l'API géo - */ - autocomplete () { - return; - } - - /** - * @param {Object} object Code insee - * @param {String} object.location Code insee - */ - search (object) { - const insee = object.location; - // Envoi la requête si le chiffre est compris entre 0 et 99999 - const response = this._requestGeoAPI({ value : insee }); - response.then(r => { - if (r instanceof Array && r.length) { - const result = r[0]; - - let location = result.nom; - // Sinon la requête ne se lancera pas - if (result.nom.length < 3) { - location = `${result.nom}, ${result.codesPostaux[0]}`; - } - - let filters = { - category : "administratif", - citycode : result.code - }; - - this.ignService.search(location, filters); - } - }); - } - - _onSearch (e) { - this.dispatchEvent(e); - } - - /** - * - * @param {Object} settings - */ - async _requestGeoAPI (settings) { - const baseURL = "https://geo.api.gouv.fr/communes"; - const format = "json"; - const fields = ["nom", "code", "codesPostaux"]; - const url = `${baseURL}?code=${settings.value}&format=${format}&fields=${fields}`; - - try { - const response = await fetch(url, { - headers : { - "Content-Type" : "application/json", - }, - }); - if (!response.ok) { - throw new Error(`Response status: ${response.status}`); - } - - const result = await response.json(); - return result; - } catch (error) { - console.error(error.message); - } - } - -} /** * @classdesc - * IGNSearchService control + * Service de recherche IGN (utilise les services IGN / Search wrapper). * - * @alias ol.control.IGNSearchService + * @alias ol.service.IGNSearchService * @module SearchService -*/ + * @extends AbstractSearchService + */ class IGNSearchService extends AbstractSearchService { /** + * Constructeur du service IGN. * @constructor - * @param {AbstractSearchServiceOptions} options options + * @param {AbstractSearchServiceOptions} options Options du service IGN (clé API, index, etc.) */ constructor (options) { options = options || {}; @@ -371,6 +48,12 @@ class IGNSearchService extends AbstractSearchService { return this; } + /** + * Initialise le service avec les options fournies. + * @protected + * @override + * @param {AbstractSearchServiceOptions} options Options de configuration du service + */ initialize (options) { super.initialize(options); @@ -444,16 +127,25 @@ class IGNSearchService extends AbstractSearchService { this._fillSearchedSuggestListContainer(suggestResults); }); - this._currentGeocodingLocation = null; - this._suggestedLocations = []; + /** + * Label du géocodage / de la recherche + * @type {String} + */ + this._currentGeocodingLocation; + /** + * Liste de résultats d'autocomplétion + * @type {Array} + */ + this._suggestedLocations; console.log(this.options); } /** - * @param {String} value Valeur de l'autocomplete - * @abstract + * Lance une autocomplétion via le service IGN. + * @override + * @param {String} value Texte à envoyer */ autocomplete (value) { if (!value) { @@ -463,38 +155,30 @@ class IGNSearchService extends AbstractSearchService { // on sauvegarde le localisant this._currentGeocodingLocation = value; - // // on limite les requêtes à partir de 3 car. saisie ! - // if (value.length < 3) { - // this._clearResults(); - // return; - // } - - // INFORMATION - // on effectue la requête au service d'autocompletion. - // on met en place des callbacks afin de recuperer les resultats ou - // les messages d'erreurs du service. - // les resultats sont affichés dans une liste deroulante. + // On effectue la requête au service d'autocompletion. this._requestAutoComplete({ text : value, - // callback onSuccess onSuccess : this._onSuccessAutoComplete.bind(this), - // callback onFailure onFailure : this._onFailureAutoComplete.bind(this) }); } + /** + * @override + * @param {AutocompleteResult} obj Objet dont le titre dérive + * @returns {String} Titre à afficher + */ getItemTitle (obj) { return obj.fullText; } /** - * Éxécute une requête au service. - * - * @param {Object} settings - service settings - * @param {String} settings.text - text - * @param {Function} settings.onSuccess - callback - * @param {Function} settings.onFailure - callback + * Exécute une requête d'autocomplétion auprès du service IGN. * @private + * @param {Object} settings Paramètres de la requête (texte, callbacks, etc.) + * @param {String} settings.text Texte à compléter + * @param {Function} settings.onSuccess Callback en cas de succès + * @param {Function} settings.onFailure Callback en cas d'échec */ _requestAutoComplete (settings) { // on ne fait pas de requête si on n'a pas renseigné de parametres ! @@ -533,8 +217,12 @@ class IGNSearchService extends AbstractSearchService { Gp.Services.autoComplete(options); } + /** + * Fonction appelée en cas de succès de l'autocomplétion. + * @param {Object} results Résultats de la requête. + * @param {Array} results.suggestedLocations Tableau de suggestions. + */ _onSuccessAutoComplete (results) { - console.log("_onSuccessAutoComplete"); let _maximumEntries = this.options.autocompleteOptions.maximumEntries; let _prettifyResults = this.options.autocompleteOptions.prettifyResults; @@ -577,8 +265,11 @@ class IGNSearchService extends AbstractSearchService { } } + /** + * Fonction appelée en cas d'erreur sur le service d'autocomplétion + * @param {ErrorService} error Erreur renvoyée par le service + */ _onFailureAutoComplete (error) { - console.log("_onFailureAutoComplete"); let _triggerGeocode = this.options.autocompleteOptions.triggerGeocode; let _triggerDelay = this.options.autocompleteOptions.triggerDelay; @@ -587,7 +278,7 @@ class IGNSearchService extends AbstractSearchService { if (results) { this._clearResults(); // on modifie la structure des reponses pour être - // compatible avec l'autocompletion ! + // compatible avec l'autocomplétion ! let locations = results.locations; for (let i = 0; i < locations.length; i++) { let location = locations[i]; @@ -609,7 +300,7 @@ class IGNSearchService extends AbstractSearchService { // où affiche t on les messages : ex. 'No suggestion matching the search' ? this._clearResults(); logger.log(error.message); - // on envoie une requete de geocodage si aucun resultat d'autocompletion + // on envoie une requete de geocodage si aucun resultat d'autocomplétion // n'a été trouvé ! Et on n'oublie pas d'annuler celle qui est en cours ! if (error.message === "No suggestion matching the search" && _triggerGeocode /* && value.length === 5 */) { if (this._triggerHandler) { @@ -707,23 +398,14 @@ class IGNSearchService extends AbstractSearchService { } /** - * this method is called by event 'click' on 'GPautoCompleteResultsList' tag div - * (cf. this._createAutoCompleteListElement), and it selects the location. - * this location displays a marker on the map. - * @param {Object} location Objet de la recherche + * Lance une recherche sur les services de géocodage de l'IGN + * @see {@link https://data.geopf.fr/geocodage/search} + * @param {IGNSearchObject} object Recherche * @abstract */ - search (location, filters = {}) { - // TODO on souhaite un comportement different pour la selection des reponses - // de l'autocompletion : - // - liste deroulante des reponses, - // - puis possibilité de cliquer sur une suggestion - // - mais aussi de la choisir avec le clavier (arrow up/down), puis valider - // par un return - // cette selection avec les fleches doit mettre à jour le input ! - // (comme un moteur de recherche de navigateur) - - // var idx = SelectorID.index(e.target.id); + search (object) { + const location = object.location; + const filters = object.filters; if (location === undefined) { return; @@ -741,8 +423,8 @@ class IGNSearchService extends AbstractSearchService { // on centre la vue et positionne le marker, à la position reprojetée dans la projection de la carte this._requestGeocoding({ - index : this.get("index") || "address,poi", - limit : this.get("limit") || 1, + index : this.get("index"), + limit : this.get("limit"), returnTrueGeometry : this.get("returnTrueGeometry"), location : label, filters : filters, @@ -779,7 +461,6 @@ class IGNSearchService extends AbstractSearchService { // on redefinie les callbacks si les callbacks de service existent var bOnSuccess = !!(this.options.geocodeOptions.serviceOptions.onSuccess !== null && typeof this.options.geocodeOptions.serviceOptions.onSuccess === "function"); if (bOnSuccess) { - console.log("bonSuccess"); var cbOnSuccess = function (e) { settings.onSuccess.bind(this, e); this.options.geocodeOptions.serviceOptions.onSuccess.bind(this, e); @@ -789,7 +470,6 @@ class IGNSearchService extends AbstractSearchService { var bOnFailure = !!(this.options.geocodeOptions.serviceOptions.onFailure !== null && typeof this.options.geocodeOptions.serviceOptions.onFailure === "function"); if (bOnFailure) { - console.log("bonFailure"); var cbOnFailure = function (e) { settings.onFailure.bind(this, e); this.options.geocodeOptions.serviceOptions.onFailure.bind(this, e); @@ -861,7 +541,10 @@ class IGNSearchService extends AbstractSearchService { return { feature : f, extent : extent }; } - /** Do something on search + /** + * Fonction appelée en cas de succès du géocodage + * + * @param {Object} results Résultats de la recherche * @private */ _onSuccessSearch (results) { @@ -889,6 +572,11 @@ class IGNSearchService extends AbstractSearchService { }); } + /** + * Fonction appelée en cas d'erreur renvoyée par le service de géocodage. + * @param {AutocompleteResult} location Localisation de l'autocomplétion. + * @param {ErrorService} error Erreur renvoyée par le service. + */ _onFailureSearch (location, error) { logger.warn(error); @@ -897,19 +585,7 @@ class IGNSearchService extends AbstractSearchService { location.position.y ]; - /** - * event triggered when an element of the results is clicked for autocompletion - * - * @event searchengine:autocomplete:click - * @property {Object} type - event - * @property {Object} location - location - * @property {Object} target - instance SearchEngine - * @example - * SearchEngine.on("searchengine:autocomplete:click", function (e) { - * console.log(e.location); - * }) - */ - + // Créé le point const geom = new Point(position); let f = new Feature({ geometry : geom }); f.set("infoPopup", this._currentGeocodingLocation); @@ -922,17 +598,12 @@ class IGNSearchService extends AbstractSearchService { } -export { AbstractSearchService, DefaultSearchService, InseeSearchService }; - export default IGNSearchService; -// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) +// Expose IGNSearchService as ol.service.IGNSearchService (for a build bundle) if (window.ol) { if (!window.ol.service) { window.ol.service = {}; } - window.ol.service.AbstractSearchService = AbstractSearchService; - window.ol.service.DefaultSearchService = DefaultSearchService; - window.ol.service.InseeSearchService = InseeSearchService; window.ol.service.IGNSearchService = IGNSearchService; } diff --git a/src/packages/Services/InseeSearchService.js b/src/packages/Services/InseeSearchService.js new file mode 100644 index 000000000..9b28bf948 --- /dev/null +++ b/src/packages/Services/InseeSearchService.js @@ -0,0 +1,141 @@ +import Logger from "../Utils/LoggerByDefault"; +import AbstractSearchService from "./AbstractSearchService"; +import IGNSearchService from "./IGNSearchService"; + +var logger = Logger.getLogger("searchengine"); + + +/** + * @classdesc + * Service de recherche pour les codes INSEE : interroge l'API geo.api.gouv.fr puis relaie vers IGNSearchService. + * @see {@link https://geo.api.gouv.fr/} + * + * @alias ol.service.InseeSearchService + * @module SearchService + */ +class InseeSearchService extends AbstractSearchService { + + /** + * Constructeur du service INSEE. + * @constructor + * @param {AbstractSearchServiceOptions} options Options du service INSEE + */ + constructor (options) { + options = options || {}; + // Aucune autocomplétion + options.autocomplete = false; + super(options); + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "InseeSearchService"; + + /** + * Service IGN utilisé pour relayer la recherche. + * @type {IGNSearchService} + * @private + */ + this.ignService = new IGNSearchService({ + autocomplete : false, + returnTrueGeometry : true, + index : "poi", + }); + + this.ignService.on(this.SEARCH_EVENT, this._onSearch.bind(this)); + } + + /** + * Désactive l'autocomplétion (non disponible pour INSEE). + * @override + */ + autocomplete () { + return; + } + + /** + * Lance une recherche par code INSEE. + * @override + * @param {Object} object Objet contenant le code INSEE + * @param {String} object.location Code INSEE à rechercher + */ + search (object) { + const insee = object.location; + // Envoi la requête si le chiffre est compris entre 0 et 99999 + const response = this._requestGeoAPI({ value : insee }); + response.then(r => { + if (r instanceof Array && r.length) { + const result = r[0]; + + let location = result.nom; + // Sinon la requête ne se lancera pas + if (result.nom.length < 3) { + location = `${result.nom}, ${result.codesPostaux[0]}`; + } + + let filters = { + category : "administratif", + citycode : result.code + }; + + const obj = { + location : location, + filters : filters, + }; + + this.ignService.search(obj); + } + }); + } + + /** + * Relaye l'événement de recherche du service IGN. + * @private + * @param {Event} e Événement de recherche + */ + _onSearch (e) { + this.dispatchEvent(e); + } + + /** + * Interroge l'API geo.api.gouv.fr pour obtenir les informations du code INSEE. + * @see {@link https://geo.api.gouv.fr/} + * @private + * @param {Object} settings Paramètres de requêtes + * @param {String} settings.value Code INSEE à interroger + * @returns {Promise} Résultat de l'API + */ + async _requestGeoAPI (settings) { + const baseURL = "https://geo.api.gouv.fr/communes"; + const format = "json"; + const fields = ["nom", "code", "codesPostaux"]; + const url = `${baseURL}?code=${settings.value}&format=${format}&fields=${fields}`; + + try { + const response = await fetch(url, { + headers : { + "Content-Type" : "application/json", + }, + }); + if (!response.ok) { + throw new Error(`Response status: ${response.status}`); + } + + const result = await response.json(); + return result; + } catch (error) { + console.error(error.message); + } + } + +} + +export default InseeSearchService; + +// Expose InseeSearchService as ol.service.InseeSearchService (for a build bundle) +if (window.ol) { + if (!window.ol.service) { + window.ol.service = {}; + } + window.ol.service.InseeSearchService = InseeSearchService; +} diff --git a/src/packages/Services/SearchServiceBase.js b/src/packages/Services/SearchServiceBase.js deleted file mode 100644 index ec58c376b..000000000 --- a/src/packages/Services/SearchServiceBase.js +++ /dev/null @@ -1,74 +0,0 @@ -import OlObject from "ol/Object"; - -/** Base class for search services - * - */ -class SearchServiceBase extends OlObject { - - constructor (options) { - super(); - options = options || {}; - if (options.searchTab) { - this._searchTab = options.searchTab || []; - }; - } - - /** Autocomplete function - * Dispatchs "searchstart" event when search starts - * Dispatchs "autocomplete" event when finished - * @param {String} search - * @param {Object} [options] - * @param {String} options.force force search even if search string is less than minChars / enter is pressed - * @api - */ - _search (search, options, what) { - // Search has started - this.dispatchEvent({ - type : "searchstart", - search : search, - options : options, - }); - // Simulate asynchronous behavior - setTimeout(function () { - const result = []; - const rex = new RegExp(search, "i"); - (this._searchTab || []).forEach((city) => { - if (rex.test(city.toLowerCase())) { - result.push(city); - } - }); - // When search is finished - this.dispatchEvent({ - type : what, - search : search, - options : options, - result : result - }); - }.bind(this), 200); - } - autocomplete (search, options) { - this._search(search, options, "autocomplete"); - } - search (search, options) { - this._search(search, options, "search"); - } - - /** Get title of an item - * @param {*} item - * @returns {String} title - */ - getItemTitle (item) { - return item; - } - -} - -export default SearchServiceBase; - -// Expose SearchEngine as ol.control.SearchEngine (for a build bundle) -if (window.ol) { - if (!window.ol.service) { - window.ol.service = {}; - } - window.ol.service.SearchServiceBase = SearchServiceBase; -} diff --git a/src/packages/Services/typedefs.js b/src/packages/Services/typedefs.js new file mode 100644 index 000000000..f9744fb31 --- /dev/null +++ b/src/packages/Services/typedefs.js @@ -0,0 +1,118 @@ +/** + * Typedefs partagés pour les Services SearchService. + * Ce fichier centralise les types réutilisables afin que typedoc génère des docs cohérentes. + */ + +/** + * Options de construction d'un service de recherche. + * @typedef {Object} AbstractSearchServiceOptions + * @property {String} [apiKey] - Clé API pour les services IGN. + * @property {Boolean} [ssl=true] - Forcer HTTPS. + * @property {AutocompleteOptions} [autocompleteOptions] - Options de l'autocomplétion. + * @property {SearchOptions} [searchOptions] - Options de la recherche finale. + * @property {GeocodeOptions} [geocodeOptions] - Options de géocodage. + * @property {Boolean} [autocomplete=true] + * @property {String} [index="address,poi"] + * @property {Number} [limit=1] + * @property {Boolean} [returnTrueGeometry=false] + */ + +/** + * Options pour l'autocomplétion. + * @typedef {Object} AutocompleteOptions + * @property {Object} [serviceOptions] - Options passées à Gp.Services.autoComplete + * @property {Number} [maximumResponses] - Nombre maximal de réponses retournées + * @property {Boolean} [triggerGeocode=false] - Si vrai, déclenche une requête de géocodage lorsque l'autocomplétion échoue + * @property {Number} [triggerDelay=1000] - Délai (ms) avant déclenchement du géocodage automatique + * @property {Boolean} [prettifyResults=false] - Si vrai, embellit/filtre les résultats + */ + + +/** + * Résultat d'une autocomplétion (voir {@link https://ignf.github.io/geoportal-access-lib/latest/jsdoc/Gp.Services.AutoComplete.SuggestedLocation.html}) + * @typedef {Object} AutocompleteResult + * @property {"StreetAddress"|"PositionOfInterest"} type - Type de suggestion. + * @property {Position} position - Coordonnées du point, dans le système de coordonnées spécifiées. + * @property {String} commune - Nom de la commune. + * @property {String} fullText - Texte complet représentant la suggestion. + * @property {String} postalCode - Code postal de la suggestion. + * @property {Number} classification - Nombre utilisé pour classigier l'importance de l'endroit suggéré de 1 (plus important) à 7 (moins important) + * @property {Array} [poiType] - Types POI détaillés. + * @property {String} [street] - Nom de la rue (types "StreetAddress" seulement). + * @property {String} [kind] - Nature du point d'intérêt, e.g. "prefecture", "municipality"... (types "PositionOfInterest" seulement). + */ + + +/** + * Objet envoyé pour le géocodage + * @typedef {Object} IGNSearchObject + * @property {AutocompleteResult|String} location - Objet utilisé pour faire la requête. + * Peut être soit le résultat de l'autocomplétion ou une chaîne de caractère. + * @property {IGNSearchFilter} [filter] - Filtres optionnels pour + */ + +/** + * Filtres optionnels pour le géocodage. + * Les filtres dépendent du type de recherche : adresses, toponymes ou parcelles cadastrales + * @typedef {Object} IGNSearchFilter + * @property {string} [postalCode] - Code postal (adresses, toponymes). + * @property {string} [inseeCode] - Code INSEE (adresses, toponymes). + * @property {string} [city] - Nom de la ville (adresses uniquement). + * @property {string} [type] - Type de toponyme (toponymes uniquement). + * @property {string} [codeDepartement] - Code département (parcelles cadastrales uniquement). + * @property {string} [codeCommune] - Code commune (parcelles cadastrales uniquement). + * @property {string} [nomCommune] - Nom de la commune (parcelles cadastrales uniquement). + * @property {string} [codeCommuneAbs] - Code commune abs (parcelles cadastrales uniquement). + * @property {string} [codeArrondissement] - Code arrondissement (parcelles cadastrales uniquement). + * @property {string} [section] - Section cadastrale (parcelles cadastrales uniquement). + * @property {string} [numero] - Numéro de parcelle (parcelles cadastrales uniquement). + * @property {string} [feuille] - Feuille cadastrale (parcelles cadastrales uniquement). + */ + + + +/** + * Position dans un système de coordonnées. + * @typedef {Object} Position + * @property {Number} x - Longitude. + * @property {Number} y - Latitude. + */ + + +/** + * Erreur du service (voir {@link https://ignf.github.io/geoportal-access-lib/latest/jsdoc/Gp.Error.html}) + * @typedef {Object} ErrorService + * @property {string} message - Message d'erreur retourné par le service. + * @property {number} status - Code de statut (-1 si inconnu). + * @property {string} type - Type d'erreur (ex. "UNKNOWN_ERROR"). + * @property {string} name - Nom de l'erreur (ex. "ErrorService"). + * @property {string} [stack] - Stack trace de l'erreur, si disponible. + */ + +/** + * Options pour la recherche finale (géocodage). + * @typedef {Object} SearchOptions + * @property {Object} [serviceOptions] - Options passées à Gp.Services.geocode. + * @property {Number} [maximumResponses] - Nombre maximal de réponses. + * @property {Boolean} [filterLayers] - Active le filtrage des résultats par couche. + * @property {String|Array} [index] - Indexs utilisés (ex. "address,poi"). + * @property {Number} [limit] - Limite de résultats. + */ + +/** + * Options pour le géocodage manuel. + * @typedef {Object} GeocodeOptions + * @property {Object} [serviceOptions] - Options passées à Gp.Services.geocode. + * @property {String} [location] - Texte à géocoder. + * @property {Function} [onSuccess] - Callback en cas de succès. + * @property {Function} [onFailure] - Callback en cas d'échec. + */ + +/** + * Résultat d'une recherche (géocodage final). + * @typedef {Object} SearchResult + * @property {import("ol/Feature").default} feature - Feature OL contenant la géométrie. + * @property {import("ol/Feature").default|undefined} [extent] - Étendue si zone géographique. + * @property {String} [infoPopup] - Texte à afficher dans un popup. + */ + From c45f31db180c4b97c5227670c46df2e896befdf2 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 24 Oct 2025 11:55:44 +0200 Subject: [PATCH 39/73] fix(search): Chgt index.js pour ajout services --- src/index.js | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/index.js b/src/index.js index dc71a2843..04b0e20c9 100644 --- a/src/index.js +++ b/src/index.js @@ -31,7 +31,6 @@ export { default as AbstractAdvancedSearch } from "./packages/Controls/SearchEng export { default as CoordinateAdvancedSearch } from "./packages/Controls/SearchEngine/CoordinateAdvancedSearch"; export { default as InseeAdvancedSearch } from "./packages/Controls/SearchEngine/InseeAdvancedSearch"; export { default as LocationAdvancedSearch } from "./packages/Controls/SearchEngine/LocationAdvancedSearch"; -export { default as IGNSearchService } from "./packages/Controls/SearchEngine/Service"; export { default as MousePosition } from "./packages/Controls/MousePosition/MousePosition"; export { default as Drawing } from "./packages/Controls/Drawing/Drawing"; export { default as Route } from "./packages/Controls/Route/Route"; @@ -56,6 +55,12 @@ export { default as ControlList } from "./packages/Controls/ControlList/ControlL export { default as ContextMenu } from "./packages/Controls/ContextMenu/ContextMenu"; export { default as Reporting } from "./packages/Controls/Reporting/Reporting"; +// Services +export { default as AbstractSearchService } from "./packages/Services/AbstractSearchService"; +export { default as DefaultSearchService } from "./packages/Services/DefaultSearchService"; +export { default as IGNSearchService } from "./packages/Services/IGNSearchService"; +export { default as InseeSearchService } from "./packages/Services/InseeSearchService"; + // proj4 export { default as Proj4 } from "proj4"; From 68ac9ff6b75591b06af6c816690b04b236ced053 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 24 Oct 2025 15:24:54 +0200 Subject: [PATCH 40/73] =?UTF-8?q?feat(search):=20Ajout=20btns=20custom=20p?= =?UTF-8?q?opup=20+=20gere=20loc=20avancee=20+=20modif=20rech.=20av.=20lie?= =?UTF-8?q?ux=20G=C3=A8re=20cas=20o=C3=B9=20l'on=20ne=20met=20pas=20de=20r?= =?UTF-8?q?echerche=20avanc=C3=A9e=20dans=20le=20constructeur=20/=20Modif?= =?UTF-8?q?=20label=20r=C3=A9sultats=20lieux=20et=20toponymes=20pour=20ajo?= =?UTF-8?q?uter=20code=20postal=20/=20Ajout=20bouton=20customis=C3=A9s=20d?= =?UTF-8?q?ans=20le=20popup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ginebase-modules-dsfr-geocodeAdvanced.html | 21 +++++- .../DSFRadvancedSearchEngineStyle.css | 12 ++++ .../img/dsfr/map-pin-add-fill.svg | 1 + .../img/dsfr/map-pin-add-line.svg | 1 + .../SearchEngine/LocationAdvancedSearch.js | 5 +- .../SearchEngine/SearchEngineAdvanced.js | 65 +++++++++++++++---- .../Controls/SearchEngine/typedefs.js | 36 ++++++++++ 7 files changed, 126 insertions(+), 15 deletions(-) create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index b1cca91f5..53398d06d 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -41,6 +41,10 @@

    Ajout du moteur de recherche avec les options par défaut

    // on cache l'image de chargement du Géoportail. document.getElementById('map').style.backgroundImage = 'none'; + let source = new ol.source.Vector({}) + let layer = new ol.layer.Vector({ + source: source + }) // 1. Création de la map map = new ol.Map({ target : "map", @@ -53,7 +57,8 @@

    Ajout du moteur de recherche avec les options par défaut

    source: new ol.source.OSM(), // zIndex : 4, opacity: 0.5 - }) + }), + layer ] }); @@ -66,10 +71,24 @@

    Ajout du moteur de recherche avec les options par défaut

    var coordinates = new ol.control.CoordinateAdvancedSearch({ }) + let addFeatureToLayer = function (feature) { + console.log(this, feature); + source.addFeature(feature.clone()); + } + // 2. Appel du SearchEngine var search = new ol.control.SearchEngineAdvanced({ advancedSearch : [insee, location, coordinates], returnTrueGeometry : true, + popupButtons : [{ + label : "Ajouter l'objet à la couche", + className : "custom-button", + icon : "fr-icon-map-pin-add-line", + attributes : { + "data-action" : "add-feature", + }, + onClick : addFeatureToLayer + }] }); // 3. Ajout du SearchEngine à la carte diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index f086038a3..900a31f47 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -86,4 +86,16 @@ div[id^=GPsearchEngine-AdvancedContainer-] .fr-accordion .fr-collapse { margin: 0 -0.25rem; padding-left: 1rem; padding-right: 1rem; +} + +.fr-icon-map-pin-add-line::before, +.fr-icon-map-pin-add-line::after { + -webkit-mask-image: url("./img/dsfr/map-pin-add-line.svg"); + mask-image: url("./img/dsfr/map-pin-add-line.svg"); +} + +.fr-icon-map-pin-add-fill::before, +.fr-icon-map-pin-add-fill::after { + -webkit-mask-image: url("./img/dsfr/map-pin-add-fill.svg"); + mask-image: url("./img/dsfr/map-pin-add-fill.svg"); } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg new file mode 100644 index 000000000..13e803d55 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg new file mode 100644 index 000000000..4b9c407c7 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/map-pin-add-line.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index d215a33d6..d46d89953 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -115,6 +115,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { handleMultipleResults (e) { const results = this.searchService.getResult(); results.forEach((result, i) => { + console.log(result); const attr = result.placeAttributes; const li = document.createElement("li"); li.className = "search-result-item" + (i>=5 ? " hidden" : ""); @@ -124,7 +125,9 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { li.appendChild(a); a.className = "fr-icon-map-pin-2-line"; a.href = "#"; - a.title = a.innerText = attr.toponym + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); + const postCode = attr.postcode[0] ? `, ${attr.postcode[0]}` : ""; + const text = attr.toponym + postCode + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); + a.title = a.innerText = text; li.addEventListener("click", () => a.click()); a.addEventListener("click", e => { e.preventDefault(); diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 6e0c0690d..5757e723b 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -72,8 +72,7 @@ class SearchEngineAdvanced extends Control { /** * Constructeur du contrôle de recherche avancée. - * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. - * @param {AbstractAdvancedSearch[]} options.advancedSearch - Recherches avancées. + * @param {SearchEngineAdvancedOptions} options - Options du constructeur. */ constructor (options) { options = options || {}; @@ -107,20 +106,20 @@ class SearchEngineAdvanced extends Control { style : createStyle, }); - this.selectInteraction.on("select", this._onSelectElement.bind(this)); - this.popup = this._createPopup(); - - // Initialize this.initialize(options); this._initContainer(options); this._initEvents(options); + + options.popupButtons = options.popupButtons ? options.popupButtons : []; + + this.selectInteraction.on("select", this._onSelectElement.bind(this)); + this.popup = this._createPopup(options.popupButtons); } /** * Initialise les options du contrôle. - * @param {SearchEngineGeocodeIGNOptions} options - Options du constructeur. - * @param {AbstractAdvancedSearch[]} options.advancedSearch - Recherches avancées. + * @param {SearchEngineAdvancedOptions} options - Options du constructeur. * @private */ initialize (options) { @@ -180,7 +179,7 @@ class SearchEngineAdvanced extends Control { /** * Initialise les événements du contrôle (géolocalisation, navigation clavier, recherche). - * @param {SearchEngineGeocodeIGNOptions} options Options du constructeur. + * @param {SearchEngineAdvancedOptions} options Options du constructeur. * @private */ _initEvents (options) { @@ -206,9 +205,11 @@ class SearchEngineAdvanced extends Control { if (e.shiftKey) { // Retourne sur l'input this.baseSearchEngine.input.focus(); - } else { + } else if (this.advancedBtn.checkVisibility()) { // Focus sur le bouton de recherche avancée this.advancedBtn.focus(); + } else { + this.baseSearchEngine.subimtBt.focus(); } } }.bind(this)); @@ -244,7 +245,7 @@ class SearchEngineAdvanced extends Control { /** * Initialise le conteneur principal du contrôle et les sous-composants. - * @param {SearchEngineGeocodeIGNOptions} options Options du constructeur + * @param {SearchEngineAdvancedOptions} options Options du constructeur * @private */ _initContainer (options) { @@ -277,7 +278,6 @@ class SearchEngineAdvanced extends Control { advancedBtn.innerHTML = "Avancée"; advancedBtn.setAttribute("aria-label", "Afficher les options avancées"); advancedBtn.setAttribute("aria-expanded", "false"); - this.baseSearchEngine.optionscontainer.appendChild(advancedBtn); // Gestion de l'affichage des options avancées const advancedContainer = this.advancedContainer = document.createElement("div"); @@ -342,6 +342,11 @@ class SearchEngineAdvanced extends Control { advancedBtn.setAttribute("aria-expanded", isHidden); this.baseSearchEngine.setActive(isHidden); }); + + // N'ajoute pas le bouton s'il n'y a pas d'options avancées + if (this._searchForms.length) { + this.baseSearchEngine.optionscontainer.appendChild(advancedBtn); + } } /** @@ -409,9 +414,10 @@ class SearchEngineAdvanced extends Control { /** * Crée et retourne l'overlay popup pour afficher les infos de feature. * @private + * @param {PopupButton[]} popupButtons - Bouton à ajouter dans le popup (en plus de la suppression / fermeture). * @returns {Overlay} Overlay du popups */ - _createPopup () { + _createPopup (popupButtons) { // Popup global let element = this._popupDiv = document.createElement("div"); // TODO : ajouter gp-feature-info-div lorsque les deux seront pareils @@ -428,6 +434,10 @@ class SearchEngineAdvanced extends Control { popupBtns.appendChild(this._addCloseButton()); popupBtns.appendChild(this._addRemoveButton()); + popupButtons.forEach(popupBtn => { + popupBtns.appendChild(this._createCustomPopupButton(popupBtn)); + }); + element.appendChild(popupContent); element.appendChild(popupBtns); @@ -515,6 +525,35 @@ class SearchEngineAdvanced extends Control { } } + /** + * Crée un bouton personnalisé pour le popup. + * @param {PopupButton} popupButton - Configuration du bouton. + * @returns {HTMLButtonElement} Bouton HTML + */ + _createCustomPopupButton (popupButton) { + const btn = document.createElement("button"); + btn.title = btn.ariaLabel = popupButton.label; + btn.className = "GPButton fr-btn fr-btn--sm fr-btn--tertiary-no-outline "; + if (popupButton.className) { + btn.className += popupButton.className; + } + if (popupButton.icon) { + btn.classList.add(popupButton.icon); + } + if (popupButton.attributes) { + Object.entries(popupButton.attributes).forEach(([key, value]) => { + btn.setAttribute(key, value); + }); + } + btn.onclick = () => { + const feature = this.popup.get("feature"); + if (feature && typeof popupButton.onClick === "function") { + popupButton.onClick.call(this, feature); + } + }; + return btn; + } + /** * Crée le bouton de géolocalisation. * @returns {HTMLButtonElement} Bouton de géolocalisation diff --git a/src/packages/Controls/SearchEngine/typedefs.js b/src/packages/Controls/SearchEngine/typedefs.js index 9bb446790..056eb1900 100644 --- a/src/packages/Controls/SearchEngine/typedefs.js +++ b/src/packages/Controls/SearchEngine/typedefs.js @@ -127,3 +127,39 @@ * Étend SearchEngineBaseOptions et ajoute les options spécifiques du service IGN. * @typedef {AbstractAdvancedSearchOptions & {coordinateSearch?: CoordinateSearchOptions}} CoordinateAdvancedSearchOptions */ + +/** + * Options pour ajouter un bouton de popup + * @typedef {Object} PopupButton + * @property {string} label - Attribut title du bouton. + * @property {string} [className] - Classe(s) CSS à appliquer au bouton. + * @property {string} [icon] - Classe d'icône à ajouter (ex: "fr-icon-delete-line"). + * @property {Object.} [attributes] - Attributs HTML supplémentaires (clé/valeur). + * @property {PopupButtonClickCallback} onClick - Fonction appelée au clic sur le bouton. + */ + +/** + * Callback appelé lors du clic sur un bouton personnalisé du popup. + * @callback PopupButtonClickCallback + * @param {Feature} feature - La feature associée au popup. + * @this SearchEngineAdvanced + */ + +/** + * Options pour le contrôle SearchEngineAdvanced. + * @typedef {Object} SearchEngineAdvancedOptions + * @property {import("./AbstractAdvancedSearch").default[]} [advancedSearch] - Liste des recherches avancées à intégrer. + * @property {PopupButton[]} [popupButtons] - Boutons personnalisés à ajouter dans le popup. + * @property {SearchEngineGeocodeIGNOptions} [baseSearchOptions] - Options pour le moteur de recherche de base. + * @property {HTMLElement|String} [target] - Élément DOM ou sélecteur cible. + * @property {String} [title] - Titre du contrôle. + * @property {String} [label] - Label affiché. + * @property {String} [hint] - Texte d'aide. + * @property {Boolean} [search] - Comportement en tant que barre de recherche. + * @property {String} [ariaLabel] - Libellé ARIA. + * @property {String} [placeholder] - Placeholder de l'input. + * @property {Number} [minChars] - Nombre minimal de caractères pour autocomplétion. + * @property {Number} [maximumEntries] - Nombre maximal d'entrées affichées. + * @property {Boolean|String} [historic] - Gestion historique local (false|true|string). + * @property {import("../../Services/AbstractSearchService").default} [searchService] - Service de recherche. + */ \ No newline at end of file From 372a0643f85e6aca3316b8484e0cb463ae00d3db Mon Sep 17 00:00:00 2001 From: viglino Date: Tue, 28 Oct 2025 12:40:53 +0100 Subject: [PATCH 41/73] RFCT refactoring + gestion des erreurs --- .../SearchEngine/GPFadvancedSearchEngine.css | 11 +- .../SearchEngine/LocationAdvancedSearch.js | 295 +++++++++++++----- 2 files changed, 224 insertions(+), 82 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 26ebe8938..dddce6a37 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -35,10 +35,6 @@ form[id^=GPAdvancedForm-] > .GPFormFooter > button { align-items: center; } -form[id^=GPAdvancedForm-] input:invalid { - border-color: #d93025; -} - form[id^=GPAdvancedForm-] ul.search-result { list-style-type: none; padding: 0; @@ -75,3 +71,10 @@ form[id^=GPAdvancedForm-] ul.search-result .search-result-actions { form[id^=GPAdvancedForm-] ul.search-result .search-result-actions:hover { background-color: transparent; } +form[id^=GPAdvancedForm-] ul.search-result .fr-message--error { + color: var(--background-flat-warning); +} + +form[id^=GPAdvancedForm-] select option.option { + background-color: var(--background-action-low-blue-france); +} diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index d46d89953..96aa5e24c 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -12,9 +12,12 @@ import IGNSearchService from "../../Services/IGNSearchService"; class LocationAdvancedSearch extends AbstractAdvancedSearch { /** - * Constructeur du contrôle LocationAdvancedSearch. + * Constructeur * @constructor * @param {AbstractAdvancedSearchOptions} options Options du constructeur + * @param {String} [options.name="Lieux et toponymes"] Nom du contrôle + * @param {Array|Object} [options.typeList] Liste des types de lieux (catégories) ou objet clé/valeur avec tableau de sous-catégories + * @extends {ol.control.AbstractAdvancedSearch} */ constructor (options) { options = options || {}; @@ -30,6 +33,9 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { limit : 10, returnTrueGeometry : true }); + + // Prevent popup validation + this.element.setAttribute("novalidate", ""); } /** @@ -49,50 +55,65 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // Do something on search (when ready) setTimeout(() => { - this.searchService.on("search", e => { - if (!e.multi) { - this.searchResult.innerHTML = ""; - } - // Format output - if (e.nbResults === 1) { - const attr = e.attr || this.searchService.getResult(0).placeAttributes; - ["postcode","citycode","city","category"].forEach(field => { - attr[field] = attr[field] || []; - }); - const into = { - infoPopup : "" + attr.toponym + "
    " + - (attr.category ? ("" + (attr.category || []).join(", ") + "
    ") : "") + - (attr.postcode ? ("Code postal : " + (attr.postcode || []).join(", ") + "
    ") : "") , - toponyme : attr.toponym, - postcode : attr.postcode[0], - postcodes : attr.postcode.join(" - "), - insee : attr.citycode[0], - citycodes : attr.citycode.join(" - "), - city : attr.city[0], - citys : attr.city.join(" - "), - category : attr.category[0], - categories : attr.category.join(" - ") - }; - - if (e.result) { - e.result.setProperties(into); - } - if (e.extent) { - e.extent.setProperties(into); - } - this.dispatchEvent(e); - } else { - this.element.parentElement.parentElement.scrollTop = 0; - if (e.nbResults === 0) { - this.searchResult.innerHTML = "
  • Aucun résultat
  • " ; - } else { - this.handleMultipleResults(e); - } - } - }); + this.searchService.on("search", e => this.handleSearch(e)); }); } + /** + * Gère les résultats de la recherche. + * @private + * @param {Event} e Événement de recherche contenant les résultats + */ + handleSearch (e) { + // Clear previous results + if (!e.multi) { + this.searchResult.innerHTML = ""; + } + // Format output + if (e.nbResults === 1) { + const attr = e.attr || this.searchService.getResult(0).placeAttributes; + ["postcode","citycode","city","category"].forEach(field => { + attr[field] = attr[field] || []; + }); + const info = { + infoPopup : "" + attr.toponym + "
    " + + (attr.category ? ("" + (attr.category || []).join(", ") + "
    ") : "") + + (attr.postcode ? ("Code postal : " + (attr.postcode || []).join(", ") + "
    ") : "") , + toponyme : attr.toponym, + postcode : attr.postcode[0], + postcodes : attr.postcode.join(" - "), + insee : attr.citycode[0], + citycodes : attr.citycode.join(" - "), + city : attr.city[0], + citys : attr.city.join(" - "), + category : attr.category[0], + categories : attr.category.join(" - ") + }; + + if (e.result) { + e.result.setProperties(info); + } + if (e.extent) { + e.extent.setProperties(info); + } + this.dispatchEvent(e); + } else { + this.element.parentElement.parentElement.scrollTop = 0; + if (e.nbResults === 0) { + this.searchResult.innerHTML = ""; + const li = document.createElement("li"); + li.className = "fr-message--error"; + li.innerText = "Aucun résultat"; + this.searchResult.appendChild(li); + li.addEventListener("click", () => { + li.remove(); + }); + } else { + this.handleMultipleResults(e); + } + } + } + /** * @override * @protected @@ -105,6 +126,68 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { * @private */ this.CLASSNAME = "LocationAdvancedSearch"; + + // Get type list from capabilities if not provided + fetch("https://data.geopf.fr/geocodage/getCapabilities").then(response => { + return response.json(); + }).then (json => { + // Get list from capabilities + const values = json.indexes.find(i => i.id==="poi").fields.find(f => f.name==="category").values; + this._typeList = values; + // Set categories if not provided in options + if (!options.typeList) { + this.setCategories(values); + } + }).catch(() => { + console.log("error"); + }); + this._typeList = {}; + // Default values + const typeList = [ "administratif", "aérodrome", "cimetière", "construction ponctuelle", "construction surfacique", "cours d'eau" ]; + this.set("typeList", options.typeList || typeList); + } + + /** Set categories list + * @param {Array|Object} categories Liste des catégories + * @param {HTMLSelectElement} [select] Élément select à remplir (par défaut celui du formulaire) + */ + setCategories (categories, select) { + select = select || this.element.querySelector("select[name='category']"); + select.innerHTML = ""; + let typeList = []; + const isArray = Array.isArray(categories); + if (isArray) { + typeList = categories; + } else if (typeof categories === "object") { + typeList = Object.keys(categories); + } else { + return; + } + typeList.forEach(k => { + const typeOption = document.createElement("option"); + typeOption.value = k.toLowerCase(); + typeOption.innerText = k; + select.appendChild(typeOption); + if (!isArray && Array.isArray(categories[k])) { + typeOption.className = "option"; + categories[k].forEach(v => { + const subOption = document.createElement("option"); + subOption.value = v.toLowerCase(); + subOption.className = "subOption"; + subOption.innerHTML = "  " + v; + select.appendChild(subOption); + }); + } + }); + if (this.filter) { + // Set current value + select.value = this.filter.category; + // If none selected, reset to first + if (!select.value) { + select.selectedIndex = 0; + this.filter.category = select.value; + } + } } /** @@ -115,7 +198,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { handleMultipleResults (e) { const results = this.searchService.getResult(); results.forEach((result, i) => { - console.log(result); + // console.log(result); const attr = result.placeAttributes; const li = document.createElement("li"); li.className = "search-result-item" + (i>=5 ? " hidden" : ""); @@ -125,9 +208,11 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { li.appendChild(a); a.className = "fr-icon-map-pin-2-line"; a.href = "#"; - const postCode = attr.postcode[0] ? `, ${attr.postcode[0]}` : ""; - const text = attr.toponym + postCode + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); + // Format title + const postCode = (attr.postcode && attr.postcode[0]) ? `, ${attr.postcode[0]}` : ""; + const text = (attr.toponym || "") + postCode + " (" + (attr.category || []).join(", ") + ") - " + (attr.city || []).join(", "); a.title = a.innerText = text; + // on click on li or link li.addEventListener("click", () => a.click()); a.addEventListener("click", e => { e.preventDefault(); @@ -163,15 +248,12 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { li.className = "search-result-actions"; const okBtn = document.createElement("button"); okBtn.className = "fr-btn fr-btn--sm fr-btn--tertiary"; - okBtn.innerText = "OK"; + okBtn.innerText = "Effacer"; okBtn.addEventListener("click", () => { this.searchResult.innerHTML = ""; this.element.parentElement.parentElement.scrollTop = 0; }); li.appendChild(okBtn); - - // Par defaut on selectionne le premier resultat - // this.dispatchEvent(e); } /** @@ -199,6 +281,12 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { label.setAttribute("for", input.id); container.appendChild(input); } + // Error message + const errorDiv = document.createElement("div"); + errorDiv.className = "GPMessagesGroup fr-messages-group"; + container.appendChild(errorDiv); + + // container return container; } @@ -226,24 +314,10 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // Type const typeSelect = document.createElement("select"); typeSelect.className = "fr-select"; - typeSelect.id = typeSelect.name = Helper.getUid("LocationAdvancedSearch-type-"); + typeSelect.id = Helper.getUid("LocationAdvancedSearch-type-"); + typeSelect.name = "category"; this._getLabelContainer("Type", "fr-select-group", typeSelect); - /* Liste des types - fetch("https://data.geopf.fr/geocodage/getCapabilities").then(response => { - return response.json(); - }).then (json => { - console.log(json); - }).catch(() => { - console.log("error"); - }); - */ - const typeList = [ "Administratif", "Aérodrome", "Cimetière", "Construction ponctuelle", "Construction surfacique", "Cours d'eau" ]; - typeList.forEach(k => { - const typeOption = document.createElement("option"); - typeOption.value = k.toLowerCase(); - typeOption.innerText = k; - typeSelect.appendChild(typeOption); - }); + this.setCategories(this.get("typeList"), typeSelect); typeSelect.addEventListener("change", () => { this.filter.category = typeSelect.value; }); @@ -256,7 +330,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { searchInput.setAttribute("minlength", "3"); searchInput.required = true; searchInput.id = Helper.getUid("LocationAdvancedSearch-search-"); - this._getLabelContainer("Renseigner un lieu *", "fr-input-group", searchInput); + this._getLabelContainer("Renseigner un lieu *", "fr-input-group", searchInput, null); // Code postal const postalInput = document.createElement("input"); @@ -281,12 +355,12 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput, "Format attendu INSEE : 5 chiffres, selon le code officiel géographique (COG)"); inseeInput.addEventListener("change", () => { - this.filter.citycode = inseeInput.value; + this.filter.citycode = inseeInput.value.toUpperCase(); }); this.filter = { category : typeSelect.value, - postcode : postalInput.value, + postcode : postalInput.value.toUpperCase(), citycode : inseeInput.value }; } @@ -299,12 +373,13 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { _onErase (e) { super._onErase(e); this.element.querySelectorAll("select").forEach(input => { - input.value = ""; + input.selectedIndex = 0; }); this.element.querySelectorAll("input").forEach(input => { input.value = ""; }); this.searchResult.innerHTML = ""; + this._clearMessages(); this.filter = { category : "", postcode : "", @@ -312,21 +387,85 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { }; } + /** Clear error messages in the form + * @private + */ + _clearMessages () { + this.element.querySelectorAll(".fr-message--error").forEach(msg => msg.remove()); + } + + /** Show error message for a given input + * @param {String} name Input name + * @param {String} [message] Message to show (if none remove message) + * @private + */ + _showMessage (name, message) { + const div = this.element.querySelector("input[name='" + name + "']").nextSibling; + if (message) { + const msg = document.createElement("p"); + msg.className = "fr-message fr-message--error"; + msg.textContent = message; + div.appendChild(msg); + } else { + div.innerHTML = ""; + } + } + /** * @protected * @override * @param {PointerEvent} e Événement de soumission + * param {String} [commune] Nom de la commune (optionnel) */ - _onSearch (e) { + _onSearch (e, commune) { super._onSearch(e); - const value = this.searchInput.value; - if (value) { - const obj = { - location : value, - filters : this.filter - }; - this.searchService.search(obj); + const value = commune || this.searchInput.value; + // Check values + this._clearMessages(); + /* + if (value.length < 3) { + this._showMessage("search", "Veuillez saisir au moins 3 caractères pour lancer la recherche."); + return; + } + */ + if (this.filter.postcode) { + const postalRegex = new RegExp(this.element.querySelector("input[name='postalCode']").pattern); + if (!postalRegex.test(this.filter.postcode)) { + this._showMessage("postalCode", "Le code postal doit être composé de 5 chiffres."); + return; + } + } + if (this.filter.citycode) { + const inseeRegex = new RegExp(this.element.querySelector("input[name='cityCode']").pattern); + if (!inseeRegex.test(this.filter.citycode)) { + this._showMessage("cityCode", "Le code INSEE doit être composé de 5 caractères (chiffres ou 2A, 2B)."); + return; + } + } + if (!value && this.filter.citycode) { + fetch(`https://geo.api.gouv.fr/communes?code=${this.filter.citycode}&format=json&fields=nom`).then(response => { + return response.json(); + }).then (json => { + if (json && json.length) { + const commune = json[0].nom; + if (commune) { + this._onSearch(e, commune); + } + } + }).catch(() => { + console.log("error"); + }); + } else { + if (value.length < 3) { + this._showMessage("search", "Veuillez saisir au moins 3 caractères pour lancer la recherche."); + return; + } } + // Search + this.searchService.search({ + location : value, + filters : this.filter + }); } } From 9ed5b0352129959a7f8dce8bed8f392aac580d03 Mon Sep 17 00:00:00 2001 From: viglino Date: Tue, 28 Oct 2025 14:25:13 +0100 Subject: [PATCH 42/73] RFC refactoring / gestion des erreurs --- .../SearchEngine/LocationAdvancedSearch.js | 42 ++++++++++++++++--- .../Services/AbstractSearchService.js | 1 + src/packages/Services/IGNSearchService.js | 8 ++++ 3 files changed, 45 insertions(+), 6 deletions(-) diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 96aa5e24c..68cc8115f 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -56,9 +56,18 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { // Do something on search (when ready) setTimeout(() => { this.searchService.on("search", e => this.handleSearch(e)); + this.searchService.on("error", e => this.handleError(e)); }); } + /** Gère les erreurs de recherche. + * @private + * @param {Event} e Événement d'erreur + */ + handleError (e) { + this.handleSearch({ nbResults : 0 }); + } + /** * Gère les résultats de la recherche. * @private @@ -442,7 +451,9 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { return; } } + // Get value from code if needed if (!value && this.filter.citycode) { + // Search commune name as search string fetch(`https://geo.api.gouv.fr/communes?code=${this.filter.citycode}&format=json&fields=nom`).then(response => { return response.json(); }).then (json => { @@ -450,17 +461,36 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { const commune = json[0].nom; if (commune) { this._onSearch(e, commune); + } else { + this.handleSearch({ nbResults : 0 }); } } }).catch(() => { - console.log("error"); + this.handleSearch({ nbResults : 0 }); }); - } else { - if (value.length < 3) { - this._showMessage("search", "Veuillez saisir au moins 3 caractères pour lancer la recherche."); - return; - } + return; + } else if (!value && this.filter.postcode) { + // Search commune name as search string + fetch(`https://apicarto.ign.fr/api/codes-postaux/communes/${this.filter.postcode}`).then(response => { + return response.json(); + }).then (json => { + if (json && json.length) { + const commune = json[0].nomCommune; + if (commune) { + this._onSearch(e, commune); + } else { + this.handleSearch({ nbResults : 0 }); + } + } + }).catch(() => { + this.handleSearch({ nbResults : 0 }); + }); + return; + } else if (value.length < 3) { + this._showMessage("search", "Veuillez saisir au moins 3 caractères pour lancer la recherche."); + return; } + // Search this.searchService.search({ location : value, diff --git a/src/packages/Services/AbstractSearchService.js b/src/packages/Services/AbstractSearchService.js index 8ef148687..247c3bfd6 100644 --- a/src/packages/Services/AbstractSearchService.js +++ b/src/packages/Services/AbstractSearchService.js @@ -48,6 +48,7 @@ class AbstractSearchService extends BaseObject { initialize (options) { this.AUTOCOMPLETE_EVENT = "autocomplete"; this.SEARCH_EVENT = "search"; + this.ERROR_EVENT = "error"; this._autocompleteLocations = []; this._locations = []; diff --git a/src/packages/Services/IGNSearchService.js b/src/packages/Services/IGNSearchService.js index 927e05f42..2fe0d2582 100644 --- a/src/packages/Services/IGNSearchService.js +++ b/src/packages/Services/IGNSearchService.js @@ -579,6 +579,14 @@ class IGNSearchService extends AbstractSearchService { */ _onFailureSearch (location, error) { logger.warn(error); + if (!location || !location.position) { + this.dispatchEvent({ + type : this.ERROR_EVENT, + location : location, + error : error + }); + return; + } let position = [ location.position.x, From 765b2d1b229af4ca47d203f5f7be04917ac8c62d Mon Sep 17 00:00:00 2001 From: viglino Date: Fri, 31 Oct 2025 09:48:56 +0100 Subject: [PATCH 43/73] RFC refactoring / gestion des calques / --- ...ginebase-modules-dsfr-geocodeAdvanced.html | 9 +- .../SearchEngine/GPFadvancedSearchEngine.css | 1 + .../SearchEngine/LocationAdvancedSearch.js | 6 +- .../SearchEngine/SearchEngineAdvanced.js | 110 +++++++----------- .../Controls/SearchEngine/typedefs.js | 1 + 5 files changed, 59 insertions(+), 68 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 53398d06d..70ef37d66 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -72,8 +72,13 @@

    Ajout du moteur de recherche avec les options par défaut

    }) let addFeatureToLayer = function (feature) { - console.log(this, feature); - source.addFeature(feature.clone()); + source.addFeature(feature); + // Feature traitée => supprimer de la recherche + return true; + /* + // Feature non traitée => conserver la recherche + return false; + */ } // 2. Appel du SearchEngine diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index dddce6a37..03722450d 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -73,6 +73,7 @@ form[id^=GPAdvancedForm-] ul.search-result .search-result-actions:hover { } form[id^=GPAdvancedForm-] ul.search-result .fr-message--error { color: var(--background-flat-warning); + white-space: normal; } form[id^=GPAdvancedForm-] select option.option { diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 68cc8115f..8a98b85d5 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -113,6 +113,9 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { const li = document.createElement("li"); li.className = "fr-message--error"; li.innerText = "Aucun résultat"; + if (!this.searchInput.value) { + li.innerText += " : précisez votre recherche en renseignant le lieu."; + } this.searchResult.appendChild(li); li.addEventListener("click", () => { li.remove(); @@ -390,7 +393,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { this.searchResult.innerHTML = ""; this._clearMessages(); this.filter = { - category : "", + category : this.element.querySelector("select[name='category']").value, postcode : "", citycode : "" }; @@ -483,6 +486,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { } } }).catch(() => { + this._showMessage("postalCode", "Aucune commune trouvée pour ce code postal."); this.handleSearch({ nbResults : 0 }); }); return; diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 5757e723b..46325be78 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -17,48 +17,29 @@ import mapPinIcon from "./map-pin-2-fill.svg"; import Feature from "ol/Feature"; import { Layer } from "ol/layer"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; -const color = "rgba(0, 0, 145, 1)"; - -const createStyle = (feature) => { - const geometryType = feature.getGeometry().getType(); - - switch (geometryType) { - case "Point": - case "MultiPoint": - return new Style({ - image : new Icon({ - src : mapPinIcon, - color : [0, 0, 145, 1], - }), - }); - - case "LineString": - case "MultiLineString": - return new Style({ - stroke : new Stroke({ - color : color, - width : 3, - }), - }); - - case "Polygon": - case "MultiPolygon": - return new Style({ - stroke : new Stroke({ - color : color, - lineDash : [8, 8], - width : 2, - }), - fill : new Fill({ - color : "rgba(0, 0, 0, 0.1)", - opacity : 0.8 - }), - }); - default: - return new Style(); - } -}; +/** Get style for features + * @param {String|Array} color - Couleur du contour + * @param {String|Array} [fillColor] - Couleur de remplissage + * @returns {Style} Style OpenLayers + */ +function getStyle (color, fillColor) { + return new Style({ + image : new Icon({ + src : mapPinIcon, + color : color, + anchor : [0.5, 1], + }), + stroke : new Stroke({ + color : color, + lineDash : [8, 8], + width : 2, + }), + fill : new Fill({ + color : fillColor || "rgba(0, 0, 0, 0.1)", + }), + }); +} /** * @classdesc @@ -93,17 +74,12 @@ class SearchEngineAdvanced extends Control { this.layer = new Vector({ source : new VectorSource({}), zIndex : Infinity, - style : createStyle, - }); - this.extent = new Vector({ - source : new VectorSource({}), - zIndex : Infinity, - style : createStyle, + style : getStyle([0, 0, 145, 1]), }); this.selectInteraction = new Select({ - layers : [this.layer, this.extent], - style : createStyle, + layers : [this.layer], + style : getStyle([145, 0, 0, 1], [145, 0, 0, 0.2]), }); // Initialize @@ -129,11 +105,6 @@ class SearchEngineAdvanced extends Control { */ this.CLASSNAME = "SearchEngineAdvanced"; - /** - * @type {Object} - */ - this._layerFeatureAssociation = {}; - /** * @type {Array} */ @@ -170,13 +141,20 @@ class SearchEngineAdvanced extends Control { if (map) { // Place les couches au dessus des autres - this.extent.setMap(map); this.layer.setMap(map); map.addInteraction(this.selectInteraction); map.addOverlay(this.popup); } } + /** + * Retourne la couche utilisée pour afficher les résultats. + * @returns {Layer} Couche des résultats + */ + getLayer () { + return this.layer; + } + /** * Initialise les événements du contrôle (géolocalisation, navigation clavier, recherche). * @param {SearchEngineAdvancedOptions} options Options du constructeur. @@ -356,18 +334,13 @@ class SearchEngineAdvanced extends Control { addResultToMap (e) { this._closePopup(); this.layer.getSource().clear(); - this.extent.getSource().clear(); let extent; if (!!e.result) { this.layer.getSource().addFeature(e.result); - // Ajout de la couche pour la retrouver plus tard - this._layerFeatureAssociation[e.result.ol_uid] = this.layer; extent = e.result.getGeometry().getExtent(); } if (!!e.extent) { - this.extent.getSource().addFeature(e.extent); - // Ajout de la couche pour la retrouver plus tard - this._layerFeatureAssociation[e.extent.ol_uid] = this.extent; + this.layer.getSource().addFeature(e.extent); extent = e.extent.getGeometry().getExtent(); } if (this.getMap()) { @@ -400,9 +373,7 @@ class SearchEngineAdvanced extends Control { this.popup.setPosition(position); this.setPopupContent(feature.get("infoPopup") || ""); this.popup.set("feature", feature); - // Récupère la couche liée; - const layer = this._layerFeatureAssociation[feature.ol_uid]; - this.popup.set("layer", layer); + this.popup.set("layer", this.layer); } else { this.popup.setPosition(undefined); this.setPopupContent(""); @@ -548,7 +519,16 @@ class SearchEngineAdvanced extends Control { btn.onclick = () => { const feature = this.popup.get("feature"); if (feature && typeof popupButton.onClick === "function") { - popupButton.onClick.call(this, feature); + // New feature sans style + const newFeature = feature.clone(); + newFeature.setStyle(undefined); + // Appel du callback + if (popupButton.onClick.call(this, newFeature)) { + // Feature traitée => supprimer de la sélection + this._closePopup(); + this.selectInteraction.getFeatures().clear(); + this.layer.getSource().removeFeature(feature); + }; } }; return btn; diff --git a/src/packages/Controls/SearchEngine/typedefs.js b/src/packages/Controls/SearchEngine/typedefs.js index 056eb1900..7bde8cde2 100644 --- a/src/packages/Controls/SearchEngine/typedefs.js +++ b/src/packages/Controls/SearchEngine/typedefs.js @@ -142,6 +142,7 @@ * Callback appelé lors du clic sur un bouton personnalisé du popup. * @callback PopupButtonClickCallback * @param {Feature} feature - La feature associée au popup. + * @returns {Boolean} Retourne true si la feature doit être supprimée de la sélection, default (false). * @this SearchEngineAdvanced */ From 280533c42ea6db3e714d9f8900658deae3ae2e53 Mon Sep 17 00:00:00 2001 From: viglino Date: Fri, 31 Oct 2025 09:57:30 +0100 Subject: [PATCH 44/73] FIX regexp code --- src/packages/Controls/SearchEngine/LocationAdvancedSearch.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 8a98b85d5..1393696ac 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -349,7 +349,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { postalInput.className = "fr-input"; postalInput.type = "text"; postalInput.name = "postalCode"; - postalInput.pattern = "(\\d{5})"; + postalInput.pattern = "^(\\d{5})$"; postalInput.title = "Code postal à 5 chiffres"; postalInput.id = Helper.getUid("LocationAdvancedSearch-postal-"); this._getLabelContainer("Code postal", "fr-input-group", postalInput); @@ -362,7 +362,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { inseeInput.className = "fr-input"; inseeInput.name = "cityCode"; inseeInput.type = "text"; - inseeInput.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; + inseeInput.pattern = "^(\\d\\d|2[A,B,a,b])\\d{3}$"; inseeInput.title = "Code INSEE sur 5 caractères"; inseeInput.id = Helper.getUid("LocationAdvancedSearch-insee-"); this._getLabelContainer("Code INSEE", "fr-input-group", inseeInput, "Format attendu INSEE : 5 chiffres, selon le code officiel géographique (COG)"); @@ -441,6 +441,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { } */ if (this.filter.postcode) { + console.log("testcode"); const postalRegex = new RegExp(this.element.querySelector("input[name='postalCode']").pattern); if (!postalRegex.test(this.filter.postcode)) { this._showMessage("postalCode", "Le code postal doit être composé de 5 chiffres."); From 381a7001dff183de3327683a0f471bcc722babd2 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 6 Nov 2025 17:59:07 +0100 Subject: [PATCH 45/73] feat(search): Retrait class fr-search-bar + ajout bouton effacer saisie --- .../SearchEngine/DSFRsearchEngineStyle.css | 20 +++++++++++++++++-- .../Controls/SearchEngine/GPFsearchEngine.css | 12 ++--------- .../SearchEngine/SearchEngineAdvanced.js | 16 ++++++++++++++- .../Controls/SearchEngine/SearchEngineBase.js | 10 ++++++++-- 4 files changed, 43 insertions(+), 15 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index f461f8fbf..0727b3f66 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -240,6 +240,10 @@ div[id^=GPgeocodeResults-] { border-bottom: 1px solid var(--grey-900-175); } +.GPSearchBar > div.GPInputGroup, .GPSearchBar > div.GPInputGroup > input { + box-shadow: inset 0 -2px 0 0 var(--border-action-high-blue-france); +} + /* Mode Mobile */ @media (max-width: 576px) { @@ -286,12 +290,12 @@ div[id^=GPgeocodeResults-] { width: 100%; } -[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"] .GPOptionsContainer button { +/* [id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"] .GPOptionsContainer button { font-size: 0.875rem; line-height: 1.5rem; min-height: 2rem; padding: 0.25rem 0.75rem; -} +} */ [id^="GPsearchEngine"] ul:empty { padding: unset; @@ -394,4 +398,16 @@ form.GPSearchBar > .GPInputGroup,form.GPSearchBar > button[id^=GPshowSearchEngin form.GPSearchBar > button[id^=GPshowSearchEnginePicto-].fr-btn { width: 48px; max-width: unset; +} + +button[id^="GPSearchEngine-erase-btn"] { + display: none; +} + +form.GPSearchBar > .GPInputGroup > input[data-empty] ~ .GPOptionsContainer > button[id^="GPSearchEngine-erase-btn"] { + display:block; +} + +form.GPSearchBar > .GPInputGroup > input[data-empty] ~ .GPOptionsContainer > button[id^="GPSearchEngine-advanced-btn"] { + display:none; } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css index ecba395ce..d9e23f8c3 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFsearchEngine.css @@ -215,22 +215,14 @@ form[id^=GPadvancedSearchForm]:has(option[value="Coordinates"]:checked) > input[ flex-direction: row; padding: 0; } -/* [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPInputGroup { - padding: 0; -} */ + [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer { display: flex; height: calc(100% - 2px); } [id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer button { - max-width: unset; - min-height: 1rem; - margin: 0.25rem 0.5rem; - border-radius: 0; -} -[id^="GPsearchEngine"] form[id^=GPsearchInput-Base-] .GPOptionsContainer button:before { - display: none; + margin: 0.5rem; } .GPautoCompleteHeader { diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 46325be78..ffe4a1c13 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -249,7 +249,7 @@ class SearchEngineAdvanced extends Control { // Ajout des options avancées const advancedBtn = this.advancedBtn = document.createElement("button"); - advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; + advancedBtn.className = "GPSearchEngine-advanced-btn fr-btn fr-btn--sm fr-icon-arrow-up-s-line fr-btn--icon-right fr-btn--tertiary-no-outline"; advancedBtn.id = Helper.getUid("GPSearchEngine-advanced-btn-"); advancedBtn.type = "button"; advancedBtn.title = "Avancée"; @@ -325,6 +325,20 @@ class SearchEngineAdvanced extends Control { if (this._searchForms.length) { this.baseSearchEngine.optionscontainer.appendChild(advancedBtn); } + + // Ajout des options avancées + const eraseBtn = this.eraseBtn = document.createElement("button"); + eraseBtn.className = "GPSearchEngine-erase-btn fr-btn fr-btn--sm fr-icon-close-circle-line fr-btn--tertiary-no-outline"; + eraseBtn.id = Helper.getUid("GPSearchEngine-erase-btn-"); + eraseBtn.type = "button"; + eraseBtn.title = "Effacer la saisie"; + eraseBtn.setAttribute("aria-label", "Effacer la saisie"); + // Gestion du bouton avancé + eraseBtn.addEventListener("click", function () { + this.baseSearchEngine.input.value = ""; + delete this.baseSearchEngine.input.dataset.empty; + }.bind(this)); + this.baseSearchEngine.optionscontainer.appendChild(eraseBtn); } /** diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 88ad1062b..ac267e5a5 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -119,6 +119,11 @@ class SearchEngineBase extends Control { if (this.searchService.get("autocomplete") !== false) { // Empty input this.input.addEventListener("input", function (e) { + if (e.target.value.length != 0) { + e.target.dataset.empty = true; + } else { + delete e.target.dataset.empty; + } if (!e.target.value) { this.showHistoric(); } @@ -228,7 +233,8 @@ class SearchEngineBase extends Control { element.id = Helper.getUid("GPsearchEngine-"); // Main container const container = this.container = document.createElement("form"); - container.className = options.search ? "GPSearchBar fr-search-bar" : ""; + // container.className = options.search ? "GPSearchBar fr-search-bar" : ""; + container.className = options.search ? "GPSearchBar" : ""; // container.className = "fr-search-bar"; container.id = Helper.getUid("GPsearchInput-Base-"); @@ -306,7 +312,7 @@ class SearchEngineBase extends Control { // Submit button if (options.searchButton) { const submit = this.subimtBt = document.createElement("button"); - submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn fr-btn--lg"; + submit.className = "GPsearchInputSubmit gpf-btn fr-icon-search-line fr-btn"; submit.id = Helper.getUid("GPshowSearchEnginePicto-"); submit.type = "submit"; if (options.title) { From f1a745b89d4a36c94b3a74dbca5bd94098f1e228 Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 19 Nov 2025 17:27:21 +0100 Subject: [PATCH 46/73] Feat(parcel) add Parcel advanced search --- build/webpack/controls.webpack.config.js | 1 + build/webpack/modules.webpack.config.js | 1 + ...ginebase-modules-dsfr-geocodeAdvanced.html | 6 +- src/index.js | 1 + .../SearchEngine/GPFadvancedSearchEngine.css | 16 ++ .../SearchEngine/LocationAdvancedSearch.js | 7 +- .../SearchEngine/ParcelAdvancedSearch.js | 266 ++++++++++++++++++ .../Controls/SearchEngine/SearchEngineBase.js | 1 + 8 files changed, 293 insertions(+), 6 deletions(-) create mode 100644 src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js diff --git a/build/webpack/controls.webpack.config.js b/build/webpack/controls.webpack.config.js index 742f119c7..38247e5e7 100644 --- a/build/webpack/controls.webpack.config.js +++ b/build/webpack/controls.webpack.config.js @@ -78,6 +78,7 @@ module.exports = (env, argv) => { case "SearchEngineGeocodeIGN": case "SearchEngineAdvanced": case "LocationAdvancedSearch": + case "ParcelAdvancedSearch": case "AbstractAdvancedSearch": case "InseeAdvancedSearch": case "CoordinateAdancedSearch": diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 6e87c4eac..0a91ca4a2 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -49,6 +49,7 @@ module.exports = (env, argv) => { "GpfExtOlSearchEngineGeocodeIGN" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineGeocodeIGN.js"), "GpfExtOlSearchEngineAdvanced" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "SearchEngineAdvanced.js"), "GpfExtOlLocationAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "LocationAdvancedSearch.js"), + "GpfExtOlParcelAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "ParcelAdvancedSearch.js"), "GpfExtOlInseeAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "InseeAdvancedSearch.js"), "GpfExtOlAbstractAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "AbstractAdvancedSearch.js"), "GpfExtOlCoordinateAdvancedSearch" : path.join(rootdir, "src", "packages", "Controls/SearchEngine", "CoordinateAdvancedSearch.js"), diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index 70ef37d66..a06320a89 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -7,6 +7,7 @@ + {{/content}} @@ -68,6 +69,9 @@

    Ajout du moteur de recherche avec les options par défaut

    var location = new ol.control.LocationAdvancedSearch({ }) + var parcel = new ol.control.ParcelAdvancedSearch({ + }) + var coordinates = new ol.control.CoordinateAdvancedSearch({ }) @@ -83,7 +87,7 @@

    Ajout du moteur de recherche avec les options par défaut

    // 2. Appel du SearchEngine var search = new ol.control.SearchEngineAdvanced({ - advancedSearch : [insee, location, coordinates], + advancedSearch : [insee, location, coordinates, parcel], returnTrueGeometry : true, popupButtons : [{ label : "Ajouter l'objet à la couche", diff --git a/src/index.js b/src/index.js index 04b0e20c9..d6e2f38c3 100644 --- a/src/index.js +++ b/src/index.js @@ -31,6 +31,7 @@ export { default as AbstractAdvancedSearch } from "./packages/Controls/SearchEng export { default as CoordinateAdvancedSearch } from "./packages/Controls/SearchEngine/CoordinateAdvancedSearch"; export { default as InseeAdvancedSearch } from "./packages/Controls/SearchEngine/InseeAdvancedSearch"; export { default as LocationAdvancedSearch } from "./packages/Controls/SearchEngine/LocationAdvancedSearch"; +export { default as ParcelAdvancedSearch } from "./packages/Controls/SearchEngine/ParcelAdvancedSearch"; export { default as MousePosition } from "./packages/Controls/MousePosition/MousePosition"; export { default as Drawing } from "./packages/Controls/Drawing/Drawing"; export { default as Route } from "./packages/Controls/Route/Route"; diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 03722450d..3e0dfdf83 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -79,3 +79,19 @@ form[id^=GPAdvancedForm-] ul.search-result .fr-message--error { form[id^=GPAdvancedForm-] select option.option { background-color: var(--background-action-low-blue-france); } + + +/* Search parcels */ +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList { + display: none; + z-index: 1; + background-color: #fff; + padding: 0; +} +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList.gpf-visible { + display: block; +} + +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList li { + height: auto; +} diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 1393696ac..43870a28d 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -441,16 +441,13 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { } */ if (this.filter.postcode) { - console.log("testcode"); - const postalRegex = new RegExp(this.element.querySelector("input[name='postalCode']").pattern); - if (!postalRegex.test(this.filter.postcode)) { + if (!this.element.querySelector("input[name='postalCode']").checkValidity()) { this._showMessage("postalCode", "Le code postal doit être composé de 5 chiffres."); return; } } if (this.filter.citycode) { - const inseeRegex = new RegExp(this.element.querySelector("input[name='cityCode']").pattern); - if (!inseeRegex.test(this.filter.citycode)) { + if (!this.element.querySelector("input[name='cityCode']").checkValidity()) { this._showMessage("cityCode", "Le code INSEE doit être composé de 5 caractères (chiffres ou 2A, 2B)."); return; } diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js new file mode 100644 index 000000000..ca7b5d3fa --- /dev/null +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -0,0 +1,266 @@ +import InseeSearchService from "../../Services/InseeSearchService"; +import Helper from "../../Utils/Helper"; +import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; + +/** + * @classdesc + * Contrôle de recherche parcellaire. + * + * @extends {AbstractAdvancedSearch} + * @alias ol.control.ParcelAdvancedSearch + * @module ParcelAdvancedSearch + */ +class ParcelAdvancedSearch extends AbstractAdvancedSearch { + + /** + * Constructeur du contrôle de recherche avancée. + * @param {AbstractAdvancedSearchOptions} options Options du constructeur + * @param {String} [options.name="Parcelles"] Nom du contrôle + * @extends {ol.control.AbstractAdvancedSearch} + */ + constructor (options) { + options = options || {}; + + options.name = options.name || "Parcelles"; + // call ol.control.Control constructor + + super(options); + + this.element.setAttribute("novalidate", ""); + } + + /** Clear error messages in the form + * @private + */ + _clearMessages () { + this.element.querySelectorAll(".fr-message--error").forEach(msg => msg.remove()); + } + + /** Show error message for a given input + * @param {String} name Input name + * @param {String} [message] Message to show (if none remove message) + * @private + */ + _showMessage (name, message) { + const div = this.element.querySelector("input[name='" + name + "']").parentElement.querySelector(".GPMessagesGroup"); + if (message) { + const msg = document.createElement("p"); + msg.className = "fr-message fr-message--error"; + msg.textContent = message; + div.appendChild(msg); + } else { + div.innerHTML = ""; + } + } + + /** + * @override + * @protected + * @param {AbstractAdvancedSearchOptions} options Options du constructeur + */ + _initEvents (options) { + super._initEvents(options); + + const comCodeInput = this.comCodeInput; + const autocompleteList = this.autocompleteList; + + let communeId = ""; + // Show/hide autocomplete list + function showAutocomplete (b) { + if (!b) { + autocompleteList.classList.remove("gpf-visible"); + return; + } + if (communeId !== comCodeInput.value) { + autocompleteList.classList.remove("gpf-visible"); + } else { + autocompleteList.classList.add("gpf-visible"); + } + } + // Show autocomplete on input + comCodeInput.addEventListener("keyup", showAutocomplete); + comCodeInput.addEventListener("focus", showAutocomplete); + comCodeInput.addEventListener("blur", (e) => { + if (e.relatedTarget !== autocompleteList) { + showAutocomplete(false); + } + }); + // Fetch commune data on change + comCodeInput.addEventListener("change", () => { + //check valid input + if (!comCodeInput.checkValidity()) { + this._showMessage("commCode", "Le code INSEE doit être composé de 5 caractères (chiffres ou 2A, 2B) ou le code postal de 5 chiffres."); + return; + } else { + this._clearMessages(); + } + this._getCommuneData(comCodeInput.value).then(data => { + // Clear previous suggestions + autocompleteList.innerHTML = ""; + communeId = ""; + if (data.length === 0) { + // errror message + } else if (data.length === 1) { + comCodeInput.value = `${data[0].code} - ${data[0].nom})`; + } else { + data.forEach(commune => { + console.log(commune); + const type = commune.codesPostaux ? "code postal" : "code INSEE"; + const option = document.createElement("li"); + option.className = "GPautoCompleteOption"; + option.setAttribute("role", "option"); + option.textContent = `${commune.code}, ${commune.nom} (${type})`; + option.addEventListener("click", () => { + communeId = comCodeInput.value = `${commune.code} (${commune.nom})`; + showAutocomplete(false); + }); + autocompleteList.appendChild(option); + }); + communeId = comCodeInput.value; + autocompleteList.classList.add("gpf-visible"); + } + }); + }); + } + + /** + * @override + * @protected + * @param {AbstractAdvancedSearchOptions} options Options du constructeur + */ + initialize (options) { + super.initialize(options); + /** + * Nom de la classe (heritage) + * @private + */ + this.CLASSNAME = "ParcelAdvancedSearch"; + + this.insseSearchService = new InseeSearchService(); + } + + /** + * Crée un conteneur label + input pour le formulaire. + * @private + * @param {String} text Texte du label + * @param {String} type Classe CSS du conteneur + * @param {HTMLElement} input Élément input à rattacher + * @param {String} [hint] Texte d'aide optionnel + * @returns {HTMLElement} Conteneur HTML + */ + _getLabelContainer (text, type, input, hint = "") { + const container = document.createElement("div"); + container.className = type; + this.inputs.push(container); + const label = document.createElement("label"); + label.className = "fr-label"; + let labelText = text; + if (hint) { + labelText = `${text} ${hint}`; + } + label.innerHTML = labelText; + container.appendChild(label); + if (input) { + label.setAttribute("for", input.id); + container.appendChild(input); + } + // Error message + const errorDiv = document.createElement("div"); + errorDiv.className = "GPMessagesGroup fr-messages-group"; + container.appendChild(errorDiv); + + // container + return container; + } + + /** + * Récupère les données d'une commune via l'API geo.api.gouv.fr + * @private + * @param {String} code Code INSEE ou code postal + * @returns {Promise} Promesse avec les données de la commune : nom + code + codesPostaux (si codepostal)) + */ + async _getCommuneData (code) { + const baseURL = "https://geo.api.gouv.fr/communes"; + const url1 = `${baseURL}?code=${code}&format=json&fields=nom,code`; + const url2 = `${baseURL}?codePostal=${code}&format=json&fields=nom,codesPostaux`; + const param ={ + headers : { + "Content-Type" : "application/json", + }, + }; + return Promise.all([ + fetch(url1, param), + fetch(url2, param) + ]).then(responses => { + return Promise.all(responses.map(res => res.json())).then(json => { + return json[0].concat(json[1]); + }); + }); + } + + /** Add inputs on form + * @override + * @protected + */ + addInputs () { + super.addInputs(); + + // Legend + const legend = document.createElement("legend"); + legend.className = "fr-fieldset__legend fr-fieldset__legend--regular"; + legend.id = Helper.getUid("ParcelAdvancedSearch-legend-"); + const hint = document.createElement("span"); + hint.className = "fr-hint-text"; + hint.textContent = "* Champs obligatoires"; + legend.appendChild(hint); + this.inputs.push(legend); + + // Code postal + const comCodeInput = this.comCodeInput = document.createElement("input"); + comCodeInput.className = "fr-input"; + comCodeInput.type = "text"; + comCodeInput.name = "commCode"; + comCodeInput.pattern = "^(\\d\\d|2[A,B,a,b,0-9])\\d{3}$"; + comCodeInput.title = "Code postal ou code INSEE"; + comCodeInput.id = Helper.getUid("ParcelAdvancedSearch-comCode-"); + const codeDiv = this._getLabelContainer("Renseigner un lieu*", "fr-input-group", comCodeInput, "Code postal ou code INSEE"); + + // Autocomplete list + const autocompleteList = this.autocompleteList = document.createElement("ul"); + autocompleteList.className = "GPautoCompleteList"; + autocompleteList.id = Helper.getUid("GPautoCompleteList-"); + autocompleteList.setAttribute("role", "listbox"); + autocompleteList.setAttribute("tabindex", "-1"); + autocompleteList.setAttribute("aria-label", "Propositions"); + codeDiv.insertBefore(autocompleteList, comCodeInput.nextSibling); + + // Input controller for accessibility + comCodeInput.setAttribute("role", "combobox"); + comCodeInput.setAttribute("aria-controls", autocompleteList.id); + comCodeInput.setAttribute("aria-expanded", "false"); + comCodeInput.setAttribute("aria-autocomplete", "list"); + comCodeInput.setAttribute("aria-haspopup", "listbox"); + comCodeInput.setAttribute("autocomplete", "off"); + + comCodeInput.addEventListener("focus", () => { + comCodeInput.setAttribute("aria-expanded", "true"); + }); + } + + /** Do search + * @protected + * @override + * @param {PointerEvent} e Événement de soumission + */ + _onSearch (e) { + super._onSearch(e); + } + +} + +export default ParcelAdvancedSearch; + +// Expose ParcelAdvancedSearch as ol.control.ParcelAdvancedSearch (for a build bundle) +if (window.ol && window.ol.control) { + window.ol.control.ParcelAdvancedSearch = ParcelAdvancedSearch; +} \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index ac267e5a5..290e2b49f 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -323,6 +323,7 @@ class SearchEngineBase extends Control { // Autocomplete container const acContainer = document.createElement("div"); + acContainer.id = Helper.getUid("GPautoCompleteContainer-"); acContainer.className = "GPautoCompleteContainer GPelementHidden gpf-hidden"; element.appendChild(acContainer); // element.appendChild(acContainer); From 91efd32b56261de2b6f089bc629b727f6fd506f6 Mon Sep 17 00:00:00 2001 From: viglino Date: Thu, 20 Nov 2025 14:29:54 +0100 Subject: [PATCH 47/73] Feat: advanced search parcel --- .../SearchEngine/GPFadvancedSearchEngine.css | 9 +- .../SearchEngine/LocationAdvancedSearch.js | 1 + .../SearchEngine/ParcelAdvancedSearch.js | 350 ++++++++++++++++-- src/packages/Services/IGNSearchService.js | 5 +- 4 files changed, 340 insertions(+), 25 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 3e0dfdf83..0ba493416 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -88,10 +88,17 @@ form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList { background-color: #fff; padding: 0; } -form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList.gpf-visible { +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] input[aria-expanded="true"] ~ .GPautoCompleteList { display: block; } form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList li { height: auto; } + +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] [id^=ParcelAdvancedSearch-section] option:first-child{ + color: #aaa; + color: var(--text-disabled-grey); + font-style: italic; +} + diff --git a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js index 43870a28d..d5e978089 100644 --- a/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/LocationAdvancedSearch.js @@ -105,6 +105,7 @@ class LocationAdvancedSearch extends AbstractAdvancedSearch { if (e.extent) { e.extent.setProperties(info); } + console.log(e); this.dispatchEvent(e); } else { this.element.parentElement.parentElement.scrollTop = 0; diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index ca7b5d3fa..df2c911fc 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -1,6 +1,9 @@ +import def from "ajv/dist/vocabularies/discriminator"; import InseeSearchService from "../../Services/InseeSearchService"; import Helper from "../../Utils/Helper"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; +import { couldStartTrivia } from "typescript"; +import IGNSearchService from "../../Services/IGNSearchService"; /** * @classdesc @@ -22,13 +25,54 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { options = options || {}; options.name = options.name || "Parcelles"; - // call ol.control.Control constructor + // call ol.control.Control constructor super(options); + // prevent HTML5 validation this.element.setAttribute("novalidate", ""); + + this.searchService = new IGNSearchService({ + index : "parcel", + limit : 1, + returnTrueGeometry : true + }); + this.searchService.on("search", e => this.handleSearch(e)); + this.searchService.on("error", e => this.handleError(e)); + } + + /** An error occured during search + * @param {Object} e Event + */ + handleError (e) { + console.log("error", e); + this.dispatchEvent(e); + } + + /** Search completed + * @param {Object} e Event + */ + handleSearch (e) { + if (e.nbResults === 0) { + this._showMessage("numero", "Aucune parcelle ne correspond à ce numéro."); + return; + } + // Format result + const attr = e.attr || this.searchService.getResult(0).placeAttributes; + delete attr.truegeometry; + attr.infoPopup = `${attr.id}
    + Parcelle n° ${attr.number.replace(/^0{1,4}/g,"")}
    + Feuille ${attr.oldmunicipalitycode}-${attr.section}
    + ${attr.city} + `; + // Set properties + e.result?.setProperties(attr); + e.extent?.setProperties(attr); + + this.dispatchEvent(e); } + /** Clear error messages in the form * @private */ @@ -42,17 +86,78 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * @private */ _showMessage (name, message) { - const div = this.element.querySelector("input[name='" + name + "']").parentElement.querySelector(".GPMessagesGroup"); + const div = this.element.querySelector("[name='" + name + "']").parentElement.querySelector(".GPMessagesGroup"); if (message) { const msg = document.createElement("p"); msg.className = "fr-message fr-message--error"; msg.textContent = message; + div.innerHTML = ""; div.appendChild(msg); } else { div.innerHTML = ""; } } + /** + * Show sections for a given prefix + * @param {String} prefix Prefix code + */ + setFeuille (prefix) { + const feuilles = this.feuilles[prefix]; + this.sectionInput.innerHTML = ""; + if (feuilles) { + const section = document.createElement("option"); + section.value = ""; + section.textContent = "Sélectionner une section"; + this.sectionInput.appendChild(section); + this.feuilles[prefix].sort().forEach(key => { + const section = document.createElement("option"); + section.value = section.textContent = key; + this.sectionInput.appendChild(section); + }); + this.sectionInput.removeAttribute("disabled"); + this.sectionInput.focus(); + } else { + this.sectionInput.setAttribute("disabled", "disabled"); + } + this.sectionInput.dispatchEvent(new Event("change")); + } + + /** Set the commune + * @param {String} [id] Commune INSEE code + */ + setCommune (id="") { + if (this.communeId !== id) { + const prefixInput = this.prefixInput; + + this.communeId = id; + prefixInput.innerHTML = ""; + if (id) { + // Fetch prefixes and sections for the selected commune + this._fetchFeuille(id).then(data => { + this.feuilles = {}; + + data.features.forEach(feuille => { + if (!this.feuilles[feuille.properties.com_abs]) { + this.feuilles[feuille.properties.com_abs] = []; + } + this.feuilles[feuille.properties.com_abs].push(feuille.properties.section); + }); + Object.keys(this.feuilles).sort().forEach(key => { + const prefix = document.createElement("option"); + prefix.value = prefix.textContent = key; + prefixInput.appendChild(prefix); + }); + this.setFeuille("000"); + }); + prefixInput.removeAttribute("disabled"); + } else { + prefixInput.setAttribute("disabled", "disabled"); + this.setFeuille(); + } + } + }; + /** * @override * @protected @@ -61,30 +166,115 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { _initEvents (options) { super._initEvents(options); + // Inputs const comCodeInput = this.comCodeInput; const autocompleteList = this.autocompleteList; + const prefixInput = this.prefixInput; + const sectionInput = this.sectionInput; + + // Current selected commune + let communeName = ""; + this.communeId = ""; + // Autocomplete selected index + let selectedIndex = -1; + // Autocomplete options + let autoOptions = []; - let communeId = ""; // Show/hide autocomplete list - function showAutocomplete (b) { - if (!b) { - autocompleteList.classList.remove("gpf-visible"); - return; + const showAutocomplete = (b) => { + if (communeName !== comCodeInput.value) { + this.setCommune(); } - if (communeId !== comCodeInput.value) { - autocompleteList.classList.remove("gpf-visible"); + if (b !== false) { + // Show autocomplete only if input value matches selected commune + b = (communeName === comCodeInput.value); + } + if (b) { + comCodeInput.setAttribute("aria-expanded", "true"); + comCodeInput.setAttribute("aria-activedescendant", autoOptions[selectedIndex]?.id || ""); } else { - autocompleteList.classList.add("gpf-visible"); + comCodeInput.setAttribute("aria-expanded", "false"); + autoOptions[selectedIndex]?.classList.remove("active"); + comCodeInput.setAttribute("aria-activedescendant", autoOptions[selectedIndex]?.id || ""); + selectedIndex = -1; } - } + }; + + + // Keyboard navigation on autocomplete list + comCodeInput.addEventListener("keydown", e => { + autoOptions = autocompleteList.querySelectorAll(".GPautoCompleteOption"); + if (autoOptions.length) { + let index = selectedIndex; + switch (e.key) { + case "Backspace": { + index = -1; + if (comCodeInput.value.length > 5) { + comCodeInput.value = comCodeInput.value.slice(0,6); + } + break; + } + case "Escape": { + index = -1; + break; + } + case "ArrowDown": { + index++; + if (index >= autoOptions.length) { + index = -1; + } + break; + } + case "ArrowUp": { + index--; + if (index < -1) { + index += autoOptions.length +1; + } + break; + } + case "Enter": { + if (index >= 0 && index < autoOptions.length) { + autoOptions[selectedIndex]?.classList.remove("active"); + autoOptions[index].click(); + index = -1; + } + e.preventDefault(); + e.stopPropagation(); + break; + } + default: { + break; + } + } + if (selectedIndex !== index) { + e.preventDefault(); + // Update selected option + autoOptions[selectedIndex]?.classList.remove("active"); + selectedIndex = index; + autoOptions[selectedIndex]?.classList.add("active"); + showAutocomplete(["ArrowUp", "ArrowDown"].includes(e.key)); + // Scroll to selected option + autoOptions[selectedIndex]?.scrollIntoView({ block : "nearest" }); + } + } + }); + // Show autocomplete on input - comCodeInput.addEventListener("keyup", showAutocomplete); + comCodeInput.addEventListener("keyup", e => { + e.preventDefault(); + e.stopPropagation(); + if (["ArrowUp", "ArrowDown", "Enter", "Escape"].includes(e.key)) { + return; + } + showAutocomplete(e); + }); comCodeInput.addEventListener("focus", showAutocomplete); comCodeInput.addEventListener("blur", (e) => { if (e.relatedTarget !== autocompleteList) { showAutocomplete(false); } }); + // Fetch commune data on change comCodeInput.addEventListener("change", () => { //check valid input @@ -94,33 +284,49 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } else { this._clearMessages(); } - this._getCommuneData(comCodeInput.value).then(data => { + this._fetchCommuneData(comCodeInput.value).then(data => { // Clear previous suggestions autocompleteList.innerHTML = ""; - communeId = ""; + communeName = ""; if (data.length === 0) { // errror message } else if (data.length === 1) { - comCodeInput.value = `${data[0].code} - ${data[0].nom})`; + communeName = comCodeInput.value = `${data[0].code} (${data[0].nom})`; + this.setCommune(data[0].code); } else { data.forEach(commune => { - console.log(commune); const type = commune.codesPostaux ? "code postal" : "code INSEE"; const option = document.createElement("li"); option.className = "GPautoCompleteOption"; option.setAttribute("role", "option"); + option.id = Helper.getUid("GPautoCompleteOption-"); option.textContent = `${commune.code}, ${commune.nom} (${type})`; option.addEventListener("click", () => { - communeId = comCodeInput.value = `${commune.code} (${commune.nom})`; + communeName = comCodeInput.value = `${commune.code} (${commune.nom})`; + this.setCommune(commune.code); showAutocomplete(false); }); autocompleteList.appendChild(option); }); - communeId = comCodeInput.value; - autocompleteList.classList.add("gpf-visible"); + communeName = comCodeInput.value; + this.setCommune(); + comCodeInput.setAttribute("aria-expanded", "true"); } }); }); + + // Fetch sections + prefixInput.addEventListener("change", () => { + this.setFeuille(prefixInput.value); + }); + // Fetch parcelles + sectionInput.addEventListener("change", () => { + if (sectionInput.value) { + this.numberInput.removeAttribute("disabled"); + } else { + this.numberInput.setAttribute("disabled", "disabled"); + } + }); } /** @@ -179,7 +385,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * @param {String} code Code INSEE ou code postal * @returns {Promise} Promesse avec les données de la commune : nom + code + codesPostaux (si codepostal)) */ - async _getCommuneData (code) { + async _fetchCommuneData (code) { const baseURL = "https://geo.api.gouv.fr/communes"; const url1 = `${baseURL}?code=${code}&format=json&fields=nom,code`; const url2 = `${baseURL}?codePostal=${code}&format=json&fields=nom,codesPostaux`; @@ -198,6 +404,35 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { }); } + /** + * Récupère les feuilles cadastrales d'une commune via le WFS Geopf + * @private + * @param {String} code Code INSEE de la commune + * @returns + */ + async _fetchFeuille (code) { + const domtom = ["97","98"].includes(code.slice(0,2)); + const dep = code.slice(0, domtom ? 3 : 2); + const com = code.slice(domtom ? 3 : 2, 5); + const url = "https://data.geopf.fr/wfs/ows?"; + const params = { + service : "WFS", + version : "2.0.0", + request : "GetFeature", + typename : "CADASTRALPARCELS.PARCELLAIRE_EXPRESS:feuille", + outputFormat : "application/json", + srsName : "CRS:84", + count : "1000", + propertyName : "com_abs,section", + cql_filter : `code_dep='${dep}' and code_com='${com}'` + }; + const queryString = new URLSearchParams(params).toString(); + const fullUrl = url + queryString; + const response = await fetch(fullUrl); + const data = await response.json(); + return data; + } + /** Add inputs on form * @override * @protected @@ -242,9 +477,36 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { comCodeInput.setAttribute("aria-haspopup", "listbox"); comCodeInput.setAttribute("autocomplete", "off"); - comCodeInput.addEventListener("focus", () => { - comCodeInput.setAttribute("aria-expanded", "true"); - }); + // Prefix + const prefixInput = this.prefixInput = document.createElement("select"); + prefixInput.className = "fr-select"; + prefixInput.name = "prefix"; + prefixInput.title = "Choisir le préfixe de la parcelle"; + prefixInput.id = Helper.getUid("ParcelAdvancedSearch-prefix-"); + prefixInput.setAttribute("disabled", "disabled"); + this._getLabelContainer("Préfix", "fr-select-group", prefixInput, ""); + + // Section + const sectionInput = this.sectionInput = document.createElement("select"); + sectionInput.className = "fr-select"; + sectionInput.name = "section"; + sectionInput.title = "Choisir la section de la parcelle"; + sectionInput.id = Helper.getUid("ParcelAdvancedSearch-section-"); + sectionInput.setAttribute("disabled", "disabled"); + this._getLabelContainer("Section*", "fr-select-group", sectionInput, ""); + + // Numero + const numberInput = this.numberInput = document.createElement("input"); + numberInput.className = "fr-input"; + numberInput.type = "text"; + numberInput.pattern = "\\d{1,4}"; + numberInput.maxLength = "4"; + numberInput.name = "numero"; + numberInput.title = "Numéro de la parcelle"; + numberInput.autocomplete = "off"; + numberInput.id = Helper.getUid("ParcelAdvancedSearch-number-"); + numberInput.setAttribute("disabled", "disabled"); + this._getLabelContainer("Numéro*", "fr-input-group", numberInput, ""); } /** Do search @@ -254,6 +516,48 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { */ _onSearch (e) { super._onSearch(e); + + this._clearMessages(); + + if (!this.communeId) { + return; + } + const prefix = this.prefixInput.value; + const section = this.sectionInput.value; + const number = this.numberInput.value; + if (!prefix) { + this._showMessage("prefix", "Le préfixe est obligatoire."); + return; + } else if (!section) { + this._showMessage("section", "La section est obligatoire."); + return; + } else if (!this.numberInput.checkValidity()) { + this._showMessage("numero", "Le numéro de parcelle doit être un nombre."); + return; + } else if (!number) { + this._showMessage("numero", "Le numéro de parcelle est obligatoire."); + return; + } + + // Search parcelle + const parcelId = this.communeId + + "000".slice(prefix.length) + prefix + + section + + "0000".slice(number.length) + number; + + this.searchService.search({ + location : parcelId + }); + } + + /** + * Réinitialise les champs du formulaire. + * @param {PointerEvent} e Événement d'effacement + * @protected + */ + _onErase (e) { + super._onErase(e); + this.setCommune(); } } diff --git a/src/packages/Services/IGNSearchService.js b/src/packages/Services/IGNSearchService.js index 2fe0d2582..4d7044a85 100644 --- a/src/packages/Services/IGNSearchService.js +++ b/src/packages/Services/IGNSearchService.js @@ -511,7 +511,10 @@ class IGNSearchService extends AbstractSearchService { ]; let f, extent; if (location.placeAttributes.truegeometry) { - let geom = JSON.parse(location.placeAttributes.truegeometry); + let geom = location.placeAttributes.truegeometry; + if (typeof geom === "string") { + JSON.parse(geom); + } let format = new GeoJSON(); let geometry = format.readGeometry(geom, { From aee763e8d0d1f88d3da483fcce95c855d70e8e8b Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 20 Nov 2025 15:29:18 +0100 Subject: [PATCH 48/73] feat(search): Ajout types recherche de base + chgt prettify result initial --- .../SearchEngine/DSFRsearchEngineStyle.css | 18 +++++- .../SearchEngine/img/dsfr/ign-mer.svg | 5 ++ .../SearchEngine/ParcelAdvancedSearch.js | 2 +- .../Controls/SearchEngine/SearchEngineBase.js | 48 ++++++++++++-- src/packages/Services/IGNSearchService.js | 64 +++++++++++++++---- src/packages/Services/typedefs.js | 4 +- 6 files changed, 117 insertions(+), 24 deletions(-) create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index 0727b3f66..80941ee26 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -308,9 +308,17 @@ div[id^=GPgeocodeResults-] { box-shadow: 0 -1px 0 0 var(--border-default-grey); } +[id^="GPsearchEngine"] ul li.GPsearchHistoric { + padding-right: 1.75rem; +} + +[id^="GPsearchEngine"] ul li.GPsearchHistoric > span { + position: absolute; + right: 1rem; + color: var(--grey-625-425); +} + [id^="GPsearchEngine"] ul li:first-child { - padding: 0.75rem 0.5rem; - color: var(--text-action-high-grey); box-shadow: none; } @@ -410,4 +418,10 @@ form.GPSearchBar > .GPInputGroup > input[data-empty] ~ .GPOptionsContainer > but form.GPSearchBar > .GPInputGroup > input[data-empty] ~ .GPOptionsContainer > button[id^="GPSearchEngine-advanced-btn"] { display:none; +} + +.fr-icon-ign-mer::before, +.fr-icon-ign-mer::after { + -webkit-mask-image: url("./img/dsfr/ign-mer.svg"); + mask-image: url("./img/dsfr/ign-mer.svg"); } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg new file mode 100644 index 000000000..7423e9e76 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/ign-mer.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index df2c911fc..4e2b48c30 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -408,7 +408,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * Récupère les feuilles cadastrales d'une commune via le WFS Geopf * @private * @param {String} code Code INSEE de la commune - * @returns + * @returns {Object} data */ async _fetchFeuille (code) { const domtom = ["97","98"].includes(code.slice(0,2)); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 290e2b49f..5c036a147 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -7,9 +7,15 @@ import Helper from "../../Utils/Helper"; // Voir les typedefs partagés dans ./typedefs.js (SearchEngineBaseOptions, SearchServiceOptions, ...) +const history = "fr-icon-history-line"; const typeClasses = { - "history" : "fr-icon-history-line", - "search" : "fr-icon-map-pin-2-line", + "StreetAddress" : "fr-icon-map-pin-2-line", + "PositionOfInterest" : { + "administratif" : "fr-icon-france-fill", + "cours d'eau" : "fr-icon-ign-mer", + "construction" : "fr-icon-train-line", + "default" : "fr-icon-map-pin-2-line", + } }; var logger = Logger.getLogger("searchengine"); @@ -500,14 +506,22 @@ class SearchEngineBase extends Control { this.input.setAttribute("data-active-option", ""); // Update list this.autocompleteList.innerHTML = ""; - const iconClass = typeClasses[type] || typeClasses["search"]; tab.forEach((item, idx) => { const li = document.createElement("li"); - li.id = Helper.getUid("GPsearchHistoric-"); - li.className = `GPsearchHistoric gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; + const iconClass = this.getIconClass(item, type); + + li.id = Helper.getUid("GPsearchResult-"); + li.className = `GPsearchResult gpf-panel__item gpf-panel__item-searchengine ${iconClass} fr-icon--sm`; li.setAttribute("role", "option"); li.setAttribute("data-idx", idx); li.innerHTML = li.title = this.getItemTitle(item); + if (type === "history") { + li.classList.add("GPsearchHistoric"); + const span = document.createElement("span"); + span.ariaHidden = "true"; + span.className = `${history} fr-icon--sm`; + li.append(span); + } this.autocompleteList.appendChild(li); li.addEventListener("click", function (e) { const idx = Number(e.target.getAttribute("data-idx")); @@ -519,6 +533,30 @@ class SearchEngineBase extends Control { }); } + /** + * Retourne la classe à ajouter pour un résultat d'autocomplétion + * @param {AutocompleteResult} item Résultat de l'autocomplétion (ou historique) + * @param {String} type Type de la recherche ("history" ou "search") + * @returns {String} classe à ajouter + */ + getIconClass (item, type) { + // let iconClass = typeClasses[type]; + let iconClass = typeClasses[item.type]; + // Cas où l'on a d'autres éléments + if (typeof iconClass === "object") { + // Cherche les types de POI + for (let i = 0; i < item.poiType.length; i++) { + const poiType = item.poiType[i]; + if (Object.hasOwn(iconClass, poiType)) { + iconClass = iconClass[poiType]; + break; + } + } + iconClass = typeof iconClass === "object" ? iconClass["default"] : iconClass; + } + return iconClass; + } + /** * Retourne le titre à afficher pour un item. * @param {Object} item Élément à afficher diff --git a/src/packages/Services/IGNSearchService.js b/src/packages/Services/IGNSearchService.js index 4d7044a85..27cdebadc 100644 --- a/src/packages/Services/IGNSearchService.js +++ b/src/packages/Services/IGNSearchService.js @@ -10,10 +10,10 @@ import GeocodeUtils from "../Utils/GeocodeUtils"; import Search from "./Search"; import Feature from "ol/Feature.js"; import Point from "ol/geom/Point.js"; -import { canvasPool } from "ol/renderer/canvas/Layer"; var logger = Logger.getLogger("searchengine"); +// logger.log = () => { }; /** * @classdesc @@ -72,11 +72,11 @@ class IGNSearchService extends AbstractSearchService { autocomplete : true, autocompleteOptions : { serviceOptions : { - maximumResponses : 5, + maximumResponses : 10, }, triggerGeocode : false, triggerDelay : 1000, - prettifyResults : false + prettifyResults : true }, }; @@ -137,8 +137,6 @@ class IGNSearchService extends AbstractSearchService { * @type {Array} */ this._suggestedLocations; - - console.log(this.options); } @@ -332,8 +330,8 @@ class IGNSearchService extends AbstractSearchService { * @private */ _prettifyAutocompleteResults (autocompleteResults) { - for (var i = autocompleteResults.length - 1; i >= 0; i--) { - var autocompleteResult = autocompleteResults[i]; + for (let i = autocompleteResults.length - 1; i >= 0; i--) { + const autocompleteResult = autocompleteResults[i]; if ((autocompleteResult.type === "StreetAddress" && autocompleteResult.kind === "municipality") || autocompleteResult.type === "PositionOfInterest" && autocompleteResult.poiType[0] === "lieu-dit habité" && autocompleteResult.poiType[1] === "zone d'habitation") { // on retire les éléments streetAdress - municipality car déjà pris en compte par POI @@ -400,37 +398,75 @@ class IGNSearchService extends AbstractSearchService { /** * Lance une recherche sur les services de géocodage de l'IGN * @see {@link https://data.geopf.fr/geocodage/search} + * @see {@link https://data.geopf.fr/geocodage/openapi} * @param {IGNSearchObject} object Recherche * @abstract */ search (object) { - const location = object.location; - const filters = object.filters; + let location = object.location; + const filters = object.filters ? object.filters : {}; if (location === undefined) { return; } // on ajoute le texte de l'autocomplétion dans l'input let label; + let index = this.get("index"); + let truegeometry = this.get("returnTrueGeometry"); if (typeof location === "string") { + // Location est un texte, on prend les valeurs par défaut label = location; } else { + // location est un objet : on vérifie les informations qu'il comporte + + // Récupère les infos (s'il y'en a) + index = location.type ? location.type : index; label = GeocodeUtils.getSuggestedLocationFreeform(location); + if (index === "PositionOfInterest") { + // Recherche d'un POI : ajout d'infos supplémentaires + let poiType = location.poiType[0]; + if (poiType === "administratif" && location.poiType[1]) { + poiType = location.poiType[1]; + } + filters.category = poiType ? poiType : null; + + // Retourne la géométrie pour certains types seulement + truegeometry = false; + const trueGeometries = ["administratif", "département", "construction", "hydrographie"]; + for (let i = 0; i < location.poiType.length; i++) { + const type = location.poiType[i]; + if (type !== "lieu-dit habité" && trueGeometries.includes(type)) { + truegeometry = true; + break; + } + } + } + filters.postalCode = location.postalCode ? location.postalCode : null; + + // Retire chaque valeurs nulles + for (const key in filters) { + if (!Object.hasOwn(filters, key)) { + continue; + }; + if (filters[key] === null) { + delete filters[key]; + } + } } - // on sauvegarde le localisant - this._currentGeocodingLocation = label; - // on centre la vue et positionne le marker, à la position reprojetée dans la projection de la carte + this._requestGeocoding({ - index : this.get("index"), + index : index, limit : this.get("limit"), - returnTrueGeometry : this.get("returnTrueGeometry"), + returnTrueGeometry : truegeometry, location : label, filters : filters, onSuccess : this._onSuccessSearch.bind(this), onFailure : this._onFailureSearch.bind(this, location), }); + // on sauvegarde le localisant + this._currentGeocodingLocation = label; } diff --git a/src/packages/Services/typedefs.js b/src/packages/Services/typedefs.js index f9744fb31..b171ee9c2 100644 --- a/src/packages/Services/typedefs.js +++ b/src/packages/Services/typedefs.js @@ -48,7 +48,7 @@ * @typedef {Object} IGNSearchObject * @property {AutocompleteResult|String} location - Objet utilisé pour faire la requête. * Peut être soit le résultat de l'autocomplétion ou une chaîne de caractère. - * @property {IGNSearchFilter} [filter] - Filtres optionnels pour + * @property {IGNSearchFilter} [filters] - Filtres optionnels pour la recherche */ /** @@ -57,7 +57,7 @@ * @typedef {Object} IGNSearchFilter * @property {string} [postalCode] - Code postal (adresses, toponymes). * @property {string} [inseeCode] - Code INSEE (adresses, toponymes). - * @property {string} [city] - Nom de la ville (adresses uniquement). + * @property {string} [city] - Nom de la ville (adresses, toponymes). * @property {string} [type] - Type de toponyme (toponymes uniquement). * @property {string} [codeDepartement] - Code département (parcelles cadastrales uniquement). * @property {string} [codeCommune] - Code commune (parcelles cadastrales uniquement). From 7aec5e150a1330d168e9e92cdc026a86192e6058 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 20 Nov 2025 15:50:00 +0100 Subject: [PATCH 49/73] =?UTF-8?q?fix(search):=20Enl=C3=A8ve=20ajout=20pret?= =?UTF-8?q?tify=20pour=20la=20recherche?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/Services/IGNSearchService.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/packages/Services/IGNSearchService.js b/src/packages/Services/IGNSearchService.js index 27cdebadc..bdef73543 100644 --- a/src/packages/Services/IGNSearchService.js +++ b/src/packages/Services/IGNSearchService.js @@ -403,7 +403,7 @@ class IGNSearchService extends AbstractSearchService { * @abstract */ search (object) { - let location = object.location; + const location = object.location; const filters = object.filters ? object.filters : {}; if (location === undefined) { @@ -422,6 +422,13 @@ class IGNSearchService extends AbstractSearchService { // Récupère les infos (s'il y'en a) index = location.type ? location.type : index; label = GeocodeUtils.getSuggestedLocationFreeform(location); + // TODO : AMÉLIORER CETTE PARTIE (REDONDANTE) + // Enlève l'ajout du prettify + if ((location.type === "PositionOfInterest" && location.poiType[0] === "administratif" && + (location.poiType[1] === "département" || location.poiType[1] === "région"))) { + label = label.substring(0, label.length - (location.poiType[1].length + 2)); + } + if (index === "PositionOfInterest") { // Recherche d'un POI : ajout d'infos supplémentaires let poiType = location.poiType[0]; From 00ca1f3a73d3986d3525c292dfd177e6e0921dfa Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 20 Nov 2025 16:00:45 +0100 Subject: [PATCH 50/73] =?UTF-8?q?fix(search):=20Chgt=20cours=20d'eau=20en?= =?UTF-8?q?=20hydrographie=20ic=C3=B4nes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/Controls/SearchEngine/SearchEngineBase.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 5c036a147..0e7af428a 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -12,8 +12,7 @@ const typeClasses = { "StreetAddress" : "fr-icon-map-pin-2-line", "PositionOfInterest" : { "administratif" : "fr-icon-france-fill", - "cours d'eau" : "fr-icon-ign-mer", - "construction" : "fr-icon-train-line", + "hydrographie" : "fr-icon-ign-mer", "default" : "fr-icon-map-pin-2-line", } }; From cc28320230a79f1198cadf4ae6c7f1331a0676d9 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 20 Nov 2025 17:04:58 +0100 Subject: [PATCH 51/73] fix(search): Fix bouton supprimer saisie IGNF/cartes.gouv.fr-editeur-carto/110 --- .../Controls/SearchEngine/SearchEngineAdvanced.js | 4 +++- .../Controls/SearchEngine/SearchEngineBase.js | 15 +++------------ 2 files changed, 6 insertions(+), 13 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index ffe4a1c13..3dc21ff1d 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -187,7 +187,7 @@ class SearchEngineAdvanced extends Control { // Focus sur le bouton de recherche avancée this.advancedBtn.focus(); } else { - this.baseSearchEngine.subimtBt.focus(); + this.eraseBtn.focus(); } } }.bind(this)); @@ -337,6 +337,8 @@ class SearchEngineAdvanced extends Control { eraseBtn.addEventListener("click", function () { this.baseSearchEngine.input.value = ""; delete this.baseSearchEngine.input.dataset.empty; + // Notifie l'input du changement + this.baseSearchEngine.input.dispatchEvent(new Event("input")); }.bind(this)); this.baseSearchEngine.optionscontainer.appendChild(eraseBtn); } diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 0e7af428a..9be6ee538 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -179,18 +179,9 @@ class SearchEngineBase extends Control { // Réaffiche la valeur précédente de l'utilisateur e.target.value = this._previousValue; } - break; - case "Enter": - // Lance la recherche - let item = list[idx]; - if (idx < 0) { - // Pas d'item sélectionné : on prend le premier de la liste - item = list[0]; - } - if (item) { - // Simule un clic sur l'élément sélectionné - item.click(); - } + // Envoie un événement de type input pour notifier le changement + this.input.dispatchEvent(new Event("input")); + break; default: if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { From ca437e6b6472b3f0949f7bd1ffda82fc03cc144c Mon Sep 17 00:00:00 2001 From: viglino Date: Thu, 20 Nov 2025 17:34:22 +0100 Subject: [PATCH 52/73] Fix: fetch numero on parcel section search --- .../SearchEngine/GPFadvancedSearchEngine.css | 1 - .../SearchEngine/ParcelAdvancedSearch.js | 171 +++++++++++++++--- 2 files changed, 146 insertions(+), 26 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index 0ba493416..fa7f8cb56 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -85,7 +85,6 @@ form[id^=GPAdvancedForm-] select option.option { form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .GPautoCompleteList { display: none; z-index: 1; - background-color: #fff; padding: 0; } form[id^=GPAdvancedForm-ParcelAdvancedSearch-] input[aria-expanded="true"] ~ .GPautoCompleteList { diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index df2c911fc..a4519f34e 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -83,13 +83,14 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { /** Show error message for a given input * @param {String} name Input name * @param {String} [message] Message to show (if none remove message) + * @param {String} [what="error"] Message type (error, warning, info) * @private */ - _showMessage (name, message) { + _showMessage (name, message, what) { const div = this.element.querySelector("[name='" + name + "']").parentElement.querySelector(".GPMessagesGroup"); if (message) { const msg = document.createElement("p"); - msg.className = "fr-message fr-message--error"; + msg.className = "fr-message fr-message--" + (what || "error"); msg.textContent = message; div.innerHTML = ""; div.appendChild(msg); @@ -98,6 +99,38 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } } + /** Change the section + * @private + */ + setSection () { + const prefix = this.prefixInput.value; + const section = this.sectionInput.value; + this.numberList.innerHTML = ""; + this._showMessage("section", "chargement en cours", "info"); + this._fetchCadastre(this.communeId, prefix, section).then(data => { + this._showMessage("section", ""); + const section = this.sectionInput.value; + if (data && data.features && data.features[0].properties.section === section) { + this.numberInput.focus(); + const numbers = []; + data.features.forEach(numero => numbers.push(numero.properties.numero)); + // Sort numbers + numbers.sort().forEach(numero => { + const option = document.createElement("li"); + option.value = option.textContent = numero.replace(/^0{1,4}/g,""); + option.addEventListener("click", () => { + this.numberInput.value = option.value; + this._onSearch(); + this.numberInput.blur(); + this.numberInput.ariaExpanded = "false"; + }); + this.numberList.appendChild(option); + }); + this.filterListNumber(); + } + }); + } + /** * Show sections for a given prefix * @param {String} prefix Prefix code @@ -110,10 +143,14 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { section.value = ""; section.textContent = "Sélectionner une section"; this.sectionInput.appendChild(section); + let previous = ""; this.feuilles[prefix].sort().forEach(key => { - const section = document.createElement("option"); - section.value = section.textContent = key; - this.sectionInput.appendChild(section); + if (key !== previous) { + const section = document.createElement("option"); + section.value = section.textContent = key; + this.sectionInput.appendChild(section); + previous = key; + } }); this.sectionInput.removeAttribute("disabled"); this.sectionInput.focus(); @@ -133,8 +170,10 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { this.communeId = id; prefixInput.innerHTML = ""; if (id) { + this._showMessage("commCode", "chargement en cours", "info"); // Fetch prefixes and sections for the selected commune - this._fetchFeuille(id).then(data => { + this._fetchCadastre(id).then(data => { + this._showMessage("commCode", ""); this.feuilles = {}; data.features.forEach(feuille => { @@ -143,10 +182,14 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } this.feuilles[feuille.properties.com_abs].push(feuille.properties.section); }); + let previous = ""; Object.keys(this.feuilles).sort().forEach(key => { - const prefix = document.createElement("option"); - prefix.value = prefix.textContent = key; - prefixInput.appendChild(prefix); + if (key !== previous) { + const prefix = document.createElement("option"); + prefix.value = prefix.textContent = key; + prefixInput.appendChild(prefix); + previous = key; + } }); this.setFeuille("000"); }); @@ -223,6 +266,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { if (index >= autoOptions.length) { index = -1; } + e.preventDefault(); break; } case "ArrowUp": { @@ -230,6 +274,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { if (index < -1) { index += autoOptions.length +1; } + e.preventDefault(); break; } case "Enter": { @@ -238,8 +283,6 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { autoOptions[index].click(); index = -1; } - e.preventDefault(); - e.stopPropagation(); break; } default: { @@ -247,7 +290,6 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } } if (selectedIndex !== index) { - e.preventDefault(); // Update selected option autoOptions[selectedIndex]?.classList.remove("active"); selectedIndex = index; @@ -261,9 +303,8 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { // Show autocomplete on input comCodeInput.addEventListener("keyup", e => { - e.preventDefault(); - e.stopPropagation(); if (["ArrowUp", "ArrowDown", "Enter", "Escape"].includes(e.key)) { + e.preventDefault(); return; } showAutocomplete(e); @@ -284,18 +325,22 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } else { this._clearMessages(); } + this._showMessage("commCode", "chargement en cours", "info"); this._fetchCommuneData(comCodeInput.value).then(data => { + this._showMessage("commCode", ""); // Clear previous suggestions autocompleteList.innerHTML = ""; communeName = ""; if (data.length === 0) { // errror message + this._showMessage("commCode", "Aucune commune ne correspond à ce code INSEE ou code postal."); + this.setCommune(); } else if (data.length === 1) { communeName = comCodeInput.value = `${data[0].code} (${data[0].nom})`; this.setCommune(data[0].code); } else { data.forEach(commune => { - const type = commune.codesPostaux ? "code postal" : "code INSEE"; + const type = commune.codesPostaux ? "code INSEE" : "code postal"; const option = document.createElement("li"); option.className = "GPautoCompleteOption"; option.setAttribute("role", "option"); @@ -319,14 +364,56 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { prefixInput.addEventListener("change", () => { this.setFeuille(prefixInput.value); }); - // Fetch parcelles + // Fetch parcelles number sectionInput.addEventListener("change", () => { if (sectionInput.value) { this.numberInput.removeAttribute("disabled"); + this.setSection(); } else { this.numberInput.setAttribute("disabled", "disabled"); } }); + + // Handle listbox for number input + this.numberInput.addEventListener("focus", () => { + this.numberInput.ariaExpanded = "true"; + }); + this.numberInput.addEventListener("blur", (e) => { + if (e.relatedTarget !== this.numberList) { + this.numberInput.ariaExpanded = "false"; + } + }); + + // Filter number list on keyup + this.numberInput.addEventListener("keyup", (e) => { + this.filterListNumber(); + if (e.key === "Enter") { + this.numberInput.ariaExpanded = "false"; + this.numberInput.blur(); + } + }); + } + + + /** Filter listbox options + */ + filterListNumber () { + const filter = this.numberInput.value.toUpperCase(); + const options = this.numberList.querySelectorAll("li"); + let hasNumber = false; + options.forEach(option => { + if (option.textContent.toUpperCase().indexOf(filter) > -1) { + option.style.display = ""; + hasNumber = true; + } else { + option.style.display = "none"; + } + }); + if (!hasNumber) { + this._showMessage("numero", "Aucun numéro de parcelle ne correspond à cette saisie."); + } else { + this._showMessage("numero", ""); + } } /** @@ -401,6 +488,9 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { return Promise.all(responses.map(res => res.json())).then(json => { return json[0].concat(json[1]); }); + }).catch(error => { + this._showMessage("commCode", "Une erreur est survenue lors de la récupération des données de la commune."); + return []; }); } @@ -408,9 +498,11 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * Récupère les feuilles cadastrales d'une commune via le WFS Geopf * @private * @param {String} code Code INSEE de la commune - * @returns + * @param {String} [prefix] Préfixe de la parcelle + * @param {String} [section] Section de la parcelle + * @returns {Promise} Promesse avec les données GeoJSON */ - async _fetchFeuille (code) { + async _fetchCadastre (code, prefix, section) { const domtom = ["97","98"].includes(code.slice(0,2)); const dep = code.slice(0, domtom ? 3 : 2); const com = code.slice(domtom ? 3 : 2, 5); @@ -419,16 +511,33 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { service : "WFS", version : "2.0.0", request : "GetFeature", - typename : "CADASTRALPARCELS.PARCELLAIRE_EXPRESS:feuille", + typename : "CADASTRALPARCELS.PARCELLAIRE_EXPRESS:" + (section ? "parcelle" : "feuille"), outputFormat : "application/json", srsName : "CRS:84", count : "1000", - propertyName : "com_abs,section", - cql_filter : `code_dep='${dep}' and code_com='${com}'` + propertyName : section ? "com_abs,section,numero" : "com_abs,section", + cql_filter : `code_dep='${dep}' and code_com='${com}'` + (section ? ` and com_abs='${prefix}' and section='${section}'` : "") }; const queryString = new URLSearchParams(params).toString(); const fullUrl = url + queryString; - const response = await fetch(fullUrl); + // Abort previous request + if (this.controller) { + this.controller.abort(); + } + // New request + this.controller = new AbortController(); + const response = await fetch(fullUrl, { + headers : { + "Content-Type" : "application/json", + }, + signal : this.controller.signal + }).catch(error => { + return null; + }); + this.controller = null; + if (!response) { + return {}; + } const data = await response.json(); return data; } @@ -504,9 +613,18 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { numberInput.name = "numero"; numberInput.title = "Numéro de la parcelle"; numberInput.autocomplete = "off"; + numberInput.ariaExpanded = "false"; numberInput.id = Helper.getUid("ParcelAdvancedSearch-number-"); numberInput.setAttribute("disabled", "disabled"); - this._getLabelContainer("Numéro*", "fr-input-group", numberInput, ""); + const numDiv = this._getLabelContainer("Numéro*", "fr-input-group", numberInput, ""); + + const numberList = this.numberList = document.createElement("ul"); + numberList.className = "GPautoCompleteList"; + numberList.id = Helper.getUid("GPautoCompleteList-"); + numberList.setAttribute("role", "listbox"); + numberList.setAttribute("tabindex", "-1"); + numberList.setAttribute("aria-label", "Propositions"); + numDiv.insertBefore(numberList, numberInput.nextSibling); } /** Do search @@ -515,7 +633,9 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * @param {PointerEvent} e Événement de soumission */ _onSearch (e) { - super._onSearch(e); + if (e) { + super._onSearch(e); + } this._clearMessages(); @@ -526,7 +646,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { const section = this.sectionInput.value; const number = this.numberInput.value; if (!prefix) { - this._showMessage("prefix", "Le préfixe est obligatoire."); + // this._showMessage("prefix", "Le préfixe est obligatoire."); return; } else if (!section) { this._showMessage("section", "La section est obligatoire."); @@ -558,6 +678,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { _onErase (e) { super._onErase(e); this.setCommune(); + this._clearMessages(); } } From bb5eab433b0d6fe15041354be86e472764aa082c Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Thu, 20 Nov 2025 18:22:38 +0100 Subject: [PATCH 53/73] =?UTF-8?q?feat(search):=20Gestion=20conteneur=20rec?= =?UTF-8?q?herche=20avanc=C3=A9e=20(focus=20et=20clic)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...ginebase-modules-dsfr-geocodeAdvanced.html | 1 + .../SearchEngine/SearchEngineAdvanced.js | 56 ++++++++++++++++++- 2 files changed, 54 insertions(+), 3 deletions(-) diff --git a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html index a06320a89..5341c5045 100644 --- a/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html +++ b/samples-src/pages/tests/SearchEngine/pages-ol-searchenginebase-modules-dsfr-geocodeAdvanced.html @@ -33,6 +33,7 @@

    Ajout du moteur de recherche avec les options par défaut

    Dernière sélection :

    + {{/content}} {{#content "js"}} diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 3dc21ff1d..5a0f06545 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -110,6 +110,13 @@ class SearchEngineAdvanced extends Control { */ this._searchForms; + /** + * Si vrai, écoute les clics sur le document pour gérer + * la modale de recherche avancée + * @type {Boolean} + */ + this.listenToClick = false; + if (options.advancedSearch && options.advancedSearch instanceof Array) { this._searchForms = options.advancedSearch; } else { @@ -193,6 +200,16 @@ class SearchEngineAdvanced extends Control { }.bind(this)); this.on("search", this.addResultToMap.bind(this)); + + // Gère le cas du conteneur de recherche avancée + ["mousedown", "focusin"].map(eventListener => document.addEventListener(eventListener, this._onDocumentClick.bind(this))); + + this.advancedBtn.addEventListener("blur", function (e) { + if (e.relatedTarget === this.baseSearchEngine.input) { + this.listenToClick = false; + this.advancedBtn.setAttribute("aria-expanded", false); + } + }.bind(this)); } /** @@ -314,12 +331,27 @@ class SearchEngineAdvanced extends Control { // Gestion du bouton avancé advancedBtn.setAttribute("aria-controls", advancedContainer.id); - advancedBtn.addEventListener("click", (e) => { + advancedBtn.addEventListener("click", function (/** @type {PointerEvent} */ e) { e.preventDefault(); const isHidden = advancedBtn.getAttribute("aria-expanded") === "false"; advancedBtn.setAttribute("aria-expanded", isHidden); - this.baseSearchEngine.setActive(isHidden); - }); + this.listenToClick = isHidden; + if (isHidden) { + // Si la modale est ouverte, on met le focus sur le premier élément focusable + const focusableSelectors = [ + "a[href]", + "button:not([disabled])", + "input:not([disabled])", + "select:not([disabled])", + "textarea:not([disabled])", + "[tabindex]:not([tabindex='-1'])" + ].join(","); + const firstFocusable = advancedContainer.querySelector(focusableSelectors); + if (firstFocusable) { + firstFocusable.focus(); + } + } + }.bind(this)); // N'ajoute pas le bouton s'il n'y a pas d'options avancées if (this._searchForms.length) { @@ -343,6 +375,24 @@ class SearchEngineAdvanced extends Control { this.baseSearchEngine.optionscontainer.appendChild(eraseBtn); } + /** + * Fonction active si la recherche avancée est active + * @param {PointerEvent} e Événement de clic sur le document + */ + _onDocumentClick (e) { + if (this.listenToClick === true) { + // Écoute des clics sur le document ==> recherche avancée active + const clickOnAdvancedContainer = (this.advancedContainer === e.target || this.advancedContainer.contains(e.target)); + const clickOnAdvancedBtn = this.advancedBtn === e.target; + if (!(clickOnAdvancedContainer || clickOnAdvancedBtn)) { + // On fait une action si un clic se produit en dehors du conteneur + // Et si le bouton de recherche avancée n'est pas cliqué + this.listenToClick = false; + this.advancedBtn.setAttribute("aria-expanded", false); + } + } + } + /** * Ajoute les résultats (features) sur la carte et ajuste la vue. * @param {Object} e Événement de recherche contenant result/extent From 1b078755f8c6d087ebe35723697281cf39a67d48 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 09:55:11 +0100 Subject: [PATCH 54/73] fix(search): Fix bouton effacer saisie (click sur item) + focus input fix IGNF/cartes.gouv.fr-editeur-carto#110 --- .../Controls/SearchEngine/SearchEngineAdvanced.js | 4 ++++ src/packages/Controls/SearchEngine/SearchEngineBase.js | 9 ++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 5a0f06545..d2b68c0be 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -371,6 +371,10 @@ class SearchEngineAdvanced extends Control { delete this.baseSearchEngine.input.dataset.empty; // Notifie l'input du changement this.baseSearchEngine.input.dispatchEvent(new Event("input")); + // Met le focus sur l'input + setTimeout(() => { + this.baseSearchEngine.input.focus(); + }, 50); }.bind(this)); this.baseSearchEngine.optionscontainer.appendChild(eraseBtn); } diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 9be6ee538..14b06a97e 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -376,7 +376,7 @@ class SearchEngineBase extends Control { acContainer.classList.add("gpf-hidden"); acContainer.classList.remove("GPelementVisible"); acContainer.classList.add("GPelementHidden"); - }, 100); + }, 50); } }, { once : true }); } @@ -387,7 +387,7 @@ class SearchEngineBase extends Control { acContainer.classList.add("gpf-hidden"); acContainer.classList.remove("GPelementVisible"); acContainer.classList.add("GPelementHidden"); - }, 100); + }, 50); } }); } @@ -459,6 +459,7 @@ class SearchEngineBase extends Control { clearTimeout(this._completeDelay); const title = this.getItemTitle(item); this.input.value = title; + this.input.dispatchEvent(new Event("input")); this._currentValue = title; this._updateHistoric(item); this._updateList(); @@ -513,9 +514,11 @@ class SearchEngineBase extends Control { li.append(span); } this.autocompleteList.appendChild(li); - li.addEventListener("click", function (e) { + li.addEventListener("click", function (/** @type {PointerEvent} */ e) { const idx = Number(e.target.getAttribute("data-idx")); + // Sélectionne l'item this.select(tab[idx]); + // Lance la recherche pour cet item this.search({ location : tab[idx] }); From 6f1e2790cd5aa5dcb309bc0bec5424ce1371529a Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 10:08:37 +0100 Subject: [PATCH 55/73] =?UTF-8?q?fix(search-advanced):=20Ferme=20modale=20?= =?UTF-8?q?recherche=20avanc=C3=A9e=20apr=C3=A8s=20une=20recherche=20fix?= =?UTF-8?q?=20IGNF/cartes.gouv.fr-editeur-carto#110?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/SearchEngine/SearchEngineAdvanced.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index d2b68c0be..3fff091b6 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -123,7 +123,8 @@ class SearchEngineAdvanced extends Control { this._searchForms = []; } - this._searchForms.forEach(search => { + this._searchForms.forEach(search => { + // Gère la recherche search.on("search", this.onAdvancedSearchResult.bind(this)); }); } @@ -374,7 +375,7 @@ class SearchEngineAdvanced extends Control { // Met le focus sur l'input setTimeout(() => { this.baseSearchEngine.input.focus(); - }, 50); + }, 50); }.bind(this)); this.baseSearchEngine.optionscontainer.appendChild(eraseBtn); } @@ -626,6 +627,9 @@ class SearchEngineAdvanced extends Control { * @private */ onAdvancedSearchResult (e) { + // Ferme la modale de recherche avancée + this.listenToClick = false; + this.advancedBtn.setAttribute("aria-expanded", false); if (e.result instanceof Array) { // TODO : GÉRER MULTIPLE RÉSULTATS } else if (e.result instanceof Feature) { From 5ea0c9c411d6b296ba4d3484163ba235fc49b1c4 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 10:40:43 +0100 Subject: [PATCH 56/73] =?UTF-8?q?fix(search):=20Chgt=20ic=C3=B4ne=20efface?= =?UTF-8?q?r=20saisie=20fix=20IGNF/cartes.gouv.fr-editeur-carto#110?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/Controls/SearchEngine/SearchEngineAdvanced.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 3fff091b6..a4afafb17 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -123,7 +123,7 @@ class SearchEngineAdvanced extends Control { this._searchForms = []; } - this._searchForms.forEach(search => { + this._searchForms.forEach(search => { // Gère la recherche search.on("search", this.onAdvancedSearchResult.bind(this)); }); @@ -361,7 +361,7 @@ class SearchEngineAdvanced extends Control { // Ajout des options avancées const eraseBtn = this.eraseBtn = document.createElement("button"); - eraseBtn.className = "GPSearchEngine-erase-btn fr-btn fr-btn--sm fr-icon-close-circle-line fr-btn--tertiary-no-outline"; + eraseBtn.className = "GPSearchEngine-erase-btn fr-btn fr-btn--sm fr-icon-close-circle-fill fr-btn--tertiary-no-outline"; eraseBtn.id = Helper.getUid("GPSearchEngine-erase-btn-"); eraseBtn.type = "button"; eraseBtn.title = "Effacer la saisie"; From 76026f1b2f0c8ec76cdcd44adf06bf42a5e77eca Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 11:50:56 +0100 Subject: [PATCH 57/73] fix(search): Ouvre popup lors d'une recherche (avancee ou non) fix IGNF/cartes.gouv.fr-editeur-carto#108 --- .../SearchEngine/SearchEngineAdvanced.js | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index a4afafb17..f295e725a 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -17,6 +17,7 @@ import mapPinIcon from "./map-pin-2-fill.svg"; import Feature from "ol/Feature"; import { Layer } from "ol/layer"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; +import Coordinate from "ol/coordinate.js"; /** Get style for features * @param {String|Array} color - Couleur du contour @@ -409,6 +410,7 @@ class SearchEngineAdvanced extends Control { if (!!e.result) { this.layer.getSource().addFeature(e.result); extent = e.result.getGeometry().getExtent(); + this._setPopupInfo(e.result); } if (!!e.extent) { this.layer.getSource().addFeature(e.extent); @@ -426,20 +428,18 @@ class SearchEngineAdvanced extends Control { } /** - * Callback lors de la sélection d'une feature (affiche le popup). - * @param {SelectEvent} e Événement de sélection - * @private + * Ajoute les infos au popup + * @param {Feature} [feature] Feature à ajouter. Si non fourni + * @param {Coordinate} [position] Position du popup */ - _onSelectElement (e) { - let position = e.mapBrowserEvent.coordinate; - if (e.selected.length) { + _setPopupInfo (feature, position) { + if (feature) { // Ferme l'ancien popup this.popup.setPosition(undefined); // Ajoute le popup - const feature = e.selected[0]; - if (feature.getGeometry().getType() === "Point") { + if (feature.getGeometry()?.getType() === "Point") { // Place le popup sur le point - position = feature.getGeometry().getCoordinates(); + position = feature.getGeometry()?.getCoordinates(); } this.popup.setPosition(position); this.setPopupContent(feature.get("infoPopup") || ""); @@ -453,6 +453,16 @@ class SearchEngineAdvanced extends Control { } } + /** + * Callback lors de la sélection d'une feature (affiche le popup). + * @param {SelectEvent} e Événement de sélection + * @private + */ + _onSelectElement (e) { + let position = e.mapBrowserEvent.coordinate; + this._setPopupInfo(e.selected.length ? e.selected[0] : null, position); + } + /** * Crée et retourne l'overlay popup pour afficher les infos de feature. * @private From 90f35ad6b717c80ea1a7f0f7615627ab1cba7bcb Mon Sep 17 00:00:00 2001 From: viglino Date: Fri, 21 Nov 2025 12:05:56 +0100 Subject: [PATCH 58/73] Fix load numero from section --- .../SearchEngine/ParcelAdvancedSearch.js | 112 ++++++++++++++++-- 1 file changed, 101 insertions(+), 11 deletions(-) diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index a4519f34e..14008af19 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -209,6 +209,13 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { _initEvents (options) { super._initEvents(options); + // Focus commune input on expand + this.on("expand", e => { + if (e.expanded) { + setTimeout(() => comCodeInput.focus(), 300); + } + }); + // Inputs const comCodeInput = this.comCodeInput; const autocompleteList = this.autocompleteList; @@ -220,8 +227,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { this.communeId = ""; // Autocomplete selected index let selectedIndex = -1; - // Autocomplete options - let autoOptions = []; + let parcelIndex = -1; // Show/hide autocomplete list const showAutocomplete = (b) => { @@ -234,19 +240,33 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } if (b) { comCodeInput.setAttribute("aria-expanded", "true"); - comCodeInput.setAttribute("aria-activedescendant", autoOptions[selectedIndex]?.id || ""); + comCodeInput.setAttribute("aria-activedescendant", autocompleteList.children[selectedIndex]?.id || ""); } else { comCodeInput.setAttribute("aria-expanded", "false"); - autoOptions[selectedIndex]?.classList.remove("active"); - comCodeInput.setAttribute("aria-activedescendant", autoOptions[selectedIndex]?.id || ""); + autocompleteList.children[selectedIndex]?.classList.remove("active"); + comCodeInput.setAttribute("aria-activedescendant", autocompleteList.children[selectedIndex]?.id || ""); selectedIndex = -1; } }; // Keyboard navigation on autocomplete list + let previousValue = ""; comCodeInput.addEventListener("keydown", e => { - autoOptions = autocompleteList.querySelectorAll(".GPautoCompleteOption"); + // Prevent default behavior for navigation keys if elements are disabled + if (e.key === "Tab") { + if (!e.shiftKey // backward tab + && comCodeInput.value // input not empty + && previousValue !== comCodeInput.value // value not changed + && prefixInput.getAttribute("disabled") === "disabled" // prefix disabled + ) { + e.preventDefault(); + comCodeInput.dispatchEvent(new Event("change")); + } + } + previousValue = comCodeInput.value; + // Autocomplete navigation + const autoOptions = autocompleteList.children; if (autoOptions.length) { let index = selectedIndex; switch (e.key) { @@ -385,20 +405,89 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { }); // Filter number list on keyup + this.numberInput.addEventListener("keydown", (e) => { + if (["ArrowUp", "ArrowDown", "Enter", "Escape"].includes(e.key)) { + e.preventDefault(); + return; + } + }); this.numberInput.addEventListener("keyup", (e) => { this.filterListNumber(); - if (e.key === "Enter") { - this.numberInput.ariaExpanded = "false"; - this.numberInput.blur(); + let index = parcelIndex; + switch (e.key) { + case "ArrowDown": { + // Next non hidden option + for (let i=parcelIndex +1; i < this.numberList.children.length; i++) { + if (this.numberList.children[i].style.display !== "none") { + index = i; + break; + } + } + if (index >= this.numberList.children.length) { + index = -1; + } + e.preventDefault(); + break; + } + case "ArrowUp": { + // Previous non hidden option + for (let i=parcelIndex -1; i >= -1; i--) { + if (i === -1 || this.numberList.children[i].style.display !== "none") { + index = i; + break; + } + } + if (index < -1) { + index += this.numberList.children.length +1; + } + e.preventDefault(); + break; + } + case "Escape": { + index = -1; + this.numberInput.ariaExpanded = "false"; + break; + } + case "Enter": { + //this.numberList.children[index]?.click(); + const value = this.numberList.children[index]?.value || ""; + if (value) { + this.numberInput.value = value; + this._onSearch(); + this.filterListNumber(); + index = -1; + setTimeout(() => this.numberInput.ariaExpanded = "false"); + } + break; + } + default: { + // reset selection + index = -1; + break; + } + } + if (parcelIndex !== index) { + // Update selected option + this.numberList.children[parcelIndex]?.classList.remove("active"); + parcelIndex = index; + this.numberList.children[parcelIndex]?.classList.add("active"); + this.numberInput.setAttribute("aria-activedescendant", this.numberList.children[parcelIndex]?.id || ""); + // Scroll to selected option + this.numberInput.ariaExpanded = "true"; + this.numberList.children[parcelIndex]?.scrollIntoView({ block : "nearest" }); } }); } - /** Filter listbox options + /** Filter listbox options on input value */ filterListNumber () { const filter = this.numberInput.value.toUpperCase(); + if (this.currentFilter === filter) { + return; + } + this.currentFilter = filter; const options = this.numberList.querySelectorAll("li"); let hasNumber = false; options.forEach(option => { @@ -413,6 +502,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { this._showMessage("numero", "Aucun numéro de parcelle ne correspond à cette saisie."); } else { this._showMessage("numero", ""); + this.numberInput.ariaExpanded = "true"; } } @@ -500,7 +590,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * @param {String} code Code INSEE de la commune * @param {String} [prefix] Préfixe de la parcelle * @param {String} [section] Section de la parcelle - * @returns {Promise} Promesse avec les données GeoJSON + * @returns {Promise} Promesse avec les données GeoJSON (feuilles ou parcelles si section renseignée) */ async _fetchCadastre (code, prefix, section) { const domtom = ["97","98"].includes(code.slice(0,2)); From 4c0566ef297b040c3fa1fcdcb89fd9c2be3462fe Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 14:47:46 +0100 Subject: [PATCH 59/73] fix(search): Enleve import Coordinate (bug) --- src/packages/Controls/SearchEngine/SearchEngineAdvanced.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index f295e725a..d9672b458 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -17,7 +17,6 @@ import mapPinIcon from "./map-pin-2-fill.svg"; import Feature from "ol/Feature"; import { Layer } from "ol/layer"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; -import Coordinate from "ol/coordinate.js"; /** Get style for features * @param {String|Array} color - Couleur du contour @@ -430,7 +429,7 @@ class SearchEngineAdvanced extends Control { /** * Ajoute les infos au popup * @param {Feature} [feature] Feature à ajouter. Si non fourni - * @param {Coordinate} [position] Position du popup + * @param {Number[]} [position] Position du popup */ _setPopupInfo (feature, position) { if (feature) { From d3dcaacb2f4206f3c34e67319cdedd285a55929e Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 17:44:05 +0100 Subject: [PATCH 60/73] fix(search): Ajout bordure extent + corrige service ign close #116 --- .../Controls/SearchEngine/SearchEngineAdvanced.js | 13 ++++++++++--- .../Controls/SearchEngine/SearchEngineBase.js | 2 +- src/packages/Services/IGNSearchService.js | 2 +- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index d9672b458..7b0f601ce 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -21,9 +21,10 @@ import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; /** Get style for features * @param {String|Array} color - Couleur du contour * @param {String|Array} [fillColor] - Couleur de remplissage + * @param {Number} [offset = 0] - Décalage de la ligne. Par défaut, 0 * @returns {Style} Style OpenLayers */ -function getStyle (color, fillColor) { +function getStyle (color, fillColor, offset = 0) { return new Style({ image : new Icon({ src : mapPinIcon, @@ -32,8 +33,9 @@ function getStyle (color, fillColor) { }), stroke : new Stroke({ color : color, - lineDash : [8, 8], + lineDash : [8, 8], width : 2, + lineDashOffset : offset }), fill : new Fill({ color : fillColor || "rgba(0, 0, 0, 0.1)", @@ -74,7 +76,7 @@ class SearchEngineAdvanced extends Control { this.layer = new Vector({ source : new VectorSource({}), zIndex : Infinity, - style : getStyle([0, 0, 145, 1]), + style : [getStyle([255, 255, 255, 1]), getStyle([0, 0, 145, 1], null, 8)], }); this.selectInteraction = new Select({ @@ -409,6 +411,7 @@ class SearchEngineAdvanced extends Control { if (!!e.result) { this.layer.getSource().addFeature(e.result); extent = e.result.getGeometry().getExtent(); + this.selectInteraction.getFeatures().push(e.result); this._setPopupInfo(e.result); } if (!!e.extent) { @@ -435,12 +438,16 @@ class SearchEngineAdvanced extends Control { if (feature) { // Ferme l'ancien popup this.popup.setPosition(undefined); + let offset = null; // Ajoute le popup if (feature.getGeometry()?.getType() === "Point") { // Place le popup sur le point position = feature.getGeometry()?.getCoordinates(); + // TODO : AMÉLIORER L'OFFSET + offset = [0, -20]; } this.popup.setPosition(position); + offset && this.popup.setOffset(offset); this.setPopupContent(feature.get("infoPopup") || ""); this.popup.set("feature", feature); this.popup.set("layer", this.layer); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 14b06a97e..b2c85e161 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -11,7 +11,7 @@ const history = "fr-icon-history-line"; const typeClasses = { "StreetAddress" : "fr-icon-map-pin-2-line", "PositionOfInterest" : { - "administratif" : "fr-icon-france-fill", + "administratif" : "fr-icon-france-line", "hydrographie" : "fr-icon-ign-mer", "default" : "fr-icon-map-pin-2-line", } diff --git a/src/packages/Services/IGNSearchService.js b/src/packages/Services/IGNSearchService.js index bdef73543..6c0f0a152 100644 --- a/src/packages/Services/IGNSearchService.js +++ b/src/packages/Services/IGNSearchService.js @@ -556,7 +556,7 @@ class IGNSearchService extends AbstractSearchService { if (location.placeAttributes.truegeometry) { let geom = location.placeAttributes.truegeometry; if (typeof geom === "string") { - JSON.parse(geom); + geom = JSON.parse(geom); } let format = new GeoJSON(); From 98209dcd72cfcee8abd2e7e4b4cbca8beffc05b6 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Fri, 21 Nov 2025 18:41:02 +0100 Subject: [PATCH 61/73] =?UTF-8?q?feat(search):=20Ajout=20ic=C3=B4nes=20pou?= =?UTF-8?q?r=20diff=C3=A9rents=20types=20de=20recherche=20sur=20les=20tran?= =?UTF-8?q?sports?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DSFRadvancedSearchEngineStyle.css | 23 +++++- .../SearchEngine/DSFRsearchEngineStyle.css | 4 +- .../SearchEngine/img/dsfr/bike-line.svg | 3 + .../img/dsfr/parking-box-line.svg | 3 + .../SearchEngine/img/dsfr/plane-line.svg | 3 + .../SearchEngine/img/dsfr/subway-line.svg | 3 + .../Controls/SearchEngine/SearchEngineBase.js | 71 ++++++++++++++----- 7 files changed, 90 insertions(+), 20 deletions(-) create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg create mode 100644 src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index 900a31f47..a57535edc 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -93,9 +93,30 @@ div[id^=GPsearchEngine-AdvancedContainer-] .fr-accordion .fr-collapse { -webkit-mask-image: url("./img/dsfr/map-pin-add-line.svg"); mask-image: url("./img/dsfr/map-pin-add-line.svg"); } - .fr-icon-map-pin-add-fill::before, .fr-icon-map-pin-add-fill::after { -webkit-mask-image: url("./img/dsfr/map-pin-add-fill.svg"); mask-image: url("./img/dsfr/map-pin-add-fill.svg"); +} + +.fr-icon-bike-line::before, +.fr-icon-bike-line::after { + -webkit-mask-image: url("./img/dsfr/bike-line.svg"); + mask-image: url("./img/dsfr/bike-line.svg"); +} + +.fr-icon-parking-box-line::before, +.fr-icon-parking-box-line::after { + -webkit-mask-image: url("./img/dsfr/parking-box-line.svg"); + mask-image: url("./img/dsfr/parking-box-line.svg"); +} +.fr-icon-plane-line::before, +.fr-icon-plane-line::after { + -webkit-mask-image: url("./img/dsfr/plane-line.svg"); + mask-image: url("./img/dsfr/plane-line.svg"); +} +.fr-icon-subway-line::before, +.fr-icon-subway-line::after { + -webkit-mask-image: url("./img/dsfr/subway-line.svg"); + mask-image: url("./img/dsfr/subway-line.svg"); } \ No newline at end of file diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css index 80941ee26..2cee62301 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRsearchEngineStyle.css @@ -412,11 +412,11 @@ button[id^="GPSearchEngine-erase-btn"] { display: none; } -form.GPSearchBar > .GPInputGroup > input[data-empty] ~ .GPOptionsContainer > button[id^="GPSearchEngine-erase-btn"] { +form.GPSearchBar > .GPInputGroup > input[data-erase] ~ .GPOptionsContainer > button[id^="GPSearchEngine-erase-btn"] { display:block; } -form.GPSearchBar > .GPInputGroup > input[data-empty] ~ .GPOptionsContainer > button[id^="GPSearchEngine-advanced-btn"] { +form.GPSearchBar > .GPInputGroup > input[data-erase] ~ .GPOptionsContainer > button[id^="GPSearchEngine-advanced-btn"] { display:none; } diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg new file mode 100644 index 000000000..f05c49135 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/bike-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg new file mode 100644 index 000000000..a27d8b801 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/parking-box-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg new file mode 100644 index 000000000..7a554dbd5 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/plane-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg b/src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg new file mode 100644 index 000000000..81d96f657 --- /dev/null +++ b/src/packages/CSS/Controls/SearchEngine/img/dsfr/subway-line.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index b2c85e161..8251c3210 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -8,12 +8,35 @@ import Helper from "../../Utils/Helper"; // Voir les typedefs partagés dans ./typedefs.js (SearchEngineBaseOptions, SearchServiceOptions, ...) const history = "fr-icon-history-line"; +const defaultIcon = "fr-icon-map-pin-2-line"; const typeClasses = { "StreetAddress" : "fr-icon-map-pin-2-line", "PositionOfInterest" : { "administratif" : "fr-icon-france-line", "hydrographie" : "fr-icon-ign-mer", - "default" : "fr-icon-map-pin-2-line", + "transport" : { + "télécabine, téléphérique transport par câble" : "fr-icon-subway-line", + "gare voyageurs uniquement" : "fr-icon-subway-line", + "voyageurs et fret" : "fr-icon-subway-line", + "station de métro" : "fr-icon-subway-line", + "gare fret uniquement" : "fr-icon-subway-line", + "gare routière" : "fr-icon-subway-line", + "station de tramway" : "fr-icon-subway-line", + "arrêt voyageurs" : "fr-icon-subway-line", + + "aérodrome" : "fr-icon-plane-line", + "héliport" : "fr-icon-plane-line", + "altiport" : "fr-icon-plane-line", + "aérogare" : "fr-icon-plane-line", + + "port" : "fr-icon-ship-2-line", + "gare maritime" : "fr-icon-ship-2-line", + + "parking" : "fr-icon-parking-box-line", + "aire de repos ou de service" : "fr-icon-parking-box-line", + + "service dédié aux vélos" : "fr-icon-bike-line", + } } }; @@ -100,7 +123,7 @@ class SearchEngineBase extends Control { initialize (options) { // Valeurs par défaut des options options.minChars = options.minChars ? options.minChars : 3; - options.maximumEntries = (typeof options.maximumEntries === "number") ? options.maximumEntries : 5; + options.maximumEntries = (typeof options.maximumEntries === "number") ? options.maximumEntries : 5; options.historic = (typeof options.historic === "string" ? options.historic : this.CLASSNAME); options.title = options.title ? options.title : "Rechercher"; options.ariaLabel = options.ariaLabel ? options.ariaLabel : "Rechercher"; @@ -124,11 +147,7 @@ class SearchEngineBase extends Control { if (this.searchService.get("autocomplete") !== false) { // Empty input this.input.addEventListener("input", function (e) { - if (e.target.value.length != 0) { - e.target.dataset.empty = true; - } else { - delete e.target.dataset.empty; - } + e.target.dataset.erase = true; if (!e.target.value) { this.showHistoric(); } @@ -186,7 +205,7 @@ class SearchEngineBase extends Control { default: if (e.target.value.length && e.target.value.length >= options.minChars && e.target.value !== this._currentValue) { this.autocomplete(e.target.value); - } + } break; } this._currentValue = e.target.value; @@ -259,7 +278,7 @@ class SearchEngineBase extends Control { } element.appendChild(this.button); } - + element.appendChild(container); const search = document.createElement("div"); @@ -268,7 +287,6 @@ class SearchEngineBase extends Control { search.classList.add(options.search ? "fr-input" : "fr-input-group"); container.appendChild(search); - // Input const input = this.input = document.createElement("input"); input.type = "text"; @@ -299,7 +317,7 @@ class SearchEngineBase extends Control { messages.id = Helper.getUid("GPMessagesGroup-"); input.setAttribute("aria-describedby", messages.id); search.appendChild(messages); - + // Options container this.optionscontainer = document.createElement("div"); this.optionscontainer.className = "GPOptionsContainer"; @@ -350,6 +368,7 @@ class SearchEngineBase extends Control { if (this.searchService.get("autocomplete") !== false) { input.addEventListener("focus", () => { + input.dataset.erase = true; input.setAttribute("aria-expanded", "true"); acContainer.classList.add("gpf-visible"); acContainer.classList.remove("gpf-hidden"); @@ -366,10 +385,15 @@ class SearchEngineBase extends Control { input.focus(); } else { // Ajout d'un event listener pour retourner sur l'input en cas de besoin - e.relatedTarget.addEventListener("blur", (e) => { + e.relatedTarget.addEventListener("blur", (/** @type {FocusEvent}*/ e) => { if (e.relatedTarget && acContainer.contains(e.relatedTarget) || e.relatedTarget === input) { input.focus(); } else { + // On doit aller sur le bouton recherche avancée + if (input.value.length === 0) { + delete input.dataset.erase; + e.relatedTarget.parentNode.querySelector("button")?.focus(); + } setTimeout(() => { input.setAttribute("aria-expanded", "false"); acContainer.classList.remove("gpf-visible"); @@ -381,6 +405,7 @@ class SearchEngineBase extends Control { }, { once : true }); } } else { + input.value.length === 0 && delete input.dataset.erase; setTimeout(() => { input.setAttribute("aria-expanded", "false"); acContainer.classList.remove("gpf-visible"); @@ -463,8 +488,8 @@ class SearchEngineBase extends Control { this._currentValue = title; this._updateHistoric(item); this._updateList(); - this.dispatchEvent({ - type : "select", + this.dispatchEvent({ + type : "select", title : this.getItemTitle(item), item : item }); @@ -523,7 +548,7 @@ class SearchEngineBase extends Control { location : tab[idx] }); }.bind(this)); - }); + }); } /** @@ -545,7 +570,19 @@ class SearchEngineBase extends Control { break; } } - iconClass = typeof iconClass === "object" ? iconClass["default"] : iconClass; + + // TODO : améliorer la fonction (faire récursif ?) + if (typeof iconClass === "object") { + // Cherche les types de POI + for (let i = 0; i < item.poiType.length; i++) { + const poiType = item.poiType[i]; + if (Object.hasOwn(iconClass, poiType)) { + iconClass = iconClass[poiType]; + break; + } + } + } + iconClass = typeof iconClass === "object" ? defaultIcon : iconClass; } return iconClass; } @@ -621,7 +658,7 @@ class SearchEngineBase extends Control { p.className = `GPMessage GPMessage--${messageType} fr-message fr-message--${messageType}`; p.id = Helper.getUid("GPMessage-"); p.textContent = message; - + messageElement.replaceChildren(p); } } From 01f7b126acd73e29768f857ec3ff560330a18d89 Mon Sep 17 00:00:00 2001 From: viglino Date: Mon, 24 Nov 2025 17:06:24 +0100 Subject: [PATCH 62/73] Fix: fetch parcel number delay on key down Fix: accessibility / set number ascendant --- .../SearchEngine/ParcelAdvancedSearch.js | 35 ++++++++++++++----- 1 file changed, 26 insertions(+), 9 deletions(-) diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index 14008af19..4a90106f2 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -2,7 +2,6 @@ import def from "ajv/dist/vocabularies/discriminator"; import InseeSearchService from "../../Services/InseeSearchService"; import Helper from "../../Utils/Helper"; import AbstractAdvancedSearch from "./AbstractAdvancedSearch"; -import { couldStartTrivia } from "typescript"; import IGNSearchService from "../../Services/IGNSearchService"; /** @@ -118,11 +117,13 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { numbers.sort().forEach(numero => { const option = document.createElement("li"); option.value = option.textContent = numero.replace(/^0{1,4}/g,""); + option.id = Helper.getUid("GPautoCompleteOption-"); option.addEventListener("click", () => { this.numberInput.value = option.value; this._onSearch(); this.numberInput.blur(); this.numberInput.ariaExpanded = "false"; + this.numberInput.setAttribute("aria-activedescendant", ""); }); this.numberList.appendChild(option); }); @@ -384,15 +385,30 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { prefixInput.addEventListener("change", () => { this.setFeuille(prefixInput.value); }); + // Fetch parcelles number - sectionInput.addEventListener("change", () => { - if (sectionInput.value) { - this.numberInput.removeAttribute("disabled"); - this.setSection(); - } else { - this.numberInput.setAttribute("disabled", "disabled"); - } - }); + let selectTout = null; + let hascliked = false; + // Handle click and change separately to manage click on the list + sectionInput.addEventListener("click", () => { + hascliked = true; + }) + sectionInput.addEventListener("change", e => { + // Wait for click event to be handled first + setTimeout(() => { + clearTimeout(selectTout); + // If clicked, do it immediately, else wait a bit (use key to navigate the list) + selectTout = setTimeout(() => { + if (sectionInput.value) { + this.numberInput.removeAttribute("disabled"); + this.setSection(); + } else { + this.numberInput.setAttribute("disabled", "disabled"); + } + }, hascliked ? 0 : 500); + }); + hascliked = false; + }) // Handle listbox for number input this.numberInput.addEventListener("focus", () => { @@ -474,6 +490,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { this.numberInput.setAttribute("aria-activedescendant", this.numberList.children[parcelIndex]?.id || ""); // Scroll to selected option this.numberInput.ariaExpanded = "true"; + this.numberInput.setAttribute("aria-activedescendant", this.numberList.children[parcelIndex]?.id || ""); this.numberList.children[parcelIndex]?.scrollIntoView({ block : "nearest" }); } }); From a9809dd37004fdf60649af5277d521cab7fd65e0 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Tue, 25 Nov 2025 16:10:35 +0100 Subject: [PATCH 63/73] fix(search): Modif focus visible input / btn effacer + corrige rech. coord fix IGNF/cartes.gouv.fr-editeur-carto#113 fix IGNF/cartes.gouv.fr-editeur-carto#110 --- .../DSFRadvancedSearchEngineStyle.css | 11 ++++++ .../SearchEngine/CoordinateAdvancedSearch.js | 39 ++++++++++++++++--- .../SearchEngine/SearchEngineAdvanced.js | 7 ++++ .../Controls/SearchEngine/SearchEngineBase.js | 10 +++-- 4 files changed, 58 insertions(+), 9 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css index a57535edc..ae7fb8821 100644 --- a/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css +++ b/src/packages/CSS/Controls/SearchEngine/DSFRadvancedSearchEngineStyle.css @@ -119,4 +119,15 @@ div[id^=GPsearchEngine-AdvancedContainer-] .fr-accordion .fr-collapse { .fr-icon-subway-line::after { -webkit-mask-image: url("./img/dsfr/subway-line.svg"); mask-image: url("./img/dsfr/subway-line.svg"); +} + +[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"].GPSearchBar > .GPInputGroup:has(> .GPsearchInputText:focus:focus-visible) { + outline-offset: 2px; + outline-width: 2px; + outline-color: #0a76f6; + outline-style: solid; +} + +[id^="GPsearchEngine"] form[id^="GPsearchInput-Base-"].GPSearchBar > .GPInputGroup >.GPsearchInputText:focus:focus-visible { + outline-style: none; } \ No newline at end of file diff --git a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js index b3425720e..b5a1b48c5 100644 --- a/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/CoordinateAdvancedSearch.js @@ -50,6 +50,9 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { this._initCoordinateSearchSystems(options); this._initCoordinateSearchUnits(options); + this._boundOnLonLatBeforeInput = this._onlonLatBeforeInput.bind(this); + this._boundOnLonLatInput = this._onlonLatInput.bind(this); + this._currentCoordinateSystem = this._coordinateSearchSystems[0]; this._currentUnit = this._coordinateSearchUnits[this._currentCoordinateSystem.type]; @@ -496,23 +499,32 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { if (this.get("unitType") === "Metric") { factor = unit === "KM" ? 0.001 : 1000; } + // TODO : Faire convertion ?! if (unit === "DMS") { this.lonLatInputs.querySelectorAll("input").forEach(input => { input.value = ""; input.minLength = "6"; input.maxLength = "6"; - input.addEventListener("beforeinput", this._onlonLatBeforeInput.bind(this)); - input.addEventListener("input", this._onlonLatInput.bind(this)); + input.addEventListener("beforeinput", this._boundOnLonLatBeforeInput); + input.addEventListener("input", this._boundOnLonLatInput); }); } else { this.lonLatInputs.querySelectorAll("input").forEach(input => { - input.value = input.value === "" || isNaN(input.value) ? "" : parseFloat(input.value) * factor; + if (unit === "DEC") { + input.value = ""; + } else { + input.value = input.value === "" || isNaN(input.value) ? "" : parseFloat(input.value) * factor; + } input.removeAttribute("minLength"); input.removeAttribute("maxLength"); - input.removeEventListener("beforeinput", this._onlonLatBeforeInput); - input.removeEventListener("input", this._onlonLatInput); + input.removeEventListener("beforeinput", this._boundOnLonLatBeforeInput); + input.removeEventListener("input", this._boundOnLonLatInput); }); } + // Réinitialise le mask de l'input (dans tous les cas) + this.getContent().querySelectorAll(".display-mask").forEach(mask => { + mask.textContent = "__°__'__\""; + }); } /** @@ -525,7 +537,8 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { // const regex = /^(?:\d{2}°\d{2}'\d{2}(?:"|''))$|^\d{6}$/; const regex = /^\d+$/; // Vérifie si c'est un chiffre - if (e.inputType.startsWith("insert") && !regex.test(e.data)) { + // TODO : améliorer cela ? vis à vis du insertLineBreak (touche entrée) + if (e.inputType.startsWith("insert") && e.inputType != "insertLineBreak" && !regex.test(e.data)) { e.preventDefault(); } } @@ -549,6 +562,7 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { * @param {InputEvent} e Événement input */ _onlonLatInput (e) { + console.log("on lon lat inputs"); const value = e.target.value; const mask = e.target.parentElement.querySelector(".display-mask"); mask.textContent = this._format(value); @@ -643,6 +657,19 @@ class CoordinateAdvancedSearch extends AbstractAdvancedSearch { return infoPopup; } + + /** + * Réinitialise les champs du formulaire. + * @param {PointerEvent} e Événement d'effacement + * @protected + */ + _onErase (e) { + super._onErase(e); + this.getContent().querySelectorAll(".display-mask").forEach(mask => { + mask.textContent = "__°__'__\""; + }); + } + } export default CoordinateAdvancedSearch; diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index 7b0f601ce..ba8afac93 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -213,6 +213,12 @@ class SearchEngineAdvanced extends Control { this.advancedBtn.setAttribute("aria-expanded", false); } }.bind(this)); + + this.baseSearchEngine.input.addEventListener("blur", function (/** @type {FocusEvent} */e) { + if (e.relatedTarget && e.relatedTarget === this.eraseBtn) { + e.target.dispatchEvent(new Event("input")); + } + }.bind(this)); } /** @@ -375,6 +381,7 @@ class SearchEngineAdvanced extends Control { // Notifie l'input du changement this.baseSearchEngine.input.dispatchEvent(new Event("input")); // Met le focus sur l'input + console.log("click"); setTimeout(() => { this.baseSearchEngine.input.focus(); }, 50); diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 8251c3210..6ba1fc3cf 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -147,7 +147,12 @@ class SearchEngineBase extends Control { if (this.searchService.get("autocomplete") !== false) { // Empty input this.input.addEventListener("input", function (e) { - e.target.dataset.erase = true; + if (e.target.value.length > 0) { + e.target.dataset.erase = true; + } else { + delete e.target.dataset.erase; + } + console.log("input", e, e.target.value); if (!e.target.value) { this.showHistoric(); } @@ -336,7 +341,7 @@ class SearchEngineBase extends Control { } // Autocomplete container - const acContainer = document.createElement("div"); + const acContainer = this.acContainer = document.createElement("div"); acContainer.id = Helper.getUid("GPautoCompleteContainer-"); acContainer.className = "GPautoCompleteContainer GPelementHidden gpf-hidden"; element.appendChild(acContainer); @@ -368,7 +373,6 @@ class SearchEngineBase extends Control { if (this.searchService.get("autocomplete") !== false) { input.addEventListener("focus", () => { - input.dataset.erase = true; input.setAttribute("aria-expanded", "true"); acContainer.classList.add("gpf-visible"); acContainer.classList.remove("gpf-hidden"); From 693ff9c9ccde23f1420bc36e993e4debf7390cbb Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 26 Nov 2025 14:39:40 +0100 Subject: [PATCH 64/73] fix(search): Ajout msg erreurs rech. av. code INSEE fix IGNF/cartes.gouv.fr-editeur-carto#143 --- .../SearchEngine/InseeAdvancedSearch.js | 18 ++++++++++++++---- .../Controls/SearchEngine/SearchEngineBase.js | 14 ++++++++++++++ src/packages/Services/InseeSearchService.js | 5 +++++ 3 files changed, 33 insertions(+), 4 deletions(-) diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js index ee70e5938..4df75b832 100644 --- a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js @@ -26,7 +26,12 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { super(options); this.inseeInput.on("search", function (e) { - this.dispatchEvent(e); + if (e.result) { + this.dispatchEvent(e); + } else { + // Pas de réponse, on affiche un message d'erreur + this.inseeInput.addMessage("Ce code INSEE est introuvable."); + } }.bind(this)); } @@ -42,6 +47,8 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { } super.initialize(options); + this._inputPattern = /^(\d\d|2[A,B,a,b])\d{3}$/; + /** * Nom de la classe (heritage) * @private @@ -71,9 +78,13 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { }) }); - this.inseeInput.input.pattern = "(\\d\\d|2[A,B,a,b])\\d{3}"; this.inseeInput.input.title = "Code INSEE sur 5 caractères"; + // TODO : laisser validation directe par l'ordi ? + // this.inseeInput.input.pattern = "^(\\d\\d|2[A,B,a,b])\\d{3}$"; + // this.inseeInput.input.maxLength = 5; + // this.inseeInput.input.minLength = 5; + this.inputs.push(inseeInput); } @@ -101,10 +112,9 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { */ _onSearch (e) { super._onSearch(e); - const pattern = this.inseeInput.input.pattern; const insee = this.inseeInput.input.value; - if (RegExp(pattern).test(insee)) { + if (this._inputPattern.test(insee)) { this.inseeInput.removeMessages(); this.inseeInput.search({ location : insee, diff --git a/src/packages/Controls/SearchEngine/SearchEngineBase.js b/src/packages/Controls/SearchEngine/SearchEngineBase.js index 6ba1fc3cf..8a0c6ad66 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineBase.js +++ b/src/packages/Controls/SearchEngine/SearchEngineBase.js @@ -663,6 +663,13 @@ class SearchEngineBase extends Control { p.id = Helper.getUid("GPMessage-"); p.textContent = message; + // Enlève la classe du type de message à l'élément parent + messageElement.parentElement?.classList.forEach(c => { + if (/^fr-.*-group$/.test(c)) { + messageElement.parentElement?.classList.add(`${c}--${messageType}`); + } + }); + messageElement.replaceChildren(p); } } @@ -676,6 +683,13 @@ class SearchEngineBase extends Control { let messageElement = this.input.ariaDescribedByElements[0]; if (messageElement) { messageElement.replaceChildren(); + + // Enlève la classe du type de message à l'élément parent + messageElement.parentElement?.classList.forEach(c => { + if (/^fr-.*-group--/.test(c)) { + messageElement.parentElement?.classList.remove(c); + } + }); } } diff --git a/src/packages/Services/InseeSearchService.js b/src/packages/Services/InseeSearchService.js index 9b28bf948..38084e68e 100644 --- a/src/packages/Services/InseeSearchService.js +++ b/src/packages/Services/InseeSearchService.js @@ -84,6 +84,11 @@ class InseeSearchService extends AbstractSearchService { }; this.ignService.search(obj); + } else { + // Pas de résultat, on envoi un événement "search" vide + this._onSearch({ + type : this.SEARCH_EVENT, + }); } }); } From 1745e07dfdc3b1564ec39fc0d9c00e3003723ef2 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 26 Nov 2025 15:47:21 +0100 Subject: [PATCH 65/73] =?UTF-8?q?fix(search):=20Ferme=20les=20accord=C3=A9?= =?UTF-8?q?ons=20de=20rech.=20av.=20apr=C3=A8s=20recherche=20fix=20IGNF/ca?= =?UTF-8?q?rtes.gouv.fr-editeur-carto#110?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Controls/SearchEngine/SearchEngineAdvanced.js | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js index ba8afac93..571612a8c 100644 --- a/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js +++ b/src/packages/Controls/SearchEngine/SearchEngineAdvanced.js @@ -653,6 +653,8 @@ class SearchEngineAdvanced extends Control { // Ferme la modale de recherche avancée this.listenToClick = false; this.advancedBtn.setAttribute("aria-expanded", false); + // Ferme les sections + this.closeAllSections(); if (e.result instanceof Array) { // TODO : GÉRER MULTIPLE RÉSULTATS } else if (e.result instanceof Feature) { @@ -660,6 +662,19 @@ class SearchEngineAdvanced extends Control { } } + /** + * Ferme toutes les accordéons et affiche les sections + */ + closeAllSections () { + this.advancedContainer.dataset.open = false; + this.advancedContainer.querySelectorAll("section").forEach(section => { + const btn = section.querySelector(".fr-accordion__title > button[aria-expanded]"); + btn.setAttribute("aria-expanded", "false"); + btn.ariaControlsElements[0].classList.remove("fr-collapse--expanded"); + section.classList.remove("fr-hidden"); + }); + } + } export default SearchEngineAdvanced; From d0209d8727d5440bfc493f9fa2b87c9bf8d651d2 Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 26 Nov 2025 16:23:02 +0100 Subject: [PATCH 66/73] Fix recherche parcelles / arrondissements Fix design: https://github.com/IGNF/cartes.gouv.fr-editeur-carto/issues/114 --- .../SearchEngine/GPFadvancedSearchEngine.css | 4 + .../SearchEngine/ParcelAdvancedSearch.js | 91 +++++++++++++++---- 2 files changed, 78 insertions(+), 17 deletions(-) diff --git a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css index fa7f8cb56..c8f99e98f 100644 --- a/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css +++ b/src/packages/CSS/Controls/SearchEngine/GPFadvancedSearchEngine.css @@ -100,4 +100,8 @@ form[id^=GPAdvancedForm-ParcelAdvancedSearch-] [id^=ParcelAdvancedSearch-section color: var(--text-disabled-grey); font-style: italic; } +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .fr-select-group:has(select:disabled) label, +form[id^=GPAdvancedForm-ParcelAdvancedSearch-] .fr-input-group:has(input:disabled) label { + color: var(--text-disabled-grey); +} diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index 4a90106f2..a6cf3dd2e 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -23,7 +23,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { constructor (options) { options = options || {}; - options.name = options.name || "Parcelles"; + options.name = options.name || "Parcelles cadastrales"; // call ol.control.Control constructor super(options); @@ -106,7 +106,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { const section = this.sectionInput.value; this.numberList.innerHTML = ""; this._showMessage("section", "chargement en cours", "info"); - this._fetchCadastre(this.communeId, prefix, section).then(data => { + this._fetchCadastre(this.communeId, this.arrondId, prefix, section).then(data => { this._showMessage("section", ""); const section = this.sectionInput.value; if (data && data.features && data.features[0].properties.section === section) { @@ -163,17 +163,19 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { /** Set the commune * @param {String} [id] Commune INSEE code + * @param {String} [arrond] Arrondissement code */ - setCommune (id="") { + setCommune (id="", arrond="000") { if (this.communeId !== id) { const prefixInput = this.prefixInput; this.communeId = id; + this.arrondId = arrond; prefixInput.innerHTML = ""; if (id) { this._showMessage("commCode", "chargement en cours", "info"); // Fetch prefixes and sections for the selected commune - this._fetchCadastre(id).then(data => { + this._fetchCadastre(id, arrond).then(data => { this._showMessage("commCode", ""); this.feuilles = {}; @@ -337,6 +339,15 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } }); + // Fomat commune name for display / postal codes + function formatCommune (commune, cpost) { + const name = `${commune.arrond || commune.code} ${commune.nom}`; + if (cpost && commune.codesPostaux) { + return `${name}, ${commune.codesPostaux.join(", ")} (postal)`; + } + return name; + } + // Fetch commune data on change comCodeInput.addEventListener("change", () => { //check valid input @@ -357,8 +368,8 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { this._showMessage("commCode", "Aucune commune ne correspond à ce code INSEE ou code postal."); this.setCommune(); } else if (data.length === 1) { - communeName = comCodeInput.value = `${data[0].code} (${data[0].nom})`; - this.setCommune(data[0].code); + communeName = comCodeInput.value = formatCommune(data[0]); + this.setCommune(data[0].code, data[0].arrond); } else { data.forEach(commune => { const type = commune.codesPostaux ? "code INSEE" : "code postal"; @@ -366,10 +377,10 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { option.className = "GPautoCompleteOption"; option.setAttribute("role", "option"); option.id = Helper.getUid("GPautoCompleteOption-"); - option.textContent = `${commune.code}, ${commune.nom} (${type})`; + option.title = option.textContent = formatCommune(commune, type); option.addEventListener("click", () => { - communeName = comCodeInput.value = `${commune.code} (${commune.nom})`; - this.setCommune(commune.code); + communeName = comCodeInput.value = formatCommune(commune); + this.setCommune(commune.code, commune.arrond); showAutocomplete(false); }); autocompleteList.appendChild(option); @@ -392,7 +403,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { // Handle click and change separately to manage click on the list sectionInput.addEventListener("click", () => { hascliked = true; - }) + }); sectionInput.addEventListener("change", e => { // Wait for click event to be handled first setTimeout(() => { @@ -408,7 +419,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { }, hascliked ? 0 : 500); }); hascliked = false; - }) + }); // Handle listbox for number input this.numberInput.addEventListener("focus", () => { @@ -590,10 +601,53 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { }; return Promise.all([ fetch(url1, param), - fetch(url2, param) + fetch(url1 + "&type=arrondissement-municipal", param), + fetch(url2, param), + fetch(url2 + "&type=arrondissement-municipal", param), ]).then(responses => { return Promise.all(responses.map(res => res.json())).then(json => { - return json[0].concat(json[1]); + // Handle insee code + if (json[1].length) { + json[1].forEach(com => { + com.arrond = com.code; + switch (com.code.slice(0,2)) { + // Paris + case "75": { + com.code = "75056"; + break; + } + // Lyon + case "69": { + com.code = "69123"; + break; + } + // Marseille + case "13": { + com.code = "13055"; + break; + } + default: { + break; + } + } + }); + } + // Handle arrondissements municipaux + if (json[3].length) { + const arrond = json[3][0]; + const cpost = arrond.codesPostaux[0]; + if (json[2].length) { + // Filter out duplicates with same postal code + const filter = json[2].filter(com => com.codesPostaux.includes(cpost) >= 0); + json[2] = json[2].filter(com => com.codesPostaux.includes(cpost) < 0); + const com = filter.find(com => com.code !== arrond.code); + if (com) { + arrond.arrond = arrond.code; + arrond.code = com.code; + } + } + } + return json[0].concat(json[1]).concat(json[2]).concat(json[3]); }); }).catch(error => { this._showMessage("commCode", "Une erreur est survenue lors de la récupération des données de la commune."); @@ -605,11 +659,12 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * Récupère les feuilles cadastrales d'une commune via le WFS Geopf * @private * @param {String} code Code INSEE de la commune + * @param {String} [arrond] Code de l'arrondissement (pour les communes avec arrondissements municipaux) * @param {String} [prefix] Préfixe de la parcelle * @param {String} [section] Section de la parcelle * @returns {Promise} Promesse avec les données GeoJSON (feuilles ou parcelles si section renseignée) */ - async _fetchCadastre (code, prefix, section) { + async _fetchCadastre (code, arrond, prefix, section) { const domtom = ["97","98"].includes(code.slice(0,2)); const dep = code.slice(0, domtom ? 3 : 2); const com = code.slice(domtom ? 3 : 2, 5); @@ -622,8 +677,8 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { outputFormat : "application/json", srsName : "CRS:84", count : "1000", - propertyName : section ? "com_abs,section,numero" : "com_abs,section", - cql_filter : `code_dep='${dep}' and code_com='${com}'` + (section ? ` and com_abs='${prefix}' and section='${section}'` : "") + propertyName : section ? "com_abs,section,numero" : "com_abs,section,code_arr", + cql_filter : `code_dep='${dep}' and code_com='${com}' and code_arr='${arrond ? arrond.slice(2) : "000"}'` + (section ? ` and com_abs='${prefix}' and section='${section}'` : "") }; const queryString = new URLSearchParams(params).toString(); const fullUrl = url + queryString; @@ -766,8 +821,10 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { return; } + // Gestion des arrondissements municipaux + const communeId = this.arrondId || this.communeId; // Search parcelle - const parcelId = this.communeId + const parcelId = communeId + "000".slice(prefix.length) + prefix + section + "0000".slice(number.length) + number; From ab1fa7a885b913a14e0cb50c674820514ae8ed0e Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 26 Nov 2025 17:06:24 +0100 Subject: [PATCH 67/73] =?UTF-8?q?Fix:=20limitation=20des=20arrondissements?= =?UTF-8?q?=20=C3=A0=20Paris=20/=20Lyon=20/=20Marseilles?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../SearchEngine/ParcelAdvancedSearch.js | 36 ++++++++++++------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index a6cf3dd2e..3c28d620e 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -194,7 +194,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { previous = key; } }); - this.setFeuille("000"); + this.setFeuille(prefixInput.value); }); prefixInput.removeAttribute("disabled"); } else { @@ -597,18 +597,28 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { const param ={ headers : { "Content-Type" : "application/json", - }, + }, }; - return Promise.all([ + // Fetch both insee code and postal code + const fetchtable = [ fetch(url1, param), - fetch(url1 + "&type=arrondissement-municipal", param), fetch(url2, param), - fetch(url2 + "&type=arrondissement-municipal", param), - ]).then(responses => { + ]; + // For municipal arrondissements, fetch them too + if (/^75|^6900|^1300|^1301/.test(code)) { + fetchtable.push( + fetch(url1 + "&type=arrondissement-municipal", param), + ); + fetchtable.push( + fetch(url2 + "&type=arrondissement-municipal", param), + ); + } + // Process responses + return Promise.all(fetchtable).then(responses => { return Promise.all(responses.map(res => res.json())).then(json => { // Handle insee code - if (json[1].length) { - json[1].forEach(com => { + if (json[2]) { + json[2].forEach(com => { com.arrond = com.code; switch (com.code.slice(0,2)) { // Paris @@ -633,13 +643,13 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { }); } // Handle arrondissements municipaux - if (json[3].length) { + if (json[3]) { const arrond = json[3][0]; const cpost = arrond.codesPostaux[0]; - if (json[2].length) { + if (json[1].length) { // Filter out duplicates with same postal code - const filter = json[2].filter(com => com.codesPostaux.includes(cpost) >= 0); - json[2] = json[2].filter(com => com.codesPostaux.includes(cpost) < 0); + const filter = json[1].filter(com => com.codesPostaux.includes(cpost) >= 0); + json[1] = json[1].filter(com => com.codesPostaux.includes(cpost) < 0); const com = filter.find(com => com.code !== arrond.code); if (com) { arrond.arrond = arrond.code; @@ -647,7 +657,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { } } } - return json[0].concat(json[1]).concat(json[2]).concat(json[3]); + return json[0].concat(json[1]).concat(json[2]||[]).concat(json[3]||[]); }); }).catch(error => { this._showMessage("commCode", "Une erreur est survenue lors de la récupération des données de la commune."); From e95571fe9eade918c9e2eb1d08309d8932a710ff Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 26 Nov 2025 17:14:29 +0100 Subject: [PATCH 68/73] Fix: check validity --- .../Controls/SearchEngine/ParcelAdvancedSearch.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index 3c28d620e..989146711 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -808,12 +808,14 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { if (e) { super._onSearch(e); } - - this._clearMessages(); - + // Nothing to do if no commune selected if (!this.communeId) { return; } + + // Check form validity + this._clearMessages(); + const prefix = this.prefixInput.value; const section = this.sectionInput.value; const number = this.numberInput.value; From 3a610d619776f458b0a3b44f6d6b257e2f3a67c7 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 26 Nov 2025 17:39:30 +0100 Subject: [PATCH 69/73] feat(search): Ajout rech. av. INSEE pour arrondissements fix IGNF/cartes.gouv.fr-editeur-carto#143 --- .../SearchEngine/InseeAdvancedSearch.js | 11 +++++++++++ src/packages/Services/InseeSearchService.js | 19 +++++++++++++++---- 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js index 4df75b832..356d49e13 100644 --- a/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/InseeAdvancedSearch.js @@ -104,6 +104,17 @@ class InseeAdvancedSearch extends AbstractAdvancedSearch { }.bind(this); } + /** + * Permet de supprimer les messages d'erreur, en plus du fonctionnement initial. + * + * @protected + * @override + */ + _onErase (e) { + super._onErase(e); + this.inseeInput.removeMessages(); + } + /** * Handler de la recherche déclenchée (bouton / submit). * Valide le code INSEE puis lance la recherche via le SearchEngine associé. diff --git a/src/packages/Services/InseeSearchService.js b/src/packages/Services/InseeSearchService.js index 38084e68e..f9ed2a0ab 100644 --- a/src/packages/Services/InseeSearchService.js +++ b/src/packages/Services/InseeSearchService.js @@ -58,11 +58,11 @@ class InseeSearchService extends AbstractSearchService { * @override * @param {Object} object Objet contenant le code INSEE * @param {String} object.location Code INSEE à rechercher + * @param {Boolean} [arr] Si vrai, recherche pour un arrondissement */ - search (object) { + search (object, arr) { const insee = object.location; - // Envoi la requête si le chiffre est compris entre 0 et 99999 - const response = this._requestGeoAPI({ value : insee }); + const response = this._requestGeoAPI({ value : insee, arr : !!arr }); response.then(r => { if (r instanceof Array && r.length) { const result = r[0]; @@ -85,6 +85,11 @@ class InseeSearchService extends AbstractSearchService { this.ignService.search(obj); } else { + // Cherche, pour les département 13, 69 et 75 si c'est un arrondissement + if (/^(13|69|75)/.test(insee) && !arr) { + this.search(object, true); + return; + } // Pas de résultat, on envoi un événement "search" vide this._onSearch({ type : this.SEARCH_EVENT, @@ -108,13 +113,19 @@ class InseeSearchService extends AbstractSearchService { * @private * @param {Object} settings Paramètres de requêtes * @param {String} settings.value Code INSEE à interroger + * @param {Boolean} [settings.arr] Si vrai, recherche sur un arrondissement * @returns {Promise} Résultat de l'API */ async _requestGeoAPI (settings) { const baseURL = "https://geo.api.gouv.fr/communes"; const format = "json"; const fields = ["nom", "code", "codesPostaux"]; - const url = `${baseURL}?code=${settings.value}&format=${format}&fields=${fields}`; + const arr = settings.arr; + let url = `${baseURL}?code=${settings.value}&format=${format}&fields=${fields}`; + + if (arr) { + url += "&type=arrondissement-municipal"; + } try { const response = await fetch(url, { From 50e8ef04a10c4b2133c431b7878729a8f3e19056 Mon Sep 17 00:00:00 2001 From: viglino Date: Wed, 26 Nov 2025 17:45:59 +0100 Subject: [PATCH 70/73] Fix: arrondissements --- src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index 989146711..ea9027301 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -605,7 +605,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { fetch(url2, param), ]; // For municipal arrondissements, fetch them too - if (/^75|^6900|^1300|^1301/.test(code)) { + if (/^75|^6900|^6938|^1300|^1301|^1320|^1321/.test(code)) { fetchtable.push( fetch(url1 + "&type=arrondissement-municipal", param), ); @@ -617,7 +617,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { return Promise.all(fetchtable).then(responses => { return Promise.all(responses.map(res => res.json())).then(json => { // Handle insee code - if (json[2]) { + if (json[2] && json[2].length) { json[2].forEach(com => { com.arrond = com.code; switch (com.code.slice(0,2)) { @@ -643,7 +643,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { }); } // Handle arrondissements municipaux - if (json[3]) { + if (json[3] && json[3].length) { const arrond = json[3][0]; const cpost = arrond.codesPostaux[0]; if (json[1].length) { From 63c137e27a4208bf8ea9fda5f4978426a93cd806 Mon Sep 17 00:00:00 2001 From: MatRouillard Date: Wed, 26 Nov 2025 18:24:22 +0100 Subject: [PATCH 71/73] =?UTF-8?q?fix(search):=20Fix=20code=5Farr=20dans=20?= =?UTF-8?q?requete=20m=C3=AAme=20sans=20arrondissement=20fix=20IGNF/cartes?= =?UTF-8?q?.gouv.fr-editeur-carto#153?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js index ea9027301..28adc14aa 100644 --- a/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js +++ b/src/packages/Controls/SearchEngine/ParcelAdvancedSearch.js @@ -165,7 +165,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { * @param {String} [id] Commune INSEE code * @param {String} [arrond] Arrondissement code */ - setCommune (id="", arrond="000") { + setCommune (id="", arrond="") { if (this.communeId !== id) { const prefixInput = this.prefixInput; @@ -688,7 +688,7 @@ class ParcelAdvancedSearch extends AbstractAdvancedSearch { srsName : "CRS:84", count : "1000", propertyName : section ? "com_abs,section,numero" : "com_abs,section,code_arr", - cql_filter : `code_dep='${dep}' and code_com='${com}' and code_arr='${arrond ? arrond.slice(2) : "000"}'` + (section ? ` and com_abs='${prefix}' and section='${section}'` : "") + cql_filter : `code_dep='${dep}' and code_com='${com}'` + (arrond ? `and code_arr='${arrond.slice(2)}'` : "") + (section ? ` and com_abs='${prefix}' and section='${section}'` : "") }; const queryString = new URLSearchParams(params).toString(); const fullUrl = url + queryString; From 8553ea7f52126a8334e11994b10a37e3211f4fcc Mon Sep 17 00:00:00 2001 From: viglino Date: Tue, 2 Dec 2025 15:18:13 +0100 Subject: [PATCH 72/73] ADD: interaction drawing --- build/webpack/modules.webpack.config.js | 2 + ...ol-drawinginteraction-modules-default.html | 93 +++++++ src/index.js | 4 + src/packages/CSS/Interactions/Drawing.css | 28 +++ src/packages/Interactions/Drawing.js | 237 ++++++++++++++++++ 5 files changed, 364 insertions(+) create mode 100644 samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html create mode 100644 src/packages/CSS/Interactions/Drawing.css create mode 100644 src/packages/Interactions/Drawing.js diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 0a91ca4a2..3582a9737 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -66,6 +66,8 @@ module.exports = (env, argv) => { "GpfExtOlControlList" : path.join(rootdir, "src", "packages", "Controls/ControlList", "ControlList.js"), "GpfExtOlContextMenu" : path.join(rootdir, "src", "packages", "Controls/ContextMenu", "ContextMenu.js"), "GpfExtOlReporting" : path.join(rootdir, "src", "packages", "Controls/Reporting", "Reporting.js"), + // Interactions + "GpfExtOlDrawingInteraction" : path.join(rootdir, "src", "packages", "Interactions/Drawing.js"), // Formats étendus "GpfExtOlFormats" : [ path.join(rootdir, "src", "packages", "Formats", "GeoJSON.js"), diff --git a/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html b/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html new file mode 100644 index 000000000..6e74c0c4f --- /dev/null +++ b/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html @@ -0,0 +1,93 @@ +{{#extend "ol-sample-modules-dsfr-layout"}} + +{{#content "vendor"}} + + +{{/content}} + +{{#content "head"}} + Sample openlayers Drawing interaction +{{/content}} + +{{#content "style"}} + +{{/content}} + +{{#content "body"}} +

    Outil de dessin avec style

    + +
    +
    +

    Dernière sélection :

    + + + +{{/content}} + +{{#content "js"}} + +{{/content}} + +{{/extend}} diff --git a/src/index.js b/src/index.js index d6e2f38c3..77d233938 100644 --- a/src/index.js +++ b/src/index.js @@ -56,6 +56,10 @@ export { default as ControlList } from "./packages/Controls/ControlList/ControlL export { default as ContextMenu } from "./packages/Controls/ContextMenu/ContextMenu"; export { default as Reporting } from "./packages/Controls/Reporting/Reporting"; + +// Interactions +export { default as DrawingInteraction } from "./packages/Interactions/Drawing"; + // Services export { default as AbstractSearchService } from "./packages/Services/AbstractSearchService"; export { default as DefaultSearchService } from "./packages/Services/DefaultSearchService"; diff --git a/src/packages/CSS/Interactions/Drawing.css b/src/packages/CSS/Interactions/Drawing.css new file mode 100644 index 000000000..89d943816 --- /dev/null +++ b/src/packages/CSS/Interactions/Drawing.css @@ -0,0 +1,28 @@ +.ol-control.ol-drawing-info { + color: var(--light-decisions-text-text-default-grey, #3A3A3A); + /* 2.Corps de texte/XS - Texte mention/Desktop & Mobile - Regular */ + font-family: Marianne; + font-size: 12px; + font-style: normal; + font-weight: 400; + line-height: 20px; + + display: inline-flex; + padding: 8px 12px; + justify-content: center; + align-items: center; + gap: 4px; + + background-color: rgba(255, 255, 255, 0.8); + border-radius: 4px; + font-size: 12px; + + top: 4em; + left: 50%; + transform: translateX(-50%); + pointer-events: none!important; +} +.ol-control.ol-drawing-info:empty { + pointer-events: none!important; + display: none; +} diff --git a/src/packages/Interactions/Drawing.js b/src/packages/Interactions/Drawing.js new file mode 100644 index 000000000..0e8ad6a29 --- /dev/null +++ b/src/packages/Interactions/Drawing.js @@ -0,0 +1,237 @@ +// import CSS +import "../CSS/Interactions/Drawing.css"; + +import Interaction from "ol/interaction/Interaction"; +import Draw from "ol/interaction/Draw"; +import Style from "ol/style/Style"; +import ImageStyle from "ol/style/Image"; +import Icon from "ol/style/Icon"; +import Stroke from "ol/style/Stroke"; +import Fill from "ol/style/Fill"; +import CircleStyle from "ol/style/Circle"; +import MultiPoint from "ol/geom/MultiPoint"; +import coordinate from "ol/coordinate"; +import Control from "ol/control/Control"; + +import mapPinIcon from "../Controls/SearchEngine/map-pin-2-fill.svg"; + +/** Get all points in coordinates + * @param {Array} coords + * @returns {Array} + * @private + */ +function getFlatCoordinates (coords) { + if (coords && coords[0].length && coords[0][0].length) { + var c = []; + for (var i=0; i { + if (!this._currentFeature) { + return; + } + switch (evt.key) { + case "Enter": { + draw.finishDrawing(); + break; + } + case "Backspace": { + this.draw.removeLastPoint(); + break; + } + } + }; + + // Draw interaction + const draw = this.draw = new Draw({ + source : options.source, + type : this._type, + style : (feature, resolution) => this.getStyle(feature, resolution), + }); + // Draw interaction events + this.draw.on("drawstart", (e) => { + this.showInfo("Double-cliquer ou appuyer sur Entrée pour terminer."); + this._currentFeature = e.feature; + document.addEventListener("keydown", onkeydown); + this.dispatchEvent(e); + }); + this.draw.on(["drawend","drawabort"], (e) => { + this.showInfo(this.getActive() ? "Cliquer pour commencer." : ""); + this._currentFeature = null; + document.removeEventListener("keydown", onkeydown); + this.dispatchEvent(e); + }); + // Info control + this.info = new InfoControl(); + } + + /** + * Overwrite OpenLayers setMap method + * + * @param {Map} map - Map. + */ + setMap (map) { + if (this.getMap()) { + this.getMap().removeInteraction(this.draw); + this.getMap().removeControl(this.info); + // reset cursor + this.getMap().getTargetElement().style.cursor = ""; + } + super.setMap(map); + // Additional setup can be done here if needed + if (map) { + map.addInteraction(this.draw); + map.addControl(this.info); + } + this.setActive(this.getActive()); + } + + /** Set interaction active state + * @param {Boolean} active Active state + */ + setActive (active) { + super.setActive(active); + if (this.draw) { + this.draw.setActive(active); + this.showInfo(active ? "Cliquer pour commencer." : ""); + } + if (this.getMap()) { + this.getMap().getTargetElement().style.cursor = this.getActive() ? "crosshair" : ""; + } + } + + /** Get drawing style + * @returns {ol_style_Style|Array} Style + */ + getStyle () { + const stroke = new Stroke({ + color : "#33b1ff", + width : 2, + }); + const fill = new Fill({ + color : "rgb(51, 177, 255, 0.2)" + }); + const image = this._image || new Icon({ + src : mapPinIcon, + color : "#000091", + anchor : [0.5, 1], + }); + const circle = new CircleStyle({ + stroke : new Stroke({ + color : "#fff", + width : 2 + }), + fill : new Fill({ + color : "#33b1ff", + }), + radius : 5, + }); + + switch (this._type) { + case "LineString": + case "Polygon": { + return [ + new Style({ + image : circle, + stroke : stroke, + fill : fill, + }), + new Style({ + image : circle, + geometry : (f) => new MultiPoint( getFlatCoordinates(f.getGeometry().getCoordinates() ) ), + }) + ]; + } + case "Point": + default: { + return new Style({ + image : image, + stroke : stroke, + fill : fill, + }); + } + } + } + + /** Set an image for drawing points + * @param {ImageStyle} image Image + */ + setImage (image) { + // TODO : allow to set custom image for point drawing + if (image instanceof ImageStyle) { + this._image = image; + } + } + + /** Get image used for drawing points + * @returns {ImageStyle} Image + */ + getImage () { + return this._image; + } + + /** + * Display info message + * @param {String} info message info + */ + showInfo (info = "") { + this.info.setInfo(info); + } + +} + + +export default DrawingInteraction; + +// Expose DrawingInteraction as ol.interaction.Drawing (for a build bundle) +if (window.ol && window.ol.interaction) { + window.ol.interaction.Drawing = DrawingInteraction; +} From ab940b0c775458c3c1ab591ab8eb028b97bff56d Mon Sep 17 00:00:00 2001 From: viglino Date: Tue, 2 Dec 2025 17:24:04 +0100 Subject: [PATCH 73/73] ADD: selecting interaction --- build/webpack/modules.webpack.config.js | 1 + ...ol-drawinginteraction-modules-default.html | 9 +++- src/index.js | 1 + src/packages/Interactions/Drawing.js | 38 ++++++++++--- src/packages/Interactions/Selecting.js | 53 +++++++++++++++++++ 5 files changed, 95 insertions(+), 7 deletions(-) create mode 100644 src/packages/Interactions/Selecting.js diff --git a/build/webpack/modules.webpack.config.js b/build/webpack/modules.webpack.config.js index 3582a9737..b5043c583 100644 --- a/build/webpack/modules.webpack.config.js +++ b/build/webpack/modules.webpack.config.js @@ -68,6 +68,7 @@ module.exports = (env, argv) => { "GpfExtOlReporting" : path.join(rootdir, "src", "packages", "Controls/Reporting", "Reporting.js"), // Interactions "GpfExtOlDrawingInteraction" : path.join(rootdir, "src", "packages", "Interactions/Drawing.js"), + "GpfExtOlSelectingInteraction" : path.join(rootdir, "src", "packages", "Interactions/Selecting.js"), // Formats étendus "GpfExtOlFormats" : [ path.join(rootdir, "src", "packages", "Formats", "GeoJSON.js"), diff --git a/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html b/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html index 6e74c0c4f..66c8264e1 100644 --- a/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html +++ b/samples-src/pages/tests/DrawingInteraction/pages-ol-drawinginteraction-modules-default.html @@ -3,6 +3,7 @@ {{#content "vendor"}} + {{/content}} {{#content "head"}} @@ -60,12 +61,13 @@

    Outil de dessin avec style

    ] }); + // Drawing const drawing = {}; ["Point", "LineString", "Polygon"].forEach(k => { drawing[k] = new ol.interaction.Drawing({ type : k, source : source - }) + }); }) layer.setStyle(drawing.Point.getStyle()); Object.keys(drawing).forEach((k, i) => { @@ -86,6 +88,11 @@

    Outil de dessin avec style

    }); }); + const select = new ol.interaction.Selecting({ + + }); + map.addInteraction(select); + select.on('dblclick', e => console.log(e.feature)); }; {{/content}} diff --git a/src/index.js b/src/index.js index 77d233938..02d4da920 100644 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,7 @@ export { default as Reporting } from "./packages/Controls/Reporting/Reporting"; // Interactions export { default as DrawingInteraction } from "./packages/Interactions/Drawing"; +export { default as SelectingInteraction } from "./packages/Interactions/Selecting"; // Services export { default as AbstractSearchService } from "./packages/Services/AbstractSearchService"; diff --git a/src/packages/Interactions/Drawing.js b/src/packages/Interactions/Drawing.js index 0e8ad6a29..a8e57e53e 100644 --- a/src/packages/Interactions/Drawing.js +++ b/src/packages/Interactions/Drawing.js @@ -14,6 +14,7 @@ import coordinate from "ol/coordinate"; import Control from "ol/control/Control"; import mapPinIcon from "../Controls/SearchEngine/map-pin-2-fill.svg"; +import VectorSource from "ol/source/Vector"; /** Get all points in coordinates * @param {Array} coords @@ -63,6 +64,7 @@ class DrawingInteraction extends Interaction { * @param {*} options Openlayers drawing options */ constructor (options) { + options = options || {}; super({ handleEvent : function (e) { this.getMap().getTargetElement().style.cursor = this.getActive() ? "crosshair" : ""; @@ -70,8 +72,9 @@ class DrawingInteraction extends Interaction { } }); this._type = options.type || "Point"; + this.setSource(options.source); - /** Finish drawing on keydown + /** Handle drawing on keydown * @param {Event} evt */ const onkeydown = (evt) => { @@ -79,10 +82,12 @@ class DrawingInteraction extends Interaction { return; } switch (evt.key) { + // Finish drawing on Enter or double click case "Enter": { - draw.finishDrawing(); + this.draw.finishDrawing(); break; } + // Remove last point on Backspace case "Backspace": { this.draw.removeLastPoint(); break; @@ -90,9 +95,12 @@ class DrawingInteraction extends Interaction { } }; + // Info control + this.info = new InfoControl(); + // Draw interaction - const draw = this.draw = new Draw({ - source : options.source, + this.draw = new Draw({ + // source : options.source, type : this._type, style : (feature, resolution) => this.getStyle(feature, resolution), }); @@ -104,13 +112,17 @@ class DrawingInteraction extends Interaction { this.dispatchEvent(e); }); this.draw.on(["drawend","drawabort"], (e) => { + if (e.type === "drawend") { + const source = this.getSource(); + if (source) { + source.addFeature(e.feature); + } + } this.showInfo(this.getActive() ? "Cliquer pour commencer." : ""); this._currentFeature = null; document.removeEventListener("keydown", onkeydown); this.dispatchEvent(e); }); - // Info control - this.info = new InfoControl(); } /** @@ -134,6 +146,20 @@ class DrawingInteraction extends Interaction { this.setActive(this.getActive()); } + /** Change the source to collect features + * @param {VectorSource} source Vector source + */ + setSource (source) { + this._source = source || null; + } + + /** The source to collect features + * @param {VectorSource} source Vector source + */ + getSource () { + return this._source || null; + } + /** Set interaction active state * @param {Boolean} active Active state */ diff --git a/src/packages/Interactions/Selecting.js b/src/packages/Interactions/Selecting.js new file mode 100644 index 000000000..15f2c3194 --- /dev/null +++ b/src/packages/Interactions/Selecting.js @@ -0,0 +1,53 @@ +import Select from "ol/interaction/Select"; + +/** Outil de selection et d'interaction avec des features + * + */ +class SelectingInteraction extends Select { + + constructor (options) { + options = options || {}; + // Prevent selecting on empty space + options.filter = (feature, layer) => { + return (!!layer); + }; + // Handle double click on selected feature + options.condition = function (e) { + if (e.type === "dblclick") { + let found = false; + map.forEachFeatureAtPixel( + e.pixel, + (feature, layer) => { + if (this.getLayer(feature)) { + found = feature; + } + return (!found); + }, + { + layerFilter : this.layerFilter_, + hitTolerance : this.hitTolerance_, + }, + ); + if (found) { + e.feature = found; + this.dispatchEvent(e); + e.stopPropagation(); + } + } + return (e.type === "singleclick"); + return (e.type === "click"); + }; + + // call parent constructor + super(options); + } + +} + + +export default SelectingInteraction; + +// Expose SelectingInteraction as ol.interaction.Selecting (for a build bundle) +if (window.ol && window.ol.interaction) { + window.ol.interaction.Selecting = SelectingInteraction; +}