mirror of
https://github.com/Theodor-Springmann-Stiftung/musenalm.git
synced 2025-10-29 09:15:33 +00:00
divmenu
This commit is contained in:
@@ -1,5 +1,4 @@
|
|||||||
const TS_MENU_COMPONENT = "ts-menu";
|
const USER_PROVIDED_MENU_BUTTON_CLASS = "div-menu-button";
|
||||||
const TS_TOGGLE_BUTTON = "ts-menu-toggle-button";
|
|
||||||
const TS_POPUP = "ts-menu-popup";
|
const TS_POPUP = "ts-menu-popup";
|
||||||
const TS_LIST = "ts-menu-list";
|
const TS_LIST = "ts-menu-list";
|
||||||
const TS_PLACEHOLDER_MESSAGE = "ts-menu-placeholder-message";
|
const TS_PLACEHOLDER_MESSAGE = "ts-menu-placeholder-message";
|
||||||
@@ -8,6 +7,16 @@ const TS_ITEM_ACTION = "ts-menu-item-action";
|
|||||||
const TS_ITEM_IS_SELECTED = "ts-item-is-selected";
|
const TS_ITEM_IS_SELECTED = "ts-item-is-selected";
|
||||||
const TS_CONTENT_CLOSE_BUTTON = "ts-content-close-button";
|
const TS_CONTENT_CLOSE_BUTTON = "ts-content-close-button";
|
||||||
const TAILWIND_HIDDEN_CLASS = "hidden";
|
const TAILWIND_HIDDEN_CLASS = "hidden";
|
||||||
|
const TS_MENU_BUTTON_STATE_DISABLED = "ts-menu-button--disabled";
|
||||||
|
// New constants for menu label states
|
||||||
|
const TS_MENU_LABEL_IN_MENU_STATE = "ts-menu-label--in-menu";
|
||||||
|
const TS_MENU_LABEL_DISPLAYED_STATE = "ts-menu-label--displayed";
|
||||||
|
//
|
||||||
|
// Prereq: child divs must have prop data-value, and a label element
|
||||||
|
// the label element must have class "menu-label"
|
||||||
|
// The child divs will be moved to the target element when selected
|
||||||
|
// The target element must be specified by the attribute target-id on the custom element
|
||||||
|
// The menu button must have the class div-menu-button
|
||||||
|
|
||||||
export class DivMenu extends HTMLElement {
|
export class DivMenu extends HTMLElement {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -17,6 +26,7 @@ export class DivMenu extends HTMLElement {
|
|||||||
this._originalChildDivs = [];
|
this._originalChildDivs = [];
|
||||||
this._observer = null;
|
this._observer = null;
|
||||||
this._menuPlaceholderMessageElement = null;
|
this._menuPlaceholderMessageElement = null;
|
||||||
|
this._menuButton = null;
|
||||||
|
|
||||||
this._toggleMenu = this._toggleMenu.bind(this);
|
this._toggleMenu = this._toggleMenu.bind(this);
|
||||||
this._handleMenuItemClick = this._handleMenuItemClick.bind(this);
|
this._handleMenuItemClick = this._handleMenuItemClick.bind(this);
|
||||||
@@ -25,29 +35,33 @@ export class DivMenu extends HTMLElement {
|
|||||||
this._handleMutations = this._handleMutations.bind(this);
|
this._handleMutations = this._handleMutations.bind(this);
|
||||||
this._checkMenuPlaceholderAndButton = this._checkMenuPlaceholderAndButton.bind(this);
|
this._checkMenuPlaceholderAndButton = this._checkMenuPlaceholderAndButton.bind(this);
|
||||||
this._clearFormElements = this._clearFormElements.bind(this);
|
this._clearFormElements = this._clearFormElements.bind(this);
|
||||||
|
this._refreshTargetOrder = this._refreshTargetOrder.bind(this); // Bind new method
|
||||||
}
|
}
|
||||||
|
|
||||||
connectedCallback() {
|
connectedCallback() {
|
||||||
this._originalChildDivs = Array.from(this.children).filter((node) => node.nodeType === Node.ELEMENT_NODE && node.tagName === "DIV");
|
this._menuButton = this.querySelector(`.${USER_PROVIDED_MENU_BUTTON_CLASS}`);
|
||||||
|
if (!this._menuButton) {
|
||||||
|
console.error(`TabSelectorMenu: User-provided menu button with class '${USER_PROVIDED_MENU_BUTTON_CLASS}' not found.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
this._originalChildDivs = Array.from(this.children).filter((node) => node.nodeType === Node.ELEMENT_NODE && node.tagName === "DIV" && !node.classList.contains(USER_PROVIDED_MENU_BUTTON_CLASS));
|
||||||
this._originalChildDivs.forEach((div) => div.remove());
|
this._originalChildDivs.forEach((div) => div.remove());
|
||||||
|
|
||||||
const componentHTML = `
|
const componentPopupHTML = `
|
||||||
<button type="button" class="${TS_TOGGLE_BUTTON}">Options</button>
|
|
||||||
<div class="${TS_POPUP}">
|
<div class="${TS_POPUP}">
|
||||||
<ul class="${TS_LIST}">
|
<ul class="${TS_LIST}">
|
||||||
<li class="${TS_PLACEHOLDER_MESSAGE}">All items are currently shown.</li>
|
<li class="${TS_PLACEHOLDER_MESSAGE}">All items are currently shown.</li>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
this.innerHTML = componentHTML;
|
this.insertAdjacentHTML("beforeend", componentPopupHTML);
|
||||||
|
|
||||||
this._menuButton = this.querySelector(`.${TS_TOGGLE_BUTTON}`);
|
|
||||||
this._menuPopupElement = this.querySelector(`.${TS_POPUP}`);
|
this._menuPopupElement = this.querySelector(`.${TS_POPUP}`);
|
||||||
this._menuListElement = this.querySelector(`.${TS_LIST}`);
|
this._menuListElement = this.querySelector(`.${TS_LIST}`);
|
||||||
this._menuPlaceholderMessageElement = this.querySelector(`.${TS_PLACEHOLDER_MESSAGE}`);
|
this._menuPlaceholderMessageElement = this.querySelector(`.${TS_PLACEHOLDER_MESSAGE}`);
|
||||||
|
|
||||||
if (!this._menuButton || !this._menuPopupElement || !this._menuListElement || !this._menuPlaceholderMessageElement) {
|
if (!this._menuPopupElement || !this._menuListElement || !this._menuPlaceholderMessageElement) {
|
||||||
console.error("CRITICAL: Essential parts missing after creating component from string.");
|
console.error("CRITICAL: Popup structure parts missing.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +76,6 @@ export class DivMenu extends HTMLElement {
|
|||||||
this._observer = new MutationObserver(this._handleMutations);
|
this._observer = new MutationObserver(this._handleMutations);
|
||||||
|
|
||||||
this._originalChildDivs.forEach((sourceDiv) => {
|
this._originalChildDivs.forEach((sourceDiv) => {
|
||||||
// Use a specific class for the menu label to distinguish from form labels
|
|
||||||
const menuLabelElement = sourceDiv.querySelector("label.menu-label");
|
const menuLabelElement = sourceDiv.querySelector("label.menu-label");
|
||||||
const itemValue = sourceDiv.dataset.value;
|
const itemValue = sourceDiv.dataset.value;
|
||||||
|
|
||||||
@@ -97,6 +110,7 @@ export class DivMenu extends HTMLElement {
|
|||||||
sourceDiv,
|
sourceDiv,
|
||||||
menuItemButton: addedMenuItemButton,
|
menuItemButton: addedMenuItemButton,
|
||||||
menuListItem: addedMenuItemButton.parentElement,
|
menuListItem: addedMenuItemButton.parentElement,
|
||||||
|
menuLabelElement: menuLabelElement,
|
||||||
selected: isInitiallySelected,
|
selected: isInitiallySelected,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -104,13 +118,14 @@ export class DivMenu extends HTMLElement {
|
|||||||
});
|
});
|
||||||
|
|
||||||
this._menuItemsMap.forEach((itemData) => {
|
this._menuItemsMap.forEach((itemData) => {
|
||||||
if (itemData.selected) {
|
|
||||||
this._updateItemState(itemData, false, true);
|
this._updateItemState(itemData, false, true);
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
// _refreshTargetOrder() is called within _updateItemState, so target should be correct.
|
||||||
this._checkMenuPlaceholderAndButton();
|
this._checkMenuPlaceholderAndButton();
|
||||||
|
|
||||||
|
if (this._menuButton) {
|
||||||
this._menuButton.addEventListener("click", this._toggleMenu);
|
this._menuButton.addEventListener("click", this._toggleMenu);
|
||||||
|
}
|
||||||
this._menuListElement.addEventListener("click", this._handleMenuItemClick);
|
this._menuListElement.addEventListener("click", this._handleMenuItemClick);
|
||||||
document.addEventListener("click", this._handleClickOutside);
|
document.addEventListener("click", this._handleClickOutside);
|
||||||
}
|
}
|
||||||
@@ -126,11 +141,13 @@ export class DivMenu extends HTMLElement {
|
|||||||
closeButton.removeEventListener("click", this._handleContentClose);
|
closeButton.removeEventListener("click", this._handleContentClose);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
if (this._menuButton) {
|
||||||
|
this._menuButton.removeEventListener("click", this._toggleMenu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_clearFormElements(parentElement) {
|
_clearFormElements(parentElement) {
|
||||||
if (!parentElement) return;
|
if (!parentElement) return;
|
||||||
|
|
||||||
const inputs = parentElement.querySelectorAll("input, textarea, select");
|
const inputs = parentElement.querySelectorAll("input, textarea, select");
|
||||||
inputs.forEach((el) => {
|
inputs.forEach((el) => {
|
||||||
switch (el.type) {
|
switch (el.type) {
|
||||||
@@ -141,10 +158,6 @@ export class DivMenu extends HTMLElement {
|
|||||||
break;
|
break;
|
||||||
case "checkbox":
|
case "checkbox":
|
||||||
case "radio":
|
case "radio":
|
||||||
// For radios, only uncheck if it's the one that was checked.
|
|
||||||
// For checkboxes, this is fine.
|
|
||||||
// A more robust radio reset might involve finding the default or unchecking all in group.
|
|
||||||
// For simplicity here, just unchecking.
|
|
||||||
el.checked = false;
|
el.checked = false;
|
||||||
break;
|
break;
|
||||||
case "file":
|
case "file":
|
||||||
@@ -153,11 +166,45 @@ export class DivMenu extends HTMLElement {
|
|||||||
default:
|
default:
|
||||||
el.value = "";
|
el.value = "";
|
||||||
}
|
}
|
||||||
if (el.tagName === "SELECT") {
|
if (el.tagName === "SELECT") el.selectedIndex = 0;
|
||||||
el.selectedIndex = 0; // Reset to the first option (often a placeholder)
|
});
|
||||||
// or -1 to clear completely if no placeholder.
|
}
|
||||||
|
|
||||||
|
_refreshTargetOrder() {
|
||||||
|
if (!this._targetElement) return;
|
||||||
|
|
||||||
|
// Detach all currently managed divs from target
|
||||||
|
const managedDivsInTarget = [];
|
||||||
|
this._menuItemsMap.forEach((item) => {
|
||||||
|
if (item.sourceDiv.parentElement === this._targetElement) {
|
||||||
|
managedDivsInTarget.push(item.sourceDiv);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
managedDivsInTarget.forEach((div) => this._targetElement.removeChild(div));
|
||||||
|
|
||||||
|
// Remove existing component-managed placeholder if any
|
||||||
|
const existingPlaceholder = this._targetElement.querySelector(`.${TS_TARGET_PLACEHOLDER_CLASS}`);
|
||||||
|
if (existingPlaceholder) {
|
||||||
|
this._targetElement.removeChild(existingPlaceholder);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append selected divs in their original order
|
||||||
|
let hasSelectedItems = false;
|
||||||
|
this._menuItemsMap.forEach((item) => {
|
||||||
|
if (item.selected) {
|
||||||
|
item.sourceDiv.classList.remove(TAILWIND_HIDDEN_CLASS); // Ensure it's visible
|
||||||
|
this._targetElement.appendChild(item.sourceDiv);
|
||||||
|
hasSelectedItems = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add placeholder if target is empty and no items were selected to be appended
|
||||||
|
if (!hasSelectedItems) {
|
||||||
|
const placeholder = document.createElement("p");
|
||||||
|
placeholder.className = TS_TARGET_PLACEHOLDER_CLASS;
|
||||||
|
placeholder.innerHTML = "<em>Selected content will appear here.</em>";
|
||||||
|
this._targetElement.appendChild(placeholder);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_checkMenuPlaceholderAndButton() {
|
_checkMenuPlaceholderAndButton() {
|
||||||
@@ -170,7 +217,12 @@ export class DivMenu extends HTMLElement {
|
|||||||
});
|
});
|
||||||
const allItemsSelected = visibleItemCount === 0;
|
const allItemsSelected = visibleItemCount === 0;
|
||||||
this._menuPlaceholderMessageElement.style.display = allItemsSelected ? "block" : "none";
|
this._menuPlaceholderMessageElement.style.display = allItemsSelected ? "block" : "none";
|
||||||
this._menuButton.disabled = allItemsSelected;
|
|
||||||
|
if (allItemsSelected) {
|
||||||
|
this._menuButton.classList.add(TS_MENU_BUTTON_STATE_DISABLED);
|
||||||
|
} else {
|
||||||
|
this._menuButton.classList.remove(TS_MENU_BUTTON_STATE_DISABLED);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_handleMutations(mutationsList) {
|
_handleMutations(mutationsList) {
|
||||||
@@ -179,10 +231,8 @@ export class DivMenu extends HTMLElement {
|
|||||||
const targetDiv = mutation.target;
|
const targetDiv = mutation.target;
|
||||||
const itemValue = targetDiv.dataset.value;
|
const itemValue = targetDiv.dataset.value;
|
||||||
const itemData = this._menuItemsMap.get(itemValue);
|
const itemData = this._menuItemsMap.get(itemValue);
|
||||||
|
|
||||||
if (itemData) {
|
if (itemData) {
|
||||||
const isNowHiddenByClass = targetDiv.classList.contains(TAILWIND_HIDDEN_CLASS);
|
const isNowHiddenByClass = targetDiv.classList.contains(TAILWIND_HIDDEN_CLASS);
|
||||||
|
|
||||||
if (isNowHiddenByClass && itemData.selected) {
|
if (isNowHiddenByClass && itemData.selected) {
|
||||||
itemData.selected = false;
|
itemData.selected = false;
|
||||||
this._updateItemState(itemData, false, true);
|
this._updateItemState(itemData, false, true);
|
||||||
@@ -205,6 +255,9 @@ export class DivMenu extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_toggleMenu(event) {
|
_toggleMenu(event) {
|
||||||
|
if (this._menuButton && this._menuButton.classList.contains(TS_MENU_BUTTON_STATE_DISABLED)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
const isHidden = this._menuPopupElement.style.display === "none" || this._menuPopupElement.style.display === "";
|
const isHidden = this._menuPopupElement.style.display === "none" || this._menuPopupElement.style.display === "";
|
||||||
if (isHidden) {
|
if (isHidden) {
|
||||||
@@ -219,6 +272,9 @@ export class DivMenu extends HTMLElement {
|
|||||||
|
|
||||||
_handleClickOutside(event) {
|
_handleClickOutside(event) {
|
||||||
if (!this.contains(event.target) && this._menuPopupElement.style.display !== "none") {
|
if (!this.contains(event.target) && this._menuPopupElement.style.display !== "none") {
|
||||||
|
if (this._menuButton && this._menuButton.contains(event.target)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
this._closeMenu();
|
this._closeMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -228,7 +284,6 @@ export class DivMenu extends HTMLElement {
|
|||||||
if (clickedButton && clickedButton.dataset.value) {
|
if (clickedButton && clickedButton.dataset.value) {
|
||||||
const itemValue = clickedButton.dataset.value;
|
const itemValue = clickedButton.dataset.value;
|
||||||
const itemData = this._menuItemsMap.get(itemValue);
|
const itemData = this._menuItemsMap.get(itemValue);
|
||||||
|
|
||||||
if (itemData) {
|
if (itemData) {
|
||||||
if (!itemData.selected) {
|
if (!itemData.selected) {
|
||||||
itemData.selected = true;
|
itemData.selected = true;
|
||||||
@@ -239,40 +294,46 @@ export class DivMenu extends HTMLElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
_updateItemState(itemData, closeMenuAfterUpdate, manageHiddenClass) {
|
_updateItemState(itemData, closeMenuAfterUpdate, manageHiddenClass) {
|
||||||
if (!itemData || !itemData.sourceDiv || !itemData.menuItemButton || !itemData.menuListItem) {
|
if (!itemData || !itemData.sourceDiv || !itemData.menuItemButton || !itemData.menuListItem || !itemData.menuLabelElement) {
|
||||||
console.warn("TabSelectorMenu: Incomplete itemData for update:", itemData);
|
console.warn("TabSelectorMenu: Incomplete itemData for update:", itemData);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const menuLabel = itemData.menuLabelElement;
|
||||||
|
|
||||||
if (itemData.selected) {
|
if (itemData.selected) {
|
||||||
if (manageHiddenClass) {
|
// No direct DOM manipulation of sourceDiv here regarding targetElement
|
||||||
itemData.sourceDiv.classList.remove(TAILWIND_HIDDEN_CLASS);
|
// _refreshTargetOrder will handle placement.
|
||||||
}
|
if (manageHiddenClass) itemData.sourceDiv.classList.remove(TAILWIND_HIDDEN_CLASS);
|
||||||
itemData.menuItemButton.classList.add(TS_ITEM_IS_SELECTED);
|
itemData.menuItemButton.classList.add(TS_ITEM_IS_SELECTED);
|
||||||
itemData.menuListItem.style.display = "none";
|
itemData.menuListItem.style.display = "none";
|
||||||
|
|
||||||
if (this._targetElement) {
|
menuLabel.classList.add(TS_MENU_LABEL_DISPLAYED_STATE);
|
||||||
this._targetElement.appendChild(itemData.sourceDiv);
|
menuLabel.classList.remove(TS_MENU_LABEL_IN_MENU_STATE);
|
||||||
} else {
|
} else {
|
||||||
console.warn(`TabSelectorMenu: Cannot mount div. Target element not found.`);
|
if (manageHiddenClass) itemData.sourceDiv.classList.add(TAILWIND_HIDDEN_CLASS);
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (manageHiddenClass) {
|
|
||||||
itemData.sourceDiv.classList.add(TAILWIND_HIDDEN_CLASS);
|
|
||||||
}
|
|
||||||
this._clearFormElements(itemData.sourceDiv);
|
this._clearFormElements(itemData.sourceDiv);
|
||||||
itemData.menuItemButton.classList.remove(TS_ITEM_IS_SELECTED);
|
itemData.menuItemButton.classList.remove(TS_ITEM_IS_SELECTED);
|
||||||
itemData.menuListItem.style.display = "";
|
itemData.menuListItem.style.display = "";
|
||||||
|
|
||||||
if (itemData.sourceDiv.parentElement !== this) {
|
menuLabel.classList.add(TS_MENU_LABEL_IN_MENU_STATE);
|
||||||
|
menuLabel.classList.remove(TS_MENU_LABEL_DISPLAYED_STATE);
|
||||||
|
|
||||||
|
// If the div is being hidden, ensure it's moved back to the component
|
||||||
|
// if it was in the target. _refreshTargetOrder will handle its removal from target.
|
||||||
|
if (itemData.sourceDiv.parentElement !== this && itemData.sourceDiv.parentElement !== this._targetElement) {
|
||||||
this.appendChild(itemData.sourceDiv);
|
this.appendChild(itemData.sourceDiv);
|
||||||
|
} else if (itemData.sourceDiv.parentElement === this._targetElement) {
|
||||||
|
// It will be removed by _refreshTargetOrder, then re-appended to 'this' if needed
|
||||||
|
// For now, just ensure it's not orphaned if _refreshTargetOrder doesn't run immediately after this.
|
||||||
|
// Actually, better to let _refreshTargetOrder handle removal from target.
|
||||||
|
// And if it's not selected, ensure it's a child of 'this' for mutation observer.
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this._refreshTargetOrder(); // Centralize target DOM updates
|
||||||
this._checkMenuPlaceholderAndButton();
|
this._checkMenuPlaceholderAndButton();
|
||||||
|
|
||||||
if (closeMenuAfterUpdate) {
|
if (closeMenuAfterUpdate) this._closeMenu();
|
||||||
this._closeMenu();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user