beam: add per-model viewer settings
This commit is contained in:
@@ -0,0 +1,216 @@
|
||||
const closeEventName = "nodedc-select-open";
|
||||
|
||||
const makeSelectId = () => `ndc-select-${Math.random().toString(16).slice(2)}`;
|
||||
|
||||
export function createGlassSelect({
|
||||
host,
|
||||
value,
|
||||
options = [],
|
||||
ariaLabel = "Выбрать",
|
||||
disabled = false,
|
||||
onChange = null,
|
||||
}) {
|
||||
if (!host) return null;
|
||||
|
||||
const selectId = makeSelectId();
|
||||
let currentValue = value;
|
||||
let currentOptions = [...options];
|
||||
let open = false;
|
||||
let menu = null;
|
||||
let menuRect = { top: 0, left: 0, width: 0 };
|
||||
|
||||
host.innerHTML = "";
|
||||
|
||||
const root = document.createElement("div");
|
||||
root.className = "nodedc-select";
|
||||
|
||||
const control = document.createElement("div");
|
||||
control.className = "nodedc-select__control";
|
||||
|
||||
const valueEl = document.createElement("div");
|
||||
valueEl.className = "nodedc-select__value";
|
||||
|
||||
const toggle = document.createElement("button");
|
||||
toggle.type = "button";
|
||||
toggle.className = "nodedc-select__toggle";
|
||||
toggle.setAttribute("aria-label", ariaLabel);
|
||||
toggle.setAttribute("aria-haspopup", "listbox");
|
||||
toggle.disabled = !!disabled;
|
||||
|
||||
const chevron = document.createElement("span");
|
||||
chevron.className = "nodedc-select__chevron";
|
||||
chevron.setAttribute("aria-hidden", "true");
|
||||
toggle.appendChild(chevron);
|
||||
|
||||
control.appendChild(valueEl);
|
||||
control.appendChild(toggle);
|
||||
root.appendChild(control);
|
||||
host.appendChild(root);
|
||||
|
||||
const selectedOption = () => (
|
||||
currentOptions.find((option) => option.value === currentValue) ||
|
||||
currentOptions[0] ||
|
||||
null
|
||||
);
|
||||
|
||||
const updateValue = () => {
|
||||
const selected = selectedOption();
|
||||
valueEl.textContent = selected?.label || "";
|
||||
toggle.setAttribute("aria-expanded", open ? "true" : "false");
|
||||
};
|
||||
|
||||
const updateMenuRect = () => {
|
||||
const rect = root.getBoundingClientRect();
|
||||
const toggleWidth = 42;
|
||||
const gap = 8;
|
||||
const menuMaxHeight = 232;
|
||||
const bottomTop = rect.bottom + 6;
|
||||
const top = bottomTop + menuMaxHeight > window.innerHeight - 12
|
||||
? Math.max(12, rect.top - menuMaxHeight - 6)
|
||||
: bottomTop;
|
||||
|
||||
menuRect = {
|
||||
top,
|
||||
left: rect.left,
|
||||
width: Math.max(180, rect.width - toggleWidth - gap),
|
||||
};
|
||||
|
||||
if (menu) {
|
||||
menu.style.top = `${menuRect.top}px`;
|
||||
menu.style.left = `${menuRect.left}px`;
|
||||
menu.style.width = `${menuRect.width}px`;
|
||||
}
|
||||
};
|
||||
|
||||
const renderMenuOptions = () => {
|
||||
if (!menu) return;
|
||||
menu.innerHTML = "";
|
||||
currentOptions.forEach((option) => {
|
||||
const item = document.createElement("button");
|
||||
item.type = "button";
|
||||
item.className = "nodedc-dropdown-option nodedc-select__option";
|
||||
item.dataset.value = option.value;
|
||||
if (option.value === currentValue) {
|
||||
item.dataset.selected = "true";
|
||||
}
|
||||
item.setAttribute("role", "option");
|
||||
item.setAttribute("aria-selected", option.value === currentValue ? "true" : "false");
|
||||
item.textContent = option.label;
|
||||
item.addEventListener("click", () => {
|
||||
setValue(option.value, { emit: true });
|
||||
closeMenu();
|
||||
});
|
||||
menu.appendChild(item);
|
||||
});
|
||||
};
|
||||
|
||||
const openMenu = () => {
|
||||
if (toggle.disabled || open) return;
|
||||
open = true;
|
||||
updateValue();
|
||||
updateMenuRect();
|
||||
window.dispatchEvent(new CustomEvent(closeEventName, { detail: { id: selectId } }));
|
||||
|
||||
menu = document.createElement("div");
|
||||
menu.className = "nodedc-dropdown-surface nodedc-select__menu";
|
||||
menu.setAttribute("role", "listbox");
|
||||
menu.style.position = "fixed";
|
||||
menu.style.top = `${menuRect.top}px`;
|
||||
menu.style.left = `${menuRect.left}px`;
|
||||
menu.style.width = `${menuRect.width}px`;
|
||||
renderMenuOptions();
|
||||
document.body.appendChild(menu);
|
||||
|
||||
document.addEventListener("mousedown", handleDocumentPointerDown);
|
||||
document.addEventListener("keydown", handleKeyDown);
|
||||
window.addEventListener(closeEventName, handleOtherSelectOpen);
|
||||
window.addEventListener("resize", updateMenuRect);
|
||||
window.addEventListener("scroll", updateMenuRect, true);
|
||||
};
|
||||
|
||||
function closeMenu() {
|
||||
if (!open) return;
|
||||
open = false;
|
||||
updateValue();
|
||||
if (menu) {
|
||||
menu.remove();
|
||||
menu = null;
|
||||
}
|
||||
document.removeEventListener("mousedown", handleDocumentPointerDown);
|
||||
document.removeEventListener("keydown", handleKeyDown);
|
||||
window.removeEventListener(closeEventName, handleOtherSelectOpen);
|
||||
window.removeEventListener("resize", updateMenuRect);
|
||||
window.removeEventListener("scroll", updateMenuRect, true);
|
||||
}
|
||||
|
||||
function handleDocumentPointerDown(event) {
|
||||
const target = event.target;
|
||||
if (
|
||||
target instanceof Node &&
|
||||
!root.contains(target) &&
|
||||
(!menu || !menu.contains(target))
|
||||
) {
|
||||
closeMenu();
|
||||
}
|
||||
}
|
||||
|
||||
function handleKeyDown(event) {
|
||||
if (event.key === "Escape") closeMenu();
|
||||
}
|
||||
|
||||
function handleOtherSelectOpen(event) {
|
||||
if (event?.detail?.id !== selectId) closeMenu();
|
||||
}
|
||||
|
||||
function setValue(nextValue, { emit = false } = {}) {
|
||||
const nextOption = currentOptions.find((option) => option.value === nextValue);
|
||||
if (!nextOption) return;
|
||||
const changed = currentValue !== nextValue;
|
||||
currentValue = nextValue;
|
||||
updateValue();
|
||||
renderMenuOptions();
|
||||
if (emit && changed && typeof onChange === "function") {
|
||||
onChange(nextValue);
|
||||
}
|
||||
}
|
||||
|
||||
const setOptions = (nextOptions = [], nextValue = currentValue) => {
|
||||
currentOptions = [...nextOptions];
|
||||
currentValue = currentOptions.some((option) => option.value === nextValue)
|
||||
? nextValue
|
||||
: currentOptions[0]?.value;
|
||||
updateValue();
|
||||
renderMenuOptions();
|
||||
};
|
||||
|
||||
const setDisabled = (nextDisabled) => {
|
||||
toggle.disabled = !!nextDisabled;
|
||||
if (toggle.disabled) closeMenu();
|
||||
};
|
||||
|
||||
const destroy = () => {
|
||||
closeMenu();
|
||||
root.remove();
|
||||
};
|
||||
|
||||
toggle.addEventListener("click", () => {
|
||||
if (open) {
|
||||
closeMenu();
|
||||
} else {
|
||||
openMenu();
|
||||
}
|
||||
});
|
||||
valueEl.addEventListener("click", openMenu);
|
||||
|
||||
updateValue();
|
||||
|
||||
return {
|
||||
root,
|
||||
setValue,
|
||||
setOptions,
|
||||
setDisabled,
|
||||
close: closeMenu,
|
||||
destroy,
|
||||
getValue: () => currentValue,
|
||||
};
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
import { createSliderControl } from "./controls/slider.js";
|
||||
|
||||
const defaultSettings = {
|
||||
themeVersion: "v3-yellow",
|
||||
themeVersion: "v4-beam-lime",
|
||||
bgOuter: "#0f0f0f",
|
||||
canvasTop: "#1c1c1c",
|
||||
canvasBottom: "#1c1c1c",
|
||||
@@ -15,14 +15,14 @@ const defaultSettings = {
|
||||
projectBtnFill: "#1c1c1c",
|
||||
projectBtnBorder: "#233044",
|
||||
projectBtnHover: "rgba(255,234,0,0.7)",
|
||||
modalBtnPrimary: "#ffea00",
|
||||
modalBtnPrimary: "#c4ff67",
|
||||
modalBtnSecondary: "#292929",
|
||||
loaderColor: "#ffea00",
|
||||
loaderColor: "#c4ff67",
|
||||
templateGlow: 80,
|
||||
toolbarBg: "#1c1c1c",
|
||||
toolbarBgActive: "#ffea00",
|
||||
toolbarBgActive: "#c4ff67",
|
||||
toolbarBorder: "#233044",
|
||||
toolbarOutline: "#ffea00",
|
||||
toolbarOutline: "#c4ff67",
|
||||
modalBg: "#1c1c1c",
|
||||
modalBgAlpha: 1,
|
||||
modalFocus: "#292929",
|
||||
@@ -35,19 +35,19 @@ const defaultSettings = {
|
||||
text: "#e6edf3",
|
||||
panel: "#1c1c1c",
|
||||
panel2: "#292929",
|
||||
highlight: "#ffea00",
|
||||
highlight: "#c4ff67",
|
||||
cubeText: "#242424",
|
||||
cubeEdges: "#e0e7ff",
|
||||
plus: "#ffea00",
|
||||
navCubeBase: "#ffea00",
|
||||
plus: "#c4ff67",
|
||||
navCubeBase: "#ffffff",
|
||||
navCubeHover: "#ffffff",
|
||||
navCubeText: "#242424",
|
||||
navCubeEdge: "#e0e7ff",
|
||||
navCubeEdgeAlpha: 0.6,
|
||||
measureLine: "#ffea00",
|
||||
measureLabel: "#ffea00",
|
||||
measureLine: "#c4ff67",
|
||||
measureLabel: "#c4ff67",
|
||||
measureLabelText: "#292929",
|
||||
measureDot: "#ffea00",
|
||||
measureDot: "#c4ff67",
|
||||
};
|
||||
|
||||
const STORAGE_KEY = "bimdc-settings-v3";
|
||||
|
||||
Reference in New Issue
Block a user