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``;
}
}