Skip to content

Commit

Permalink
Highlight DOM Nodes onHover of Lexical DevTools Node (#2728)
Browse files Browse the repository at this point in the history
  • Loading branch information
noi5e authored Aug 3, 2022
1 parent 8c65579 commit d44f6db
Show file tree
Hide file tree
Showing 9 changed files with 199 additions and 47 deletions.
136 changes: 104 additions & 32 deletions packages/lexical-devtools/src/devtools/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,46 +16,118 @@ chrome.devtools.panels.create(
},
);

chrome.runtime.onConnect.addListener((port) => {
port.onMessage.addListener((message) => {
if (message.name === 'highlight') {
chrome.devtools.inspectedWindow.eval(`
highlight('${message.lexicalKey}');
`);
}

if (message.name === 'dehighlight') {
chrome.devtools.inspectedWindow.eval(`
dehighlight();
`);
}
});
});

function handleShown() {
// Use devtools.inspectedWindow.eval() to get editorState updates. For more info on security concerns:
// Security concerns related to chrome.devtools.inspectedWindow.eval():
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/devtools/inspectedWindow/eval
chrome.devtools.inspectedWindow
// Attach a registerUpdateListener within the window to subscribe to changes in editorState.
// After the initial registration, all editorState updates are done via browser.runtime.onConnect & window.postMessage
.eval(
`
const editor = document.querySelectorAll('div[data-lexical-editor]')[0]
.__lexicalEditor;
const initialEditorState = editor.getEditorState();
// custom JSON serialization
// existing editorState.toJSON() does not contain Lexical ._key property
const serializeEditorState = (editorState) => {
const nodeMap = Object.fromEntries(editorState._nodeMap);
return {nodeMap};
};

// query document for Lexical editor instance.
// TODO: add support multiple Lexical editors within the same page
// lexicalKey is attached to each DOM node like so: DOMNode[__lexicalKey_{editorKey}]
chrome.devtools.inspectedWindow.eval(`
const editorDOMNode = document.querySelectorAll(
'div[data-lexical-editor]',
)[0];
const editor = editorDOMNode.__lexicalEditor;
const editorKey = editorDOMNode.__lexicalEditor._key;
const lexicalKey = '__lexicalKey_' + editorKey;
`);

// depth first search to find DOM node with lexicalKey
chrome.devtools.inspectedWindow.eval(`
const findDOMNode = (node, targetKey) => {
if (targetKey === 'root') {
return editorDOMNode;
}
if (node[lexicalKey] && node[lexicalKey] === targetKey) {
return node;
}
for (let i = 0; i < node.childNodes.length; i++) {
const child = findDOMNode(node.childNodes[i], targetKey);
if (child) return child;
}
return null;
};
`);

// functions to highlight/dehighlight DOM nodes onHover of DevTools nodes
chrome.devtools.inspectedWindow.eval(`
const highlight = (lexicalKey) => {
const node = findDOMNode(editorDOMNode, lexicalKey);
const {width, height, top, left} = node.getBoundingClientRect();
highlightOverlay.style.width = width + 'px';
highlightOverlay.style.height = height + 'px';
highlightOverlay.style.top = top + window.scrollY + 'px';
highlightOverlay.style.left = left + window.scrollX + 'px';
highlightOverlay.style.display = 'block';
};
const dehighlight = () => {
highlightOverlay.style.display = 'none';
};
`);

// append highlight overlay <div> to document
chrome.devtools.inspectedWindow.eval(`
const highlightOverlay = document.createElement('div');
highlightOverlay.style.position = 'absolute';
highlightOverlay.style.background = 'rgba(119, 182, 255, 0.5)';
highlightOverlay.style.border = '1px dashed #77b6ff';
document.body.appendChild(highlightOverlay);
`);

// send initial editorState to devtools app through window.postMessage.
// the existing editorState.toJSON() does not contain lexicalKey
// therefore, we have a custom serializeEditorState helper
chrome.devtools.inspectedWindow.eval(`
const initialEditorState = editor.getEditorState();
const serializeEditorState = (editorState) => {
const nodeMap = Object.fromEntries(editorState._nodeMap);
return {nodeMap};
};
window.postMessage(
{
editorState: serializeEditorState(initialEditorState),
name: 'editor-update',
type: 'FROM_PAGE',
},
'*',
);
`);

// Attach a registerUpdateListener within the window to subscribe to changes in editorState.
// After the initial registration, all editorState updates are done via browser.runtime.onConnect & window.postMessage
chrome.devtools.inspectedWindow.eval(`
editor.registerUpdateListener(({editorState}) => {
window.postMessage(
{
editorState: serializeEditorState(initialEditorState),
editorState: serializeEditorState(editorState),
name: 'editor-update',
type: 'FROM_PAGE',
},
'*',
);
editor.registerUpdateListener(({editorState}) => {
window.postMessage(
{
editorState: serializeEditorState(editorState),
name: 'editor-update',
type: 'FROM_PAGE',
},
'*',
);
});
`,
); // to do: add error handling eg. .then(handleError)
});
`);
}
33 changes: 31 additions & 2 deletions packages/lexical-devtools/src/panel/components/App/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import './index.css';

import {DevToolsTree} from 'packages/lexical-devtools/types';
import * as React from 'react';
import {useEffect, useRef, useState} from 'react';
import {useCallback, useEffect, useRef, useState} from 'react';

import TreeView from '../TreeView';

Expand All @@ -26,6 +26,30 @@ function App(): JSX.Element {
setNodeMap(newNodeMap);
};

// highlight & dehighlight the corresponding DOM nodes onHover of DevTools nodes
const highlightDOMNode = useCallback(
(lexicalKey: string) => {
port.current?.postMessage({
lexicalKey,
name: 'highlight',
tabId: window.chrome.devtools.inspectedWindow.tabId,
type: 'FROM_APP',
});
},
[port],
);

const deHighlightDOMNode = useCallback(
(lexicalKey: string) => {
port.current?.postMessage({
name: 'dehighlight',
tabId: window.chrome.devtools.inspectedWindow.tabId,
type: 'FROM_APP',
});
},
[port],
);

useEffect(() => {
// create and initialize the messaging port to receive editorState updates
port.current = window.chrome.runtime.connect();
Expand Down Expand Up @@ -64,7 +88,12 @@ function App(): JSX.Element {
<p>Loading...</p>
</div>
) : (
<TreeView viewClassName="tree-view-output" nodeMap={nodeMap} />
<TreeView
deHighlightDOMNode={deHighlightDOMNode}
highlightDOMNode={highlightDOMNode}
viewClassName="tree-view-output"
nodeMap={nodeMap}
/>
)}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,8 @@
padding: 0;
text-decoration: none;
}

.tree-node:hover > .chevron-button {
background: #77b6ff;
color: #222;
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,17 @@
user-select: none;
}

.tree-node {
padding-left: 1em;
width: 100%;
.tree-node:hover {
background: #77b6ff;
color: #222;
}

.tree-node-wrapper {
cursor: pointer;
white-space: nowrap;
width: 100%;
}

.tree-node-wrapper:first-child {
width: 100vw;
}
37 changes: 29 additions & 8 deletions packages/lexical-devtools/src/panel/components/TreeNode/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,26 @@ function TreeNode({
__text,
__type,
children,
deHighlightDOMNode,
depth,
highlightDOMNode,
lexicalKey,
monospaceWidth,
}: DevToolsNode): JSX.Element {
const [isExpanded, setIsExpanded] = useState(true);

const handleChevronClick = () => {
setIsExpanded(!isExpanded);
};

const handleMouseEnter: React.MouseEventHandler = (event) => {
highlightDOMNode(lexicalKey);
};

const handleMouseLeave: React.MouseEventHandler = (event) => {
deHighlightDOMNode(lexicalKey);
};

const nodeString = ` (${lexicalKey}) ${__type} ${
__text ? '"' + __text + '"' : ''
}`;
Expand All @@ -40,15 +51,25 @@ function TreeNode({
''
);

const leftIndent = depth * parseFloat(monospaceWidth) + 'em';

return (
<div className="tree-node" key={lexicalKey}>
{children.length > 0 ? (
<Chevron handleClick={handleChevronClick} isExpanded={isExpanded} />
) : (
<button className="indentation">&#9654;</button>
)}
{nodeString}
{<br />}
<div className="tree-node-wrapper" key={lexicalKey}>
<div
className="tree-node"
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
style={{paddingLeft: leftIndent}}>
<span style={{width: 'var(--monospace-character-width)'}}>&nbsp;</span>
{children.length > 0 ? (
<Chevron handleClick={handleChevronClick} isExpanded={isExpanded} />
) : (
<span style={{width: 'var(--monospace-character-width)'}}>
&nbsp;
</span>
)}
{nodeString}
</div>
{isExpanded ? childNodes : ''}
</div>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
background: #222;
color: #fff;
margin: 0;
padding: 1em;
padding: 1em 0;
font-size: 1.2em;
overflow: auto;
text-align: left;
-moz-osx-font-smoothing: grayscale;
font-weight: 400;
--indentation-size: 1em;
width: max-content;
}
12 changes: 12 additions & 0 deletions packages/lexical-devtools/src/panel/components/TreeView/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,21 @@ import * as React from 'react';
import TreeNode from '../TreeNode';

function TreeView({
deHighlightDOMNode,
highlightDOMNode,
viewClassName,
nodeMap,
}: {
deHighlightDOMNode: (lexicalKey: string) => void;
highlightDOMNode: (lexicalKey: string) => void;
viewClassName: string;
nodeMap: DevToolsTree;
}): JSX.Element {
// read CSS variable from the DOM in order to pass it to TreeNode
const monospaceWidth = getComputedStyle(
document.documentElement,
).getPropertyValue('--monospace-character-width');

// takes flat JSON structure, nests child comments inside parents
const depthFirstSearch = (
map: DevToolsTree = nodeMap,
Expand All @@ -38,8 +47,11 @@ function TreeView({
...node,
__type: node.__type,
children,
deHighlightDOMNode,
depth,
highlightDOMNode,
lexicalKey: node.__key,
monospaceWidth,
};
};

Expand Down
4 changes: 4 additions & 0 deletions packages/lexical-devtools/src/panel/components/main/index.css
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
:root {
--monospace-character-width: 1.2em;
}

body {
margin: 0;
font-family: monospace;
Expand Down
3 changes: 3 additions & 0 deletions packages/lexical-devtools/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ export interface DevToolsNode {
__text?: string;
__type: string;
children: Array<DevToolsNode>;
deHighlightDOMNode: (lexicalKey: string) => void;
depth: number;
highlightDOMNode: (lexicalKey: string) => void;
lexicalKey: string;
monospaceWidth: string;
}

2 comments on commit d44f6db

@vercel
Copy link

@vercel vercel bot commented on d44f6db Aug 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

lexical – ./packages/lexical-website-new

lexical-git-main-fbopensource.vercel.app
lexical-fbopensource.vercel.app
lexicaljs.com
lexical.dev
lexicaljs.org
www.lexical.dev

@vercel
Copy link

@vercel vercel bot commented on d44f6db Aug 3, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

lexical-playground – ./packages/lexical-playground

lexical-playground-git-main-fbopensource.vercel.app
lexical-playground-fbopensource.vercel.app
playground.lexical.dev
lexical-playground.vercel.app

Please sign in to comment.