mirror of
https://github.com/litruv/picoGraph.git
synced 2026-07-24 10:46:06 +10:00
Initial commit
moved from private version control
This commit is contained in:
295
scripts/ui/workspace/WorkspacePaletteManager.js
Normal file
295
scripts/ui/workspace/WorkspacePaletteManager.js
Normal file
@@ -0,0 +1,295 @@
|
||||
/**
|
||||
* @typedef {import('../BlueprintWorkspace.js').BlueprintWorkspace} BlueprintWorkspace
|
||||
* @typedef {import('../../nodes/nodeTypes.js').NodeDefinition} NodeDefinition
|
||||
* @typedef {import('../BlueprintWorkspace.js').PaletteNodeDefinition} PaletteNodeDefinition
|
||||
*/
|
||||
|
||||
import { PALETTE_DRAG_MIME } from "../BlueprintWorkspaceConstants.js";
|
||||
|
||||
/**
|
||||
* Coordinates palette rendering, search handling, and palette drag state.
|
||||
*/
|
||||
export class WorkspacePaletteManager {
|
||||
#workspace;
|
||||
/** @type {Set<string>} */
|
||||
#collapsedCategories;
|
||||
|
||||
/**
|
||||
* @param {BlueprintWorkspace} workspace Owning workspace instance.
|
||||
*/
|
||||
constructor(workspace) {
|
||||
this.#workspace = workspace;
|
||||
this.#collapsedCategories = new Set();
|
||||
}
|
||||
|
||||
/**
|
||||
* Wires palette UI listeners and renders initial content.
|
||||
*/
|
||||
initialize() {
|
||||
const search = this.#workspace.paletteSearch;
|
||||
search.addEventListener("input", () => {
|
||||
this.render(search.value);
|
||||
});
|
||||
|
||||
this.render();
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates palette DOM structure using the provided search filter.
|
||||
*
|
||||
* @param {string} [query] Optional search query.
|
||||
*/
|
||||
render(query) {
|
||||
const searchValue = query ?? "";
|
||||
const baseDefinitions = this.#workspace.registry.search(searchValue);
|
||||
const filteredBase = baseDefinitions.filter(
|
||||
(definition) => definition.id !== "get_var" && definition.id !== "set_var"
|
||||
);
|
||||
const variableShortcuts =
|
||||
this.#buildVariableShortcutDefinitions(searchValue);
|
||||
const definitions = filteredBase.concat(variableShortcuts);
|
||||
|
||||
const list = this.#workspace.paletteList;
|
||||
list.innerHTML = "";
|
||||
|
||||
if (!definitions.length) {
|
||||
const empty = document.createElement("p");
|
||||
empty.className = "palette-empty";
|
||||
empty.textContent = "No matching nodes";
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
|
||||
const grouped = new Map();
|
||||
definitions.forEach((definition) => {
|
||||
if (!grouped.has(definition.category)) {
|
||||
grouped.set(definition.category, []);
|
||||
}
|
||||
grouped.get(definition.category).push(definition);
|
||||
});
|
||||
|
||||
const isSearchActive = Boolean(searchValue && searchValue.trim().length);
|
||||
const categories = [...grouped.keys()].sort((a, b) => a.localeCompare(b));
|
||||
|
||||
categories.forEach((category) => {
|
||||
const entries = grouped
|
||||
.get(category)
|
||||
.slice()
|
||||
.sort((a, b) => a.title.localeCompare(b.title));
|
||||
|
||||
const section = document.createElement("section");
|
||||
section.className = "palette-section";
|
||||
section.dataset.category = category;
|
||||
|
||||
const header = document.createElement("button");
|
||||
header.type = "button";
|
||||
header.className = "palette-section-header";
|
||||
header.textContent = category;
|
||||
const slug = category.toLowerCase().replace(/[^a-z0-9]+/g, "_");
|
||||
const headerId = `palette_section_${slug}`;
|
||||
const bodyId = `${headerId}_body`;
|
||||
header.id = headerId;
|
||||
header.setAttribute("aria-controls", bodyId);
|
||||
const collapsed =
|
||||
!isSearchActive && this.#collapsedCategories.has(category);
|
||||
header.setAttribute("aria-expanded", String(!collapsed));
|
||||
header.addEventListener("click", () => {
|
||||
if (this.#collapsedCategories.has(category)) {
|
||||
this.#collapsedCategories.delete(category);
|
||||
} else {
|
||||
this.#collapsedCategories.add(category);
|
||||
}
|
||||
this.render(this.#workspace.paletteSearch.value);
|
||||
requestAnimationFrame(() => {
|
||||
const refreshedSection = [
|
||||
...this.#workspace.paletteList.querySelectorAll(".palette-section"),
|
||||
].find((element) => element.dataset.category === category);
|
||||
const refreshedHeader = refreshedSection?.querySelector(
|
||||
".palette-section-header"
|
||||
);
|
||||
if (refreshedHeader instanceof HTMLButtonElement) {
|
||||
refreshedHeader.focus();
|
||||
}
|
||||
});
|
||||
});
|
||||
section.appendChild(header);
|
||||
|
||||
const body = document.createElement("div");
|
||||
body.className = "palette-section-body";
|
||||
body.setAttribute("role", "group");
|
||||
body.id = bodyId;
|
||||
body.setAttribute("aria-labelledby", headerId);
|
||||
body.dataset.category = category;
|
||||
if (collapsed) {
|
||||
body.hidden = true;
|
||||
section.classList.add("is-collapsed");
|
||||
}
|
||||
|
||||
entries.forEach((definition) => {
|
||||
const isVariableShortcut = Boolean(
|
||||
definition.shortcut?.type === "variable"
|
||||
);
|
||||
const item = this.#createDefinitionButton(definition, {
|
||||
className: "palette-item",
|
||||
showCategory: false,
|
||||
draggable: !isVariableShortcut,
|
||||
onSelect: () => {
|
||||
this.#workspace.createNodeFromDefinition(definition);
|
||||
},
|
||||
onDragStart: isVariableShortcut
|
||||
? undefined
|
||||
: (event) => {
|
||||
this.#handlePaletteDragStart(event, definition.id);
|
||||
},
|
||||
onDragEnd: isVariableShortcut
|
||||
? undefined
|
||||
: () => {
|
||||
this.#handlePaletteDragEnd();
|
||||
},
|
||||
});
|
||||
body.appendChild(item);
|
||||
});
|
||||
|
||||
section.appendChild(body);
|
||||
list.appendChild(section);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows external callers to cancel palette drag state.
|
||||
*/
|
||||
handleDragEnd() {
|
||||
this.#handlePaletteDragEnd();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds a button node for palette entries with attached drag/click handlers.
|
||||
*
|
||||
* @param {PaletteNodeDefinition} definition Target definition.
|
||||
* @param {{ className?: string, showCategory?: boolean, draggable?: boolean, onSelect: () => void, onDragStart?: (event: DragEvent) => void, onDragEnd?: (event: DragEvent) => void }} options Button configuration.
|
||||
* @returns {HTMLButtonElement}
|
||||
*/
|
||||
#createDefinitionButton(definition, options) {
|
||||
const button = document.createElement("button");
|
||||
button.type = "button";
|
||||
button.className = options?.className ?? "palette-item";
|
||||
button.dataset.definitionId = definition.id;
|
||||
|
||||
const showCategory = options?.showCategory ?? true;
|
||||
|
||||
if (showCategory) {
|
||||
const category = document.createElement("span");
|
||||
category.className = "palette-item-category";
|
||||
category.textContent = definition.category;
|
||||
button.appendChild(category);
|
||||
}
|
||||
|
||||
const title = document.createElement("span");
|
||||
title.className = "palette-item-name";
|
||||
title.textContent = definition.title;
|
||||
button.appendChild(title);
|
||||
|
||||
if (definition.description) {
|
||||
button.title = definition.description;
|
||||
}
|
||||
|
||||
button.addEventListener("click", () => {
|
||||
options.onSelect();
|
||||
});
|
||||
|
||||
if (options?.draggable) {
|
||||
button.draggable = true;
|
||||
button.addEventListener("dragstart", (event) => {
|
||||
options.onDragStart?.(event);
|
||||
});
|
||||
button.addEventListener("dragend", (event) => {
|
||||
options.onDragEnd?.(event);
|
||||
});
|
||||
}
|
||||
|
||||
return button;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepares drag metadata when beginning a palette drag interaction.
|
||||
*
|
||||
* @param {DragEvent} event Drag event payload.
|
||||
* @param {string} definitionId Node definition identifier being dragged.
|
||||
*/
|
||||
#handlePaletteDragStart(event, definitionId) {
|
||||
if (!event.dataTransfer) {
|
||||
return;
|
||||
}
|
||||
|
||||
event.dataTransfer.setData(PALETTE_DRAG_MIME, definitionId);
|
||||
event.dataTransfer.setData("text/plain", definitionId);
|
||||
event.dataTransfer.effectAllowed = "copy";
|
||||
this.#workspace.workspaceDragDepth = 0;
|
||||
this.#workspace.workspaceElement.classList.add("is-palette-dragging");
|
||||
this.#workspace.activePaletteDefinitionId = definitionId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleans up workspace styling once a palette drag interaction ends.
|
||||
*/
|
||||
#handlePaletteDragEnd() {
|
||||
this.#workspace.workspaceDragDepth = 0;
|
||||
this.#workspace.workspaceElement.classList.remove("is-palette-dragging");
|
||||
this.#workspace.workspaceElement.classList.remove("is-drag-target");
|
||||
this.#workspace.activePaletteDefinitionId = null;
|
||||
this.#workspace.dragManager.removePaletteGhost();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds shortcut node definitions for existing workspace variables.
|
||||
*
|
||||
* @param {string} [query] Optional search filter.
|
||||
* @returns {Array<PaletteNodeDefinition>}
|
||||
*/
|
||||
#buildVariableShortcutDefinitions(query) {
|
||||
const normalizedQuery = (query ?? "").trim().toLowerCase();
|
||||
const includeAll = normalizedQuery.length === 0;
|
||||
const entries = [];
|
||||
|
||||
this.#workspace.variableOrder.forEach((variableId) => {
|
||||
const variable = this.#workspace.variables.get(variableId);
|
||||
if (!variable) {
|
||||
return;
|
||||
}
|
||||
|
||||
const displayName = (variable.name ?? "").trim() || variable.id;
|
||||
const normalizedName = displayName.toLowerCase();
|
||||
const tokens = [
|
||||
normalizedName,
|
||||
variableId.toLowerCase(),
|
||||
`get ${normalizedName}`,
|
||||
`set ${normalizedName}`,
|
||||
];
|
||||
|
||||
if (
|
||||
!includeAll &&
|
||||
!tokens.some((token) => token.includes(normalizedQuery))
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
entries.push({
|
||||
id: `variable_get_${variableId}`,
|
||||
title: `Get ${displayName}`,
|
||||
category: "Variables",
|
||||
description: `Get ${displayName}`,
|
||||
shortcut: { type: "variable", action: "get", variableId },
|
||||
});
|
||||
|
||||
entries.push({
|
||||
id: `variable_set_${variableId}`,
|
||||
title: `Set ${displayName}`,
|
||||
category: "Variables",
|
||||
description: `Set ${displayName}`,
|
||||
shortcut: { type: "variable", action: "set", variableId },
|
||||
});
|
||||
});
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user