mirror of
https://github.com/sotam0316/drawNET.git
synced 2026-04-25 03:58:37 +09:00
Initial commit: drawNET Alpha v1.0 - Professional Topology Designer with Full i18n and Performance Optimizations
This commit is contained in:
@@ -0,0 +1,17 @@
|
||||
import { state } from '../state.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
* fetchAssets - Fetches the complete asset and pack list from the server
|
||||
*/
|
||||
export async function fetchAssets() {
|
||||
try {
|
||||
const response = await fetch('/assets');
|
||||
const data = await response.json();
|
||||
state.assetsData = data.assets || [];
|
||||
return data;
|
||||
} catch (e) {
|
||||
logger.critical("Failed to fetch assets:", e);
|
||||
return { assets: [], packs: [] };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
import { state } from '../state.js';
|
||||
import { t } from '../i18n.js';
|
||||
|
||||
export const FIXED_ASSETS = [
|
||||
{ id: 'fixed-group', type: 'group', label: 'Group Container', icon: 'group.png', is_img: true },
|
||||
{ id: 'fixed-rect', type: 'rect', label: 'Rectangle', icon: 'far fa-square', is_img: false },
|
||||
{ id: 'fixed-rounded-rect', type: 'rounded-rect', label: 'Rounded Rectangle', icon: 'fas fa-vector-square', is_img: false },
|
||||
{ id: 'fixed-circle', type: 'circle', label: 'Circle', icon: 'far fa-circle', is_img: false },
|
||||
{ id: 'fixed-text', type: 'text-box', label: 'Text Box', icon: 'fas fa-font', is_img: false },
|
||||
{ id: 'fixed-label', type: 'label', label: 'Label', icon: 'fas fa-tag', is_img: false },
|
||||
{ id: 'fixed-blank', type: 'blank', label: 'Blank Point (Invisible)', icon: 'fas fa-crosshairs', is_img: false, extra_style: 'opacity: 0.5;' },
|
||||
{ id: 'fixed-dot', type: 'dot', label: 'Dot (Small visible point)', icon: 'fas fa-circle', is_img: false, extra_style: 'font-size: 6px;' },
|
||||
{ id: 'fixed-ellipse', type: 'ellipse', label: 'Ellipse', icon: 'fas fa-egg', is_img: false, extra_style: 'transform: rotate(-45deg);' },
|
||||
{ id: 'fixed-polyline', type: 'polyline', label: 'Polyline', icon: 'fas fa-chart-line', is_img: false },
|
||||
{ id: 'fixed-browser', type: 'browser', label: 'Browser', icon: 'fas fa-window-maximize', is_img: false },
|
||||
{ id: 'fixed-user', type: 'user', label: 'User', icon: 'fas fa-user', is_img: false },
|
||||
{ id: 'fixed-admin', type: 'admin', label: 'Admin', icon: 'fas fa-user-shield', is_img: false },
|
||||
{ id: 'fixed-triangle', type: 'triangle', label: 'Triangle', icon: 'fas fa-caret-up', is_img: false, extra_style: 'font-size: 22px; margin-top: -2px;' },
|
||||
{ id: 'fixed-diamond', type: 'diamond', label: 'Diamond', icon: 'fas fa-square', is_img: false, extra_style: 'transform: rotate(45deg) scale(0.8);' },
|
||||
{ id: 'fixed-parallelogram', type: 'parallelogram', label: 'Parallelogram', icon: 'fas fa-square', is_img: false, extra_style: 'transform: skewX(-20deg) scaleX(1.2);' },
|
||||
{ id: 'fixed-cylinder', type: 'cylinder', label: 'Cylinder', icon: 'fas fa-database', is_img: false },
|
||||
{ id: 'fixed-document', type: 'document', label: 'Document', icon: 'fas fa-file-alt', is_img: false },
|
||||
{ id: 'fixed-manual-input', type: 'manual-input', label: 'Manual Input', icon: 'fas fa-keyboard', is_img: false },
|
||||
{ id: 'fixed-table', type: 'table', label: 'Table', icon: 'fas fa-table', is_img: false },
|
||||
{ id: 'fixed-rack', type: 'rack', label: 'Rack', icon: 'fas fa-columns', is_img: false },
|
||||
{ id: 'fixed-hourglass', type: 'hourglass', label: 'Hourglass', icon: 'fas fa-hourglass-half', is_img: false },
|
||||
{ id: 'fixed-arrow-up', type: 'arrow-up', label: 'Arrow Up', icon: 'fas fa-arrow-up', is_img: false },
|
||||
{ id: 'fixed-arrow-down', type: 'arrow-down', label: 'Arrow Down', icon: 'fas fa-arrow-down', is_img: false },
|
||||
{ id: 'fixed-arrow-left', type: 'arrow-left', label: 'Arrow Left', icon: 'fas fa-arrow-left', is_img: false },
|
||||
{ id: 'fixed-arrow-right', type: 'arrow-right', label: 'Arrow Right', icon: 'fas fa-arrow-right', is_img: false },
|
||||
{ id: 'fixed-rich-card', type: 'rich-card', label: t('rich_card') || 'Rich Text Card', icon: 'fas fa-address-card', is_img: false }
|
||||
];
|
||||
|
||||
export function renderFixedSection(container) {
|
||||
const fixedSection = document.createElement('div');
|
||||
fixedSection.className = 'asset-category fixed-category';
|
||||
|
||||
const title = t('fixed_objects') || 'Fixed Objects';
|
||||
|
||||
fixedSection.innerHTML = `
|
||||
<div class="category-header">
|
||||
<span data-i18n="fixed_objects">${title}</span>
|
||||
<i class="fas fa-chevron-down"></i>
|
||||
</div>
|
||||
<div class="category-collapsed-icon" title="${title}">
|
||||
<i class="fas fa-layer-group" style="font-size: 20px; color: #64748b;"></i>
|
||||
</div>
|
||||
<div class="category-content">
|
||||
<div class="asset-grid"></div>
|
||||
</div>
|
||||
<div class="category-divider" style="height: 1px; background: rgba(255,255,255,0.1); margin: 15px 10px;"></div>
|
||||
`;
|
||||
|
||||
// Toggle Logic
|
||||
const header = fixedSection.querySelector('.category-header');
|
||||
header.addEventListener('click', () => {
|
||||
if (!document.getElementById('sidebar').classList.contains('collapsed')) {
|
||||
fixedSection.classList.toggle('active');
|
||||
}
|
||||
});
|
||||
|
||||
// Handle Sidebar Collapsed State (Flyout)
|
||||
const collapsedIcon = fixedSection.querySelector('.category-collapsed-icon');
|
||||
collapsedIcon.addEventListener('click', (e) => {
|
||||
if (document.getElementById('sidebar').classList.contains('collapsed')) {
|
||||
e.stopPropagation();
|
||||
// Since we don't want to import renderFlyout here to avoid circular dependencies
|
||||
// we'll dispatch a custom event or check if it's available globally (though not recommended)
|
||||
// or just let assets.js handle it if we restructure.
|
||||
// For now, let's trigger a click on a hidden flyout handler or use state.
|
||||
const event = new CustomEvent('render-fixed-flyout', {
|
||||
detail: {
|
||||
container: fixedSection,
|
||||
assets: FIXED_ASSETS,
|
||||
meta: { id: 'fixed_objects', label: title }
|
||||
}
|
||||
});
|
||||
document.dispatchEvent(event);
|
||||
}
|
||||
});
|
||||
|
||||
const grid = fixedSection.querySelector('.asset-grid');
|
||||
FIXED_ASSETS.forEach(asset => {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'asset-item';
|
||||
item.draggable = true;
|
||||
item.dataset.type = asset.type;
|
||||
item.dataset.assetId = asset.id;
|
||||
item.title = asset.label;
|
||||
|
||||
if (asset.is_img) {
|
||||
item.innerHTML = `<img src="/static/assets/${asset.icon}" width="22" height="22" loading="lazy">`;
|
||||
} else {
|
||||
item.innerHTML = `<i class="${asset.icon}" style="font-size: 18px; color: #64748b; ${asset.extra_style || ''}"></i>`;
|
||||
}
|
||||
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('assetId', asset.id);
|
||||
state.assetMap[asset.id] = {
|
||||
id: asset.id,
|
||||
type: asset.type,
|
||||
path: asset.is_img ? asset.icon : '',
|
||||
label: asset.label
|
||||
};
|
||||
});
|
||||
grid.appendChild(item);
|
||||
});
|
||||
|
||||
container.appendChild(fixedSection);
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { state } from '../state.js';
|
||||
import { t } from '../i18n.js';
|
||||
import { fetchAssets } from './api.js';
|
||||
import { renderLibrary, createAssetElement, renderFlyout } from './renderer.js';
|
||||
import { renderPackSelector } from './selector.js';
|
||||
import { initSearch } from './search.js';
|
||||
import { renderFixedSection } from './fixed_objects.js';
|
||||
import { logger } from '../utils/logger.js';
|
||||
|
||||
/**
|
||||
* initAssets - Main initialization for the asset library sidebar
|
||||
*/
|
||||
export async function initAssets() {
|
||||
try {
|
||||
const data = await fetchAssets();
|
||||
const library = document.querySelector('.asset-library');
|
||||
if (!library) return;
|
||||
|
||||
// 1. Initial Header & Global UI
|
||||
library.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; margin: 24px 16px 12px; border-bottom: 1px solid rgba(255,255,255,0.05); padding-bottom: 8px;">
|
||||
<h3 style="margin:0; font-size: 10px; text-transform: uppercase; letter-spacing: 1.5px; color: var(--sub-text); font-weight: 800;">
|
||||
${t('asset_library') || 'Asset Library'}
|
||||
</h3>
|
||||
<button id="manage-packs-btn" style="background:none; border:none; color: var(--sub-text); cursor:pointer; font-size: 12px; transition: color 0.2s;" title="Manage Packages">
|
||||
<i class="fas fa-filter"></i>
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
// 2. Setup Manage Packs Button
|
||||
document.getElementById('manage-packs-btn').onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
renderPackSelector(data.packs || []);
|
||||
};
|
||||
|
||||
// 3. Initialize selection if empty
|
||||
if (state.selectedPackIds.length === 0 && data.packs && data.packs.length > 0) {
|
||||
state.selectedPackIds = data.packs.map(p => p.id);
|
||||
localStorage.setItem('selectedPackIds', JSON.stringify(state.selectedPackIds));
|
||||
}
|
||||
|
||||
// 4. Render Sections
|
||||
renderFixedSection(library);
|
||||
renderLibrary({ ...data, state }, library, renderPackSelector);
|
||||
|
||||
// 5. Handle Flyouts for Fixed/Custom Sections via Event
|
||||
document.addEventListener('render-fixed-flyout', (e) => {
|
||||
const { container, assets, meta } = e.detail;
|
||||
renderFlyout(container, assets, meta);
|
||||
});
|
||||
|
||||
initSearch();
|
||||
|
||||
} catch (e) {
|
||||
logger.critical("Asset Initialization Error:", e);
|
||||
}
|
||||
}
|
||||
|
||||
export { createAssetElement, renderFlyout, renderPackSelector };
|
||||
@@ -0,0 +1,197 @@
|
||||
import { t } from '../i18n.js';
|
||||
import { DEFAULTS } from '../constants.js';
|
||||
|
||||
/**
|
||||
* createAssetElement - Creates a single asset item DOM element
|
||||
* @param {Object} asset
|
||||
* @returns {HTMLElement}
|
||||
*/
|
||||
export function createAssetElement(asset, compositeId = null) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'asset-item';
|
||||
item.draggable = true;
|
||||
item.dataset.type = asset.type || asset.id;
|
||||
item.dataset.assetId = compositeId || asset.id;
|
||||
item.title = asset.label;
|
||||
|
||||
// Support legacy 'path', new 'views.icon' or FontAwesome icon class
|
||||
const isFontAwesome = asset.is_img === false || (!asset.path && !asset.views && asset.icon && asset.icon.includes('fa-'));
|
||||
|
||||
if (isFontAwesome) {
|
||||
item.innerHTML = `
|
||||
<i class="${asset.icon}" style="font-size: 18px; color: #64748b; ${asset.extra_style || ''}"></i>
|
||||
<span>${t(asset.id) || asset.label || asset.id}</span>
|
||||
`;
|
||||
} else {
|
||||
const iconPath = (asset.views && asset.views.icon) ? asset.views.icon : (asset.path || DEFAULTS.DEFAULT_ICON);
|
||||
item.innerHTML = `
|
||||
<img src="/static/assets/${iconPath}" width="24" height="24" loading="lazy">
|
||||
<span>${t(asset.id) || asset.label || asset.id}</span>
|
||||
`;
|
||||
}
|
||||
item.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('assetId', item.dataset.assetId);
|
||||
});
|
||||
return item;
|
||||
}
|
||||
|
||||
/**
|
||||
* renderFlyout - Renders the floating category window when sidebar is collapsed
|
||||
* @param {HTMLElement} container
|
||||
* @param {Array} assets
|
||||
* @param {Object} meta
|
||||
*/
|
||||
export function renderFlyout(container, assets, meta) {
|
||||
document.querySelectorAll('.category-flyout').forEach(f => f.remove());
|
||||
|
||||
const flyout = document.createElement('div');
|
||||
flyout.className = 'category-flyout glass-panel active';
|
||||
|
||||
const rect = container.getBoundingClientRect();
|
||||
flyout.style.top = `${Math.max(10, Math.min(window.innerHeight - 400, rect.top))}px`;
|
||||
flyout.style.left = '90px';
|
||||
|
||||
flyout.innerHTML = `
|
||||
<div class="flyout-header">
|
||||
<strong>${t(meta.id) || meta.name || meta.label}</strong>
|
||||
<small>${assets.length} items</small>
|
||||
</div>
|
||||
<div class="flyout-content" style="max-height: 400px; overflow-y: auto;"></div>
|
||||
`;
|
||||
const content = flyout.querySelector('.flyout-content');
|
||||
|
||||
// Group by category for flyout as well
|
||||
const groups = assets.reduce((acc, a) => {
|
||||
const cat = a.category || 'Other';
|
||||
if (!acc[cat]) acc[cat] = [];
|
||||
acc[cat].push(a);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
Object.keys(groups).sort().forEach(catName => {
|
||||
if (Object.keys(groups).length > 1 || catName !== 'Other') {
|
||||
const label = document.createElement('div');
|
||||
label.className = 'asset-group-label';
|
||||
label.textContent = catName;
|
||||
content.appendChild(label);
|
||||
}
|
||||
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'asset-grid flyout-grid';
|
||||
groups[catName].forEach(asset => grid.appendChild(createAssetElement(asset)));
|
||||
content.appendChild(grid);
|
||||
});
|
||||
|
||||
document.body.appendChild(flyout);
|
||||
|
||||
const closeFlyout = (e) => {
|
||||
if (!flyout.contains(e.target)) {
|
||||
flyout.remove();
|
||||
document.removeEventListener('click', closeFlyout);
|
||||
}
|
||||
};
|
||||
setTimeout(() => document.addEventListener('click', closeFlyout), 10);
|
||||
}
|
||||
|
||||
/**
|
||||
* renderLibrary - The main loop that renders the entire asset library
|
||||
*/
|
||||
export function renderLibrary(data, library, renderPackSelector) {
|
||||
const { state } = data; // We expect state to be passed or accessible
|
||||
const packs = data.packs || [];
|
||||
const filteredPacks = packs.filter(p => state.selectedPackIds.includes(p.id));
|
||||
|
||||
// 1. Clear Library (except fixed categories and header)
|
||||
const dynamicPacks = library.querySelectorAll('.asset-category:not(.fixed-category)');
|
||||
dynamicPacks.forEach(p => p.remove());
|
||||
|
||||
// 2. Group Custom Assets by Pack -> Category
|
||||
const packGroups = state.assetsData.reduce((acc, asset) => {
|
||||
const packId = asset.pack_id || 'legacy';
|
||||
if (packId !== 'legacy' && !state.selectedPackIds.includes(packId)) return acc;
|
||||
|
||||
const category = asset.category || 'Other';
|
||||
if (!acc[packId]) acc[packId] = {};
|
||||
if (!acc[packId][category]) acc[packId][category] = [];
|
||||
|
||||
acc[packId][category].push(asset);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// 3. Render each visible Pack
|
||||
Object.keys(packGroups).forEach((packId, index) => {
|
||||
const packMeta = packs.find(p => p.id === packId) || { id: packId, name: (packId === 'legacy' ? t('other_assets') : packId) };
|
||||
const categories = packGroups[packId];
|
||||
|
||||
const allAssetsInPack = Object.values(categories).flat();
|
||||
|
||||
// Ensure proper path is set for pack-based assets
|
||||
if (packId !== 'legacy') {
|
||||
allAssetsInPack.forEach(asset => {
|
||||
if (asset.views && asset.views.icon) {
|
||||
const icon = asset.views.icon;
|
||||
// Only prepend packs/ if not already present
|
||||
if (!icon.startsWith('packs/')) {
|
||||
asset.path = `packs/${packId}/${icon}`;
|
||||
} else {
|
||||
asset.path = icon;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const firstAsset = allAssetsInPack[0] || {};
|
||||
const iconPath = (firstAsset.views && firstAsset.views.icon) ? firstAsset.views.icon : (firstAsset.path || DEFAULTS.DEFAULT_ICON);
|
||||
|
||||
const catContainer = document.createElement('div');
|
||||
catContainer.className = `asset-category shadow-sm`;
|
||||
|
||||
const header = document.createElement('div');
|
||||
header.className = 'category-header';
|
||||
header.innerHTML = `<span>${packMeta.name || packMeta.id}</span><i class="fas fa-chevron-down"></i>`;
|
||||
header.onclick = () => {
|
||||
if (!document.getElementById('sidebar').classList.contains('collapsed')) {
|
||||
catContainer.classList.toggle('active');
|
||||
}
|
||||
};
|
||||
|
||||
const collapsedIcon = document.createElement('div');
|
||||
collapsedIcon.className = 'category-collapsed-icon';
|
||||
collapsedIcon.title = packMeta.name || packMeta.id;
|
||||
collapsedIcon.innerHTML = `<img src="/static/assets/${iconPath}" width="28" height="28">`;
|
||||
collapsedIcon.onclick = (e) => {
|
||||
if (document.getElementById('sidebar').classList.contains('collapsed')) {
|
||||
e.stopPropagation();
|
||||
renderFlyout(catContainer, allAssetsInPack, packMeta);
|
||||
}
|
||||
};
|
||||
|
||||
const content = document.createElement('div');
|
||||
content.className = 'category-content';
|
||||
|
||||
// Render each Category within the Pack
|
||||
Object.keys(categories).sort().forEach(catName => {
|
||||
const assets = categories[catName];
|
||||
|
||||
if (Object.keys(categories).length > 1 || catName !== 'Other') {
|
||||
const subHeader = document.createElement('div');
|
||||
subHeader.className = 'asset-group-label';
|
||||
subHeader.textContent = catName;
|
||||
content.appendChild(subHeader);
|
||||
}
|
||||
|
||||
const itemGrid = document.createElement('div');
|
||||
itemGrid.className = 'asset-grid';
|
||||
assets.forEach(asset => {
|
||||
const compositeId = `${packId}|${asset.id}`;
|
||||
state.assetMap[compositeId] = asset;
|
||||
itemGrid.appendChild(createAssetElement(asset, compositeId));
|
||||
});
|
||||
content.appendChild(itemGrid);
|
||||
});
|
||||
|
||||
catContainer.append(header, collapsedIcon, content);
|
||||
library.appendChild(catContainer);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
import { state } from '../state.js';
|
||||
|
||||
/**
|
||||
* initSearch - Initializes the asset search input listener
|
||||
*/
|
||||
export function initSearch() {
|
||||
const searchInput = document.getElementById('asset-search');
|
||||
if (!searchInput) return;
|
||||
|
||||
searchInput.addEventListener('input', (e) => {
|
||||
const query = e.target.value.toLowerCase().trim();
|
||||
const categories = document.querySelectorAll('.asset-category');
|
||||
|
||||
categories.forEach(cat => {
|
||||
const items = cat.querySelectorAll('.asset-item');
|
||||
let hasVisibleItem = false;
|
||||
|
||||
items.forEach(item => {
|
||||
const assetId = item.dataset.assetId;
|
||||
const asset = state.assetMap[assetId];
|
||||
if (!asset) return;
|
||||
|
||||
const matches = (asset.type && asset.type.toLowerCase().includes(query)) ||
|
||||
(asset.label && asset.label.toLowerCase().includes(query)) ||
|
||||
(asset.vendor && asset.vendor.toLowerCase().includes(query));
|
||||
|
||||
item.style.display = matches ? 'flex' : 'none';
|
||||
if (matches) hasVisibleItem = true;
|
||||
});
|
||||
|
||||
cat.style.display = (hasVisibleItem || query === '') ? 'block' : 'none';
|
||||
if (query !== '' && hasVisibleItem) cat.classList.add('active');
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { state } from '../state.js';
|
||||
import { t } from '../i18n.js';
|
||||
|
||||
/**
|
||||
* renderPackSelector - Show a modal to enable/disable asset packages
|
||||
*/
|
||||
export function renderPackSelector(packs) {
|
||||
let modal = document.getElementById('pack-selector-modal');
|
||||
if (!modal) {
|
||||
modal = document.createElement('div');
|
||||
modal.id = 'pack-selector-modal';
|
||||
modal.className = 'modal-overlay';
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
const selectedIds = state.selectedPackIds;
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-content" style="width: 400px; max-height: 80vh; overflow-y: auto;">
|
||||
<div class="modal-header">
|
||||
<h3>${t('manage_packages') || 'Manage Packages'}</h3>
|
||||
<i class="fas fa-times close-modal"></i>
|
||||
</div>
|
||||
<div class="modal-body" style="padding: 15px;">
|
||||
<p style="font-size: 12px; color: #64748b; margin-bottom: 15px;">Select which asset packages to load in your sidebar.</p>
|
||||
<div class="pack-list">
|
||||
${packs.map(p => `
|
||||
<div class="pack-item" style="display: flex; align-items: center; justify-content: space-between; padding: 10px; border-bottom: 1px solid rgba(255,255,255,0.05);">
|
||||
<div style="display: flex; align-items: center; gap: 10px;">
|
||||
<i class="fas fa-box" style="color: #38bdf8; font-size: 14px;"></i>
|
||||
<span style="font-size: 13px; font-weight: 600;">${p.name || p.id}</span>
|
||||
</div>
|
||||
<input type="checkbox" data-id="${p.id}" ${selectedIds.includes(p.id) ? 'checked' : ''} style="width: 18px; height: 18px; cursor: pointer;">
|
||||
</div>
|
||||
`).join('')}
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer" style="padding: 15px; border-top: 1px solid rgba(255,255,255,0.05); text-align: right;">
|
||||
<button class="btn-primary apply-packs" style="background: #38bdf8; color: #0f172a; border: none; padding: 8px 16px; border-radius: 6px; font-weight: 700; cursor: pointer;">Apply & Refresh</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
modal.classList.add('active');
|
||||
|
||||
// Close handlers
|
||||
modal.querySelector('.close-modal').onclick = () => modal.classList.remove('active');
|
||||
modal.onclick = (e) => { if (e.target === modal) modal.classList.remove('active'); };
|
||||
|
||||
// Apply handler
|
||||
modal.querySelector('.apply-packs').onclick = () => {
|
||||
const checkboxes = modal.querySelectorAll('input[type="checkbox"]');
|
||||
const newSelectedIds = Array.from(checkboxes)
|
||||
.filter(cb => cb.checked)
|
||||
.map(cb => cb.dataset.id);
|
||||
|
||||
state.selectedPackIds = newSelectedIds;
|
||||
localStorage.setItem('selectedPackIds', JSON.stringify(newSelectedIds));
|
||||
|
||||
// This is a global refresh to re-init everything with new filters
|
||||
// For a more seamless experience, we could re-call initAssets()
|
||||
window.location.reload();
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user