mirror of
https://github.com/withastro/astro.git
synced 2025-03-24 23:21:57 -05:00
refactor: dev overlay to make it easier to work with VT (#8966)
This commit is contained in:
parent
463e03633e
commit
262cef2487
10 changed files with 399 additions and 306 deletions
5
.changeset/healthy-hornets-kiss.md
Normal file
5
.changeset/healthy-hornets-kiss.md
Normal file
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
'astro': patch
|
||||
---
|
||||
|
||||
Fix Dev Overlay not working properly when view transitions are enabled
|
|
@ -21,7 +21,11 @@ import type { TSConfig } from '../core/config/tsconfig.js';
|
|||
import type { AstroCookies } from '../core/cookies/index.js';
|
||||
import type { ResponseWithEncoding } from '../core/endpoint/index.js';
|
||||
import type { AstroIntegrationLogger, Logger, LoggerLevel } from '../core/logger/core.js';
|
||||
import type { AstroDevOverlay, DevOverlayCanvas } from '../runtime/client/dev-overlay/overlay.js';
|
||||
import type { DevOverlayHighlight } from '../runtime/client/dev-overlay/ui-library/highlight.js';
|
||||
import type { Icon } from '../runtime/client/dev-overlay/ui-library/icons.js';
|
||||
import type { DevOverlayTooltip } from '../runtime/client/dev-overlay/ui-library/tooltip.js';
|
||||
import type { DevOverlayWindow } from '../runtime/client/dev-overlay/ui-library/window.js';
|
||||
import type { AstroComponentFactory, AstroComponentInstance } from '../runtime/server/index.js';
|
||||
import type { OmitIndexSignature, Simplify } from '../type-utils.js';
|
||||
import type { SUPPORTED_MARKDOWN_FILE_EXTENSIONS } from './../core/constants.js';
|
||||
|
@ -2322,3 +2326,13 @@ export type DevOverlayMetadata = Window &
|
|||
root: string;
|
||||
};
|
||||
};
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
'astro-dev-overlay': AstroDevOverlay;
|
||||
'astro-dev-overlay-window': DevOverlayWindow;
|
||||
'astro-dev-overlay-plugin-canvas': DevOverlayCanvas;
|
||||
'astro-dev-overlay-tooltip': DevOverlayTooltip;
|
||||
'astro-dev-overlay-highlight': DevOverlayHighlight;
|
||||
}
|
||||
}
|
||||
|
|
84
packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
Normal file
84
packages/astro/src/runtime/client/dev-overlay/entrypoint.ts
Normal file
|
@ -0,0 +1,84 @@
|
|||
import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js';
|
||||
import { type AstroDevOverlay, type DevOverlayPlugin } from './overlay.js';
|
||||
|
||||
let overlay: AstroDevOverlay;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const [
|
||||
{ loadDevOverlayPlugins },
|
||||
{ default: astroDevToolPlugin },
|
||||
{ default: astroAuditPlugin },
|
||||
{ default: astroXrayPlugin },
|
||||
{ AstroDevOverlay, DevOverlayCanvas },
|
||||
{ DevOverlayCard },
|
||||
{ DevOverlayHighlight },
|
||||
{ DevOverlayTooltip },
|
||||
{ DevOverlayWindow },
|
||||
] = await Promise.all([
|
||||
// @ts-expect-error
|
||||
import('astro:dev-overlay'),
|
||||
import('./plugins/astro.js'),
|
||||
import('./plugins/audit.js'),
|
||||
import('./plugins/xray.js'),
|
||||
import('./overlay.js'),
|
||||
import('./ui-library/card.js'),
|
||||
import('./ui-library/highlight.js'),
|
||||
import('./ui-library/tooltip.js'),
|
||||
import('./ui-library/window.js'),
|
||||
]);
|
||||
|
||||
// Register custom elements
|
||||
customElements.define('astro-dev-overlay', AstroDevOverlay);
|
||||
customElements.define('astro-dev-overlay-window', DevOverlayWindow);
|
||||
customElements.define('astro-dev-overlay-plugin-canvas', DevOverlayCanvas);
|
||||
customElements.define('astro-dev-overlay-tooltip', DevOverlayTooltip);
|
||||
customElements.define('astro-dev-overlay-highlight', DevOverlayHighlight);
|
||||
customElements.define('astro-dev-overlay-card', DevOverlayCard);
|
||||
|
||||
overlay = document.createElement('astro-dev-overlay');
|
||||
|
||||
const preparePlugin = (
|
||||
pluginDefinition: DevOverlayPluginDefinition,
|
||||
builtIn: boolean
|
||||
): DevOverlayPlugin => {
|
||||
const eventTarget = new EventTarget();
|
||||
const plugin = {
|
||||
...pluginDefinition,
|
||||
builtIn: builtIn,
|
||||
active: false,
|
||||
status: 'loading' as const,
|
||||
eventTarget: eventTarget,
|
||||
};
|
||||
|
||||
// Events plugins can send to the overlay to update their status
|
||||
eventTarget.addEventListener('plugin-notification', (evt) => {
|
||||
const target = overlay.shadowRoot?.querySelector(`[data-plugin-id="${plugin.id}"]`);
|
||||
if (!target) return;
|
||||
|
||||
let newState = true;
|
||||
if (evt instanceof CustomEvent) {
|
||||
newState = evt.detail.state ?? true;
|
||||
}
|
||||
|
||||
target.querySelector('.notification')?.toggleAttribute('data-active', newState);
|
||||
});
|
||||
|
||||
return plugin;
|
||||
};
|
||||
|
||||
const customPluginsDefinitions = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[];
|
||||
const plugins: DevOverlayPlugin[] = [
|
||||
...[astroDevToolPlugin, astroXrayPlugin, astroAuditPlugin].map((pluginDef) =>
|
||||
preparePlugin(pluginDef, true)
|
||||
),
|
||||
...customPluginsDefinitions.map((pluginDef) => preparePlugin(pluginDef, false)),
|
||||
];
|
||||
|
||||
overlay.plugins = plugins;
|
||||
|
||||
document.body.append(overlay);
|
||||
|
||||
document.addEventListener('astro:after-swap', () => {
|
||||
document.body.append(overlay);
|
||||
});
|
||||
});
|
|
@ -1,79 +1,47 @@
|
|||
/* eslint-disable no-console */
|
||||
// @ts-expect-error
|
||||
import { loadDevOverlayPlugins } from 'astro:dev-overlay';
|
||||
import type { DevOverlayPlugin as DevOverlayPluginDefinition } from '../../../@types/astro.js';
|
||||
import astroDevToolPlugin from './plugins/astro.js';
|
||||
import astroAuditPlugin from './plugins/audit.js';
|
||||
import astroXrayPlugin from './plugins/xray.js';
|
||||
import { DevOverlayCard } from './ui-library/card.js';
|
||||
import { DevOverlayHighlight } from './ui-library/highlight.js';
|
||||
import { getIconElement, isDefinedIcon, type Icon } from './ui-library/icons.js';
|
||||
import { DevOverlayTooltip } from './ui-library/tooltip.js';
|
||||
import { DevOverlayWindow } from './ui-library/window.js';
|
||||
|
||||
type DevOverlayPlugin = DevOverlayPluginDefinition & {
|
||||
export type DevOverlayPlugin = DevOverlayPluginDefinition & {
|
||||
builtIn: boolean;
|
||||
active: boolean;
|
||||
status: 'ready' | 'loading' | 'error';
|
||||
eventTarget: EventTarget;
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
const WS_EVENT_NAME = 'astro-dev-overlay';
|
||||
const HOVER_DELAY = 750;
|
||||
const WS_EVENT_NAME = 'astro-dev-overlay';
|
||||
|
||||
const builtinPlugins: DevOverlayPlugin[] = [
|
||||
astroDevToolPlugin,
|
||||
astroXrayPlugin,
|
||||
astroAuditPlugin,
|
||||
].map((plugin) => ({
|
||||
...plugin,
|
||||
active: false,
|
||||
status: 'loading',
|
||||
eventTarget: new EventTarget(),
|
||||
}));
|
||||
export class AstroDevOverlay extends HTMLElement {
|
||||
shadowRoot: ShadowRoot;
|
||||
hoverTimeout: number | undefined;
|
||||
isHidden: () => boolean = () => this.devOverlay?.hasAttribute('data-hidden') ?? true;
|
||||
devOverlay: HTMLDivElement | undefined;
|
||||
plugins: DevOverlayPlugin[] = [];
|
||||
HOVER_DELAY = 750;
|
||||
hasBeenInitialized = false;
|
||||
|
||||
const customPluginsImports = (await loadDevOverlayPlugins()) as DevOverlayPluginDefinition[];
|
||||
const customPlugins: DevOverlayPlugin[] = [];
|
||||
customPlugins.push(
|
||||
...customPluginsImports.map((plugin) => ({
|
||||
...plugin,
|
||||
active: false,
|
||||
status: 'loading' as const,
|
||||
eventTarget: new EventTarget(),
|
||||
}))
|
||||
);
|
||||
|
||||
const plugins: DevOverlayPlugin[] = [...builtinPlugins, ...customPlugins];
|
||||
|
||||
for (const plugin of plugins) {
|
||||
plugin.eventTarget.addEventListener('plugin-notification', (evt) => {
|
||||
const target = overlay.shadowRoot?.querySelector(`[data-plugin-id="${plugin.id}"]`);
|
||||
if (!target) return;
|
||||
|
||||
let newState = true;
|
||||
if (evt instanceof CustomEvent) {
|
||||
newState = evt.detail.state ?? true;
|
||||
}
|
||||
|
||||
target.querySelector('.notification')?.toggleAttribute('data-active', newState);
|
||||
});
|
||||
constructor() {
|
||||
super();
|
||||
this.shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
class AstroDevOverlay extends HTMLElement {
|
||||
shadowRoot: ShadowRoot;
|
||||
hoverTimeout: number | undefined;
|
||||
isHidden: () => boolean = () => this.devOverlay?.hasAttribute('data-hidden') ?? true;
|
||||
devOverlay: HTMLDivElement | undefined;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
// connect component
|
||||
async connectedCallback() {
|
||||
// Happens whenever the component is connected to the DOM
|
||||
// When view transitions are enabled, this happens every time the view changes
|
||||
async connectedCallback() {
|
||||
if (!this.hasBeenInitialized) {
|
||||
this.shadowRoot.innerHTML = `
|
||||
<style>
|
||||
:host {
|
||||
z-index: 999999;
|
||||
view-transition-name: astro-dev-overlay;
|
||||
display: contents;
|
||||
}
|
||||
|
||||
::view-transition-old(astro-dev-overlay),
|
||||
::view-transition-new(astro-dev-overlay) {
|
||||
animation: none;
|
||||
}
|
||||
|
||||
#dev-overlay {
|
||||
position: fixed;
|
||||
bottom: 7.5%;
|
||||
|
@ -250,258 +218,252 @@ document.addEventListener('DOMContentLoaded', async () => {
|
|||
<div id="dev-overlay">
|
||||
<div id="dev-bar">
|
||||
<div id="bar-container">
|
||||
${builtinPlugins.map((plugin) => this.getPluginTemplate(plugin)).join('')}
|
||||
${this.plugins
|
||||
.filter((plugin) => plugin.builtIn)
|
||||
.map((plugin) => this.getPluginTemplate(plugin))
|
||||
.join('')}
|
||||
<div class="separator"></div>
|
||||
${customPlugins.map((plugin) => this.getPluginTemplate(plugin)).join('')}
|
||||
${this.plugins
|
||||
.filter((plugin) => !plugin.builtIn)
|
||||
.map((plugin) => this.getPluginTemplate(plugin))
|
||||
.join('')}
|
||||
</div>
|
||||
</div>
|
||||
<button id="minimize-button">${getIconElement('arrow-down')?.outerHTML}</button>
|
||||
</div>`;
|
||||
|
||||
this.devOverlay = this.shadowRoot.querySelector<HTMLDivElement>('#dev-overlay')!;
|
||||
this.attachEvents();
|
||||
|
||||
// Init plugin lazily
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(async () => {
|
||||
await this.initAllPlugins();
|
||||
});
|
||||
} else {
|
||||
// Fallback to setTimeout for.. Safari...
|
||||
setTimeout(async () => {
|
||||
await this.initAllPlugins();
|
||||
}, 200);
|
||||
}
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
const items = this.shadowRoot.querySelectorAll<HTMLDivElement>('.item');
|
||||
items.forEach((item) => {
|
||||
item.addEventListener('click', async (e) => {
|
||||
const target = e.currentTarget;
|
||||
if (!target || !(target instanceof HTMLElement)) return;
|
||||
|
||||
const id = target.dataset.pluginId;
|
||||
if (!id) return;
|
||||
|
||||
const plugin = this.getPluginById(id);
|
||||
if (!plugin) return;
|
||||
|
||||
if (plugin.status === 'loading') {
|
||||
await this.initPlugin(plugin);
|
||||
}
|
||||
|
||||
this.togglePluginStatus(plugin);
|
||||
});
|
||||
});
|
||||
|
||||
const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button');
|
||||
if (minimizeButton && this.devOverlay) {
|
||||
minimizeButton.addEventListener('click', () => {
|
||||
this.toggleOverlay(false);
|
||||
this.toggleMinimizeButton(false);
|
||||
});
|
||||
// Create plugin canvases
|
||||
this.plugins.forEach((plugin) => {
|
||||
if (!this.hasBeenInitialized) {
|
||||
console.log(`Creating plugin canvas for ${plugin.id}`);
|
||||
const pluginCanvas = document.createElement('astro-dev-overlay-plugin-canvas');
|
||||
pluginCanvas.dataset.pluginId = plugin.id;
|
||||
this.shadowRoot?.append(pluginCanvas);
|
||||
}
|
||||
|
||||
const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar');
|
||||
if (devBar) {
|
||||
// On hover:
|
||||
// - If the overlay is hidden, show it after the hover delay
|
||||
// - If the overlay is visible, show the minimize button after the hover delay
|
||||
(['mouseenter', 'focusin'] as const).forEach((event) => {
|
||||
devBar.addEventListener(event, () => {
|
||||
if (this.hoverTimeout) {
|
||||
window.clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
this.togglePluginStatus(plugin, plugin.active);
|
||||
});
|
||||
|
||||
if (this.isHidden()) {
|
||||
this.hoverTimeout = window.setTimeout(() => {
|
||||
this.toggleOverlay(true);
|
||||
}, HOVER_DELAY);
|
||||
} else {
|
||||
this.hoverTimeout = window.setTimeout(() => {
|
||||
this.toggleMinimizeButton(true);
|
||||
}, HOVER_DELAY);
|
||||
}
|
||||
});
|
||||
});
|
||||
// Init plugin lazily - This is safe to do here because only plugins that are not initialized yet will be affected
|
||||
if ('requestIdleCallback' in window) {
|
||||
window.requestIdleCallback(async () => {
|
||||
await this.initAllPlugins();
|
||||
});
|
||||
} else {
|
||||
// Fallback to setTimeout for.. Safari...
|
||||
setTimeout(async () => {
|
||||
await this.initAllPlugins();
|
||||
}, 200);
|
||||
}
|
||||
|
||||
// On unhover:
|
||||
// - Reset every timeout, as to avoid showing the overlay/minimize button when the user didn't really want to hover
|
||||
// - If the overlay is visible, hide the minimize button after the hover delay
|
||||
devBar.addEventListener('mouseleave', () => {
|
||||
this.hasBeenInitialized = true;
|
||||
}
|
||||
|
||||
attachEvents() {
|
||||
const items = this.shadowRoot.querySelectorAll<HTMLDivElement>('.item');
|
||||
items.forEach((item) => {
|
||||
item.addEventListener('click', async (e) => {
|
||||
const target = e.currentTarget;
|
||||
if (!target || !(target instanceof HTMLElement)) return;
|
||||
|
||||
const id = target.dataset.pluginId;
|
||||
if (!id) return;
|
||||
|
||||
const plugin = this.getPluginById(id);
|
||||
if (!plugin) return;
|
||||
|
||||
if (plugin.status === 'loading') {
|
||||
await this.initPlugin(plugin);
|
||||
}
|
||||
|
||||
this.togglePluginStatus(plugin);
|
||||
});
|
||||
});
|
||||
|
||||
const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button');
|
||||
if (minimizeButton && this.devOverlay) {
|
||||
minimizeButton.addEventListener('click', () => {
|
||||
this.toggleOverlay(false);
|
||||
this.toggleMinimizeButton(false);
|
||||
});
|
||||
}
|
||||
|
||||
const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar');
|
||||
if (devBar) {
|
||||
// On hover:
|
||||
// - If the overlay is hidden, show it after the hover delay
|
||||
// - If the overlay is visible, show the minimize button after the hover delay
|
||||
(['mouseenter', 'focusin'] as const).forEach((event) => {
|
||||
devBar.addEventListener(event, () => {
|
||||
if (this.hoverTimeout) {
|
||||
window.clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
|
||||
if (!this.isHidden()) {
|
||||
if (this.isHidden()) {
|
||||
this.hoverTimeout = window.setTimeout(() => {
|
||||
this.toggleMinimizeButton(false);
|
||||
}, HOVER_DELAY);
|
||||
this.toggleOverlay(true);
|
||||
}, this.HOVER_DELAY);
|
||||
} else {
|
||||
this.hoverTimeout = window.setTimeout(() => {
|
||||
this.toggleMinimizeButton(true);
|
||||
}, this.HOVER_DELAY);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// On click, show the overlay if it's hidden, it's likely the user wants to interact with it
|
||||
devBar.addEventListener('click', () => {
|
||||
// On unhover:
|
||||
// - Reset every timeout, as to avoid showing the overlay/minimize button when the user didn't really want to hover
|
||||
// - If the overlay is visible, hide the minimize button after the hover delay
|
||||
devBar.addEventListener('mouseleave', () => {
|
||||
if (this.hoverTimeout) {
|
||||
window.clearTimeout(this.hoverTimeout);
|
||||
}
|
||||
|
||||
if (!this.isHidden()) {
|
||||
this.hoverTimeout = window.setTimeout(() => {
|
||||
this.toggleMinimizeButton(false);
|
||||
}, this.HOVER_DELAY);
|
||||
}
|
||||
});
|
||||
|
||||
// On click, show the overlay if it's hidden, it's likely the user wants to interact with it
|
||||
devBar.addEventListener('click', () => {
|
||||
if (!this.isHidden()) return;
|
||||
this.toggleOverlay(true);
|
||||
});
|
||||
|
||||
devBar.addEventListener('keyup', (event) => {
|
||||
if (event.code === 'Space' || event.code === 'Enter') {
|
||||
if (!this.isHidden()) return;
|
||||
this.toggleOverlay(true);
|
||||
});
|
||||
|
||||
devBar.addEventListener('keyup', (event) => {
|
||||
if (event.code === 'Space' || event.code === 'Enter') {
|
||||
if (!this.isHidden()) return;
|
||||
this.toggleOverlay(true);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async initAllPlugins() {
|
||||
await Promise.all(
|
||||
plugins
|
||||
.filter((plugin) => plugin.status === 'loading')
|
||||
.map((plugin) => this.initPlugin(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
async initPlugin(plugin: DevOverlayPlugin) {
|
||||
if (plugin.status === 'ready') return;
|
||||
|
||||
const shadowRoot = this.getPluginCanvasById(plugin.id)!.shadowRoot!;
|
||||
|
||||
try {
|
||||
console.info(`Initing plugin ${plugin.id}`);
|
||||
await plugin.init?.(shadowRoot, plugin.eventTarget);
|
||||
plugin.status = 'ready';
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:init`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to init plugin ${plugin.id}, error: ${e}`);
|
||||
plugin.status = 'error';
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
getPluginTemplate(plugin: DevOverlayPlugin) {
|
||||
return `<button class="item" data-plugin-id="${plugin.id}">
|
||||
async initAllPlugins() {
|
||||
await Promise.all(
|
||||
this.plugins
|
||||
.filter((plugin) => plugin.status === 'loading')
|
||||
.map((plugin) => this.initPlugin(plugin))
|
||||
);
|
||||
}
|
||||
|
||||
async initPlugin(plugin: DevOverlayPlugin) {
|
||||
if (plugin.status === 'ready') return;
|
||||
|
||||
const shadowRoot = this.getPluginCanvasById(plugin.id)!.shadowRoot!;
|
||||
|
||||
try {
|
||||
console.info(`Initializing plugin ${plugin.id}`);
|
||||
await plugin.init?.(shadowRoot, plugin.eventTarget);
|
||||
plugin.status = 'ready';
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:init`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`Failed to init plugin ${plugin.id}, error: ${e}`);
|
||||
plugin.status = 'error';
|
||||
}
|
||||
}
|
||||
|
||||
getPluginTemplate(plugin: DevOverlayPlugin) {
|
||||
return `<button class="item" data-plugin-id="${plugin.id}">
|
||||
<div class="icon">${this.getPluginIcon(plugin.icon)}<div class="notification"></div></div>
|
||||
<span class="sr-only">${plugin.name}</span>
|
||||
</button>`;
|
||||
}
|
||||
|
||||
getPluginIcon(icon: Icon) {
|
||||
if (isDefinedIcon(icon)) {
|
||||
return getIconElement(icon)?.outerHTML;
|
||||
}
|
||||
|
||||
getPluginIcon(icon: Icon) {
|
||||
if (isDefinedIcon(icon)) {
|
||||
return getIconElement(icon)?.outerHTML;
|
||||
}
|
||||
return icon;
|
||||
}
|
||||
|
||||
return icon;
|
||||
}
|
||||
getPluginById(id: string) {
|
||||
return this.plugins.find((plugin) => plugin.id === id);
|
||||
}
|
||||
|
||||
getPluginById(id: string) {
|
||||
return plugins.find((plugin) => plugin.id === id);
|
||||
}
|
||||
getPluginCanvasById(id: string) {
|
||||
return this.shadowRoot.querySelector(`astro-dev-overlay-plugin-canvas[data-plugin-id="${id}"]`);
|
||||
}
|
||||
|
||||
getPluginCanvasById(id: string) {
|
||||
return this.shadowRoot.querySelector(
|
||||
`astro-dev-overlay-plugin-canvas[data-plugin-id="${id}"]`
|
||||
);
|
||||
}
|
||||
togglePluginStatus(plugin: DevOverlayPlugin, status?: boolean) {
|
||||
plugin.active = status ?? !plugin.active;
|
||||
const target = this.shadowRoot.querySelector(`[data-plugin-id="${plugin.id}"]`);
|
||||
if (!target) return;
|
||||
target.classList.toggle('active', plugin.active);
|
||||
this.getPluginCanvasById(plugin.id)?.toggleAttribute('data-active', plugin.active);
|
||||
|
||||
togglePluginStatus(plugin: DevOverlayPlugin, status?: boolean) {
|
||||
plugin.active = status ?? !plugin.active;
|
||||
const target = this.shadowRoot.querySelector(`[data-plugin-id="${plugin.id}"]`);
|
||||
if (!target) return;
|
||||
target.classList.toggle('active', plugin.active);
|
||||
this.getPluginCanvasById(plugin.id)?.toggleAttribute('data-active', plugin.active);
|
||||
plugin.eventTarget.dispatchEvent(
|
||||
new CustomEvent('plugin-toggle', {
|
||||
detail: {
|
||||
state: plugin.active,
|
||||
plugin,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
plugin.eventTarget.dispatchEvent(
|
||||
new CustomEvent('plugin-toggle', {
|
||||
detail: {
|
||||
state: plugin.active,
|
||||
plugin,
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:toggle`, { state: plugin.active });
|
||||
}
|
||||
}
|
||||
|
||||
toggleMinimizeButton(newStatus?: boolean) {
|
||||
const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button');
|
||||
if (!minimizeButton) return;
|
||||
|
||||
if (newStatus !== undefined) {
|
||||
if (newStatus === true) {
|
||||
minimizeButton.removeAttribute('inert');
|
||||
minimizeButton.style.opacity = '1';
|
||||
} else {
|
||||
minimizeButton.setAttribute('inert', '');
|
||||
minimizeButton.style.opacity = '0';
|
||||
}
|
||||
} else {
|
||||
minimizeButton.toggleAttribute('inert');
|
||||
minimizeButton.style.opacity = minimizeButton.hasAttribute('inert') ? '0' : '1';
|
||||
}
|
||||
}
|
||||
|
||||
toggleOverlay(newStatus?: boolean) {
|
||||
const barContainer = this.shadowRoot.querySelector<HTMLDivElement>('#bar-container');
|
||||
const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar');
|
||||
|
||||
if (newStatus !== undefined) {
|
||||
if (newStatus === true) {
|
||||
this.devOverlay?.removeAttribute('data-hidden');
|
||||
barContainer?.removeAttribute('inert');
|
||||
devBar?.removeAttribute('tabindex');
|
||||
} else {
|
||||
this.devOverlay?.setAttribute('data-hidden', '');
|
||||
barContainer?.setAttribute('inert', '');
|
||||
devBar?.setAttribute('tabindex', '0');
|
||||
}
|
||||
} else {
|
||||
this.devOverlay?.toggleAttribute('data-hidden');
|
||||
barContainer?.toggleAttribute('inert');
|
||||
if (this.isHidden()) {
|
||||
devBar?.setAttribute('tabindex', '0');
|
||||
} else {
|
||||
devBar?.removeAttribute('tabindex');
|
||||
}
|
||||
}
|
||||
if (import.meta.hot) {
|
||||
import.meta.hot.send(`${WS_EVENT_NAME}:${plugin.id}:toggle`, { state: plugin.active });
|
||||
}
|
||||
}
|
||||
|
||||
class DevOverlayCanvas extends HTMLElement {
|
||||
shadowRoot: ShadowRoot;
|
||||
toggleMinimizeButton(newStatus?: boolean) {
|
||||
const minimizeButton = this.shadowRoot.querySelector<HTMLDivElement>('#minimize-button');
|
||||
if (!minimizeButton) return;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
|
||||
// connect component
|
||||
async connectedCallback() {
|
||||
this.shadowRoot.innerHTML = ``;
|
||||
if (newStatus !== undefined) {
|
||||
if (newStatus === true) {
|
||||
minimizeButton.removeAttribute('inert');
|
||||
minimizeButton.style.opacity = '1';
|
||||
} else {
|
||||
minimizeButton.setAttribute('inert', '');
|
||||
minimizeButton.style.opacity = '0';
|
||||
}
|
||||
} else {
|
||||
minimizeButton.toggleAttribute('inert');
|
||||
minimizeButton.style.opacity = minimizeButton.hasAttribute('inert') ? '0' : '1';
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define('astro-dev-overlay', AstroDevOverlay);
|
||||
customElements.define('astro-dev-overlay-window', DevOverlayWindow);
|
||||
customElements.define('astro-dev-overlay-plugin-canvas', DevOverlayCanvas);
|
||||
customElements.define('astro-dev-overlay-tooltip', DevOverlayTooltip);
|
||||
customElements.define('astro-dev-overlay-highlight', DevOverlayHighlight);
|
||||
customElements.define('astro-dev-overlay-card', DevOverlayCard);
|
||||
toggleOverlay(newStatus?: boolean) {
|
||||
const barContainer = this.shadowRoot.querySelector<HTMLDivElement>('#bar-container');
|
||||
const devBar = this.shadowRoot.querySelector<HTMLDivElement>('#dev-bar');
|
||||
|
||||
const overlay = document.createElement('astro-dev-overlay');
|
||||
overlay.style.zIndex = '999999';
|
||||
document.body.append(overlay);
|
||||
if (newStatus !== undefined) {
|
||||
if (newStatus === true) {
|
||||
this.devOverlay?.removeAttribute('data-hidden');
|
||||
barContainer?.removeAttribute('inert');
|
||||
devBar?.removeAttribute('tabindex');
|
||||
} else {
|
||||
this.devOverlay?.setAttribute('data-hidden', '');
|
||||
barContainer?.setAttribute('inert', '');
|
||||
devBar?.setAttribute('tabindex', '0');
|
||||
}
|
||||
} else {
|
||||
this.devOverlay?.toggleAttribute('data-hidden');
|
||||
barContainer?.toggleAttribute('inert');
|
||||
if (this.isHidden()) {
|
||||
devBar?.setAttribute('tabindex', '0');
|
||||
} else {
|
||||
devBar?.removeAttribute('tabindex');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create plugin canvases
|
||||
plugins.forEach((plugin) => {
|
||||
const pluginCanvas = document.createElement('astro-dev-overlay-plugin-canvas');
|
||||
pluginCanvas.dataset.pluginId = plugin.id;
|
||||
overlay.shadowRoot?.append(pluginCanvas);
|
||||
});
|
||||
});
|
||||
export class DevOverlayCanvas extends HTMLElement {
|
||||
shadowRoot: ShadowRoot;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.shadowRoot = this.attachShadow({ mode: 'open' });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,12 +1,11 @@
|
|||
import type { DevOverlayPlugin } from '../../../../@types/astro.js';
|
||||
import type { DevOverlayWindow } from '../ui-library/window.js';
|
||||
|
||||
export default {
|
||||
id: 'astro',
|
||||
name: 'Astro',
|
||||
icon: 'astro:logo',
|
||||
init(canvas) {
|
||||
const astroWindow = document.createElement('astro-dev-overlay-window') as DevOverlayWindow;
|
||||
const astroWindow = document.createElement('astro-dev-overlay-window');
|
||||
|
||||
astroWindow.windowTitle = 'Astro';
|
||||
astroWindow.windowIcon = 'astro:logo';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { DevOverlayPlugin } from '../../../../@types/astro.js';
|
||||
import type { DevOverlayHighlight } from '../ui-library/highlight.js';
|
||||
import type { DevOverlayTooltip } from '../ui-library/tooltip.js';
|
||||
import { attachTooltipToHighlight, createHighlight, positionHighlight } from './utils/highlight.js';
|
||||
|
||||
const icon =
|
||||
|
@ -26,20 +25,39 @@ export default {
|
|||
init(canvas, eventTarget) {
|
||||
let audits: { highlightElement: DevOverlayHighlight; auditedElement: HTMLElement }[] = [];
|
||||
|
||||
selectorBasedRules.forEach((rule) => {
|
||||
document.querySelectorAll(rule.selector).forEach((el) => {
|
||||
createAuditProblem(rule, el);
|
||||
});
|
||||
});
|
||||
lint();
|
||||
|
||||
if (audits.length > 0) {
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('plugin-notification', {
|
||||
detail: {
|
||||
state: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
document.addEventListener('astro:after-swap', lint);
|
||||
document.addEventListener('astro:page-load', refreshLintPositions);
|
||||
|
||||
function lint() {
|
||||
audits.forEach(({ highlightElement }) => {
|
||||
highlightElement.remove();
|
||||
});
|
||||
audits = [];
|
||||
|
||||
selectorBasedRules.forEach((rule) => {
|
||||
document.querySelectorAll(rule.selector).forEach((el) => {
|
||||
createAuditProblem(rule, el);
|
||||
});
|
||||
});
|
||||
|
||||
if (audits.length > 0) {
|
||||
eventTarget.dispatchEvent(
|
||||
new CustomEvent('plugin-notification', {
|
||||
detail: {
|
||||
state: true,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function refreshLintPositions() {
|
||||
audits.forEach(({ highlightElement, auditedElement }) => {
|
||||
const rect = auditedElement.getBoundingClientRect();
|
||||
positionHighlight(highlightElement, rect);
|
||||
});
|
||||
}
|
||||
|
||||
function createAuditProblem(rule: AuditRule, originalElement: Element) {
|
||||
|
@ -60,17 +78,12 @@ export default {
|
|||
audits.push({ highlightElement: highlight, auditedElement: originalElement as HTMLElement });
|
||||
|
||||
(['scroll', 'resize'] as const).forEach((event) => {
|
||||
window.addEventListener(event, () => {
|
||||
audits.forEach(({ highlightElement, auditedElement }) => {
|
||||
const newRect = auditedElement.getBoundingClientRect();
|
||||
positionHighlight(highlightElement, newRect);
|
||||
});
|
||||
});
|
||||
window.addEventListener(event, refreshLintPositions);
|
||||
});
|
||||
}
|
||||
|
||||
function buildAuditTooltip(rule: AuditRule) {
|
||||
const tooltip = document.createElement('astro-dev-overlay-tooltip') as DevOverlayTooltip;
|
||||
const tooltip = document.createElement('astro-dev-overlay-tooltip');
|
||||
tooltip.sections = [
|
||||
{
|
||||
icon: 'warning',
|
||||
|
|
|
@ -2,16 +2,21 @@ import type { DevOverlayHighlight } from '../../ui-library/highlight.js';
|
|||
import type { Icon } from '../../ui-library/icons.js';
|
||||
|
||||
export function createHighlight(rect: DOMRect, icon?: Icon) {
|
||||
const highlight = document.createElement('astro-dev-overlay-highlight') as DevOverlayHighlight;
|
||||
const highlight = document.createElement('astro-dev-overlay-highlight');
|
||||
if (icon) highlight.icon = icon;
|
||||
|
||||
highlight.tabIndex = 0;
|
||||
|
||||
positionHighlight(highlight, rect);
|
||||
if (rect.width === 0 || rect.height === 0) {
|
||||
highlight.style.display = 'none';
|
||||
} else {
|
||||
positionHighlight(highlight, rect);
|
||||
}
|
||||
return highlight;
|
||||
}
|
||||
|
||||
export function positionHighlight(highlight: DevOverlayHighlight, rect: DOMRect) {
|
||||
highlight.style.display = 'block';
|
||||
// Make an highlight that is 10px bigger than the element on all sides
|
||||
highlight.style.top = `${Math.max(rect.top + window.scrollY - 10, 0)}px`;
|
||||
highlight.style.left = `${Math.max(rect.left + window.scrollX - 10, 0)}px`;
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import type { DevOverlayMetadata, DevOverlayPlugin } from '../../../../@types/astro.js';
|
||||
import type { DevOverlayHighlight } from '../ui-library/highlight.js';
|
||||
import type { DevOverlayTooltip } from '../ui-library/tooltip.js';
|
||||
import { attachTooltipToHighlight, createHighlight, positionHighlight } from './utils/highlight.js';
|
||||
|
||||
const icon =
|
||||
|
@ -12,9 +11,18 @@ export default {
|
|||
icon: icon,
|
||||
init(canvas) {
|
||||
let islandsOverlays: { highlightElement: DevOverlayHighlight; island: HTMLElement }[] = [];
|
||||
|
||||
addIslandsOverlay();
|
||||
|
||||
document.addEventListener('astro:after-swap', addIslandsOverlay);
|
||||
document.addEventListener('astro:page-load', refreshIslandsOverlayPositions);
|
||||
|
||||
function addIslandsOverlay() {
|
||||
islandsOverlays.forEach(({ highlightElement }) => {
|
||||
highlightElement.remove();
|
||||
});
|
||||
islandsOverlays = [];
|
||||
|
||||
const islands = document.querySelectorAll<HTMLElement>('astro-island');
|
||||
|
||||
islands.forEach((island) => {
|
||||
|
@ -22,6 +30,7 @@ export default {
|
|||
const islandElement = (island.children[0] as HTMLElement) || island;
|
||||
|
||||
// If the island is hidden, don't show an overlay on it
|
||||
// TODO: For `client:only` islands, it might not have finished loading yet, so we should wait for that
|
||||
if (islandElement.offsetParent === null || computedStyle.display === 'none') {
|
||||
return;
|
||||
}
|
||||
|
@ -36,17 +45,19 @@ export default {
|
|||
});
|
||||
|
||||
(['scroll', 'resize'] as const).forEach((event) => {
|
||||
window.addEventListener(event, () => {
|
||||
islandsOverlays.forEach(({ highlightElement, island: islandElement }) => {
|
||||
const newRect = islandElement.getBoundingClientRect();
|
||||
positionHighlight(highlightElement, newRect);
|
||||
});
|
||||
});
|
||||
window.addEventListener(event, refreshIslandsOverlayPositions);
|
||||
});
|
||||
}
|
||||
|
||||
function refreshIslandsOverlayPositions() {
|
||||
islandsOverlays.forEach(({ highlightElement, island: islandElement }) => {
|
||||
const rect = islandElement.getBoundingClientRect();
|
||||
positionHighlight(highlightElement, rect);
|
||||
});
|
||||
}
|
||||
|
||||
function buildIslandTooltip(island: HTMLElement) {
|
||||
const tooltip = document.createElement('astro-dev-overlay-tooltip') as DevOverlayTooltip;
|
||||
const tooltip = document.createElement('astro-dev-overlay-tooltip');
|
||||
tooltip.sections = [];
|
||||
|
||||
const islandProps = island.getAttribute('props')
|
||||
|
|
|
@ -59,7 +59,7 @@ export class DevOverlayTooltip extends HTMLElement {
|
|||
|
||||
.section-content {
|
||||
max-height: 250px;
|
||||
overflow-y: scroll;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-title {
|
||||
|
|
|
@ -281,7 +281,7 @@ async function getScriptsAndStyles({ pipeline, filePath }: GetScriptsAndStylesPa
|
|||
scripts.add({
|
||||
props: {
|
||||
type: 'module',
|
||||
src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/dev-overlay/overlay.js'),
|
||||
src: await resolveIdToUrl(moduleLoader, 'astro/runtime/client/dev-overlay/entrypoint.js'),
|
||||
},
|
||||
children: '',
|
||||
});
|
||||
|
|
Loading…
Add table
Reference in a new issue