feat(sidebar): rename demoGroup to demoGroups, add multi-group support, search by group name, and context menu group navigation
This commit is contained in:
@@ -4,7 +4,7 @@ import { WccDashboard, getSectionItems } from './wcc-dashboard.js';
|
||||
import type { TTemplateFactory } from './wcctools.helpers.js';
|
||||
import { getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
||||
import type { IWccSection, TElementType } from '../wcctools.interfaces.js';
|
||||
import { WccContextmenu } from './wcc-contextmenu.js';
|
||||
import { WccContextmenu, type IContextMenuItem } from './wcc-contextmenu.js';
|
||||
|
||||
@customElement('wcc-sidebar')
|
||||
export class WccSidebar extends DeesElement {
|
||||
@@ -422,6 +422,7 @@ export class WccSidebar extends DeesElement {
|
||||
color: #555;
|
||||
padding: 0.125rem 0.625rem 0.25rem;
|
||||
display: block;
|
||||
cursor: context-menu;
|
||||
}
|
||||
|
||||
.item-group .selectOption {
|
||||
@@ -429,6 +430,15 @@ export class WccSidebar extends DeesElement {
|
||||
margin-right: 0.25rem;
|
||||
}
|
||||
|
||||
.item-group.group-highlight {
|
||||
background: rgba(59, 130, 246, 0.15);
|
||||
transition: background 0.3s ease;
|
||||
}
|
||||
|
||||
.item-group.group-filter-match {
|
||||
border-color: rgba(245, 158, 11, 0.5);
|
||||
}
|
||||
|
||||
/* Resize handle */
|
||||
.resize-handle {
|
||||
position: absolute;
|
||||
@@ -517,12 +527,49 @@ export class WccSidebar extends DeesElement {
|
||||
|
||||
private showContextMenu(e: MouseEvent, sectionName: string, itemName: string) {
|
||||
const isPinned = this.isPinned(sectionName, itemName);
|
||||
WccContextmenu.show(e, [
|
||||
const section = this.dashboardRef?.sections?.find(s => s.name === sectionName);
|
||||
const sectionEntries = section ? getSectionItems(section) : [];
|
||||
const foundEntry = sectionEntries.find(([name]) => name === itemName);
|
||||
const item = foundEntry?.[1];
|
||||
const groups = item ? this.getElementGroups(item) : [];
|
||||
|
||||
const menuItems: IContextMenuItem[] = [
|
||||
{
|
||||
name: isPinned ? 'Unpin' : 'Pin',
|
||||
iconName: isPinned ? 'push_pin' : 'push_pin',
|
||||
action: () => this.togglePin(sectionName, itemName),
|
||||
},
|
||||
];
|
||||
|
||||
if (groups.length > 0) {
|
||||
menuItems.push({
|
||||
name: 'Show in Group:',
|
||||
iconName: 'folder',
|
||||
action: () => {},
|
||||
disabled: true,
|
||||
});
|
||||
for (const groupName of groups) {
|
||||
menuItems.push({
|
||||
name: groupName,
|
||||
iconName: 'label',
|
||||
action: () => this.scrollToGroup(sectionName, groupName),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
WccContextmenu.show(e, menuItems);
|
||||
}
|
||||
|
||||
private showGroupContextMenu(e: MouseEvent, groupName: string) {
|
||||
WccContextmenu.show(e, [
|
||||
{
|
||||
name: `Show "${groupName}"`,
|
||||
iconName: 'filter_alt',
|
||||
action: () => {
|
||||
this.searchQuery = groupName;
|
||||
this.dispatchEvent(new CustomEvent('searchChanged', { detail: this.searchQuery }));
|
||||
},
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -569,7 +616,9 @@ export class WccSidebar extends DeesElement {
|
||||
${pinnedEntries.map(({ sectionName, itemName, item, section }) => {
|
||||
const isSelected = this.selectedItem === item;
|
||||
const type = section.type === 'elements' ? 'element' : 'page';
|
||||
const icon = section.type === 'elements' ? 'featured_video' : 'insert_drive_file';
|
||||
const icon = section.type === 'elements'
|
||||
? (this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video')
|
||||
: 'insert_drive_file';
|
||||
|
||||
return html`
|
||||
<div
|
||||
@@ -603,7 +652,13 @@ export class WccSidebar extends DeesElement {
|
||||
return this.dashboardRef.sections.map((section) => {
|
||||
// Check if section has any matching items
|
||||
const entries = getSectionItems(section);
|
||||
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||
const filteredEntries = entries.filter(([name, item]) => {
|
||||
if (this.matchesSearch(name)) return true;
|
||||
const rawGroups = (item as any).demoGroups;
|
||||
if (!rawGroups) return false;
|
||||
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
|
||||
return groups.some(g => this.matchesSearch(g));
|
||||
});
|
||||
|
||||
// Hide section if no items match the search
|
||||
if (filteredEntries.length === 0 && this.searchQuery) {
|
||||
@@ -635,7 +690,13 @@ export class WccSidebar extends DeesElement {
|
||||
private renderSectionItems(section: IWccSection) {
|
||||
const entries = getSectionItems(section);
|
||||
// Filter entries by search query
|
||||
const filteredEntries = entries.filter(([name]) => this.matchesSearch(name));
|
||||
const filteredEntries = entries.filter(([name, item]) => {
|
||||
if (this.matchesSearch(name)) return true;
|
||||
const rawGroups = (item as any).demoGroups;
|
||||
if (!rawGroups) return false;
|
||||
const groups: string[] = Array.isArray(rawGroups) ? rawGroups : [rawGroups];
|
||||
return groups.some(g => this.matchesSearch(g));
|
||||
});
|
||||
|
||||
if (section.type === 'pages') {
|
||||
return filteredEntries.map(([pageName, item]) => {
|
||||
@@ -655,16 +716,21 @@ export class WccSidebar extends DeesElement {
|
||||
`;
|
||||
});
|
||||
} else {
|
||||
// type === 'elements' - group by demoGroup
|
||||
// type === 'elements' - group by demoGroups (supports string | string[])
|
||||
const groupedItems = new Map<string | null, Array<[string, any]>>();
|
||||
|
||||
for (const entry of filteredEntries) {
|
||||
const [, item] = entry;
|
||||
const group = (item as any).demoGroup || null;
|
||||
if (!groupedItems.has(group)) {
|
||||
groupedItems.set(group, []);
|
||||
const rawGroups = (item as any).demoGroups;
|
||||
const groups: Array<string | null> = rawGroups
|
||||
? (Array.isArray(rawGroups) ? rawGroups : [rawGroups])
|
||||
: [null];
|
||||
for (const group of groups) {
|
||||
if (!groupedItems.has(group)) {
|
||||
groupedItems.set(group, []);
|
||||
}
|
||||
groupedItems.get(group)!.push(entry);
|
||||
}
|
||||
groupedItems.get(group)!.push(entry);
|
||||
}
|
||||
|
||||
// Build a unified list of render items (ungrouped elements and groups)
|
||||
@@ -681,11 +747,10 @@ export class WccSidebar extends DeesElement {
|
||||
renderItems.push({ type: 'element', entry, sortKey: entry[0].toLowerCase() });
|
||||
}
|
||||
|
||||
// Add groups (sorted by their first element's name)
|
||||
// Add groups (sorted by group name)
|
||||
for (const [groupName, items] of groupedItems) {
|
||||
if (groupName === null) continue;
|
||||
const firstElementName = items[0]?.[0] || '';
|
||||
renderItems.push({ type: 'group', groupName, items, sortKey: firstElementName.toLowerCase() });
|
||||
renderItems.push({ type: 'group', groupName, items, sortKey: groupName.toLowerCase() });
|
||||
}
|
||||
|
||||
// Sort all items alphabetically by sortKey
|
||||
@@ -697,8 +762,11 @@ export class WccSidebar extends DeesElement {
|
||||
return this.renderElementItem(item.entry, section);
|
||||
} else {
|
||||
return html`
|
||||
<div class="item-group">
|
||||
<span class="item-group-legend">${item.groupName}</span>
|
||||
<div class="item-group ${this.isGroupFilterMatch(item.groupName) ? 'group-filter-match' : ''}" data-group="${item.groupName}">
|
||||
<span
|
||||
class="item-group-legend"
|
||||
@contextmenu=${(e: MouseEvent) => this.showGroupContextMenu(e, item.groupName)}
|
||||
>${item.groupName}</span>
|
||||
${item.items.map((entry) => this.renderElementItem(entry, section))}
|
||||
</div>
|
||||
`;
|
||||
@@ -753,6 +821,7 @@ export class WccSidebar extends DeesElement {
|
||||
`;
|
||||
} else {
|
||||
// Single demo element
|
||||
const icon = this.getElementGroups(item).length > 1 ? 'library_books' : 'featured_video';
|
||||
return html`
|
||||
<div
|
||||
class="selectOption ${isSelected ? 'selected' : ''} ${isPinned ? 'pinned' : ''}"
|
||||
@@ -762,7 +831,7 @@ export class WccSidebar extends DeesElement {
|
||||
}}
|
||||
@contextmenu=${(e: MouseEvent) => this.showContextMenu(e, section.name, elementName)}
|
||||
>
|
||||
<i class="material-symbols-outlined">featured_video</i>
|
||||
<i class="material-symbols-outlined">${icon}</i>
|
||||
<div class="text">${this.highlightMatch(elementName)}</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -810,6 +879,37 @@ export class WccSidebar extends DeesElement {
|
||||
return name.toLowerCase().includes(this.searchQuery.toLowerCase());
|
||||
}
|
||||
|
||||
private isGroupFilterMatch(groupName: string): boolean {
|
||||
return !!this.searchQuery && groupName.toLowerCase() === this.searchQuery.toLowerCase();
|
||||
}
|
||||
|
||||
private getElementGroups(item: any): string[] {
|
||||
const raw = item?.demoGroups;
|
||||
if (!raw) return [];
|
||||
return Array.isArray(raw) ? raw : [raw];
|
||||
}
|
||||
|
||||
private scrollToGroup(sectionName: string, groupName: string) {
|
||||
// Ensure the section is not collapsed
|
||||
this.collapsedSections.delete(sectionName);
|
||||
// Clear any active search so all groups are visible
|
||||
this.searchQuery = '';
|
||||
this.requestUpdate();
|
||||
|
||||
// After render, scroll to the group element
|
||||
this.updateComplete.then(() => {
|
||||
const groupEl = this.shadowRoot?.querySelector(
|
||||
`.item-group[data-group="${groupName}"]`
|
||||
);
|
||||
if (groupEl) {
|
||||
groupEl.scrollIntoView({ behavior: 'smooth', block: 'center' });
|
||||
// Brief highlight flash
|
||||
groupEl.classList.add('group-highlight');
|
||||
setTimeout(() => groupEl.classList.remove('group-highlight'), 1500);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private highlightMatch(text: string): TemplateResult {
|
||||
if (!this.searchQuery) return html`${text}`;
|
||||
const lowerText = text.toLowerCase();
|
||||
|
||||
Reference in New Issue
Block a user