Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/main' into refactor-npm-package-…
Browse files Browse the repository at this point in the history
…process-gh-5869
  • Loading branch information
etrepum committed Apr 23, 2024
2 parents 4001c7f + 0b02af5 commit 5cdcab5
Show file tree
Hide file tree
Showing 23 changed files with 676 additions and 135 deletions.
1 change: 1 addition & 0 deletions packages/lexical-devtools/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ $ npm run dev
- Extension activity log: [chrome://extensions/?activity=eddfjidloofnnmloonifcjkpmfmlblab](chrome://extensions/?activity=eddfjidloofnnmloonifcjkpmfmlblab)
- Status of ServiceWorkers: [chrome://serviceworker-internals/?devtools](chrome://serviceworker-internals/?devtools)
- WXT Framework debugging: `DEBUG_WXT=1 npm run dev`
- If you detach the Dev Tools in a separate window, and press `Cmd+Option+I` while Dev Tools window is focused, you will invoke the Dev Tools for the Dev Tools window.

## Design

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ function EditorsRefreshCTA({tabID, setErrorMessage}: Props) {
);

injectedPegasusService
.refreshLexicalEditorsForTabID()
.refreshLexicalEditors()
.catch((err) => {
setErrorMessage(err.message);
console.error(err);
Expand Down
71 changes: 71 additions & 0 deletions packages/lexical-devtools/src/element-picker/element-overlay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import {BoundingBox, ElementOverlayOptions} from './utils';

export default class ElementOverlay {
overlay: HTMLDivElement;
shadowContainer: HTMLDivElement;
shadowRoot: ShadowRoot;
usingShadowDOM?: boolean;

constructor(options: ElementOverlayOptions) {
this.overlay = document.createElement('div');
this.overlay.className = options.className || '_ext-element-overlay';
this.overlay.style.background =
options.style?.background || 'rgba(250, 240, 202, 0.2)';
this.overlay.style.borderColor = options.style?.borderColor || '#F95738';
this.overlay.style.borderStyle = options.style?.borderStyle || 'solid';
this.overlay.style.borderRadius = options.style?.borderRadius || '1px';
this.overlay.style.borderWidth = options.style?.borderWidth || '1px';
this.overlay.style.boxSizing = options.style?.boxSizing || 'border-box';
this.overlay.style.cursor = options.style?.cursor || 'crosshair';
this.overlay.style.position = options.style?.position || 'absolute';
this.overlay.style.zIndex = options.style?.zIndex || '2147483647';

this.shadowContainer = document.createElement('div');
this.shadowContainer.className = '_ext-element-overlay-container';
this.shadowContainer.style.position = 'absolute';
this.shadowContainer.style.top = '0px';
this.shadowContainer.style.left = '0px';
this.shadowRoot = this.shadowContainer.attachShadow({mode: 'open'});
}

addToDOM(parent: Node, useShadowDOM: boolean) {
this.usingShadowDOM = useShadowDOM;
if (useShadowDOM) {
parent.insertBefore(this.shadowContainer, parent.firstChild);
this.shadowRoot.appendChild(this.overlay);
} else {
parent.appendChild(this.overlay);
}
}

removeFromDOM() {
this.setBounds({height: 0, width: 0, x: 0, y: 0});
this.overlay.remove();
if (this.usingShadowDOM) {
this.shadowContainer.remove();
}
}

captureCursor() {
this.overlay.style.pointerEvents = 'auto';
}

ignoreCursor() {
this.overlay.style.pointerEvents = 'none';
}

setBounds({x, y, width, height}: BoundingBox) {
this.overlay.style.left = x + 'px';
this.overlay.style.top = y + 'px';
this.overlay.style.width = width + 'px';
this.overlay.style.height = height + 'px';
}
}
131 changes: 131 additions & 0 deletions packages/lexical-devtools/src/element-picker/element-picker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import ElementOverlay from './element-overlay';
import {ElementOverlayOptions, getElementBounds} from './utils';

type ElementCallback<T> = (el: HTMLElement) => T;
type ElementPickerOptions = {
parentElement?: Node;
useShadowDOM?: boolean;
onClick?: ElementCallback<void>;
onHover?: ElementCallback<void>;
elementFilter?: ElementCallback<boolean | HTMLElement>;
};

export default class ElementPicker {
private overlay: ElementOverlay;
private active: boolean;
private options?: ElementPickerOptions;
private target?: HTMLElement;
private mouseX?: number;
private mouseY?: number;
private tickReq?: number;

constructor(overlayOptions?: ElementOverlayOptions) {
this.active = false;
this.overlay = new ElementOverlay(overlayOptions ?? {});
}

start(options: ElementPickerOptions): boolean {
if (this.active) {
return false;
}

this.active = true;
this.options = options;
document.addEventListener('mousemove', this.handleMouseMove, true);
document.addEventListener('click', this.handleClick, true);

this.overlay.addToDOM(
options.parentElement ?? document.body,
options.useShadowDOM ?? true,
);

this.tick();

return true;
}

stop() {
this.active = false;
this.options = undefined;
document.removeEventListener('mousemove', this.handleMouseMove, true);
document.removeEventListener('click', this.handleClick, true);

this.overlay.removeFromDOM();
this.target = undefined;
this.mouseX = undefined;
this.mouseY = undefined;

if (this.tickReq) {
window.cancelAnimationFrame(this.tickReq);
}
}

private handleMouseMove = (event: MouseEvent) => {
this.mouseX = event.clientX;
this.mouseY = event.clientY;
};

private handleClick = (event: MouseEvent) => {
if (this.target && this.options?.onClick) {
this.options.onClick(this.target);
}
event.preventDefault();
};

private tick = () => {
this.updateTarget();
this.tickReq = window.requestAnimationFrame(this.tick);
};

private updateTarget() {
if (this.mouseX === undefined || this.mouseY === undefined) {
return;
}

// Peek through the overlay to find the new target
this.overlay.ignoreCursor();
const elAtCursor = document.elementFromPoint(this.mouseX, this.mouseY);
let newTarget = elAtCursor as HTMLElement;
this.overlay.captureCursor();

// If the target hasn't changed, there's nothing to do
if (!newTarget || newTarget === this.target) {
return;
}

// If we have an element filter and the new target doesn't match,
// clear out the target
if (this.options?.elementFilter) {
const filterResult = this.options.elementFilter(newTarget);
if (filterResult === false) {
this.target = undefined;
this.overlay.setBounds({height: 0, width: 0, x: 0, y: 0});
return;
}
// If the filter returns an element, use that element as new target
else if (typeof filterResult !== 'boolean') {
if (filterResult === this.target) {
return;
}
newTarget = filterResult;
}
}

this.target = newTarget;

const bounds = getElementBounds(newTarget);
this.overlay.setBounds(bounds);

if (this.options?.onHover) {
this.options.onHover(newTarget);
}
}
}
11 changes: 11 additions & 0 deletions packages/lexical-devtools/src/element-picker/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

import ElementPicker from './element-picker';

export {ElementPicker};
41 changes: 41 additions & 0 deletions packages/lexical-devtools/src/element-picker/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/

export interface BoundingBox {
x: number;
y: number;
width: number;
height: number;
}

export interface ElementOverlayStyleOptions {
background?: string;
borderColor?: string;
borderStyle?: string;
borderRadius?: string;
borderWidth?: string;
boxSizing?: string;
cursor?: string;
position?: string;
zIndex?: string;
}

export type ElementOverlayOptions = {
className?: string;
style?: ElementOverlayStyleOptions;
};

export const getElementBounds = (el: HTMLElement): BoundingBox => {
const rect = el.getBoundingClientRect();
return {
height: el.offsetHeight,
width: el.offsetWidth,
x: window.pageXOffset + rect.left,
y: window.pageYOffset + rect.top,
};
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
import type {Tabs} from 'wxt/browser';
import type {StoreApi} from 'zustand';

import {IS_FIREFOX} from 'shared/environment';

import {ExtensionState} from '../../store';

export default class ActionIconWatchdog {
private constructor(
private readonly extensionStore: StoreApi<ExtensionState>,
) {}

static async start(store: StoreApi<ExtensionState>) {
return new ActionIconWatchdog(store).init();
}

async init() {
const tabs = await browser.tabs.query({});
await Promise.all(
tabs.map(this.checkAndHandleRestrictedPageIfSo.bind(this)),
);

browser.tabs.onCreated.addListener((tab) => {
this.checkAndHandleRestrictedPageIfSo(tab);
});

// Listen to URL changes on the active tab and update the DevTools icon.
browser.tabs.onUpdated.addListener(this.handleTabsUpdatedEvent.bind(this));
}

private async setIcon(
lexicalBuildType: 'restricted' | 'enabled',
tabId: number,
) {
const action = IS_FIREFOX ? browser.browserAction : browser.action;

await action.setIcon({
path: {
'128': browser.runtime.getURL(
lexicalBuildType === 'enabled'
? '/icon/128.png'
: '/icon/128-restricted.png',
),
'16': browser.runtime.getURL(
lexicalBuildType === 'enabled'
? '/icon/16.png'
: '/icon/16-restricted.png',
),
'32': browser.runtime.getURL(
lexicalBuildType === 'enabled'
? '/icon/32.png'
: '/icon/32-restricted.png',
),
'48': browser.runtime.getURL(
lexicalBuildType === 'enabled'
? '/icon/48.png'
: '/icon/48-restricted.png',
),
},
tabId: tabId,
});

if (lexicalBuildType === 'restricted') {
this.extensionStore.getState().markTabAsRestricted(tabId);
}
}

private handleTabsUpdatedEvent(
tabId: number,
_changeInfo: unknown,
tab: Tabs.Tab,
): void {
this.checkAndHandleRestrictedPageIfSo(tab);
}

private isRestrictedBrowserPage(url: string | undefined) {
return (
!url || ['chrome:', 'about:', 'file:'].includes(new URL(url).protocol)
);
}

private async checkAndHandleRestrictedPageIfSo(tab: Tabs.Tab) {
if (tab.id == null) {
return;
}

if (tab.id == null || this.isRestrictedBrowserPage(tab.url)) {
return this.setIcon('restricted', tab.id);
}

return this.setIcon('enabled', tab.id);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
import {registerRPCService} from '@webext-pegasus/rpc';
import {initPegasusTransport} from '@webext-pegasus/transport/background';

import {initExtensionStoreBackend} from '../../store.ts';
import {
initExtensionStoreBackend,
useExtensionStore as extensionStore,
} from '../../store.ts';
import ActionIconWatchdog from './ActionIconWatchdog.ts';
import {getTabIDService} from './getTabIDService';

export default defineBackground(() => {
Expand All @@ -20,4 +24,6 @@ export default defineBackground(() => {
// Store initialization so other extension surfaces can use it
// as all changes go through background SW
initExtensionStoreBackend();

ActionIconWatchdog.start(extensionStore).catch(console.error);
});
Loading

0 comments on commit 5cdcab5

Please sign in to comment.