added runtime toolbox customization (show/hide)
parent
f2196f0c2a
commit
7d19311098
31
index.html
31
index.html
|
|
@ -40,6 +40,9 @@
|
||||||
<button id="btn-addons" title="Manage addons">
|
<button id="btn-addons" title="Manage addons">
|
||||||
<span class="icon">🔌</span> Addons
|
<span class="icon">🔌</span> Addons
|
||||||
</button>
|
</button>
|
||||||
|
<button id="btn-customize" title="Show/hide toolbox categories and blocks">
|
||||||
|
<span class="icon">⚙</span> Customize
|
||||||
|
</button>
|
||||||
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
<span id="connection-status" class="status-disconnected">Disconnected</span>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
@ -51,6 +54,34 @@
|
||||||
<div id="blockly-div"></div>
|
<div id="blockly-div"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Customize toolbox sidebar -->
|
||||||
|
<div id="customizer-panel" class="ide-panel hidden">
|
||||||
|
<div class="panel-header">
|
||||||
|
<span class="panel-title">Customize Toolbox</span>
|
||||||
|
<button id="customizer-done" class="cust-done-btn" title="Exit customize mode">Done</button>
|
||||||
|
</div>
|
||||||
|
<div class="panel-body">
|
||||||
|
<div class="cust-presets">
|
||||||
|
<div class="cust-preset-row">
|
||||||
|
<select id="preset-select" title="Saved presets">
|
||||||
|
<option value="">— select preset —</option>
|
||||||
|
</select>
|
||||||
|
<button id="preset-load" title="Load selected preset">Load</button>
|
||||||
|
<button id="preset-delete" class="btn-danger" title="Delete selected preset">Del</button>
|
||||||
|
</div>
|
||||||
|
<div class="cust-preset-row">
|
||||||
|
<input type="text" id="preset-name" placeholder="Preset name..." />
|
||||||
|
<button id="preset-save" title="Save current settings as preset">Save</button>
|
||||||
|
</div>
|
||||||
|
<div class="cust-preset-row">
|
||||||
|
<button id="preset-show-all" title="Show everything">Show All</button>
|
||||||
|
<button id="preset-hide-all" title="Hide everything">Hide All</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div id="customizer-tree" class="customizer-tree"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Projects right sidebar -->
|
<!-- Projects right sidebar -->
|
||||||
<div id="projects-panel" class="ide-panel">
|
<div id="projects-panel" class="ide-panel">
|
||||||
<div class="panel-header">
|
<div class="panel-header">
|
||||||
|
|
|
||||||
|
|
@ -76,8 +76,95 @@ export function clearAddonCategories() {
|
||||||
addonCategories.length = 0;
|
addonCategories.length = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildToolbox(deviceId) {
|
/**
|
||||||
|
* Returns the full unfiltered category list for a device (for the customizer UI).
|
||||||
|
*/
|
||||||
|
export function getFullCategories(deviceId) {
|
||||||
const profile = devices[deviceId || currentId];
|
const profile = devices[deviceId || currentId];
|
||||||
|
if (!profile) return [];
|
||||||
|
const contents = [...profile.categories];
|
||||||
|
if (addonCategories.length) {
|
||||||
|
contents.push(...addonCategories);
|
||||||
|
}
|
||||||
|
contents.push(...builtinCategories);
|
||||||
|
return contents.filter(c => c.kind === 'category');
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- toolbox filter (show/hide categories & blocks) ---
|
||||||
|
|
||||||
|
const FILTER_KEY = 'esp32block_toolbox_filter';
|
||||||
|
const PRESETS_KEY = 'esp32block_toolbox_presets';
|
||||||
|
|
||||||
|
let _customizeMode = false;
|
||||||
|
|
||||||
|
export function setCustomizeMode(on) { _customizeMode = on; }
|
||||||
|
export function isCustomizeMode() { return _customizeMode; }
|
||||||
|
|
||||||
|
export function getFilter(deviceId) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(localStorage.getItem(FILTER_KEY)) || {};
|
||||||
|
return data[deviceId || currentId] || {};
|
||||||
|
} catch { return {}; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function setFilter(deviceId, filter) {
|
||||||
|
let data;
|
||||||
|
try { data = JSON.parse(localStorage.getItem(FILTER_KEY)) || {}; }
|
||||||
|
catch { data = {}; }
|
||||||
|
data[deviceId || currentId] = filter;
|
||||||
|
localStorage.setItem(FILTER_KEY, JSON.stringify(data));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- presets ---
|
||||||
|
|
||||||
|
export function getSavedPresets() {
|
||||||
|
try { return JSON.parse(localStorage.getItem(PRESETS_KEY)) || []; }
|
||||||
|
catch { return []; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function savePreset(name, filter) {
|
||||||
|
const presets = getSavedPresets();
|
||||||
|
const idx = presets.findIndex(p => p.name === name);
|
||||||
|
if (idx >= 0) presets[idx] = { name, filter };
|
||||||
|
else presets.push({ name, filter });
|
||||||
|
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function deletePreset(name) {
|
||||||
|
const presets = getSavedPresets().filter(p => p.name !== name);
|
||||||
|
localStorage.setItem(PRESETS_KEY, JSON.stringify(presets));
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- filter application ---
|
||||||
|
|
||||||
|
function applyFilter(categories, deviceId) {
|
||||||
|
const filter = getFilter(deviceId);
|
||||||
|
const hidCats = new Set(filter.hiddenCategories || []);
|
||||||
|
const hidBlocks = new Set(filter.hiddenBlocks || []);
|
||||||
|
if (!hidCats.size && !hidBlocks.size) return categories;
|
||||||
|
|
||||||
|
const result = [];
|
||||||
|
for (const item of categories) {
|
||||||
|
if (item.kind === 'sep') { result.push(item); continue; }
|
||||||
|
if (item.kind === 'category' && hidCats.has(item.name)) continue;
|
||||||
|
if (item.kind === 'category' && item.contents) {
|
||||||
|
const filtered = item.contents.filter(b => !hidBlocks.has(b.type));
|
||||||
|
if (filtered.length === 0) continue;
|
||||||
|
result.push({ ...item, contents: filtered });
|
||||||
|
} else {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result.filter((item, i, arr) => {
|
||||||
|
if (item.kind !== 'sep') return true;
|
||||||
|
if (i === 0 || i === arr.length - 1) return false;
|
||||||
|
return arr[i - 1]?.kind !== 'sep';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildToolbox(deviceId) {
|
||||||
|
const id = deviceId || currentId;
|
||||||
|
const profile = devices[id];
|
||||||
if (!profile) return { kind: 'categoryToolbox', contents: [] };
|
if (!profile) return { kind: 'categoryToolbox', contents: [] };
|
||||||
|
|
||||||
const contents = [...profile.categories];
|
const contents = [...profile.categories];
|
||||||
|
|
@ -88,5 +175,6 @@ export function buildToolbox(deviceId) {
|
||||||
contents.push({ kind: 'sep' });
|
contents.push({ kind: 'sep' });
|
||||||
contents.push(...builtinCategories);
|
contents.push(...builtinCategories);
|
||||||
|
|
||||||
return { kind: 'categoryToolbox', contents };
|
if (_customizeMode) return { kind: 'categoryToolbox', contents };
|
||||||
|
return { kind: 'categoryToolbox', contents: applyFilter(contents, id) };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,7 @@ import { executeCode, stopExecution, saveToDevice, writeFileToDevice } from './s
|
||||||
import { flashFirmware } from './serial/flasher.js';
|
import { flashFirmware } from './serial/flasher.js';
|
||||||
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
import { appendToTerminal, clearTerminal } from './ui/terminal.js';
|
||||||
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
|
import { initResizablePanels, initPanelToggles, initProjectTabs, setProjectsPanelCallbacks } from './ui/panels.js';
|
||||||
|
import { initToolboxCustomizer, toggleCustomizeMode, refreshCustomizer } from './ui/toolboxCustomizer.js';
|
||||||
import { initProjectsDialog, refreshAll as refreshProjects, refreshDeviceList } from './ui/projectsDialog.js';
|
import { initProjectsDialog, refreshAll as refreshProjects, refreshDeviceList } from './ui/projectsDialog.js';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
||||||
|
|
@ -62,6 +63,10 @@ function rebuildDeviceSelect() {
|
||||||
|
|
||||||
setDeviceListRefreshCallback(rebuildDeviceSelect);
|
setDeviceListRefreshCallback(rebuildDeviceSelect);
|
||||||
|
|
||||||
|
// Toolbox customizer (show/hide categories & blocks)
|
||||||
|
initToolboxCustomizer(refreshToolbox);
|
||||||
|
document.getElementById('btn-customize').addEventListener('click', toggleCustomizeMode);
|
||||||
|
|
||||||
// ─── Live Code Preview ───────────────────────────────────
|
// ─── Live Code Preview ───────────────────────────────────
|
||||||
|
|
||||||
const codeOutput = document.getElementById('code-output');
|
const codeOutput = document.getElementById('code-output');
|
||||||
|
|
@ -167,8 +172,8 @@ deviceSelect.value = getDeviceId();
|
||||||
deviceSelect.addEventListener('change', () => {
|
deviceSelect.addEventListener('change', () => {
|
||||||
setDeviceId(deviceSelect.value);
|
setDeviceId(deviceSelect.value);
|
||||||
refreshToolbox();
|
refreshToolbox();
|
||||||
|
refreshCustomizer();
|
||||||
updateCodePreview();
|
updateCodePreview();
|
||||||
// Update Flash button tooltip/label based on device
|
|
||||||
btnFlash.title = canFlashInBrowser()
|
btnFlash.title = canFlashInBrowser()
|
||||||
? 'Flash MicroPython firmware'
|
? 'Flash MicroPython firmware'
|
||||||
: 'Download firmware (drag to device)';
|
: 'Download firmware (drag to device)';
|
||||||
|
|
|
||||||
185
src/style.css
185
src/style.css
|
|
@ -832,3 +832,188 @@ html, body {
|
||||||
color: var(--bg-toolbar);
|
color: var(--bg-toolbar);
|
||||||
border-color: var(--red);
|
border-color: var(--red);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* --- Toolbox Customizer Sidebar Panel --- */
|
||||||
|
#customizer-panel {
|
||||||
|
width: 280px;
|
||||||
|
min-width: 0;
|
||||||
|
border-left: 1px solid var(--border);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#customizer-panel.hidden {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
#customizer-panel .panel-body {
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-done-btn {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 2px 12px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.4px;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
.cust-done-btn:hover { opacity: 0.85; }
|
||||||
|
|
||||||
|
/* Preset controls */
|
||||||
|
.cust-presets {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 5px;
|
||||||
|
padding: 8px;
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-preset-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-preset-row select,
|
||||||
|
.cust-preset-row input {
|
||||||
|
flex: 1;
|
||||||
|
background: var(--bg-primary);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 4px 6px;
|
||||||
|
font-size: 11px;
|
||||||
|
outline: none;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-preset-row select:focus,
|
||||||
|
.cust-preset-row input:focus {
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-preset-row button {
|
||||||
|
background: var(--bg-surface);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
padding: 4px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
white-space: nowrap;
|
||||||
|
transition: background 0.15s, border-color 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-preset-row button:hover {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-preset-row button.btn-danger:hover {
|
||||||
|
background: var(--red);
|
||||||
|
border-color: var(--red);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Category/block tree */
|
||||||
|
.customizer-tree {
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 4px 6px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-cat {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 4px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
user-select: none;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-cat:hover { background: var(--bg-surface); }
|
||||||
|
|
||||||
|
.cust-swatch {
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border-radius: 3px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-cat-label {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
color: var(--text-primary);
|
||||||
|
flex: 1;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-expand {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: 11px;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 1px 4px;
|
||||||
|
border-radius: 3px;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
.cust-expand:hover {
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-cb {
|
||||||
|
accent-color: var(--accent);
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-blocks {
|
||||||
|
padding-left: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-block-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
transition: opacity 0.15s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-block-row:hover { background: var(--bg-surface); }
|
||||||
|
|
||||||
|
.cust-block-label {
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--text-secondary);
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cust-dimmed { opacity: 0.4; }
|
||||||
|
|
||||||
|
.cust-cb:disabled {
|
||||||
|
opacity: 0.3;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Active customize button highlight */
|
||||||
|
#btn-customize.active {
|
||||||
|
background: var(--accent);
|
||||||
|
color: var(--bg-toolbar);
|
||||||
|
border-color: var(--accent);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,259 @@
|
||||||
|
import {
|
||||||
|
getDeviceId,
|
||||||
|
getFullCategories,
|
||||||
|
getFilter,
|
||||||
|
setFilter,
|
||||||
|
setCustomizeMode,
|
||||||
|
isCustomizeMode,
|
||||||
|
getSavedPresets,
|
||||||
|
savePreset,
|
||||||
|
deletePreset,
|
||||||
|
} from '../devices/registry.js';
|
||||||
|
|
||||||
|
let refreshToolboxFn = () => {};
|
||||||
|
let panelEl, treeEl, presetSelect, presetNameInput, btnDone;
|
||||||
|
|
||||||
|
export function initToolboxCustomizer(refreshCb) {
|
||||||
|
refreshToolboxFn = refreshCb;
|
||||||
|
panelEl = document.getElementById('customizer-panel');
|
||||||
|
treeEl = document.getElementById('customizer-tree');
|
||||||
|
presetSelect = document.getElementById('preset-select');
|
||||||
|
presetNameInput = document.getElementById('preset-name');
|
||||||
|
btnDone = document.getElementById('customizer-done');
|
||||||
|
|
||||||
|
btnDone.addEventListener('click', exitCustomizeMode);
|
||||||
|
document.getElementById('preset-load').addEventListener('click', loadSelectedPreset);
|
||||||
|
document.getElementById('preset-save').addEventListener('click', saveCurrentAsPreset);
|
||||||
|
document.getElementById('preset-delete').addEventListener('click', deleteSelectedPreset);
|
||||||
|
document.getElementById('preset-show-all').addEventListener('click', showAll);
|
||||||
|
document.getElementById('preset-hide-all').addEventListener('click', hideAll);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toggleCustomizeMode() {
|
||||||
|
if (isCustomizeMode()) exitCustomizeMode();
|
||||||
|
else enterCustomizeMode();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function refreshCustomizer() {
|
||||||
|
if (isCustomizeMode()) {
|
||||||
|
renderTree();
|
||||||
|
renderPresetDropdown();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function enterCustomizeMode() {
|
||||||
|
setCustomizeMode(true);
|
||||||
|
refreshToolboxFn();
|
||||||
|
renderTree();
|
||||||
|
renderPresetDropdown();
|
||||||
|
panelEl.classList.remove('hidden');
|
||||||
|
document.getElementById('btn-customize').classList.add('active');
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}
|
||||||
|
|
||||||
|
function exitCustomizeMode() {
|
||||||
|
setCustomizeMode(false);
|
||||||
|
panelEl.classList.add('hidden');
|
||||||
|
document.getElementById('btn-customize').classList.remove('active');
|
||||||
|
refreshToolboxFn();
|
||||||
|
window.dispatchEvent(new Event('resize'));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Filter helpers ──────────────────────────────────────
|
||||||
|
|
||||||
|
function currentFilter() {
|
||||||
|
const f = getFilter(getDeviceId());
|
||||||
|
return {
|
||||||
|
hiddenCategories: new Set(f.hiddenCategories || []),
|
||||||
|
hiddenBlocks: new Set(f.hiddenBlocks || []),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveFilter(hiddenCategories, hiddenBlocks) {
|
||||||
|
setFilter(getDeviceId(), {
|
||||||
|
hiddenCategories: [...hiddenCategories],
|
||||||
|
hiddenBlocks: [...hiddenBlocks],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Preset actions ──────────────────────────────────────
|
||||||
|
|
||||||
|
function renderPresetDropdown() {
|
||||||
|
const presets = getSavedPresets();
|
||||||
|
presetSelect.innerHTML = '<option value="">— select preset —</option>';
|
||||||
|
for (const p of presets) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.name;
|
||||||
|
opt.textContent = p.name;
|
||||||
|
presetSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function loadSelectedPreset() {
|
||||||
|
const name = presetSelect.value;
|
||||||
|
if (!name) return;
|
||||||
|
const presets = getSavedPresets();
|
||||||
|
const preset = presets.find(p => p.name === name);
|
||||||
|
if (!preset) return;
|
||||||
|
setFilter(getDeviceId(), preset.filter);
|
||||||
|
renderTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrentAsPreset() {
|
||||||
|
const name = presetNameInput.value.trim();
|
||||||
|
if (!name) { presetNameInput.focus(); return; }
|
||||||
|
const f = getFilter(getDeviceId());
|
||||||
|
savePreset(name, f);
|
||||||
|
presetNameInput.value = '';
|
||||||
|
renderPresetDropdown();
|
||||||
|
presetSelect.value = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteSelectedPreset() {
|
||||||
|
const name = presetSelect.value;
|
||||||
|
if (!name) return;
|
||||||
|
deletePreset(name);
|
||||||
|
renderPresetDropdown();
|
||||||
|
}
|
||||||
|
|
||||||
|
function showAll() {
|
||||||
|
saveFilter(new Set(), new Set());
|
||||||
|
renderTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
function hideAll() {
|
||||||
|
const cats = getFullCategories(getDeviceId());
|
||||||
|
const hiddenCats = new Set();
|
||||||
|
const hiddenBlocks = new Set();
|
||||||
|
for (const cat of cats) {
|
||||||
|
hiddenCats.add(cat.name);
|
||||||
|
if (cat.contents) {
|
||||||
|
for (const b of cat.contents) {
|
||||||
|
if (b.type) hiddenBlocks.add(b.type);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
saveFilter(hiddenCats, hiddenBlocks);
|
||||||
|
renderTree();
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Tree rendering ──────────────────────────────────────
|
||||||
|
|
||||||
|
function renderTree() {
|
||||||
|
const cats = getFullCategories(getDeviceId());
|
||||||
|
const { hiddenCategories, hiddenBlocks } = currentFilter();
|
||||||
|
treeEl.innerHTML = '';
|
||||||
|
|
||||||
|
for (const cat of cats) {
|
||||||
|
const catHidden = hiddenCategories.has(cat.name);
|
||||||
|
|
||||||
|
// Category row
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'cust-cat' + (catHidden ? ' cust-dimmed' : '');
|
||||||
|
|
||||||
|
const catCb = document.createElement('input');
|
||||||
|
catCb.type = 'checkbox';
|
||||||
|
catCb.checked = !catHidden;
|
||||||
|
catCb.className = 'cust-cb';
|
||||||
|
|
||||||
|
const swatch = document.createElement('span');
|
||||||
|
swatch.className = 'cust-swatch';
|
||||||
|
if (cat.colour) {
|
||||||
|
swatch.style.background = `hsl(${cat.colour}, 60%, 50%)`;
|
||||||
|
} else {
|
||||||
|
swatch.style.background = 'var(--text-muted)';
|
||||||
|
}
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.className = 'cust-cat-label';
|
||||||
|
label.textContent = cat.name;
|
||||||
|
|
||||||
|
const hasBlocks = cat.contents && cat.contents.length > 0;
|
||||||
|
|
||||||
|
const toggle = document.createElement('button');
|
||||||
|
toggle.className = 'cust-expand';
|
||||||
|
toggle.innerHTML = '▸';
|
||||||
|
toggle.title = 'Expand blocks';
|
||||||
|
|
||||||
|
row.appendChild(catCb);
|
||||||
|
row.appendChild(swatch);
|
||||||
|
row.appendChild(label);
|
||||||
|
if (hasBlocks) row.appendChild(toggle);
|
||||||
|
treeEl.appendChild(row);
|
||||||
|
|
||||||
|
// Block list (collapsed by default)
|
||||||
|
const blockList = document.createElement('div');
|
||||||
|
blockList.className = 'cust-blocks hidden';
|
||||||
|
|
||||||
|
if (hasBlocks) {
|
||||||
|
for (const block of cat.contents) {
|
||||||
|
if (!block.type) continue;
|
||||||
|
|
||||||
|
const bRow = document.createElement('div');
|
||||||
|
const bHidden = catHidden || hiddenBlocks.has(block.type);
|
||||||
|
bRow.className = 'cust-block-row' + (bHidden ? ' cust-dimmed' : '');
|
||||||
|
|
||||||
|
const bCb = document.createElement('input');
|
||||||
|
bCb.type = 'checkbox';
|
||||||
|
bCb.checked = !catHidden && !hiddenBlocks.has(block.type);
|
||||||
|
bCb.disabled = catHidden;
|
||||||
|
bCb.className = 'cust-cb';
|
||||||
|
|
||||||
|
const bLabel = document.createElement('span');
|
||||||
|
bLabel.className = 'cust-block-label';
|
||||||
|
bLabel.textContent = block.type.replace(/_/g, ' ');
|
||||||
|
|
||||||
|
bRow.appendChild(bCb);
|
||||||
|
bRow.appendChild(bLabel);
|
||||||
|
blockList.appendChild(bRow);
|
||||||
|
|
||||||
|
bCb.addEventListener('change', () => {
|
||||||
|
const f = currentFilter();
|
||||||
|
if (bCb.checked) {
|
||||||
|
f.hiddenBlocks.delete(block.type);
|
||||||
|
} else {
|
||||||
|
f.hiddenBlocks.add(block.type);
|
||||||
|
}
|
||||||
|
saveFilter(f.hiddenCategories, f.hiddenBlocks);
|
||||||
|
bRow.classList.toggle('cust-dimmed', !bCb.checked);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
treeEl.appendChild(blockList);
|
||||||
|
|
||||||
|
// Expand toggle
|
||||||
|
if (hasBlocks) {
|
||||||
|
toggle.addEventListener('click', () => {
|
||||||
|
blockList.classList.toggle('hidden');
|
||||||
|
toggle.innerHTML = blockList.classList.contains('hidden') ? '▸' : '▾';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Category checkbox
|
||||||
|
catCb.addEventListener('change', () => {
|
||||||
|
const f = currentFilter();
|
||||||
|
if (catCb.checked) {
|
||||||
|
f.hiddenCategories.delete(cat.name);
|
||||||
|
} else {
|
||||||
|
f.hiddenCategories.add(cat.name);
|
||||||
|
}
|
||||||
|
saveFilter(f.hiddenCategories, f.hiddenBlocks);
|
||||||
|
row.classList.toggle('cust-dimmed', !catCb.checked);
|
||||||
|
|
||||||
|
const childCbs = blockList.querySelectorAll('.cust-cb');
|
||||||
|
childCbs.forEach(cb => {
|
||||||
|
cb.disabled = !catCb.checked;
|
||||||
|
if (!catCb.checked) {
|
||||||
|
cb.checked = false;
|
||||||
|
cb.closest('.cust-block-row')?.classList.add('cust-dimmed');
|
||||||
|
} else {
|
||||||
|
const type = cb.closest('.cust-block-row')
|
||||||
|
?.querySelector('.cust-block-label')?.textContent.replace(/ /g, '_');
|
||||||
|
const isHidden = f.hiddenBlocks.has(type);
|
||||||
|
cb.checked = !isHidden;
|
||||||
|
cb.closest('.cust-block-row')?.classList.toggle('cust-dimmed', isHidden);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue