├── .github └── FUNDING.yml ├── .gitignore ├── .parcelrc ├── .postcssrc ├── CONTRIBUTING.md ├── README.md ├── docs ├── context-actions.png ├── panel-screenshot.png └── sidebar-screenshot.png ├── package-lock.json ├── package.json ├── src ├── colors.scss ├── global.scss ├── icons │ ├── check.svg │ ├── info.svg │ ├── inspect.svg │ ├── intoview.svg │ ├── loader-simple.svg │ ├── loader.svg │ ├── refresh.svg │ ├── settings.svg │ ├── warning.svg │ └── wrong.svg ├── scripts │ ├── background.js │ ├── classes │ │ ├── ContextChecks.js │ │ ├── ContextsContainer.js │ │ ├── StackingContext.js │ │ └── TabStatus.js │ ├── components │ │ ├── ConnectionContext.js │ │ ├── Context │ │ │ ├── Context.js │ │ │ └── Context.scss │ │ ├── ContextDetails │ │ │ └── ContextDetails.js │ │ ├── ContextsContainer │ │ │ ├── ContextsContainer.js │ │ │ └── ContextsContainer.scss │ │ ├── ContextsTree │ │ │ ├── ContextsTree.js │ │ │ └── ContextsTree.scss │ │ ├── DataContext.js │ │ ├── NodeDetails │ │ │ └── NodeDetails.js │ │ ├── OptionBar │ │ │ ├── OptionBar.js │ │ │ └── OptionBar.scss │ │ ├── OptionBarButton │ │ │ ├── OptionBarButton.js │ │ │ └── OptionBarButton.scss │ │ ├── OptionBarLabel │ │ │ ├── OptionBarLabel.js │ │ │ └── OptionBarLabel.scss │ │ ├── OptionBarSeparator │ │ │ ├── OptionBarSeparator.js │ │ │ └── OptionBarSeparator.scss │ │ ├── OptionBarSpacer │ │ │ └── OptionBarSpacer.js │ │ ├── OrderedContextsList │ │ │ ├── OrderedContextsList.js │ │ │ └── OrderedContextsList.scss │ │ ├── PanelContent │ │ │ ├── PanelContent.js │ │ │ └── PanelContent.scss │ │ ├── PanelSettings │ │ │ ├── PanelSettings.js │ │ │ └── PanelSettings.scss │ │ ├── SVG.js │ │ ├── Section │ │ │ ├── Section.js │ │ │ └── Section.scss │ │ ├── SettingsContext.js │ │ ├── SidebarContent │ │ │ ├── SidebarContent.js │ │ │ └── SidebarContent.scss │ │ ├── Sidepane │ │ │ ├── Sidepane.js │ │ │ └── Sidepane.scss │ │ ├── Spinner │ │ │ ├── Spinner.js │ │ │ └── Spinner.scss │ │ └── TreeItem │ │ │ ├── TreeItem.js │ │ │ └── TreeItem.scss │ ├── content.js │ └── utils │ │ ├── DOMTraversal.js │ │ └── utils.js └── views │ ├── devtools │ ├── devtools.html │ └── devtools.js │ ├── elements-sidebar │ ├── sidebar-preact.html │ └── sidebar.js │ └── panel │ ├── panel.html │ └── panel.js └── static ├── assets ├── icon128.png ├── icon16.png ├── icon32.png └── icon48.png └── manifest.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: andreadev_it 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 12 | polar: # Replace with a single Polar username 13 | buy_me_a_coffee: # Replace with a single Buy Me a Coffee username 14 | thanks_dev: # Replace with a single thanks.dev username 15 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .parcel-cache 3 | dist -------------------------------------------------------------------------------- /.parcelrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@parcel/config-default", 3 | "transformers": { 4 | "*.svg": ["@parcel/transformer-raw"] 5 | }, 6 | "reporters": ["...", "parcel-reporter-static-files-copy"] 7 | } -------------------------------------------------------------------------------- /.postcssrc: -------------------------------------------------------------------------------- 1 | { 2 | "modules": true, 3 | "plugins": { 4 | "autoprefixer": {} 5 | } 6 | } -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # CSS Stacking Context Inspector 2 | 3 | This devtools extension for Google Chrome and Firefox allows you to inspect all the stacking contexts created in the current tab, along with some useful informations about any particular stacking context and its DOM element. 4 | 5 | Thank you for your interest in contributing to the development of this extension! If you found a bug that you want to report, or have a feature request, please post it in the "Issues" section here on github. 6 | 7 | This extension uses the following tools: 8 | * **preact** - frontend framework 9 | * **SASS** - CSS preprocessor 10 | * **parcel** - package builder 11 | 12 | To simplify the data-passing between the different extension components, the bg-script library is used. To better understand how it should be use, please refer to its [github page](https://github.com/andreadev-it/bg-script). 13 | 14 | # Local development 15 | 16 | ## Prerequisites 17 | 18 | To start coding on this project, you should already have `node` and `npm` installed in your system. 19 | 20 | ## Environment setup 21 | 22 | To set up your local environment, clone this repository and launch the following command: 23 | ```sh 24 | npm install 25 | ``` 26 | 27 | ## Build the extension 28 | 29 | There are two npm scripts available to build the extension: `build` and `build-dev`. 30 | 31 | To build the extension without code minification, you can launch this command: 32 | ```sh 33 | npm run build-dev 34 | ``` 35 | 36 | After testing, when the extension is working, you can prepare a production-ready build by launching the following command: 37 | ```sh 38 | npm run build 39 | ``` 40 | 41 | These commands will create a `dist` folder with the compiled code that can be used for testing. 42 | 43 | If you find issues related to file not being correctly loaded in the extension, you can try deleting the `dist` and the `.parcel-cache` folders. After that you can run the build command again to create a clean build. 44 | 45 | ## Test the extension 46 | 47 | ### Chrome 48 | First, go to the extension page: `chrome://extensions` 49 | 50 | Make sure that the toggle "Developer mode" on the top right is active, click on the "Load unpacked" button and select the recently generated `dist` folder. 51 | 52 | When you rebuild the extension, you'll need to go to the extension page and click the "reload" button in the bottom-right corner of the extension card to reflect the changes. 53 | 54 | To inspect the devtools extension, just press `CTRL + SHIFT + J` with the devtools window focused. This will open a second devtools window that will let you inspect the first one. 55 | 56 | ### Firefox 57 | Start by going to the debugging page: `about:debugging` 58 | 59 | Now click on the "This Firefox" section on the left, and click on the "Load temporary Add-on" button. Select any file within the `dist` folder to load the extension. 60 | 61 | To debug it, you can use the [Firefox Browser Toolbox](https://developer.mozilla.org/en-US/docs/Tools/Browser_Toolbox). Go in the "about:debugging" page and click on the "Analyze" button next to the temporarily loaded extension name. 62 | 63 | You might also want to take a look at [this](https://extensionworkshop.com/documentation/develop/debugging/#debugging-developer-tools-pages-and-panels) page for further information that will help you inspect the extension devtools sidebar and panel. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stacking Contexts Inspector 2 | The Stacking Contexts Inspector is a [devtools extension for Google Chrome](https://chrome.google.com/webstore/detail/css-stacking-context-insp/apjeljpachdcjkgnamgppgfkmddadcki) and [Firefox](https://addons.mozilla.org/en-US/firefox/addon/css-stacking-context-inspector/) that allows you to analyse the stacking contexts available on a webpage. Here you will find all the information you need to get up and running with using this tool. 3 | 4 | This extension will add a **new panel** to the devtools and a **new sidebar** on the elements panel. 5 | 6 | ### Known bugs and things to keep in mind 7 | * This extension work by analysing the page at a specific point in time (when you open the panel or the sidebar). It won't automatically react to changes in the inspected page, but it will show you a warning when it detects changes that might be altering the stacking contexts. When you see that warning, you can use a button to refresh the stacking contexts at any time. 8 | * This extension can't currently analyse third-party iframes within the page (work in progress). 9 | 10 | If you find any other issue or have some feature request, feel free to use the [Issues section](https://github.com/andreadev-it/stacking-contexts-inspector/issues) here on github to share them. 11 | 12 | ## The Stacking Contexts panel 13 | This panel will show you a tree-like representation of all the stacking contexts available on the page. It will also show some "containers" that represent the document that the stacking contexts reside within (useful to instantly see which stacking contexts were found within iframes). These containers will be shown with a bold, italic font and a grey color (the `#document` root is an exemple of a container). 14 | 15 | ![Screenshot of the Stacking Contexts panel in devtools](docs/panel-screenshot.png) 16 | 17 | When hovering over a context, the DOM element will be highlighted in the page. If you don't see any highlight, it might be because the element is not visible, or it's outside of the viewport. 18 | 19 | When you hover over a context, or have a specific context selected (everywhere within the extension), two buttons will appear on the right of the context: **Inspect** and **Scroll into view**. 20 | 21 | ![Screenshot of a context with the two buttons visible](docs/context-actions.png) 22 | 23 | **Inspect** will select the DOM element in the elements panel. **Scroll into view** will scroll the page in order to show the DOM element related to the context. 24 | 25 | If you click on a context, it will be selected and some in-depth information will be shown on the sidepane. 26 | 27 | ### The panel sidepane 28 | The sidepane shows information related to the stacking context that is currently selected in the tree view. It is divided into two sections: **Info about the context** and **Children z-index order**. 29 | 30 | In the first section, you can see why this DOM element is creating a new stacking context. There might be multiple reasons for this, and all of them will be shown as an unordered list in this section. 31 | 32 | In the second section, you can find a list of all the stacking contexts that are children of the selected context. They'll be ordered from the one which is visible on top (higher z-index) to the one that will show behind everything else (lower z-index). It will also show all the elements that have no z-index applied, but have a position of "relative" or "absolute", since these values will slightly alter the order the elements are printed (might be changed in the future). When you hover over the contexts in this view, two buttons will appear on the right (the same as in the tree view). They allow you to inspect the related DOM element or scroll the viewport to show it. 33 | 34 | ## The Stacking Contexts sidebar in the Elements panel 35 | When you install this extension, a new sidebar will be added to the right of the elements panel, where you can also find the "Styles", "Computed", "Layout" and other sidebars. It might not be immediatly visible, in that case you can make the sidebar larger to show all options, or click on the "»" button in the top-right to show all the hidden sidebars names. 36 | 37 | ![Screenshot of the Stacking Contexts sidebar in the elements panel](docs/sidebar-screenshot.png) 38 | 39 | In the first option bar, you can see a button with a "refresh" icon (on the right of "Node details"). Press that button whenever there are some changes in the page that could impact the stacking contexts. 40 | 41 | The sidebar is divided into multiple sections: 42 | 43 | ### Node details 44 | This section will give you an immediate feedback on whether or not the z-index property is working on the DOM element. If the element selected has no z-index and doesn't create a stacking context, a coherent message will appear. 45 | 46 | ### Context details 47 | This section will show an unordered list that explains why the current element is creating a new stacking context. If you don't want it to be creating a context, you can use this information to know where to tweak your CSS. 48 | 49 | ### Parent context 50 | This is very self-explanatory: it just shows which element is the parent stacking context. It will show you the element which is the closest ancestor to the currently selected element, and which also creates a stacking context. 51 | 52 | ### Siblings z-index order 53 | Similarly to the "children z-index order" in the Stacking Context panel, it will show a list of stacking contexts along with their z-indexes, in order from the one that shows on top to the one that shows behind everything (from highest to lowest z-index). 54 | The stacking contexts shown in this list are the ones that directly compare to the current element on their z-index values, since they're all children of the same stacking context. The currently selected context will also appear on this list. 55 | -------------------------------------------------------------------------------- /docs/context-actions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/docs/context-actions.png -------------------------------------------------------------------------------- /docs/panel-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/docs/panel-screenshot.png -------------------------------------------------------------------------------- /docs/sidebar-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/docs/sidebar-screenshot.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "css-stacking-context-inspector", 3 | "version": "1.1.15", 4 | "description": "Help inspecting the css stacking contexts and solving the z-index war.", 5 | "scripts": { 6 | "test": "echo \"Error: no test specified\" && exit 1", 7 | "build-dev": "parcel build src/scripts/background.js src/**/*.html src/scripts/content.js --no-optimize", 8 | "build": "parcel build src/scripts/background.js src/**/*.html src/scripts/content.js" 9 | }, 10 | "author": "Andrea Dragotta ", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@parcel/transformer-sass": "^2.0.0-beta.3.1", 14 | "autoprefixer": "^10.3.1", 15 | "parcel": "^2.0.0-beta.3.1", 16 | "postcss": "^8.3.6", 17 | "postcss-modules": "^3.2.2", 18 | "sass": "^1.32.8" 19 | }, 20 | "dependencies": { 21 | "@andreadev/bg-script": "^1.1.10", 22 | "@parcel/config-default": "^2.0.0-beta.3.1", 23 | "@parcel/transformer-js": "^2.0.0-beta.3.1", 24 | "@parcel/transformer-raw": "^2.0.0-beta.3.1", 25 | "@parcel/transformer-svg-react": "^2.0.0-beta.3.1", 26 | "@svgr/parcel-plugin-svgr": "^5.5.0", 27 | "parcel-reporter-static-files-copy": "^1.3.0", 28 | "preact": "^10.5.12" 29 | }, 30 | "browserslist": [ 31 | "Chrome 50" 32 | ] 33 | } 34 | -------------------------------------------------------------------------------- /src/colors.scss: -------------------------------------------------------------------------------- 1 | #root { 2 | --pageBg: #fff; 3 | --bodyColor: #333; 4 | --bodyPrimaryColor: #111; 5 | --borderColor: #ccc; 6 | --optionBarBackground: #f3f3f3; 7 | --icon: #777; 8 | --iconHover: #333; 9 | --iconActive: rgb(138 180 248); 10 | --contextBg: transparent; 11 | --contextHoverBg: #eee; 12 | --contextSelectedBg: #cfe8fc; 13 | --elementTagNameColor: rgb(0, 0, 170); 14 | --elementIdColor: rgb(255, 0, 0); 15 | --elementClassesColor: rgb(0, 136, 0); 16 | --elementInertColor: #333; 17 | --elementPseudoColor: #e21dd5; 18 | --tagBg: #ddd; 19 | --btnBg: #fff; 20 | --btnColor: #333; 21 | --btnBorderColor: #ccc; 22 | --btnHoverBg: #eee; 23 | --btnHoverColor: #000; 24 | } 25 | 26 | #root.theme-dark { 27 | --pageBg: hsl(225, 6%, 13%); 28 | --bodyColor: #aaa; 29 | --bodyPrimaryColor: rgb(232, 234, 237); 30 | --borderColor: #3d3d3d; 31 | --optionBarBackground: hsl(225, 5%, 17%); 32 | --icon: #888; 33 | --iconHover: #bbb; 34 | --iconActive: rgb(138 180 248); 35 | --contextBg: transparent; 36 | --contextHoverBg: rgb(34, 42, 54); 37 | --contextSelectedBg: rgb(7, 61, 105); 38 | --elementTagNameColor: rgb(93, 176, 215); 39 | --elementIdColor: rgb(230, 130, 130); 40 | --elementClassesColor: rgb(242, 151, 102); 41 | --elementInertColor: rgb(192, 192, 192); 42 | --elementPseudoColor: #ed77e5; 43 | --tagBg: rgb(34, 42, 54); 44 | --scrollbarCornerBg: #242424; 45 | --scrollbarThumbBg: #333; 46 | --scrollbarTrackBg: rgb(36,36,36); 47 | --btnBg: hsl(225, 6%, 13%); 48 | --btnColor: #aaa; 49 | --btnBorderColor: #3d3d3d; 50 | --btnHoverBg: rgb(34, 42, 54); 51 | --btnHoverColor: #fff; 52 | } 53 | 54 | #root { 55 | color: var(--bodyColor); 56 | background-color: var(--pageBg); 57 | height: 100%; 58 | overflow: auto; 59 | } 60 | 61 | #root.theme-dark::-webkit-scrollbar, 62 | #root.theme-dark *::-webkit-scrollbar { 63 | width: 14px; 64 | height: 14px; 65 | } 66 | 67 | #root.theme-dark::-webkit-scrollbar-corner, 68 | #root.theme-dark *::-webkit-scrollbar-corner { 69 | background-color: var(--scrollbarCornerBg); 70 | } 71 | 72 | #root.theme-dark::-webkit-scrollbar-thumb, 73 | #root.theme-dark *::-webkit-scrollbar-thumb { 74 | background-color: var(--scrollbarThumbBg); 75 | box-shadow: inset 0 0 1px rgb(255 255 255 / 50%); 76 | } 77 | 78 | #root.theme-dark::-webkit-scrollbar-track, 79 | #root.theme-dark *::-webkit-scrollbar-track { 80 | background-color: var(--scrollbarTrackBg); 81 | box-shadow: inset 0 0 1px rgb(255 255 255 / 30%); 82 | } 83 | 84 | #root.theme-dark input[type="checkbox"] { 85 | filter: invert(1); 86 | } 87 | -------------------------------------------------------------------------------- /src/global.scss: -------------------------------------------------------------------------------- 1 | :global { 2 | @import './colors.scss'; 3 | } 4 | 5 | html, body { 6 | height: 100%; 7 | margin: 0px; 8 | } 9 | 10 | body { 11 | font-size: 12px; 12 | font-family: 'Segoe UI', Tahoma, sans-serif; 13 | } 14 | 15 | :global(.inline-icon) svg { 16 | width: 15px; 17 | height: 15px; 18 | vertical-align: text-bottom; 19 | } 20 | 21 | :global(.btn) { 22 | font-size: 12px; 23 | background-color: var(--btnBg); 24 | color: var(--btnColor); 25 | padding: 4px 9px; 26 | border: solid 1px var(--btnBorderColor); 27 | border-radius: 4px; 28 | cursor: pointer; 29 | } 30 | 31 | :global(.btn):hover { 32 | background-color: var(--btnHoverBg); 33 | color: var(--btnHoverColor); 34 | } -------------------------------------------------------------------------------- /src/icons/check.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/inspect.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/icons/intoview.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/icons/loader-simple.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/icons/loader.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/refresh.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/icons/settings.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/icons/warning.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/icons/wrong.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/scripts/background.js: -------------------------------------------------------------------------------- 1 | import { BackgroundHandler } from '@andreadev/bg-script'; 2 | import TabStatus from './classes/TabStatus'; 3 | 4 | // Contain all the tabs status (see TabStatus class for more details) 5 | let tabs = new Map(); 6 | 7 | let DEFAULT_SETTINGS = { 8 | "dom-changed-warning": true, 9 | "contexts-click-to-expand": false 10 | }; 11 | 12 | let settings = null; 13 | 14 | 15 | /** 16 | * Initialize the settings inside local storage to their default values 17 | */ 18 | function initExtensionSettings() { 19 | chrome.storage.local.set({ "settings": DEFAULT_SETTINGS }); 20 | } 21 | 22 | /** 23 | * Load the extension settings 24 | * 25 | * @returns {Promise} 26 | */ 27 | function loadExtensionSettings() { 28 | return new Promise( (resolve, reject) => { 29 | try { 30 | chrome.storage.local.get(["settings"], (result) => { 31 | settings = result.settings; 32 | resolve(settings); 33 | }); 34 | } 35 | catch (e) { 36 | reject(e); 37 | } 38 | }); 39 | } 40 | 41 | /** 42 | * Send a message to the extension panels to warn that the contexts should be refreshed. 43 | */ 44 | async function notifySettingsChanged(tabId) { 45 | let panelConnection = await getScriptConnection("panel", tabId, false); 46 | let sidebarConnection = await getScriptConnection("sidebar", tabId, false); 47 | 48 | if (panelConnection) { 49 | await panelConnection.setShouldUpdateSettings(true); 50 | } 51 | if (sidebarConnection) { 52 | await sidebarConnection.setShouldUpdateSettings(true); 53 | } 54 | } 55 | 56 | /** 57 | * Save the new settings in the local chrome extension storage 58 | * 59 | * @param {Object} newSettings The updated settings 60 | * @returns {Promise} 61 | */ 62 | function saveExtensionSettings(newSettings) { 63 | return new Promise( (resolve, reject) => { 64 | try { 65 | chrome.storage.local.set({ settings: newSettings }, () => { 66 | 67 | resolve(); 68 | }); 69 | } 70 | catch (e) { 71 | reject(e); 72 | } 73 | }); 74 | } 75 | 76 | /** 77 | * Inject the content script into a tab 78 | */ 79 | function injectScript(tabId) { 80 | return chrome.scripting.executeScript({ 81 | target: {tabId}, 82 | files: ['/scripts/content.js'], 83 | }); 84 | } 85 | 86 | /** 87 | * Get a script connection, and if there is no script associated with the tab, it injects it. 88 | * 89 | * @param {string} scriptId 90 | * @param {number} tabId 91 | * @param {boolean} analysePage Only when the scriptId is 'content', it forces a page analysis before returning the connection. 92 | * @returns {Promise} The connection to the content script 93 | */ 94 | async function getScriptConnection(scriptId, tabId, injectOnFail=true) { 95 | 96 | if (!bgHandler.hasConnectedScript(scriptId, tabId)) { 97 | // Not returning anything if there is no script attached and we don't want to automatically inject the content script 98 | if (!injectOnFail) return undefined; 99 | 100 | await injectScript(tabId); 101 | } 102 | 103 | let conn = await bgHandler.getScriptConnection(scriptId, tabId); 104 | 105 | if (conn) return conn; 106 | } 107 | 108 | /** 109 | * Analyzes the page related to a specific tab id and returns a JSON description of the stacking contexts. 110 | * 111 | * @param {number} tabId 112 | * @returns {Array} The list of contexts in the page. 113 | */ 114 | async function analysePage(tabId) { 115 | let connection = await getScriptConnection("content", tabId); 116 | 117 | await connection.analysePage(); 118 | let contexts = await connection.getAllContextsJSON(); 119 | return contexts; 120 | } 121 | 122 | /** 123 | * Highlight a context on a specific page. 124 | * 125 | * @param {number} tabId 126 | * @param {number} contextId The id of the context to be highlighted 127 | */ 128 | async function highlightContext(tabId, contextId) { 129 | let connection = await getScriptConnection("content", tabId); 130 | await connection.highlightContext(contextId); 131 | } 132 | 133 | /** 134 | * Remove the highlight that was set on the context. 135 | * 136 | * @param {number} tabId 137 | */ 138 | async function undoHighlightContext(tabId) { 139 | let connection = await getScriptConnection("content", tabId); 140 | await connection.undoHighlightContext(); 141 | } 142 | 143 | /** 144 | * Scrolls the page related to a specific tab id in order to show a context. 145 | * 146 | * @param {number} tabId 147 | * @param {number} contextId 148 | */ 149 | async function scrollToContext(tabId, contextId) { 150 | let connection = await getScriptConnection("content", tabId); 151 | await connection.scrollToContext(contextId); 152 | } 153 | 154 | async function getPathFromContext(tabId, contextId) { 155 | let connection = await getScriptConnection("content", tabId); 156 | return await connection.getPathFromContext(contextId); 157 | } 158 | 159 | /** 160 | * Instruct the content script related to a specific tab to get the details about the last inspected element. 161 | * 162 | * @param {number} tabId 163 | * @param {number} elementIndex The index of the last inspected element inside the DOM 164 | */ 165 | async function detectLastInspectedElement(tabId, elementIndex) { 166 | let connection = await getScriptConnection("content", tabId); 167 | await connection.detectLastInspectedElement(elementIndex); 168 | } 169 | 170 | /** 171 | * Get some details about the last selected element and its context 172 | * 173 | * @param {number} tabId 174 | * @returns {Object} The element details 175 | */ 176 | async function getInspectedElementDetails(tabId) { 177 | let connection = await getScriptConnection("content", tabId); 178 | let elementDetails = await connection.getInspectedElementDetails(); 179 | 180 | return elementDetails; 181 | } 182 | 183 | async function getPageFramesSources(tabId) { 184 | let connection = await getScriptConnection("content", tabId); 185 | let sources = await connection.getPageFramesSources(); 186 | return sources; 187 | } 188 | 189 | /** 190 | * Start the DOM Observer 191 | * 192 | * @param {number} tabId 193 | */ 194 | async function startDOMObserver(tabId) { 195 | let connection = await getScriptConnection("content", tabId); 196 | console.log("Starting DOM Observer...") 197 | await connection.startDOMObserver(); 198 | } 199 | 200 | /** 201 | * Stop the DOM Observer 202 | * 203 | * @param {number} tabId 204 | */ 205 | async function stopDOMObserver(tabId) { 206 | let connection = await getScriptConnection("content", tabId); 207 | console.log("Stopping DOM Observer...") 208 | await connection.stopDOMObserver(); 209 | } 210 | 211 | /** 212 | * Send a message to the extension panels to warn that the contexts should be refreshed. 213 | */ 214 | async function sendDOMChangedWarning(tabId) { 215 | let panelConnection = await getScriptConnection("panel", tabId, false); 216 | let sidebarConnection = await getScriptConnection("sidebar", tabId, false); 217 | 218 | if (panelConnection) { 219 | await panelConnection.setShouldUpdate(true); 220 | } 221 | if (sidebarConnection) { 222 | await sidebarConnection.setShouldUpdate(true); 223 | } 224 | } 225 | 226 | /** 227 | * Update the devtools pages visibility status in order to decide whether to start or stop the DOM observer 228 | * 229 | * @param {number} tabId The tab id 230 | * @param {string} scriptId The script id 231 | * @param {boolean} visibilityStatus The current visibility status of the page where the specified script id is used 232 | */ 233 | function updateDevtoolsPageStatus(tabId, scriptId, isActive) { 234 | console.log(`The script '${scriptId}' related to the tab '${tabId}' is currently ${isActive ? 'active' : 'hidden'}`) 235 | 236 | let tabStatus = tabs.get(tabId); 237 | if (tabStatus == undefined) { 238 | tabStatus = new TabStatus(); 239 | tabs.set(tabId, tabStatus); 240 | } 241 | 242 | let isInspected = tabStatus.isBeingInspected; 243 | 244 | switch (scriptId) { 245 | case "panel": 246 | tabStatus.isPanelActive = isActive; 247 | break 248 | case "sidebar": 249 | tabStatus.isSidebarActive = isActive; 250 | break; 251 | default: 252 | return; 253 | } 254 | 255 | let isCurrentlyInspected = tabStatus.isBeingInspected; 256 | 257 | // If the overall page inspected status has changed, decide whether to start or stop the DOM Observer 258 | if (isInspected == isCurrentlyInspected) return; 259 | 260 | if (isCurrentlyInspected) { 261 | startDOMObserver(tabId); 262 | } 263 | else { 264 | stopDOMObserver(tabId); 265 | } 266 | } 267 | 268 | /** 269 | * Handle the background handler errors (right now it just prints them to the console) 270 | */ 271 | function onHandlerError(details) { 272 | console.log(details.errorId); 273 | console.error(details.error); 274 | } 275 | 276 | let bgHandler = new BackgroundHandler({ 277 | analysePage, 278 | highlightContext, 279 | undoHighlightContext, 280 | scrollToContext, 281 | getPathFromContext, 282 | detectLastInspectedElement, 283 | getInspectedElementDetails, 284 | getPageFramesSources, 285 | sendDOMChangedWarning, 286 | updateDevtoolsPageStatus, 287 | loadExtensionSettings, 288 | saveExtensionSettings, 289 | notifySettingsChanged 290 | }, { 291 | errorCallback: onHandlerError 292 | }); 293 | 294 | /** 295 | * Initialization method 296 | */ 297 | function init() { 298 | // Add "connection received" handler to update tab status 299 | bgHandler.addListener("connectionreceived", ({scriptId, tabId}) => { 300 | // Devtools script are tab-agnostic by default, so I'm appending the tab id to it using `scriptid-tabid` format 301 | if (tabId == null) { 302 | // Find the tab id delimiter 303 | let delimiter = scriptId.search("-"); 304 | // Get the tab id 305 | tabId = parseInt(scriptId.substring(delimiter + 1)); 306 | // Get the clean script id 307 | scriptId = scriptId.substring(0, delimiter); 308 | // Notify the change 309 | updateDevtoolsPageStatus(tabId, scriptId, true); 310 | } 311 | }); 312 | 313 | // Add "connection ended" handler to update tab status 314 | bgHandler.addListener("connectionended", ({scriptId, tabId}) => { 315 | // Devtools script are tab-agnostic by default, so I'm appending the tab id to it using `scriptid-tabid` format 316 | if (tabId == null) { 317 | // Find the tab id delimiter 318 | let delimiter = scriptId.search("-"); 319 | // Get the tab id 320 | tabId = parseInt(scriptId.substring(delimiter + 1)); 321 | // Get the clean script id 322 | scriptId = scriptId.substring(0, delimiter); 323 | // Notify the change 324 | updateDevtoolsPageStatus(tabId, scriptId, false); 325 | } 326 | }); 327 | 328 | // Initialize Settings 329 | loadExtensionSettings() 330 | .then((settings) => { 331 | if (!settings) { 332 | initExtensionSettings(); 333 | } 334 | }) 335 | .catch(() => { 336 | initExtensionSettings(); 337 | }); 338 | } 339 | 340 | init(); 341 | -------------------------------------------------------------------------------- /src/scripts/classes/ContextChecks.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates a new check to do on a DOM element to make sure if it is a stacking context. 3 | * 4 | * @property {string} description This text will be shown in the devtools to explain why the element is a stacking context. 5 | * @property {function} callback To this callback will be passed the DOM element and its computed style. It must return 6 | * wether or not the check is passed. 7 | */ 8 | export class ContextCheck { 9 | 10 | description = ""; 11 | callback = null; 12 | 13 | constructor(description, callback) { 14 | if (!description || !callback) { 15 | throw "Both a description and a callback are needed to create a new context check" 16 | } 17 | this.description = description; 18 | this.callback = callback; 19 | } 20 | 21 | /** 22 | * Executes the check callback on the element passed as a parameter. Returns wether or not this Node creates a static context. 23 | * 24 | * @param {Node} element The element to check. 25 | * @param {CSSStyleDeclaration} elementStyle Optional. The element computed styles (it will be loaded if not passed) 26 | * @returns {boolean} 27 | */ 28 | exec(element, elementStyle) { 29 | if (!element) throw "Cannot execute a check without an element"; 30 | if (!elementStyle) { 31 | elementStyle = window.getComputedStyle(element); 32 | } 33 | 34 | return this.callback(element, elementStyle); 35 | } 36 | 37 | } 38 | 39 | /* 40 | * List of checks to do on the element in order for it to create a new stacking context. 41 | * These checks are based on this MDN article: 42 | * https://developer.mozilla.org/en-US/docs/Web/CSS/CSS_Positioning/Understanding_z_index/The_stacking_context 43 | */ 44 | 45 | export const zIndexWithPosition = new ContextCheck( 46 | "The element has a z-index set and the position is not 'static'", 47 | (_element, styles) => { 48 | if (styles.position !== "static" && 49 | styles.zIndex !== "auto") 50 | { 51 | return true; 52 | } 53 | return false; 54 | } 55 | ); 56 | 57 | export const positionFixedSticky = new ContextCheck( 58 | "The element position has the value 'fixed' or 'sticky'", 59 | (_element, styles) => { 60 | if (styles.position == "fixed" || 61 | styles.position == "sticky") 62 | { 63 | return true; 64 | } 65 | return false; 66 | } 67 | ); 68 | 69 | export const containerTypeIsSizeOrInlineSize = new ContextCheck( 70 | "The element has a container-type set to 'size' or 'inline-size'", 71 | (_element, styles) => { 72 | return ( 73 | styles.containerType == "size" || 74 | styles.containerType == "inline-size" 75 | ); 76 | } 77 | ); 78 | 79 | export const flexChildWithZIndex = new ContextCheck( 80 | "This element is a child of a flex container and has a z-index set", 81 | (element, styles) => { 82 | if (element.parentElement) { 83 | let parentStyles = window.getComputedStyle(element.parentElement); 84 | 85 | if (parentStyles.display == "flex" && styles.zIndex !== "auto") { 86 | return true; 87 | } 88 | } 89 | return false; 90 | } 91 | ); 92 | 93 | export const gridChildWithZIndex = new ContextCheck( 94 | "This element is a child of a grid container and has a z-index set", 95 | (element, styles) => { 96 | if (element.parentElement) { 97 | let parentStyles = window.getComputedStyle(element.parentElement); 98 | 99 | if (parentStyles.display == "grid" && styles.zIndex !== "auto") { 100 | return true; 101 | } 102 | } 103 | return false; 104 | } 105 | ); 106 | 107 | export const notFullOpacity = new ContextCheck( 108 | "The element has an opacity value smaller than 1", 109 | (_element, styles) => { 110 | 111 | if (styles.opacity == "auto") return false; 112 | 113 | let opacity = parseFloat(styles.opacity); 114 | 115 | if (isNaN(opacity) || opacity == 1) return false; 116 | 117 | return true; 118 | } 119 | ); 120 | 121 | export const mixBlendMode = new ContextCheck( 122 | "The element has a non-default mix blend mode value", 123 | (_element, styles) => { 124 | if (styles.mixBlendMode !== "normal") { 125 | return true; 126 | } 127 | return false; 128 | } 129 | ); 130 | 131 | export const notNoneProperties = new ContextCheck( 132 | "The element has one of the following properties set: transform, filter, perspective, clip-path, mask, maskImage, maskBorder", 133 | (_element, styles) => { 134 | let toCheck = [ 135 | styles.transform, 136 | styles.filter, 137 | styles.perspective, 138 | styles.clipPath, 139 | styles.mask, styles.webkitMask, 140 | styles.maskImage, styles.webkitMaskImage, 141 | styles.maskBorder, styles.webkitMaskBoxImage 142 | ]; 143 | return toCheck.some( (prop) => prop !== undefined && prop !== "none" && prop !== "" ); 144 | } 145 | ); 146 | 147 | export const isolationSet = new ContextCheck( 148 | "The element has the isolation property set to 'isolate'", 149 | (_element, styles) => styles.isolation == "isolate" 150 | ); 151 | 152 | export const willChange = new ContextCheck( 153 | "The element has a will-change value with a property that will create a context when its value is not the default one", 154 | (_element, styles) => { 155 | let toCheck = ["mix-blend-mode", 156 | "transform", 157 | "filter", 158 | "perspective", 159 | "clip-path", 160 | "mask", "-webkit-mask", 161 | "mask-image", "-webkit-mask-image", 162 | "mask-border", "-webkit-mask-box-image" 163 | ] 164 | return toCheck.some( (prop) => styles.willChange.includes(prop) ); 165 | } 166 | ); 167 | 168 | export const containValue = new ContextCheck( 169 | "The element has a contain value that includes one of the following: layout, paint (or a composite value like 'strict' or 'content')", 170 | (_element, styles) => { 171 | let toCheck = ["layout", "paint", "strict", "content"]; 172 | return toCheck.some( (prop) => styles.contain.includes(prop) ); 173 | } 174 | ); 175 | 176 | export const webkitOverflowScrolling = new ContextCheck( 177 | "The element has the webkit-overflow-scrolling property set to 'touch'", 178 | (_element, styles) => styles.webkitOverflowScrolling == "touch" 179 | ); 180 | 181 | /** 182 | * A list of checks that will be fired on the elements. 183 | */ 184 | export const activeChecks = [ 185 | zIndexWithPosition, 186 | positionFixedSticky, 187 | containerTypeIsSizeOrInlineSize, 188 | flexChildWithZIndex, 189 | gridChildWithZIndex, 190 | notFullOpacity, 191 | mixBlendMode, 192 | notNoneProperties, 193 | isolationSet, 194 | willChange, 195 | containValue, 196 | webkitOverflowScrolling, 197 | ]; 198 | -------------------------------------------------------------------------------- /src/scripts/classes/ContextsContainer.js: -------------------------------------------------------------------------------- 1 | import StackingContext from './StackingContext'; 2 | 3 | /** 4 | * Class to represent a document container or an iframe. It should just be a wrapper around contexts 5 | * 6 | * @class 7 | * @extends StackingContext 8 | * @property {string} name 9 | * @property {string} type Makes it easy to tell containers apart from normal stacking contexts 10 | */ 11 | 12 | class ContextsContainer extends StackingContext { 13 | 14 | name = ""; 15 | type = "container"; 16 | 17 | /** 18 | * Creates a new ContextsContainer 19 | * 20 | * @param {string} name The container name 21 | * @param {Node} element The element related to this container (documentElement) 22 | * @param {boolean} isInIframe (only for superclass usage) 23 | * @param {Node} frame The iframe that contains this element 24 | * @param {StackingContext} parent The parent stacking context (or container) 25 | */ 26 | constructor(name, element=null, isInIframe, frame=null, parent=null) { 27 | super(element, isInIframe, frame, parent); 28 | 29 | if (!name) throw "The container must have a name"; 30 | 31 | this.name = name; 32 | } 33 | 34 | /** 35 | * @returns A JSON-friendly object to represent this container 36 | */ 37 | toJSON() { 38 | return { 39 | id: this.id, 40 | type: "container", 41 | name: this.name, 42 | children: this.children.map((c) => c.id), 43 | parent: this.parent?.id 44 | }; 45 | } 46 | } 47 | 48 | export default ContextsContainer; -------------------------------------------------------------------------------- /src/scripts/classes/StackingContext.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class will be used to represent a static context. 3 | * 4 | * @property {number} id 5 | * @property {StackingContext} parent The parent static context. 6 | * @property {Array} children The static context children. 7 | * @property {Node} element The element that is creating the static context. 8 | * @property {Node} isInIframe Variable to check if this stacking context is inside an iframe. 9 | * @property {Node} frame The node that function as the frame for this context element, it might be undefined for the top frame or an iframe element. 10 | * @property {Array} passedChecks A list of strings that enumerates all the reason why this static context has been created. 11 | */ 12 | class StackingContext { 13 | 14 | id = null; 15 | parent = null; 16 | children = []; 17 | element = null; 18 | pseudoElement = null; 19 | 20 | passedChecks = [] 21 | 22 | /** 23 | * Creates a new Stacking Context object 24 | * 25 | * @param {Node} element 26 | * @param {boolean} isInIframe 27 | * @param {Node} frame 28 | * @param {StackingContext} parentContext 29 | * @param {Array} passedChecks 30 | */ 31 | constructor(element, isInIframe, frame=null, parentContext, passedChecks = [], pseudoElement = null) { 32 | this.element = element; 33 | this.isInIframe = isInIframe ?? false; 34 | this.frame = frame; 35 | this.parent = parentContext ?? null; 36 | this.passedChecks = passedChecks; 37 | this.pseudoElement = pseudoElement; 38 | } 39 | 40 | /** 41 | * Adds a static context as a children of this context 42 | * 43 | * @param {StackingContext} context 44 | */ 45 | addChild(context) { 46 | this.children.push(context); 47 | } 48 | 49 | /** 50 | * This will convert a stacking context to a JSON-friendly object in order to pass it to the background script and the devtools panel and sidebar. 51 | * 52 | * @returns The JSON-friendly static context representation 53 | */ 54 | toJSON() { 55 | let parentId = this.parent?.id ?? null; 56 | let childrenIds = this.children.map( (child) => child.id ); 57 | 58 | let classes = []; 59 | if (this.element?.classList?.length > 0) { 60 | classes = [...this.element.classList]; 61 | } 62 | 63 | let allElements = Array.from(document.getElementsByTagName('*')); 64 | let elementIndex = (this.element) ? allElements.indexOf(this.element) : null; 65 | 66 | let elementStyles = window.getComputedStyle(this.element); 67 | 68 | let elementDescription = { 69 | tagName: this.element?.tagName?.toLowerCase() ?? "document", 70 | id: this.element?.id ?? "", 71 | classes, 72 | index: (elementIndex >= 0) ? elementIndex : null, 73 | styles: { 74 | zIndex: elementStyles.zIndex, 75 | position: elementStyles.position 76 | } 77 | } 78 | 79 | return { 80 | id: this.id, 81 | parent: parentId, 82 | children: childrenIds, 83 | element: elementDescription, 84 | passedChecks: this.passedChecks, 85 | isInIframe: this.isInIframe, 86 | pseudoElement: this.pseudoElement 87 | }; 88 | } 89 | } 90 | 91 | export default StackingContext; 92 | -------------------------------------------------------------------------------- /src/scripts/classes/TabStatus.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This class is used to represent the status of a tab. It contains informations on whether or not this 3 | * tab is currently inspected and which script they are connected to (panel, sidebar or both). 4 | * This class is manly used to check whether or not the DOM Observer should run. 5 | * 6 | * @property {boolean} isPanelActive Indicates whether the tab is being inspected with the Stacking Context Inspector panel or not 7 | * @property {boolean} isSidebarActive Indicates whether the tab is being inspected from the Stacking Context sidebar within the Elements panel or not 8 | */ 9 | class TabStatus { 10 | isPanelActive = false; 11 | isSidebarActive = false; 12 | 13 | get isBeingInspected() { 14 | return this.isPanelActive || this.isSidebarActive; 15 | } 16 | } 17 | 18 | export default TabStatus; -------------------------------------------------------------------------------- /src/scripts/components/ConnectionContext.js: -------------------------------------------------------------------------------- 1 | import { h, createContext } from 'preact'; 2 | import { useState, useEffect } from 'preact/hooks'; 3 | import { BackgroundScript } from '@andreadev/bg-script'; 4 | 5 | export const ConnectionContext = createContext(); 6 | 7 | let bgScript = null; 8 | 9 | async function notifyVisibilityChange(status) { 10 | let isActive = status ?? !document.hidden; 11 | 12 | let connection = await bgScript.getConnection(); 13 | let tabId = chrome.devtools.inspectedWindow.tabId; 14 | await connection.updateDevtoolsPageStatus(tabId, bgScript.scriptId, isActive); 15 | } 16 | 17 | const ConnectionContextProvider = ({ scriptId, context, children }) => { 18 | 19 | let [shouldUpdate, rawSetShouldUpdate] = useState(false); 20 | let [shouldUpdateSettings, rawSetShouldUpdateSettings] = useState(true); 21 | 22 | // I did this because I didn't felt like passing the state update function to the library (I thought it could cause problems) 23 | const setShouldUpdate = (value) => { 24 | rawSetShouldUpdate(value); 25 | } 26 | 27 | const setShouldUpdateSettings = (value) => { 28 | rawSetShouldUpdateSettings(value); 29 | } 30 | 31 | const getConnection = async () => { 32 | return await bgScript.getConnection(); 33 | } 34 | 35 | if (bgScript == null) { 36 | // Init the Background Script handler 37 | bgScript = new BackgroundScript(scriptId, { setShouldUpdate, setShouldUpdateSettings }, { context }); 38 | // Add the visibilitychange event listener and immediatly notify the current status 39 | window.addEventListener("visibilitychange", () => notifyVisibilityChange()); 40 | // Handle disconnection 41 | window.addEventListener("unload", () => notifyVisibilityChange(false)); 42 | notifyVisibilityChange(); 43 | } 44 | 45 | let providerValue = { 46 | getConnection, 47 | shouldUpdate, 48 | setShouldUpdate, 49 | shouldUpdateSettings, 50 | setShouldUpdateSettings 51 | } 52 | 53 | return ( 54 | 55 | {children} 56 | 57 | ); 58 | } 59 | 60 | export default ConnectionContextProvider; -------------------------------------------------------------------------------- /src/scripts/components/Context/Context.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useContext } from 'preact/hooks'; 3 | import SVG from '../SVG'; 4 | import { ConnectionContext } from '../ConnectionContext'; 5 | import { getNodeFromPath } from '../../utils/utils'; 6 | 7 | import InspectIcon from '../../../icons/inspect.svg'; 8 | import IntoviewIcon from '../../../icons/intoview.svg'; 9 | import * as styles from "./Context.scss"; 10 | 11 | const Context = ({context, noHighlight=false, delegatedClass="", ...props}) => { 12 | 13 | let {tagName, id, classes, index} = context.element; 14 | let tabId = chrome.devtools.inspectedWindow.tabId; 15 | 16 | let { getConnection } = useContext(ConnectionContext); 17 | 18 | let inspectNode = async () => { 19 | let connection = await getConnection(); 20 | let path = await connection.getPathFromContext(tabId, context.id); 21 | let command = ` 22 | inspect( 23 | ( 24 | ${getNodeFromPath.toString()} 25 | )(${ JSON.stringify(path) }) 26 | ) 27 | `; 28 | chrome.devtools.inspectedWindow.eval(command); 29 | } 30 | 31 | let scrollToNode = async () => { 32 | let connection = await getConnection(); 33 | await connection.scrollToContext(tabId, context.id); 34 | } 35 | 36 | let highlightNode = async () => { 37 | if (noHighlight) return; 38 | 39 | let connection = await getConnection(); 40 | await connection.highlightContext(tabId, context.id); 41 | } 42 | 43 | let cancelHighlight = async () => { 44 | if (noHighlight) return; 45 | 46 | let connection = await getConnection(); 47 | await connection.undoHighlightContext(tabId); 48 | } 49 | 50 | let classNames = `${styles.context} ${delegatedClass}`; 51 | let pseudo = context.pseudoElement; 52 | 53 | return ( 54 |
highlightNode()} onMouseLeave={() => cancelHighlight()} {...props}> 55 |
56 | {tagName} 57 | { 58 | pseudo && ( 59 | :{pseudo} 60 | ) 61 | } 62 | { 63 | !pseudo && id && ( 64 | #{id} 65 | ) 66 | } 67 | { 68 | !pseudo && !id && classes && classes.length > 0 && ( 69 | .{classes.join('.')} 70 | ) 71 | } 72 |
73 |
74 | inspectNode() } /> 75 | scrollToNode() } /> 76 |
77 |
78 | ); 79 | } 80 | 81 | export default Context; 82 | -------------------------------------------------------------------------------- /src/scripts/components/Context/Context.scss: -------------------------------------------------------------------------------- 1 | .context { 2 | position: relative; 3 | padding: 3px 5px; 4 | cursor: default; 5 | flex-grow: 1; 6 | overflow: hidden; 7 | font-family: 'consolas', 'lucida console', 'courier new', monospace; 8 | 9 | &:hover { 10 | background-color: var(--contextHoverBg); 11 | } 12 | 13 | .contextDescriptor { 14 | position: relative; 15 | }; 16 | 17 | .pseudo { 18 | color: var(--elementPseudoColor); 19 | } 20 | 21 | .contextTag { 22 | font-weight: bold; 23 | color: var(--elementTagNameColor); 24 | } 25 | 26 | .contextId { 27 | color: var(--elementIdColor); 28 | } 29 | 30 | .contextClasses { 31 | color: var(--elementClassesColor); 32 | } 33 | 34 | .contextActions { 35 | display: none; 36 | position: absolute; 37 | box-sizing: border-box; 38 | right: 0px; 39 | top: 0px; 40 | height: 100%; 41 | padding: 0em 1em; 42 | align-items: center; 43 | background-color: var(--contextHoverBg); 44 | } 45 | 46 | &:hover .contextActions { 47 | display: flex; 48 | } 49 | 50 | .contextActions i { 51 | padding: 5px; 52 | cursor: pointer; 53 | } 54 | 55 | .contextActions i > svg { 56 | fill: var(--icon); 57 | } 58 | 59 | .contextActions i:hover > svg { 60 | fill: var(--iconHover); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/scripts/components/ContextDetails/ContextDetails.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | 3 | const ContextDetails = ({context}) => { 4 | return ( 5 | <> 6 | { 7 | context && context.passedChecks?.length > 0 && ( 8 | <> 9 |
10 | Z-index value: {context.element.styles.zIndex} 11 |
12 |
    13 | { 14 | context.passedChecks.map( (check) => ( 15 |
  • 16 | {check} 17 |
  • 18 | )) 19 | } 20 |
21 | 22 | ) 23 | } 24 | { 25 | context && context.passedChecks?.length == 0 && ( 26 | <> 27 | This is just a container 28 | 29 | ) 30 | } 31 | 32 | ); 33 | } 34 | 35 | export default ContextDetails; 36 | -------------------------------------------------------------------------------- /src/scripts/components/ContextsContainer/ContextsContainer.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useContext } from 'preact/hooks'; 3 | import { ConnectionContext } from '../ConnectionContext'; 4 | 5 | import styles from "./ContextsContainer.scss"; 6 | 7 | const ContextsContainer = ({container, noHighlight, delegatedClass, ...props}) => { 8 | 9 | let tabId = chrome.devtools.inspectedWindow.tabId; 10 | let { getConnection } = useContext(ConnectionContext); 11 | 12 | 13 | let highlightNode = async () => { 14 | if (noHighlight) return; 15 | 16 | let connection = await getConnection(); 17 | await connection.highlightContext(tabId, container.id); 18 | } 19 | 20 | let cancelHighlight = async () => { 21 | if (noHighlight) return; 22 | 23 | let connection = await getConnection(); 24 | await connection.undoHighlightContext(tabId); 25 | } 26 | 27 | let classes = `${styles.contextsContainer} ${delegatedClass}`; 28 | 29 | return ( 30 |
highlightNode()} 33 | onMouseLeave={() => cancelHighlight()} 34 | {...props} 35 | > 36 | {container.name} 37 |
38 | ); 39 | } 40 | 41 | export default ContextsContainer; -------------------------------------------------------------------------------- /src/scripts/components/ContextsContainer/ContextsContainer.scss: -------------------------------------------------------------------------------- 1 | .contextsContainer { 2 | padding: 3px 5px; 3 | cursor: default; 4 | flex-grow: 1; 5 | overflow: hidden; 6 | font-family: 'consolas', 'lucida console', 'courier new', monospace; 7 | font-style: italic; 8 | font-weight: bold; 9 | color: var(--bodyColor); 10 | } -------------------------------------------------------------------------------- /src/scripts/components/ContextsTree/ContextsTree.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useEffect, useState, useContext } from 'preact/hooks'; 3 | import { isValuableContainer } from '../../../scripts/utils/utils'; 4 | import { SettingsContext } from '../SettingsContext'; 5 | import TreeItem from '../TreeItem/TreeItem'; 6 | import Spinner from '../../../scripts/components/Spinner/Spinner'; 7 | 8 | import styles from "./ContextsTree.scss"; 9 | 10 | const ContextsTree = ({contexts, onSelectContext, ...props}) => { 11 | 12 | let [contextsState, setContextsState] = useState([]); 13 | let [selected, setSelected] = useState(null); 14 | let { settings } = useContext(SettingsContext); 15 | 16 | useEffect( () => { 17 | let newState = contexts.map( (el, i) => { 18 | return { 19 | visible: false, 20 | open: false, 21 | depth: 0 22 | } 23 | }); 24 | 25 | newState.forEach( (state, i) => { 26 | if (i == 0) return; 27 | 28 | let previous = newState[i-1]; 29 | let previousContext = contexts[i-1]; 30 | let currentContext = contexts[i]; 31 | let depth = (previous) ? previous.depth : 0; 32 | 33 | if (currentContext.parent == previousContext.parent) { 34 | state.depth = previous.depth; 35 | return; 36 | } 37 | if (currentContext.parent == previousContext) { 38 | state.depth = depth + 1; 39 | return; 40 | } 41 | 42 | parentIndex = contexts.findIndex( (c) => c == currentContext.parent ); 43 | state.depth = newState[parentIndex].depth + 1; 44 | }); 45 | 46 | if (newState.length > 0) { 47 | newState[0].visible = true; 48 | } 49 | 50 | setContextsState(newState); 51 | }, contexts); 52 | 53 | let openContext = (contextToOpen, index) => { 54 | let newState = [...contextsState]; 55 | 56 | contexts.forEach( (context, i) => { 57 | if (context.parent == contextToOpen) { 58 | newState[i].visible = true; 59 | } 60 | }); 61 | 62 | newState[index].open = true; 63 | 64 | setContextsState(newState); 65 | } 66 | 67 | let closeContext = (contextToClose, index) => { 68 | let newState = [...contextsState]; 69 | let temp = [contextToClose]; 70 | 71 | contexts.forEach( (context, i) => { 72 | if (temp.includes(context.parent)) { 73 | temp.push(context); 74 | newState[i].visible = false; 75 | newState[i].open = false; 76 | } 77 | }); 78 | 79 | newState[index].open = false; 80 | 81 | setContextsState(newState); 82 | } 83 | 84 | let selectContext = (context) => { 85 | if (context.id == selected) return; 86 | 87 | setSelected(context.id); 88 | onSelectContext(context); 89 | } 90 | 91 | let shouldInsert = (context) => { 92 | if (!context) return false; 93 | if (context?.type == "container" && isValuableContainer(context)) return true; 94 | return context?.type !== "container"; 95 | } 96 | 97 | return ( 98 |
99 | { 100 | contextsState.map( (state, i) => ( 101 | <> 102 | { 103 | shouldInsert(contexts[i]) && ( 104 | ( (state.open) ? closeContext(contexts[i], i) : openContext(contexts[i], i) ) 112 | } 113 | onClick={ 114 | () => (selectContext(contexts[i]) , (!state.open && settings["contexts-click-to-expand"]) ? openContext(contexts[i], i) : null ) 115 | } 116 | /> 117 | ) 118 | } 119 | 120 | )) 121 | } 122 | { 123 | contextsState.length == 0 && ( 124 | 125 | ) 126 | } 127 |
128 | ) 129 | } 130 | 131 | export default ContextsTree; -------------------------------------------------------------------------------- /src/scripts/components/ContextsTree/ContextsTree.scss: -------------------------------------------------------------------------------- 1 | .contextsTree { 2 | position: relative; 3 | } -------------------------------------------------------------------------------- /src/scripts/components/DataContext.js: -------------------------------------------------------------------------------- 1 | import { h, createContext } from 'preact'; 2 | import { useContext, useState } from 'preact/hooks'; 3 | import { generateContextTree } from '../utils/utils'; 4 | import { ConnectionContext } from './ConnectionContext'; 5 | 6 | export const DataContext = createContext(); 7 | 8 | const DataContextProvider = ({ children }) => { 9 | 10 | const { getConnection, setShouldUpdate } = useContext(ConnectionContext); 11 | 12 | const [contexts, setContextsRaw] = useState([]); 13 | 14 | const setContexts = (contextList) => { 15 | // Link all children and parents by their IDs 16 | generateContextTree(contextList); 17 | // Update the context 18 | setContextsRaw(contextList); 19 | } 20 | 21 | const refreshContexts = async () => { 22 | let connection = await getConnection(); 23 | 24 | // TEMP 25 | let tabId = chrome.devtools.inspectedWindow.tabId; 26 | let pageContexts = await connection.analysePage(tabId); 27 | 28 | setContexts(pageContexts); 29 | setShouldUpdate(false); 30 | } 31 | 32 | const cleanContexts = () => { 33 | setContexts([]); 34 | } 35 | 36 | const getPageFrames = async () => { 37 | let connection = await getConnection(); 38 | let tabId = chrome.devtools.inspectedWindow.tabId; 39 | return await connection.getPageFramesSources(tabId); 40 | } 41 | 42 | return ( 43 | 44 | {children} 45 | 46 | ) 47 | } 48 | 49 | export default DataContextProvider; -------------------------------------------------------------------------------- /src/scripts/components/NodeDetails/NodeDetails.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from "preact"; 2 | import SVG from '../SVG'; 3 | import CheckIcon from '../../../icons/check.svg'; 4 | import WrongIcon from '../../../icons/wrong.svg'; 5 | 6 | const NodeDetails = ({node}) => { 7 | 8 | let description = "There is no element currently selected. Please inspect an element or select one from the elements panel."; 9 | let additionalInfo = null; 10 | 11 | if (node) { 12 | 13 | if (node.createsContext) { 14 | description = ( 15 | Is z-index working: 16 | ); 17 | // description = "z-index: working"; 18 | additionalInfo = "The z-index property is correctly working in this element. If the result isn't what you are expecting, please head over to the stacking context panel."; 19 | } 20 | 21 | if (!node.createsContext && node.zIndex !== "auto") { 22 | description = ( 23 | Is z-index working: 24 | ); 25 | // description = "z-index: not working"; 26 | additionalInfo = "The z-index property is set, but it's not working because this element does not create a new stacking context."; 27 | } 28 | else if (!node.createsContext) { 29 | description = "This element does not create a stacking context." 30 | } 31 | } 32 | 33 | return ( 34 |
35 |

{description}

36 | { 37 | additionalInfo && ( 38 |
39 | Info 40 | {additionalInfo} 41 |
42 | ) 43 | } 44 |
45 | ); 46 | } 47 | 48 | export default NodeDetails; 49 | -------------------------------------------------------------------------------- /src/scripts/components/OptionBar/OptionBar.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import OptionBarSeparator from '../OptionBarSeparator/OptionBarSeparator'; 3 | 4 | import * as styles from "./OptionBar.scss"; 5 | 6 | const OptionBar = ({ title, children, ...props }) => ( 7 |
8 | { 9 | title && ( 10 | <> 11 | {title} 12 | { 13 | children && ( 14 | 15 | ) 16 | } 17 | 18 | ) 19 | } 20 | { children } 21 |
22 | ); 23 | 24 | export default OptionBar; 25 | -------------------------------------------------------------------------------- /src/scripts/components/OptionBar/OptionBar.scss: -------------------------------------------------------------------------------- 1 | .optionBar { 2 | display: flex; 3 | align-items: stretch; 4 | height: 27px; 5 | background-color: var(--optionBarBackground); 6 | border-top: solid 1px var(--borderColor); 7 | border-bottom: solid 1px var(--borderColor); 8 | color: var(--bodyColor); 9 | 10 | &:first-child { 11 | border-top: solid 0px; 12 | } 13 | 14 | .optionBarTitle { 15 | display: flex; 16 | align-items: center; 17 | padding: 0px 1em; 18 | } 19 | } -------------------------------------------------------------------------------- /src/scripts/components/OptionBarButton/OptionBarButton.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import SVG from '../SVG'; 3 | 4 | import styles from "./OptionBarButton.scss"; 5 | 6 | const OptionBarButton = ({ text, icon, ...props }) => { 7 | return ( 8 |
9 | { 10 | text && ( 11 | {text} 12 | ) 13 | } 14 | { 15 | icon && ( 16 | 17 | ) 18 | } 19 |
20 | ); 21 | } 22 | 23 | export default OptionBarButton; -------------------------------------------------------------------------------- /src/scripts/components/OptionBarButton/OptionBarButton.scss: -------------------------------------------------------------------------------- 1 | .optionBarButton { 2 | display: flex; 3 | align-items: center; 4 | padding: 0px 1em; 5 | font-size: 12px; 6 | font-weight: lighter; 7 | color: inherit; 8 | background-color: transparent; 9 | cursor: pointer; 10 | outline: none; 11 | } 12 | 13 | .optionBarButton.iconBtn { 14 | padding: 0px 9px; 15 | } 16 | 17 | .optionBarButton { 18 | 19 | &:hover { 20 | background-color: rgba(0,0,0,0.05); 21 | } 22 | 23 | &:active { 24 | background-color: rgba(0,0,0,0.1); 25 | } 26 | 27 | .iconBtn:hover { 28 | background-color: transparent; 29 | } 30 | 31 | .optionBarButtonIcon { 32 | fill: var(--icon); 33 | } 34 | 35 | &:hover .optionBarButtonIcon { 36 | fill: var(--iconHover); 37 | } 38 | 39 | &[data-status='active'] .optionBarButtonIcon { 40 | fill: var(--iconActive); 41 | } 42 | } -------------------------------------------------------------------------------- /src/scripts/components/OptionBarLabel/OptionBarLabel.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import SVG from '../SVG'; 3 | 4 | import styles from "./OptionBarLabel.scss"; 5 | 6 | const OptionBarLabel = ({ icon, text, ...props }) => { 7 | return ( 8 |
9 | { 10 | icon && ( 11 | 12 | ) 13 | } 14 | { 15 | text && ( 16 | {text} 17 | ) 18 | } 19 |
20 | ); 21 | } 22 | 23 | export default OptionBarLabel; -------------------------------------------------------------------------------- /src/scripts/components/OptionBarLabel/OptionBarLabel.scss: -------------------------------------------------------------------------------- 1 | .optionBarLabel { 2 | display: flex; 3 | align-items: center; 4 | padding: 0px 9px; 5 | font-size: 12px; 6 | font-weight: lighter; 7 | color: inherit; 8 | background-color: transparent; 9 | outline: none; 10 | user-select: none; 11 | cursor: default; 12 | } 13 | 14 | .labelIcon { 15 | margin-right: 6px; 16 | } -------------------------------------------------------------------------------- /src/scripts/components/OptionBarSeparator/OptionBarSeparator.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | import styles from './OptionBarSeparator.scss'; 4 | 5 | const OptionBarSeparator = () => ( 6 |
7 | ); 8 | 9 | export default OptionBarSeparator; -------------------------------------------------------------------------------- /src/scripts/components/OptionBarSeparator/OptionBarSeparator.scss: -------------------------------------------------------------------------------- 1 | .optionBarSeparator { 2 | background-color: #ccc; 3 | width: 1px; 4 | margin: 5px 4px; 5 | } -------------------------------------------------------------------------------- /src/scripts/components/OptionBarSpacer/OptionBarSpacer.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | 3 | const OptionBarSpacer = ({...props}) => ( 4 |
5 | ); 6 | 7 | export default OptionBarSpacer; -------------------------------------------------------------------------------- /src/scripts/components/OrderedContextsList/OrderedContextsList.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import Context from '../Context/Context'; 3 | import styles from "./OrderedContextsList.scss"; 4 | 5 | function paintOrderFunction(a, b) { 6 | let aIndex = 0; 7 | let bIndex = 0; 8 | 9 | if (a.element.styles.zIndex !== "auto") aIndex = parseInt(a.element.styles.zIndex); 10 | if (b.element.styles.zIndex !== "auto") bIndex = parseInt(b.element.styles.zIndex); 11 | 12 | if (aIndex == 0 && a.element.styles.position !== "static") { 13 | aIndex = 0.5; 14 | } 15 | if (bIndex == 0 && b.element.styles.position !== "static") { 16 | bIndex = 0.5; 17 | } 18 | 19 | // When they're the same, the last element in the DOM gets print on top 20 | aIndex += 0.1; 21 | 22 | return bIndex - aIndex; 23 | } 24 | 25 | const OrderedContextsList = ({contexts}) => { 26 | 27 | let ordered = [...contexts]; 28 | 29 | ordered = ordered.filter( (context) => context.type !== "container" ); 30 | 31 | ordered.sort(paintOrderFunction); 32 | 33 | return ( 34 |
35 | { 36 | ordered.map( (context) => ( 37 |
38 |
39 | { 40 | context.element.styles.zIndex !== "auto" && ( 41 | <>{context.element.styles.zIndex} 42 | ) 43 | } 44 | { 45 | context.element.styles.zIndex == "auto" && ( 46 | <>{(context.element.styles.position !== "static") ? context.element.styles.position : '-'} 47 | ) 48 | } 49 |
50 | 51 |
52 | )) 53 | } 54 |
55 | ); 56 | } 57 | 58 | export default OrderedContextsList; -------------------------------------------------------------------------------- /src/scripts/components/OrderedContextsList/OrderedContextsList.scss: -------------------------------------------------------------------------------- 1 | .contextListItem { 2 | display: flex; 3 | align-items: center; 4 | 5 | .contextPosition { 6 | padding: 2px 5px; 7 | margin: 5px; 8 | background-color: var(--tagBg); 9 | color: var(--bodyColor); 10 | border-radius: 5px; 11 | } 12 | } -------------------------------------------------------------------------------- /src/scripts/components/PanelContent/PanelContent.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useContext, useEffect, useState } from 'preact/hooks'; 3 | import { DataContext } from '../DataContext'; 4 | import { ConnectionContext } from '../ConnectionContext'; 5 | import { SettingsContext } from '../SettingsContext'; 6 | import OptionBar from '../OptionBar/OptionBar'; 7 | import OptionBarButton from '../OptionBarButton/OptionBarButton'; 8 | import OptionBarLabel from '../OptionBarLabel/OptionBarLabel'; 9 | import OptionBarSpacer from '../OptionBarSpacer/OptionBarSpacer'; 10 | import ContextsTree from '../ContextsTree/ContextsTree'; 11 | import Sidepane from '../Sidepane/Sidepane'; 12 | import PanelSettings from '../PanelSettings/PanelSettings'; 13 | 14 | import RefreshIcon from '../../../icons/refresh.svg'; 15 | import WarningIcon from '../../../icons/warning.svg'; 16 | import SettingsIcon from '../../../icons/settings.svg'; 17 | import styles from "./PanelContent.scss"; 18 | 19 | const PanelContent = () => { 20 | 21 | let { shouldUpdate } = useContext(ConnectionContext); 22 | let { settings } = useContext(SettingsContext); 23 | let {contexts, refreshContexts, cleanContexts} = useContext(DataContext); 24 | let [selectedContext, setSelectedContext] = useState(null); 25 | let [settingsOpen, setSettingsOpen] = useState(false); 26 | 27 | let analysePage = async () => { 28 | setSelectedContext(null); 29 | await refreshContexts(); 30 | } 31 | 32 | let toggleSettings = () => { 33 | setSettingsOpen(!settingsOpen); 34 | } 35 | 36 | useEffect( async () => { 37 | await analysePage(); 38 | 39 | chrome.devtools.network.onNavigated.addListener(refreshContexts); 40 | 41 | return () => { 42 | chrome.devtools.network.onNavigated.removeListener(refreshContexts); 43 | } 44 | }, []); 45 | 46 | return ( 47 |
48 | 49 | analysePage()} /> 50 | { shouldUpdate && settings["dom-changed-warning"] && () } 51 | 52 | toggleSettings()} /> 53 | 54 | 55 | setSelectedContext(context)} /> 56 | 57 |
58 | ); 59 | } 60 | 61 | export default PanelContent; -------------------------------------------------------------------------------- /src/scripts/components/PanelContent/PanelContent.scss: -------------------------------------------------------------------------------- 1 | #panelRoot { 2 | display: grid; 3 | grid-template-areas: 4 | "menu menu" 5 | "settings settings" 6 | "content sidepane"; 7 | grid-template-rows: auto auto 1fr; 8 | grid-template-columns: 1fr auto; 9 | height: 100vh; 10 | } 11 | 12 | #panelMenu { 13 | grid-area: menu; 14 | border-top: solid 0px; 15 | } 16 | 17 | #panelTree { 18 | grid-area: content; 19 | overflow: auto; 20 | } 21 | 22 | #panelSidepane { 23 | grid-area: sidepane; 24 | overflow: auto; 25 | } 26 | 27 | #panelSettings { 28 | grid-area: settings; 29 | } 30 | 31 | #panelSettings.hidden { 32 | display: none; 33 | } -------------------------------------------------------------------------------- /src/scripts/components/PanelSettings/PanelSettings.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useContext } from 'preact/hooks'; 3 | import { SettingsContext } from '../SettingsContext'; 4 | 5 | import styles from './PanelSettings.scss'; 6 | 7 | const PanelSettings = ({className="", ...props}) => { 8 | let settingsLabels = { 9 | "dom-changed-warning": "Show DOM changed warning", 10 | "contexts-click-to-expand": "Expand context on click (tree view)" 11 | } 12 | 13 | let { settings, saveSettings } = useContext(SettingsContext); 14 | 15 | let checkboxClicked = (event) => { 16 | let value = event.target.checked; 17 | let settingName = event.target.name; 18 | settings[settingName] = value; 19 | saveSettings(settings); 20 | } 21 | 22 | return ( 23 |
24 | { 25 | settings && Object.keys(settings).map((settingName) => ( 26 |
27 | 31 |
32 | )) 33 | } 34 |
35 | ); 36 | } 37 | 38 | export default PanelSettings; -------------------------------------------------------------------------------- /src/scripts/components/PanelSettings/PanelSettings.scss: -------------------------------------------------------------------------------- 1 | .settingsContainer { 2 | background-color: var(--optionBarBackground); 3 | color: var(--bodyColor); 4 | border-bottom: solid 1px var(--borderColor); 5 | display: grid; 6 | grid-template-columns: 1fr; 7 | grid-gap: 5px; 8 | padding: 5px; 9 | } 10 | 11 | .setting { 12 | label { 13 | display: flex; 14 | align-items: center; 15 | gap: 5px; 16 | color: var(--bodyPrimaryColor); 17 | } 18 | } 19 | 20 | @media screen and (min-width: 400px) { 21 | .settingsContainer { 22 | grid-template-columns: 1fr 1fr; 23 | } 24 | } 25 | 26 | @media screen and (min-width: 900px) { 27 | .settingsContainer { 28 | grid-template-columns: repeat(4, 1fr); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/scripts/components/SVG.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useEffect, useState } from 'preact/hooks'; 3 | 4 | /** Get the single svg element from a string that may contain also other elements */ 5 | function svgFromString(html) { 6 | let template = document.createElement('template'); 7 | template.innerHTML = html; 8 | return template.content.cloneNode(true).querySelector('svg'); 9 | } 10 | 11 | /** Get the svg element as a string from a file source */ 12 | async function getSVGHTML(src) { 13 | let file = await fetch(src); 14 | let content = await file.text(); 15 | let SVGElement = svgFromString(content); 16 | return SVGElement.outerHTML; 17 | } 18 | 19 | /** Creates an inline SVG from a source */ 20 | const SVG = ({src, ...props}) => { 21 | 22 | let [SVGHTML, setSVGHTML] = useState(""); 23 | 24 | useEffect( async () => { 25 | let HTML = await getSVGHTML(src); 26 | setSVGHTML(HTML); 27 | }, [src]); 28 | 29 | return ( 30 | 31 | ) 32 | } 33 | 34 | export default SVG; -------------------------------------------------------------------------------- /src/scripts/components/Section/Section.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import OptionBar from '../OptionBar/OptionBar'; 3 | 4 | import styles from './Section.scss'; 5 | 6 | const Section = ({ title, buttons, children }) => ( 7 | <> 8 | {buttons} 9 |
10 | {children} 11 |
12 | 13 | ); 14 | 15 | export default Section; -------------------------------------------------------------------------------- /src/scripts/components/Section/Section.scss: -------------------------------------------------------------------------------- 1 | .content { 2 | position: relative; 3 | padding: 1em; 4 | color: var(--bodyColor); 5 | 6 | ul { 7 | padding-inline-start: 1.5em; 8 | } 9 | } -------------------------------------------------------------------------------- /src/scripts/components/SettingsContext.js: -------------------------------------------------------------------------------- 1 | import { h, createContext } from 'preact'; 2 | import { useContext, useState, useEffect } from 'preact/hooks'; 3 | import { ConnectionContext } from './ConnectionContext'; 4 | 5 | export const SettingsContext = createContext(); 6 | 7 | const SettingsContextProvider = ({ children }) => { 8 | 9 | const { 10 | getConnection, 11 | shouldUpdateSettings, 12 | setShouldUpdateSettings 13 | } = useContext(ConnectionContext); 14 | 15 | const [settings, updateSettings] = useState(null); 16 | 17 | const loadSettings = async function () { 18 | let connection = await getConnection(); 19 | let loadedSettings = await connection.loadExtensionSettings(); 20 | updateSettings(loadedSettings); 21 | setShouldUpdateSettings(false); 22 | } 23 | 24 | const saveSettings = async function (settings) { 25 | let connection = await getConnection(); 26 | await connection.saveExtensionSettings(settings); 27 | await connection.notifySettingsChanged(chrome.devtools.inspectedWindow.tabId); 28 | } 29 | 30 | // Initialization and auto updating when needed 31 | useEffect(() => { 32 | if (shouldUpdateSettings) 33 | loadSettings(); 34 | }, [shouldUpdateSettings]); 35 | 36 | 37 | if (settings == null) { 38 | loadSettings(); 39 | } 40 | 41 | return ( 42 | 43 | {children} 44 | 45 | ) 46 | } 47 | 48 | export default SettingsContextProvider; -------------------------------------------------------------------------------- /src/scripts/components/SidebarContent/SidebarContent.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useContext, useEffect, useState } from 'preact/hooks'; 3 | import { DataContext } from '../DataContext'; 4 | import { ConnectionContext } from '../ConnectionContext'; 5 | import { SettingsContext } from '../SettingsContext'; 6 | import { getPathFromNode } from '../../utils/utils'; 7 | import Section from '../Section/Section'; 8 | import OptionBarButton from '../OptionBarButton/OptionBarButton'; 9 | import OptionBarLabel from '../OptionBarLabel/OptionBarLabel'; 10 | import Context from '../Context/Context'; 11 | import OrderedContextsList from '../OrderedContextsList/OrderedContextsList'; 12 | import NodeDetails from '../NodeDetails/NodeDetails'; 13 | import ContextDetails from '../ContextDetails/ContextDetails'; 14 | import Spinner from '../Spinner/Spinner'; 15 | import ContextsContainer from '../ContextsContainer/ContextsContainer'; 16 | 17 | import RefreshIcon from '../../../icons/refresh.svg'; 18 | import WarningIcon from '../../../icons/warning.svg'; 19 | import SVG from '../SVG'; 20 | 21 | import styles from './SidebarContent.scss'; 22 | 23 | const SmallSpinner = () => ( 24 | 25 | ) 26 | 27 | const SidebarContent = () => { 28 | let [curNode, setCurNode] = useState(null); 29 | let [contextsLoaded, setContextsLoaded] = useState(false); 30 | 31 | let { getConnection, shouldUpdate } = useContext(ConnectionContext); 32 | let { settings } = useContext(SettingsContext); 33 | let {contexts, refreshContexts} = useContext(DataContext); 34 | 35 | let context = contexts[curNode?.contextId] ?? null; 36 | let parentContext = curNode?.createsContext ? context?.parent : context; 37 | 38 | const handleInspectedElement = () => { 39 | 40 | chrome.devtools.inspectedWindow.eval( 41 | `(${getPathFromNode.toString()})($0)`, 42 | async ( elementPath , isError) => { 43 | // If there is an error or the element is unreachable, set cur node as null 44 | if (isError || elementPath === null) { 45 | setCurNode(null); 46 | return; 47 | } 48 | 49 | let tabId = chrome.devtools.inspectedWindow.tabId; 50 | 51 | let connection = await getConnection(); 52 | await connection.detectLastInspectedElement(tabId, elementPath); 53 | 54 | elementDetails = await connection.getInspectedElementDetails(tabId); 55 | setCurNode(elementDetails); 56 | } 57 | ); 58 | } 59 | 60 | const refreshContextsCache = async () => { 61 | setContextsLoaded(false); 62 | await refreshContexts(); 63 | setContextsLoaded(true); 64 | } 65 | 66 | useEffect(async () => { 67 | 68 | await refreshContextsCache(); 69 | chrome.devtools.panels.elements.onSelectionChanged.addListener( handleInspectedElement ); 70 | handleInspectedElement(); 71 | 72 | let cleanup = () => { 73 | chrome.devtools.panels.elements.onSelectionChanged.removeListener( handleInspectedElement ); 74 | } 75 | 76 | return cleanup; 77 | }, []); 78 | 79 | let NodeDetailsMenu = ( 80 | <> 81 | refreshContextsCache()} /> 82 | { shouldUpdate && settings["dom-changed-warning"] && } 83 | 84 | ) 85 | 86 | return ( 87 | <> 88 |
89 | { 90 | !curNode && ( 91 |

There is no element currently selected, or the element is not reachable.

92 | ) 93 | } 94 | { 95 | curNode && context && ( 96 | 97 | ) 98 | } 99 |
100 |
101 | { 102 | shouldUpdate && settings["dom-changed-warning"] && ( 103 |
104 |
105 | 106 | There were some changes in the page. 107 |
108 |
109 | 110 |
111 |
112 | ) 113 | } 114 | { 115 | contextsLoaded && curNode?.createsContext && context && ( 116 | 117 | ) 118 | } 119 | { 120 | !contextsLoaded && ( 121 | 122 | ) 123 | } 124 |
125 |
126 | { 127 | contextsLoaded && parentContext && ( 128 | <> 129 | { 130 | parentContext.type == "container" ? ( 131 | 132 | ) : ( 133 | 134 | ) 135 | } 136 | 137 | ) 138 | } 139 | { 140 | !contextsLoaded && ( 141 | 142 | ) 143 | } 144 |
145 |
146 | { 147 | contextsLoaded && parentContext?.children?.length > 0 && ( 148 | 149 | ) 150 | } 151 | { 152 | !contextsLoaded && ( 153 | 154 | ) 155 | } 156 |
157 | 158 | ); 159 | } 160 | 161 | export default SidebarContent; 162 | -------------------------------------------------------------------------------- /src/scripts/components/SidebarContent/SidebarContent.scss: -------------------------------------------------------------------------------- 1 | .domChanged { 2 | margin-bottom: 16px; 3 | } 4 | -------------------------------------------------------------------------------- /src/scripts/components/Sidepane/Sidepane.js: -------------------------------------------------------------------------------- 1 | import { h, Fragment } from 'preact'; 2 | import { useContext } from 'preact/hooks'; 3 | import { ConnectionContext } from '../ConnectionContext'; 4 | import { SettingsContext } from '../SettingsContext'; 5 | import Section from '../Section/Section'; 6 | import OrderedContextsList from '../OrderedContextsList/OrderedContextsList'; 7 | import SVG from '../SVG'; 8 | 9 | import styles from "./Sidepane.scss"; 10 | import WarningIcon from '../../../icons/warning.svg'; 11 | 12 | const Sidepane = ({context, ...props}) => { 13 | 14 | let { shouldUpdate } = useContext(ConnectionContext); 15 | let { settings } = useContext(SettingsContext); 16 | 17 | return ( 18 |
19 |
20 | { 21 | shouldUpdate && settings["dom-changed-warning"] && ( 22 | <> 23 |
24 | 25 | There were some changes in the page. The stacking context informations might be out of date. 26 |
27 | 28 | ) 29 | } 30 | { 31 | context && (context.passedChecks?.length > 0) && ( 32 |
    33 | { 34 | context.passedChecks.map((check) => ( 35 |
  • {check}
  • 36 | )) 37 | } 38 |
39 | ) 40 | } 41 | { 42 | context && (context.passedChecks?.length == 0) && "This is just a container." 43 | } 44 |
45 |
46 | { 47 | context?.children?.length > 0 && ( 48 | 49 | ) 50 | } 51 |
52 |
53 | ) 54 | } 55 | 56 | export default Sidepane; -------------------------------------------------------------------------------- /src/scripts/components/Sidepane/Sidepane.scss: -------------------------------------------------------------------------------- 1 | .sidepane { 2 | width: 300px; 3 | border-left: solid 1px var(--borderColor); 4 | overflow: auto; 5 | } -------------------------------------------------------------------------------- /src/scripts/components/Spinner/Spinner.js: -------------------------------------------------------------------------------- 1 | import {h} from 'preact'; 2 | import SVG from '../SVG'; 3 | 4 | import LoaderIcon from '../../../icons/loader-simple.svg'; 5 | 6 | import styles from './Spinner.scss'; 7 | 8 | export const Spinner = ({ position='absolute', width=100, height=100 }) => ( 9 |
10 | 11 |
12 | ); 13 | 14 | export default Spinner; -------------------------------------------------------------------------------- /src/scripts/components/Spinner/Spinner.scss: -------------------------------------------------------------------------------- 1 | .spinner { 2 | position: absolute; 3 | top: 0; 4 | left: 0; 5 | width: 100%; 6 | height: 100%; 7 | display: flex; 8 | align-items: center; 9 | justify-content: center; 10 | 11 | i { 12 | width: 100px; 13 | height: 100px; 14 | display: block; 15 | fill: var(--icon); 16 | animation: spin .5s linear infinite; 17 | } 18 | } 19 | 20 | @keyframes spin { 21 | 100% { 22 | transform: rotate(360deg); 23 | } 24 | } -------------------------------------------------------------------------------- /src/scripts/components/TreeItem/TreeItem.js: -------------------------------------------------------------------------------- 1 | import { h } from 'preact'; 2 | import { useContext } from 'preact/hooks'; 3 | import { ConnectionContext } from '../ConnectionContext'; 4 | import Context from '../Context/Context'; 5 | import ContextsContainer from '../ContextsContainer/ContextsContainer'; 6 | import { isValuableContainer } from '../../utils/utils'; 7 | 8 | import styles from "./TreeItem.scss"; 9 | 10 | const TreeItem = ({context, depth, isVisible, onToggle, ...props}) => { 11 | 12 | let {getConnection} = useContext(ConnectionContext); 13 | let tabId = chrome.devtools.inspectedWindow.tabId; 14 | let isContainer = context.type == "container"; 15 | 16 | let highlightContext = async () => { 17 | let conn = await getConnection(); 18 | await conn.highlightContext(tabId, context.id); 19 | } 20 | 21 | let undoHighlightContext = async () => { 22 | let conn = await getConnection(); 23 | await conn.undoHighlightContext(tabId); 24 | } 25 | 26 | let shouldShowToggle = () => { 27 | return isValuableContainer(context); 28 | } 29 | 30 | let handleToggle = (event) => { 31 | event.stopPropagation(); 32 | onToggle(context) 33 | } 34 | 35 | return ( 36 |
highlightContext()} 43 | onMouseLeave={() => undoHighlightContext()} 44 | {...props} 45 | > 46 | { 47 | shouldShowToggle() && ( 48 |
49 | 50 |
51 | ) 52 | } 53 | { 54 | isContainer && ( 55 | 56 | ) 57 | } 58 | { 59 | !isContainer && ( 60 | 61 | ) 62 | } 63 |
64 | ); 65 | } 66 | 67 | export default TreeItem; -------------------------------------------------------------------------------- /src/scripts/components/TreeItem/TreeItem.scss: -------------------------------------------------------------------------------- 1 | 2 | 3 | .treeItem { 4 | position: relative; 5 | cursor: pointer; 6 | 7 | &:hover { 8 | background-color: var(--contextHoverBg); 9 | } 10 | 11 | &[data-selected] { 12 | background-color: var(--contextSelectedBg); 13 | 14 | .treeItemContext { 15 | background-color: var(--contextSelectedBg) !important; 16 | } 17 | } 18 | 19 | .treeItemContext { 20 | cursor: pointer; 21 | } 22 | 23 | .contextToggle { 24 | position: absolute; 25 | top: 50%; 26 | transform: translateY(-50%) translateX(-18px); 27 | padding: 5px; 28 | } 29 | 30 | .contextToggleIcon { 31 | display: inline-block; 32 | position: relative; 33 | } 34 | 35 | /* Here I'm using the icon color, because these styles are related to the little triangle on the side of a context in the tree */ 36 | &[data-status=closed] .contextToggle .contextToggleIcon { 37 | border-left: solid 4px var(--icon); 38 | border-top: solid 4px transparent; 39 | border-bottom: solid 4px transparent; 40 | } 41 | 42 | &[data-status=open] .contextToggle .contextToggleIcon { 43 | border-top: solid 4px var(--icon); 44 | border-left: solid 4px transparent; 45 | border-right: solid 4px transparent; 46 | top: -2px; 47 | } 48 | 49 | /* 50 | Horrible, but I couldn't find any other way to do it. 51 | It will show the context actions (the little icons on the right) when the tree item is hovered 52 | */ 53 | &:hover .treeItemContext > div:nth-child(2) { 54 | background-color: var(--contextHoverBg); 55 | display: flex; 56 | } 57 | 58 | &[data-selected] .treeItemContext > div:nth-child(2) { 59 | background-color: var(--contextSelectedBg); 60 | display: flex; 61 | } 62 | } -------------------------------------------------------------------------------- /src/scripts/content.js: -------------------------------------------------------------------------------- 1 | import DOMTraversal from "./utils/DOMTraversal"; 2 | import StackingContext from './classes/StackingContext'; 3 | import { BackgroundScript } from '@andreadev/bg-script'; 4 | import { getNodeFromPath, getPathFromNode } from "./utils/utils"; 5 | 6 | var allContexts = null; 7 | var rootContext = null; 8 | var highlightDOM = initHighlightDOM(); 9 | var lastInspectedElement = null; 10 | var observer = null; 11 | var isObserverActive = false; 12 | 13 | 14 | /** 15 | * Analyse the page and initialize the 'allContexts' and 'rootContext' variables. 16 | */ 17 | async function analysePage() { 18 | [rootContext, allContexts] = DOMTraversal.getContextsFromPage(); 19 | } 20 | 21 | /** 22 | * Returns a copy of all the contexts converted to JSON. 23 | * 24 | * @return {Array} list of all contexts converted to JSON. 25 | */ 26 | function getAllContextsJSON() { 27 | let allContextsJSON = allContexts.map( context => context.toJSON() ); 28 | return allContextsJSON; 29 | } 30 | 31 | /** 32 | * Creates the element that will be used to highlight the contexts in the page. 33 | * 34 | * @return {Node} The actual DOM element with coherent styles applied. 35 | */ 36 | function initHighlightDOM() { 37 | let element = document.createElement("div"); 38 | element.id = "devtools-stacking-context-highlight"; 39 | element.style.backgroundColor = "rgba(0,200,255, 0.7)"; 40 | element.style.position = "fixed"; 41 | element.style.zIndex = "2147483647"; 42 | element.style.display = "none"; 43 | return element; 44 | } 45 | 46 | /** 47 | * Highlight the DOM Element related to a context. 48 | * 49 | * @param {number} id The id of the context to be highlighted. 50 | */ 51 | function highlightContext(id) { 52 | let elementBCR = allContexts[id].element.getBoundingClientRect(); 53 | let relPos = getRelativePosition(allContexts[id]); 54 | highlightDOM.style.top = (elementBCR.top + relPos.top) + "px"; 55 | highlightDOM.style.left = (elementBCR.left + relPos.left) + "px"; 56 | highlightDOM.style.width = elementBCR.width + "px"; 57 | highlightDOM.style.height = elementBCR.height + "px"; 58 | highlightDOM.style.display = "block"; 59 | document.body.appendChild(highlightDOM); 60 | } 61 | 62 | /** 63 | * Get the relative position of a specific element in the page (iframes included) 64 | * 65 | * @param {StackingContext} context 66 | * @return {Object} The relative position of the context frame 67 | */ 68 | function getRelativePosition(context) { 69 | let relativePosition = { top: 0, left: 0 }; 70 | let current = context; 71 | 72 | while (current) { 73 | if (current.frame == null) break; 74 | 75 | if (current.type === "container") { 76 | let frameBCR = current.frame.getBoundingClientRect(); 77 | relativePosition.top += frameBCR.top; 78 | relativePosition.left += frameBCR.left; 79 | } 80 | 81 | current = current.parent; 82 | } 83 | 84 | return relativePosition; 85 | } 86 | 87 | /** 88 | * Remove the highlight element from the page. 89 | */ 90 | function undoHighlightContext() { 91 | highlightDOM.style.display = "none"; 92 | document.body.removeChild(highlightDOM); 93 | } 94 | 95 | /** 96 | * Scroll the page to show the element related to a specific context. 97 | * 98 | * @param {number} id The context id. 99 | */ 100 | function scrollToContext(id) { 101 | allContexts[id].element.scrollIntoView(); 102 | } 103 | 104 | /** 105 | * Find the stacking context that contains a specific node as a child of its element. If the node creates a stacking context, return it. 106 | * 107 | * @param {Node} node The node of which we want to find the context. 108 | * @return {StackingContext} The context associated with the node or one of its parents. 109 | */ 110 | function getContextIdFromNode(node) { 111 | 112 | let lastParent = node; 113 | while (lastParent !== null) { 114 | 115 | foundContext = allContexts.find( (context) => context.element === lastParent ); 116 | 117 | if (foundContext) { 118 | return foundContext; 119 | } 120 | 121 | // Check if I've reached a shadowroot container and work my way around it 122 | if (lastParent.parentElement === null) { 123 | 124 | let root = lastParent.getRootNode(); 125 | 126 | if (root instanceof ShadowRoot) { 127 | lastParent = root.host; 128 | continue; 129 | } 130 | } 131 | 132 | lastParent = lastParent.parentElement; 133 | } 134 | 135 | return null; 136 | } 137 | 138 | /** 139 | * Returns a path to get the element of a specific context even if it is inside an iframe or shadow DOM 140 | * 141 | * @param {Integer} contextId 142 | * @returns {Object[]} 143 | */ 144 | function getPathFromContext(contextId) { 145 | let context = allContexts.find( (context) => context.id == contextId ); 146 | return getPathFromNode( context.element ); 147 | } 148 | 149 | /** 150 | * Detect which node was tagged as "last inspected" from the devtools, and initialize the related variable. 151 | */ 152 | function detectLastInspectedElement(elementPath) { 153 | 154 | let element = null; 155 | 156 | try { 157 | element = getNodeFromPath(elementPath); 158 | } 159 | catch (e) { 160 | element = null; 161 | } 162 | 163 | if (!element) throw "Cannot find element"; 164 | 165 | lastInspectedElement = element; 166 | } 167 | 168 | /** 169 | * Get some details about the last inspected element. 170 | * 171 | * @returns {Object} An object containing the id of the context associated to the element and whether or not the element creates a new stacking context. 172 | */ 173 | function getInspectedElementDetails() { 174 | 175 | let details = { 176 | createsContext: false, 177 | contextId: 0, 178 | zIndex: "auto" 179 | }; 180 | 181 | let passedChecks = DOMTraversal.getPassedChecks(lastInspectedElement); 182 | 183 | if (passedChecks.length > 0) details.createsContext = true; 184 | 185 | let context = getContextIdFromNode(lastInspectedElement); 186 | 187 | details.contextId = context.id; 188 | 189 | let styles = window.getComputedStyle(lastInspectedElement); 190 | 191 | details.zIndex = styles.zIndex; 192 | 193 | return details; 194 | } 195 | 196 | /** 197 | * Get all the iframes sources, to implement inspection on iframes 198 | * 199 | * @return {Array} All the iframes src attributes 200 | */ 201 | function getPageFramesSources() { 202 | let iframes = Array.from( document.getElementsByTagName('iframe') ); 203 | let sources = iframes.map( (iframe) => iframe.src ); 204 | sources = sources.filter( (src) => src !== "" ); 205 | sources = [...new Set(sources)]; 206 | return sources; 207 | } 208 | 209 | /** 210 | * Attach a mutation observer to the DOM and notify the extension that the contexts should be refreshed 211 | */ 212 | function setupDOMObserver() { 213 | // This is used to debounce the "DOM Changed" notification 214 | let lastCalled = new Date().getTime(); 215 | 216 | // Callback for the observer 217 | const callback = (mutationsList, observer) => { 218 | // Check if it has passed enough time from the last call 219 | let now = new Date().getTime(); 220 | let debounceTime = 1000; // one second 221 | 222 | if (now - lastCalled < debounceTime) return; 223 | 224 | // If we're not interested in these mutation, stop the function 225 | if (!mutationsList.some(isImportantMutation)) return; 226 | 227 | // If all the mutations are internal, stop the function 228 | if (mutationsList.every(isInternalMutation)) return; 229 | 230 | lastCalled = now; 231 | sendDOMChangedWarning(); 232 | } 233 | 234 | observer = new MutationObserver(callback); 235 | 236 | } 237 | 238 | /** 239 | * Just a utility function to allow adding and removing it as an event listener 240 | */ 241 | function disconnectObserver() { 242 | if (!isObserverActive) { 243 | console.warn("Tried stopping the observer, but it was already stopped."); 244 | return; 245 | } 246 | observer.disconnect(); 247 | isObserverActive = false; 248 | } 249 | 250 | /** 251 | * Start observing the changes in the DOM 252 | */ 253 | function startDOMObserver() { 254 | 255 | if (isObserverActive) { 256 | console.warn("Tried starting the observer, but it was already running."); 257 | return; 258 | } 259 | 260 | // Observer all the changes of the DOM elements position and attribute for the body tag and its children 261 | const targetNode = document.body; 262 | const config = { attributes: true, childList: true, subtree: true }; 263 | 264 | observer.observe(targetNode, config); 265 | isObserverActive = true; 266 | 267 | // Disconnect the observer when the user is leaving the page 268 | window.addEventListener("beforeunload", disconnectObserver); 269 | } 270 | 271 | 272 | /** 273 | * Stop observing the changes in the DOM 274 | */ 275 | function stopDOMObserver() { 276 | disconnectObserver(); 277 | window.removeEventListener("beforeunload", disconnectObserver); 278 | } 279 | 280 | 281 | /** 282 | * Check whether or not we're interested in this mutation and want to send a warning in the extension panel and sidebar 283 | * 284 | * @param {MutationRecord} mutation 285 | * @returns {boolean} 286 | */ 287 | function isImportantMutation(mutation) { 288 | // If a DOM element has changed position or has been added / removed, it should notify 289 | if (mutation.type === "childList") return true; 290 | 291 | // If the "style", "class", "id" or "data-xxx" attributes change, it should notify (although any attribute might change the stacking contexts, this is just to make it lighter) 292 | if (mutation.type === "attributes") { 293 | 294 | let checkAttrs = ["style", "class", "id"]; 295 | if (checkAttrs.includes(mutation.attributeName)) return true; 296 | 297 | if (mutation.attributeName.startsWith("data-")) return true; 298 | } 299 | 300 | return false; 301 | } 302 | 303 | /** 304 | * Check whether a specific mutation found by the observer was caused by our own extension or was generated by the page 305 | * 306 | * @param {MutationRecord} mutation 307 | * @returns {boolean} 308 | */ 309 | function isInternalMutation(mutation) { 310 | // Check if the target is our own overlay 311 | if (mutation.target.id == "devtools-stacking-context-highlight") { 312 | return true; 313 | } 314 | 315 | // Check if the mutation was caused by our overlay 316 | if (mutation.type == "childList") { 317 | let element = null; 318 | 319 | if (mutation.addedNodes.length == 1) element = mutation.addedNodes[0]; 320 | else if (mutation.removedNodes.length == 1) element = mutation.removedNodes[0]; 321 | 322 | if (element?.id == "devtools-stacking-context-highlight") { 323 | return true; 324 | } 325 | } 326 | 327 | return false; 328 | } 329 | 330 | /** 331 | * Send a DOM Changed warning to the extension panel and sidebar 332 | */ 333 | async function sendDOMChangedWarning() { 334 | let connection = await bgScript.getConnection(); 335 | 336 | // Stop if a connection hasn't been found 337 | if (connection == null) return; 338 | 339 | let tabId = await connection.$getMyTabId(); 340 | 341 | await connection.sendDOMChangedWarning(tabId); 342 | } 343 | 344 | 345 | // Setup the DOM Observer 346 | setupDOMObserver(); 347 | 348 | // Create the connection to the background script exposing the methods. 349 | let scriptId = "content"; 350 | if (window.top !== window.self) { 351 | scriptId += "." + window.location.href; 352 | } 353 | 354 | var bgScript = new BackgroundScript(scriptId, { 355 | analysePage, 356 | getAllContextsJSON, 357 | highlightContext, 358 | undoHighlightContext, 359 | scrollToContext, 360 | getPathFromContext, 361 | detectLastInspectedElement, 362 | getInspectedElementDetails, 363 | getPageFramesSources, 364 | startDOMObserver, 365 | stopDOMObserver 366 | }); -------------------------------------------------------------------------------- /src/scripts/utils/DOMTraversal.js: -------------------------------------------------------------------------------- 1 | import { activeChecks } from '../classes/ContextChecks.js'; 2 | import StackingContext from '../classes/StackingContext.js'; 3 | import ContextsContainer from '../classes/ContextsContainer'; 4 | 5 | let allContexts = null; 6 | 7 | /** 8 | * Utility method to traverse a DOM element and check if it's a stacking context. It's recursive and 9 | * will traverse all the children down the tree. 10 | * 11 | * @param {Node} element The element to traverse 12 | * @param {StackingContext} parentContext The parent context 13 | * @param {boolean} isInIframe True if we are traversing an iframe 14 | * @param {Object} frame The current frame (may be undefined or an iframe element). 15 | */ 16 | function traverse(element, parentContext, isInIframe=false, frame) { 17 | let context = null; 18 | 19 | let passedChecks = getPassedChecks(element); 20 | if (passedChecks.length > 0) { 21 | context = new StackingContext(element, isInIframe, frame, parentContext, passedChecks); 22 | parentContext.addChild(context); 23 | let id = allContexts.push(context); 24 | context.id = id - 1; // This will help us referring to this context from other scripts 25 | 26 | // set the new context as the parent for this element children 27 | parentContext = context; 28 | } 29 | 30 | // Check for pseudoelements (after / before) 31 | let beforePassedChecks = getPassedChecks(element, ':before'); 32 | let afterPassedChecks = getPassedChecks(element, ':after'); 33 | 34 | if (beforePassedChecks.length > 0) { 35 | context = new StackingContext(element, isInIframe, frame, parentContext, beforePassedChecks, 'before'); 36 | parentContext.addChild(context); 37 | let id = allContexts.push(context); 38 | context.id = id - 1; 39 | } 40 | 41 | if (afterPassedChecks.length > 0) { 42 | context = new StackingContext(element, isInIframe, frame, parentContext, afterPassedChecks, 'after'); 43 | parentContext.addChild(context); 44 | let id = allContexts.push(context); 45 | context.id = id - 1; 46 | } 47 | 48 | for (let child of element.children) { 49 | if (element.nodeType == Node.ELEMENT_NODE) { 50 | // We need to pass down the "IsInIframe" and "frame" 51 | traverse(child, parentContext, isInIframe, frame); 52 | } 53 | } 54 | 55 | // If element is a traversable iframe... 56 | if ( isTraversableIframe(element) ) { 57 | // Get the client rect to pass the relative position of the iframe in respect to the top window 58 | let iframeDoc = element.contentDocument; 59 | 60 | // Start traversing its documentElement 61 | let container = new ContextsContainer("#document (iframe)", iframeDoc.documentElement, isInIframe, element, parentContext); 62 | parentContext.addChild(container); 63 | let id = allContexts.push(container); 64 | container.id = id - 1; 65 | 66 | traverse( 67 | iframeDoc.body, 68 | container, 69 | true, 70 | element 71 | ); 72 | } 73 | // Check if the element contains an open shadow DOM 74 | else if ( hasTraversableShadowDOM(element) ) { 75 | let shadowRoot = element.shadowRoot; 76 | 77 | // Shadow roots can't be a container because they are not taken into consideration for stacking contexts 78 | 79 | for (let child of shadowRoot.children) { 80 | if (element.nodeType == Node.ELEMENT_NODE) { 81 | traverse(child, parentContext, true, frame); 82 | } 83 | } 84 | } 85 | } 86 | 87 | /** 88 | * Check if an element is an iframe that exposes its content to javascript 89 | * 90 | * @param {Node} element The element to be checked 91 | * @returns {boolean} 92 | */ 93 | function isTraversableIframe(element) { 94 | return element.children.length == 0 && 95 | element.tagName === "IFRAME" && 96 | element.contentDocument?.documentElement; 97 | } 98 | 99 | /** 100 | * Check whether or not this element has an open shadow DOM 101 | * 102 | * @param {Node} element The element to be checked 103 | * @returns {boolean} 104 | */ 105 | function hasTraversableShadowDOM(element) { 106 | return element.shadowRoot?.mode == "open"; 107 | } 108 | 109 | /** 110 | * Check if an element is a stacking context and returns a list of passed checks. 111 | * 112 | * @param {Node} element The DOM element to check for a stacking context 113 | * @returns 114 | */ 115 | export function getPassedChecks(element, pseudo) { 116 | let styles = window.getComputedStyle(element, pseudo); 117 | let passed = []; 118 | 119 | if (pseudo) { 120 | if (!styles.content || styles.content == 'none') { 121 | return []; 122 | } 123 | } 124 | 125 | for (let check of activeChecks) { 126 | if (check.exec(element, styles)) { 127 | passed.push(check.description); 128 | } 129 | } 130 | 131 | return passed; 132 | } 133 | 134 | /** 135 | * Start traversing the DOM from the documentElement and extracts all the stacking contexts in the page. 136 | * 137 | * @returns {Array} Returns an array with two elements: the root context and a list of all contexts. 138 | */ 139 | export function getContextsFromPage() { 140 | let rootContainer = new ContextsContainer("#document", document.documentElement, false); 141 | rootContainer.id = 0; 142 | allContexts = [rootContainer]; 143 | 144 | traverse(document.body, rootContainer); 145 | 146 | return [rootContainer, allContexts]; 147 | } 148 | 149 | export default { 150 | getPassedChecks, 151 | getContextsFromPage 152 | }; 153 | -------------------------------------------------------------------------------- /src/scripts/utils/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Get a list of JSON-friendly stacking contexts that have only the context id set as the parent/children, and links them. 3 | * 4 | * @param {Array} contextsList A list of contexts with their 5 | * @returns {Object} A representation of a stacking context adapted for the devtools scripts. 6 | */ 7 | export function generateContextTree(contextsList) { 8 | let root = contextsList[0]; 9 | 10 | function linkChildren(context) { 11 | // Get a context from its id (since the ids start from 0, the id is also its index inside the array) 12 | context.children = context.children.map( (child) => contextsList[child] ); 13 | for (let child of context.children) { 14 | child.parent = context; 15 | linkChildren(child); 16 | } 17 | } 18 | 19 | linkChildren(root); 20 | 21 | return root; 22 | } 23 | 24 | export function isValuableContainer(container) { 25 | // Check if has children 26 | if (container.children.length == 0) return false; 27 | 28 | // Check if some children is a stacking context 29 | if (container.children.some((child) => child.type !== "container")) return true; 30 | 31 | // Check the container children to see if they're valuable to show 32 | if (container.children.some((child) => isValuableContainer(child))) return true; 33 | 34 | return false; 35 | } 36 | 37 | /** 38 | * This function returns an array of objects that represent the path that it takes to get from the main document to an element 39 | * regardless if it is inside an iframe, a shadow DOM or multiples of them. It walks up the documents until it reaches the main one. 40 | * If the element is inside a closed shadow root it will be unreachable, and "null" will be returned. 41 | * 42 | * !!! This function will be executed from its string representation, do not use other custom functions inside of it! 43 | * 44 | * @param {Node} node 45 | * @returns {Object[] | null} 46 | */ 47 | export function getPathFromNode(node) { 48 | 49 | let curElement = node; 50 | let path = []; 51 | let curRoot = null; 52 | 53 | do { 54 | // get root node of current element. Might be a shadow-root or an iframe document 55 | curRoot = curElement.getRootNode(); 56 | 57 | 58 | let pathFragment = { 59 | index: Array.from(curRoot.querySelectorAll('*')).findIndex((el) => el === curElement), 60 | type: "document" 61 | } 62 | 63 | if (curRoot instanceof ShadowRoot) { 64 | // element is inside a shadowRoot 65 | pathFragment.type = "shadow"; 66 | 67 | // if it's inside a closed shadow root, just return null, since it will be unreachable from the outside 68 | if (curRoot.mode === "closed") { 69 | return null; 70 | } 71 | 72 | curElement = curRoot.host; 73 | } 74 | else if (curRoot !== document) { 75 | // is inside an iframe 76 | pathFragment.type = "iframe"; 77 | 78 | curElement = curRoot.defaultView.frameElement; 79 | } 80 | 81 | path.push(pathFragment); 82 | 83 | } while (curRoot !== document); 84 | 85 | return path.reverse(); 86 | } 87 | 88 | /** 89 | * Get a Node from the path generated by the getPathFromNode function 90 | * 91 | * @param {Object} path 92 | * @returns {Node} 93 | */ 94 | export function getNodeFromPath(path) { 95 | 96 | let curElement = null; 97 | 98 | const elementFromIndex = (container, index) => container.querySelectorAll('*')[index]; 99 | 100 | for (let pathFragment of path) { 101 | switch (pathFragment.type) { 102 | case "document": 103 | curElement = elementFromIndex(document, pathFragment.index); 104 | break; 105 | case "shadow": 106 | curElement = elementFromIndex(curElement.shadowRoot, pathFragment.index); 107 | break; 108 | case "iframe": 109 | curElement = elementFromIndex(curElement.contentDocument, pathFragment.index); 110 | break; 111 | } 112 | } 113 | 114 | return curElement; 115 | } -------------------------------------------------------------------------------- /src/views/devtools/devtools.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/views/devtools/devtools.js: -------------------------------------------------------------------------------- 1 | chrome.devtools.panels.create("Stacking Contexts", "", "/views/panel/panel.html"); 2 | 3 | chrome.devtools.panels.elements.createSidebarPane("Stacking Contexts", (sidebar) => { 4 | sidebar.setPage("/views/elements-sidebar/sidebar-preact.html"); 5 | }); -------------------------------------------------------------------------------- /src/views/elements-sidebar/sidebar-preact.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /src/views/elements-sidebar/sidebar.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import DataContextProvider from '../../scripts/components/DataContext'; 3 | import ConnectionContextProvider from '../../scripts/components/ConnectionContext'; 4 | import SettingsContextProvider from '../../scripts/components/SettingsContext'; 5 | import SidebarContent from '../../scripts/components/SidebarContent/SidebarContent'; 6 | 7 | import '../../global.scss'; 8 | 9 | const App = () => { 10 | 11 | document.body.addEventListener("contextmenu", (e) => e.preventDefault() ); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | let root = document.getElementById('root'); 25 | root.classList.add("theme-" + chrome.devtools.panels.themeName); 26 | 27 | render(, root); -------------------------------------------------------------------------------- /src/views/panel/panel.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | -------------------------------------------------------------------------------- /src/views/panel/panel.js: -------------------------------------------------------------------------------- 1 | import { h, render } from 'preact'; 2 | import DataContextProvider from '../../scripts/components/DataContext'; 3 | import ConnectionContextProvider from '../../scripts/components/ConnectionContext'; 4 | import PanelContent from '../../scripts/components/PanelContent/PanelContent'; 5 | 6 | import '../../global.scss'; 7 | import SettingsContextProvider from '../../scripts/components/SettingsContext'; 8 | 9 | const App = () => { 10 | 11 | document.body.addEventListener("contextmenu", (e) => e.preventDefault() ); 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | ) 22 | } 23 | 24 | let root = document.getElementById('root'); 25 | root.classList.add("theme-" + chrome.devtools.panels.themeName); 26 | 27 | render(, root); -------------------------------------------------------------------------------- /static/assets/icon128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/static/assets/icon128.png -------------------------------------------------------------------------------- /static/assets/icon16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/static/assets/icon16.png -------------------------------------------------------------------------------- /static/assets/icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/static/assets/icon32.png -------------------------------------------------------------------------------- /static/assets/icon48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andreadev-it/stacking-contexts-inspector/1cfabf26aa8233fbe6d14aa16a97df99bf72518f/static/assets/icon48.png -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "CSS Stacking Context inspector", 3 | "version": "1.1.15", 4 | "description": "Helps inspecting the css stacking contexts and solving the z-index war.", 5 | "manifest_version": 3, 6 | "background": { 7 | "service_worker": "scripts/background.js", 8 | "type": "module" 9 | }, 10 | "devtools_page": "./views/devtools/devtools.html", 11 | "content_scripts": [ 12 | { 13 | "matches": [""], 14 | "js": ["/scripts/content.js"] 15 | } 16 | ], 17 | "permissions": ["storage", "scripting"], 18 | "host_permissions": ["http://*/*", "https://*/*"], 19 | "icons": { 20 | "16": "/assets/icon16.png", 21 | "32": "/assets/icon32.png", 22 | "48": "/assets/icon48.png", 23 | "128": "/assets/icon128.png" 24 | } 25 | } 26 | --------------------------------------------------------------------------------