Move menu building to Stimulus (WIP)
TODO: cache menu container, build ontology and load templates from external HTML source.
This commit is contained in:
78
js/controllers/menu_controller.js
Normal file
78
js/controllers/menu_controller.js
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
// Global ATON
|
||||||
|
import { Controller } from "@hotwired/stimulus"
|
||||||
|
import AppState from "../state.js";
|
||||||
|
|
||||||
|
const html = String.raw;
|
||||||
|
const domParser = new DOMParser;
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
static targets = ['trigger', 'layers', 'ontology'];
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
console.log('#menu controller connected');
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Open settings panel
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
toggleMenu(event) {
|
||||||
|
ATON.UI.setSidePanelRight();
|
||||||
|
ATON.UI.showSidePanel({header: 'Menu'});
|
||||||
|
this.#buildMenuPanel(ATON.UI.elSidePanel);
|
||||||
|
this.#buildLayersMenu(AppState.normalizedNodes, this.layersTarget);
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @param {Event} event
|
||||||
|
*/
|
||||||
|
toggleNode(event) {
|
||||||
|
/**
|
||||||
|
* The node's id
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
const id = event.params.node;
|
||||||
|
const status = event.target.checked;
|
||||||
|
ATON.getSceneNode(id).toggle(status);
|
||||||
|
AppState.normalizedNodes.find(n => n.id === id).active = status;
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* 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} nodes The normalized scene nodes (IDs and status)
|
||||||
|
* @param {HTMLElement} tab Tab content element
|
||||||
|
*/
|
||||||
|
#buildLayersMenu(nodes, tab) {
|
||||||
|
for(let node of nodes) {
|
||||||
|
const menuItem = html`
|
||||||
|
<div class="form-check form-switch ms-${node.depth} ps-${node.depth} mt-2">
|
||||||
|
<input class="form-check-input" type="checkbox" ${node.active ? 'checked' : ''} role="switch"
|
||||||
|
title="Mostra / nascondi layer"
|
||||||
|
data-menu-node-param="${node.id}"
|
||||||
|
data-action="change->menu#toggleNode">
|
||||||
|
<label class="form-check-label">${node.id}</label>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
// Awful?
|
||||||
|
tab.appendChild(
|
||||||
|
domParser.parseFromString(menuItem, 'text/html').querySelector('div')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
107
js/ui.js
107
js/ui.js
@@ -1,42 +1,9 @@
|
|||||||
import AppState from "./state.js";
|
import AppState from "./state.js";
|
||||||
import { changeLightDirection, toggleAmbientOcclusion } from "./utils/environment.js";
|
|
||||||
import { resetClipping, addClippingPlane } from "./utils/clipping.js";
|
|
||||||
import { traverseOntology } from "./ontology.js";
|
import { traverseOntology } from "./ontology.js";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @module UI
|
* @module UI
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const domParser = new DOMParser;
|
|
||||||
|
|
||||||
const contentMenuTabs = `
|
|
||||||
<!-- Nav tabs -->
|
|
||||||
<ul class="nav nav-pills" id="content-tabs" role="tablist">
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link active" id="layer-tab" data-bs-toggle="tab" data-bs-target="#layer" type="button" role="tab" aria-controls="layer" aria-selected="false">
|
|
||||||
<i class="bi bi-boxes me-1"></i> Elementi 3D
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item" role="presentation">
|
|
||||||
<button class="nav-link" id="media-tab" data-bs-toggle="tab" data-bs-target="#content" type="button" role="tab" aria-controls="media" aria-selected="true">
|
|
||||||
<i class="bi bi-diagram-3 me-1"></i> Contenuti
|
|
||||||
</button>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
|
|
||||||
<!-- Tab panes -->
|
|
||||||
<div class="tab-content ps-4 ms-3 overflow-y-auto">
|
|
||||||
<div class="tab-pane active p-3 ms-2" id="layer" role="tabpanel" aria-labelledby="layer-tab" tabindex="0"></div>
|
|
||||||
<div class="tab-pane pt-3" id="content" role="tabpanel" aria-labelledby="media-tab" tabindex="0"></div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
const audioExample = `
|
|
||||||
<button type="button" id="audio-example" class="text-left btn aton-btn fs-6 mx-2" data-bs-toggle="modal" data-bs-target="#audio1">
|
|
||||||
<i class="bi bi-play-btn me-2"></i> Esempio audio (<em>Che fiero costume</em>)
|
|
||||||
</button>
|
|
||||||
`;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
* @param {String} triggerSelector - Usually, the close modal trigger element(s) selector
|
* @param {String} triggerSelector - Usually, the close modal trigger element(s) selector
|
||||||
@@ -54,73 +21,6 @@ export function pauseAudio(triggerSelector) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
/**
|
|
||||||
* Resets the UI state (essentially hides the clipper toolbar if visible...)
|
|
||||||
* @todo Other elements to reset?? Restore inital lighting conditions and viewpoint...
|
|
||||||
*/
|
|
||||||
function reset() {
|
|
||||||
document.querySelector('#clipper-bar')?.classList.add('d-none');
|
|
||||||
document.querySelector('#clipper')?.classList.remove('border', 'border-2', 'border-white');
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Right-side main menu panel
|
|
||||||
* @param {String} triggerId - The menu button id
|
|
||||||
* @param {String} ontologyJsonPath
|
|
||||||
*/
|
|
||||||
function toggleContentMenu(triggerId, ontologyJsonPath) {
|
|
||||||
const btn = document.querySelector(`#${triggerId}`);
|
|
||||||
|
|
||||||
btn.addEventListener('click', async () => {
|
|
||||||
ATON.UI.setSidePanelRight();
|
|
||||||
ATON.UI.showSidePanel({header: 'Menu'});
|
|
||||||
// Append tabs, then tab panes
|
|
||||||
const tabs = domParser.parseFromString(contentMenuTabs, 'text/html');
|
|
||||||
ATON.UI.elSidePanel.appendChild(tabs.querySelector('#content-tabs'));
|
|
||||||
ATON.UI.elSidePanel.appendChild(tabs.querySelector('.tab-content'));
|
|
||||||
buildLayersMenu(AppState.normalizedNodes, ATON.UI.elSidePanel.querySelector('#layer'));
|
|
||||||
buildOntologyMenu(await traverseOntology(ontologyJsonPath), ATON.UI.elSidePanel.querySelector('#content'));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* @todo Don't rebuild it every time the side panel is shown...
|
|
||||||
* @param {Array} nodes The normalized scene nodes (IDs and status)
|
|
||||||
* @param {HTMLElement} sidePanel ATON's side panel element
|
|
||||||
*/
|
|
||||||
function buildLayersMenu(nodes, sidePanel) {
|
|
||||||
for(let node of nodes) {
|
|
||||||
const menuItem = document.createElement('div');
|
|
||||||
menuItem.className = `form-check form-switch ms-${node.depth} ps-${node.depth} mt-2`;
|
|
||||||
const checkbox = document.createElement('input');
|
|
||||||
checkbox.type = 'checkbox';
|
|
||||||
checkbox.className = "form-check-input";
|
|
||||||
checkbox.checked = node.active;
|
|
||||||
checkbox.role = 'switch';
|
|
||||||
checkbox.title = "Mostra / nascondi layer";
|
|
||||||
|
|
||||||
menuItem.appendChild(checkbox);
|
|
||||||
|
|
||||||
const label = document.createElement('label');
|
|
||||||
label.className = "form-check-label";
|
|
||||||
label.textContent = node.id;
|
|
||||||
|
|
||||||
menuItem.appendChild(label);
|
|
||||||
|
|
||||||
sidePanel.appendChild(menuItem);
|
|
||||||
// Will this ever work??
|
|
||||||
menuItem.addEventListener('change', event => toggleNode(node.id, event.target.checked));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This is terrible...
|
|
||||||
* @param {String} id
|
|
||||||
* @param {Boolean} status
|
|
||||||
*/
|
|
||||||
const toggleNode = (id, status) => {
|
|
||||||
ATON.getSceneNode(id).toggle(status);
|
|
||||||
AppState.normalizedNodes.find(n => n.id === id).active = status;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @see traverseOntology
|
* @see traverseOntology
|
||||||
* @param {Object} ontology The traversed ontology object (temp)
|
* @param {Object} ontology The traversed ontology object (temp)
|
||||||
@@ -146,11 +46,4 @@ function buildOntologyMenu(ontology, sidePanel) {
|
|||||||
mainNode.appendChild(domainList);
|
mainNode.appendChild(domainList);
|
||||||
list.appendChild(mainNode);
|
list.appendChild(mainNode);
|
||||||
sidePanel.appendChild(list);
|
sidePanel.appendChild(list);
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Initialize required components for scene UI
|
|
||||||
* @param {String} ontologyJsonPath
|
|
||||||
*/
|
|
||||||
export async function initUI(ontologyJsonPath) {
|
|
||||||
toggleContentMenu('menu', ontologyJsonPath);
|
|
||||||
}
|
}
|
||||||
@@ -2,6 +2,7 @@ import { Application } from '@hotwired/stimulus';
|
|||||||
import SettingController from '../controllers/settings_controller.js';
|
import SettingController from '../controllers/settings_controller.js';
|
||||||
import ToolbarController from '../controllers/toolbar_controller.js';
|
import ToolbarController from '../controllers/toolbar_controller.js';
|
||||||
import ClipperController from '../controllers/clipper_controller.js';
|
import ClipperController from '../controllers/clipper_controller.js';
|
||||||
|
import MenuController from '../controllers/menu_controller.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize Stimulus controllers
|
* Initialize Stimulus controllers
|
||||||
@@ -11,4 +12,5 @@ export function initStimulus() {
|
|||||||
Stimulus.register("settings", SettingController);
|
Stimulus.register("settings", SettingController);
|
||||||
Stimulus.register("toolbar", ToolbarController);
|
Stimulus.register("toolbar", ToolbarController);
|
||||||
Stimulus.register("clipper", ClipperController);
|
Stimulus.register("clipper", ClipperController);
|
||||||
|
Stimulus.register("menu", MenuController);
|
||||||
}
|
}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
<script type="module" src="./index.js"></script>
|
<script type="module" src="./index.js"></script>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body data-bs-theme="light">
|
<body data-bs-theme="light" data-controller="menu">
|
||||||
<div id="toolbar" class="aton-toolbar-top w-100"
|
<div id="toolbar" class="aton-toolbar-top w-100"
|
||||||
data-controller="toolbar clipper" data-clipper-enabled-value="false">
|
data-controller="toolbar clipper" data-clipper-enabled-value="false">
|
||||||
<a class="btn aton-btn fs-5" href="/a/scaenae" id="back" title="Torna alla mappa">
|
<a class="btn aton-btn fs-5" href="/a/scaenae" id="back" title="Torna alla mappa">
|
||||||
@@ -89,7 +89,8 @@
|
|||||||
<button class="btn aton-btn d-inline px-4 py-4" id="clipZ"
|
<button class="btn aton-btn d-inline px-4 py-4" id="clipZ"
|
||||||
data-clipper-target="axis" data-clipper-axis-param="z" data-action="clipper#clip" title="Sezione Z"></button>
|
data-clipper-target="axis" data-clipper-axis-param="z" data-action="clipper#clip" title="Sezione Z"></button>
|
||||||
</div>
|
</div>
|
||||||
<a class="btn aton-btn fs-5 float-end" id="menu" title="Menu">
|
<a class="btn aton-btn fs-5 float-end" id="menu" title="Menu"
|
||||||
|
data-menu-target="trigger" data-action="menu#toggleMenu">
|
||||||
<i class="bi bi-list"></i>
|
<i class="bi bi-list"></i>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -118,8 +119,24 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<template id="tmpl-menu-tabs">
|
<template id="tmpl-menu-tabs">
|
||||||
<ul class="nav nav-pills" id="content-tabs">...</ul>
|
<!-- Nav tabs -->
|
||||||
<div class="tab-content ...">...</div>
|
<ul class="nav nav-pills" id="content-tabs" role="tablist">
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link active" id="layer-tab" data-bs-toggle="tab" data-bs-target="#layer" type="button" role="tab" aria-controls="layer" aria-selected="false">
|
||||||
|
<i class="bi bi-boxes me-1"></i> Elementi 3D
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item" role="presentation">
|
||||||
|
<button class="nav-link" id="media-tab" data-bs-toggle="tab" data-bs-target="#content" type="button" role="tab" aria-controls="media" aria-selected="true">
|
||||||
|
<i class="bi bi-diagram-3 me-1"></i> Contenuti
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
<!-- Tab panes -->
|
||||||
|
<div class="tab-content ps-4 ms-3 overflow-y-auto">
|
||||||
|
<div class="tab-pane active p-3 ms-2" data-menu-target="layers" id="layer" role="tabpanel" aria-labelledby="layer-tab" tabindex="0"></div>
|
||||||
|
<div class="tab-pane pt-3" data-menu-target="ontology" id="content" role="tabpanel" aria-labelledby="media-tab" tabindex="0"></div>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<!-- TODO CSS-only popover -->
|
<!-- TODO CSS-only popover -->
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import { openScene } from "../../js/scene.js";
|
|||||||
import { config } from "../../config.js";
|
import { config } from "../../config.js";
|
||||||
import AppState from "../../js/state.js";
|
import AppState from "../../js/state.js";
|
||||||
import { normalizeNodes } from "../../js/utils/nodeUtils.js";
|
import { normalizeNodes } from "../../js/utils/nodeUtils.js";
|
||||||
import { initUI } from "../../js/ui.js";
|
|
||||||
import { initStimulus } from "../../js/utils/stimulus.js";
|
import { initStimulus } from "../../js/utils/stimulus.js";
|
||||||
|
|
||||||
initStimulus();
|
initStimulus();
|
||||||
@@ -12,4 +11,3 @@ const marker = config.markers.find(m => m.id === 'ssgp');
|
|||||||
AppState.normalizedNodes = normalizeNodes(marker.nodes);
|
AppState.normalizedNodes = normalizeNodes(marker.nodes);
|
||||||
|
|
||||||
openScene(marker, AppState.normalizedNodes);
|
openScene(marker, AppState.normalizedNodes);
|
||||||
initUI(location.pathname + '/ontology.json');
|
|
||||||
|
|||||||
Reference in New Issue
Block a user