0
Fork 0
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:
Erika 2023-10-31 23:35:32 +01:00 committed by GitHub
parent 463e03633e
commit 262cef2487
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 399 additions and 306 deletions

View file

@ -0,0 +1,5 @@
---
'astro': patch
---
Fix Dev Overlay not working properly when view transitions are enabled

View file

@ -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;
}
}

View 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);
});
});

View file

@ -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' });
}
}

View file

@ -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';

View file

@ -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',

View file

@ -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`;

View file

@ -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')

View file

@ -59,7 +59,7 @@ export class DevOverlayTooltip extends HTMLElement {
.section-content {
max-height: 250px;
overflow-y: scroll;
overflow-y: auto;
}
.modal-title {

View file

@ -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: '',
});