495 lines
16 KiB
TypeScript
495 lines
16 KiB
TypeScript
import { DeesElement, property, html, customElement, type TemplateResult, queryAsync, render, domtools } from '@design.estate/dees-element';
|
|
import { resolveTemplateFactory, getDemoAtIndex, getDemoCount, hasMultipleDemos } from './wcctools.helpers.js';
|
|
import type { TTemplateFactory } from './wcctools.helpers.js';
|
|
import type { IWccConfig, IWccSection, TElementType } from '../wcctools.interfaces.js';
|
|
|
|
import * as plugins from '../wcctools.plugins.js';
|
|
|
|
// wcc tools
|
|
import './wcc-frame.js';
|
|
import './wcc-sidebar.js';
|
|
import './wcc-properties.js';
|
|
import { type TTheme } from './wcc-properties.js';
|
|
import { breakpoints } from '@design.estate/dees-domtools';
|
|
import { WccFrame } from './wcc-frame.js';
|
|
|
|
/**
|
|
* Get filtered and sorted items from a section
|
|
*/
|
|
export const getSectionItems = (section: IWccSection): Array<[string, any]> => {
|
|
let entries = Object.entries(section.items);
|
|
|
|
// Apply filter if provided
|
|
if (section.filter) {
|
|
entries = entries.filter(([name, item]) => section.filter(name, item));
|
|
}
|
|
|
|
// Apply sort if provided
|
|
if (section.sort) {
|
|
entries.sort(section.sort);
|
|
}
|
|
|
|
return entries;
|
|
};
|
|
|
|
@customElement('wcc-dashboard')
|
|
export class WccDashboard extends DeesElement {
|
|
|
|
@property()
|
|
accessor sections: IWccSection[] = [];
|
|
|
|
@property()
|
|
accessor selectedSection: IWccSection | null = null;
|
|
|
|
@property()
|
|
accessor selectedType: TElementType;
|
|
|
|
@property()
|
|
accessor selectedItemName: string;
|
|
|
|
@property()
|
|
accessor selectedItem: TTemplateFactory | DeesElement;
|
|
|
|
@property({ type: Number })
|
|
accessor selectedDemoIndex: number = 0;
|
|
|
|
@property()
|
|
accessor selectedViewport: plugins.deesDomtools.breakpoints.TViewport = 'desktop';
|
|
|
|
@property()
|
|
accessor selectedTheme: TTheme = 'dark';
|
|
|
|
@property()
|
|
accessor searchQuery: string = '';
|
|
|
|
// Derived from selectedViewport - no need for separate property
|
|
public get isNative(): boolean {
|
|
return this.selectedViewport === 'native';
|
|
}
|
|
|
|
@property()
|
|
accessor warning: string = null;
|
|
|
|
private frameScrollY: number = 0;
|
|
private sidebarScrollY: number = 0;
|
|
private scrollPositionsApplied: boolean = false;
|
|
|
|
@queryAsync('wcc-frame')
|
|
accessor wccFrame: Promise<WccFrame>;
|
|
|
|
constructor(config?: IWccConfig) {
|
|
super();
|
|
if (config && config.sections) {
|
|
this.sections = config.sections;
|
|
console.log('got sections:', this.sections.map(s => s.name));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Find an item by name across all sections, returns the item and its section
|
|
*/
|
|
public findItemByName(name: string): { item: any; section: IWccSection } | null {
|
|
for (const section of this.sections) {
|
|
const entries = getSectionItems(section);
|
|
const found = entries.find(([itemName]) => itemName === name);
|
|
if (found) {
|
|
return { item: found[1], section };
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Find a section by name (URL-decoded)
|
|
*/
|
|
public findSectionByName(name: string): IWccSection | null {
|
|
return this.sections.find(s => s.name === name) || null;
|
|
}
|
|
|
|
public render(): TemplateResult {
|
|
return html`
|
|
<style>
|
|
:host {
|
|
background: #fcfcfc;
|
|
display: block;
|
|
box-sizing: border-box;
|
|
}
|
|
:host([hidden]) {
|
|
display: none;
|
|
}
|
|
</style>
|
|
<wcc-sidebar
|
|
.dashboardRef=${this}
|
|
.selectedItem=${this.selectedItem}
|
|
.searchQuery=${this.searchQuery}
|
|
.isNative=${this.isNative}
|
|
@selectedType=${(eventArg) => {
|
|
this.selectedType = eventArg.detail;
|
|
}}
|
|
@selectedItemName=${(eventArg) => {
|
|
this.selectedItemName = eventArg.detail;
|
|
}}
|
|
@selectedItem=${(eventArg) => {
|
|
this.selectedItem = eventArg.detail;
|
|
}}
|
|
@searchChanged=${(eventArg: CustomEvent) => {
|
|
this.searchQuery = eventArg.detail;
|
|
this.updateUrlWithScrollState();
|
|
}}
|
|
></wcc-sidebar>
|
|
<wcc-properties
|
|
.dashboardRef=${this}
|
|
.warning="${this.warning}"
|
|
.selectedItem=${this.selectedItem}
|
|
.selectedViewport=${this.selectedViewport}
|
|
.selectedTheme=${this.selectedTheme}
|
|
.isNative=${this.isNative}
|
|
@selectedViewport=${(eventArg) => {
|
|
this.selectedViewport = eventArg.detail;
|
|
this.scheduleUpdate();
|
|
}}
|
|
@selectedTheme=${(eventArg) => {
|
|
this.selectedTheme = eventArg.detail;
|
|
}}
|
|
@editorStateChanged=${async (eventArg) => {
|
|
const frame = await this.wccFrame;
|
|
if (frame) {
|
|
frame.advancedEditorOpen = eventArg.detail.isOpen;
|
|
frame.requestUpdate();
|
|
}
|
|
}}
|
|
@toggleNative=${() => {
|
|
this.toggleNative();
|
|
}}
|
|
></wcc-properties>
|
|
<wcc-frame id="wccFrame" viewport=${this.selectedViewport} .isNative=${this.isNative}>
|
|
</wcc-frame>
|
|
`;
|
|
}
|
|
|
|
public setWarning(warningTextArg: string) {
|
|
if (this.warning !== warningTextArg) {
|
|
console.log(warningTextArg);
|
|
this.warning = warningTextArg;
|
|
setTimeout(() => {
|
|
this.scheduleUpdate();
|
|
}, 0);
|
|
}
|
|
}
|
|
|
|
public toggleNative() {
|
|
// Toggle between 'native' and 'desktop' viewports
|
|
this.selectedViewport = this.selectedViewport === 'native' ? 'desktop' : 'native';
|
|
this.buildUrl();
|
|
}
|
|
|
|
public async firstUpdated() {
|
|
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
|
|
|
// Add ESC key handler for native mode
|
|
document.addEventListener('keydown', (event) => {
|
|
if (event.key === 'Escape' && this.isNative) {
|
|
this.selectedViewport = 'desktop';
|
|
this.buildUrl();
|
|
}
|
|
});
|
|
|
|
// Set up scroll listeners after DOM is ready
|
|
setTimeout(() => {
|
|
this.setupScrollListeners();
|
|
}, 500);
|
|
|
|
// New route format with section name
|
|
this.domtools.router.on(
|
|
'/wcctools-route/:sectionName/:itemName/:demoIndex/:viewport/:theme',
|
|
async (routeInfo) => {
|
|
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
|
this.selectedSection = this.findSectionByName(sectionName);
|
|
this.selectedItemName = routeInfo.params.itemName;
|
|
this.selectedDemoIndex = parseInt(routeInfo.params.demoIndex) || 0;
|
|
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
|
this.selectedTheme = routeInfo.params.theme as TTheme;
|
|
|
|
if (this.selectedSection) {
|
|
// Find item within the section
|
|
const entries = getSectionItems(this.selectedSection);
|
|
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
|
if (found) {
|
|
this.selectedItem = found[1];
|
|
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
|
}
|
|
} else {
|
|
// Fallback: try legacy format (element/page as section name)
|
|
const legacyType = routeInfo.params.sectionName;
|
|
if (legacyType === 'element' || legacyType === 'page') {
|
|
this.selectedType = legacyType as TElementType;
|
|
// Find item in any matching section
|
|
const result = this.findItemByName(routeInfo.params.itemName);
|
|
if (result) {
|
|
this.selectedItem = result.item;
|
|
this.selectedSection = result.section;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore state from query parameters
|
|
if (routeInfo.queryParams) {
|
|
const search = routeInfo.queryParams.search;
|
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
|
|
|
if (search) {
|
|
this.searchQuery = search;
|
|
} else {
|
|
this.searchQuery = '';
|
|
}
|
|
if (frameScrollY) {
|
|
this.frameScrollY = parseInt(frameScrollY);
|
|
}
|
|
if (sidebarScrollY) {
|
|
this.sidebarScrollY = parseInt(sidebarScrollY);
|
|
}
|
|
|
|
// Apply scroll positions after a short delay to ensure DOM is ready
|
|
setTimeout(() => {
|
|
this.applyScrollPositions();
|
|
}, 100);
|
|
} else {
|
|
this.searchQuery = '';
|
|
}
|
|
|
|
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
|
this.selectedTheme === 'bright'
|
|
? domtoolsInstance.themeManager.goBright()
|
|
: domtoolsInstance.themeManager.goDark();
|
|
}
|
|
);
|
|
|
|
// Legacy route without demo index (for backwards compatibility)
|
|
this.domtools.router.on(
|
|
'/wcctools-route/:sectionName/:itemName/:viewport/:theme',
|
|
async (routeInfo) => {
|
|
const sectionName = decodeURIComponent(routeInfo.params.sectionName);
|
|
this.selectedSection = this.findSectionByName(sectionName);
|
|
this.selectedItemName = routeInfo.params.itemName;
|
|
this.selectedDemoIndex = 0;
|
|
this.selectedViewport = routeInfo.params.viewport as breakpoints.TViewport;
|
|
this.selectedTheme = routeInfo.params.theme as TTheme;
|
|
|
|
if (this.selectedSection) {
|
|
const entries = getSectionItems(this.selectedSection);
|
|
const found = entries.find(([name]) => name === routeInfo.params.itemName);
|
|
if (found) {
|
|
this.selectedItem = found[1];
|
|
this.selectedType = this.selectedSection.type === 'elements' ? 'element' : 'page';
|
|
}
|
|
} else {
|
|
// Fallback: try legacy format
|
|
const legacyType = routeInfo.params.sectionName;
|
|
if (legacyType === 'element' || legacyType === 'page') {
|
|
this.selectedType = legacyType as TElementType;
|
|
const result = this.findItemByName(routeInfo.params.itemName);
|
|
if (result) {
|
|
this.selectedItem = result.item;
|
|
this.selectedSection = result.section;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Restore state from query parameters
|
|
if (routeInfo.queryParams) {
|
|
const search = routeInfo.queryParams.search;
|
|
const frameScrollY = routeInfo.queryParams.frameScrollY;
|
|
const sidebarScrollY = routeInfo.queryParams.sidebarScrollY;
|
|
|
|
if (search) {
|
|
this.searchQuery = search;
|
|
} else {
|
|
this.searchQuery = '';
|
|
}
|
|
if (frameScrollY) {
|
|
this.frameScrollY = parseInt(frameScrollY);
|
|
}
|
|
if (sidebarScrollY) {
|
|
this.sidebarScrollY = parseInt(sidebarScrollY);
|
|
}
|
|
|
|
// Apply scroll positions after a short delay to ensure DOM is ready
|
|
setTimeout(() => {
|
|
this.applyScrollPositions();
|
|
}, 100);
|
|
} else {
|
|
this.searchQuery = '';
|
|
}
|
|
|
|
const domtoolsInstance = await plugins.deesDomtools.elementBasic.setup();
|
|
this.selectedTheme === 'bright'
|
|
? domtoolsInstance.themeManager.goBright()
|
|
: domtoolsInstance.themeManager.goDark();
|
|
}
|
|
);
|
|
}
|
|
|
|
public async updated(changedPropertiesArg: Map<string, any>) {
|
|
this.domtools = await plugins.deesDomtools.DomTools.setupDomTools();
|
|
await this.domtools.router._handleRouteState();
|
|
const wccFrame: WccFrame = this.shadowRoot.querySelector('wcc-frame');
|
|
|
|
if (changedPropertiesArg.has('selectedItemName')) {
|
|
document.title = this.selectedItemName;
|
|
};
|
|
|
|
if (this.selectedType === 'page' && this.selectedItem) {
|
|
if (typeof this.selectedItem === 'function') {
|
|
console.log('slotting page.');
|
|
const viewport = await wccFrame.getViewportElement();
|
|
const pageFactory = this.selectedItem as TTemplateFactory;
|
|
const pageTemplate = await resolveTemplateFactory(pageFactory);
|
|
render(pageTemplate, viewport);
|
|
console.log('rendered page.');
|
|
} else {
|
|
console.error('The selected item looks strange:');
|
|
console.log(this.selectedItem);
|
|
}
|
|
} else if (this.selectedType === 'element' && this.selectedItem) {
|
|
console.log('slotting element.');
|
|
const anonItem: any = this.selectedItem;
|
|
if (!anonItem.demo) {
|
|
this.setWarning(`component ${anonItem.name} does not expose a demo property.`);
|
|
return;
|
|
}
|
|
|
|
// Support both single demo (function) and multiple demos (array)
|
|
const isArray = Array.isArray(anonItem.demo);
|
|
const isFunction = typeof anonItem.demo === 'function';
|
|
|
|
if (!isArray && !isFunction) {
|
|
this.setWarning(
|
|
`component ${anonItem.name} has demo property, but it is not a function or array of functions`
|
|
);
|
|
return;
|
|
}
|
|
|
|
// Get the specific demo to render
|
|
const demoFactory = getDemoAtIndex(anonItem.demo, this.selectedDemoIndex);
|
|
if (!demoFactory) {
|
|
this.setWarning(
|
|
`component ${anonItem.name} does not have a demo at index ${this.selectedDemoIndex + 1}`
|
|
);
|
|
return;
|
|
}
|
|
|
|
this.setWarning(null);
|
|
const viewport = await wccFrame.getViewportElement();
|
|
const demoTemplate = await resolveTemplateFactory(demoFactory);
|
|
render(demoTemplate, viewport);
|
|
}
|
|
}
|
|
|
|
public buildUrl() {
|
|
const sectionName = this.selectedSection
|
|
? encodeURIComponent(this.selectedSection.name)
|
|
: this.selectedType; // Fallback for legacy
|
|
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
|
const queryParams = new URLSearchParams();
|
|
|
|
if (this.searchQuery) {
|
|
queryParams.set('search', this.searchQuery);
|
|
}
|
|
if (this.frameScrollY > 0) {
|
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
|
}
|
|
if (this.sidebarScrollY > 0) {
|
|
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
|
}
|
|
|
|
const queryString = queryParams.toString();
|
|
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
|
|
this.domtools.router.pushUrl(fullUrl);
|
|
}
|
|
|
|
private scrollUpdateTimeout: NodeJS.Timeout;
|
|
private scrollListenersAttached: boolean = false;
|
|
|
|
public async setupScrollListeners() {
|
|
// Prevent duplicate listeners
|
|
if (this.scrollListenersAttached) {
|
|
return;
|
|
}
|
|
|
|
const wccFrame = await this.wccFrame;
|
|
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
|
|
|
if (wccFrame) {
|
|
// The frame element itself is the scrollable container
|
|
wccFrame.addEventListener('scroll', () => {
|
|
this.frameScrollY = wccFrame.scrollTop;
|
|
this.debouncedScrollUpdate();
|
|
});
|
|
this.scrollListenersAttached = true;
|
|
}
|
|
|
|
if (wccSidebar) {
|
|
// The sidebar element itself is the scrollable container
|
|
wccSidebar.addEventListener('scroll', () => {
|
|
this.sidebarScrollY = wccSidebar.scrollTop;
|
|
this.debouncedScrollUpdate();
|
|
});
|
|
}
|
|
}
|
|
|
|
private debouncedScrollUpdate() {
|
|
clearTimeout(this.scrollUpdateTimeout);
|
|
this.scrollUpdateTimeout = setTimeout(() => {
|
|
this.updateUrlWithScrollState();
|
|
}, 300);
|
|
}
|
|
|
|
private updateUrlWithScrollState() {
|
|
const sectionName = this.selectedSection
|
|
? encodeURIComponent(this.selectedSection.name)
|
|
: this.selectedType; // Fallback for legacy
|
|
const baseUrl = `/wcctools-route/${sectionName}/${this.selectedItemName}/${this.selectedDemoIndex}/${this.selectedViewport}/${this.selectedTheme}`;
|
|
const queryParams = new URLSearchParams();
|
|
|
|
if (this.searchQuery) {
|
|
queryParams.set('search', this.searchQuery);
|
|
}
|
|
if (this.frameScrollY > 0) {
|
|
queryParams.set('frameScrollY', this.frameScrollY.toString());
|
|
}
|
|
if (this.sidebarScrollY > 0) {
|
|
queryParams.set('sidebarScrollY', this.sidebarScrollY.toString());
|
|
}
|
|
|
|
const queryString = queryParams.toString();
|
|
const fullUrl = queryString ? `${baseUrl}?${queryString}` : baseUrl;
|
|
|
|
// Use replaceState to update URL without navigation
|
|
window.history.replaceState(null, '', fullUrl);
|
|
}
|
|
|
|
public async applyScrollPositions() {
|
|
// Only apply scroll positions once to avoid interfering with user scrolling
|
|
if (this.scrollPositionsApplied) {
|
|
return;
|
|
}
|
|
|
|
const wccFrame = await this.wccFrame;
|
|
const wccSidebar = this.shadowRoot.querySelector('wcc-sidebar');
|
|
|
|
if (wccFrame && this.frameScrollY > 0) {
|
|
// The frame element itself is the scrollable container
|
|
wccFrame.scrollTop = this.frameScrollY;
|
|
}
|
|
|
|
if (wccSidebar && this.sidebarScrollY > 0) {
|
|
// The sidebar element itself is the scrollable container
|
|
wccSidebar.scrollTop = this.sidebarScrollY;
|
|
}
|
|
|
|
this.scrollPositionsApplied = true;
|
|
}
|
|
}
|