├── .DS_Store ├── .gitignore ├── .npmignore ├── LICENSE ├── NOTES.md ├── README.md ├── assets └── cssexy_logo_npm_2024-05-09@10x.png ├── client ├── .DS_Store ├── App.jsx ├── cdp │ ├── cdp0process.js │ ├── cdp1enable.js │ └── cdp2rules.js ├── components │ ├── RulesAllComp.jsx │ ├── RulesUserAgentComp.jsx │ ├── SidebarComp.jsx │ ├── SidebarStyling.jsx │ └── iFrameComp.jsx ├── patchFile.js ├── puppeteer │ ├── pup.js │ ├── pupProcess.js │ └── pupRules.js ├── slices │ └── rulesSlice.js ├── store.js └── stylesheets │ └── styles.css ├── data └── .DS_Store ├── index.html ├── index.jsx ├── nodemon.json ├── package-lock.json ├── package.json ├── scripts ├── getTargetPort.js ├── postInstall.js ├── startRemoteChrome.js └── startRemoteChrome.sh ├── server └── server.js └── vite.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/cssexy/cf8adcd362c7ea9495c7d4d7dcc1713949ce43d0/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist/ 3 | data/*/* 4 | .env 5 | .vscode 6 | client/data 7 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | vite.config.js 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 OSLabs Beta 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /NOTES.md: -------------------------------------------------------------------------------- 1 | # COMMIT, PR, ETC. NOTES 2 | 3 | Commit notes: 4 | 5 | keith_2024-04-04: 6 | target port (e.g. 8000 for Backtrack) can be obtained programatically 7 | after linking cssxe in a target repo and adding the 'sexy' script described below to the package.json of the target repo. 8 | and running cssxe by running npm run sexy from the target repo (after running the target repo in its own node process as usual.) 9 | keith_2024-03-28_npmLink: 10 | npm link working. 11 | cssxe can now be run from inside of another repo. 12 | To link, in the cssxe directory first run npm init, then npm link. 13 | Then, in the target repo directory, run npm link cssxe. 14 | Then add the following script to the target repo package.json: 15 | "sexy": "TARGET_DIR=$(pwd) npm run cssxe:dev --prefix node_modules/cssxe". 16 | the cssxe package doesn’t programatically obtain the port (at the moment) due to being run with npm link. so for now its set to 8000, the .env file. But if cssxe was installed as an npm package the logic for getting the port programatically would work now. 17 | 18 | 19 | keith_puppeteer_2024-03-25: 20 | To run CSSxe in puppeteer mode: 21 | - in .env, set VITE_PUPPETEER_MODE to true. 22 | - run dev-pup or prod-pup, respectively (rather than dev or prod). 23 | 24 | To change the target port: 25 | - in .env, change VITE_PROXY to the desired port. 26 | 27 | To change the target directory path (i.e. the path to backtrack on my computer vs yours): 28 | - in .env, change VITE_TARGET_DIR_PATH to the desired path. 29 | - (This is temporary, until CSSxe is a npm package installed in the root of the target repo.) 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CSSexy 2 | 3 |

4 | CSSexy Logo 5 |

6 | 7 | Because your website deserves to look hot. 8 | 9 | ![Javascript](https://img.shields.io/badge/JavaScript-323330?style=for-the-badge&logo=javascript&logoColor=F7DF1E) 10 | ![npm](https://img.shields.io/badge/npm-CB3837?style=for-the-badge&logo=npm&logoColor=white) 11 | ![Puppeteer](https://img.shields.io/badge/Puppeteer-40B5A4?style=for-the-badge&logo=Puppeteer&logoColor=white) 12 | ![NodeJS](https://img.shields.io/badge/node.js-6DA55F?style=for-the-badge&logo=node.js&logoColor=white) 13 | ![Express.js](https://img.shields.io/badge/Express%20js-000000?style=for-the-badge&logo=express&logoColor=white) 14 | ![React](https://img.shields.io/badge/react-%2320232a.svg?style=for-the-badge&logo=react&logoColor=%2361DAFB) 15 | ![Redux](https://img.shields.io/badge/Redux-593D88?style=for-the-badge&logo=redux&logoColor=white) 16 | ![Vite](https://img.shields.io/badge/Vite-B73BFE?style=for-the-badge&logo=vite&logoColor=FFD62E) 17 | ![HTML5](https://img.shields.io/badge/html5-%23E34F26.svg?style=for-the-badge&logo=html5&logoColor=white) 18 | ![CSS3](https://img.shields.io/badge/css3-%231572B6.svg?style=for-the-badge&logo=css3&logoColor=white) 19 | ![Jest](https://img.shields.io/badge/-jest-%23C21325?style=for-the-badge&logo=jest&logoColor=white) 20 | 21 | ## Table of Contents 22 | 23 | - [Introduction](#introduction) 24 | - [Features](#features) 25 | - [Installation](#installation) 26 | - [The CSSexy Team](#team) 27 | - [Contributing](#contributing) 28 | - [License](#license) 29 | 30 | ## Introduction 31 | 32 | We are thrilled to unveil CSSexy, a powerful tool designed to streamline CSS editing and management within your web applications. CSSexy taps into the power of Puppeteer and Chrome Developer Tools to provide a comprehensive interface for viewing and modifying CSS styling rules in real time. You can view where in the source code that the CSS rule is applied, and modify that source with a simple, clean interface. 33 | 34 | ## Features 35 | 36 | - Load your target applicaiton into CSSexy 37 | - View CSS properties by clicking an element on the DOM 38 | - Modify CSS properties in CSSexy and save changes to source code 39 | 40 | ## Installation 41 | 42 | 1. Clone the repo 43 | 2. Run your application on localhost:8000 44 | 3. Run the scripts to install dependencies and start the app 45 | 46 | ```bash 47 | # Example installation steps 48 | git clone https://github.com/oslabs-beta/cssexy.git 49 | cd cssexy 50 | npm install 51 | npm run dev 52 | ``` 53 | 54 | ## The CSSexy Team 55 | 56 | | Developed By | GitHub | LinkedIn | 57 | | :-------------: | :------------------------------------------------------------------------------------------------------------------------------------------------: | :--------------------------------------------------------------------------------------------------------------------------------------------: | 58 | | Mike Basta | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/mikebasta) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/mikebasta/) | 59 | | Elena Netepenko | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/Elena-Netepenko) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/elena-netepenko/) | 60 | | Rob Sand | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/rjsandman) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/) | 61 | | Keith Gibson | [![Github](https://img.shields.io/badge/github-%23121011.svg?style=for-the-badge&logo=github&logoColor=white)](https://github.com/keithgibson) | [![LinkedIn](https://img.shields.io/badge/LinkedIn-%230077B5.svg?logo=linkedin&logoColor=white)](https://www.linkedin.com/in/keithrgibson/) | 62 | | | 63 | 64 | ## Contributing 65 | 66 | Contributions are the foundation of the open-source community. Your contributions help improve our application for developers around the world and are greatly appreciated. 67 | 68 | Feel free to fork the project, implement changes, and submit pull requests to help perfect this product and solve problems others might be facing. 69 | 70 | If you like what CSSexy is doing, please star our project on GitHub! Stars will help boost CSSexy's visibility to developers who may find our product useful or be interested in contributing. 71 | 72 | If you notice any bugs or would like to request features, please browse our [Issues page.](https://github.com/oslabs-beta/cssxe/issues) 73 | 74 | ## License 75 | 76 | CSSexy is developed under the [MIT license.](https://en.wikipedia.org/wiki/MIT_License) 77 | -------------------------------------------------------------------------------- /assets/cssexy_logo_npm_2024-05-09@10x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/cssexy/cf8adcd362c7ea9495c7d4d7dcc1713949ce43d0/assets/cssexy_logo_npm_2024-05-09@10x.png -------------------------------------------------------------------------------- /client/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/oslabs-beta/cssexy/cf8adcd362c7ea9495c7d4d7dcc1713949ce43d0/client/.DS_Store -------------------------------------------------------------------------------- /client/App.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import SidebarComp from './components/SidebarComp'; 4 | import IframeComp from './components/iFrameComp'; 5 | 6 | const App = () => { 7 | 8 | // to access .env files on the client when using ES6 modules + Vite, we need to use 'import.meta.env'. 9 | // Vite gives us the import.meta.env object. It's not available in standalone node.js files, which Vite doesn’t touch. 10 | // we also need to prefix each variable with 'VITE_' that we want Vite to treat as an environment variable, ala 'REACT_APP_' for create-react-app builds. 11 | 12 | const proxy = import.meta.env.VITE_PROXY; 13 | const targetUrl = `http://localhost:${proxy}` 14 | return ( 15 |
16 | 17 | 22 |
23 | ) 24 | }; 25 | 26 | export default App; 27 | -------------------------------------------------------------------------------- /client/cdp/cdp0process.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cdp0Process.js 3 | * Process the given selector using Chrome DevTools Protocol (CDP). 4 | * 5 | * @imports CDP from 'chrome-remote-interface' 6 | * 7 | * @modules 8 | * cdp1enable.js: Enable the domains 9 | * cdp2rules.js: Process the rules/styles for the given selector 10 | * 11 | */ 12 | import fs from 'fs'; 13 | import CDP from 'chrome-remote-interface'; 14 | import { writeFileSync, mkdir } from 'node:fs'; 15 | 16 | import cdpEnable from './cdp1enable.js'; 17 | import cdpRules from './cdp2rules.js'; 18 | 19 | /** 20 | * cdpProcess 21 | * @param {string} attrs - The aatributes received from the iframe 22 | * @param {string} proxy - The proxy to connect to for the Chrome DevTools Protocol 23 | * 24 | */ 25 | 26 | const cdpProcess = async (data) => { 27 | const id = data?.id; 28 | const innerHTML = data?.innerHTML; 29 | const nodeName = data?.nodeName; 30 | const className = data?.className; 31 | // const proxy = data?.proxy; 32 | const nodeType = data?.nodeType; 33 | const textContent = data?.textContent; 34 | // const attributes = data?.attributes; 35 | const selector = data?.selector; 36 | 37 | // console.log('cdpProcess: proxy:', proxy); 38 | let cdpClient; 39 | try { 40 | // setting our selector based on the attributes we received from the iframe. 41 | // starting with the most specific selector and working our way down. 42 | // let selector = ''; 43 | // if (id) { 44 | // console.log('element id:', id); 45 | // selector = `#${id}`; 46 | // } 47 | // else if (className && !className.includes(' ')) { 48 | // console.log('element class:', className); 49 | // selector = `.${className}`; 50 | // } 51 | // else if (nodeName) { 52 | // console.log('element nodeName:', nodeName); 53 | // console.log('element className:', className); 54 | // selector = `${nodeName}`; 55 | // } 56 | // else if (innerHTML) { 57 | // console.log('element innerHTML:', innerHTML); 58 | // selector = `${innerHTML}`; 59 | // } 60 | // else if (textContent) { 61 | // console.log('element textContent:', textContent); 62 | // selector = `${textContent}`; 63 | // } 64 | 65 | console.log('cdpProcess: selector:', selector); 66 | 67 | // console.log('cdpProcess: trying to connect to CDP'); 68 | 69 | // cdpClient is a newly created object that serves as our interface to send commands 70 | // and listen to events in Chrome via the Chrome DevTools Protocol (CDP) by way of 71 | // chrome-remote-interface, a library that allows for easy access to the Chrome DevTools Protocol. 72 | cdpClient = await CDP(); 73 | // a version where we specify the tab we want to connect to, though I didn’t notice any difference or benefit in trying it. 74 | // cdpClient = await CDP({tab: 'http://localhost:5555'}); 75 | 76 | // console.log('Connected to Chrome DevTools Protocol via chrome-remote-interface'); 77 | 78 | // extracting the 'domains' from the CDP client. 79 | const {DOM, CSS, Network, Page, Overlay, iframeNode, styleSheets } = await cdpEnable(cdpClient); 80 | 81 | // these allow us to see / save all of the methods and properties that the CDP client exposes. 82 | // fs.writeFileSync('./data/domains/DOM.json', JSON.stringify(Object.entries(DOM), null, 2)); 83 | // fs.writeFileSync('./data/domains/Network.json', JSON.stringify(Object.entries(Network), null, 2)); 84 | // fs.writeFileSync('./data/domains/Page.json', JSON.stringify(Object.entries(Page), null, 2)); 85 | // fs.writeFileSync('./data/domains/CSS.json', JSON.stringify(Object.entries(CSS), null, 2)); 86 | 87 | // this is the core functionality of cssxe that retrieves styles from a website 88 | // console.log('cdpProcess: calling cdpRules'); 89 | 90 | // right now, result is an object that has both the matched and inline styles for the element clicked. 91 | const result = await cdpRules(cdpClient, DOM, CSS, Network, Page, Overlay, iframeNode, selector, styleSheets); 92 | // console.log(`Rules for ${selector} retrieved`, result); 93 | return result; 94 | 95 | 96 | } catch (err) { 97 | console.error('Error connecting to Chrome', err); 98 | } 99 | finally { 100 | // It is considered a best practice to close resources such as connections in a finally block. 101 | // This ensures they are properly cleaned up, even in the event of an error. 102 | // Leaving connections open can lead to resource leaks and potential issues with system performance. 103 | if (cdpClient) { 104 | await cdpClient.close(); 105 | console.log('CDP client closed'); 106 | } 107 | } 108 | } 109 | 110 | 111 | 112 | export default cdpProcess; 113 | -------------------------------------------------------------------------------- /client/cdp/cdp1enable.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cdp1Enable.js 3 | * Enables the necessary CDP listeners and state on the browser side to interact with the specified domain. 4 | * 5 | * @param {object} client - The client object for interacting with the browser. 6 | * @param {string} port - The URL of the page to navigate to. 7 | * @return {object} An object containing the enabled DOM, CSS, Network, and Page domains. 8 | */ 9 | 10 | import { writeFileSync, mkdir } from 'node:fs'; 11 | 12 | const cdpEnable = async (cdpClient) => { 13 | // extract the different 'domains' from the client. 14 | const { DOM, CSS, Network, Page, Overlay } = cdpClient; 15 | 16 | // getting the target URL from the environment variables 17 | const targetUrl = `http://localhost:${process.env.VITE_PROXY}/`; 18 | console.log('cdpEnable: targetUrl:', targetUrl); 19 | 20 | // 'enable' on a domain sets up the necessary listeners and state on the browser side to interact with that domain. 21 | // this is a prerequisite step before we can use the methods provided by each of the domains. 22 | // enabling a domain starts the flow of events and allows command execution within that domain. 23 | 24 | // DOM: to interact with the structure of the DOM. 25 | // CSS: to query and manipulate CSS styles. 26 | // Network: to inspect network activity and manage network conditions. 27 | // Page: to control page navigation, lifecycle, and size. 28 | await Promise.all([DOM.enable(() => { }), CSS.enable(() => { }), Network.enable(() => { }), Page.enable(() => { }), Overlay.enable(() => { })]); 29 | 30 | const styleSheets = {} 31 | 32 | // console.log('cdpEnable: DOM, CSS, Network, and Page domains are enabled'); 33 | CSS.styleSheetAdded((param) => { 34 | if (param.header.sourceMapURL) { 35 | // console.log('styleSheetAdded with sourceMapURL'); 36 | const id = param.header.styleSheetId; 37 | 38 | const sourceMapData = Buffer.from(param.header.sourceMapURL.split(',')[1], 'base64').toString('utf-8'); 39 | const decodedMap = JSON.parse(sourceMapData); 40 | // console.log('\n\n\n'); 41 | // console.log('decodedMap', decodedMap); 42 | writeFileSync('./data/output/decodedMap.json', JSON.stringify(decodedMap, null, 2)); 43 | const sources = decodedMap.sources; 44 | const absolutePaths = [] 45 | const relativePaths = []; 46 | sources.forEach(source => { 47 | // splitting the source string on the '://' 48 | // pushing the second part, the path, into the paths array 49 | if (source.includes('://')) { 50 | relativePaths.push(source.split('://')[1]); 51 | } 52 | else { 53 | absolutePaths.push(source); 54 | } 55 | }) 56 | 57 | styleSheets[id] = { 58 | sources, 59 | absolutePaths, 60 | relativePaths 61 | } 62 | } 63 | else { 64 | // console.log('styleSheetAdded: no sourceMapURL'); 65 | // console.log('styleSheetParamHeader:', param.header); 66 | } 67 | }); 68 | 69 | // console.log('getting nodes'); 70 | // getFlattenedDocument: returns a flattened array of the DOM tree at the specified depth 71 | // if no depth is specified, the entire DOM tree is returned. 72 | // depth: depth of the dom tree that we want 73 | // -> -1 means we want to get the entire DOM tree. 74 | // -> >= 0 would correspond to a specific depth of the DOM tree. 75 | // however, it is deprecated. 76 | const { nodes } = await DOM.getFlattenedDocument({ depth: -1 }); 77 | 78 | // Create the directory before trying to add files. 79 | await mkdir((new URL('../../data/output/', import.meta.url)), { recursive: true }, (err) => { 80 | if (err) throw err; 81 | }); 82 | 83 | // writeFileSync('./data/output/nodes.json', JSON.stringify(nodes, null, 2)); 84 | 85 | // Find nodes where the nodeName property is 'IFRAME'. 86 | // In looking through the nodes, I saw only one IFRAME node, which corresponded to the root node of the iframe. 87 | // TBD if there would be more than one if the site we are targeting has iframes within it. 88 | const iframeNodeId = await nodes.filter(node => node.nodeName === 'IFRAME')[0].nodeId; 89 | 90 | // console.log('iframeNodeId', iframeNodeId); 91 | 92 | // describeNode: gets a description of a node with a given DOM nodeId, i.e. the type of node, its name, and its children. 93 | const { node } = await DOM.describeNode({ nodeId: iframeNodeId }); 94 | 95 | // console.log('cdpEnable: node', node); 96 | 97 | // from there we get the contentDocument of the iframeNode, 98 | // which is the html document of the iframe 99 | const iframeNode = node.contentDocument; 100 | // console.log('Node inside iframe', iframeNode); 101 | 102 | // this saves the nodes 103 | writeFileSync('./data/output/nodes.json', JSON.stringify(nodes, null, 2)); 104 | 105 | // this saves the contentDocument node of the iframe 106 | writeFileSync('./data/output/iframeNode.json', JSON.stringify(iframeNode, null, 2)); 107 | 108 | // Return the enabled domains and the nodeId of the iframe root node to the process 109 | return { DOM, CSS, Network, Page, Overlay, iframeNode, styleSheets }; 110 | } 111 | 112 | export default cdpEnable; 113 | -------------------------------------------------------------------------------- /client/cdp/cdp2rules.js: -------------------------------------------------------------------------------- 1 | /** 2 | * cdp2styles.js 3 | * Retrieves the CSS rules for a specified DOM node, returns the applied rules 4 | * 5 | * @param {object} cdpClient - The Chrome DevTools Protocol client 6 | * @param {object} DOM - The DOM domain object 7 | * @param {object} CSS - The CSS domain object 8 | * @param {object} Network - The Network domain object 9 | * @param {object} Page - The Page domain object 10 | * @param {object} iframeNode - The iframe node object 11 | * @param {string} selector - The CSS selector for the node 12 | * @return {array} The applied CSS rules 13 | */ 14 | 15 | import fs from 'fs'; 16 | const cdpInlineRules = async (CSS, nodeId, selector) => { 17 | // retrieve the inline styles for the node with the provided nodeId 18 | try { 19 | 20 | const { inlineStyle } = await CSS.getInlineStylesForNode({ nodeId }); 21 | 22 | const inlineRule = []; 23 | 24 | // console.log('cdpInlineRules: inlineRule:', inlineRule); 25 | 26 | // check if there are any inline styles for this node 27 | if (inlineStyle) { 28 | 29 | console.log(`Found: inline styles for selector '${selector}' with nodeId ${nodeId}.`); 30 | // push the inline styles to the inlineRule array 31 | inlineRule.push({ 32 | "rule": { 33 | "origin": "inline", 34 | "style": inlineStyle, 35 | } 36 | }) 37 | 38 | } else { 39 | // if no inline styles are present 40 | console.log(`Not Found: inline styles for selector '${selector}' with nodeId ${nodeId}.`); 41 | } 42 | return inlineRule; 43 | 44 | } catch (error) { 45 | console.log('cdpInlineRules: error:', error); 46 | } 47 | } 48 | 49 | const cdpRules = async (cdpClient, DOM, CSS, Network, Page, Overlay, iframeNode, selector, styleSheets) => { 50 | 51 | 52 | 53 | const iframeNodeId = iframeNode.nodeId; 54 | // console.log('cdpRules: root frame node id:', iframeNodeId); 55 | 56 | // Get the nodeId of the node based on its CSS selector 57 | const { nodeId } = await DOM.querySelector({ 58 | nodeId: iframeNodeId, 59 | selector: selector 60 | }); 61 | 62 | // console.log('cdpRules: nodeId for selector', selector, 'is:', nodeId); 63 | 64 | // console.log('cdpRules: Getting inline styles for element:', selector); 65 | // Get the inline styles 66 | const inlineRules = await cdpInlineRules(CSS, nodeId, selector); 67 | 68 | 69 | // console.log('cdpRules: Getting matched styles for element:', selector); 70 | 71 | // get all CSS rules that are applied to the node 72 | // => matchedCSSRules contains CSS rules that are directly applied to the node 73 | // => inherited contains the CSS rules that are passed down from the node's ancestors 74 | // => cssKeyframesRules includes all the @keyframes rules applied to the node 75 | 76 | const { matchedCSSRules, inherited: inheritedRules, cssKeyframesRules: keyframeRules } = await CSS.getMatchedStylesForNode({ nodeId }); 77 | const regularRules = []; 78 | const userAgentRules = []; 79 | 80 | // console.log('cdpRules: matchedCSSRules:', matchedCSSRules); 81 | 82 | // this separates the matchedCSSRules into regularRules and userAgentRules 83 | // ahead of them being returned to iframeComp, where they then update the store 84 | // via dispatches. 85 | const parseMatchedCSSRules = async (matchedCSSRules) => { 86 | await matchedCSSRules.forEach((rule) => { 87 | if (rule.rule.origin === 'regular') { 88 | regularRules.push(rule); 89 | } 90 | else if (rule.rule.origin === 'user-agent') { 91 | userAgentRules.push(rule); 92 | } 93 | }) 94 | } 95 | parseMatchedCSSRules(matchedCSSRules); 96 | 97 | const result = { 98 | inlineRules, 99 | regularRules, 100 | userAgentRules, 101 | styleSheets, 102 | inheritedRules, 103 | // keyframeRules 104 | } 105 | 106 | // fs.writeFileSync('./data/output/allRules.json', JSON.stringify(result, null, 2)); 107 | // fs.writeFileSync('./data/output/inlineRules.json', JSON.stringify(inlineRules, null, 2)); 108 | // fs.writeFileSync('./data/output/regularRules.json', JSON.stringify(regularRules, null, 2)); 109 | // fs.writeFileSync('./data/output/userAgentRules.json', JSON.stringify(userAgentRules, null, 2)); 110 | // fs.writeFileSync('./data/output/inheritedRules.json', JSON.stringify(inheritedRules, null, 2)); 111 | // fs.writeFileSync('./data/output/keyframeRules.json', JSON.stringify(keyframeRules, null, 2)); 112 | 113 | console.log('cdpRules: returning result {inlineRules, regularRules, userAgentRules}'); 114 | return result; 115 | } 116 | 117 | export default cdpRules 118 | -------------------------------------------------------------------------------- /client/components/RulesAllComp.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useMemo, useEffect } from 'react'; 2 | import { useSelector } from 'react-redux'; 3 | import SidebarStyling from './SidebarStyling.jsx'; 4 | import RulesUserAgentComp from "./RulesUserAgentComp.jsx"; 5 | 6 | function RulesAllComp() { 7 | // contains all of the inline styles (styles specified directly on a component) 8 | const inlineRules = useSelector(state => state.rules.inlineRules); 9 | // contains all of the regular styles (styles specified in .css files) 10 | const regularRules = useSelector(state => state.rules.regularRules); 11 | // contains information about all of the .css files that are currently loaded 12 | const styleSheets = useSelector(state => state.rules.styleSheets); 13 | 14 | // tracks the path of the currently selected .s/css file 15 | const [sourcePath, setSourcePath] = useState(null); 16 | // tracks the name of the currently selected .s/css file 17 | const [sourceName, setSourceName] = useState(null); 18 | // tracks the path of the first .s/css file in the array of .css files 19 | const [firstSourcePath, setFirstSourcePath] = useState(null); 20 | 21 | const compareSpecificityDescending = (obj1, obj2) => { 22 | if (obj1.calculatedSpecificity.a !== obj2.calculatedSpecificity.a) { 23 | return obj1.calculatedSpecificity.a < obj2.calculatedSpecificity.a ? 1 : -1; 24 | } 25 | // If 'a' values are equal, compare the 'b' values 26 | else if (obj1.calculatedSpecificity.b !== obj2.calculatedSpecificity.b) { 27 | return obj1.calculatedSpecificity.b < obj2.calculatedSpecificity.b ? 1 : -1; 28 | } 29 | // If 'b' values are equal, compare the 'c' values 30 | else if (obj1.calculatedSpecificity.c !== obj2.calculatedSpecificity.c) { 31 | return obj1.calculatedSpecificity.c < obj2.calculatedSpecificity.c ? 1 : -1; 32 | } 33 | else return 0; 34 | }; 35 | 36 | useEffect(() => { 37 | if (regularRules.length > 0) { 38 | // styleSheetId is a variable that we use to keep track of which .css file we want to look at 39 | const styleSheetId = regularRules[0]?.rule.style.styleSheetId; 40 | // we set the firstSourcePath variable to the absolute path (if it exists) or the relative path of the first .css file returned by the styleSheets object for the clicked element. 41 | setFirstSourcePath(styleSheets[styleSheetId]?.absolutePaths[0] ? styleSheets[styleSheetId].absolutePaths[0] : styleSheets[styleSheetId]?.relativePaths[0]); 42 | // if the first .css file is different from the currently selected .css file, we update the sourcePath variable to reflect the new selection 43 | if (styleSheets[styleSheetId] && sourcePath !== firstSourcePath) { 44 | setSourcePath(firstSourcePath); 45 | const splitPaths = firstSourcePath.split('/'); 46 | // sourceNameString is a variable that we use to keep track of the name of the currently selected .css file 47 | const sourceNameString = `/${splitPaths[splitPaths.length - 1]}`; 48 | // we update the sourceName variable to reflect the new selection 49 | setSourceName(sourceNameString); 50 | } 51 | } 52 | // useEffect is a hook provided by React. It lets us run code when specific pieces of data change. In this case, if the regularRules or styleSheets data changes, we want to run the code inside the useEffect block 53 | }, [styleSheets, regularRules]); 54 | 55 | // map() to create an array of SidebarStyling components 56 | // from the inlineRules data and another from the regularRules data. 57 | const RulesInlineComp = inlineRules.map((each, idx) => { 58 | return ( 59 | 64 | ) 65 | }); 66 | 67 | // sort all selector blocks rendered in UI - based on specificity, from highest to lowest 68 | const regularRulesSorted = regularRules.toSorted(compareSpecificityDescending); 69 | const RulesRegularComp = regularRulesSorted.map((each, idx) => { 70 | let regularSelector = ''; 71 | if (each.matchingSelectors.length === 1) regularSelector = each.rule.selectorList.selectors[each.matchingSelectors[0]].text; 72 | // combine selectors where there're multiple selectors in matchingSelectors array, e.g. '.btn, #active' 73 | else if (each.matchingSelectors.length > 1) { 74 | for (let i = 0; i < each.matchingSelectors.length; i++) { 75 | const idx = each.matchingSelectors[i]; 76 | regularSelector += each.rule.selectorList.selectors[idx].text; 77 | if (i !== each.matchingSelectors.length - 1) regularSelector += ', '; 78 | } 79 | }; 80 | return ( 81 | 88 | ) 89 | }); 90 | 91 | return ( 92 |
93 |

inline

94 | {/* ternary to render a line break if there are no rules. Improves readability imo */} 95 | <>{RulesInlineComp.length ? RulesInlineComp :
} 96 | {/*

.css

*/} 97 |

{sourceName ? sourceName : 'css'}

98 | {/* same ternary, same reason */} 99 | <>{RulesRegularComp.length ? RulesRegularComp :
} 100 | 101 |
102 | ) 103 | }; 104 | 105 | export default RulesAllComp; 106 | -------------------------------------------------------------------------------- /client/components/RulesUserAgentComp.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { nanoid } from 'nanoid'; 3 | import { useSelector } from 'react-redux'; 4 | import SidebarStyling from './SidebarStyling.jsx'; 5 | 6 | /* Styles included: default browser styles*/ 7 | 8 | function RulesUserAgentComp() { 9 | const userAgentRulesData = useSelector(state => state.rules.userAgentRules); 10 | const shortToLongMap = useSelector(state => state.rules.shortToLongMap); 11 | // userAgentRules stores data by selector, so that we can display by selector 12 | const userAgentRules = {}; 13 | 14 | const ObjToArr = stylesObj => { 15 | const arr = []; 16 | for (let style in stylesObj) { 17 | arr.push({ 18 | name: style, 19 | value: stylesObj[style].val, 20 | isActive: stylesObj[style].isActive 21 | }) 22 | } 23 | return arr; 24 | }; 25 | 26 | const compareSpecificityDescending = (obj1, obj2) => { 27 | if (obj1.specificity.a !== obj2.specificity.a) { 28 | return obj1.specificity.a < obj2.specificity.a ? 1 : -1; 29 | } 30 | // If 'a' values are equal, compare the 'b' values 31 | else if (obj1.specificity.b !== obj2.specificity.b) { 32 | return obj1.specificity.b < obj2.specificity.b ? 1 : -1; 33 | } 34 | // If 'b' values are equal, compare the 'c' values 35 | else if (obj1.specificity.c !== obj2.specificity.c) { 36 | return obj1.specificity.c < obj2.specificity.c ? 1 : -1; 37 | } 38 | else return 0; 39 | }; 40 | 41 | userAgentRulesData.forEach(style => { 42 | let userAgentSelector; 43 | // user-agent styles typically have only 1 matching selector 44 | if (style.matchingSelectors.length === 1) { 45 | userAgentSelector = style.rule.selectorList.selectors[style.matchingSelectors[0]].text; 46 | const specificity = style.calculatedSpecificity; 47 | // we are only showing valid selectors which have styles attached to them 48 | if (style.rule.style.cssProperties.length) { 49 | if (!userAgentRules[userAgentSelector]) { 50 | userAgentRules[userAgentSelector] = { 51 | properties: {}, 52 | specificity 53 | }; 54 | }; 55 | }; 56 | } 57 | // if you encounter the error below, add the logic that iterates through all matching selectors and finds the one with highest specificity 58 | else throw new Error('MULTIPLE MATCHING SELECTORS ARE FOUND IN "matchingSelectors" ARRAY!'); 59 | 60 | // add all longhand properties 61 | for (let cssProperty of style.rule.style.cssProperties) { 62 | if (cssProperty.value) { 63 | userAgentRules[userAgentSelector]['properties'][cssProperty.name] = { 64 | val: cssProperty.value, 65 | isActive: cssProperty.isActive 66 | } 67 | } 68 | } 69 | const shorthandStyles = style.rule.style.shorthandEntries; 70 | if (shorthandStyles.length) { 71 | for (let shortStyle of shorthandStyles) { 72 | // add all shorthand properties 73 | if (shortStyle.value) { 74 | userAgentRules[userAgentSelector]['properties'][shortStyle.name] = { 75 | val: shortStyle.value, 76 | isActive: shortStyle.isActive 77 | }; 78 | 79 | // get and remove longhand properties corresponding to each shorthand 80 | const longhands = shortToLongMap[shortStyle.name]; 81 | longhands.forEach(lh => { 82 | if (userAgentRules[userAgentSelector]['properties'][lh]) delete userAgentRules[userAgentSelector]['properties'][lh]; 83 | }) 84 | } 85 | } 86 | } 87 | }); 88 | 89 | // convert userAgentRules object into array, sort it by specificity in descending order and generate jsx components to render 90 | const userAgentRulesAr = []; 91 | for (let selector in userAgentRules) { 92 | userAgentRulesAr.push({ 93 | selector: selector, 94 | properties: userAgentRules[selector].properties, 95 | specificity: userAgentRules[selector].specificity 96 | }); 97 | }; 98 | 99 | userAgentRulesAr.sort(compareSpecificityDescending); 100 | 101 | const sidebarStylingComponents = userAgentRulesAr.map(each => { 102 | return ( 103 | 109 | ) 110 | }); 111 | 112 | return ( 113 |
114 |

user agent

115 | {/* making this conditionally rendered as otherwise there is a bottom border where there's not one for inline and regular */} 116 | {sidebarStylingComponents.length > 0 && sidebarStylingComponents} 117 |
118 | ) 119 | }; 120 | 121 | export default RulesUserAgentComp; 122 | -------------------------------------------------------------------------------- /client/components/SidebarComp.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import RulesAllComp from "./RulesAllComp.jsx"; 3 | 4 | function SidebarComp() { 5 | // local state variable for toggling the sidebar 6 | const [isCollapsed, setIsCollapsed] = useState(false); 7 | 8 | // Toggling the sidebar visibility when the user presses Shift + Enter 9 | useEffect(() => { 10 | const handleKeyDown = (event) => { 11 | if (event.metaKey && event.key === 'Enter') { 12 | console.log('Shift + Enter pressed. Toggling sidebar visibility.'); 13 | 14 | // The `setIsCollapsed` function is used to update the state. 15 | // The `prevCollapsed` parameter is the previous value of `isCollapsed`. 16 | // The `!prevCollapsed` - the negation of the prior value of this state variable - is the new value of `isCollapsed`. 17 | // In other words, this function toggles the value of `isCollapsed`. 18 | // 'functional update' of state: 19 | // This pattern in React and ensures that the state is always updated correctly, even when multiple calls are made in quick succession. 20 | setIsCollapsed((prevCollapsed) => !prevCollapsed); 21 | } 22 | }; 23 | 24 | // Adding the event listener to the window 25 | window.addEventListener('keydown', handleKeyDown); 26 | 27 | // Cleaning up the event listener when the component unmounts. 28 | // React runs this later, after the component is removed from the DOM. 29 | return () => { 30 | window.removeEventListener('keydown', handleKeyDown); 31 | }; 32 | // not passing any dependencies, as we only want to add the event listener once. 33 | // if we did pass dependencies, the effect would run every time one of them changes 34 | // -> it would first run the cleanup function, and then add the event listener again. 35 | }, []); 36 | 37 | return ( 38 | 39 | // if isCollapsed is true, the sidebar-container and the sidebar will be collapsed 40 |
41 |
42 |

styles

43 | 44 |
45 | 50 |
51 | ); 52 | } 53 | 54 | export default SidebarComp; 55 | -------------------------------------------------------------------------------- /client/components/SidebarStyling.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import { useSelector, useDispatch } from 'react-redux'; 3 | import { updateInlineRules, updateRegularRules, updateUserAgentRules, updateInheritedRules, updateKeyframeRules, updateStyleSheets, findActiveStyles, updateShortLongMaps, setIsActiveFlag, updateNodeData, updateMidShortMap } from '../slices/rulesSlice.js'; 4 | 5 | function SidebarStyling(props) { 6 | 7 | const dispatch = useDispatch(); 8 | 9 | const data = useSelector(state => state.nodeData.data); 10 | const inlineRules = useSelector(state => state.rules.inlineRules); 11 | 12 | // console.log('SidebarStyling: props', props); 13 | 14 | // spread operator to make a deep copy of props, so that we can then modify it. 15 | const liveProps = {...props}; 16 | 17 | // console.log('liveProps', liveProps); 18 | 19 | const [values, setValues] = useState({}); 20 | 21 | // this useEffect ensures that 'values' is updated only when props.cssProperties changes, rather than on every re-render. 22 | // i was getting some rerendering errors prior to this when modifying source files. 23 | useEffect(() => { 24 | setValues( 25 | liveProps.cssProperties.reduce((acc, cssProp) => { 26 | acc[cssProp.name] = cssProp.value; 27 | return acc; 28 | }, {}) 29 | ); 30 | }, [props.cssProperties]); 31 | 32 | // this is the same as the fetch and reducer code in iFrameComp.jsx. 33 | // a good refactor would be to place this into its own reducer and fetch function, or at least in a nother file that is called by both iFrameComp and this file. 34 | const callCdp = async () => { 35 | if (!data) { 36 | console.log('RunCdp: runCdp: data is undefined'); 37 | return; 38 | } 39 | const response = await fetch('/cdp', { 40 | method: 'POST', 41 | headers: { 42 | 'Content-Type': 'application/json', 43 | }, 44 | body: JSON.stringify(data), 45 | }); 46 | 47 | const result = await response.json(); 48 | console.log('sidebarStyling: runCdp: result', result); 49 | 50 | if (result) { 51 | console.log('sidebarStyling: runCdp: data', data); 52 | dispatch(updateNodeData(data)); 53 | 54 | dispatch(updateInlineRules(result.inlineRules)); 55 | dispatch(updateRegularRules(result.regularRules)); 56 | dispatch(updateUserAgentRules(result.userAgentRules)); 57 | dispatch(updateStyleSheets(result.styleSheets)); 58 | dispatch(updateInheritedRules(result.inheritedRules)); 59 | 60 | // actions needed for style overwrite functionality 61 | dispatch(updateShortLongMaps()); 62 | dispatch(updateMidShortMap()); 63 | dispatch(setIsActiveFlag()); 64 | dispatch(findActiveStyles()); 65 | 66 | } 67 | }; 68 | 69 | const handleSubmit = async (cssProp, event) => { 70 | // console.log('cssProp', cssProp); 71 | const updatedCssProp = {...cssProp, value: values[cssProp.name]}; 72 | updatedCssProp.valuePrev = cssProp.value; 73 | updatedCssProp.textPrev = updatedCssProp.text; 74 | updatedCssProp.text = `${cssProp.name}: ${values[updatedCssProp.name]};`; 75 | // console.log('\n'); 76 | // console.log(cssProp.name); 77 | // console.log(cssProp.value); 78 | // console.log('->'); 79 | // console.log(updatedCssProp.value); 80 | // console.log('\n'); 81 | 82 | updatedCssProp.selector = liveProps.selector; 83 | updatedCssProp.sourcePath = liveProps.sourcePath; 84 | 85 | const textPrevAll = inlineRules[0].rule.style.cssText; 86 | updatedCssProp.textPrevAll = textPrevAll; 87 | 88 | console.log('updatedCssProp', updatedCssProp); 89 | // console.log('TRY: /patch'); 90 | try { 91 | const response = await fetch('/patch', { 92 | method: 'POST', 93 | headers: { 94 | 'Content-Type': 'application/json', 95 | }, 96 | body: JSON.stringify(updatedCssProp), 97 | 98 | }); 99 | const result = await response.json(); 100 | 101 | if (result === true) { 102 | // console.log('TRY: /runCdp'); 103 | 104 | // wait for .5 seconds. not doing this atm leads to a mismatch between the value in the input field and the corresponding value in the file, which then prevents further editing of that value (until the element is clicked again) because our patchFile function matches one to the other in order to replace the value with the new value. 105 | // inline styles need a bit more time, so if theres no source path, wait 1 second. 106 | await new Promise(resolve => updatedCssProp.sourcePath ? setTimeout(resolve, 500) : setTimeout(resolve, 1000)); 107 | 108 | // running CDP again to update our redux store after patching the file. 109 | await callCdp(); 110 | 111 | // probably around here is where we'll track undo/redo. 112 | } 113 | } 114 | catch(error) { 115 | console.log('error in runCdp', error); 116 | } 117 | }; 118 | 119 | const styleParagraphs = liveProps.cssProperties.map((cssProp, idx) => { 120 | if ((liveProps.origin === 'user-agent')) { 121 | return ( 122 |

123 | 124 | {cssProp.name}: 125 | 126 | 127 | {cssProp.value} 128 | 129 |

130 | ) 131 | } 132 | // if not user agent style, then it's a regular or inline style (at the moment 2024-04-03), which we make editable below. 133 | else if ((liveProps.origin && cssProp.text)) { 134 | // cssProperties arr includes both user defined 'shorthand' styles and css 'longhand' styles. We want to render only user defined styles => style is user defined if it has .text property 135 | // shorthand example: 'border-bottom: 3px solid blueviolet' 136 | // longhand example: border-bottom-width: 3px; border-bottom-style: solid; border-bottom-color: blueviolet 137 | return ( 138 |

139 | 140 | {cssProp.name}: 141 | 142 | setValues({...values, [cssProp.name]: e.target.value})} 146 | onKeyDown={(e) => { 147 | if (e.key === 'Enter') { 148 | handleSubmit(cssProp, e) 149 | // 'blur': makes the input field lose focus, i.e. the cursor disappears, it won't be editable until it's clicked again 150 | e.currentTarget.blur() 151 | } 152 | }} 153 | spellCheck='false' /* Disable spellcheck, i.e. no more red squiggles under the values when clicked/edited but not a complete word */ 154 | /> 155 | 156 |

157 | ) 158 | } 159 | } 160 | ) 161 | 162 | return ( 163 |
164 |
165 | {liveProps.selector &&

{liveProps.selector}

} 166 | {/* {liveProps?.source?.paths[0] &&

{liveProps?.source?.paths[0]}

} */} 167 |
168 | {styleParagraphs} 169 |
170 |
171 | ) 172 | }; 173 | 174 | export default SidebarStyling; 175 | -------------------------------------------------------------------------------- /client/components/iFrameComp.jsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import { useDispatch, useSelector } from 'react-redux'; 3 | import { updateInlineRules, updateRegularRules, updateUserAgentRules, updateInheritedRules, updateKeyframeRules, updateStyleSheets, findActiveStyles, updateShortLongMaps, updateMidShortMap, setIsActiveFlag, setSpecificity, resetCache, updateNodeData } from '../slices/rulesSlice.js'; 4 | import DOMPath from 'chrome-dompath'; 5 | 6 | /** 7 | * Renders an iframe component with event handling for click events. 8 | * 9 | * @param {Object} props - The component props. 10 | * @param {string} props.src - The source URL for the iframe. 11 | * @param {string} props.className - The CSS class name for the iframe. 12 | * @returns {JSX.Element} The rendered iframe component. 13 | */ 14 | 15 | const iFrameComp = ({ src, proxy, className }) => { 16 | // const inlineRules = useSelector(state => state.rules.inlineRules); 17 | // console.log('INLINE RULES: ', inlineRules); 18 | // const regularRules = useSelector(state => state.rules.regularRules); 19 | // console.log('REGULAR RULES: ', regularRules); 20 | // const userAgentRules = useSelector(state => state.rules.userAgentRules); 21 | // console.log('USER AGENT RULES: ', userAgentRules); 22 | // const isActiveCache = useSelector(state => state.rules.isActiveCache); 23 | // console.log('IS ACTIVE CACHE: ', isActiveCache); 24 | 25 | const dispatch = useDispatch(); 26 | 27 | // waiting for the iframe DOM to load before we add event listeners 28 | // without this, the event listeners would try to be added to an unexisting iframe 29 | useEffect(() => { 30 | // getting our iframe 31 | const iframe = document.querySelector(`.${className}`); 32 | // console.log('iFrameComp: iframe', iframe); 33 | 34 | const handleLoad = () => { 35 | try { 36 | const iframeDoc = iframe.contentDocument || iframe.contentWindow.document; 37 | // console.log('iFrameComp: iframeDoc', iframeDoc); 38 | 39 | const handleClick = async (element) => { 40 | console.log('iFrameComp: element', element); 41 | 42 | // getting the 'selector' of the element, i.e. the same thing that one would get by inspecting the element with dev tools, then right clicking in the dom tree and selecting copy selector. 43 | // we get this using the DOMPath library, which is a port of the relevant piece of Chromium's Chrome Dev Tools front-end code that does the same thing. This should get us a unique, specific selector for the clicked element every time. In testing so far it has always worked. 2024-04-01_08-14-PM. 44 | // https://github.com/testimio/DOMPath 45 | 46 | const selector = DOMPath.fullQualifiedSelector(element); 47 | // with true, we get an 'optimized' selector. doesn’t seem to matter which we choose so far. they both have worked. I'm including true now so we recall its an option. if for some reason it doesn’t work, we can switch to false (i.e. only pass one param, the selector) 48 | // true: #landingAndSticky > div > h1 49 | // false: div#landingAndSticky > div > h1 50 | // console.log('\n\n'); 51 | console.log('iFrameComp: selector', selector); 52 | // console.log('\n\n'); 53 | 54 | const data = { 55 | // id: element.id, 56 | // innerHTML: element.innerHTML, 57 | // nodeName: element.nodeName, 58 | // className: element.className, 59 | // proxy: proxy, 60 | // nodeType: element.nodeType, 61 | // textContent: element.textContent, 62 | // attributes: element.attributes, 63 | selector 64 | }; 65 | 66 | // a POST request to the /cdp endpoint 67 | const response = await fetch('/cdp', { 68 | method: 'POST', 69 | headers: { 70 | 'Content-Type': 'application/json', 71 | }, 72 | body: JSON.stringify(data), 73 | }); 74 | 75 | // console.log('iFrameComp: response', response); 76 | 77 | const result = await response.json(); 78 | 79 | dispatch(updateNodeData(data)); 80 | // console.log('iFrameComp: Result returned from /cdp'); 81 | // console.log('iFrameComp: Result : ', result); 82 | 83 | // dispatching the results from the /cdp endpoint to the store 84 | dispatch(updateInlineRules(result.inlineRules)); 85 | dispatch(updateRegularRules(result.regularRules)); 86 | dispatch(updateUserAgentRules(result.userAgentRules)); 87 | dispatch(updateStyleSheets(result.styleSheets)); 88 | dispatch(updateInheritedRules(result.inheritedRules)); 89 | // dispatch(updateKeyframeRules(result.keyframeRules)); 90 | 91 | // actions needed for style overwrite functionality 92 | dispatch(resetCache()); 93 | dispatch(updateShortLongMaps()); 94 | dispatch(updateMidShortMap()); 95 | dispatch(setIsActiveFlag()); 96 | dispatch(setSpecificity()); 97 | dispatch(findActiveStyles()); 98 | }; 99 | 100 | 101 | // This event listener needs to be added to the iframe's contentDocument because 102 | // we're listening for clicks inside the iframe, and those clicks won't be 103 | // handled by React's event delegation system. By adding this event listener, 104 | // we're essentially making the iframe's contentDocument a "portal" for 105 | // clicks to be handled by React. 106 | iframeDoc.addEventListener('click', (event) => { 107 | const element = event.target; 108 | const localName = element.localName; 109 | 110 | // Calling the handleClick function 111 | handleClick(element); 112 | 113 | 114 | // switch the focus to cssxe when the user clicks on something that isnt an input, textarea, or dropdown (select) field. 115 | // without this their interaction with those elements is broken/interrupted, e.g. clicking in a text field in bookswap. 116 | if (localName !== 'input' && localName !== 'textarea' && localName !== 'select') { 117 | 118 | // Set focus back to the parent document 119 | // This allows CSSxe to receive keyboard events again after a click has taken place inside the iframe. 120 | // before doing this, CSSxe would not receive keyboard events again until we clicked inside of the sidebar 121 | window.parent.focus(); 122 | } 123 | 124 | }, false); 125 | 126 | return () => { 127 | // Cleanup function to remove event listener 128 | // Prevents memory leaks 129 | iframeDoc.removeEventListener('click', handleClick, false); 130 | }; 131 | } catch (error) { 132 | console.error("Can't access iframe content:", error); 133 | } 134 | }; 135 | 136 | if (iframe) { 137 | iframe.addEventListener('load', handleLoad); 138 | // Cleanup function to remove event listener 139 | return () => { 140 | iframe.removeEventListener('load', handleLoad); 141 | }; 142 | } 143 | }, []); 144 | 145 | return ( 146 |