Files
scaenae/js/controllers/menu_controller.js

203 lines
6.7 KiB
JavaScript

// Global ATON
import { Controller } from "@hotwired/stimulus";
import AppState from "../state.js";
import { traverseOntology } from "../ontology.js";
const html = String.raw;
const domParser = new DOMParser;
// TODO: hard-coded, but follows a convention...
const ontologyJsonPath = location.pathname + 'ontology.json';
export default class extends Controller {
static targets = ['trigger', 'layers', 'ontology'];
connect() {
console.log('#menu controller connected');
}
/**
* Open settings panel
*/
async toggleMenu() {
ATON.UI.setSidePanelRight();
ATON.UI.showSidePanel({header: 'Menu'});
this.#buildMenuPanel(ATON.UI.elSidePanel);
this.#buildLayersMenu(AppState.treeNodes, this.layersTarget);
this.#buildOntologyMenu(await traverseOntology(ontologyJsonPath), this.ontologyTarget);
}
/**
* @param {Event} event
*/
toggleNode(event) {
/**
* The node's id
* @type {string}
*/
const id = event?.params.node;
const status = event?.target?.checked;
const node = AppState.normalizedNodes.find(n => n.id === id);
if (node.children.length > 0) {
this.#toggleGroup(node, status);
this.#syncGroupCheckboxes(node, status, this.layersTarget);
} else {
ATON.getSceneNode(id).toggle(status);
node.active = status;
}
}
/**
* Recursively toggle children in a nodes group
* @param {Object} groupNode
* @param {Boolean} status
*/
#toggleGroup(groupNode, status) {
for (const child of groupNode.children) {
if (child.model) {
ATON.getSceneNode(child.id).toggle(status);
child.active = status;
}
if (child.children.length > 0) {
this.#toggleGroup(child, status);
}
}
}
#syncGroupCheckboxes(groupNode, status, container) {
for (const child of groupNode.children) {
const checkbox = container.querySelector(
`[data-menu-node-param="${child.id}"]`
);
if (checkbox) checkbox.checked = status;
if (child.children.length > 0) {
this.#syncGroupCheckboxes(child, status, container);
}
}
}
/**
* Clone a <template> by id
* @param {String} id
* @returns {DocumentFragment}
*/
#cloneTemplate(id) {
return document.getElementById(id)?.content?.cloneNode(true);
}
/**
* Create the left-side settings panel
* content
* @param {Element} panel
*/
#buildMenuPanel(panel) {
const fragment = this.#cloneTemplate('tmpl-menu-tabs');
panel.appendChild(fragment);
}
/**
* @todo Don't rebuild it every time, use caching, return a container with checkboxes
* @param {Array<import("../state.js").NormalizedSceneNode>} tree The normalized scene nodes tree
* @param {HTMLElement} tab Tab content element
*/
#buildLayersMenu(tree, tab) {
const heading = document.createElement('h1');
heading.classList.add('fs-5', 'fw-bold', 'ms-0');
heading.textContent = 'Teatro'; // Hard-coded!!
tab.appendChild(heading);
for (const node of tree) {
tab.appendChild(
node.children.length > 0
? this.#createLayerGroup(node)
: this.#createLayerToggle(node)
);
}
}
/**
* @param {import("../state.js").NormalizedSceneNode} node
* @returns {HTMLDivElement}
*/
#createLayerToggle(node) {
const toggle = html`
<label class="toggle-control ms-${node.depth} ps-${node.depth} mt-1 mb-1">
<input id="${node.id}" type="checkbox" ${node.active ? 'checked' : ''} role="switch"
data-menu-node-param="${node.id}"
data-action="change->menu#toggleNode">
<span class="control"></span>
<span class="ms-4 ps-1 fs-6" title="Mostra / nascondi layer">${node.id}</span>
</label>
`;
return domParser.parseFromString(toggle, 'text/html').querySelector('label');
}
/**
* @param {import("../state.js").NormalizedSceneNode} node
* @returns {HTMLDetailsElement}
*/
#createLayerGroup(node) {
const { trigger, collapseDiv } = this.#createNodeCollapse(node);
const wrapper = document.createElement('div');
collapseDiv.appendChild(this.#createLayerToggle(node));
for (const child of node.children) {
collapseDiv.appendChild(
child.children.length > 0
? this.#createLayerGroup(child)
: this.#createLayerToggle(child)
);
}
wrapper.appendChild(trigger);
wrapper.appendChild(collapseDiv);
wrapper.classList.add('m-0', 'p-0');
return wrapper;
}
/**
*
* @param {import("../state.js").NormalizedSceneNode} node
* @returns {{trigger: HTMLButtonElement, collapseDiv: HTMLDivElement}}
*/
#createNodeCollapse(node) {
const cleanId = node.id.replace(/[\s\/\-]+/g, '');
const trigger = document.createElement('button');
trigger.className = 'btn btn-link p-0 fs-6 fw-bold text-decoration-none text-reset';
trigger.setAttribute('data-bs-toggle', 'collapse');
trigger.setAttribute('data-bs-target', `#group-${cleanId}`);
trigger.innerHTML = `<i class="bi bi-chevron-down me-1"></i>${node.id}`;
const collapseDiv = document.createElement('div');
collapseDiv.className = 'collapse';
collapseDiv.id = `group-${cleanId}`;
return {trigger, collapseDiv};
}
/**
* Temporary implementation to show domains only
* @todo Don't rebuild it every time, use caching, return a container
* @param {Object} ontology The traversed ontology object (temp)
* @param {HTMLElement} tab Tab content element
*/
#buildOntologyMenu(ontology, tab) {
console.debug(ontology);
const mainNode = tab.querySelector('#ontology-list');
mainNode.textContent = ontology.ontology;
let domainList = html`
<ul class="list-group mt-2" id="domains-list"></ul>
`;
// Very fragile and ugly!!
mainNode.innerHTML += domainList;
domainList = tab.querySelector('#domains-list');
for(let domain of ontology.domains) {
const domainItem = html`
<li class="list-group-item">${domain.label}</li>
`;
domainList.innerHTML += domainItem;
}
}
}