From 7d45e2f71710ce4f496e8724637aadf5e262e1b5 Mon Sep 17 00:00:00 2001 From: EpicKiwi Date: Sun, 7 Jun 2026 12:22:41 +0200 Subject: [PATCH] Ajout d'un panneau vertical pour le resultat de la recerche --- css/style.css | 104 +++++++++- icons/arrow-left.svg | 19 ++ icons/arrow-right.svg | 19 ++ index.html | 20 +- js/components/bidi-panel.js | 264 ++++++++++++++++++++++++++ js/components/feature-short-header.js | 43 +++++ js/components/feature.js | 26 +++ js/index.js | 72 ++++++- 8 files changed, 549 insertions(+), 18 deletions(-) create mode 100644 icons/arrow-left.svg create mode 100644 icons/arrow-right.svg create mode 100644 js/components/bidi-panel.js create mode 100644 js/components/feature-short-header.js create mode 100644 js/components/feature.js diff --git a/css/style.css b/css/style.css index 7794b4f..c61fd99 100644 --- a/css/style.css +++ b/css/style.css @@ -5,12 +5,29 @@ body, html { body { pointer-events: none; + + display: flex; + flex-direction: column; + justify-content: flex-start; + align-items: stretch; + + min-height: 100vh; } body > * { pointer-events: all; } +body > hr { + border: none; + padding: 0; + margin: 0; + flex: 1; + min-height: 50vh; + flex-shrink: 0; + pointer-events: none; +} + /* HEADER and NAV */ #main-header { @@ -112,4 +129,89 @@ body > * { filter: drop-shadow(0 0 10px black); opacity: clamp(0, calc( 1 - ( var(--zoom-level) - 18 ) ), 1) ; pointer-events: none; -} \ No newline at end of file +} + +/* SAERCH FORM */ + +#search-section { + position: sticky; + top: 0; + left: 0; + z-index: 100i; +} + +/* PANEL */ + +#result-panel { + background: black; + border-top: 3px white solid; + + color: white; +} + +#result-panel.empty, +#result-panel:empty{ + display:none; +} + +#result-panel > * { + padding: 1em; + margin: 0; +} + +#result-panel::part(navigation-panel) { + background: black; + color: white; + box-shadow: 0 0 10px black; + border-top: solid 1px rgba(255, 255, 255, 0.5); +} + +#result-panel::part(dot), +#result-panel::part(dot-active) { + border-radius: 0; + width: 1ex; + height: 1ex; + margin-top: 0.5ex; +} + +#result-panel::part(dot-active) { + transform-origin: 0% 100%; + transform: scaleX(1) scaleY(1.5); +} + +camp-feature-short-header { + display: grid; + grid-template-rows: 1fr min-content; + grid-template-columns: 1fr; +} + +camp-feature-short-header h2 { + margin: 0; + padding: 0; + font-size: 1em; + font-weight: bold; +} + +camp-feature-short-header .feature-location { + margin: 0; + padding: 0; + font-size: 0.6em; + opacity: 0.75; +} + +#search-result { + list-style-type: none; +} + +#search-result > li:not(:first-child) { + margin-top: 1em; +} + +#search-result > li:not(:last-child) { + margin-bottom: 1em; +} + +#search-result > li > a { + text-decoration: none; + color: inherit; +} diff --git a/icons/arrow-left.svg b/icons/arrow-left.svg new file mode 100644 index 0000000..666b191 --- /dev/null +++ b/icons/arrow-left.svg @@ -0,0 +1,19 @@ + + + + diff --git a/icons/arrow-right.svg b/icons/arrow-right.svg new file mode 100644 index 0000000..51a6708 --- /dev/null +++ b/icons/arrow-right.svg @@ -0,0 +1,19 @@ + + + + diff --git a/index.html b/index.html index 32e8aab..7df51b7 100644 --- a/index.html +++ b/index.html @@ -7,6 +7,7 @@ +
@@ -14,26 +15,17 @@
- - - + +
+

-
+ - \ No newline at end of file + diff --git a/js/components/bidi-panel.js b/js/components/bidi-panel.js new file mode 100644 index 0000000..6a85da0 --- /dev/null +++ b/js/components/bidi-panel.js @@ -0,0 +1,264 @@ +const TEMPLATE = document.createElement("template"); +TEMPLATE.innerHTML = ` + + +
+ + +` + +export class BidiPanelElement extends HTMLElement { + + shadow + + mutationObserver + + #lastActive + #currentIndex + + get contentContainer(){ + return this.shadow.getElementById("content") + } + + get progressBar(){ + return this.shadow.getElementById("panel-progress-bar") + } + + get progressDotsContainer(){ + return this.shadow.getElementById("progress-dots") + } + + get activeChildrenIndex(){ + let thisGeometry = this.getBoundingClientRect() + let progress = this.contentContainer.scrollLeft / (this.contentContainer.scrollWidth - thisGeometry.width) + let focusedElementIndex = Math.floor(progress * this.children.length) + if(Number.isFinite(focusedElementIndex)){ + return focusedElementIndex + } else { + return null + } + } + + set activeChildrenIndex(newIndex){ + this.setActiveChildrenIndex(newIndex, {behavior: "auto"}) + } + + setActiveChildrenIndex(newIndex, options){ + let newPanel = this.children[newIndex] + let newPanelRect = newPanel.getBoundingClientRect() + let selfRect = this.getBoundingClientRect() + this.contentContainer.scrollTo({ + left: this.contentContainer.scrollLeft + ( newPanelRect.left - selfRect.left ), + behavior: options?.behavior || "auto" + }) + this.#currentIndex = newIndex + } + + get activeChildren(){ + return this.children[this.activeChildrenIndex] + } + + connectedCallback(){ + if(!this.shadow){ + this.shadow = this.attachShadow({ mode: "open" }) + this.shadow.append(TEMPLATE.content.cloneNode(true)) + } + this.contentContainer.addEventListener("scroll", this.handleContentScroll.bind(this)) + this.shadow.getElementById("nav-left-panel").addEventListener("click", () => this.previous()); + this.shadow.getElementById("nav-right-panel").addEventListener("click", () => this.next()); + + this.mutationObserver = (new MutationObserver(this.handleMutations.bind(this))) + this.mutationObserver.observe(this, {childList:true}) + + this.updateProgress() + } + + handleMutations(mutation){ + let newIndex = this.#currentIndex + this.updateProgress() + if(Number.isFinite(newIndex)){ + this.setActiveChildrenIndex(newIndex, {behavior: "instant"}) + } + } + + handleContentScroll(e){ + this.updateProgress() + this.#currentIndex = this.activeChildrenIndex + if(this.#currentIndex != this.#lastActive){ + this.dispatchEvent(new ActivePanelChangeEvent("activePanelChange", this.#currentIndex)) + this.#lastActive = this.#currentIndex + } + } + + updateProgress(){ + let focusedElementIndex = this.activeChildrenIndex; + + let progressBar = this.progressBar; + progressBar.value = focusedElementIndex; + progressBar.min = 0; + progressBar.max = this.children.length-1; + + this.classList.toggle("single-panel", this.children.length == 1) + this.classList.toggle("empty", this.children.length == 0) + + let dotsContainer = this.progressDotsContainer; + if(dotsContainer.children.length != parseInt(progressBar.max)+1){ + let dots = (new Array(this.children.length)) + .fill(0) + .map(() => { + let el = document.createElement("div") + el.classList.add("dot") + el.part = "dot" + return el + }); + dotsContainer.replaceChildren(...dots) + } + for(let inactiveDot of dotsContainer.querySelectorAll(".active")){ + inactiveDot.part = "dot" + inactiveDot.classList.remove("active") + } + + let activeDot = dotsContainer.children[parseInt(progressBar.value)] + if(activeDot){ + activeDot.part = "dot-active" + activeDot.classList.add("active") + } + } + + next(){ + let nextItem = this.activeChildrenIndex + 1; + if(nextItem < this.children.length){ + this.activeChildrenIndex = nextItem + } + } + + previous(){ + let nextItem = this.activeChildrenIndex - 1; + if(nextItem >= 0){ + this.activeChildrenIndex = nextItem + } + } +} + +export class ActivePanelChangeEvent extends Event { + constructor(name, activePanelIndex){ + super(name) + this.activePanelIndex = activePanelIndex + } +} + +customElements.define("camp-bidi-panel", BidiPanelElement); diff --git a/js/components/feature-short-header.js b/js/components/feature-short-header.js new file mode 100644 index 0000000..4dda14f --- /dev/null +++ b/js/components/feature-short-header.js @@ -0,0 +1,43 @@ +const TEMPLATE = document.createElement("template") +TEMPLATE.innerHTML = ` +

+ +` + +export class FeatureShortHeaderElement extends HTMLElement { + + #feature + + get feature(){ + return this.#feature + } + + set feature(value){ + this.#feature = value + } + + connectedCallback(){ + this.updateContent() + } + + updateContent(){ + if(!this.feature){ + this.replaceChildren() + } else { + this.replaceChildren(TEMPLATE.content.cloneNode(true)) + this.querySelector(".feature-name").textContent = + this.feature?.properties?.name || + this.feature.mapSymbol.genericName || + this.feature.id + + let featurePoint = this.feature.asPoint() + + this.querySelector(".feature-location").textContent = + this.feature?.properties?.["location-description"] || + `${featurePoint.geometry.coordinates[1]}, ${featurePoint.geometry.coordinates[0]}` + } + } + +} + +customElements.define("camp-feature-short-header", FeatureShortHeaderElement) diff --git a/js/components/feature.js b/js/components/feature.js new file mode 100644 index 0000000..a8fb046 --- /dev/null +++ b/js/components/feature.js @@ -0,0 +1,26 @@ +import "./feature-short-header.js" + +const TEMPLATE = document.createElement("template") +TEMPLATE.innerHTML = ` + +` + +export class FeatureElement extends HTMLElement { + + feature + + connectedCallback(){ + this.updateContent() + } + + updateContent(){ + this.replaceChildren(TEMPLATE.content.cloneNode(true)) + + let header = this.querySelector("camp-feature-short-header") + header.feature = this.feature + header.updateContent() + } + +} + +customElements.define("camp-feature", FeatureElement) diff --git a/js/index.js b/js/index.js index 2bff483..28141bd 100644 --- a/js/index.js +++ b/js/index.js @@ -1,5 +1,7 @@ import * as MAP from "./map.js" import { PlaceDatabase } from "./places.js"; +import {FeatureElement} from "./components/feature.js" +import "./components/feature-short-header.js" /** La carte */ @@ -47,18 +49,82 @@ search_form.addEventListener("submit", e => { let a = document.createElement("a") el.append(a) a.href = "#"+encodeURIComponent(result_item.ref) - a.textContent = result_item.feature.properties.name+" ("+result_item.ref+")" + a.addEventListener("click", e => { + e.preventDefault() + openSearchResultItem(result_item.feature) + }) + let header = document.createElement("camp-feature-short-header") + header.feature = result_item.feature + a.append(header) resultElements.push(el) } + if(!document.getElementById("search-result")){ + let el = document.createElement("ol") + el.id = "search-result" + document.getElementById("result-panel").replaceChildren(el) + } + if(resultElements.length > 0){ document.getElementById("search-result").replaceChildren(...resultElements) } else { document.getElementById("search-result").replaceChildren(document.createTextNode("Pas de resultat")) } + + document.getElementById("search-result").children[0]?.scrollIntoView() } else { - document.getElementById("search-result").replaceChildren([]) + document.getElementById("search-result")?.remove() } } -}) \ No newline at end of file +}) + +function openSearchResultItem(feature){ + let searchResultContainer = document.getElementById("search-result") + if(searchResultContainer){ + let featureIndex = -1 + let featureFound = false + let newChildren = [] + for(let el of searchResultContainer.children){ + let featureHeader = el.querySelector("camp-feature-short-header") + if(!featureHeader) + continue + let root = document.createElement("camp-feature") + root.feature = featureHeader.feature + newChildren.push(root) + if(!featureFound){ + featureIndex += 1 + if(feature == root.feature){ + featureFound = true + } + } + } + let panel = document.getElementById("result-panel") + panel.replaceChildren(...newChildren) + requestAnimationFrame(() => { + if(featureFound){ + panel.setActiveChildrenIndex(featureIndex, {behavior: "instant"}) + } else { + panel.activeChildrenIndex = 0 + } + }) + let newUrl = new URL(window.location) + newUrl.hash = feature.id + window.history.replaceState(newUrl.toString(), "") + } +} + +document.getElementById("result-panel").addEventListener("activePanelChange", e => { + let activeElement = e.target.children[e.activePanelIndex] + if(activeElement instanceof FeatureElement){ + let feature = activeElement.feature + if(feature){ + MAP.highlight(feature.id) + } else { + MAP.unhighlight_all() + } + console.log("active panel changed: "+e.activePanelIndex, feature) + } else { + MAP.unhighlight_all() + } +})