import { settings } from "./settings.js"; import { getIconElement, isDefinedIcon } from "./ui-library/icons.js"; const WS_EVENT_NAME = "astro-dev-toolbar"; const WS_EVENT_NAME_DEPRECATED = "astro-dev-overlay"; const HOVER_DELAY = 2 * 1e3; const DEVBAR_HITBOX_ABOVE = 42; class AstroDevToolbar extends HTMLElement { shadowRoot; delayedHideTimeout; devToolbarContainer; apps = []; hasBeenInitialized = false; // TODO: This should be dynamic based on the screen size or at least configurable, erika - 2023-11-29 customAppsToShow = 3; constructor() { super(); this.shadowRoot = this.attachShadow({ mode: "open" }); } /** * All one-time DOM setup runs through here. Only ever call this once, * in connectedCallback(), and protect it from being called again. */ init() { this.shadowRoot.innerHTML = `
${this.apps.filter((app) => app.builtIn && !["astro:settings", "astro:more"].includes(app.id)).map((app) => this.getAppTemplate(app)).join("")} ${this.apps.filter((app) => !app.builtIn).length > 0 ? `
${this.apps.filter((app) => !app.builtIn).slice(0, this.customAppsToShow).map((app) => this.getAppTemplate(app)).join("")}` : ""} ${this.apps.filter((app) => !app.builtIn).length > this.customAppsToShow ? this.getAppTemplate( this.apps.find((app) => app.builtIn && app.id === "astro:more") ) : ""}
${this.getAppTemplate(this.apps.find((app) => app.builtIn && app.id === "astro:settings"))}
`; this.devToolbarContainer = this.shadowRoot.querySelector("#dev-toolbar-root"); this.attachEvents(); this.apps.forEach(async (app) => { settings.logger.verboseLog(`Creating app canvas for ${app.id}`); const appCanvas = document.createElement("astro-dev-toolbar-app-canvas"); appCanvas.dataset.appId = app.id; this.shadowRoot?.append(appCanvas); }); if ("requestIdleCallback" in window) { window.requestIdleCallback( async () => { this.apps.map((app) => this.initApp(app)); }, { timeout: 300 } ); } else { setTimeout(async () => { this.apps.map((app) => this.initApp(app)); }, 300); } } // This is called whenever the component is connected to the DOM. // This happens on first page load, and on each page change when // view transitions are used. connectedCallback() { if (!this.hasBeenInitialized) { this.init(); this.hasBeenInitialized = true; } this.apps.forEach(async (app) => { await this.setAppStatus(app, app.active); }); } attachEvents() { const items = this.shadowRoot.querySelectorAll(".item"); items.forEach((item) => { item.addEventListener("click", async (event) => { const target = event.currentTarget; if (!target || !(target instanceof HTMLElement)) return; const id = target.dataset.appId; if (!id) return; const app = this.getAppById(id); if (!app) return; event.stopPropagation(); await this.toggleAppStatus(app); }); }); ["mouseenter", "focusin"].forEach((event) => { this.devToolbarContainer.addEventListener(event, () => { this.clearDelayedHide(); if (this.isHidden()) { this.setToolbarVisible(true); } }); }); ["mouseleave", "focusout"].forEach((event) => { this.devToolbarContainer.addEventListener(event, () => { this.clearDelayedHide(); if (this.getActiveApp() || this.isHidden()) { return; } this.triggerDelayedHide(); }); }); document.addEventListener("keyup", (event) => { if (event.key !== "Escape") return; if (this.isHidden()) return; const activeApp = this.getActiveApp(); if (activeApp) { this.toggleAppStatus(activeApp); } else { this.setToolbarVisible(false); } }); } async initApp(app) { const shadowRoot = this.getAppCanvasById(app.id).shadowRoot; app.status = "loading"; try { settings.logger.verboseLog(`Initializing app ${app.id}`); await app.init?.(shadowRoot, app.eventTarget); app.status = "ready"; if (import.meta.hot) { import.meta.hot.send(`${WS_EVENT_NAME}:${app.id}:initialized`); import.meta.hot.send(`${WS_EVENT_NAME_DEPRECATED}:${app.id}:initialized`); } } catch (e) { console.error(`Failed to init app ${app.id}, error: ${e}`); app.status = "error"; if (import.meta.hot) { import.meta.hot.send("astro:devtoolbar:error:init", { app, error: e instanceof Error ? e.stack : e }); } const appButton = this.getAppButtonById(app.id); const appTooltip = appButton?.querySelector(".item-tooltip"); if (appButton && appTooltip) { appButton.toggleAttribute("data-app-error", true); appTooltip.innerText = `Error initializing ${app.name}`; } } } getAppTemplate(app) { return ``; } getAppById(id) { return this.apps.find((app) => app.id === id); } getAppCanvasById(id) { return this.shadowRoot.querySelector( `astro-dev-toolbar-app-canvas[data-app-id="${id}"]` ); } getAppButtonById(id) { return this.shadowRoot.querySelector(`[data-app-id="${id}"]`); } async toggleAppStatus(app) { const activeApp = this.getActiveApp(); if (activeApp) { const closeApp = await this.setAppStatus(activeApp, false); if (!closeApp) return; } if (app.status !== "ready") return; if (app !== activeApp) { await this.setAppStatus(app, true); if (import.meta.hot && app.id !== "astro:more") { import.meta.hot.send("astro:devtoolbar:app:toggled", { app }); } } } async setAppStatus(app, newStatus) { const appCanvas = this.getAppCanvasById(app.id); if (!appCanvas) return false; if (app.active && !newStatus && app.beforeTogglingOff) { const shouldToggleOff = await app.beforeTogglingOff(appCanvas.shadowRoot); if (!shouldToggleOff) return false; } app.active = newStatus ?? !app.active; const mainBarButton = this.getAppButtonById(app.id); const moreBarButton = this.getAppCanvasById("astro:more")?.shadowRoot?.querySelector( `[data-app-id="${app.id}"]` ); if (mainBarButton) { mainBarButton.classList.toggle("active", app.active); } if (moreBarButton) { moreBarButton.classList.toggle("active", app.active); } if (app.active) { appCanvas.style.display = "block"; appCanvas.setAttribute("data-active", ""); } else { appCanvas.style.display = "none"; appCanvas.removeAttribute("data-active"); } [ "app-toggled", // Deprecated // TODO: Remove in Astro 5.0 "plugin-toggled" ].forEach((eventName) => { app.eventTarget.dispatchEvent( new CustomEvent(eventName, { detail: { state: app.active, app } }) ); }); if (import.meta.hot) { import.meta.hot.send(`${WS_EVENT_NAME}:${app.id}:toggled`, { state: app.active }); import.meta.hot.send(`${WS_EVENT_NAME_DEPRECATED}:${app.id}:toggled`, { state: app.active }); } return true; } isHidden() { return this.devToolbarContainer?.hasAttribute("data-hidden") ?? true; } getActiveApp() { return this.apps.find((app) => app.active); } clearDelayedHide() { window.clearTimeout(this.delayedHideTimeout); this.delayedHideTimeout = void 0; } triggerDelayedHide() { this.clearDelayedHide(); this.delayedHideTimeout = window.setTimeout(() => { this.setToolbarVisible(false); this.delayedHideTimeout = void 0; }, HOVER_DELAY); } setToolbarVisible(newStatus) { const barContainer = this.shadowRoot.querySelector("#bar-container"); const devBar = this.shadowRoot.querySelector("#dev-bar"); const devBarHitboxAbove = this.shadowRoot.querySelector("#dev-bar-hitbox-above"); if (newStatus === true) { this.devToolbarContainer?.removeAttribute("data-hidden"); barContainer?.removeAttribute("inert"); devBar?.removeAttribute("tabindex"); if (devBarHitboxAbove) devBarHitboxAbove.style.height = "0"; return; } if (newStatus === false) { this.devToolbarContainer?.setAttribute("data-hidden", ""); barContainer?.setAttribute("inert", ""); devBar?.setAttribute("tabindex", "0"); if (devBarHitboxAbove) devBarHitboxAbove.style.height = `${DEVBAR_HITBOX_ABOVE}px`; return; } } setNotificationVisible(newStatus) { this.devToolbarContainer?.toggleAttribute("data-no-notification", !newStatus); const moreCanvas = this.getAppCanvasById("astro:more"); moreCanvas?.shadowRoot?.querySelector("#dropdown")?.toggleAttribute("data-no-notification", !newStatus); } } class DevToolbarCanvas extends HTMLElement { shadowRoot; constructor() { super(); this.shadowRoot = this.attachShadow({ mode: "open" }); } connectedCallback() { this.shadowRoot.innerHTML = ` `; } } function getAppIcon(icon) { if (isDefinedIcon(icon)) { return getIconElement(icon).outerHTML; } return icon; } export { AstroDevToolbar, DevToolbarCanvas, getAppIcon };