import { html, LitElement, css, PropertyValues } from "lit"; import { styleMap } from "lit/directives/style-map.js"; import { customElement, property, queryAssignedElements, state, query, } from "lit/decorators.js"; import "@supersoniks/concorde/core/components/ui/menu/menu-item"; import { Size } from "../_css/size"; import { Shadow, shadowable } from "../_css/shadow"; const tagName = "sonic-menu"; @customElement(tagName) export class MenuItems extends LitElement { static styles = [ css` :host { display: block; --sc-menu-gap: 0.15rem; } :host > menu { display: flex; padding: 0; margin: 0; } .hidden { display: none !important; } /* SCROLLABLE*/ #menu-content { display: flex; padding: 0.35em; margin: 0; } :host([scrollable]) #menu-content { scrollbar-width: none; max-width: 100%; -ms-overflow-style: none; scroll-snap-align: start; white-space: nowrap; border-radius: min(calc(var(--sc-btn-rounded) * 2), 0.4em); transition: mask-image 0.15s linear; } :host([scrollable][direction="row"]) #menu-content { overflow-x: scroll; scroll-snap-align: start; scroll-snap-type: x mandatory; } :host([scrollable][direction="column"]) #menu-content { overflow-y: scroll; scroll-snap-align: start; scroll-snap-type: y mandatory; } :host([scrollable])::-webkit-scrollbar { display: none !important; } :host([scrollable][direction="row"].shadow-right) #menu-content { -webkit-mask-image: linear-gradient( to left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); mask-image: linear-gradient( to left, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); } :host([scrollable][direction="row"].shadow-left) #menu-content { -webkit-mask-image: linear-gradient( to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); mask-image: linear-gradient( to right, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); } :host([scrollable][direction="row"].shadow-left.shadow-right) #menu-content { -webkit-mask-image: linear-gradient( to right, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 10%, rgba(0, 0, 0, 1) 90%, rgba(0, 0, 0, 0) 100% ); mask-image: linear-gradient( to right, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 10%, rgba(0, 0, 0, 1) 90%, rgba(0, 0, 0, 0) 100% ); } :host([scrollable][direction="column"].shadow-top) #menu-content { -webkit-mask-image: linear-gradient( to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); mask-image: linear-gradient( to bottom, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); } :host([scrollable][direction="column"].shadow-bottom) #menu-content { -webkit-mask-image: linear-gradient( to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); mask-image: linear-gradient( to top, rgba(0, 0, 0, 0), rgba(0, 0, 0, 1) 10% ); } :host([scrollable][direction="column"].shadow-top.shadow-bottom) #menu-content { -webkit-mask-image: linear-gradient( to top, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 10%, rgba(0, 0, 0, 1) 90%, rgba(0, 0, 0, 0) 100% ); mask-image: linear-gradient( to bottom, rgba(0, 0, 0, 0) 0%, rgba(0, 0, 0, 1) 10%, rgba(0, 0, 0, 1) 90%, rgba(0, 0, 0, 0) 100% ); } `, shadowable, ]; /** * Taille du composant, peut avoir un effet sur ses composants enfants * tels que les dividers / boutons,... qui n'auraient pas d'attribut size précisés. */ @property({ type: String, reflect: true }) size?: Size; /** * Direction */ @property({ type: String, reflect: true }) direction: "row" | "column" = "column"; @property({ type: String }) gap = "var(--sc-menu-gap)"; @property({ type: String, reflect: true }) align: | "center" | "left" | "right" = "left"; /** * Ombre */ @property({ type: String, reflect: true }) shadow: Shadow = null; @property({ type: String }) moreShape: "square" | "circle" = "circle"; @property({ type: Boolean }) scrollable = false; observer: ResizeObserver | null = null; attributeObserver: MutationObserver | null = null; /** * Propriété min-width du bouton */ @property({ type: String }) minWidth = "0"; @query("menu") menu!: HTMLElement; @query("#menu-content") menuContent!: HTMLElement; @queryAssignedElements({ selector: "*" }) menuChildren!: Array; @queryAssignedElements({ slot: "more", selector: "*" }) moreElements!: Array; @state() hasMoreElements = false; checkIfMore() { this.hasMoreElements = !!this.moreElements?.length; } moreSlotChange() { this.checkIfMore(); this.updateIsScollable(); } updated(_changedProperties: PropertyValues): void { const moreBtn = this.querySelector(".more-btn"); if (this.size && moreBtn) { moreBtn.setAttribute("size", this.size); } super.updated(_changedProperties); } setDividersSize(assignedElements: Array) { assignedElements.forEach((elt) => { elt.setAttribute("size", "sm"); this.direction == "row" ? elt.style.setProperty("margin", "0 .1rem ") : elt.style.setProperty("margin", " 0.1rem 0"); }); } mainSlotChange() { this.setChildrenSize(this.menuChildren); this.setDividersSize(this.menuChildren); this.updateIsScollable(); this.updateScrollPosition(); this.observeMenuItemsAttributes(); } observeMenuItemsAttributes() { // Déconnecter l'ancien observer si existant this.attributeObserver?.disconnect(); // Créer un nouvel observer pour surveiller les changements d'attribut 'active' this.attributeObserver = new MutationObserver(() => { this.updateScrollPosition(); }); // Observer chaque menu-item this.menuChildren.forEach((item) => { this.attributeObserver!.observe(item, { attributes: true, attributeFilter: ["active"], }); }); } updateScrollPosition() { // Si on est en mode scrollable et qu'il y a exactement un seul menu-item actif if (this.scrollable) { const activeItems = this.menuChildren.filter( (item) => item.hasAttribute("active") && item.getAttribute("active") !== "false" ); if (activeItems.length === 1) { requestAnimationFrame(() => { activeItems[0].scrollIntoView({ behavior: "smooth", block: "nearest", inline: "center", }); }); } } } connectedCallback(): void { this.observer = new ResizeObserver(this.updateIsScollable); // observe if menu elements are overflowing and initiate scrollable this.observer.observe(this); super.connectedCallback(); } protected firstUpdated(_changedProperties: PropertyValues): void { this.menuContent.addEventListener("scrollend", this.handleScrollEnd); } // Écouter l'événement scrollend pour mettre à jour les ombres après le scroll handleScrollEnd = () => { this.setScrollShadow(this.menuContent, this.direction); }; disconnectedCallback(): void { this.observer?.disconnect(); this.attributeObserver?.disconnect(); this.menuContent.removeEventListener("scrollend", this.handleScrollEnd); super.disconnectedCallback(); } updateIsScollable = () => { if (this.scrollable) { this.initScrollable(); this.setScrollShadow(this.menuContent, this.direction); } }; initScrollable(): void { let isDown = false; let startX: number; let scrollLeft: number; if (this.scrollable) { this.addEventListener("mousedown", (e) => { isDown = true; this.classList.add("active"); startX = e.pageX - this.menuContent.offsetLeft; scrollLeft = this.menuContent.scrollLeft; }); this.addEventListener("mouseleave", () => { isDown = false; this.classList.remove("active"); }); this.addEventListener("mouseup", () => { isDown = false; this.classList.remove("active"); }); this.addEventListener("mousemove", (e) => { if (!isDown) return; e.preventDefault(); const x = e.pageX - this.menuContent.offsetLeft; const walk = (x - startX) * 1.5; //scroll-fast this.menuContent.scrollLeft = scrollLeft - walk; this.setScrollShadow(this.menuContent, this.direction); }); this.addEventListener("scroll", (e) => { e.preventDefault(); // const delta = Math.sign(e.deltaY); // this.scrollLeft += delta * 50; this.setScrollShadow(this.menuContent, this.direction); }); } } setScrollShadow(target: HTMLElement, direction: string): void { if (direction == "row") { if (target.scrollLeft > 0) { this.classList.add("shadow-left"); } else { this.classList.remove("shadow-left"); } if (target.scrollLeft < target.scrollWidth - target.offsetWidth) { this.classList.add("shadow-right"); } else { this.classList.remove("shadow-right"); } } else if (direction == "column") { if (target.scrollTop > 0) { this.classList.add("shadow-top"); } else { this.classList.remove("shadow-top"); } if (target.scrollTop < target.scrollHeight - (target.offsetHeight + 1)) { this.classList.add("shadow-bottom"); } else { this.classList.remove("shadow-bottom"); } } } setChildrenSize(menuItems: Array) { menuItems.forEach((elt) => { if (this.size) { elt.setAttribute("size", this.size); } if (this.align) { if ( elt.getAttribute("shape") != "square" && elt.getAttribute("shape") != "circle" ) { elt.setAttribute("align", this.align); } } if (this.direction == "row") { if (elt.getAttribute("shape") == "block") { elt.setAttribute("shape", "default"); } } }); } render() { const menuStyles = { minWidth: this.minWidth, flexDirection: this.direction, }; const isMenuRow = this.direction == "row"; const menuContentStyles = { gap: this.gap, flexDirection: this.direction, }; const popStyles = { display: "block", alignSelf: isMenuRow ? "center" : "flex-start", justifySelf: "center", flexDirection: this.direction, }; const popBtnStyles = { marginLeft: isMenuRow ? "" : ".55em", }; return html` `; } }