186 lines
7.1 KiB
JavaScript
186 lines
7.1 KiB
JavaScript
/** Dropdown UI component for layer navigation */
|
|
import { DROPDOWN_CONTAINER_STYLES, DROPDOWN_ITEM_BASE_STYLES, DROPDOWN_ITEM_ACTIVE_COLOR, DROPDOWN_ITEM_ACTIVE_BG, DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT, DROPDOWN_ITEM_HOVER_BG, DEPTH_INDENT_PX, BASE_PADDING_PX, CHEVRON_COLLAPSED, CHEVRON_EXPANDED, CHEVRON_ATTR, LAYER_DROPDOWN_ATTR, } from "./consts.js";
|
|
import { applyStyles, getLayerDisplayName } from "./utils.js";
|
|
import { PLUGIN_ELEMENT_ATTR } from "../utils.js";
|
|
let activeDropdown = null;
|
|
let activeLabel = null;
|
|
let outsideMousedownHandler = null;
|
|
let activeOnHoverEnd = null;
|
|
let activeKeydownHandler = null;
|
|
function createDropdownItem(layer, isActive, { onSelect, onHover, onHoverEnd }) {
|
|
const item = document.createElement("div");
|
|
item.textContent = getLayerDisplayName(layer);
|
|
applyStyles(item, DROPDOWN_ITEM_BASE_STYLES);
|
|
const depth = layer.depth ?? 0;
|
|
if (depth > 0) {
|
|
item.style.paddingLeft = `${BASE_PADDING_PX + depth * DEPTH_INDENT_PX}px`;
|
|
}
|
|
if (isActive) {
|
|
item.style.color = DROPDOWN_ITEM_ACTIVE_COLOR;
|
|
item.style.backgroundColor = DROPDOWN_ITEM_ACTIVE_BG;
|
|
item.style.fontWeight = DROPDOWN_ITEM_ACTIVE_FONT_WEIGHT;
|
|
}
|
|
item.addEventListener("mouseenter", () => {
|
|
if (!isActive)
|
|
item.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
if (onHover)
|
|
onHover(layer);
|
|
});
|
|
item.addEventListener("mouseleave", () => {
|
|
if (!isActive)
|
|
item.style.backgroundColor = "transparent";
|
|
if (onHoverEnd)
|
|
onHoverEnd();
|
|
});
|
|
item.addEventListener("click", (e) => {
|
|
e.stopPropagation();
|
|
e.preventDefault();
|
|
onSelect(layer);
|
|
});
|
|
return item;
|
|
}
|
|
/** Create the dropdown DOM element with layer items */
|
|
export function createDropdownElement(layers, currentElement, callbacks) {
|
|
const container = document.createElement("div");
|
|
container.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
container.setAttribute(PLUGIN_ELEMENT_ATTR, "true");
|
|
applyStyles(container, DROPDOWN_CONTAINER_STYLES);
|
|
layers.forEach((layer) => {
|
|
const isActive = layer.element === currentElement;
|
|
container.appendChild(createDropdownItem(layer, isActive, callbacks));
|
|
});
|
|
return container;
|
|
}
|
|
/** Add chevron indicator and pointer-events to the label */
|
|
export function enhanceLabelWithChevron(label) {
|
|
if (label.querySelector(`[${CHEVRON_ATTR}]`))
|
|
return;
|
|
const chevron = document.createElement("span");
|
|
chevron.setAttribute(CHEVRON_ATTR, "true");
|
|
chevron.style.display = "inline-flex";
|
|
chevron.innerHTML = CHEVRON_COLLAPSED;
|
|
label.appendChild(chevron);
|
|
label.style.display = "inline-flex";
|
|
label.style.alignItems = "center";
|
|
label.style.cursor = "pointer";
|
|
label.style.userSelect = "none";
|
|
label.style.whiteSpace = "nowrap";
|
|
label.style.pointerEvents = "auto";
|
|
label.setAttribute(LAYER_DROPDOWN_ATTR, "true");
|
|
label.setAttribute(PLUGIN_ELEMENT_ATTR, "true");
|
|
}
|
|
function setupKeyboardNavigation(dropdown, layers, currentElement, { onSelect, onHover, onHoverEnd }) {
|
|
const items = Array.from(dropdown.children);
|
|
let focusedIndex = layers.findIndex((l) => l.element === currentElement);
|
|
const setFocusedItem = (index) => {
|
|
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
const prev = items[focusedIndex];
|
|
if (prev.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
|
|
prev.style.backgroundColor = "transparent";
|
|
}
|
|
}
|
|
focusedIndex = index;
|
|
if (focusedIndex >= 0 && focusedIndex < items.length) {
|
|
const cur = items[focusedIndex];
|
|
if (cur.style.color !== DROPDOWN_ITEM_ACTIVE_COLOR) {
|
|
cur.style.backgroundColor = DROPDOWN_ITEM_HOVER_BG;
|
|
}
|
|
cur.scrollIntoView({ block: "nearest" });
|
|
if (onHover && focusedIndex >= 0 && focusedIndex < layers.length) {
|
|
onHover(layers[focusedIndex]);
|
|
}
|
|
}
|
|
};
|
|
activeKeydownHandler = (e) => {
|
|
if (e.key === "ArrowDown") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setFocusedItem(focusedIndex < items.length - 1 ? focusedIndex + 1 : 0);
|
|
}
|
|
else if (e.key === "ArrowUp") {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
setFocusedItem(focusedIndex > 0 ? focusedIndex - 1 : items.length - 1);
|
|
}
|
|
else if (e.key === "Enter" && focusedIndex >= 0 && focusedIndex < layers.length) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
if (onHoverEnd)
|
|
onHoverEnd();
|
|
onSelect(layers[focusedIndex]);
|
|
closeDropdown();
|
|
}
|
|
};
|
|
document.addEventListener("keydown", activeKeydownHandler, true);
|
|
}
|
|
function setupOutsideClickHandler(dropdown, label) {
|
|
let skipFirst = true;
|
|
outsideMousedownHandler = (e) => {
|
|
if (skipFirst) {
|
|
skipFirst = false;
|
|
return;
|
|
}
|
|
const target = e.target;
|
|
if (!dropdown.contains(target) && target !== label) {
|
|
closeDropdown();
|
|
}
|
|
};
|
|
document.addEventListener("mousedown", outsideMousedownHandler, true);
|
|
}
|
|
/** Show the dropdown below the label element */
|
|
export function showDropdown(label, layers, currentElement, callbacks) {
|
|
closeDropdown();
|
|
const dropdown = createDropdownElement(layers, currentElement, {
|
|
...callbacks,
|
|
onSelect: (layer) => {
|
|
if (callbacks.onHoverEnd)
|
|
callbacks.onHoverEnd();
|
|
callbacks.onSelect(layer);
|
|
closeDropdown();
|
|
},
|
|
});
|
|
const overlay = label.parentElement;
|
|
if (!overlay)
|
|
return;
|
|
dropdown.style.top = `${label.offsetTop + label.offsetHeight + 2}px`;
|
|
dropdown.style.left = `${label.offsetLeft}px`;
|
|
overlay.appendChild(dropdown);
|
|
activeDropdown = dropdown;
|
|
activeLabel = label;
|
|
const chevronEl = label.querySelector(`[${CHEVRON_ATTR}]`);
|
|
if (chevronEl) {
|
|
chevronEl.innerHTML = CHEVRON_EXPANDED;
|
|
}
|
|
activeOnHoverEnd = callbacks.onHoverEnd ?? null;
|
|
setupKeyboardNavigation(dropdown, layers, currentElement, callbacks);
|
|
setupOutsideClickHandler(dropdown, label);
|
|
}
|
|
/** Close the active dropdown and clean up listeners */
|
|
export function closeDropdown() {
|
|
const chevronEl = activeLabel?.querySelector(`[${CHEVRON_ATTR}]`);
|
|
if (chevronEl) {
|
|
chevronEl.innerHTML = CHEVRON_COLLAPSED;
|
|
}
|
|
activeLabel = null;
|
|
if (activeOnHoverEnd) {
|
|
activeOnHoverEnd();
|
|
activeOnHoverEnd = null;
|
|
}
|
|
if (activeDropdown && activeDropdown.parentNode) {
|
|
activeDropdown.remove();
|
|
}
|
|
activeDropdown = null;
|
|
if (outsideMousedownHandler) {
|
|
document.removeEventListener("mousedown", outsideMousedownHandler, true);
|
|
outsideMousedownHandler = null;
|
|
}
|
|
if (activeKeydownHandler) {
|
|
document.removeEventListener("keydown", activeKeydownHandler, true);
|
|
activeKeydownHandler = null;
|
|
}
|
|
}
|
|
/** Check if a dropdown is currently visible */
|
|
export function isDropdownOpen() {
|
|
return activeDropdown !== null;
|
|
}
|
|
//# sourceMappingURL=dropdown-ui.js.map
|