diff --git a/packages/lexical-devtools/README.md b/packages/lexical-devtools/README.md index 7e1b7b579d8..74d27cb9925 100644 --- a/packages/lexical-devtools/README.md +++ b/packages/lexical-devtools/README.md @@ -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 diff --git a/packages/lexical-devtools/src/components/EditorsRefreshCTA.tsx b/packages/lexical-devtools/src/components/EditorsRefreshCTA.tsx index 22bb8d1373b..01986bf2b88 100644 --- a/packages/lexical-devtools/src/components/EditorsRefreshCTA.tsx +++ b/packages/lexical-devtools/src/components/EditorsRefreshCTA.tsx @@ -30,7 +30,7 @@ function EditorsRefreshCTA({tabID, setErrorMessage}: Props) { ); injectedPegasusService - .refreshLexicalEditorsForTabID() + .refreshLexicalEditors() .catch((err) => { setErrorMessage(err.message); console.error(err); diff --git a/packages/lexical-devtools/src/element-picker/element-overlay.ts b/packages/lexical-devtools/src/element-picker/element-overlay.ts new file mode 100644 index 00000000000..72d14b49c08 --- /dev/null +++ b/packages/lexical-devtools/src/element-picker/element-overlay.ts @@ -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'; + } +} diff --git a/packages/lexical-devtools/src/element-picker/element-picker.ts b/packages/lexical-devtools/src/element-picker/element-picker.ts new file mode 100644 index 00000000000..ae03dbec11e --- /dev/null +++ b/packages/lexical-devtools/src/element-picker/element-picker.ts @@ -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 = (el: HTMLElement) => T; +type ElementPickerOptions = { + parentElement?: Node; + useShadowDOM?: boolean; + onClick?: ElementCallback; + onHover?: ElementCallback; + elementFilter?: ElementCallback; +}; + +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); + } + } +} diff --git a/packages/lexical-devtools/src/element-picker/index.ts b/packages/lexical-devtools/src/element-picker/index.ts new file mode 100644 index 00000000000..bad087297d1 --- /dev/null +++ b/packages/lexical-devtools/src/element-picker/index.ts @@ -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}; diff --git a/packages/lexical-devtools/src/element-picker/utils.ts b/packages/lexical-devtools/src/element-picker/utils.ts new file mode 100644 index 00000000000..9a71d4d7bf5 --- /dev/null +++ b/packages/lexical-devtools/src/element-picker/utils.ts @@ -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, + }; +}; diff --git a/packages/lexical-devtools/src/entrypoints/background/ActionIconWatchdog.ts b/packages/lexical-devtools/src/entrypoints/background/ActionIconWatchdog.ts new file mode 100644 index 00000000000..124d67f54b7 --- /dev/null +++ b/packages/lexical-devtools/src/entrypoints/background/ActionIconWatchdog.ts @@ -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, + ) {} + + static async start(store: StoreApi) { + 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); + } +} diff --git a/packages/lexical-devtools/src/entrypoints/background/index.ts b/packages/lexical-devtools/src/entrypoints/background/index.ts index d0c1de4e14e..4dd130799d4 100644 --- a/packages/lexical-devtools/src/entrypoints/background/index.ts +++ b/packages/lexical-devtools/src/entrypoints/background/index.ts @@ -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(() => { @@ -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); }); diff --git a/packages/lexical-devtools/src/entrypoints/devtools-panel/App.tsx b/packages/lexical-devtools/src/entrypoints/devtools-panel/App.tsx index 6eaf81c6e8b..e6cc9776725 100644 --- a/packages/lexical-devtools/src/entrypoints/devtools-panel/App.tsx +++ b/packages/lexical-devtools/src/entrypoints/devtools-panel/App.tsx @@ -6,33 +6,25 @@ * */ -import type {IInjectedPegasusService} from '../injected/InjectedPegasusService'; -import type {EditorState} from 'lexical'; - import './App.css'; import { - Accordion, - AccordionButton, - AccordionIcon, - AccordionItem, - AccordionPanel, Alert, AlertIcon, Box, + ButtonGroup, Flex, Spacer, } from '@chakra-ui/react'; -import {TreeView} from '@lexical/devtools-core'; -import {getRPCService} from '@webext-pegasus/rpc'; import * as React from 'react'; -import {useMemo, useState} from 'react'; +import {useState} from 'react'; import lexicalLogo from '@/public/lexical.svg'; import EditorsRefreshCTA from '../../components/EditorsRefreshCTA'; import {useExtensionStore} from '../../store'; -import {SerializedRawEditorState} from '../../types'; +import {EditorInspectorButton} from './components/EditorInspectorButton'; +import {EditorsList} from './components/EditorsList'; interface Props { tabID: number; @@ -45,31 +37,22 @@ function App({tabID}: Props) { const states = lexicalState[tabID] ?? {}; const lexicalCount = Object.keys(states ?? {}).length; - const injectedPegasusService = useMemo( - () => - getRPCService('InjectedPegasusService', { - context: 'window', - tabId: tabID, - }), - [tabID], - ); - return ( <> - - - Lexical logo + + - + + - - + {states === undefined ? ( Loading... ) : ( @@ -79,8 +62,17 @@ function App({tabID}: Props) { )} - - + + + + Lexical logo + {errorMessage !== '' ? ( @@ -89,50 +81,7 @@ function App({tabID}: Props) { {lexicalCount > 0 ? ( - - {Object.entries(states).map(([key, state]) => ( - -

- - - ID: {key} - - - -

- - - injectedPegasusService - .setEditorReadOnly(key, isReadonly) - .catch((e) => setErrorMessage(e.stack)) - } - editorState={state as EditorState} - setEditorState={(editorState) => - injectedPegasusService - .setEditorState( - key, - editorState as SerializedRawEditorState, - ) - .catch((e) => setErrorMessage(e.stack)) - } - generateContent={(exportDOM) => - injectedPegasusService.generateTreeViewContent( - key, - exportDOM, - ) - } - /> - -
- ))} -
+ ) : ( diff --git a/packages/lexical-devtools/src/entrypoints/devtools-panel/components/EditorInspectorButton.tsx b/packages/lexical-devtools/src/entrypoints/devtools-panel/components/EditorInspectorButton.tsx new file mode 100644 index 00000000000..d1019b92303 --- /dev/null +++ b/packages/lexical-devtools/src/entrypoints/devtools-panel/components/EditorInspectorButton.tsx @@ -0,0 +1,46 @@ +/** + * 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 {IconButton, Image} from '@chakra-ui/react'; +import {getRPCService} from '@webext-pegasus/rpc'; +import * as React from 'react'; + +import {IInjectedPegasusService} from '../../injected/InjectedPegasusService'; + +interface Props { + tabID: number; + setErrorMessage: (value: string) => void; +} + +export function EditorInspectorButton({tabID, setErrorMessage}: Props) { + const handleClick = () => { + const injectedPegasusService = getRPCService( + 'InjectedPegasusService', + {context: 'window', tabId: tabID}, + ); + + injectedPegasusService + .refreshLexicalEditors() + .then(() => injectedPegasusService.toggleEditorPicker()) + .catch((err) => { + setErrorMessage(err.message); + console.error(err); + }); + }; + + return ( + } + /> + ); +} diff --git a/packages/lexical-devtools/src/entrypoints/devtools-panel/components/EditorsList.tsx b/packages/lexical-devtools/src/entrypoints/devtools-panel/components/EditorsList.tsx new file mode 100644 index 00000000000..865062d456d --- /dev/null +++ b/packages/lexical-devtools/src/entrypoints/devtools-panel/components/EditorsList.tsx @@ -0,0 +1,102 @@ +/** + * 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 {IInjectedPegasusService} from '../../injected/InjectedPegasusService'; +import type {EditorState} from 'lexical'; + +// import './App.css'; +import { + Accordion, + AccordionButton, + AccordionIcon, + AccordionItem, + AccordionPanel, + Box, +} from '@chakra-ui/react'; +import {TreeView} from '@lexical/devtools-core'; +import {getRPCService} from '@webext-pegasus/rpc'; +import * as React from 'react'; +import {useEffect, useMemo, useState} from 'react'; + +import {useExtensionStore} from '../../../store'; +import {SerializedRawEditorState} from '../../../types'; + +interface Props { + tabID: number; + setErrorMessage: (value: string) => void; +} + +export function EditorsList({tabID, setErrorMessage}: Props) { + const [expandedItems, setExpandedItems] = useState([0]); + const {lexicalState, selectedEditorKey} = useExtensionStore(); + const states = lexicalState[tabID] ?? {}; + const selectedEditorIdx = Object.keys(states).findIndex( + (key) => key === selectedEditorKey[tabID], + ); + + useEffect(() => { + if (selectedEditorIdx !== -1) { + setExpandedItems([selectedEditorIdx]); + } + }, [selectedEditorIdx]); + + const injectedPegasusService = useMemo( + () => + getRPCService('InjectedPegasusService', { + context: 'window', + tabId: tabID, + }), + [tabID], + ); + + return ( + + {Object.entries(states).map(([key, state]) => ( + +

+ + + ID: {key} + + + +

+ + + injectedPegasusService + .setEditorReadOnly(key, isReadonly) + .catch((e) => setErrorMessage(e.stack)) + } + editorState={state as EditorState} + setEditorState={(editorState) => + injectedPegasusService + .setEditorState(key, editorState as SerializedRawEditorState) + .catch((e) => setErrorMessage(e.stack)) + } + generateContent={(exportDOM) => + injectedPegasusService.generateTreeViewContent(key, exportDOM) + } + /> + +
+ ))} +
+ ); +} diff --git a/packages/lexical-devtools/src/entrypoints/injected/InjectedPegasusService.ts b/packages/lexical-devtools/src/entrypoints/injected/InjectedPegasusService.ts index 19f900843f4..664006f0fb1 100644 --- a/packages/lexical-devtools/src/entrypoints/injected/InjectedPegasusService.ts +++ b/packages/lexical-devtools/src/entrypoints/injected/InjectedPegasusService.ts @@ -11,16 +11,20 @@ import {IPegasusRPCService, PegasusRPCMessage} from '@webext-pegasus/rpc'; import {LexicalEditor} from 'lexical'; import {StoreApi} from 'zustand'; +import {ElementPicker} from '../../element-picker'; import {readEditorState} from '../../lexicalForExtension'; import {deserializeEditorState} from '../../serializeEditorState'; import {ExtensionState} from '../../store'; import {SerializedRawEditorState} from '../../types'; +import {isLexicalNode} from '../../utils/isLexicalNode'; import scanAndListenForEditors from './scanAndListenForEditors'; import { queryLexicalEditorByKey, queryLexicalNodeByKey, } from './utils/queryLexicalByKey'; +const ELEMENT_PICKER_STYLE = {borderColor: '#0000ff'}; + export type IInjectedPegasusService = InstanceType< typeof InjectedPegasusService >; @@ -28,13 +32,15 @@ export type IInjectedPegasusService = InstanceType< export class InjectedPegasusService implements IPegasusRPCService { + private pickerActive: ElementPicker | null = null; + constructor( private readonly tabID: number, private readonly extensionStore: StoreApi, private readonly commandLog: WeakMap, ) {} - refreshLexicalEditorsForTabID() { + refreshLexicalEditors() { scanAndListenForEditors(this.tabID, this.extensionStore, this.commandLog); } @@ -78,4 +84,40 @@ export class InjectedPegasusService editor.setEditorState(deserializeEditorState(editorState)); } + + toggleEditorPicker(): void { + if (this.pickerActive != null) { + this.pickerActive?.stop(); + this.pickerActive = null; + + return; + } + + this.pickerActive = new ElementPicker({style: ELEMENT_PICKER_STYLE}); + this.pickerActive.start({ + elementFilter: (el) => { + let parent: HTMLElement | null = el; + while (parent != null && parent.tagName !== 'BODY') { + if ('__lexicalEditor' in parent) { + return parent; + } + parent = parent.parentElement; + } + + return false; + }, + + onClick: (el) => { + this.pickerActive?.stop(); + this.pickerActive = null; + if (isLexicalNode(el)) { + this.extensionStore + .getState() + .setSelectedEditorKey(this.tabID, el.__lexicalEditor.getKey()); + } else { + console.warn('Selected Element is not a Lexical node'); + } + }, + }); + } } diff --git a/packages/lexical-devtools/src/entrypoints/injected/utils/queryLexicalNodes.ts b/packages/lexical-devtools/src/entrypoints/injected/utils/queryLexicalNodes.ts index bb635fa5a6f..4df2b4b350e 100644 --- a/packages/lexical-devtools/src/entrypoints/injected/utils/queryLexicalNodes.ts +++ b/packages/lexical-devtools/src/entrypoints/injected/utils/queryLexicalNodes.ts @@ -6,15 +6,10 @@ * */ import {LexicalHTMLElement} from '../../../types'; +import {isLexicalNode} from '../../../utils/isLexicalNode'; export default function queryLexicalNodes(): LexicalHTMLElement[] { return Array.from( document.querySelectorAll('div[data-lexical-editor]'), ).filter(isLexicalNode); } - -function isLexicalNode( - node: LexicalHTMLElement | Element, -): node is LexicalHTMLElement { - return (node as LexicalHTMLElement).__lexicalEditor !== undefined; -} diff --git a/packages/lexical-devtools/src/entrypoints/popup/App.css b/packages/lexical-devtools/src/entrypoints/popup/App.css index 72a3cbc4bf1..e6bd89156df 100644 --- a/packages/lexical-devtools/src/entrypoints/popup/App.css +++ b/packages/lexical-devtools/src/entrypoints/popup/App.css @@ -17,17 +17,3 @@ body { margin: 0 auto; padding: 1rem; } - -.logo { - height: 2em; - /* padding: 1.5em; */ - will-change: filter; - transition: filter 300ms; -} -.logo:hover { - filter: drop-shadow(0 0 2em #61dafbaa); -} - -.card { - padding-top: 2em; -} diff --git a/packages/lexical-devtools/src/entrypoints/popup/App.tsx b/packages/lexical-devtools/src/entrypoints/popup/App.tsx index 2887b4f1f04..05733be0c3d 100644 --- a/packages/lexical-devtools/src/entrypoints/popup/App.tsx +++ b/packages/lexical-devtools/src/entrypoints/popup/App.tsx @@ -7,11 +7,10 @@ */ import './App.css'; +import {Box, Flex} from '@chakra-ui/react'; import * as React from 'react'; import {useState} from 'react'; -import lexicalLogo from '@/public/lexical.svg'; - import EditorsRefreshCTA from '../../components/EditorsRefreshCTA'; import {useExtensionStore} from '../../store'; @@ -27,38 +26,46 @@ function App({tabID}: Props) { const lexicalCount = Object.keys(states ?? {}).length; return ( - <> -
- - Lexical logo - -
+ {errorMessage !== '' ? ( -
{errorMessage}
+ + {errorMessage} + ) : null} -
- {states === undefined ? ( - Loading... - ) : ( + + {states === null ? ( - Found {lexicalCount} editor{lexicalCount > 1 ? 's' : ''} on - the page - {lexicalCount > 0 ? ( - <> - {' '} - ✅ -
- Open the developer tools, and "Lexical" tab will appear to the - right. - - ) : null} + This is a restricted browser page. Lexical DevTools cannot access + this page.
+ ) : states === undefined ? ( + Loading... + ) : ( + <> + + Found {lexicalCount} editor + {lexicalCount > 1 || lexicalCount === 0 ? 's' : ''} on the page + {lexicalCount > 0 ? ( + <> + {' '} + ✅ +
+ Open the developer tools, and "Lexical" tab will appear to the + right. + + ) : null} +
+ + + + + )} -

- -

-
- +
+ ); } diff --git a/packages/lexical-devtools/src/lexicalForExtension.ts b/packages/lexical-devtools/src/lexicalForExtension.ts index bdd537f3d1e..3b45ed74f52 100644 --- a/packages/lexical-devtools/src/lexicalForExtension.ts +++ b/packages/lexical-devtools/src/lexicalForExtension.ts @@ -59,17 +59,25 @@ export function $getSelection(): null | lexical.BaseSelection { export function $isElementNode( node: lexical.LexicalNode | null | undefined, ): node is lexical.ElementNode { + if (node == null) { + return false; + } + const editor = getActiveEditor(); - const ElementNode = Object.getPrototypeOf( - editor._nodes.get('paragraph')!.klass, - ); + const ParagraphNode = editor._nodes.get('paragraph')!.klass; + const ElementNode = Object.getPrototypeOf(ParagraphNode.prototype); - return node instanceof ElementNode; + // eslint-disable-next-line no-prototype-builtins + return ElementNode.isPrototypeOf(node); } export function $isTextNode( node: lexical.LexicalNode | null | undefined, ): node is lexical.TextNode { + if (node == null) { + return false; + } + const editor = getActiveEditor(); const TextNode = editor._nodes.get('text')!.klass; diff --git a/packages/lexical-devtools/src/public/icon/128-restricted.png b/packages/lexical-devtools/src/public/icon/128-restricted.png new file mode 100644 index 00000000000..781567d2fbe Binary files /dev/null and b/packages/lexical-devtools/src/public/icon/128-restricted.png differ diff --git a/packages/lexical-devtools/src/public/icon/16-restricted.png b/packages/lexical-devtools/src/public/icon/16-restricted.png new file mode 100644 index 00000000000..c97f6ffce25 Binary files /dev/null and b/packages/lexical-devtools/src/public/icon/16-restricted.png differ diff --git a/packages/lexical-devtools/src/public/icon/32-restricted.png b/packages/lexical-devtools/src/public/icon/32-restricted.png new file mode 100644 index 00000000000..26d39aa852f Binary files /dev/null and b/packages/lexical-devtools/src/public/icon/32-restricted.png differ diff --git a/packages/lexical-devtools/src/public/icon/48-restricted.png b/packages/lexical-devtools/src/public/icon/48-restricted.png new file mode 100644 index 00000000000..dcde663ad7f Binary files /dev/null and b/packages/lexical-devtools/src/public/icon/48-restricted.png differ diff --git a/packages/lexical-devtools/src/public/inspect.svg b/packages/lexical-devtools/src/public/inspect.svg new file mode 100644 index 00000000000..aee18e2be47 --- /dev/null +++ b/packages/lexical-devtools/src/public/inspect.svg @@ -0,0 +1,10 @@ + + + + diff --git a/packages/lexical-devtools/src/store.ts b/packages/lexical-devtools/src/store.ts index a447f8c9565..3926e9830a6 100644 --- a/packages/lexical-devtools/src/store.ts +++ b/packages/lexical-devtools/src/store.ts @@ -17,17 +17,37 @@ import {SerializedRawEditorState} from './types'; export interface ExtensionState { lexicalState: { - [tabID: number]: {[editorKey: string]: SerializedRawEditorState}; + [tabID: number]: {[editorKey: string]: SerializedRawEditorState} | null; }; + selectedEditorKey: { + [tabID: number]: string | null; + }; + markTabAsRestricted: (tabID: number) => void; setStatesForTab: ( id: number, states: {[editorKey: string]: SerializedRawEditorState}, ) => void; + setSelectedEditorKey: (tabID: number, editorKey: string | null) => void; } export const useExtensionStore = create()( subscribeWithSelector((set) => ({ lexicalState: {}, + markTabAsRestricted: (tabID: number) => + set((state) => ({ + lexicalState: { + ...state.lexicalState, + [tabID]: null, + }, + })), + selectedEditorKey: {}, + setSelectedEditorKey: (tabID: number, editorKey: string | null) => + set((state) => ({ + selectedEditorKey: { + ...state.selectedEditorKey, + [tabID]: editorKey, + }, + })), setStatesForTab: ( id: number, states: {[editorKey: string]: SerializedRawEditorState}, diff --git a/packages/lexical-devtools/src/utils/isLexicalNode.ts b/packages/lexical-devtools/src/utils/isLexicalNode.ts new file mode 100644 index 00000000000..f3c3b52f6c9 --- /dev/null +++ b/packages/lexical-devtools/src/utils/isLexicalNode.ts @@ -0,0 +1,15 @@ +/** + * 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 {LexicalHTMLElement} from '../types'; + +export function isLexicalNode( + node: LexicalHTMLElement | Element, +): node is LexicalHTMLElement { + return (node as LexicalHTMLElement).__lexicalEditor !== undefined; +}