feat: make icons and popups dynamic & change file structure (#3336)

* chore: components and popups folder

* fix: run scripts on document_end

* feat: make icons and popups dynamic

* fix: restricted page handling

Co-authored-by: Adithya Vardhan <im-adithya@Adithyas-MacBook-Air.local>
This commit is contained in:
Adithya Vardhan
2022-11-10 21:33:27 +05:30
committed by GitHub
parent bcee9cc89b
commit c6b9b2f7aa
41 changed files with 332 additions and 104 deletions

8
.hintrc Normal file
View File

@ -0,0 +1,8 @@
{
"extends": [
"development"
],
"hints": {
"meta-viewport": "off"
}
}

View File

@ -14,7 +14,7 @@ const {readFileSync, writeFileSync} = require('fs');
const {copy, ensureDir, move, remove} = require('fs-extra');
const {join} = require('path');
const STATIC_FILES = ['icons'];
const STATIC_FILES = ['icons', 'popups'];
const preProcess = async (destinationPath, tempPath) => {
await remove(destinationPath); // Clean up from previously completed builds

View File

@ -18,9 +18,9 @@
"48": "icons/48-disabled.png",
"128": "icons/128-disabled.png"
},
"default_popup": "src/popup/index.html"
"default_popup": "popups/disabled.html"
},
"devtools_page": "src/devtools/index.html",
"devtools_page": "src/main/index.html",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"web_accessible_resources": ["src/inject/index.js"],
"background": {
@ -31,7 +31,7 @@
{
"matches": ["<all_urls>"],
"js": ["src/content/index.js"],
"run_at": "document_start"
"run_at": "document_end"
}
]
}

View File

@ -18,9 +18,9 @@
"48": "icons/48-disabled.png",
"128": "icons/128-disabled.png"
},
"default_popup": "src/popup/index.html"
"default_popup": "popups/disabled.html"
},
"devtools_page": "src/devtools/index.html",
"devtools_page": "src/main/index.html",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"web_accessible_resources": ["src/inject/index.js"],
"background": {
@ -31,7 +31,7 @@
{
"matches": ["<all_urls>"],
"js": ["src/content/index.js"],
"run_at": "document_start"
"run_at": "document_end"
}
]
}

View File

@ -22,10 +22,10 @@
"48": "icons/48-disabled.png",
"128": "icons/128-disabled.png"
},
"default_popup": "src/popup/index.html",
"default_popup": "popups/disabled.html",
"browser_style": true
},
"devtools_page": "src/devtools/index.html",
"devtools_page": "src/main/index.html",
"content_security_policy": "script-src 'self' 'unsafe-eval'; object-src 'self'",
"web_accessible_resources": ["src/inject/index.js"],
"background": {
@ -36,7 +36,7 @@
{
"matches": ["<all_urls>"],
"js": ["src/content/index.js"],
"run_at": "document_start"
"run_at": "document_end"
}
]
}

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 428 B

After

Width:  |  Height:  |  Size: 428 B

View File

Before

Width:  |  Height:  |  Size: 813 B

After

Width:  |  Height:  |  Size: 813 B

View File

Before

Width:  |  Height:  |  Size: 1.0 KiB

After

Width:  |  Height:  |  Size: 1.0 KiB

View File

@ -0,0 +1,18 @@
<script src="shared.js"></script>
<link rel="stylesheet" href="shared.css" />
<style>
html,
body {
min-width: 410px;
min-height: 33px;
}
hr {
width: 100%;
}
</style>
<p>
<b>This page doesn&rsquo;t appear to be using Lexical.</b>
<br />
If this seems wrong, follow the troubleshooting instructions.
</p>

View File

@ -0,0 +1,18 @@
<script src="shared.js"></script>
<link rel="stylesheet" href="shared.css" />
<style>
html,
body {
min-width: 460px;
min-height: 39px;
}
hr {
width: 100%;
}
</style>
<p>
<b>This page is using the production build of Lexical. &#x2705;</b>
<br />
Open the developer tools, and "Lexical" tab will appear to the right.
</p>

View File

@ -0,0 +1,14 @@
<script src="shared.js"></script>
<link rel="stylesheet" href="shared.css" />
<style>
html,
body {
min-width: 286px;
min-height: 33px;
}
</style>
<p>
<b>This is a restricted browser page.</b>
<br />
Lexical devtools cannot access this page.
</p>

View File

@ -0,0 +1,8 @@
html,
body {
font-size: 14px;
}
body {
margin: 8px;
}

View File

@ -0,0 +1,33 @@
/**
* 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.
*
*/
/* globals chrome */
'use strict';
document.addEventListener('DOMContentLoaded', function () {
// Make links work
const links = document.getElementsByTagName('a');
for (let i = 0; i < links.length; i++) {
(function () {
const ln = links[i];
const location = ln.href;
ln.onclick = function () {
chrome.tabs.create({active: true, url: location});
return false;
};
})();
}
// Work around https://bugs.chromium.org/p/chromium/issues/detail?id=428044
document.body.style.opacity = 0;
document.body.style.transition = 'opacity ease-out .4s';
requestAnimationFrame(function () {
document.body.style.opacity = 1;
});
});

View File

@ -10,9 +10,15 @@
// Each tab will have a separate messaging port for the devTools app & the inspectedWindow's content script, eg. { tabId: { reactPort, contentScriptPort } }
const tabsToPorts: Record<
number,
{contentScriptPort?: chrome.runtime.Port; reactPort?: chrome.runtime.Port}
{
contentScriptPort?: chrome.runtime.Port;
reactPort?: chrome.runtime.Port;
devtoolsPort?: chrome.runtime.Port;
}
> = {};
const IS_FIREFOX: boolean = navigator.userAgent.indexOf('Firefox') >= 0;
// The Lexical DevTools React UI sends a message to initialize the port.
chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
port.onMessage.addListener((message) => {
@ -28,20 +34,26 @@ chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
return;
}
if (message.name === 'init' && message.type === 'FROM_APP') {
if (message.name === 'init' && port.name === 'react-app') {
tabsToPorts[tabId] = tabsToPorts[tabId] ? tabsToPorts[tabId] : {};
tabsToPorts[message.tabId].reactPort = port;
return;
}
if (message.name === 'init' && message.type === 'FROM_CONTENT') {
if (message.name === 'init' && port.name === 'content-script') {
tabsToPorts[tabId] = tabsToPorts[tabId] ? tabsToPorts[tabId] : {};
tabsToPorts[tabId].contentScriptPort = port;
port.postMessage({
name: 'checkForLexical',
});
return;
}
// initial editorState requested from devtools panel
if (message.name === 'init' && message.type === 'FROM_DEVTOOLS') {
if (message.name === 'lexical-found' && port.name === 'content-script') {
setIconAndPopup('production', tabId);
}
if (message.name === 'init' && port.name === 'devtools') {
const contentScriptPort = tabsToPorts[tabId].contentScriptPort;
if (contentScriptPort) {
contentScriptPort.postMessage({
@ -70,3 +82,55 @@ chrome.runtime.onConnect.addListener((port: chrome.runtime.Port) => {
}
});
});
function isRestrictedBrowserPage(url: string | undefined) {
return !url || new URL(url).protocol === 'chrome:';
}
function checkAndHandleRestrictedPageIfSo(tab: chrome.tabs.Tab) {
if (tab && tab.id && isRestrictedBrowserPage(tab.url)) {
setIconAndPopup('restricted', tab.id);
}
}
if (!IS_FIREFOX) {
chrome.tabs.query({}, (tabs) =>
tabs.forEach(checkAndHandleRestrictedPageIfSo),
);
chrome.tabs.onCreated.addListener((tab: chrome.tabs.Tab) => {
checkAndHandleRestrictedPageIfSo(tab);
});
}
// Listen to URL changes on the active tab and update the DevTools icon.
chrome.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
if (IS_FIREFOX) {
// We don't properly detect protected URLs in Firefox at the moment.
// However we can reset the DevTools icon to its loading state when the URL changes.
// It will be updated to the correct icon by the onMessage callback below.
if (tab.active && changeInfo.status === 'loading') {
setIconAndPopup('disabled', tabId);
}
} else {
// Don't reset the icon to the loading state for Chrome or Edge.
// The onUpdated callback fires more frequently for these browsers,
// often after onMessage has been called.
checkAndHandleRestrictedPageIfSo(tab);
}
});
function setIconAndPopup(lexicalBuildType: string, tabId: number) {
chrome.browserAction.setIcon({
path: {
'128': 'icons/128-' + lexicalBuildType + '.png',
'16': 'icons/16-' + lexicalBuildType + '.png',
'32': 'icons/32-' + lexicalBuildType + '.png',
'48': 'icons/48-' + lexicalBuildType + '.png',
},
tabId: tabId,
});
chrome.browserAction.setPopup({
popup: 'popups/' + lexicalBuildType + '.html',
tabId: tabId,
});
}

View File

@ -43,7 +43,6 @@ function App(): JSX.Element {
lexicalKey,
name: 'highlight',
tabId: window.chrome.devtools.inspectedWindow.tabId,
type: 'FROM_APP',
});
},
[port],
@ -54,7 +53,6 @@ function App(): JSX.Element {
port.current?.postMessage({
name: 'dehighlight',
tabId: window.chrome.devtools.inspectedWindow.tabId,
type: 'FROM_APP',
});
},
[port],
@ -62,13 +60,14 @@ function App(): JSX.Element {
useEffect(() => {
// create and initialize the messaging port to receive editorState updates
port.current = window.chrome.runtime.connect();
port.current = window.chrome.runtime.connect({
name: 'react-app',
});
// post init message to background JS so tabId will be registered
port.current.postMessage({
name: 'init',
tabId: window.chrome.devtools.inspectedWindow.tabId,
type: 'FROM_APP',
});
return () => {

View File

@ -5,6 +5,7 @@
margin: 0;
padding: 0;
text-decoration: none;
-webkit-user-select: none;
user-select: none;
}

View File

@ -13,9 +13,13 @@ declare global {
interface DocumentEventMap {
editorStateUpdate: CustomEvent;
highlight: CustomEvent;
lexicalPresenceUpdate: CustomEvent;
}
}
let backendDisconnected = false;
let backendInitialized = false;
// for security reasons, content scripts cannot read Lexical's changes to the DOM
// in order to access the editorState, we inject this script directly into the page
const script = document.createElement('script');
@ -23,29 +27,20 @@ script.src = chrome.runtime.getURL('src/inject/index.js');
document.documentElement.appendChild(script);
if (script.parentNode) script.parentNode.removeChild(script);
const port = chrome.runtime.connect();
port.postMessage({
name: 'init',
type: 'FROM_CONTENT',
const port = chrome.runtime.connect({
name: 'content-script',
});
// Listen to editorState updates from the inspected page, via the registerUpdateListener injected by devtools.js
window.addEventListener('message', function (event) {
if (event.source !== window) {
// Security check: https://developer.chrome.com/docs/extensions/mv3/content_scripts/#host-page-communication
return;
}
function sayHelloToBackend() {
port.postMessage({
name: 'init',
});
}
if (
event.data.type &&
event.data.type === 'FROM_PAGE' &&
event.data.name === 'editor-update'
) {
document.addEventListener('lexicalPresenceUpdate', function (e) {
if (e.detail.lexical) {
port.postMessage({
editorState: event.data.editorState,
name: 'editor-update',
type: 'FROM_CONTENT',
name: 'lexical-found',
});
}
});
@ -54,7 +49,6 @@ document.addEventListener('editorStateUpdate', function (e) {
port.postMessage({
editorState: e.detail.editorState,
name: 'editor-update',
type: 'FROM_CONTENT',
});
});
@ -69,7 +63,21 @@ function getCloneInto(): CloneInto | null {
const cloneInto = getCloneInto();
function handleDisconnect() {
backendDisconnected = true;
// TODO: remove event listeners and post shutdown message
}
port.onMessage.addListener((message) => {
if (message.name === 'checkForLexical') {
backendInitialized = true;
// As we load scripts on document_end, we wait for the
// page to load before dispatching checkForLexical event
window.onload = function () {
document.dispatchEvent(new CustomEvent('checkForLexical'));
};
}
if (message.name === 'highlight') {
const data = {lexicalKey: message.lexicalKey as string};
const detail =
@ -91,3 +99,16 @@ port.onMessage.addListener((message) => {
document.dispatchEvent(new CustomEvent('loadEditorState'));
}
});
port.onDisconnect.addListener(handleDisconnect);
sayHelloToBackend();
if (!backendInitialized) {
const intervalID = setInterval(() => {
if (backendInitialized || backendDisconnected) {
clearInterval(intervalID);
} else {
sayHelloToBackend();
}
}, 500);
}

View File

@ -1,29 +0,0 @@
/**
* 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.
*
*/
const port = chrome.runtime.connect();
// Create the panel which appears within the browser's DevTools, loading the Lexical DevTools App within index.html.
chrome.devtools.panels.create(
'Lexical',
'',
'/src/panel/index.html',
function (panel) {
panel.onShown.addListener(handleShown);
// to do: add handleHidden() listener
},
);
function handleShown() {
// init message goes → background script → content script
// content script handles initial load of editorState
port.postMessage({
name: 'init',
tabId: chrome.devtools.inspectedWindow.tabId,
type: 'FROM_DEVTOOLS',
});
}

View File

@ -15,6 +15,7 @@ import {
RangeSelectionJSON,
} from 'packages/lexical-devtools/types';
let lexical: boolean;
let editorDOMNode: LexicalHTMLElement | null, editorKey: string | null;
const serializePoint = (point: PointType) => {
@ -57,6 +58,14 @@ const postEditorState = (editorState: EditorState) => {
document.dispatchEvent(new CustomEvent('editorStateUpdate', {detail: data}));
};
document.addEventListener('checkForLexical', function (e) {
lexical = document.querySelectorAll('div[data-lexical-editor]').length > 0;
const data = {lexical: lexical};
document.dispatchEvent(
new CustomEvent('lexicalPresenceUpdate', {detail: data}),
);
});
document.addEventListener('loadEditorState', function (e) {
// query document for Lexical editor instance.
// TODO: add support multiple Lexical editors within the same page

View File

@ -0,0 +1,73 @@
/**
* 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.
*
*/
let panelCreated = false;
const port = chrome.runtime.connect({
name: 'devtools',
});
function syncSavedPreferences() {
// TODO: Save devtools panel settings
}
syncSavedPreferences();
function createPanelIfLexicalLoaded() {
if (panelCreated) {
return;
}
chrome.devtools.inspectedWindow.eval(
`window.document.querySelectorAll('div[data-lexical-editor]').length > 0`,
function (pageHasLexical, error) {
if (!pageHasLexical) {
return;
}
panelCreated = true;
clearInterval(loadCheckInterval);
// Create the panel which appears within the browser devtools
chrome.devtools.panels.create(
'Lexical',
'',
'src/panel/index.html',
function (panel) {
panel.onShown.addListener(handleShown);
// TODO: add handleHidden() listener
},
);
},
);
}
function handleShown() {
// init message goes → background script → content script
// content script handles initial load of editorState
port.postMessage({
name: 'init',
tabId: chrome.devtools.inspectedWindow.tabId,
});
}
// Load (or reload) the DevTools extension when the user navigates to a new page.
function checkPageForLexical() {
syncSavedPreferences();
createPanelIfLexicalLoaded();
}
// Check for Lexical before loading the DevTools extension when the user navigates to a new page.
chrome.devtools.network.onNavigated.addListener(checkPageForLexical);
// In case Lexical is added after page load
const loadCheckInterval = setInterval(function () {
createPanelIfLexicalLoaded();
}, 1000);
createPanelIfLexicalLoaded();

View File

@ -2,11 +2,30 @@
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lexical Developer Tools</title>
<style>
html {
display: flex;
}
body {
margin: 0;
padding: 0;
flex: 1;
display: flex;
}
#container {
display: flex;
flex: 1;
width: 100%;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
</style>
</head>
<body>
<div id="root"></div>
<script type="module" src="./components/main/index.tsx"></script>
<div id="container">Unable to find Lexical on the page.</div>
<script type="module" src="./index.tsx"></script>
</body>
</html>

View File

@ -11,9 +11,9 @@ import './index.css';
import * as React from 'react';
import * as ReactDOM from 'react-dom/client';
import App from '../App';
import App from '../components/App';
ReactDOM.createRoot(document.getElementById('root') as Element).render(
ReactDOM.createRoot(document.getElementById('container') as Element).render(
<React.StrictMode>
<App />
</React.StrictMode>,

View File

@ -1,27 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>Lexical Developer Tools</title>
<style>
html,
body {
font-size: 14px;
font-family: sans-serif;
width: 300px;
}
body {
padding: 8px;
}
</style>
</head>
<body>
<p>
<b>
If this page uses Lexical, you can see its state tree by going to
Developer Tools &#10140; Lexical.
</b>
</p>
</body>
</html>

View File

@ -24,10 +24,9 @@ export default defineConfig({
input: {
background: resolve(root, 'background', 'index.ts'),
content: resolve(root, 'content', 'index.ts'),
devtools: resolve(root, 'devtools', 'index.html'),
inject: resolve(root, 'inject', 'index.ts'),
panel: resolve(root, 'panel', 'index.html'),
popup: resolve(root, 'popup', 'index.html')
main: resolve(root, 'main', 'index.html'),
panel: resolve(root, 'panel', 'index.html')
},
output: {
entryFileNames: (chunk) => `src/${chunk.name}/index.js`