├── .babelrc ├── .gitignore ├── demo ├── app.js ├── index.html └── package.json ├── docs ├── BoundIpc.html ├── Browser.html ├── Browser.js.html ├── BrowserManager.html ├── BrowserManager.js.html ├── ElementHandle.html ├── ElementHandle.js.html ├── EventEmitter.html ├── EventEmitter.js.html ├── Frame.html ├── Frame.js.html ├── Ipc.html ├── NetworkManager.js.html ├── Page.html ├── Page.js.html ├── Target.html ├── Target.js.html ├── USKeyboardLayout.js.html ├── WindowPage.html ├── fonts │ ├── Montserrat │ │ ├── Montserrat-Bold.eot │ │ ├── Montserrat-Bold.ttf │ │ ├── Montserrat-Bold.woff │ │ ├── Montserrat-Bold.woff2 │ │ ├── Montserrat-Regular.eot │ │ ├── Montserrat-Regular.ttf │ │ ├── Montserrat-Regular.woff │ │ └── Montserrat-Regular.woff2 │ └── Source-Sans-Pro │ │ ├── sourcesanspro-light-webfont.eot │ │ ├── sourcesanspro-light-webfont.svg │ │ ├── sourcesanspro-light-webfont.ttf │ │ ├── sourcesanspro-light-webfont.woff │ │ ├── sourcesanspro-light-webfont.woff2 │ │ ├── sourcesanspro-regular-webfont.eot │ │ ├── sourcesanspro-regular-webfont.svg │ │ ├── sourcesanspro-regular-webfont.ttf │ │ ├── sourcesanspro-regular-webfont.woff │ │ └── sourcesanspro-regular-webfont.woff2 ├── global.html ├── images.js.html ├── index.html ├── index.js.html ├── ipc.js.html ├── scripts │ ├── collapse.js │ ├── linenumber.js │ ├── nav.js │ ├── polyfill.js │ ├── prettify │ │ ├── Apache-License-2.0.txt │ │ ├── lang-css.js │ │ └── prettify.js │ └── search.js ├── style.css.js.html ├── styles │ ├── jsdoc.css │ └── prettify.css └── util.js.html ├── jsdoc.json ├── package-lock.json ├── package.json ├── preload └── webview.preload.js ├── readme.md ├── renderer ├── Browser.js ├── BrowserManager.js ├── ElementHandle.js ├── EventEmitter.js ├── Frame.js ├── KeyboardShortcuts.js ├── NetworkManager.js ├── Page.js ├── Target.js ├── USKeyboardLayout.js ├── images.js ├── index.js ├── ipc.js ├── libs │ ├── chrome-search.js │ ├── chrome-tabs │ │ ├── chrome-tabs.css.js │ │ └── chrome-tabs.js │ ├── chrome-zoom.js │ └── localshortcutswrap.js ├── style.css.js └── util.js ├── renderer_lib ├── Browser.js ├── BrowserManager.js ├── ElementHandle.js └── EventEmitter.js └── screenshot └── 1.gif /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["electron"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules -------------------------------------------------------------------------------- /demo/app.js: -------------------------------------------------------------------------------- 1 | // Modules to control application life and create native browser window 2 | const { app, BrowserWindow, ipcMain } = require('electron') 3 | 4 | process.env.ipcLog = 1 5 | 6 | var mainWindow 7 | function createWindow() { 8 | // Create the browser window. 9 | mainWindow = new BrowserWindow({ 10 | show: false, 11 | webPreferences: { 12 | webSecurity: false, 13 | webviewTag: true, 14 | // devTools: true, 15 | nodeIntegration: true, 16 | nodeIntegrationInSubFrames: true, 17 | // preload: path.join(__dirname, 'preload.js') 18 | } 19 | }) 20 | mainWindow.maximize(); 21 | mainWindow.show(); 22 | // mainWindow.webContents.openDevTools(); 23 | 24 | // and load the index.html of the app. 25 | mainWindow.loadFile("./index.html") 26 | // Open the DevTools. 27 | // mainWindow.webContents.openDevTools() 28 | } 29 | 30 | // This method will be called when Electron has finished 31 | // initialization and is ready to create browser windows. 32 | // Some APIs can only be used after this event occurs. 33 | app.whenReady().then(createWindow) 34 | 35 | // Quit when all windows are closed. 36 | app.on('window-all-closed', function () { 37 | // On macOS it is common for applications and their menu bar 38 | // to stay active until the user quits explicitly with Cmd + Q 39 | if (process.platform !== 'darwin') app.quit() 40 | }) 41 | 42 | app.on('activate', function () { 43 | // On macOS it's common to re-create a window in the app when the 44 | // dock icon is clicked and there are no other windows open. 45 | if (BrowserWindow.getAllWindows().length === 0) createWindow() 46 | }) 47 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 57 | 58 | 59 | 69 |
70 | 161 | 162 | 163 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-puppeteer-test", 3 | "version": "1.0.0", 4 | "description": "electron-puppeteer-test", 5 | "build": { 6 | "directories": { 7 | "buildResources": "build", 8 | "output": "dist" 9 | }, 10 | "productName": "electron-puppeteer-test", 11 | "appId": "com.sunteng.electron-puppeteer-test.app", 12 | "mac": { 13 | "target": [ 14 | "dmg", 15 | "zip" 16 | ], 17 | "icon": "build/icon/512x512.png" 18 | }, 19 | "win": { 20 | "target": [ 21 | "nsis", 22 | "zip" 23 | ], 24 | "icon": "build/icon/icon.ico" 25 | } 26 | }, 27 | "main": "app.js", 28 | "scripts": { 29 | "start": "electron ." 30 | }, 31 | "devDependencies": { 32 | "electron": "^8.2.3", 33 | "electron-builder": "^22.4.1" 34 | }, 35 | "dependencies": { 36 | "electron-puppeteer": "^1.0.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Bold.eot -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Bold.ttf -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Bold.woff -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Bold.woff2 -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Regular.eot -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Regular.ttf -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Regular.woff -------------------------------------------------------------------------------- /docs/fonts/Montserrat/Montserrat-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Montserrat/Montserrat-Regular.woff2 -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2 -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff -------------------------------------------------------------------------------- /docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/docs/fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2 -------------------------------------------------------------------------------- /docs/scripts/collapse.js: -------------------------------------------------------------------------------- 1 | function hideAllButCurrent(){ 2 | //by default all submenut items are hidden 3 | //but we need to rehide them for search 4 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(parent) { 5 | parent.style.display = "none"; 6 | }); 7 | 8 | //only current page (if it exists) should be opened 9 | var file = window.location.pathname.split("/").pop().replace(/\.html/, ''); 10 | document.querySelectorAll("nav > ul > li > a").forEach(function(parent) { 11 | var href = parent.attributes.href.value.replace(/\.html/, ''); 12 | if (file === href) { 13 | parent.parentNode.querySelectorAll("ul li").forEach(function(elem) { 14 | elem.style.display = "block"; 15 | }); 16 | } 17 | }); 18 | } 19 | 20 | hideAllButCurrent(); -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | 11 | if (source && source[0]) { 12 | anchorHash = document.location.hash.substring(1); 13 | lines = source[0].getElementsByTagName('li'); 14 | totalLines = lines.length; 15 | 16 | for (; i < totalLines; i++) { 17 | lineNumber++; 18 | lineId = 'line' + lineNumber; 19 | lines[i].id = lineId; 20 | if (lineId === anchorHash) { 21 | lines[i].className += ' selected'; 22 | } 23 | } 24 | } 25 | })(); 26 | -------------------------------------------------------------------------------- /docs/scripts/nav.js: -------------------------------------------------------------------------------- 1 | function scrollToNavItem() { 2 | var path = window.location.href.split('/').pop().replace(/\.html/, ''); 3 | document.querySelectorAll('nav a').forEach(function(link) { 4 | var href = link.attributes.href.value.replace(/\.html/, ''); 5 | if (path === href) { 6 | link.scrollIntoView({block: 'center'}); 7 | return; 8 | } 9 | }) 10 | } 11 | 12 | scrollToNavItem(); 13 | -------------------------------------------------------------------------------- /docs/scripts/polyfill.js: -------------------------------------------------------------------------------- 1 | //IE Fix, src: https://www.reddit.com/r/programminghorror/comments/6abmcr/nodelist_lacks_foreach_in_internet_explorer/ 2 | if (typeof(NodeList.prototype.forEach)!==typeof(alert)){ 3 | NodeList.prototype.forEach=Array.prototype.forEach; 4 | } -------------------------------------------------------------------------------- /docs/scripts/prettify/Apache-License-2.0.txt: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /docs/scripts/prettify/prettify.js: -------------------------------------------------------------------------------- 1 | var q=null;window.PR_SHOULD_USE_CONTINUATION=!0; 2 | (function(){function L(a){function m(a){var f=a.charCodeAt(0);if(f!==92)return f;var b=a.charAt(1);return(f=r[b])?f:"0"<=b&&b<="7"?parseInt(a.substring(1),8):b==="u"||b==="x"?parseInt(a.substring(2),16):a.charCodeAt(1)}function e(a){if(a<32)return(a<16?"\\x0":"\\x")+a.toString(16);a=String.fromCharCode(a);if(a==="\\"||a==="-"||a==="["||a==="]")a="\\"+a;return a}function h(a){for(var f=a.substring(1,a.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),a= 3 | [],b=[],o=f[0]==="^",c=o?1:0,i=f.length;c122||(d<65||j>90||b.push([Math.max(65,j)|32,Math.min(d,90)|32]),d<97||j>122||b.push([Math.max(97,j)&-33,Math.min(d,122)&-33]))}}b.sort(function(a,f){return a[0]-f[0]||f[1]-a[1]});f=[];j=[NaN,NaN];for(c=0;ci[0]&&(i[1]+1>i[0]&&b.push("-"),b.push(e(i[1])));b.push("]");return b.join("")}function y(a){for(var f=a.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=f.length,d=[],c=0,i=0;c=2&&a==="["?f[c]=h(j):a!=="\\"&&(f[c]=j.replace(/[A-Za-z]/g,function(a){a=a.charCodeAt(0);return"["+String.fromCharCode(a&-33,a|32)+"]"}));return f.join("")}for(var t=0,s=!1,l=!1,p=0,d=a.length;p=5&&"lang-"===b.substring(0,5))&&!(o&&typeof o[1]==="string"))c=!1,b="src";c||(r[f]=b)}i=d;d+=f.length;if(c){c=o[1];var j=f.indexOf(c),k=j+c.length;o[2]&&(k=f.length-o[2].length,j=k-c.length);b=b.substring(5);B(l+i,f.substring(0,j),e,p);B(l+i+j,c,C(b,c),p);B(l+i+k,f.substring(k),e,p)}else p.push(l+i,b)}a.e=p}var h={},y;(function(){for(var e=a.concat(m), 9 | l=[],p={},d=0,g=e.length;d=0;)h[n.charAt(k)]=r;r=r[1];n=""+r;p.hasOwnProperty(n)||(l.push(r),p[n]=q)}l.push(/[\S\s]/);y=L(l)})();var t=m.length;return e}function u(a){var m=[],e=[];a.tripleQuotedStrings?m.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,q,"'\""]):a.multiLineStrings?m.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/, 10 | q,"'\"`"]):m.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,q,"\"'"]);a.verbatimStrings&&e.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,q]);var h=a.hashComments;h&&(a.cStyleComments?(h>1?m.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,q,"#"]):m.push(["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,q,"#"]),e.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,q])):m.push(["com",/^#[^\n\r]*/, 11 | q,"#"]));a.cStyleComments&&(e.push(["com",/^\/\/[^\n\r]*/,q]),e.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,q]));a.regexLiterals&&e.push(["lang-regex",/^(?:^^\.?|[!+-]|!=|!==|#|%|%=|&|&&|&&=|&=|\(|\*|\*=|\+=|,|-=|->|\/|\/=|:|::|;|<|<<|<<=|<=|=|==|===|>|>=|>>|>>=|>>>|>>>=|[?@[^]|\^=|\^\^|\^\^=|{|\||\|=|\|\||\|\|=|~|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(h=a.types)&&e.push(["typ",h]);a=(""+a.keywords).replace(/^ | $/g, 12 | "");a.length&&e.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),q]);m.push(["pln",/^\s+/,q," \r\n\t\xa0"]);e.push(["lit",/^@[$_a-z][\w$@]*/i,q],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,q],["pln",/^[$_a-z][\w$@]*/i,q],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,q,"0123456789"],["pln",/^\\[\S\s]?/,q],["pun",/^.[^\s\w"-$'./@\\`]*/,q]);return x(m,e)}function D(a,m){function e(a){switch(a.nodeType){case 1:if(k.test(a.className))break;if("BR"===a.nodeName)h(a), 13 | a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)e(a);break;case 3:case 4:if(p){var b=a.nodeValue,d=b.match(t);if(d){var c=b.substring(0,d.index);a.nodeValue=c;(b=b.substring(d.index+d[0].length))&&a.parentNode.insertBefore(s.createTextNode(b),a.nextSibling);h(a);c||a.parentNode.removeChild(a)}}}}function h(a){function b(a,d){var e=d?a.cloneNode(!1):a,f=a.parentNode;if(f){var f=b(f,1),g=a.nextSibling;f.appendChild(e);for(var h=g;h;h=g)g=h.nextSibling,f.appendChild(h)}return e} 14 | for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=b(a.nextSibling,0),e;(e=a.parentNode)&&e.nodeType===1;)a=e;d.push(a)}var k=/(?:^|\s)nocode(?:\s|$)/,t=/\r\n?|\n/,s=a.ownerDocument,l;a.currentStyle?l=a.currentStyle.whiteSpace:window.getComputedStyle&&(l=s.defaultView.getComputedStyle(a,q).getPropertyValue("white-space"));var p=l&&"pre"===l.substring(0,3);for(l=s.createElement("LI");a.firstChild;)l.appendChild(a.firstChild);for(var d=[l],g=0;g=0;){var h=m[e];A.hasOwnProperty(h)?window.console&&console.warn("cannot override language handler %s",h):A[h]=a}}function C(a,m){if(!a||!A.hasOwnProperty(a))a=/^\s*=o&&(h+=2);e>=c&&(a+=2)}}catch(w){"console"in window&&console.log(w&&w.stack?w.stack:w)}}var v=["break,continue,do,else,for,if,return,while"],w=[[v,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"], 18 | "catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],F=[w,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],G=[w,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"], 19 | H=[G,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var"],w=[w,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],I=[v,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"], 20 | J=[v,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],v=[v,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],K=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)/,N=/\S/,O=u({keywords:[F,H,w,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+ 21 | I,J,v],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};k(O,["default-code"]);k(x([],[["pln",/^[^]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",/^]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]), 22 | ["default-markup","htm","html","mxml","xhtml","xml","xsl"]);k(x([["pln",/^\s+/,q," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,q,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css", 23 | /^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);k(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);k(u({keywords:F,hashComments:!0,cStyleComments:!0,types:K}),["c","cc","cpp","cxx","cyc","m"]);k(u({keywords:"null,true,false"}),["json"]);k(u({keywords:H,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:K}),["cs"]);k(u({keywords:G,cStyleComments:!0}),["java"]);k(u({keywords:v,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);k(u({keywords:I,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}), 24 | ["cv","py"]);k(u({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);k(u({keywords:J,hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["rb"]);k(u({keywords:w,cStyleComments:!0,regexLiterals:!0}),["js"]);k(u({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,true,try,unless,until,when,while,yes", 25 | hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);k(x([],[["str",/^[\S\s]+/]]),["regex"]);window.prettyPrintOne=function(a,m,e){var h=document.createElement("PRE");h.innerHTML=a;e&&D(h,e);E({g:m,i:e,h:h});return h.innerHTML};window.prettyPrint=function(a){function m(){for(var e=window.PR_SHOULD_USE_CONTINUATION?l.now()+250:Infinity;p=0){var k=k.match(g),f,b;if(b= 26 | !k){b=n;for(var o=void 0,c=b.firstChild;c;c=c.nextSibling)var i=c.nodeType,o=i===1?o?b:c:i===3?N.test(c.nodeValue)?b:o:o;b=(f=o===b?void 0:o)&&"CODE"===f.tagName}b&&(k=f.className.match(g));k&&(k=k[1]);b=!1;for(o=n.parentNode;o;o=o.parentNode)if((o.tagName==="pre"||o.tagName==="code"||o.tagName==="xmp")&&o.className&&o.className.indexOf("prettyprint")>=0){b=!0;break}b||((b=(b=n.className.match(/\blinenums\b(?::(\d+))?/))?b[1]&&b[1].length?+b[1]:!0:!1)&&D(n,b),d={g:k,h:n,i:b},E(d))}}p ul > li:not(.level-hide)").forEach(function(elem) { 16 | elem.style.display = "block"; 17 | }); 18 | 19 | if (typeof hideAllButCurrent === "function"){ 20 | //let's do what ever collapse wants to do 21 | hideAllButCurrent(); 22 | } else { 23 | //menu by default should be opened 24 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 25 | elem.style.display = "block"; 26 | }); 27 | } 28 | } else { 29 | //we are searching 30 | document.documentElement.setAttribute(searchAttr, ''); 31 | 32 | //show all parents 33 | document.querySelectorAll("nav > ul > li").forEach(function(elem) { 34 | elem.style.display = "block"; 35 | }); 36 | //hide all results 37 | document.querySelectorAll("nav > ul > li > ul li").forEach(function(elem) { 38 | elem.style.display = "none"; 39 | }); 40 | //show results matching filter 41 | document.querySelectorAll("nav > ul > li > ul a").forEach(function(elem) { 42 | if (!contains(elem.parentNode, search)) { 43 | return; 44 | } 45 | elem.parentNode.style.display = "block"; 46 | }); 47 | //hide parents without children 48 | document.querySelectorAll("nav > ul > li").forEach(function(parent) { 49 | var countSearchA = 0; 50 | parent.querySelectorAll("a").forEach(function(elem) { 51 | if (contains(elem, search)) { 52 | countSearchA++; 53 | } 54 | }); 55 | 56 | var countUl = 0; 57 | var countUlVisible = 0; 58 | parent.querySelectorAll("ul").forEach(function(ulP) { 59 | // count all elements that match the search 60 | if (contains(ulP, search)) { 61 | countUl++; 62 | } 63 | 64 | // count all visible elements 65 | var children = ulP.children 66 | for (i=0; i ul { 269 | padding: 0 10px; 270 | } 271 | 272 | nav > ul > li > a { 273 | color: #606; 274 | margin-top: 10px; 275 | } 276 | 277 | nav ul ul a { 278 | color: hsl(207, 1%, 60%); 279 | border-left: 1px solid hsl(207, 10%, 86%); 280 | } 281 | 282 | nav ul ul a, 283 | nav ul ul a:active { 284 | padding-left: 20px 285 | } 286 | 287 | nav h2 { 288 | font-size: 13px; 289 | margin: 10px 0 0 0; 290 | padding: 0; 291 | } 292 | 293 | nav > h2 > a { 294 | margin: 10px 0 -10px; 295 | color: #606 !important; 296 | } 297 | 298 | footer { 299 | color: hsl(0, 0%, 28%); 300 | margin-left: 250px; 301 | display: block; 302 | padding: 15px; 303 | font-style: italic; 304 | font-size: 90%; 305 | } 306 | 307 | .ancestors { 308 | color: #999 309 | } 310 | 311 | .ancestors a { 312 | color: #999 !important; 313 | } 314 | 315 | .clear { 316 | clear: both 317 | } 318 | 319 | .important { 320 | font-weight: bold; 321 | color: #950B02; 322 | } 323 | 324 | .yes-def { 325 | text-indent: -1000px 326 | } 327 | 328 | .type-signature { 329 | color: #CA79CA 330 | } 331 | 332 | .type-signature:last-child { 333 | color: #eee; 334 | } 335 | 336 | .name, .signature { 337 | font-family: Consolas, Monaco, 'Andale Mono', monospace 338 | } 339 | 340 | .signature { 341 | color: #fc83ff; 342 | } 343 | 344 | .details { 345 | margin-top: 6px; 346 | border-left: 2px solid #DDD; 347 | line-height: 20px; 348 | font-size: 14px; 349 | } 350 | 351 | .details dt { 352 | width: auto; 353 | float: left; 354 | padding-left: 10px; 355 | } 356 | 357 | .details dd { 358 | margin-left: 70px; 359 | margin-top: 6px; 360 | margin-bottom: 6px; 361 | } 362 | 363 | .details ul { 364 | margin: 0 365 | } 366 | 367 | .details ul { 368 | list-style-type: none 369 | } 370 | 371 | .details pre.prettyprint { 372 | margin: 0 373 | } 374 | 375 | .details .object-value { 376 | padding-top: 0 377 | } 378 | 379 | .description { 380 | margin-bottom: 1em; 381 | margin-top: 1em; 382 | } 383 | 384 | .code-caption { 385 | font-style: italic; 386 | font-size: 107%; 387 | margin: 0; 388 | } 389 | 390 | .prettyprint { 391 | font-size: 14px; 392 | overflow: auto; 393 | } 394 | 395 | .prettyprint.source { 396 | width: inherit; 397 | line-height: 18px; 398 | display: block; 399 | background-color: #0d152a; 400 | color: #aeaeae; 401 | } 402 | 403 | .prettyprint code { 404 | line-height: 18px; 405 | display: block; 406 | background-color: #0d152a; 407 | color: #4D4E53; 408 | } 409 | 410 | .prettyprint > code { 411 | padding: 15px; 412 | } 413 | 414 | .prettyprint .linenums code { 415 | padding: 0 15px 416 | } 417 | 418 | .prettyprint .linenums li:first-of-type code { 419 | padding-top: 15px 420 | } 421 | 422 | .prettyprint code span.line { 423 | display: inline-block 424 | } 425 | 426 | .prettyprint.linenums { 427 | padding-left: 70px; 428 | -webkit-user-select: none; 429 | -moz-user-select: none; 430 | -ms-user-select: none; 431 | user-select: none; 432 | } 433 | 434 | .prettyprint.linenums ol { 435 | padding-left: 0 436 | } 437 | 438 | .prettyprint.linenums li { 439 | border-left: 3px #34446B solid; 440 | } 441 | 442 | .prettyprint.linenums li.selected, .prettyprint.linenums li.selected * { 443 | background-color: #34446B; 444 | } 445 | 446 | .prettyprint.linenums li * { 447 | -webkit-user-select: text; 448 | -moz-user-select: text; 449 | -ms-user-select: text; 450 | user-select: text; 451 | } 452 | 453 | .prettyprint.linenums li code:empty:after { 454 | content:""; 455 | display:inline-block; 456 | width:0px; 457 | } 458 | 459 | table { 460 | border-spacing: 0; 461 | border: 1px solid #ddd; 462 | border-collapse: collapse; 463 | border-radius: 3px; 464 | box-shadow: 0 1px 3px rgba(0,0,0,0.1); 465 | width: 100%; 466 | font-size: 14px; 467 | margin: 1em 0; 468 | } 469 | 470 | td, th { 471 | margin: 0px; 472 | text-align: left; 473 | vertical-align: top; 474 | padding: 10px; 475 | display: table-cell; 476 | } 477 | 478 | thead tr, thead tr { 479 | background-color: #fff; 480 | font-weight: bold; 481 | border-bottom: 1px solid #ddd; 482 | } 483 | 484 | .params .type { 485 | white-space: nowrap; 486 | } 487 | 488 | .params code { 489 | white-space: pre; 490 | } 491 | 492 | .params td, .params .name, .props .name, .name code { 493 | color: #4D4E53; 494 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 495 | font-size: 100%; 496 | } 497 | 498 | .params td { 499 | border-top: 1px solid #eee 500 | } 501 | 502 | .params td.description > p:first-child, .props td.description > p:first-child { 503 | margin-top: 0; 504 | padding-top: 0; 505 | } 506 | 507 | .params td.description > p:last-child, .props td.description > p:last-child { 508 | margin-bottom: 0; 509 | padding-bottom: 0; 510 | } 511 | 512 | span.param-type, .params td .param-type, .param-type dd { 513 | color: #606; 514 | font-family: Consolas, Monaco, 'Andale Mono', monospace 515 | } 516 | 517 | .param-type dt, .param-type dd { 518 | display: inline-block 519 | } 520 | 521 | .param-type { 522 | margin: 14px 0; 523 | } 524 | 525 | .disabled { 526 | color: #454545 527 | } 528 | 529 | /* navicon button */ 530 | .navicon-button { 531 | display: none; 532 | position: relative; 533 | padding: 2.0625rem 1.5rem; 534 | transition: 0.25s; 535 | cursor: pointer; 536 | -webkit-user-select: none; 537 | -moz-user-select: none; 538 | -ms-user-select: none; 539 | user-select: none; 540 | opacity: .8; 541 | } 542 | .navicon-button .navicon:before, .navicon-button .navicon:after { 543 | transition: 0.25s; 544 | } 545 | .navicon-button:hover { 546 | transition: 0.5s; 547 | opacity: 1; 548 | } 549 | .navicon-button:hover .navicon:before, .navicon-button:hover .navicon:after { 550 | transition: 0.25s; 551 | } 552 | .navicon-button:hover .navicon:before { 553 | top: .825rem; 554 | } 555 | .navicon-button:hover .navicon:after { 556 | top: -.825rem; 557 | } 558 | 559 | /* navicon */ 560 | .navicon { 561 | position: relative; 562 | width: 2.5em; 563 | height: .3125rem; 564 | background: #000; 565 | transition: 0.3s; 566 | border-radius: 2.5rem; 567 | } 568 | .navicon:before, .navicon:after { 569 | display: block; 570 | content: ""; 571 | height: .3125rem; 572 | width: 2.5rem; 573 | background: #000; 574 | position: absolute; 575 | z-index: -1; 576 | transition: 0.3s 0.25s; 577 | border-radius: 1rem; 578 | } 579 | .navicon:before { 580 | top: .625rem; 581 | } 582 | .navicon:after { 583 | top: -.625rem; 584 | } 585 | 586 | /* open */ 587 | .nav-trigger:checked + label:not(.steps) .navicon:before, 588 | .nav-trigger:checked + label:not(.steps) .navicon:after { 589 | top: 0 !important; 590 | } 591 | 592 | .nav-trigger:checked + label .navicon:before, 593 | .nav-trigger:checked + label .navicon:after { 594 | transition: 0.5s; 595 | } 596 | 597 | /* Minus */ 598 | .nav-trigger:checked + label { 599 | -webkit-transform: scale(0.75); 600 | transform: scale(0.75); 601 | } 602 | 603 | /* × and + */ 604 | .nav-trigger:checked + label.plus .navicon, 605 | .nav-trigger:checked + label.x .navicon { 606 | background: transparent; 607 | } 608 | 609 | .nav-trigger:checked + label.plus .navicon:before, 610 | .nav-trigger:checked + label.x .navicon:before { 611 | -webkit-transform: rotate(-45deg); 612 | transform: rotate(-45deg); 613 | background: #FFF; 614 | } 615 | 616 | .nav-trigger:checked + label.plus .navicon:after, 617 | .nav-trigger:checked + label.x .navicon:after { 618 | -webkit-transform: rotate(45deg); 619 | transform: rotate(45deg); 620 | background: #FFF; 621 | } 622 | 623 | .nav-trigger:checked + label.plus { 624 | -webkit-transform: scale(0.75) rotate(45deg); 625 | transform: scale(0.75) rotate(45deg); 626 | } 627 | 628 | .nav-trigger:checked ~ nav { 629 | left: 0 !important; 630 | } 631 | 632 | .nav-trigger:checked ~ .overlay { 633 | display: block; 634 | } 635 | 636 | .nav-trigger { 637 | position: fixed; 638 | top: 0; 639 | clip: rect(0, 0, 0, 0); 640 | } 641 | 642 | .overlay { 643 | display: none; 644 | position: fixed; 645 | top: 0; 646 | bottom: 0; 647 | left: 0; 648 | right: 0; 649 | width: 100%; 650 | height: 100%; 651 | background: hsla(0, 0%, 0%, 0.5); 652 | z-index: 1; 653 | } 654 | 655 | /* nav level */ 656 | .level-hide { 657 | display: none; 658 | } 659 | html[data-search-mode] .level-hide { 660 | display: block; 661 | } 662 | 663 | 664 | @media only screen and (max-width: 680px) { 665 | body { 666 | overflow-x: hidden; 667 | } 668 | 669 | nav { 670 | background: #FFF; 671 | width: 250px; 672 | height: 100%; 673 | position: fixed; 674 | top: 0; 675 | right: 0; 676 | bottom: 0; 677 | left: -250px; 678 | z-index: 3; 679 | padding: 0 10px; 680 | transition: left 0.2s; 681 | } 682 | 683 | .navicon-button { 684 | display: inline-block; 685 | position: fixed; 686 | top: 1.5em; 687 | right: 0; 688 | z-index: 2; 689 | } 690 | 691 | #main { 692 | width: 100%; 693 | } 694 | 695 | #main h1.page-title { 696 | margin: 1em 0; 697 | } 698 | 699 | #main section { 700 | padding: 0; 701 | } 702 | 703 | footer { 704 | margin-left: 0; 705 | } 706 | } 707 | 708 | /** Add a '#' to static members */ 709 | [data-type="member"] a::before { 710 | content: '#'; 711 | display: inline-block; 712 | margin-left: -14px; 713 | margin-right: 5px; 714 | } 715 | 716 | #disqus_thread{ 717 | margin-left: 30px; 718 | } 719 | 720 | @font-face { 721 | font-family: 'Montserrat'; 722 | font-style: normal; 723 | font-weight: 400; 724 | src: url('../fonts/Montserrat/Montserrat-Regular.eot'); /* IE9 Compat Modes */ 725 | src: url('../fonts/Montserrat/Montserrat-Regular.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 726 | url('../fonts/Montserrat/Montserrat-Regular.woff2') format('woff2'), /* Super Modern Browsers */ 727 | url('../fonts/Montserrat/Montserrat-Regular.woff') format('woff'), /* Pretty Modern Browsers */ 728 | url('../fonts/Montserrat/Montserrat-Regular.ttf') format('truetype'); /* Safari, Android, iOS */ 729 | } 730 | 731 | @font-face { 732 | font-family: 'Montserrat'; 733 | font-style: normal; 734 | font-weight: 700; 735 | src: url('../fonts/Montserrat/Montserrat-Bold.eot'); /* IE9 Compat Modes */ 736 | src: url('../fonts/Montserrat/Montserrat-Bold.eot?#iefix') format('embedded-opentype'), /* IE6-IE8 */ 737 | url('../fonts/Montserrat/Montserrat-Bold.woff2') format('woff2'), /* Super Modern Browsers */ 738 | url('../fonts/Montserrat/Montserrat-Bold.woff') format('woff'), /* Pretty Modern Browsers */ 739 | url('../fonts/Montserrat/Montserrat-Bold.ttf') format('truetype'); /* Safari, Android, iOS */ 740 | } 741 | 742 | @font-face { 743 | font-family: 'Source Sans Pro'; 744 | src: url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot'); 745 | src: url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.eot?#iefix') format('embedded-opentype'), 746 | url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff2') format('woff2'), 747 | url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.woff') format('woff'), 748 | url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.ttf') format('truetype'), 749 | url('../fonts/Source-Sans-Pro/sourcesanspro-regular-webfont.svg#source_sans_proregular') format('svg'); 750 | font-weight: 400; 751 | font-style: normal; 752 | } 753 | 754 | @font-face { 755 | font-family: 'Source Sans Pro'; 756 | src: url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot'); 757 | src: url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.eot?#iefix') format('embedded-opentype'), 758 | url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff2') format('woff2'), 759 | url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.woff') format('woff'), 760 | url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.ttf') format('truetype'), 761 | url('../fonts/Source-Sans-Pro/sourcesanspro-light-webfont.svg#source_sans_prolight') format('svg'); 762 | font-weight: 300; 763 | font-style: normal; 764 | 765 | } -------------------------------------------------------------------------------- /docs/styles/prettify.css: -------------------------------------------------------------------------------- 1 | .pln { 2 | color: #ddd; 3 | } 4 | 5 | /* string content */ 6 | .str { 7 | color: #61ce3c; 8 | } 9 | 10 | /* a keyword */ 11 | .kwd { 12 | color: #fbde2d; 13 | } 14 | 15 | /* a comment */ 16 | .com { 17 | color: #aeaeae; 18 | } 19 | 20 | /* a type name */ 21 | .typ { 22 | color: #8da6ce; 23 | } 24 | 25 | /* a literal value */ 26 | .lit { 27 | color: #fbde2d; 28 | } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #ddd; 33 | } 34 | 35 | /* lisp open bracket */ 36 | .opn { 37 | color: #000000; 38 | } 39 | 40 | /* lisp close bracket */ 41 | .clo { 42 | color: #000000; 43 | } 44 | 45 | /* a markup tag name */ 46 | .tag { 47 | color: #8da6ce; 48 | } 49 | 50 | /* a markup attribute name */ 51 | .atn { 52 | color: #fbde2d; 53 | } 54 | 55 | /* a markup attribute value */ 56 | .atv { 57 | color: #ddd; 58 | } 59 | 60 | /* a declaration */ 61 | .dec { 62 | color: #EF5050; 63 | } 64 | 65 | /* a variable name */ 66 | .var { 67 | color: #c82829; 68 | } 69 | 70 | /* a function name */ 71 | .fun { 72 | color: #4271ae; 73 | } 74 | 75 | /* Specify class=linenums on a pre to get line numbering */ 76 | ol.linenums { 77 | margin-top: 0; 78 | margin-bottom: 0; 79 | } 80 | -------------------------------------------------------------------------------- /jsdoc.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "allowUnknownTags": true 4 | }, 5 | "source": { 6 | "include": ["renderer"] 7 | }, 8 | "plugins": [ 9 | "plugins/markdown", 10 | "./node_modules/@pixi/jsdoc-template/plugins/es6-fix" 11 | ], 12 | "opts": { 13 | "destination": "./docs/", 14 | "encoding": "utf8", 15 | "template": "./node_modules/docdash" 16 | }, 17 | "templates": { 18 | "cleverLinks": false, 19 | "monospaceLinks": false, 20 | "default": { 21 | "outputSourceFiles": true 22 | } 23 | }, 24 | "docdash": { 25 | "sort": false, 26 | "search": true 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-puppeteer", 3 | "description": "electron webview driver like puppeteer", 4 | "author": { 5 | "name": "replace5", 6 | "email": "feng524822@sina.com" 7 | }, 8 | "version": "1.1.1", 9 | "main": "renderer_lib/index.js", 10 | "module": "renderer/index.js", 11 | "dependencies": { 12 | "@pixi/jsdoc-template": "^2.6.0", 13 | "@replace5/electron-localshortcut": "^1.0.2", 14 | "draggabilly": "^2.2.0", 15 | "electron-context-menu": "^2.0.0", 16 | "jsdoc-template": "^1.2.0" 17 | }, 18 | "keywords": [ 19 | "electron", 20 | "puppeteer", 21 | "webview" 22 | ], 23 | "repository": { 24 | "type": "git", 25 | "url": "git+https://github.com/replac5/electron-puppeteer" 26 | }, 27 | "scripts": { 28 | "docs": "node_modules/.bin/jsdoc -c jsdoc.json --verbose", 29 | "babel": "npx babel renderer --out-dir renderer_lib" 30 | }, 31 | "devDependencies": { 32 | "babel-cli": "^6.26.0", 33 | "babel-preset-electron": "^1.4.15", 34 | "docdash": "^1.2.0", 35 | "jsdoc": "^3.6.4" 36 | }, 37 | "license": "ISC" 38 | } 39 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # electron-puppeteer 2 | 3 | > electron webview 的驱动,Api 和 puppeteer 高度相似 4 | 5 | ## 效果预览 6 | 7 | ![预览图](https://s1.ax1x.com/2020/05/11/YG5p7T.gif) 8 | 9 | ## 基础使用 10 | 11 | > **注意** webview.preload.js 必须是`file`或`asar`协议 12 | 13 | ```javascript 14 | // set the script type="module" 15 | const path = require("path") 16 | const electronPuppeteer = require("electron-puppeteer").default 17 | 18 | async function run() { 19 | // open a browser 20 | const browser = await electronPuppeteer.launch({ 21 | // container must has height, example: `
` 22 | container: document.getElementById("container"), 23 | startUrl: "https://segmentfault.com/", 24 | createPage: true, 25 | partition: "persist:test", 26 | // webview.preload.js的路径 27 | preload: 28 | "file://" + 29 | path.join( 30 | __dirname, 31 | "node_modules/electron-puppeteer/preload/webview.preload.js", 32 | ), 33 | }) 34 | 35 | const pages = await browser.pages() 36 | const page = pages[0] 37 | // input search text 38 | let $search = await page.$("#searchBox") 39 | let word = "electron" 40 | for (let i = 0; i < word.length; i++) { 41 | let c = word.charAt(i) 42 | await $search.press(c) 43 | } 44 | // click search button 45 | await page.click(".header-search button") 46 | 47 | const page2 = await browser.newPage() 48 | await page2.goto("https://segmentfault.com/blogs") 49 | } 50 | 51 | run() 52 | ``` 53 | 54 | ### 切换多个 browser 55 | 56 | ```javascript 57 | const path = require("path") 58 | import electronPuppeteer from "./node_modules/electron-puppeteer/renderer/index.js" 59 | 60 | const browserMap = new Map() 61 | function showBrowser(name) { 62 | const browser = browserMap.get(name) 63 | if (!browser) { 64 | browser = await electronPuppeteer.launch({ 65 | container: document.getElementById('container'), 66 | preload: "file://" + 67 | path.join( 68 | __dirname, 69 | "node_modules/electron-puppeteer/preload/webview.preload.js", 70 | ), 71 | }) 72 | // do something 73 | } 74 | browser.bringToFront() 75 | } 76 | ``` 77 | 78 | ### [Api](./doc/index.html) 79 | 80 | ## Demo 81 | 82 | > node 版本建议在 v11+ 83 | 84 | 1. 到 demo 目录运行`npm install` 85 | 2. 运行 npm start 86 | 87 | ## 注意事项 88 | 89 | > 所属 BrowserWindow 的 webPreferences.nodeIntegration 需要为 true,否则无法获取和操作 webivew 下的 iframe 90 | > 所属 BrowserWindow 的 webPreferences.devtools 不能为 false,否则 launch 时传入 devtools 将无效 91 | > launch 时传入的 webPreferences 属性都必须在 BrowserWindow 内配置,否则无法单独生效 92 | -------------------------------------------------------------------------------- /renderer/BrowserManager.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file BrowserManager类 3 | */ 4 | import Browser from "./Browser.js" 5 | import KeyboardShortcuts from "./KeyboardShortcuts.js" 6 | 7 | /** 8 | * @class BrowserManager 9 | */ 10 | export default class BrowserManager { 11 | /** 12 | * @constructor BrowserManager 13 | * 14 | */ 15 | constructor() { 16 | this._browsers = new Map() 17 | // 快捷键 18 | new KeyboardShortcuts(this) 19 | } 20 | /** 21 | * 打开浏览器 22 | * @param {*} options 见Browser的options配置 23 | */ 24 | launch(options) { 25 | const browser = new Browser(this, options) 26 | return browser 27 | .init() 28 | .then(() => (this._browsers.set(browser.id, browser), browser)) 29 | } 30 | get size() { 31 | return this._browsers.size 32 | } 33 | /** 34 | * 获取最早打开的browser实例 35 | */ 36 | getEarliest() { 37 | let browsers = [] 38 | this._browsers.forEach((item) => { 39 | browsers.push(item) 40 | }) 41 | 42 | return browsers.sort((a, b) => a.startTime - b.startTime).shift() 43 | } 44 | /** 45 | * 通过browserId获取browser实例 46 | * @param {string} browserId browser.id 47 | */ 48 | get(browserId) { 49 | return this._browsers.get(browserId) 50 | } 51 | /** 52 | * 获取当前最视窗最前端的browser实例,也就是激活的browser实例 53 | */ 54 | frontBrowser() { 55 | let front = null 56 | this._browsers.forEach((item) => { 57 | if (item.isFront === true) { 58 | front = item 59 | } 60 | }) 61 | 62 | return front 63 | } 64 | frontPage() { 65 | let browser = this.frontBrowser() 66 | return browser && browser.frontPage() 67 | } 68 | /** 69 | * 删除browser,不可直接调用 70 | * 如需要关闭browser,请调用browser.close() 71 | * @private 72 | * @param {string} browserId 73 | */ 74 | _removeBrowser(browserId) { 75 | this._browsers.delete(browserId) 76 | } 77 | /** 78 | * 激活browser,不可直接调用 79 | * 如需要激活页面,请调用browser.bringToFront() 80 | * @private 81 | * @param {string} pageId 82 | */ 83 | _bringBrowserToFront(browserId) { 84 | this._browsers.forEach((browser) => { 85 | if (browserId === browser.id) { 86 | browser._doFront() 87 | } else { 88 | browser._doBack() 89 | } 90 | }) 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /renderer/ElementHandle.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ElementHandle类 3 | */ 4 | 5 | import EventEmitter from "./EventEmitter.js" 6 | import USKeyboardLayout from "./USKeyboardLayout.js" 7 | import {uniqueId, sleep} from "./util.js" 8 | 9 | /** 10 | * @class ElementHandle frame的dom操作句柄 11 | * @extends EventEmitter 12 | * 13 | * @property {Frame} frame 所属frame实例 14 | * @property {string} selector 选择器 15 | * @property {string} baseSelector 基础选择器,先查找baseSelector,再在baseSelector的基础上查找当前节点 16 | * 17 | */ 18 | export default class ElementHandle extends EventEmitter { 19 | /** 20 | * @constructor ElementHandle 21 | * 22 | * @param {Frame} frame 所属frame实例 23 | * @param {string} selector 选择器 24 | * @param {string} baseSelector 基础选择器,先查找baseSelector,再在baseSelector的基础上查找当前节点 25 | * 26 | */ 27 | constructor(frame, selector, baseSelector) { 28 | super() 29 | 30 | this.id = uniqueId("ElementHandle_") 31 | this.ipc = frame.ipc 32 | this.frame = frame 33 | this.selector = selector 34 | this.baseSelector = baseSelector 35 | } 36 | _joinSelfSelector() { 37 | var baseSelector = this.baseSelector.slice(0) 38 | baseSelector.push(this.selector) 39 | return baseSelector 40 | } 41 | _joinSelector(selector, baseSelector) { 42 | baseSelector = baseSelector.slice(0) 43 | baseSelector.push(selector) 44 | return baseSelector 45 | } 46 | /** 47 | * 基于当前节点,查询新的单个节点, 对应elm.querySelector(selector) 48 | * @param {string} selector 49 | * 50 | * @return {ElementHandle} 51 | */ 52 | $(selector) { 53 | return new ElementHandle(this.frame, selector, this._joinSelfSelector()) 54 | } 55 | /** 56 | * 基于当前节点,查询新的节点集合, 对应elm.querySelectorAll(selector) 57 | * @param {string} selector 58 | * 59 | * @return {Promise} 60 | */ 61 | $$(selector) { 62 | return this.ipc 63 | .send("elementHandle.$$", { 64 | selector: selector, 65 | baseSelector: this._joinSelfSelector(), 66 | }) 67 | .then((length) => { 68 | return new Array(length).map((v, i) => { 69 | return new ElementHandle( 70 | this.contentFrame(), 71 | [selector, i], 72 | this._joinSelfSelector() 73 | ) 74 | }) 75 | }) 76 | } 77 | /** 78 | * 查找节点,并将查找的节点作为参数传为pageFunction 79 | * @param {string} selector 80 | * @param {Function} pageFunction 81 | * 82 | * @return {Promise<*>} 83 | */ 84 | $eval(selector, pageFunction) { 85 | var args = [].slice.call(arguments, 1) 86 | 87 | return this.ipc.send("elementHandle.$eval", { 88 | selector: selector, 89 | baseSelector: this._joinSelfSelector(), 90 | pageFunction: pageFunction.toString(), 91 | args: args, 92 | }) 93 | } 94 | /** 95 | * 查找节点集合,并将查找的节点集合作为参数传为pageFunction 96 | * @param {string} selector 97 | * @param {Function} pageFunction 98 | * 99 | * @return {Promise<*>} 100 | */ 101 | $$eval(selector, pageFunction) { 102 | var args = [].slice.call(arguments, 1) 103 | 104 | return this.ipc.send("elementHandle.$$eval", { 105 | selector: selector, 106 | baseSelector: this._joinSelfSelector(), 107 | pageFunction: pageFunction.toString(), 108 | args: args, 109 | }) 110 | } 111 | /** 112 | * todo 113 | */ 114 | $x(/* expression */) { 115 | return Promise.reject("todo") 116 | } 117 | /** 118 | * todo 119 | */ 120 | asElement() { 121 | return Promise.reject("todo") 122 | } 123 | /** 124 | * boundingBox 125 | * 126 | * @return {Promise} 127 | */ 128 | boundingBox() { 129 | return this.ipc.send("elementHandle.boundingBox", { 130 | selector: this._joinSelfSelector(), 131 | // baseSelector: this.baseSelector, 132 | }) 133 | } 134 | /** 135 | * offsetTop 136 | * 137 | * @return {Promise} 138 | */ 139 | get offsetTop() { 140 | return this.ipc.send("elementHandle.offsetTop", { 141 | selector: this._joinSelfSelector(), 142 | }) 143 | } 144 | /** 145 | * offsetTop 146 | * 147 | * @return {Promise} 148 | */ 149 | get offsetLeft() { 150 | return this.ipc.send("elementHandle.offsetLeft", { 151 | selector: this._joinSelfSelector(), 152 | }) 153 | } 154 | /** 155 | * 获取/设置节点文本 156 | * 157 | * @return {Promise} 158 | */ 159 | textContent(text) { 160 | return this.ipc.send("elementHandle.textContent", { 161 | selector: this._joinSelfSelector(), 162 | text: text, 163 | }) 164 | } 165 | /** 166 | * todo 167 | */ 168 | boxModel() { 169 | return Promise.reject("todo") 170 | } 171 | /** 172 | * 点击当前节点 173 | * @param {*} options 暂不支持 174 | * 175 | * @return {Promise} 176 | */ 177 | click(options) { 178 | return this.ipc.send("elementHandle.click", { 179 | selector: this._joinSelfSelector(), 180 | options, 181 | }) 182 | } 183 | /** 184 | * 显示当前节点 185 | * @param {object} options 配置选项 186 | * @param {string} [options.display] 要设置的display值,默认为block 187 | */ 188 | show(options) { 189 | return this.ipc.send("elementHandle.show", { 190 | selector: this._joinSelfSelector(), 191 | options: options, 192 | }) 193 | } 194 | /** 195 | * 隐藏当前节点 196 | */ 197 | hide() { 198 | return this.ipc.send("elementHandle.hide", { 199 | selector: this._joinSelfSelector(), 200 | }) 201 | } 202 | /** 203 | * 当前节点所属frame 204 | * 205 | * @return {Frame} 206 | */ 207 | contentFrame() { 208 | return this.frame 209 | } 210 | /** 211 | * todo 212 | */ 213 | dispose() { 214 | return Promise.reject("todo") 215 | } 216 | /** 217 | * todo 218 | */ 219 | executionContext() { 220 | return Promise.reject("todo") 221 | } 222 | /** 223 | * 设置checked属性为true,并触发change事件 224 | * 225 | * @return {Promise} 226 | */ 227 | check() { 228 | return this.ipc.send("elementHandle.check", { 229 | selector: this._joinSelfSelector(), 230 | }) 231 | } 232 | /** 233 | * 设置checked属性为false,并触发change事件 234 | * 235 | * @return {Promise} 236 | */ 237 | uncheck() { 238 | return this.ipc.send("elementHandle.uncheck", { 239 | selector: this._joinSelfSelector(), 240 | }) 241 | } 242 | /** 243 | * todo 244 | */ 245 | getProperties() { 246 | return Promise.reject("todo") 247 | } 248 | /** 249 | * todo 250 | */ 251 | getProperty(/* propertyName */) { 252 | return Promise.reject("todo") 253 | } 254 | /** 255 | * focus当前节点 256 | * 257 | * @return {Promise} 258 | */ 259 | focus() { 260 | // console.log('focus: ', this._joinSelfSelector()); 261 | return this.ipc.send("elementHandle.focus", { 262 | selector: this._joinSelfSelector(), 263 | }) 264 | } 265 | /** 266 | * 取消聚焦当前节点 267 | * 268 | * @return {Promise} 269 | */ 270 | // @noitce puppeteer不支持blur 271 | blur() { 272 | return this.ipc.send("elementHandle.blur", { 273 | selector: this._joinSelfSelector(), 274 | }) 275 | } 276 | /** 277 | * 获取节点的属性集合 278 | * 279 | * @return {Promise>} 280 | */ 281 | getAttributes() { 282 | return this.ipc 283 | .send("elementHandle.getAttributes", { 284 | selector: this._joinSelfSelector(), 285 | }) 286 | .then(function (attributes) { 287 | var map = new Map() 288 | for (var attr of attributes) { 289 | if (attributes.hasOwnProperty(attr)) { 290 | map.set(attr, { 291 | // @notice: 先简单实现 292 | jsonValue: (function (value) { 293 | return value 294 | })(attributes[attr]), 295 | }) 296 | } 297 | } 298 | return map 299 | }) 300 | } 301 | /** 302 | * 获取节点的指定属性值 303 | * 304 | * @return {Promise} 通过jsonValue()获取属性值 305 | */ 306 | getAttribute(attrName) { 307 | return this.ipc 308 | .send("elementHandle.getAttribute", { 309 | selector: this._joinSelfSelector(), 310 | attrName: attrName, 311 | }) 312 | .then(function (value) { 313 | // @notice: 先简单实现 314 | return { 315 | jsonValue: function () { 316 | return value 317 | }, 318 | } 319 | }) 320 | } 321 | /** 322 | * hover当前节点 323 | * 324 | * @return {Promise} 325 | */ 326 | hover() { 327 | return this.ipc.send("elementHandle.hover", { 328 | selector: this._joinSelfSelector(), 329 | }) 330 | } 331 | /** 332 | * todo 333 | */ 334 | isIntersectingViewport() { 335 | return Promise.reject("todo") 336 | } 337 | /** 338 | * todo 339 | */ 340 | jsonValue() { 341 | return Promise.reject("todo") 342 | } 343 | /** 344 | * 键入文本 345 | * @param {string} text 输入的文本内容 346 | * @param {*} options 暂不支持 347 | */ 348 | // todo: 暂不支持options 349 | press(text, options) { 350 | var key = USKeyboardLayout[text] 351 | 352 | return this.ipc.send("elementHandle.press", { 353 | selector: this._joinSelfSelector(), 354 | keyCode: (key && key.keyCode) || 0, 355 | text: text, 356 | options: options, 357 | }) 358 | } 359 | /** 360 | * todo 361 | */ 362 | screenshot(/* options */) { 363 | return Promise.reject("todo") 364 | } 365 | /** 366 | * todo 367 | */ 368 | tap() { 369 | return Promise.reject("todo") 370 | } 371 | /** 372 | * todo 373 | */ 374 | toString() { 375 | return Promise.reject("todo") 376 | } 377 | /** 378 | * 输入文字 379 | * @param {string} text 输入的文本内容 380 | * @param {Object} options 选项 381 | * @param {number} [options.delay] 输入间隔 382 | */ 383 | async type(text, options) { 384 | for (let i = 0; i < text.length; i++) { 385 | await this.press(text.charAt(i)) 386 | await sleep(options && options.delay) 387 | } 388 | return true 389 | } 390 | /** 391 | * todo 392 | */ 393 | uploadFile(/* ...filePaths */) { 394 | return Promise.reject("todo") 395 | } 396 | } 397 | -------------------------------------------------------------------------------- /renderer/EventEmitter.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * EventEmitter v5.2.6 - git.io/ee 3 | * Unlicense - http://unlicense.org/ 4 | * Oliver Caldwell - https://oli.me.uk/ 5 | * @preserve 6 | */ 7 | 8 | /** 9 | * Class for managing events. 10 | * Can be extended to provide event functionality in other classes. 11 | * 12 | * @class EventEmitter Manages event registering and emitting. 13 | */ 14 | function EventEmitter() {} 15 | 16 | // Shortcuts to improve speed and size 17 | var proto = EventEmitter.prototype 18 | 19 | /** 20 | * Finds the index of the listener for the event in its storage array. 21 | * 22 | * @ignore 23 | * @param {Function[]} listeners Array of listeners to search through. 24 | * @param {Function} listener Method to look for. 25 | * @return {Number} Index of the specified listener, -1 if not found 26 | * @api private 27 | */ 28 | function indexOfListener(listeners, listener) { 29 | var i = listeners.length 30 | while (i--) { 31 | if (listeners[i].listener === listener) { 32 | return i 33 | } 34 | } 35 | 36 | return -1 37 | } 38 | 39 | /** 40 | * Alias a method while keeping the context correct, to allow for overwriting of target method. 41 | * 42 | * @ignore 43 | * @param {String} name The name of the target method. 44 | * @return {Function} The aliased method 45 | * @api private 46 | */ 47 | function alias(name) { 48 | return function aliasClosure() { 49 | return this[name].apply(this, arguments) 50 | } 51 | } 52 | 53 | /** 54 | * Returns the listener array for the specified event. 55 | * Will initialise the event object and listener arrays if required. 56 | * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them. 57 | * Each property in the object response is an array of listener functions. 58 | * 59 | * @param {String|RegExp} evt Name of the event to return the listeners from. 60 | * @return {Function[]|Object} All listener functions for the event. 61 | */ 62 | proto.getListeners = function getListeners(evt) { 63 | var events = this._getEvents() 64 | var response 65 | var key 66 | 67 | // Return a concatenated array of all matching events if 68 | // the selector is a regular expression. 69 | if (evt instanceof RegExp) { 70 | response = {} 71 | for (key in events) { 72 | if (events.hasOwnProperty(key) && evt.test(key)) { 73 | response[key] = events[key] 74 | } 75 | } 76 | } else { 77 | response = events[evt] || (events[evt] = []) 78 | } 79 | 80 | return response 81 | } 82 | 83 | /** 84 | * Takes a list of listener objects and flattens it into a list of listener functions. 85 | * 86 | * @param {Object[]} listeners Raw listener objects. 87 | * @return {Function[]} Just the listener functions. 88 | */ 89 | proto.flattenListeners = function flattenListeners(listeners) { 90 | var flatListeners = [] 91 | var i 92 | 93 | for (i = 0; i < listeners.length; i += 1) { 94 | flatListeners.push(listeners[i].listener) 95 | } 96 | 97 | return flatListeners 98 | } 99 | 100 | /** 101 | * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful. 102 | * 103 | * @param {String|RegExp} evt Name of the event to return the listeners from. 104 | * @return {Object} All listener functions for an event in an object. 105 | */ 106 | proto.getListenersAsObject = function getListenersAsObject(evt) { 107 | var listeners = this.getListeners(evt) 108 | var response 109 | 110 | if (listeners instanceof Array) { 111 | response = {} 112 | response[evt] = listeners 113 | } 114 | 115 | return response || listeners 116 | } 117 | 118 | function isValidListener(listener) { 119 | if (typeof listener === "function" || listener instanceof RegExp) { 120 | return true 121 | } else if (listener && typeof listener === "object") { 122 | return isValidListener(listener.listener) 123 | } else { 124 | return false 125 | } 126 | } 127 | 128 | /** 129 | * Adds a listener function to the specified event. 130 | * The listener will not be added if it is a duplicate. 131 | * If the listener returns true then it will be removed after it is called. 132 | * If you pass a regular expression as the event name then the listener will be added to all events that match it. 133 | * 134 | * @param {String|RegExp} evt Name of the event to attach the listener to. 135 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 136 | * @return {Object} Current instance of EventEmitter for chaining. 137 | */ 138 | proto.addListener = function addListener(evt, listener) { 139 | if (!isValidListener(listener)) { 140 | throw new TypeError("listener must be a function") 141 | } 142 | 143 | var listeners = this.getListenersAsObject(evt) 144 | var listenerIsWrapped = typeof listener === "object" 145 | var key 146 | 147 | for (key in listeners) { 148 | if ( 149 | listeners.hasOwnProperty(key) && 150 | indexOfListener(listeners[key], listener) === -1 151 | ) { 152 | listeners[key].push( 153 | listenerIsWrapped 154 | ? listener 155 | : { 156 | listener: listener, 157 | once: false, 158 | } 159 | ) 160 | } 161 | } 162 | 163 | return this 164 | } 165 | 166 | /** 167 | * Alias of addListener 168 | */ 169 | proto.on = alias("addListener") 170 | 171 | /** 172 | * Semi-alias of addListener. It will add a listener that will be 173 | * automatically removed after its first execution. 174 | * 175 | * @param {String|RegExp} evt Name of the event to attach the listener to. 176 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 177 | * @return {Object} Current instance of EventEmitter for chaining. 178 | */ 179 | proto.addOnceListener = function addOnceListener(evt, listener) { 180 | return this.addListener(evt, { 181 | listener: listener, 182 | once: true, 183 | }) 184 | } 185 | 186 | /** 187 | * Alias of addOnceListener. 188 | */ 189 | proto.once = alias("addOnceListener") 190 | 191 | /** 192 | * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad. 193 | * You need to tell it what event names should be matched by a regex. 194 | * 195 | * @param {String} evt Name of the event to create. 196 | * @return {Object} Current instance of EventEmitter for chaining. 197 | */ 198 | proto.defineEvent = function defineEvent(evt) { 199 | this.getListeners(evt) 200 | return this 201 | } 202 | 203 | /** 204 | * Uses defineEvent to define multiple events. 205 | * 206 | * @param {String[]} evts An array of event names to define. 207 | * @return {Object} Current instance of EventEmitter for chaining. 208 | */ 209 | proto.defineEvents = function defineEvents(evts) { 210 | for (var i = 0; i < evts.length; i += 1) { 211 | this.defineEvent(evts[i]) 212 | } 213 | return this 214 | } 215 | 216 | /** 217 | * Removes a listener function from the specified event. 218 | * When passed a regular expression as the event name, it will remove the listener from all events that match it. 219 | * 220 | * @param {String|RegExp} evt Name of the event to remove the listener from. 221 | * @param {Function} listener Method to remove from the event. 222 | * @return {Object} Current instance of EventEmitter for chaining. 223 | */ 224 | proto.removeListener = function removeListener(evt, listener) { 225 | var listeners = this.getListenersAsObject(evt) 226 | var index 227 | var key 228 | 229 | for (key in listeners) { 230 | if (listeners.hasOwnProperty(key)) { 231 | index = indexOfListener(listeners[key], listener) 232 | 233 | if (index !== -1) { 234 | listeners[key].splice(index, 1) 235 | } 236 | } 237 | } 238 | 239 | return this 240 | } 241 | 242 | /** 243 | * Alias of removeListener 244 | */ 245 | proto.off = alias("removeListener") 246 | 247 | /** 248 | * Adds listeners in bulk using the manipulateListeners method. 249 | * If you pass an object as the first argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added. 250 | * You can also pass it a regular expression to add the array of listeners to all events that match it. 251 | * Yeah, this function does quite a bit. That's probably a bad thing. 252 | * 253 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once. 254 | * @param {Function[]} [listeners] An optional array of listener functions to add. 255 | * @return {Object} Current instance of EventEmitter for chaining. 256 | */ 257 | proto.addListeners = function addListeners(evt, listeners) { 258 | // Pass through to manipulateListeners 259 | return this.manipulateListeners(false, evt, listeners) 260 | } 261 | 262 | /** 263 | * Removes listeners in bulk using the manipulateListeners method. 264 | * If you pass an object as the first argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 265 | * You can also pass it an event name and an array of listeners to be removed. 266 | * You can also pass it a regular expression to remove the listeners from all events that match it. 267 | * 268 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once. 269 | * @param {Function[]} [listeners] An optional array of listener functions to remove. 270 | * @return {Object} Current instance of EventEmitter for chaining. 271 | */ 272 | proto.removeListeners = function removeListeners(evt, listeners) { 273 | // Pass through to manipulateListeners 274 | return this.manipulateListeners(true, evt, listeners) 275 | } 276 | 277 | /** 278 | * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level. 279 | * The first argument will determine if the listeners are removed (true) or added (false). 280 | * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 281 | * You can also pass it an event name and an array of listeners to be added/removed. 282 | * You can also pass it a regular expression to manipulate the listeners of all events that match it. 283 | * 284 | * @param {Boolean} remove True if you want to remove listeners, false if you want to add. 285 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once. 286 | * @param {Function[]} [listeners] An optional array of listener functions to add/remove. 287 | * @return {Object} Current instance of EventEmitter for chaining. 288 | */ 289 | proto.manipulateListeners = function manipulateListeners( 290 | remove, 291 | evt, 292 | listeners 293 | ) { 294 | var i 295 | var value 296 | var single = remove ? this.removeListener : this.addListener 297 | var multiple = remove ? this.removeListeners : this.addListeners 298 | 299 | // If evt is an object then pass each of its properties to this method 300 | if (typeof evt === "object" && !(evt instanceof RegExp)) { 301 | for (i in evt) { 302 | if (evt.hasOwnProperty(i) && (value = evt[i])) { 303 | // Pass the single listener straight through to the singular method 304 | if (typeof value === "function") { 305 | single.call(this, i, value) 306 | } else { 307 | // Otherwise pass back to the multiple function 308 | multiple.call(this, i, value) 309 | } 310 | } 311 | } 312 | } else { 313 | // So evt must be a string 314 | // And listeners must be an array of listeners 315 | // Loop over it and pass each one to the multiple method 316 | i = listeners.length 317 | while (i--) { 318 | single.call(this, evt, listeners[i]) 319 | } 320 | } 321 | 322 | return this 323 | } 324 | 325 | /** 326 | * Removes all listeners from a specified event. 327 | * If you do not specify an event then all listeners will be removed. 328 | * That means every event will be emptied. 329 | * You can also pass a regex to remove all events that match it. 330 | * 331 | * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed. 332 | * @return {Object} Current instance of EventEmitter for chaining. 333 | */ 334 | proto.removeEvent = function removeEvent(evt) { 335 | var type = typeof evt 336 | var events = this._getEvents() 337 | var key 338 | 339 | // Remove different things depending on the state of evt 340 | if (type === "string") { 341 | // Remove all listeners for the specified event 342 | delete events[evt] 343 | } else if (evt instanceof RegExp) { 344 | // Remove all events matching the regex. 345 | for (key in events) { 346 | if (events.hasOwnProperty(key) && evt.test(key)) { 347 | delete events[key] 348 | } 349 | } 350 | } else { 351 | // Remove all listeners in all events 352 | delete this._events 353 | } 354 | 355 | return this 356 | } 357 | 358 | /** 359 | * Alias of removeEvent. 360 | * 361 | * Added to mirror the node API. 362 | */ 363 | proto.removeAllListeners = alias("removeEvent") 364 | 365 | /** 366 | * Emits an event of your choice. 367 | * When emitted, every listener attached to that event will be executed. 368 | * If you pass the optional argument array then those arguments will be passed to every listener upon execution. 369 | * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately. 370 | * So they will not arrive within the array on the other side, they will be separate. 371 | * You can also pass a regular expression to emit to all events that match it. 372 | * 373 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 374 | * @param {Array} [args] Optional array of arguments to be passed to each listener. 375 | * @return {Object} Current instance of EventEmitter for chaining. 376 | */ 377 | proto.emitEvent = function emitEvent(evt, args) { 378 | var listenersMap = this.getListenersAsObject(evt) 379 | var listeners 380 | var listener 381 | var i 382 | var key 383 | var response 384 | 385 | for (key in listenersMap) { 386 | if (listenersMap.hasOwnProperty(key)) { 387 | listeners = listenersMap[key].slice(0) 388 | 389 | for (i = 0; i < listeners.length; i++) { 390 | // If the listener returns true then it shall be removed from the event 391 | // The function is executed either with a basic call or an apply if there is an args array 392 | listener = listeners[i] 393 | 394 | if (listener.once === true) { 395 | this.removeListener(evt, listener.listener) 396 | } 397 | 398 | response = listener.listener.apply(this, args || []) 399 | 400 | if (response === this._getOnceReturnValue()) { 401 | this.removeListener(evt, listener.listener) 402 | } 403 | } 404 | } 405 | } 406 | 407 | return this 408 | } 409 | 410 | /** 411 | * Alias of emitEvent 412 | */ 413 | proto.trigger = alias("emitEvent") 414 | 415 | /** 416 | * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on. 417 | * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it. 418 | * 419 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 420 | * @param {...*} Optional additional arguments to be passed to each listener. 421 | * @return {Object} Current instance of EventEmitter for chaining. 422 | */ 423 | proto.emit = function emit(evt) { 424 | var args = Array.prototype.slice.call(arguments, 1) 425 | return this.emitEvent(evt, args) 426 | } 427 | 428 | /** 429 | * Sets the current value to check against when executing listeners. If a 430 | * listeners return value matches the one set here then it will be removed 431 | * after execution. This value defaults to true. 432 | * 433 | * @param {*} value The new value to check for when executing listeners. 434 | * @return {Object} Current instance of EventEmitter for chaining. 435 | */ 436 | proto.setOnceReturnValue = function setOnceReturnValue(value) { 437 | this._onceReturnValue = value 438 | return this 439 | } 440 | 441 | /** 442 | * Fetches the current value to check against when executing listeners. If 443 | * the listeners return value matches this one then it should be removed 444 | * automatically. It will return true by default. 445 | * 446 | * @return {*|Boolean} The current value to check for or the default, true. 447 | * @api private 448 | */ 449 | proto._getOnceReturnValue = function _getOnceReturnValue() { 450 | if (this.hasOwnProperty("_onceReturnValue")) { 451 | return this._onceReturnValue 452 | } else { 453 | return true 454 | } 455 | } 456 | 457 | /** 458 | * Fetches the events object and creates one if required. 459 | * 460 | * @return {Object} The events storage object. 461 | * @api private 462 | */ 463 | proto._getEvents = function _getEvents() { 464 | return this._events || (this._events = {}) 465 | } 466 | 467 | /** 468 | * Reverts the global {@link EventEmitter} to its previous value and returns a reference to this version. 469 | * 470 | * @return {Function} Non conflicting EventEmitter class. 471 | */ 472 | EventEmitter.noConflict = function noConflict() { 473 | return EventEmitter 474 | } 475 | 476 | export default EventEmitter 477 | -------------------------------------------------------------------------------- /renderer/KeyboardShortcuts.js: -------------------------------------------------------------------------------- 1 | import {register} from "./libs/localshortcutswrap" 2 | 3 | const shortcuts = [ 4 | { 5 | action: "closePage", 6 | keys: "CommandOrControl+W", 7 | }, 8 | { 9 | action: "newPage", 10 | keys: "CommandOrControl+T", 11 | }, 12 | { 13 | action: "reload", 14 | keys: "CommandOrControl+R", 15 | }, 16 | { 17 | action: "toggleDevtools", 18 | keys: "F12", 19 | }, 20 | { 21 | action: "search", 22 | keys: "CommandOrControl+F", 23 | }, 24 | { 25 | action: "zoomOut", 26 | keys: "CommandOrControl+-", 27 | }, 28 | { 29 | action: "zoomIn", 30 | keys: ["CommandOrControl+=", "CommandOrControl+Plus"], 31 | }, 32 | { 33 | action: "zoomReset", 34 | keys: "CommandOrControl+0", 35 | }, 36 | ] 37 | 38 | export default class KeyboardShortcuts { 39 | constructor(browserManger) { 40 | this.init(browserManger) 41 | } 42 | init(browserManger) { 43 | this.browserManger = browserManger 44 | shortcuts.forEach((item) => { 45 | ;[].concat(item.keys).forEach((key) => { 46 | register(key, this[item.action], {ctx: this}) 47 | }) 48 | }) 49 | } 50 | _getFrontBrowser() { 51 | let browser = this.browserManger.frontBrowser() 52 | if (!browser || !browser.isVisible()) { 53 | return null 54 | } 55 | return browser 56 | } 57 | _getFrontPage() { 58 | let browser = this._getFrontBrowser() 59 | if (!browser) { 60 | return null 61 | } 62 | return browser.frontPage() 63 | } 64 | newPage() { 65 | let browser = this._getFrontBrowser() 66 | if (browser) { 67 | browser.newPage() 68 | } 69 | } 70 | closePage() { 71 | let frontPage = this._getFrontPage() 72 | if (frontPage) { 73 | frontPage.close() 74 | } 75 | } 76 | reload() { 77 | let frontPage = this._getFrontPage() 78 | if (frontPage && frontPage.isReady) { 79 | frontPage.reload() 80 | } 81 | } 82 | toggleDevtools() { 83 | let frontPage = this._getFrontPage() 84 | if (frontPage && frontPage.isReady) { 85 | if (frontPage.webview.isDevToolsOpened()) { 86 | frontPage.webview.closeDevTools() 87 | } else { 88 | frontPage.webview.openDevTools() 89 | } 90 | } 91 | } 92 | search() { 93 | let frontPage = this._getFrontPage() 94 | if (frontPage && frontPage.isReady) { 95 | frontPage.chromeSearch.show() 96 | } 97 | } 98 | zoomOut() { 99 | let frontPage = this._getFrontPage() 100 | if (frontPage && frontPage.isReady) { 101 | frontPage.chromeZoom.zoomOut() 102 | } 103 | } 104 | zoomIn() { 105 | let frontPage = this._getFrontPage() 106 | if (frontPage && frontPage.isReady) { 107 | frontPage.chromeZoom.zoomIn() 108 | } 109 | } 110 | zoomReset() { 111 | let frontPage = this._getFrontPage() 112 | if (frontPage && frontPage.isReady) { 113 | frontPage.chromeZoom.zoomReset() 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /renderer/Target.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file Target类 3 | */ 4 | 5 | /** 6 | * @class Target 7 | * 8 | */ 9 | export default class Target { 10 | /** 11 | * @constructor Target 12 | * 13 | * @param {*} page target对应page 14 | * @param {*} opener 当前target的opener 15 | */ 16 | constructor(page, opener) { 17 | this._page = page 18 | this._opener = opener 19 | page._setTarget(this) 20 | } 21 | /** 22 | * 当前target所属browser 23 | * @return {Browser} 24 | */ 25 | browser() { 26 | return this._page.browser() 27 | } 28 | /** 29 | * 当前target的opener 30 | * @return {Target} 31 | */ 32 | opener() { 33 | return this._opener 34 | } 35 | /** 36 | * @async 37 | * 获取当前target的page 38 | * @return {Page} 39 | */ 40 | async page() { 41 | return this._page 42 | } 43 | /** 44 | * 获取当前target的类型 45 | * @return {string} 46 | */ 47 | type() { 48 | return "page" 49 | } 50 | /** 51 | * 打开的url 52 | * @return {string} 53 | */ 54 | url() { 55 | return this._page.url() 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /renderer/USKeyboardLayout.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2017 Google Inc. All rights reserved. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the 'License'); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an 'AS IS' BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | /** 18 | * @typedef {Object} KeyDefinition 19 | * @property {number=} keyCode 20 | * @property {number=} shiftKeyCode 21 | * @property {string=} key 22 | * @property {string=} shiftKey 23 | * @property {string=} code 24 | * @property {string=} text 25 | * @property {string=} shiftText 26 | * @property {number=} location 27 | */ 28 | 29 | /** 30 | * @type {Object} 31 | */ 32 | export default { 33 | "0": {keyCode: 48, key: "0", code: "Digit0"}, 34 | "1": {keyCode: 49, key: "1", code: "Digit1"}, 35 | "2": {keyCode: 50, key: "2", code: "Digit2"}, 36 | "3": {keyCode: 51, key: "3", code: "Digit3"}, 37 | "4": {keyCode: 52, key: "4", code: "Digit4"}, 38 | "5": {keyCode: 53, key: "5", code: "Digit5"}, 39 | "6": {keyCode: 54, key: "6", code: "Digit6"}, 40 | "7": {keyCode: 55, key: "7", code: "Digit7"}, 41 | "8": {keyCode: 56, key: "8", code: "Digit8"}, 42 | "9": {keyCode: 57, key: "9", code: "Digit9"}, 43 | Power: {key: "Power", code: "Power"}, 44 | Eject: {key: "Eject", code: "Eject"}, 45 | Abort: {keyCode: 3, code: "Abort", key: "Cancel"}, 46 | Help: {keyCode: 6, code: "Help", key: "Help"}, 47 | Backspace: {keyCode: 8, code: "Backspace", key: "Backspace"}, 48 | Tab: {keyCode: 9, code: "Tab", key: "Tab"}, 49 | Numpad5: { 50 | keyCode: 12, 51 | shiftKeyCode: 101, 52 | key: "Clear", 53 | code: "Numpad5", 54 | shiftKey: "5", 55 | location: 3, 56 | }, 57 | NumpadEnter: { 58 | keyCode: 13, 59 | code: "NumpadEnter", 60 | key: "Enter", 61 | text: "\r", 62 | location: 3, 63 | }, 64 | Enter: {keyCode: 13, code: "Enter", key: "Enter", text: "\r"}, 65 | "\r": {keyCode: 13, code: "Enter", key: "Enter", text: "\r"}, 66 | "\n": {keyCode: 13, code: "Enter", key: "Enter", text: "\r"}, 67 | ShiftLeft: {keyCode: 16, code: "ShiftLeft", key: "Shift", location: 1}, 68 | ShiftRight: {keyCode: 16, code: "ShiftRight", key: "Shift", location: 2}, 69 | ControlLeft: {keyCode: 17, code: "ControlLeft", key: "Control", location: 1}, 70 | ControlRight: { 71 | keyCode: 17, 72 | code: "ControlRight", 73 | key: "Control", 74 | location: 2, 75 | }, 76 | AltLeft: {keyCode: 18, code: "AltLeft", key: "Alt", location: 1}, 77 | AltRight: {keyCode: 18, code: "AltRight", key: "Alt", location: 2}, 78 | Pause: {keyCode: 19, code: "Pause", key: "Pause"}, 79 | CapsLock: {keyCode: 20, code: "CapsLock", key: "CapsLock"}, 80 | Escape: {keyCode: 27, code: "Escape", key: "Escape"}, 81 | Convert: {keyCode: 28, code: "Convert", key: "Convert"}, 82 | NonConvert: {keyCode: 29, code: "NonConvert", key: "NonConvert"}, 83 | Space: {keyCode: 32, code: "Space", key: " "}, 84 | Numpad9: { 85 | keyCode: 33, 86 | shiftKeyCode: 105, 87 | key: "PageUp", 88 | code: "Numpad9", 89 | shiftKey: "9", 90 | location: 3, 91 | }, 92 | PageUp: {keyCode: 33, code: "PageUp", key: "PageUp"}, 93 | Numpad3: { 94 | keyCode: 34, 95 | shiftKeyCode: 99, 96 | key: "PageDown", 97 | code: "Numpad3", 98 | shiftKey: "3", 99 | location: 3, 100 | }, 101 | PageDown: {keyCode: 34, code: "PageDown", key: "PageDown"}, 102 | End: {keyCode: 35, code: "End", key: "End"}, 103 | Numpad1: { 104 | keyCode: 35, 105 | shiftKeyCode: 97, 106 | key: "End", 107 | code: "Numpad1", 108 | shiftKey: "1", 109 | location: 3, 110 | }, 111 | Home: {keyCode: 36, code: "Home", key: "Home"}, 112 | Numpad7: { 113 | keyCode: 36, 114 | shiftKeyCode: 103, 115 | key: "Home", 116 | code: "Numpad7", 117 | shiftKey: "7", 118 | location: 3, 119 | }, 120 | ArrowLeft: {keyCode: 37, code: "ArrowLeft", key: "ArrowLeft"}, 121 | Numpad4: { 122 | keyCode: 37, 123 | shiftKeyCode: 100, 124 | key: "ArrowLeft", 125 | code: "Numpad4", 126 | shiftKey: "4", 127 | location: 3, 128 | }, 129 | Numpad8: { 130 | keyCode: 38, 131 | shiftKeyCode: 104, 132 | key: "ArrowUp", 133 | code: "Numpad8", 134 | shiftKey: "8", 135 | location: 3, 136 | }, 137 | ArrowUp: {keyCode: 38, code: "ArrowUp", key: "ArrowUp"}, 138 | ArrowRight: {keyCode: 39, code: "ArrowRight", key: "ArrowRight"}, 139 | Numpad6: { 140 | keyCode: 39, 141 | shiftKeyCode: 102, 142 | key: "ArrowRight", 143 | code: "Numpad6", 144 | shiftKey: "6", 145 | location: 3, 146 | }, 147 | Numpad2: { 148 | keyCode: 40, 149 | shiftKeyCode: 98, 150 | key: "ArrowDown", 151 | code: "Numpad2", 152 | shiftKey: "2", 153 | location: 3, 154 | }, 155 | ArrowDown: {keyCode: 40, code: "ArrowDown", key: "ArrowDown"}, 156 | Select: {keyCode: 41, code: "Select", key: "Select"}, 157 | Open: {keyCode: 43, code: "Open", key: "Execute"}, 158 | PrintScreen: {keyCode: 44, code: "PrintScreen", key: "PrintScreen"}, 159 | Insert: {keyCode: 45, code: "Insert", key: "Insert"}, 160 | Numpad0: { 161 | keyCode: 45, 162 | shiftKeyCode: 96, 163 | key: "Insert", 164 | code: "Numpad0", 165 | shiftKey: "0", 166 | location: 3, 167 | }, 168 | Delete: {keyCode: 46, code: "Delete", key: "Delete"}, 169 | NumpadDecimal: { 170 | keyCode: 46, 171 | shiftKeyCode: 110, 172 | code: "NumpadDecimal", 173 | key: "\u0000", 174 | shiftKey: ".", 175 | location: 3, 176 | }, 177 | Digit0: {keyCode: 48, code: "Digit0", shiftKey: ")", key: "0"}, 178 | Digit1: {keyCode: 49, code: "Digit1", shiftKey: "!", key: "1"}, 179 | Digit2: {keyCode: 50, code: "Digit2", shiftKey: "@", key: "2"}, 180 | Digit3: {keyCode: 51, code: "Digit3", shiftKey: "#", key: "3"}, 181 | Digit4: {keyCode: 52, code: "Digit4", shiftKey: "$", key: "4"}, 182 | Digit5: {keyCode: 53, code: "Digit5", shiftKey: "%", key: "5"}, 183 | Digit6: {keyCode: 54, code: "Digit6", shiftKey: "^", key: "6"}, 184 | Digit7: {keyCode: 55, code: "Digit7", shiftKey: "&", key: "7"}, 185 | Digit8: {keyCode: 56, code: "Digit8", shiftKey: "*", key: "8"}, 186 | Digit9: {keyCode: 57, code: "Digit9", shiftKey: "(", key: "9"}, 187 | KeyA: {keyCode: 65, code: "KeyA", shiftKey: "A", key: "a"}, 188 | KeyB: {keyCode: 66, code: "KeyB", shiftKey: "B", key: "b"}, 189 | KeyC: {keyCode: 67, code: "KeyC", shiftKey: "C", key: "c"}, 190 | KeyD: {keyCode: 68, code: "KeyD", shiftKey: "D", key: "d"}, 191 | KeyE: {keyCode: 69, code: "KeyE", shiftKey: "E", key: "e"}, 192 | KeyF: {keyCode: 70, code: "KeyF", shiftKey: "F", key: "f"}, 193 | KeyG: {keyCode: 71, code: "KeyG", shiftKey: "G", key: "g"}, 194 | KeyH: {keyCode: 72, code: "KeyH", shiftKey: "H", key: "h"}, 195 | KeyI: {keyCode: 73, code: "KeyI", shiftKey: "I", key: "i"}, 196 | KeyJ: {keyCode: 74, code: "KeyJ", shiftKey: "J", key: "j"}, 197 | KeyK: {keyCode: 75, code: "KeyK", shiftKey: "K", key: "k"}, 198 | KeyL: {keyCode: 76, code: "KeyL", shiftKey: "L", key: "l"}, 199 | KeyM: {keyCode: 77, code: "KeyM", shiftKey: "M", key: "m"}, 200 | KeyN: {keyCode: 78, code: "KeyN", shiftKey: "N", key: "n"}, 201 | KeyO: {keyCode: 79, code: "KeyO", shiftKey: "O", key: "o"}, 202 | KeyP: {keyCode: 80, code: "KeyP", shiftKey: "P", key: "p"}, 203 | KeyQ: {keyCode: 81, code: "KeyQ", shiftKey: "Q", key: "q"}, 204 | KeyR: {keyCode: 82, code: "KeyR", shiftKey: "R", key: "r"}, 205 | KeyS: {keyCode: 83, code: "KeyS", shiftKey: "S", key: "s"}, 206 | KeyT: {keyCode: 84, code: "KeyT", shiftKey: "T", key: "t"}, 207 | KeyU: {keyCode: 85, code: "KeyU", shiftKey: "U", key: "u"}, 208 | KeyV: {keyCode: 86, code: "KeyV", shiftKey: "V", key: "v"}, 209 | KeyW: {keyCode: 87, code: "KeyW", shiftKey: "W", key: "w"}, 210 | KeyX: {keyCode: 88, code: "KeyX", shiftKey: "X", key: "x"}, 211 | KeyY: {keyCode: 89, code: "KeyY", shiftKey: "Y", key: "y"}, 212 | KeyZ: {keyCode: 90, code: "KeyZ", shiftKey: "Z", key: "z"}, 213 | MetaLeft: {keyCode: 91, code: "MetaLeft", key: "Meta", location: 1}, 214 | MetaRight: {keyCode: 92, code: "MetaRight", key: "Meta", location: 2}, 215 | ContextMenu: {keyCode: 93, code: "ContextMenu", key: "ContextMenu"}, 216 | NumpadMultiply: {keyCode: 106, code: "NumpadMultiply", key: "*", location: 3}, 217 | NumpadAdd: {keyCode: 107, code: "NumpadAdd", key: "+", location: 3}, 218 | NumpadSubtract: {keyCode: 109, code: "NumpadSubtract", key: "-", location: 3}, 219 | NumpadDivide: {keyCode: 111, code: "NumpadDivide", key: "/", location: 3}, 220 | F1: {keyCode: 112, code: "F1", key: "F1"}, 221 | F2: {keyCode: 113, code: "F2", key: "F2"}, 222 | F3: {keyCode: 114, code: "F3", key: "F3"}, 223 | F4: {keyCode: 115, code: "F4", key: "F4"}, 224 | F5: {keyCode: 116, code: "F5", key: "F5"}, 225 | F6: {keyCode: 117, code: "F6", key: "F6"}, 226 | F7: {keyCode: 118, code: "F7", key: "F7"}, 227 | F8: {keyCode: 119, code: "F8", key: "F8"}, 228 | F9: {keyCode: 120, code: "F9", key: "F9"}, 229 | F10: {keyCode: 121, code: "F10", key: "F10"}, 230 | F11: {keyCode: 122, code: "F11", key: "F11"}, 231 | F12: {keyCode: 123, code: "F12", key: "F12"}, 232 | F13: {keyCode: 124, code: "F13", key: "F13"}, 233 | F14: {keyCode: 125, code: "F14", key: "F14"}, 234 | F15: {keyCode: 126, code: "F15", key: "F15"}, 235 | F16: {keyCode: 127, code: "F16", key: "F16"}, 236 | F17: {keyCode: 128, code: "F17", key: "F17"}, 237 | F18: {keyCode: 129, code: "F18", key: "F18"}, 238 | F19: {keyCode: 130, code: "F19", key: "F19"}, 239 | F20: {keyCode: 131, code: "F20", key: "F20"}, 240 | F21: {keyCode: 132, code: "F21", key: "F21"}, 241 | F22: {keyCode: 133, code: "F22", key: "F22"}, 242 | F23: {keyCode: 134, code: "F23", key: "F23"}, 243 | F24: {keyCode: 135, code: "F24", key: "F24"}, 244 | NumLock: {keyCode: 144, code: "NumLock", key: "NumLock"}, 245 | ScrollLock: {keyCode: 145, code: "ScrollLock", key: "ScrollLock"}, 246 | AudioVolumeMute: { 247 | keyCode: 173, 248 | code: "AudioVolumeMute", 249 | key: "AudioVolumeMute", 250 | }, 251 | AudioVolumeDown: { 252 | keyCode: 174, 253 | code: "AudioVolumeDown", 254 | key: "AudioVolumeDown", 255 | }, 256 | AudioVolumeUp: {keyCode: 175, code: "AudioVolumeUp", key: "AudioVolumeUp"}, 257 | MediaTrackNext: {keyCode: 176, code: "MediaTrackNext", key: "MediaTrackNext"}, 258 | MediaTrackPrevious: { 259 | keyCode: 177, 260 | code: "MediaTrackPrevious", 261 | key: "MediaTrackPrevious", 262 | }, 263 | MediaStop: {keyCode: 178, code: "MediaStop", key: "MediaStop"}, 264 | MediaPlayPause: {keyCode: 179, code: "MediaPlayPause", key: "MediaPlayPause"}, 265 | Semicolon: {keyCode: 186, code: "Semicolon", shiftKey: ":", key: ";"}, 266 | Equal: {keyCode: 187, code: "Equal", shiftKey: "+", key: "="}, 267 | NumpadEqual: {keyCode: 187, code: "NumpadEqual", key: "=", location: 3}, 268 | Comma: {keyCode: 188, code: "Comma", shiftKey: "<", key: ","}, 269 | Minus: {keyCode: 189, code: "Minus", shiftKey: "_", key: "-"}, 270 | Period: {keyCode: 190, code: "Period", shiftKey: ">", key: "."}, 271 | Slash: {keyCode: 191, code: "Slash", shiftKey: "?", key: "/"}, 272 | Backquote: {keyCode: 192, code: "Backquote", shiftKey: "~", key: "`"}, 273 | BracketLeft: {keyCode: 219, code: "BracketLeft", shiftKey: "{", key: "["}, 274 | Backslash: {keyCode: 220, code: "Backslash", shiftKey: "|", key: "\\"}, 275 | BracketRight: {keyCode: 221, code: "BracketRight", shiftKey: "}", key: "]"}, 276 | Quote: {keyCode: 222, code: "Quote", shiftKey: '"', key: "'"}, 277 | AltGraph: {keyCode: 225, code: "AltGraph", key: "AltGraph"}, 278 | Props: {keyCode: 247, code: "Props", key: "CrSel"}, 279 | Cancel: {keyCode: 3, key: "Cancel", code: "Abort"}, 280 | Clear: {keyCode: 12, key: "Clear", code: "Numpad5", location: 3}, 281 | Shift: {keyCode: 16, key: "Shift", code: "ShiftLeft", location: 1}, 282 | Control: {keyCode: 17, key: "Control", code: "ControlLeft", location: 1}, 283 | Alt: {keyCode: 18, key: "Alt", code: "AltLeft", location: 1}, 284 | Accept: {keyCode: 30, key: "Accept"}, 285 | ModeChange: {keyCode: 31, key: "ModeChange"}, 286 | " ": {keyCode: 32, key: " ", code: "Space"}, 287 | Print: {keyCode: 42, key: "Print"}, 288 | Execute: {keyCode: 43, key: "Execute", code: "Open"}, 289 | "\u0000": {keyCode: 46, key: "\u0000", code: "NumpadDecimal", location: 3}, 290 | a: {keyCode: 65, key: "a", code: "KeyA"}, 291 | b: {keyCode: 66, key: "b", code: "KeyB"}, 292 | c: {keyCode: 67, key: "c", code: "KeyC"}, 293 | d: {keyCode: 68, key: "d", code: "KeyD"}, 294 | e: {keyCode: 69, key: "e", code: "KeyE"}, 295 | f: {keyCode: 70, key: "f", code: "KeyF"}, 296 | g: {keyCode: 71, key: "g", code: "KeyG"}, 297 | h: {keyCode: 72, key: "h", code: "KeyH"}, 298 | i: {keyCode: 73, key: "i", code: "KeyI"}, 299 | j: {keyCode: 74, key: "j", code: "KeyJ"}, 300 | k: {keyCode: 75, key: "k", code: "KeyK"}, 301 | l: {keyCode: 76, key: "l", code: "KeyL"}, 302 | m: {keyCode: 77, key: "m", code: "KeyM"}, 303 | n: {keyCode: 78, key: "n", code: "KeyN"}, 304 | o: {keyCode: 79, key: "o", code: "KeyO"}, 305 | p: {keyCode: 80, key: "p", code: "KeyP"}, 306 | q: {keyCode: 81, key: "q", code: "KeyQ"}, 307 | r: {keyCode: 82, key: "r", code: "KeyR"}, 308 | s: {keyCode: 83, key: "s", code: "KeyS"}, 309 | t: {keyCode: 84, key: "t", code: "KeyT"}, 310 | u: {keyCode: 85, key: "u", code: "KeyU"}, 311 | v: {keyCode: 86, key: "v", code: "KeyV"}, 312 | w: {keyCode: 87, key: "w", code: "KeyW"}, 313 | x: {keyCode: 88, key: "x", code: "KeyX"}, 314 | y: {keyCode: 89, key: "y", code: "KeyY"}, 315 | z: {keyCode: 90, key: "z", code: "KeyZ"}, 316 | Meta: {keyCode: 91, key: "Meta", code: "MetaLeft", location: 1}, 317 | "*": {keyCode: 106, key: "*", code: "NumpadMultiply", location: 3}, 318 | "+": {keyCode: 107, key: "+", code: "NumpadAdd", location: 3}, 319 | "-": {keyCode: 109, key: "-", code: "NumpadSubtract", location: 3}, 320 | "/": {keyCode: 111, key: "/", code: "NumpadDivide", location: 3}, 321 | ";": {keyCode: 186, key: ";", code: "Semicolon"}, 322 | "=": {keyCode: 187, key: "=", code: "Equal"}, 323 | ",": {keyCode: 188, key: ",", code: "Comma"}, 324 | ".": {keyCode: 190, key: ".", code: "Period"}, 325 | "`": {keyCode: 192, key: "`", code: "Backquote"}, 326 | "[": {keyCode: 219, key: "[", code: "BracketLeft"}, 327 | "\\": {keyCode: 220, key: "\\", code: "Backslash"}, 328 | "]": {keyCode: 221, key: "]", code: "BracketRight"}, 329 | "'": {keyCode: 222, key: "'", code: "Quote"}, 330 | Attn: {keyCode: 246, key: "Attn"}, 331 | CrSel: {keyCode: 247, key: "CrSel", code: "Props"}, 332 | ExSel: {keyCode: 248, key: "ExSel"}, 333 | EraseEof: {keyCode: 249, key: "EraseEof"}, 334 | Play: {keyCode: 250, key: "Play"}, 335 | ZoomOut: {keyCode: 251, key: "ZoomOut"}, 336 | ")": {keyCode: 48, key: ")", code: "Digit0"}, 337 | "!": {keyCode: 49, key: "!", code: "Digit1"}, 338 | "@": {keyCode: 50, key: "@", code: "Digit2"}, 339 | "#": {keyCode: 51, key: "#", code: "Digit3"}, 340 | $: {keyCode: 52, key: "$", code: "Digit4"}, 341 | "%": {keyCode: 53, key: "%", code: "Digit5"}, 342 | "^": {keyCode: 54, key: "^", code: "Digit6"}, 343 | "&": {keyCode: 55, key: "&", code: "Digit7"}, 344 | "(": {keyCode: 57, key: "(", code: "Digit9"}, 345 | A: {keyCode: 65, key: "A", code: "KeyA"}, 346 | B: {keyCode: 66, key: "B", code: "KeyB"}, 347 | C: {keyCode: 67, key: "C", code: "KeyC"}, 348 | D: {keyCode: 68, key: "D", code: "KeyD"}, 349 | E: {keyCode: 69, key: "E", code: "KeyE"}, 350 | F: {keyCode: 70, key: "F", code: "KeyF"}, 351 | G: {keyCode: 71, key: "G", code: "KeyG"}, 352 | H: {keyCode: 72, key: "H", code: "KeyH"}, 353 | I: {keyCode: 73, key: "I", code: "KeyI"}, 354 | J: {keyCode: 74, key: "J", code: "KeyJ"}, 355 | K: {keyCode: 75, key: "K", code: "KeyK"}, 356 | L: {keyCode: 76, key: "L", code: "KeyL"}, 357 | M: {keyCode: 77, key: "M", code: "KeyM"}, 358 | N: {keyCode: 78, key: "N", code: "KeyN"}, 359 | O: {keyCode: 79, key: "O", code: "KeyO"}, 360 | P: {keyCode: 80, key: "P", code: "KeyP"}, 361 | Q: {keyCode: 81, key: "Q", code: "KeyQ"}, 362 | R: {keyCode: 82, key: "R", code: "KeyR"}, 363 | S: {keyCode: 83, key: "S", code: "KeyS"}, 364 | T: {keyCode: 84, key: "T", code: "KeyT"}, 365 | U: {keyCode: 85, key: "U", code: "KeyU"}, 366 | V: {keyCode: 86, key: "V", code: "KeyV"}, 367 | W: {keyCode: 87, key: "W", code: "KeyW"}, 368 | X: {keyCode: 88, key: "X", code: "KeyX"}, 369 | Y: {keyCode: 89, key: "Y", code: "KeyY"}, 370 | Z: {keyCode: 90, key: "Z", code: "KeyZ"}, 371 | ":": {keyCode: 186, key: ":", code: "Semicolon"}, 372 | "<": {keyCode: 188, key: "<", code: "Comma"}, 373 | _: {keyCode: 189, key: "_", code: "Minus"}, 374 | ">": {keyCode: 190, key: ">", code: "Period"}, 375 | "?": {keyCode: 191, key: "?", code: "Slash"}, 376 | "~": {keyCode: 192, key: "~", code: "Backquote"}, 377 | "{": {keyCode: 219, key: "{", code: "BracketLeft"}, 378 | "|": {keyCode: 220, key: "|", code: "Backslash"}, 379 | "}": {keyCode: 221, key: "}", code: "BracketRight"}, 380 | '"': {keyCode: 222, key: '"', code: "Quote"}, 381 | SoftLeft: {key: "SoftLeft", code: "SoftLeft", location: 4}, 382 | SoftRight: {key: "SoftRight", code: "SoftRight", location: 4}, 383 | Camera: {keyCode: 44, key: "Camera", code: "Camera", location: 4}, 384 | Call: {key: "Call", code: "Call", location: 4}, 385 | EndCall: {keyCode: 95, key: "EndCall", code: "EndCall", location: 4}, 386 | VolumeDown: { 387 | keyCode: 182, 388 | key: "VolumeDown", 389 | code: "VolumeDown", 390 | location: 4, 391 | }, 392 | VolumeUp: {keyCode: 183, key: "VolumeUp", code: "VolumeUp", location: 4}, 393 | } 394 | -------------------------------------------------------------------------------- /renderer/images.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file image to js 3 | */ 4 | const loadingGif = 5 | "data:image/gif;base64,R0lGODlhIAAgALMAAP///7Ozs/v7+9bW1uHh4fLy8rq6uoGBgTQ0NAEBARsbG8TExJeXl/39/VRUVAAAACH/C05FVFNDQVBFMi4wAwEAAAAh+QQFBQAAACwAAAAAIAAgAAAE5xDISSlLrOrNp0pKNRCdFhxVolJLEJQUoSgOpSYT4RowNSsvyW1icA16k8MMMRkCBjskBTFDAZyuAEkqCfxIQ2hgQRFvAQEEIjNxVDW6XNE4YagRjuBCwe60smQUDnd4Rz1ZAQZnFAGDd0hihh12CEE9kjAEVlycXIg7BAsMB6SlnJ87paqbSKiKoqusnbMdmDC2tXQlkUhziYtyWTxIfy6BE8WJt5YEvpJivxNaGmLHT0VnOgGYf0dZXS7APdpB309RnHOG5gDqXGLDaC457D1zZ/V/nmOM82XiHQjYKhKP1oZmADdEAAAh+QQFBQAAACwAAAAAGAAXAAAEchDISasKNeuJFKoHs4mUYlJIkmjIV54Soypsa0wmLSnqoTEtBw52mG0AjhYpBxioEqRNy8V0qFzNw+GGwlJki4lBqx1IBgjMkRIghwjrzcDti2/Gh7D9qN774wQGAYOEfwCChIV/gYmDho+QkZKTR3p7EQAh+QQFBQAAACwBAAAAHQAOAAAEchDISWdANesNHHJZwE2DUSEo5SjKKB2HOKGYFLD1CB/DnEoIlkti2PlyuKGEATMBaAACSyGbEDYD4zN1YIEmh0SCQQgYehNmTNNaKsQJXmBuuEYPi9ECAU/UFnNzeUp9VBQEBoFOLmFxWHNoQw6RWEocEQAh+QQFBQAAACwHAAAAGQARAAAEaRDICdZZNOvNDsvfBhBDdpwZgohBgE3nQaki0AYEjEqOGmqDlkEnAzBUjhrA0CoBYhLVSkm4SaAAWkahCFAWTU0A4RxzFWJnzXFWJJWb9pTihRu5dvghl+/7NQmBggo/fYKHCX8AiAmEEQAh+QQFBQAAACwOAAAAEgAYAAAEZXCwAaq9ODAMDOUAI17McYDhWA3mCYpb1RooXBktmsbt944BU6zCQCBQiwPB4jAihiCK86irTB20qvWp7Xq/FYV4TNWNz4oqWoEIgL0HX/eQSLi69boCikTkE2VVDAp5d1p0CW4RACH5BAUFAAAALA4AAAASAB4AAASAkBgCqr3YBIMXvkEIMsxXhcFFpiZqBaTXisBClibgAnd+ijYGq2I4HAamwXBgNHJ8BEbzgPNNjz7LwpnFDLvgLGJMdnw/5DRCrHaE3xbKm6FQwOt1xDnpwCvcJgcJMgEIeCYOCQlrF4YmBIoJVV2CCXZvCooHbwGRcAiKcmFUJhEAIfkEBQUAAAAsDwABABEAHwAABHsQyAkGoRivELInnOFlBjeM1BCiFBdcbMUtKQdTN0CUJru5NJQrYMh5VIFTTKJcOj2HqJQRhEqvqGuU+uw6AwgEwxkOO55lxIihoDjKY8pBoThPxmpAYi+hKzoeewkTdHkZghMIdCOIhIuHfBMOjxiNLR4KCW1ODAlxSxEAIfkEBQUAAAAsCAAOABgAEgAABGwQyEkrCDgbYvvMoOF5ILaNaIoGKroch9hacD3MFMHUBzMHiBtgwJMBFolDB4GoGGBCACKRcAAUWAmzOWJQExysQsJgWj0KqvKalTiYPhp1LBFTtp10Is6mT5gdVFx1bRN8FTsVCAqDOB9+KhEAIfkEBQUAAAAsAgASAB0ADgAABHgQyEmrBePS4bQdQZBdR5IcHmWEgUFQgWKaKbWwwSIhc4LonsXhBSCsQoOSScGQDJiWwOHQnAxWBIYJNXEoFCiEWDI9jCzESey7GwMM5doEwW4jJoypQQ743u1WcTV0CgFzbhJ5XClfHYd/EwZnHoYVDgiOfHKQNREAIfkEBQUAAAAsAAAPABkAEQAABGeQqUQruDjrW3vaYCZ5X2ie6EkcKaooTAsi7ytnTq046BBsNcTvItz4AotMwKZBIC6H6CVAJaCcT0CUBTgaTg5nTCu9GKiDEMPJg5YBBOpwlnVzLwtqyKnZagZWahoMB2M3GgsHSRsRACH5BAUFAAAALAEACAARABgAAARcMKR0gL34npkUyyCAcAmyhBijkGi2UW02VHFt33iu7yiDIDaD4/erEYGDlu/nuBAOJ9Dvc2EcDgFAYIuaXS3bbOh6MIC5IAP5Eh5fk2exC4tpgwZyiyFgvhEMBBEAIfkEBQUAAAAsAAACAA4AHQAABHMQyAnYoViSlFDGXBJ808Ep5KRwV8qEg+pRCOeoioKMwJK0Ekcu54h9AoghKgXIMZgAApQZcCCu2Ax2O6NUud2pmJcyHA4L0uDM/ljYDCnGfGakJQE5YH0wUBYBAUYfBIFkHwaBgxkDgX5lgXpHAXcpBIsRADs=" 6 | const faviconPng = 7 | "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABJUlEQVR42mKgOogpKJBMTM08kJCS+RXAJ1UbVBRDUWQIRmACWmwEpHxuN8GpkRIdAOuRfehwnQDXe7696C+eJkdvoox+ceEd/7DWFZyUcinO6JNBf3FOP/z+HGXirkX0hzXs8YJbG78ZvHp2dtaP/3E+Owqwcv1aJLDGSl8Ap6kY7JDmcjfK6UklaDvR4iBfy/Zq+19cyCETqF7AdAiilF6RGbYV0hFWOm9dbyYBiu0QIBcK85XLm090guoDGI3AUPiOM3HJrtbhKs8XBvh7k4mO+DkNMWC0CL6saS5D1Q0IERcRrBKdVy729Ti0apaojlEFHvI1k+Q03t6HESOeMUbjILUJCpo0bK8C7DxI9ezFMtibDuiLQY8oDJn/h5yUKctM1AYAkF4mBkXjJukAAAAASUVORK5CYII=" 8 | export {loadingGif, faviconPng} 9 | -------------------------------------------------------------------------------- /renderer/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 入口文件 3 | * 4 | */ 5 | import BrowserManager from "./BrowserManager.js" 6 | export default new BrowserManager() 7 | export {BrowserManager as Class} 8 | -------------------------------------------------------------------------------- /renderer/ipc.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file ipc通信类 3 | */ 4 | const {remote} = require("electron") 5 | import util from "./util.js" 6 | 7 | const isDevelopment = process.env.NODE_ENV === "development" 8 | const ipcLog = process.env.ipcLog 9 | 10 | // renderer to preload 11 | const SEND_NAME = "electron-puppeteer_r2p" 12 | // preload to renderer 13 | const RECEIVE_NAME = "electron-puppeteer_p2r" 14 | // ack prefix 15 | const ACK_PREFIX = "ack_r2p_" 16 | // browserWindow connect 17 | export const BROWSER_WINDOW_PAGE_CONNECT = "electron-puppeteer_page_connect" 18 | 19 | /** 20 | * @class Ipc 21 | */ 22 | export default class Ipc { 23 | /** 24 | * @constructor Ipc 25 | */ 26 | constructor(webview, options) { 27 | this.webview = webview 28 | this.options = options || {} 29 | this._hold = false 30 | this._destroyed = false 31 | this._holdTasks = [] 32 | this._listeners = [] 33 | // UUID -> routingId的映射 34 | this._routingIdMaps = new Map() 35 | this._bindEvent() 36 | } 37 | /** 38 | * @typedef {Object} IpcEvent 39 | * @property {string} UUID iframe的UUID, mainFrame为~,任意frame为* 40 | * @property {string} name 消息名 41 | * @property {string} ack 回执消息名 42 | * @property {boolean} isMainFrame 是否为主页面 43 | * @property {IpcEvent} originalEvent 原生事件 44 | */ 45 | /** 46 | * @typedef {Function} IpcListener 47 | * @param {IpcEvent} evt 消息对象 48 | * @param {Object} payload 消息数据 49 | */ 50 | 51 | /** 52 | * 绑定消息监听事件 53 | * @private 54 | */ 55 | _bindEvent() { 56 | this.webview.addEventListener("ipc-message", (originalEvent) => { 57 | if (originalEvent.channel === RECEIVE_NAME) { 58 | let evt = originalEvent.args[0] 59 | evt.originalEvent = originalEvent 60 | this._handleMessageEvent(evt) 61 | } 62 | }) 63 | 64 | // 页面跳转之前 65 | // hold住所有消息,等完成后再继续发送,否则会丢失 66 | this.webview.addEventListener("will-navigate", () => { 67 | this._hold = true 68 | isDevelopment && 69 | ipcLog && 70 | console.warn("%cipc hold", "font-weight:bold;", this.webview) 71 | }) 72 | 73 | const onRecover = () => { 74 | if (this._hold) { 75 | isDevelopment && 76 | ipcLog && 77 | console.warn("%cipc recover", "font-weight:bold;", this.webview) 78 | this._hold = false 79 | this._runHoldTasks() 80 | } 81 | } 82 | 83 | // 页面ready后重新发送消息 84 | this.webview.addEventListener("dom-ready", onRecover) 85 | 86 | this.webview.addEventListener("close", () => { 87 | this._destroyed = true 88 | }) 89 | this.webview.addEventListener("destroyed", () => { 90 | this._destroyed = true 91 | }) 92 | } 93 | _handleMessageEvent(evt) { 94 | if (evt.isAck) { 95 | isDevelopment && 96 | ipcLog && 97 | console.log( 98 | "%cipc ack", 99 | "font-weight:bold;color:green;", 100 | `name: ${evt.name},`, 101 | `UUID: ${evt.UUID},`, 102 | `payload: ${JSON.stringify(evt.payload)}` 103 | ) 104 | } else { 105 | isDevelopment && 106 | ipcLog && 107 | console.log( 108 | "%cipc receive", 109 | "font-weight:bold;color:darkCyan;", 110 | `name: ${evt.name},`, 111 | `ack: ${evt.ack},`, 112 | `UUID: ${evt.UUID},`, 113 | `payload: ${JSON.stringify(evt.payload)}` 114 | ) 115 | } 116 | 117 | if (!evt.isAck && evt.UUID !== "*") { 118 | this._routingIdMaps.set(evt.UUID, evt.routingId) 119 | } 120 | 121 | let results = [] 122 | for (let i = 0; i < this._listeners.length; i++) { 123 | let item = this._listeners[i] 124 | if ( 125 | (item.UUID === "*" || item.UUID === evt.UUID) && 126 | [].concat(item.name).indexOf(evt.name) > -1 && 127 | !!item.isAck === !!evt.isAck 128 | ) { 129 | let once = item.isAck || item.once 130 | results.push( 131 | item.listener.call(this.webview, evt.payload, evt, evt.error) 132 | ) 133 | if (once) { 134 | this._listeners.splice(i--, 1) 135 | } 136 | // ack是唯一的,无需往后匹配 137 | if (item.isAck) { 138 | break 139 | } 140 | } 141 | } 142 | 143 | // reply 144 | // 没有ack则认为不需要回复 145 | if (!evt.isAck && evt.ack) { 146 | Promise.race(results.slice(0, 1)).then( 147 | (result) => { 148 | this._sendAck(evt.UUID, evt.ack, null, result, evt.isMainFrame) 149 | }, 150 | (err) => { 151 | this._sendAck( 152 | evt.UUID, 153 | evt.ack, 154 | (err && err.toString()) || "-", 155 | null, 156 | evt.isMainFrame 157 | ) 158 | } 159 | ) 160 | } 161 | 162 | return results 163 | } 164 | _runHoldTasks() { 165 | while (this._holdTasks.length) { 166 | let task = this._holdTasks.shift() 167 | if (task.isAck) { 168 | this._sendAck( 169 | task.UUID, 170 | task.name, 171 | task.error, 172 | task.payload, 173 | task.isMainFrame 174 | ) 175 | } else { 176 | this.send( 177 | task.UUID, 178 | task.name, 179 | task.payload, 180 | task.timeout, 181 | task.retry 182 | ).then(task.resolve, task.reject) 183 | } 184 | } 185 | } 186 | // 生成ack_name 187 | _generatorAckName(name) { 188 | return util.uniqueId(ACK_PREFIX + name + "_") 189 | } 190 | _sendAck(UUID, ack, err, payload, isMainFrame) { 191 | if (this._destroyed) { 192 | return false 193 | } 194 | if (this._hold) { 195 | this._holdTasks.push({ 196 | UUID, 197 | name: ack, 198 | isAck: true, 199 | error: err, 200 | payload: payload, 201 | isMainFrame: isMainFrame, 202 | }) 203 | } else { 204 | isDevelopment && 205 | ipcLog && 206 | console.log( 207 | "%cipc reply", 208 | "font-weight:bold;color:#c59519", 209 | `name: ${ack},`, 210 | `UUID: ${UUID},`, 211 | `result: ${JSON.stringify(payload)}` 212 | ) 213 | 214 | let sender = this._getSender(UUID) 215 | if (sender) { 216 | try { 217 | sender(SEND_NAME, { 218 | UUID, 219 | name: ack, 220 | ack: "", 221 | error: err, 222 | isAck: true, 223 | payload: payload, 224 | isMainFrame: isMainFrame, 225 | }) 226 | } catch (e) {} 227 | } 228 | } 229 | } 230 | _isWebviewInDocument() { 231 | return ( 232 | (this.webview.isDestroyed && !this.webview.isDestroyed()) || 233 | (this.webview.ownerDocument && 234 | this.webview.ownerDocument.contains(this.webview)) 235 | ) 236 | } 237 | _getSender(UUID) { 238 | if (!this._isWebviewInDocument()) { 239 | return null 240 | } 241 | 242 | switch (UUID) { 243 | // any 244 | case "*": 245 | return (name, data) => { 246 | let sended = new Set() 247 | this._routingIdMaps.forEach((routingId) => { 248 | if (!sended.has(routingId)) { 249 | sended.add(routingId) 250 | let contentsId = this.webview.getWebContentsId() 251 | let contents = remote.webContents.fromId(contentsId) 252 | contents.sendToFrame(routingId, name, data) 253 | } 254 | }) 255 | } 256 | // mainFrame 257 | case "~": 258 | return this.webview.send.bind(this.webview) 259 | // other 260 | default: 261 | let routingId = this._routingIdMaps.get(UUID) 262 | if (!routingId) { 263 | console.error("ipc reply error, failed to map routingId") 264 | throw "ipc error" 265 | } 266 | let contentsId = this.webview.getWebContentsId() 267 | let contents = remote.webContents.fromId(contentsId) 268 | return contents.sendToFrame.bind(contents, routingId) 269 | } 270 | } 271 | 272 | /** 273 | * 给webview内的指定iframe发送消息 274 | * @param {string} UUID iframe的UUID, mainFrame为~,任意frame为* 275 | * @param {string|string[]} name 消息名, 支持合并发送payload相同的消息 276 | * @param {Object} payload 传输数据 277 | * @param {number} timeout 等待回复时间 278 | * @param {number} retry 重试次数,默认不重试,仅在超时的情况才会重试 279 | * 280 | * @return {Promise} 返回promise,等待消息回复内容 281 | */ 282 | send(UUID, name, payload, timeout, retry) { 283 | if (this._destroyed) { 284 | return Promise.reject("ipc webview destroyed") 285 | } 286 | 287 | if (Array.isArray(name)) { 288 | return Promise.all( 289 | name.map((item) => this.send(item, payload, timeout, retry)) 290 | ) 291 | } 292 | 293 | if (this._hold) { 294 | return new Promise((resolve, reject) => { 295 | this._holdTasks.push({ 296 | resolve, 297 | reject, 298 | UUID, 299 | name, 300 | payload, 301 | timeout, 302 | retry, 303 | isAck: false, 304 | }) 305 | }) 306 | } 307 | 308 | timeout = timeout || this.options.timeout || 1e4 309 | let ack = this._generatorAckName(name) 310 | 311 | return new Promise((resolve, reject) => { 312 | // 收到回执信息,触发回调 313 | let onAck = (result, evt) => { 314 | window.clearTimeout(timer) 315 | if (evt.error) { 316 | return reject(evt.error) 317 | } 318 | resolve(result) 319 | } 320 | // 放入监听队列 321 | this._listeners.push({ 322 | UUID, 323 | name: ack, 324 | listener: onAck, 325 | once: true, 326 | isAck: true, 327 | }) 328 | // 超时判断 329 | let timer = window.setTimeout(() => { 330 | this.off(UUID, ack, onAck) 331 | let payloadString = JSON.stringify(payload) 332 | if (payloadString.length > 200) { 333 | payloadString = payloadString.substr(0, 200) + "..." 334 | } 335 | reject(`ipc.timeout.send: ${name}@${UUID}, payload: ${payloadString}`) 336 | }, timeout) 337 | 338 | retry = retry || 0 339 | 340 | isDevelopment && 341 | ipcLog && 342 | console.log( 343 | "%cipc send", 344 | "font-weight:bold;color:#00f", 345 | `name: ${name},`, 346 | `ack: ${ack},`, 347 | `UUID: ${UUID},`, 348 | `payload: ${JSON.stringify(payload)}` 349 | ) 350 | 351 | // 获取发送消息的sender 352 | let sender = this._getSender(UUID) 353 | if (sender) { 354 | // 发送消息 355 | sender(SEND_NAME, { 356 | UUID, 357 | name, 358 | ack, 359 | payload, 360 | isAck: false, 361 | }) 362 | } 363 | }).catch((err) => { 364 | if (typeof err === "string" && err.startsWith("ipc.timeout")) { 365 | isDevelopment && 366 | ipcLog && 367 | console.log( 368 | "%cipc timeout", 369 | "font-weight:bold;color:#f00", 370 | `name: ${name},`, 371 | `ack: ${ack},`, 372 | `UUID: ${UUID},`, 373 | `payload: ${JSON.stringify(payload)}` 374 | ) 375 | } else { 376 | // console.error("ipc send error:", err) 377 | } 378 | 379 | if (--retry >= 0) { 380 | return this.send(UUID, name, payload, timeout, retry) 381 | } 382 | 383 | return Promise.reject(err) 384 | }) 385 | } 386 | /** 387 | * 当收到某消息时立即发送指定消息给发送方, 和ack不同 388 | * @param {string} UUID iframe的UUID, mainFrame为~,任意frame为* 389 | * @param {string} trigger 触发的消息名 390 | * @param {string|string[]} name 消息名, 支持合并发送payload相同的消息 391 | * @param {Object} payload 传输数据 392 | */ 393 | sendOn(UUID, trigger, name, payload) { 394 | this.on(UUID, trigger, () => { 395 | this.send(UUID, name, payload) 396 | }) 397 | } 398 | /** 399 | * 监听webview内iframe消息 400 | * @param {string} UUID 消息来源iframe的UUID, mainFrame为~,任意frame为* 401 | * @param {string|string[]} name 消息名 402 | * @param {IpcListener} listener 响应函数 403 | * @param {boolean} unique 同名消息只绑定一次, 再次绑定时会取消前一次的绑定 404 | * 405 | * @return {Ipc} this 406 | * 407 | */ 408 | on(UUID, name, listener, unique) { 409 | if (unique) { 410 | this.off(UUID, name) 411 | } 412 | 413 | this._listeners.push({ 414 | UUID, 415 | name, 416 | listener, 417 | once: false, 418 | isAck: false, 419 | }) 420 | return this 421 | } 422 | /** 423 | * 单次监听webview内的消息 424 | * @param {string} UUID iframe的UUID, mainFrame为~,任意frame为* 425 | * @param {string|string[]} name 消息名 426 | * @param {IpcListener} listener 响应函数 427 | * 428 | * @return {Ipc} this 429 | * 430 | */ 431 | once(UUID, name, listener) { 432 | this._listeners.push({ 433 | UUID, 434 | name, 435 | listener, 436 | once: true, 437 | isAck: false, 438 | }) 439 | return this 440 | } 441 | /** 442 | * 取消监听 443 | * @param {string} UUID iframe的UUID, mainFrame为~,任意frame为* 444 | * @param {string|string[]} name 消息名 445 | * @param {IpcListener} [listener] 响应函数 446 | * @return {Ipc} this 447 | */ 448 | off(UUID, name, listener) { 449 | this._listeners = this._listeners.filter((item) => { 450 | if ( 451 | item.UUID === UUID && 452 | item.name.toString() === name.toString() && 453 | (!listener || item.listener === listener) 454 | ) { 455 | return false 456 | } 457 | return true 458 | }) 459 | return this 460 | } 461 | /** 462 | * 模拟触发消息 463 | * @param {string} UUID iframe的UUID, mainFrame为~,任意frame为* 464 | * @param {string} name 消息名 465 | * @param {*} payload 消息参数 466 | * @param {*} error 错误信息 467 | * @return {*} 首个响应消息内容 468 | */ 469 | dispatch(UUID, name, payload, error) { 470 | let results = this._handleMessageEvent({ 471 | UUID, 472 | name, 473 | payload, 474 | error, 475 | isAck: false, 476 | ack: false, 477 | }) 478 | 479 | return results.slice(0, 1) 480 | } 481 | } 482 | 483 | const ipcPool = new WeakMap() 484 | 485 | /** 486 | * 已绑定了UUID的ipc 487 | */ 488 | export class BoundIpc { 489 | static setOptions(options) { 490 | BoundIpc.options = options 491 | } 492 | constructor(webview, UUID) { 493 | this.UUID = UUID 494 | 495 | if (ipcPool.has(webview)) { 496 | this.executor = ipcPool.get(webview) 497 | } else { 498 | this.executor = new Ipc(webview, BoundIpc.options) 499 | ipcPool.set(webview, this.executor) 500 | } 501 | } 502 | /** 503 | * 发送消息 504 | * @param {string|string[]} name 消息名, 支持合并发送payload相同的消息 505 | * @param {Object} payload 传输数据 506 | * @param {number} timeout 等待回复时间 507 | * @param {number} retry 重试次数,默认不重试,仅在超时的情况才会重试 508 | * 509 | * @return {Promise} 返回promise,等待消息回复内容 510 | */ 511 | send(name, payload, timeout, retry) { 512 | return this.executor.send(this.UUID, name, payload, timeout, retry) 513 | } 514 | /** 515 | * 当收到某消息时立即发送指定消息给发送方, 和ack不同 516 | * @param {string} trigger 触发的消息名 517 | * @param {string|string[]} name 消息名, 支持合并发送payload相同的消息 518 | * @param {Object} payload 传输数据 519 | * 520 | * @return {BoundIpc} this 521 | */ 522 | sendOn(trigger, name, payload) { 523 | this.executor.sendOn(this.UUID, trigger, name, payload) 524 | return this 525 | } 526 | /** 527 | * 监听消息 528 | * @param {string|string[]} name 消息名 529 | * @param {IpcListener} listener 响应函数 530 | * @param {boolean} unique 同名消息只绑定一次, 再次绑定时会取消前一次的绑定 531 | * 532 | * @return {BoundIpc} this 533 | * 534 | */ 535 | on(name, listener, unique) { 536 | this.executor.on(this.UUID, name, listener, unique) 537 | return this 538 | } 539 | /** 540 | * 单次监听webview内的消息 541 | * @param {string|string[]} name 消息名 542 | * @param {IpcListener} listener 响应函数 543 | * 544 | * @return {BoundIpc} this 545 | * 546 | */ 547 | once(name, listener) { 548 | this.executor.once(this.UUID, name, listener) 549 | return this 550 | } 551 | /** 552 | * 取消监听 553 | * @param {string|string[]} name 消息名 554 | * @param {IpcListener} listener 响应函数 555 | * 556 | * @return {BoundIpc} this 557 | * 558 | */ 559 | off(name, listener) { 560 | this.executor.off(this.UUID, name, listener) 561 | return this 562 | } 563 | /** 564 | * 手动触发消息 565 | * @param {string} name 消息名 566 | * @param {*} payload 消息内容 567 | * @param {*} error 错误信息 568 | */ 569 | dispatch(name, payload, error) { 570 | return this.executor.dispatch(this.UUID, name, payload, error) 571 | } 572 | } 573 | -------------------------------------------------------------------------------- /renderer/libs/chrome-search.js: -------------------------------------------------------------------------------- 1 | import {parseStrToDOM, importStyle, debounce} from "../util" 2 | import {register, unregister} from "./localshortcutswrap" 3 | 4 | export default class ChromeSearch { 5 | constructor(page) { 6 | this.page = page 7 | this.webview = page._getWebview() 8 | 9 | this.doms = {} 10 | this.value = "" 11 | this.needCreate = true 12 | 13 | this.evDelegateClick = this.evDelegateClick.bind(this) 14 | this.evKeyDown = this.evKeyDown.bind(this) 15 | this.evTextInput = this.evTextInput.bind(this) 16 | this.evEscClick = this.evEscClick.bind(this) 17 | } 18 | create() { 19 | let container = parseStrToDOM(` 20 | 28 | `) 29 | 30 | let doms = (this.doms = { 31 | container, 32 | count: container.querySelector(".chrome-search-count"), 33 | textInput: container.querySelector(".chrome-search-input"), 34 | }) 35 | 36 | container.addEventListener("click", this.evDelegateClick, false) 37 | doms.textInput.addEventListener("input", this.evTextInput, false) 38 | doms.textInput.addEventListener("keyup", this.evKeyDown, false) 39 | register("Esc", this.evEscClick, {only: true}) 40 | this.page._getDOM().appendChild(container) 41 | } 42 | evDelegateClick(evt) { 43 | let target = evt.target 44 | let currentTarget = evt.currentTarget 45 | 46 | let role 47 | while (!role && target !== currentTarget) { 48 | target = target.parentNode 49 | role = target.getAttribute("role") 50 | } 51 | 52 | switch (role) { 53 | case "pre": 54 | this.find({ 55 | forward: false, 56 | findNext: true, 57 | }) 58 | break 59 | case "next": 60 | this.find({ 61 | forward: true, 62 | findNext: true, 63 | }) 64 | break 65 | case "close": 66 | this.hide() 67 | break 68 | } 69 | } 70 | evTextInput(evt) { 71 | let value = (this.value = evt.target.value) 72 | if (!value) { 73 | return 74 | } 75 | 76 | this.debounceFind() 77 | } 78 | evKeyDown(evt) { 79 | if (evt.keyCode === 13) { 80 | this.find({ 81 | forward: !evt.shiftKey, 82 | findNext: true, 83 | }) 84 | } 85 | } 86 | evEscClick() { 87 | this.hide() 88 | } 89 | debounceFind = debounce(function (option) { 90 | this.find(option) 91 | }, 300) 92 | find(option) { 93 | let p = new Promise((resolve, reject) => { 94 | let timeoutId = setTimeout(() => reject("timeout"), 2e3) 95 | let onFounInPage = (e) => { 96 | let result = e.result 97 | if (result.requestId === requestId) { 98 | window.clearTimeout(timeoutId) 99 | this.webview.removeEventListener("found-in-page", onFounInPage) 100 | resolve(result) 101 | } 102 | } 103 | 104 | this.webview.addEventListener("found-in-page", onFounInPage) 105 | 106 | let requestId = this.webview.findInPage(this.value, option) 107 | }) 108 | 109 | p.then((result) => { 110 | this.updateCount(result.activeMatchOrdinal, result.matches) 111 | }).catch((e) => { 112 | if (e === "timeout") { 113 | return e 114 | } 115 | throw e 116 | }) 117 | return p 118 | } 119 | updateCount(current, count) { 120 | this.doms.count.innerText = `${current}/${count}` 121 | } 122 | show() { 123 | if (this.needCreate) { 124 | this.create() 125 | this.doms.textInput.focus() 126 | this.needCreate = false 127 | } else { 128 | this.doms.textInput.setSelectionRange(0, 99999) 129 | } 130 | } 131 | hide() { 132 | this.destroy() 133 | this.needCreate = true 134 | } 135 | destroy() { 136 | let doms = this.doms 137 | doms.container.removeEventListener("click", this.evDelegateClick) 138 | doms.textInput.removeEventListener("input", this.evTextInput) 139 | doms.container.remove() 140 | this.webview.stopFindInPage("clearSelection") 141 | unregister("Esc", this.evEscClick) 142 | } 143 | } 144 | 145 | const icon = { 146 | pre: ``, 147 | next: ``, 148 | close: ``, 149 | } 150 | 151 | importStyle( 152 | ` 153 | .chrome-search { 154 | position: absolute; 155 | display: flex; 156 | top: 0; 157 | right: 30px; 158 | width: 350px; 159 | height: 42px; 160 | background: #fff; 161 | padding: 5px 10px; 162 | user-select:none; 163 | box-shadow: -3px 1px 4px #e3e3e3; 164 | border: 1px solid #e3e3e3; 165 | } 166 | .chrome-search-input { 167 | border: none; 168 | flex: 1 0 200px; 169 | } 170 | .chrome-search-input:focus { 171 | outline: none; 172 | } 173 | .chrome-search-count { 174 | flex: 1 0 50x; 175 | margin-right: 2px; 176 | margin-left: 2px; 177 | text-align: center; 178 | line-height: 30px; 179 | } 180 | .chrome-search-eparator { 181 | width: 0; 182 | border-left: 1px solid #e3e3e3; 183 | margin-right: 2px; 184 | margin-left: 2px; 185 | height: 30px; 186 | } 187 | .chrome-search-pre, 188 | .chrome-search-next, 189 | .chrome-search-close { 190 | margin-top: 3px; 191 | margin-left: 5px; 192 | width: 24px; 193 | height: 24px; 194 | cursor: pointer; 195 | border-radius: 15px; 196 | text-align: center; 197 | padding: 6px 0; 198 | font-weight: bold; 199 | transition-duration: .3s; 200 | text-align: center; 201 | } 202 | .chrome-search-pre svg, 203 | .chrome-search-next svg, 204 | .chrome-search-close svg{ 205 | vertical-align: top; 206 | } 207 | .chrome-search-pre svg, 208 | .chrome-search-next svg { 209 | transform: rotate(90deg); 210 | } 211 | .chrome-search-pre:hover, 212 | .chrome-search-next:hover, 213 | .chrome-search-close:hover{ 214 | background: #e3e3e3; 215 | } 216 | .chrome-search-pre:active, 217 | .chrome-search-next:active, 218 | .chrome-search-close:active{ 219 | background: #f7f5f5; 220 | } 221 | `, 222 | "electron-puppeteer-chrome-search" 223 | ) 224 | -------------------------------------------------------------------------------- /renderer/libs/chrome-tabs/chrome-tabs.css.js: -------------------------------------------------------------------------------- 1 | export default ` 2 | .chrome-tabs { 3 | box-sizing: border-box; 4 | position: relative; 5 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol"; 6 | font-size: 12px; 7 | height: 46px; 8 | padding: 8px 40px 4px 14px; 9 | background: #dee1e6; 10 | border-radius: 5px 5px 0 0; 11 | display: flex; 12 | overflow: hidden; 13 | } 14 | .chrome-tabs * { 15 | box-sizing: inherit; 16 | font: inherit; 17 | } 18 | .chrome-tabs .chrome-tabs-content { 19 | position: relative; 20 | height: 100%; 21 | flex: 1; 22 | } 23 | .chrome-tabs .chrome-tabs-ctrl { 24 | padding-left: 7px; 25 | padding-right: 4px; 26 | user-select: none; 27 | } 28 | .chrome-tabs .chrome-tabs-button-custom[active] { 29 | background: #fff; 30 | } 31 | .chrome-tabs .chrome-tabs-button-add { 32 | position: absolute; 33 | top: 10px; 34 | left: 0; 35 | margin-left: 3px; 36 | margin-right: 0; 37 | background: #f2f2f2; 38 | } 39 | .chrome-tabs .chrome-tabs-button { 40 | display: inline-block; 41 | width: 28px; 42 | line-height: 28px; 43 | margin-right: 2px; 44 | text-align: center; 45 | font-size: 14px; 46 | cursor: pointer; 47 | border-radius: 50%; 48 | transition: color 0.5s linear, background 0.5s linear; 49 | } 50 | .chrome-tabs .chrome-tabs-button[disabled] { 51 | color: #aaa; 52 | } 53 | .chrome-tabs .chrome-tabs-button:last-child { 54 | margin-right: 0; 55 | } 56 | .chrome-tabs .chrome-tabs-button svg { 57 | margin: 7px auto; 58 | vertical-align: top; 59 | } 60 | .chrome-tabs .chrome-tabs-button:not([disabled]):hover { 61 | color: #296aef; 62 | background: #fff; 63 | } 64 | .chrome-tabs .chrome-tab { 65 | position: absolute; 66 | left: 0; 67 | height: 36px; 68 | width: 240px; 69 | border: 0; 70 | margin: 0; 71 | z-index: 1; 72 | pointer-events: none; 73 | } 74 | .chrome-tabs .chrome-tab, 75 | .chrome-tabs .chrome-tab * { 76 | user-select: none; 77 | cursor: default; 78 | } 79 | .chrome-tabs .chrome-tab .chrome-tab-dividers { 80 | position: absolute; 81 | top: 7px; 82 | bottom: 7px; 83 | left: var(--tab-content-margin); 84 | right: var(--tab-content-margin); 85 | } 86 | .chrome-tabs .chrome-tab .chrome-tab-dividers, 87 | .chrome-tabs .chrome-tab .chrome-tab-dividers::before, 88 | .chrome-tabs .chrome-tab .chrome-tab-dividers::after { 89 | pointer-events: none; 90 | } 91 | .chrome-tabs .chrome-tab .chrome-tab-dividers::before, 92 | .chrome-tabs .chrome-tab .chrome-tab-dividers::after { 93 | content: ""; 94 | display: block; 95 | position: absolute; 96 | top: 0; 97 | bottom: 0; 98 | width: 1px; 99 | background: #a9adb0; 100 | opacity: 1; 101 | transition: opacity 0.2s ease; 102 | } 103 | .chrome-tabs .chrome-tab .chrome-tab-dividers::before { 104 | left: 0; 105 | } 106 | .chrome-tabs .chrome-tab .chrome-tab-dividers::after { 107 | right: 0; 108 | } 109 | .chrome-tabs .chrome-tab:first-child .chrome-tab-dividers::before, 110 | .chrome-tabs .chrome-tab:last-child .chrome-tab-dividers::after { 111 | opacity: 0; 112 | } 113 | .chrome-tabs .chrome-tab .chrome-tab-background { 114 | position: absolute; 115 | top: 0; 116 | left: 0; 117 | width: 100%; 118 | height: 100%; 119 | overflow: hidden; 120 | pointer-events: none; 121 | } 122 | .chrome-tabs .chrome-tab .chrome-tab-background > svg { 123 | width: 100%; 124 | height: 100%; 125 | } 126 | .chrome-tabs .chrome-tab .chrome-tab-background > svg .chrome-tab-geometry { 127 | fill: #f4f5f6; 128 | } 129 | .chrome-tabs .chrome-tab[active] { 130 | z-index: 5; 131 | } 132 | .chrome-tabs .chrome-tab[active] .chrome-tab-background > svg .chrome-tab-geometry { 133 | fill: #fff; 134 | } 135 | .chrome-tabs .chrome-tab:not([active]) .chrome-tab-background { 136 | transition: opacity 0.2s ease; 137 | opacity: 0; 138 | } 139 | @media (hover: hover) { 140 | .chrome-tabs .chrome-tab:not([active]):hover { 141 | z-index: 2; 142 | } 143 | .chrome-tabs .chrome-tab:not([active]):hover .chrome-tab-background { 144 | opacity: 1; 145 | } 146 | } 147 | .chrome-tabs .chrome-tab.chrome-tab-was-just-added { 148 | top: 10px; 149 | animation: chrome-tab-was-just-added 120ms forwards ease-in-out; 150 | } 151 | .chrome-tabs .chrome-tab .chrome-tab-content { 152 | position: absolute; 153 | display: flex; 154 | top: 0; 155 | bottom: 0; 156 | left: var(--tab-content-margin); 157 | right: var(--tab-content-margin); 158 | padding: 9px 8px; 159 | border-top-left-radius: 8px; 160 | border-top-right-radius: 8px; 161 | overflow: hidden; 162 | pointer-events: all; 163 | } 164 | .chrome-tabs .chrome-tab[is-mini] .chrome-tab-content { 165 | padding-left: 2px; 166 | padding-right: 2px; 167 | } 168 | .chrome-tabs .chrome-tab .chrome-tab-favicon { 169 | position: relative; 170 | flex-shrink: 0; 171 | flex-grow: 0; 172 | height: 16px; 173 | width: 16px; 174 | background-size: 16px; 175 | margin-left: 4px; 176 | } 177 | .chrome-tabs .chrome-tab[is-small] .chrome-tab-favicon { 178 | margin-left: 0; 179 | } 180 | .chrome-tabs .chrome-tab[is-mini]:not([active]) .chrome-tab-favicon { 181 | margin-left: auto; 182 | margin-right: auto; 183 | } 184 | .chrome-tabs .chrome-tab[is-mini][active] .chrome-tab-favicon { 185 | display: none; 186 | } 187 | .chrome-tabs .chrome-tab .chrome-tab-title { 188 | flex: 1; 189 | vertical-align: top; 190 | line-height: 16px; 191 | overflow: hidden; 192 | white-space: nowrap; 193 | margin-left: 4px; 194 | color: #5f6368; 195 | -webkit-mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 24px), transparent); 196 | mask-image: linear-gradient(90deg, #000 0%, #000 calc(100% - 24px), transparent); 197 | } 198 | .chrome-tabs .chrome-tab[is-small] .chrome-tab-title { 199 | margin-left: 0; 200 | } 201 | .chrome-tabs .chrome-tab .chrome-tab-favicon + .chrome-tab-title, 202 | .chrome-tabs .chrome-tab[is-small] .chrome-tab-favicon + .chrome-tab-title { 203 | margin-left: 8px; 204 | } 205 | .chrome-tabs .chrome-tab[is-smaller] .chrome-tab-favicon + .chrome-tab-title, 206 | .chrome-tabs .chrome-tab[is-mini] .chrome-tab-title { 207 | display: none; 208 | } 209 | .chrome-tabs .chrome-tab[active] .chrome-tab-title { 210 | color: #45474a; 211 | } 212 | .chrome-tabs .chrome-tab .chrome-tab-drag-handle { 213 | position: absolute; 214 | top: 0; 215 | bottom: 0; 216 | right: 0; 217 | left: 0; 218 | border-top-left-radius: 8px; 219 | border-top-right-radius: 8px; 220 | } 221 | .chrome-tabs .chrome-tab .chrome-tab-close { 222 | flex-grow: 0; 223 | flex-shrink: 0; 224 | position: relative; 225 | width: 16px; 226 | height: 16px; 227 | border-radius: 50%; 228 | background-image: url("data:image/svg+xml;utf8,"); 229 | background-position: center center; 230 | background-repeat: no-repeat; 231 | background-size: 8px 8px; 232 | } 233 | @media (hover: hover) { 234 | .chrome-tabs .chrome-tab .chrome-tab-close:hover { 235 | background-color: #e8eaed; 236 | } 237 | .chrome-tabs .chrome-tab .chrome-tab-close:hover:active { 238 | background-color: #dadce0; 239 | } 240 | } 241 | @media not all and (hover: hover) { 242 | .chrome-tabs .chrome-tab .chrome-tab-close:active { 243 | background-color: #dadce0; 244 | } 245 | } 246 | @media (hover: hover) { 247 | .chrome-tabs .chrome-tab:not([active]) .chrome-tab-close:not(:hover):not(:active) { 248 | opacity: 0.8; 249 | } 250 | } 251 | .chrome-tabs .chrome-tab[is-smaller] .chrome-tab-close { 252 | margin-left: auto; 253 | } 254 | .chrome-tabs .chrome-tab[is-mini]:not([active]) .chrome-tab-close { 255 | display: none; 256 | } 257 | .chrome-tabs .chrome-tab[is-mini][active] .chrome-tab-close { 258 | margin-left: auto; 259 | margin-right: auto; 260 | } 261 | @-moz-keyframes chrome-tab-was-just-added { 262 | to { 263 | top: 0; 264 | } 265 | } 266 | @-webkit-keyframes chrome-tab-was-just-added { 267 | to { 268 | top: 0; 269 | } 270 | } 271 | @-o-keyframes chrome-tab-was-just-added { 272 | to { 273 | top: 0; 274 | } 275 | } 276 | @keyframes chrome-tab-was-just-added { 277 | to { 278 | top: 0; 279 | } 280 | } 281 | .chrome-tabs.chrome-tabs-is-sorting .chrome-tab:not(.chrome-tab-is-dragging), 282 | .chrome-tabs:not(.chrome-tabs-is-sorting) .chrome-tab.chrome-tab-was-just-dragged { 283 | transition: transform 120ms ease-in-out; 284 | } 285 | .chrome-tabs .chrome-tabs-bottom-bar { 286 | position: absolute; 287 | bottom: 0; 288 | height: 4px; 289 | left: 0; 290 | width: 100%; 291 | background: #fff; 292 | z-index: 10; 293 | } 294 | .chrome-tabs-optional-shadow-below-bottom-bar { 295 | position: relative; 296 | height: 1px; 297 | width: 100%; 298 | background-image: url("data:image/svg+xml;utf8,"); 299 | background-size: 1px 1px; 300 | background-repeat: repeat-x; 301 | background-position: 0% 0%; 302 | } 303 | @media only screen and (-webkit-min-device-pixel-ratio: 2), only screen and (min--moz-device-pixel-ratio: 2), only screen and (-o-min-device-pixel-ratio: 2/1), only screen and (min-device-pixel-ratio: 2), only screen and (min-resolution: 192dpi), only screen and (min-resolution: 2dppx) { 304 | .chrome-tabs-optional-shadow-below-bottom-bar { 305 | background-image: url("data:image/svg+xml;utf8,"); 306 | } 307 | } 308 | ` 309 | -------------------------------------------------------------------------------- /renderer/libs/chrome-tabs/chrome-tabs.js: -------------------------------------------------------------------------------- 1 | import {isElementInViewport} from "../../util.js" 2 | 3 | const Draggabilly = require("draggabilly") 4 | const TAB_ADD_BUTTON_FIXSIZE = 10 5 | const TAB_CONTENT_MARGIN = 9 6 | const TAB_CONTENT_OVERLAP_DISTANCE = 1 7 | 8 | // const TAB_OVERLAP_DISTANCE = 9 | // TAB_CONTENT_MARGIN * 2 + TAB_CONTENT_OVERLAP_DISTANCE; 10 | 11 | const TAB_CONTENT_MIN_WIDTH = 24 12 | const TAB_CONTENT_MAX_WIDTH = 240 13 | 14 | const TAB_SIZE_SMALL = 84 15 | const TAB_SIZE_SMALLER = 60 16 | const TAB_SIZE_MINI = 48 17 | 18 | const noop = () => {} 19 | 20 | const closest = (value, array) => { 21 | let closest = Infinity 22 | let closestIndex = -1 23 | 24 | array.forEach((v, i) => { 25 | if (Math.abs(value - v) < closest) { 26 | closest = Math.abs(value - v) 27 | closestIndex = i 28 | } 29 | }) 30 | 31 | return closestIndex 32 | } 33 | 34 | const tabTemplate = ` 35 |
36 |
37 |
38 | 39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | ` 48 | 49 | const defaultTapProperties = { 50 | title: "New tab", 51 | favicon: false, 52 | } 53 | 54 | let instanceId = 0 55 | 56 | class ChromeTabs { 57 | constructor() { 58 | this.draggabillies = [] 59 | } 60 | 61 | init(el) { 62 | this.el = el 63 | 64 | this.instanceId = instanceId 65 | this.el.setAttribute("data-chrome-tabs-instance-id", this.instanceId) 66 | instanceId += 1 67 | 68 | this.setupCustomProperties() 69 | this.setupStyleEl() 70 | this.setupEvents() 71 | this.layoutTabs() 72 | this.setupDraggabilly() 73 | } 74 | 75 | emit(eventName, data) { 76 | this.el.dispatchEvent(new CustomEvent(eventName, {detail: data})) 77 | } 78 | 79 | setupCustomProperties() { 80 | this.el.style.setProperty("--tab-content-margin", `${TAB_CONTENT_MARGIN}px`) 81 | } 82 | 83 | setupStyleEl() { 84 | this.styleEl = document.createElement("style") 85 | this.el.appendChild(this.styleEl) 86 | } 87 | 88 | setupEvents() { 89 | window.addEventListener("resize", () => { 90 | if (isElementInViewport(this.el)) { 91 | this.cleanUpPreviouslyDraggedTabs() 92 | this.layoutTabs() 93 | } 94 | }) 95 | 96 | // this.el.addEventListener("dblclick", (event) => { 97 | // if ([this.el, this.tabContentEl].includes(event.target)) { 98 | // this.emit("tabBlank"); 99 | // } 100 | // }); 101 | 102 | // this.tabEls.forEach((tabEl) => this.setTabCloseEventListener(tabEl)) 103 | } 104 | 105 | get tabEls() { 106 | return Array.prototype.slice.call(this.el.querySelectorAll(".chrome-tab")) 107 | } 108 | 109 | get tabContentEl() { 110 | return this.el.querySelector(".chrome-tabs-content") 111 | } 112 | 113 | get tabButtonAddEl() { 114 | return this.el.querySelector(".chrome-tabs-button-add") 115 | } 116 | 117 | get tabCtrlEl() { 118 | return this.el.querySelector(".chrome-tabs-ctrl") 119 | } 120 | 121 | get tabContentWidths() { 122 | const numberOfTabs = this.tabEls.length 123 | const tabsContentWidth = this.tabContentEl.clientWidth 124 | const tabsCumulativeOverlappedWidth = 125 | (numberOfTabs - 1) * TAB_CONTENT_OVERLAP_DISTANCE 126 | const targetWidth = 127 | (tabsContentWidth - 128 | 2 * TAB_CONTENT_MARGIN + 129 | tabsCumulativeOverlappedWidth) / 130 | numberOfTabs 131 | const clampedTargetWidth = Math.max( 132 | TAB_CONTENT_MIN_WIDTH, 133 | Math.min(TAB_CONTENT_MAX_WIDTH, targetWidth) 134 | ) 135 | const flooredClampedTargetWidth = Math.floor(clampedTargetWidth) 136 | const totalTabsWidthUsingTarget = 137 | flooredClampedTargetWidth * numberOfTabs + 138 | 2 * TAB_CONTENT_MARGIN - 139 | tabsCumulativeOverlappedWidth 140 | const totalExtraWidthDueToFlooring = 141 | tabsContentWidth - totalTabsWidthUsingTarget 142 | 143 | // TODO - Support tabs with different widths / e.g. "pinned" tabs 144 | const widths = [] 145 | let extraWidthRemaining = totalExtraWidthDueToFlooring 146 | for (let i = 0; i < numberOfTabs; i += 1) { 147 | const extraWidth = 148 | flooredClampedTargetWidth < TAB_CONTENT_MAX_WIDTH && 149 | extraWidthRemaining > 0 150 | ? 1 151 | : 0 152 | widths.push(flooredClampedTargetWidth + extraWidth) 153 | if (extraWidthRemaining > 0) extraWidthRemaining -= 1 154 | } 155 | 156 | return widths 157 | } 158 | 159 | get tabContentPositions() { 160 | const positions = [] 161 | const tabContentWidths = this.tabContentWidths 162 | 163 | let position = TAB_CONTENT_MARGIN 164 | tabContentWidths.forEach((width, i) => { 165 | const offset = i * TAB_CONTENT_OVERLAP_DISTANCE 166 | positions.push(position - offset) 167 | position += width 168 | }) 169 | 170 | return positions 171 | } 172 | 173 | get tabPositions() { 174 | const positions = [] 175 | 176 | this.tabContentPositions.forEach((contentPosition) => { 177 | positions.push(contentPosition - TAB_CONTENT_MARGIN) 178 | }) 179 | 180 | return positions 181 | } 182 | 183 | layoutTabs() { 184 | const tabContentWidths = this.tabContentWidths 185 | let tabContentWidthsCount = 0 186 | 187 | this.tabEls.forEach((tabEl, i) => { 188 | const contentWidth = tabContentWidths[i] 189 | const width = contentWidth + 2 * TAB_CONTENT_MARGIN 190 | tabContentWidthsCount += contentWidth 191 | 192 | tabEl.style.width = width + "px" 193 | tabEl.removeAttribute("is-small") 194 | tabEl.removeAttribute("is-smaller") 195 | tabEl.removeAttribute("is-mini") 196 | 197 | if (contentWidth < TAB_SIZE_SMALL) tabEl.setAttribute("is-small", "") 198 | if (contentWidth < TAB_SIZE_SMALLER) tabEl.setAttribute("is-smaller", "") 199 | if (contentWidth < TAB_SIZE_MINI) tabEl.setAttribute("is-mini", "") 200 | }) 201 | 202 | let styleHTML = "" 203 | this.tabPositions.forEach((position, i) => { 204 | styleHTML += ` 205 | .chrome-tabs[data-chrome-tabs-instance-id="${ 206 | this.instanceId 207 | }"] .chrome-tab:nth-child(${i + 1}) { 208 | transform: translate3d(${position}px, 0, 0) 209 | } 210 | ` 211 | }) 212 | 213 | let tabButtonAddElLeft = 214 | tabContentWidthsCount + TAB_CONTENT_MARGIN * 2 + TAB_ADD_BUTTON_FIXSIZE 215 | if ( 216 | tabButtonAddElLeft > 217 | this.tabContentEl.clientWidth + TAB_ADD_BUTTON_FIXSIZE 218 | ) { 219 | tabButtonAddElLeft = 220 | this.tabContentEl.clientWidth + TAB_ADD_BUTTON_FIXSIZE 221 | } 222 | this.tabButtonAddEl.style.left = 223 | this.tabCtrlEl.clientWidth + tabButtonAddElLeft + "px" 224 | this.styleEl.innerHTML = styleHTML 225 | } 226 | 227 | createNewTabEl() { 228 | const div = document.createElement("div") 229 | div.innerHTML = tabTemplate 230 | return div.firstElementChild 231 | } 232 | 233 | addTab(tabProperties, {animate = true, background = false} = {}) { 234 | const tabEl = this.createNewTabEl() 235 | 236 | if (animate) { 237 | tabEl.classList.add("chrome-tab-was-just-added") 238 | setTimeout(() => tabEl.classList.remove("chrome-tab-was-just-added"), 500) 239 | } 240 | 241 | tabProperties = Object.assign({}, defaultTapProperties, tabProperties) 242 | this.tabContentEl.appendChild(tabEl) 243 | this.setTabCloseEventListener(tabEl) 244 | this.updateTab(tabEl, tabProperties) 245 | this.emit("tabAdd", {tabEl, ...tabProperties}) 246 | if (!background) this.setCurrentTab(tabEl) 247 | this.cleanUpPreviouslyDraggedTabs() 248 | this.layoutTabs() 249 | this.setupDraggabilly() 250 | 251 | return tabEl 252 | } 253 | 254 | setTabCloseEventListener(tabEl) { 255 | tabEl 256 | .querySelector(".chrome-tab-close") 257 | .addEventListener("click", () => this.removeTab(tabEl)) 258 | } 259 | 260 | get activeTabEl() { 261 | return this.el.querySelector(".chrome-tab[active]") 262 | } 263 | 264 | hasActiveTab() { 265 | return !!this.activeTabEl 266 | } 267 | 268 | setCurrentTab(tabEl, trigger) { 269 | const activeTabEl = this.activeTabEl 270 | if (activeTabEl === tabEl) return 271 | if (activeTabEl) activeTabEl.removeAttribute("active") 272 | tabEl.setAttribute("active", "") 273 | this.emit("activeTabChange", {tabEl, trigger}) 274 | } 275 | 276 | removeTab(tabEl, trigger) { 277 | if (tabEl === this.activeTabEl) { 278 | if (tabEl.nextElementSibling) { 279 | this.setCurrentTab(tabEl.nextElementSibling) 280 | } else if (tabEl.previousElementSibling) { 281 | this.setCurrentTab(tabEl.previousElementSibling) 282 | } 283 | } 284 | tabEl.parentNode.removeChild(tabEl) 285 | this.emit("tabRemove", {tabEl, trigger}) 286 | this.cleanUpPreviouslyDraggedTabs() 287 | this.layoutTabs() 288 | this.setupDraggabilly() 289 | } 290 | 291 | updateTab(tabEl, tabProperties) { 292 | if (tabProperties.title) { 293 | let titleEl = tabEl.querySelector(".chrome-tab-title") 294 | titleEl.textContent = tabProperties.title 295 | titleEl.parentNode.title = `${tabProperties.title}\n${tabProperties.url}` 296 | } 297 | 298 | const faviconEl = tabEl.querySelector(".chrome-tab-favicon") 299 | if (tabProperties.favicon) { 300 | faviconEl.style.backgroundImage = `url('${tabProperties.favicon}')` 301 | faviconEl.removeAttribute("hidden", "") 302 | } /* else { 303 | faviconEl.setAttribute('hidden', '') 304 | faviconEl.removeAttribute('style') 305 | } */ 306 | 307 | if (tabProperties.id) { 308 | tabEl.setAttribute("data-tab-id", tabProperties.id) 309 | } 310 | } 311 | 312 | cleanUpPreviouslyDraggedTabs() { 313 | this.tabEls.forEach((tabEl) => 314 | tabEl.classList.remove("chrome-tab-was-just-dragged") 315 | ) 316 | } 317 | 318 | setupDraggabilly() { 319 | const tabEls = this.tabEls 320 | const tabPositions = this.tabPositions 321 | 322 | if (this.isDragging) { 323 | this.isDragging = false 324 | this.el.classList.remove("chrome-tabs-is-sorting") 325 | this.draggabillyDragging.element.classList.remove( 326 | "chrome-tab-is-dragging" 327 | ) 328 | this.draggabillyDragging.element.style.transform = "" 329 | this.draggabillyDragging.dragEnd() 330 | this.draggabillyDragging.isDragging = false 331 | this.draggabillyDragging.positionDrag = noop // Prevent Draggabilly from updating tabEl.style.transform in later frames 332 | this.draggabillyDragging.destroy() 333 | this.draggabillyDragging = null 334 | } 335 | 336 | this.draggabillies.forEach((d) => d.destroy()) 337 | 338 | tabEls.forEach((tabEl, originalIndex) => { 339 | const originalTabPositionX = tabPositions[originalIndex] 340 | const draggabilly = new Draggabilly(tabEl, { 341 | axis: "x", 342 | handle: ".chrome-tab-drag-handle", 343 | containment: this.tabContentEl, 344 | }) 345 | 346 | this.draggabillies.push(draggabilly) 347 | 348 | draggabilly.on("pointerDown", () => { 349 | this.setCurrentTab(tabEl) 350 | }) 351 | 352 | draggabilly.on("dragStart", () => { 353 | this.isDragging = true 354 | this.draggabillyDragging = draggabilly 355 | tabEl.classList.add("chrome-tab-is-dragging") 356 | this.el.classList.add("chrome-tabs-is-sorting") 357 | }) 358 | 359 | draggabilly.on("dragEnd", () => { 360 | this.isDragging = false 361 | const finalTranslateX = parseFloat(tabEl.style.left, 10) 362 | tabEl.style.transform = `translate3d(0, 0, 0)` 363 | 364 | // Animate dragged tab back into its place 365 | requestAnimationFrame(() => { 366 | tabEl.style.left = "0" 367 | tabEl.style.transform = `translate3d(${finalTranslateX}px, 0, 0)` 368 | 369 | requestAnimationFrame(() => { 370 | tabEl.classList.remove("chrome-tab-is-dragging") 371 | this.el.classList.remove("chrome-tabs-is-sorting") 372 | 373 | tabEl.classList.add("chrome-tab-was-just-dragged") 374 | 375 | requestAnimationFrame(() => { 376 | tabEl.style.transform = "" 377 | 378 | this.layoutTabs() 379 | this.setupDraggabilly() 380 | }) 381 | }) 382 | }) 383 | }) 384 | 385 | draggabilly.on("dragMove", (event, pointer, moveVector) => { 386 | // Current index be computed within the event since it can change during the dragMove 387 | const tabEls = this.tabEls 388 | const currentIndex = tabEls.indexOf(tabEl) 389 | 390 | const currentTabPositionX = originalTabPositionX + moveVector.x 391 | const destinationIndexTarget = closest( 392 | currentTabPositionX, 393 | tabPositions 394 | ) 395 | const destinationIndex = Math.max( 396 | 0, 397 | Math.min(tabEls.length, destinationIndexTarget) 398 | ) 399 | 400 | if (currentIndex !== destinationIndex) { 401 | this.animateTabMove(tabEl, currentIndex, destinationIndex) 402 | } 403 | }) 404 | }) 405 | } 406 | 407 | animateTabMove(tabEl, originIndex, destinationIndex) { 408 | if (destinationIndex < originIndex) { 409 | tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex]) 410 | } else { 411 | tabEl.parentNode.insertBefore(tabEl, this.tabEls[destinationIndex + 1]) 412 | } 413 | this.emit("tabReorder", {tabEl, originIndex, destinationIndex}) 414 | this.layoutTabs() 415 | } 416 | } 417 | 418 | export default ChromeTabs 419 | -------------------------------------------------------------------------------- /renderer/libs/chrome-zoom.js: -------------------------------------------------------------------------------- 1 | import {parseStrToDOM, importStyle} from "../util" 2 | 3 | // 缩放刻度 4 | const zoomOutFactors = [90, 80, 75, 67, 50, 33, 25] 5 | const zoomInFactors = [110, 125, 150, 175, 200, 250, 300, 400, 500] 6 | 7 | export default class ChromeZoom { 8 | constructor(page) { 9 | this.page = page 10 | this.webview = page._getWebview() 11 | 12 | this.doms = {} 13 | this.value = "" 14 | this.zoomFactorIndex = 0 15 | this.created = false 16 | this.hideTimer = null 17 | 18 | this.page.once("dom-ready", () => { 19 | let webContents = this.page.getWebContents() 20 | webContents.on("zoom-changed", (_, zoomDirection) => { 21 | switch (zoomDirection) { 22 | case "in": 23 | this.zoomIn() 24 | break 25 | case "out": 26 | this.zoomOut() 27 | break 28 | } 29 | }) 30 | }) 31 | } 32 | create() { 33 | let container = parseStrToDOM(` 34 |
35 | 100% 36 | ${icon.out} 37 | ${icon.in} 38 | 重置 39 |
40 | `) 41 | 42 | let doms = (this.doms = { 43 | container, 44 | text: container.querySelector(".chrome-zoom-text"), 45 | out: container.querySelector(".chrome-zoom-out"), 46 | in: container.querySelector(".chrome-zoom-in"), 47 | reset: container.querySelector(".chrome-zoom-reset"), 48 | }) 49 | 50 | doms.out.addEventListener("click", this.zoomOut.bind(this), false) 51 | doms.in.addEventListener("click", this.zoomIn.bind(this), false) 52 | doms.reset.addEventListener("click", this.zoomReset.bind(this), false) 53 | this.page._getDOM().appendChild(container) 54 | } 55 | zoomOut() { 56 | if (zoomOutFactors.length + this.zoomFactorIndex <= 0) { 57 | return 58 | } 59 | 60 | this.zoomFactorIndex -= 1 61 | this.zoom() 62 | } 63 | zoomIn() { 64 | if (zoomOutFactors.length - this.zoomFactorIndex + 1 < 0) { 65 | return 66 | } 67 | 68 | this.zoomFactorIndex += 1 69 | this.zoom() 70 | } 71 | zoomReset() { 72 | this.zoomFactorIndex = 0 73 | this.zoom() 74 | } 75 | zoom() { 76 | this.zoomFactorIndex = Math.min( 77 | Math.max(-zoomOutFactors.length, parseInt(this.zoomFactorIndex, 10)), 78 | zoomInFactors.length 79 | ) 80 | let zoomFactor = 100 81 | if (this.zoomFactorIndex < 0) { 82 | zoomFactor = zoomOutFactors[Math.abs(this.zoomFactorIndex) - 1] 83 | } else if (this.zoomFactorIndex > 0) { 84 | zoomFactor = zoomInFactors[this.zoomFactorIndex - 1] 85 | } 86 | 87 | if (this.webview) { 88 | this.webview.setZoomFactor(zoomFactor / 100) 89 | } 90 | this.show() 91 | this.doms.text.textContent = zoomFactor + "%" 92 | } 93 | show() { 94 | if (this.hideTimer) { 95 | clearTimeout(this.hideTimer) 96 | } 97 | this.hideTimer = setTimeout(() => { 98 | this.hide() 99 | }, 2e3) 100 | 101 | if (!this.created) { 102 | this.created = true 103 | this.create() 104 | } 105 | } 106 | hide() { 107 | this.destroy() 108 | this.created = false 109 | } 110 | destroy() { 111 | let doms = this.doms 112 | doms.container.remove() 113 | } 114 | } 115 | 116 | const icon = { 117 | out: ``, 118 | in: ``, 119 | } 120 | 121 | importStyle( 122 | ` 123 | .chrome-zoom { 124 | position: absolute; 125 | display: flex; 126 | top: 0; 127 | right: 30px; 128 | width: 256px; 129 | height: 42px; 130 | line-height: 30px; 131 | background: #fff; 132 | padding: 5px 15px; 133 | user-select:none; 134 | box-shadow: -3px 1px 4px #e3e3e3; 135 | border: 1px solid #e3e3e3; 136 | font-size: 12px; 137 | color: #000; 138 | } 139 | .chrome-zoom-text { 140 | width: 100px; 141 | text-align: left; 142 | } 143 | .chrome-zoom-in, 144 | .chrome-zoom-out { 145 | margin-top: 3px; 146 | margin-left: 5px; 147 | width: 24px; 148 | height: 24px; 149 | cursor: pointer; 150 | border-radius: 15px; 151 | text-align: center; 152 | padding: 6px 0; 153 | font-weight: bold; 154 | transition-duration: .3s; 155 | text-align: center; 156 | } 157 | .chrome-zoom-in svg, 158 | .chrome-zoom-out svg { 159 | vertical-align: top; 160 | } 161 | .chrome-zoom-in:hover, 162 | .chrome-zoom-out:hover{ 163 | background: #e3e3e3; 164 | } 165 | .chrome-zoom-in:active, 166 | .chrome-zoom-out:active { 167 | background: #f7f5f5; 168 | } 169 | .chrome-zoom-reset { 170 | color: rgb(26, 115, 232); 171 | height: 30px; 172 | line-height: 28px; 173 | width: 64px; 174 | text-align: center; 175 | border: 1px solid rgb(218, 220, 224); 176 | border-radius: 2px; 177 | margin-left: 15px; 178 | cursor: pointer; 179 | } 180 | `, 181 | "electron-puppeteer-chrome-zoom" 182 | ) 183 | -------------------------------------------------------------------------------- /renderer/libs/localshortcutswrap.js: -------------------------------------------------------------------------------- 1 | const {remote} = require("electron") 2 | const currentWindow = remote.getCurrentWindow() 3 | const localshortcut = require("@replace5/electron-localshortcut") 4 | 5 | const shortcusMap = new Map() 6 | 7 | /** 8 | * 事件对象 9 | * @param opt 10 | * @prop {boolean} opt.only 如果为true 绑定key的事件中仅执行该事件 11 | */ 12 | function createEvenet(key, callback, opt) { 13 | return { 14 | key, 15 | cb: callback, 16 | only: opt.only, 17 | ctx: opt.ctx, 18 | } 19 | } 20 | 21 | /** 22 | * 事件执行规则 23 | * @param {*} events 24 | */ 25 | function exec(events) { 26 | // only : 绑定key的事件中仅执行该事件 27 | let event = events.find((e) => e.only) 28 | if (event) { 29 | event.cb.call(event.ctx) 30 | return 31 | } 32 | 33 | events.forEach(({cb, ctx}) => cb.call(ctx)) 34 | } 35 | 36 | export function register(key, callback, opt) { 37 | let event = createEvenet(key, callback, opt) 38 | if (shortcusMap.has(key)) { 39 | const events = shortcusMap.get(key) 40 | events.push(event) 41 | } else { 42 | shortcusMap.set(key, [event]) 43 | 44 | let onShortcutEvent = () => { 45 | exec(shortcusMap.get(key)) 46 | } 47 | localshortcut.register(currentWindow, key, onShortcutEvent) 48 | } 49 | } 50 | 51 | export function unregister(key, callback) { 52 | let events = shortcusMap.get(key) 53 | if (callback) { 54 | events.forEach((event, index, events) => { 55 | if (event.cb === callback) { 56 | events.splice(index, 1) 57 | } 58 | }) 59 | // 事件为空,删除 60 | if (!events.length) { 61 | shortcusMap.delete(key) 62 | } 63 | } else { 64 | w 65 | shortcusMap.delete(key) 66 | localshortcut.unregister(currentWindow, key) 67 | } 68 | } 69 | 70 | export function unregisterAll() { 71 | localshortcut.unregisterAll() 72 | shortcusMap.clear() 73 | } 74 | -------------------------------------------------------------------------------- /renderer/style.css.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file css to js 3 | */ 4 | 5 | export default ` 6 | .electron-puppeteer-browser { 7 | position: absolute; 8 | width: 100%; 9 | height: 100%; 10 | left: 0; 11 | top: 0; 12 | z-index: 1; 13 | background: #fff; 14 | } 15 | .electron-puppeteer-pages { 16 | position: relative; 17 | flex: 1; 18 | height: calc(100% - 46px); 19 | } 20 | .electron-puppeteer-page { 21 | position: absolute; 22 | left: 0; 23 | top: 0; 24 | width: 100%; 25 | height: 100%; 26 | display: none; 27 | } 28 | .electron-puppeteer-page webview { 29 | width: 100%; 30 | height: 100%; 31 | } 32 | .chrome-tabs { 33 | -webkit-app-region: drag; 34 | } 35 | .chrome-tab, 36 | .chrome-tabs-button { 37 | -webkit-app-region: no-drag; 38 | } 39 | ` 40 | -------------------------------------------------------------------------------- /renderer/util.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @file util 3 | */ 4 | export function isString(a) { 5 | return Object.prototype.toString.call(a) === "[object String]" 6 | } 7 | 8 | export function isFunction(a) { 9 | return Object.prototype.toString.call(a) === "[object Function]" 10 | } 11 | 12 | export function isArray(a) { 13 | return Object.prototype.toString.call(a) === "[object Array]" 14 | } 15 | 16 | export function isObject(a) { 17 | return Object.prototype.toString.call(a) === "[object Object]" 18 | } 19 | 20 | export function isRegExp(a) { 21 | return Object.prototype.toString.call(a) === "[object RegExp]" 22 | } 23 | 24 | export function uniqueId(prefix) { 25 | return ( 26 | (prefix || "") + 27 | Math.random().toString(32).substr(1) + 28 | Date.now().toString(32) 29 | ) 30 | } 31 | 32 | export function sleep(time) { 33 | return new Promise((resolve) => { 34 | setTimeout(() => { 35 | resolve(true) 36 | }, time) 37 | }) 38 | } 39 | 40 | export function loggerDecorator(target, name, descriptor) { 41 | var oldValue = descriptor.value 42 | 43 | if ( 44 | typeof descriptor.value === "function" && 45 | process.env.NODE_ENV === "development" 46 | ) { 47 | descriptor.value = function () { 48 | var args = [].slice.call(arguments) 49 | 50 | // hidden pwd, password 51 | args = args.map(function (item) { 52 | var keys = (item && Object.keys(item)) || [] 53 | if (keys.indexOf("pwd") > -1 || keys.indexOf("password") > -1) { 54 | return Object.assign({}, item, {pwd: "******"}) 55 | } 56 | }) 57 | 58 | console.info( 59 | `Calling ${target.constructor.name}.${name}: ${target} ,with: `, 60 | ...args 61 | ) 62 | 63 | var ret = oldValue.apply(this, arguments) 64 | 65 | if (ret && ret.then) { 66 | ret.then((ret) => { 67 | console.info( 68 | `Return ${target.constructor.name}.${name}: ${target} ,with`, 69 | ret 70 | ) 71 | }) 72 | } else { 73 | console.info( 74 | `Return ${target.constructor.name}.${name}: ${target} ,with`, 75 | ret 76 | ) 77 | } 78 | 79 | return ret 80 | } 81 | } 82 | 83 | return descriptor 84 | } 85 | 86 | // 拓展指定类的原型方法,方法涞源于自己的其它函数的返回值,如page的goto方法来源于page.mainFrame() 87 | export function proxyBindDecorator(proxyMethods, proxierGetFn) { 88 | return function (target) { 89 | proxyMethods.forEach((methodName) => { 90 | target.prototype[methodName] = function () { 91 | var proxier = proxierGetFn.call(this) 92 | return proxier[methodName].apply(proxier, arguments) 93 | } 94 | }) 95 | 96 | return target 97 | } 98 | } 99 | 100 | // 插入style节点 101 | export function importStyle(styleContent, id) { 102 | if (id && document.getElementById(id)) { 103 | return 104 | } 105 | 106 | var style = document.createElement("style") 107 | style.type = "text/css" 108 | if (id) { 109 | style.id = id 110 | } 111 | document.getElementsByTagName("head")[0].appendChild(style) 112 | if (style.styleSheet) { 113 | style.styleSheet.cssText = styleContent 114 | } else { 115 | style.appendChild(document.createTextNode(styleContent)) 116 | } 117 | } 118 | 119 | // 将dom的position修改为可相对定位的节点 120 | export function setDomAsOffsetParent(dom) { 121 | var style = window.getComputedStyle(dom, null) 122 | var set = new Set("absolute", "fixed", "relative", "sticky") 123 | if (!set.has(style.position)) { 124 | dom.style.position = "relative" 125 | } 126 | } 127 | 128 | export function TimeoutPromise(fn, timeout) { 129 | return new Promise(function (resolve, reject) { 130 | var timer 131 | var ctx = { 132 | isTimeout: false, 133 | timeoutCallback: null, 134 | rejectCallback: null, 135 | } 136 | if (timeout) { 137 | timer = setTimeout(function () { 138 | ctx.isTimeout = true 139 | if (ctx.timeoutCallback) { 140 | ctx.timeoutCallback() 141 | } 142 | reject("promise.timeout") 143 | if (ctx.rejectCallback) { 144 | ctx.rejectCallback() 145 | } 146 | }, timeout) 147 | } 148 | 149 | function rewriteResolve(data) { 150 | if (ctx.isTimeout) { 151 | return 152 | } 153 | 154 | if (timer) { 155 | clearTimeout(timer) 156 | } 157 | 158 | resolve(data) 159 | } 160 | 161 | function rewriteReject(err) { 162 | if (ctx.isTimeout) { 163 | return 164 | } 165 | 166 | if (timer) { 167 | clearTimeout(timer) 168 | } 169 | 170 | reject(err) 171 | if (ctx.rejectCallback) { 172 | ctx.rejectCallback() 173 | } 174 | } 175 | 176 | fn.call(this, rewriteResolve, rewriteReject, ctx) 177 | }) 178 | } 179 | 180 | export function parseStrToDOM(str) { 181 | let tmp = document.createElement("div") 182 | tmp.innerHTML = str.trim() 183 | const childNodes = tmp.childNodes 184 | return childNodes.length === 1 ? childNodes[0] : childNodes 185 | } 186 | 187 | export function debounce(fn, wait = 1e3) { 188 | let timeoutID 189 | return function (...argv) { 190 | if (timeoutID) { 191 | clearTimeout(timeoutID) 192 | } 193 | timeoutID = setTimeout(() => { 194 | fn.apply(this, argv) 195 | }, wait) 196 | } 197 | } 198 | 199 | export function isElementInViewport(el) { 200 | var rect = el.getBoundingClientRect() 201 | 202 | return ( 203 | rect.top >= 0 && 204 | rect.left >= 0 && 205 | rect.bottom <= 206 | (window.innerHeight || document.documentElement.clientHeight) && 207 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) && 208 | rect.width > 0 && 209 | rect.height > 0 210 | ) 211 | } 212 | 213 | export default { 214 | uniqueId, 215 | loggerDecorator, 216 | proxyBindDecorator, 217 | importStyle, 218 | TimeoutPromise, 219 | parseStrToDOM, 220 | debounce, 221 | isElementInViewport, 222 | } 223 | -------------------------------------------------------------------------------- /renderer_lib/BrowserManager.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _Browser = require("./Browser.js"); 8 | 9 | var _Browser2 = _interopRequireDefault(_Browser); 10 | 11 | var _KeyboardShortcuts = require("./KeyboardShortcuts.js"); 12 | 13 | var _KeyboardShortcuts2 = _interopRequireDefault(_KeyboardShortcuts); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | /** 18 | * @class BrowserManager 19 | */ 20 | /** 21 | * @file BrowserManager类 22 | */ 23 | class BrowserManager { 24 | /** 25 | * @constructor BrowserManager 26 | * 27 | */ 28 | constructor() { 29 | this._browsers = new Map(); 30 | // 快捷键 31 | new _KeyboardShortcuts2.default(this); 32 | } 33 | /** 34 | * 打开浏览器 35 | * @param {*} options 见Browser的options配置 36 | */ 37 | launch(options) { 38 | const browser = new _Browser2.default(this, options); 39 | return browser.init().then(() => (this._browsers.set(browser.id, browser), browser)); 40 | } 41 | get size() { 42 | return this._browsers.size; 43 | } 44 | /** 45 | * 获取最早打开的browser实例 46 | */ 47 | getEarliest() { 48 | let browsers = []; 49 | this._browsers.forEach(item => { 50 | browsers.push(item); 51 | }); 52 | 53 | return browsers.sort((a, b) => a.startTime - b.startTime).shift(); 54 | } 55 | /** 56 | * 通过browserId获取browser实例 57 | * @param {string} browserId browser.id 58 | */ 59 | get(browserId) { 60 | return this._browsers.get(browserId); 61 | } 62 | /** 63 | * 获取当前最视窗最前端的browser实例,也就是激活的browser实例 64 | */ 65 | frontBrowser() { 66 | let front = null; 67 | this._browsers.forEach(item => { 68 | if (item.isFront === true) { 69 | front = item; 70 | } 71 | }); 72 | 73 | return front; 74 | } 75 | frontPage() { 76 | let browser = this.frontBrowser(); 77 | return browser && browser.frontPage(); 78 | } 79 | /** 80 | * 删除browser,不可直接调用 81 | * 如需要关闭browser,请调用browser.close() 82 | * @private 83 | * @param {string} browserId 84 | */ 85 | _removeBrowser(browserId) { 86 | this._browsers.delete(browserId); 87 | } 88 | /** 89 | * 激活browser,不可直接调用 90 | * 如需要激活页面,请调用browser.bringToFront() 91 | * @private 92 | * @param {string} pageId 93 | */ 94 | _bringBrowserToFront(browserId) { 95 | this._browsers.forEach(browser => { 96 | if (browserId === browser.id) { 97 | browser._doFront(); 98 | } else { 99 | browser._doBack(); 100 | } 101 | }); 102 | } 103 | } 104 | exports.default = BrowserManager; -------------------------------------------------------------------------------- /renderer_lib/ElementHandle.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _EventEmitter = require("./EventEmitter.js"); 8 | 9 | var _EventEmitter2 = _interopRequireDefault(_EventEmitter); 10 | 11 | var _USKeyboardLayout = require("./USKeyboardLayout.js"); 12 | 13 | var _USKeyboardLayout2 = _interopRequireDefault(_USKeyboardLayout); 14 | 15 | var _util = require("./util.js"); 16 | 17 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 18 | 19 | function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } /** 20 | * @file ElementHandle类 21 | */ 22 | 23 | /** 24 | * @class ElementHandle frame的dom操作句柄 25 | * @extends EventEmitter 26 | * 27 | * @property {Frame} frame 所属frame实例 28 | * @property {string} selector 选择器 29 | * @property {string} baseSelector 基础选择器,先查找baseSelector,再在baseSelector的基础上查找当前节点 30 | * 31 | */ 32 | class ElementHandle extends _EventEmitter2.default { 33 | /** 34 | * @constructor ElementHandle 35 | * 36 | * @param {Frame} frame 所属frame实例 37 | * @param {string} selector 选择器 38 | * @param {string} baseSelector 基础选择器,先查找baseSelector,再在baseSelector的基础上查找当前节点 39 | * 40 | */ 41 | constructor(frame, selector, baseSelector) { 42 | super(); 43 | 44 | this.id = (0, _util.uniqueId)("ElementHandle_"); 45 | this.ipc = frame.ipc; 46 | this.frame = frame; 47 | this.selector = selector; 48 | this.baseSelector = baseSelector; 49 | } 50 | _joinSelfSelector() { 51 | var baseSelector = this.baseSelector.slice(0); 52 | baseSelector.push(this.selector); 53 | return baseSelector; 54 | } 55 | _joinSelector(selector, baseSelector) { 56 | baseSelector = baseSelector.slice(0); 57 | baseSelector.push(selector); 58 | return baseSelector; 59 | } 60 | /** 61 | * 基于当前节点,查询新的单个节点, 对应elm.querySelector(selector) 62 | * @param {string} selector 63 | * 64 | * @return {ElementHandle} 65 | */ 66 | $(selector) { 67 | return new ElementHandle(this.frame, selector, this._joinSelfSelector()); 68 | } 69 | /** 70 | * 基于当前节点,查询新的节点集合, 对应elm.querySelectorAll(selector) 71 | * @param {string} selector 72 | * 73 | * @return {Promise} 74 | */ 75 | $$(selector) { 76 | return this.ipc.send("elementHandle.$$", { 77 | selector: selector, 78 | baseSelector: this._joinSelfSelector() 79 | }).then(length => { 80 | return new Array(length).map((v, i) => { 81 | return new ElementHandle(this.contentFrame(), [selector, i], this._joinSelfSelector()); 82 | }); 83 | }); 84 | } 85 | /** 86 | * 查找节点,并将查找的节点作为参数传为pageFunction 87 | * @param {string} selector 88 | * @param {Function} pageFunction 89 | * 90 | * @return {Promise<*>} 91 | */ 92 | $eval(selector, pageFunction) { 93 | var args = [].slice.call(arguments, 1); 94 | 95 | return this.ipc.send("elementHandle.$eval", { 96 | selector: selector, 97 | baseSelector: this._joinSelfSelector(), 98 | pageFunction: pageFunction.toString(), 99 | args: args 100 | }); 101 | } 102 | /** 103 | * 查找节点集合,并将查找的节点集合作为参数传为pageFunction 104 | * @param {string} selector 105 | * @param {Function} pageFunction 106 | * 107 | * @return {Promise<*>} 108 | */ 109 | $$eval(selector, pageFunction) { 110 | var args = [].slice.call(arguments, 1); 111 | 112 | return this.ipc.send("elementHandle.$$eval", { 113 | selector: selector, 114 | baseSelector: this._joinSelfSelector(), 115 | pageFunction: pageFunction.toString(), 116 | args: args 117 | }); 118 | } 119 | /** 120 | * todo 121 | */ 122 | $x() /* expression */{ 123 | return Promise.reject("todo"); 124 | } 125 | /** 126 | * todo 127 | */ 128 | asElement() { 129 | return Promise.reject("todo"); 130 | } 131 | /** 132 | * boundingBox 133 | * 134 | * @return {Promise} 135 | */ 136 | boundingBox() { 137 | return this.ipc.send("elementHandle.boundingBox", { 138 | selector: this._joinSelfSelector() 139 | // baseSelector: this.baseSelector, 140 | }); 141 | } 142 | /** 143 | * offsetTop 144 | * 145 | * @return {Promise} 146 | */ 147 | get offsetTop() { 148 | return this.ipc.send("elementHandle.offsetTop", { 149 | selector: this._joinSelfSelector() 150 | }); 151 | } 152 | /** 153 | * offsetTop 154 | * 155 | * @return {Promise} 156 | */ 157 | get offsetLeft() { 158 | return this.ipc.send("elementHandle.offsetLeft", { 159 | selector: this._joinSelfSelector() 160 | }); 161 | } 162 | /** 163 | * 获取/设置节点文本 164 | * 165 | * @return {Promise} 166 | */ 167 | textContent(text) { 168 | return this.ipc.send("elementHandle.textContent", { 169 | selector: this._joinSelfSelector(), 170 | text: text 171 | }); 172 | } 173 | /** 174 | * todo 175 | */ 176 | boxModel() { 177 | return Promise.reject("todo"); 178 | } 179 | /** 180 | * 点击当前节点 181 | * @param {*} options 暂不支持 182 | * 183 | * @return {Promise} 184 | */ 185 | click(options) { 186 | return this.ipc.send("elementHandle.click", { 187 | selector: this._joinSelfSelector(), 188 | options 189 | }); 190 | } 191 | /** 192 | * 显示当前节点 193 | * @param {object} options 配置选项 194 | * @param {string} [options.display] 要设置的display值,默认为block 195 | */ 196 | show(options) { 197 | return this.ipc.send("elementHandle.show", { 198 | selector: this._joinSelfSelector(), 199 | options: options 200 | }); 201 | } 202 | /** 203 | * 隐藏当前节点 204 | */ 205 | hide() { 206 | return this.ipc.send("elementHandle.hide", { 207 | selector: this._joinSelfSelector() 208 | }); 209 | } 210 | /** 211 | * 当前节点所属frame 212 | * 213 | * @return {Frame} 214 | */ 215 | contentFrame() { 216 | return this.frame; 217 | } 218 | /** 219 | * todo 220 | */ 221 | dispose() { 222 | return Promise.reject("todo"); 223 | } 224 | /** 225 | * todo 226 | */ 227 | executionContext() { 228 | return Promise.reject("todo"); 229 | } 230 | /** 231 | * 设置checked属性为true,并触发change事件 232 | * 233 | * @return {Promise} 234 | */ 235 | check() { 236 | return this.ipc.send("elementHandle.check", { 237 | selector: this._joinSelfSelector() 238 | }); 239 | } 240 | /** 241 | * 设置checked属性为false,并触发change事件 242 | * 243 | * @return {Promise} 244 | */ 245 | uncheck() { 246 | return this.ipc.send("elementHandle.uncheck", { 247 | selector: this._joinSelfSelector() 248 | }); 249 | } 250 | /** 251 | * todo 252 | */ 253 | getProperties() { 254 | return Promise.reject("todo"); 255 | } 256 | /** 257 | * todo 258 | */ 259 | getProperty() /* propertyName */{ 260 | return Promise.reject("todo"); 261 | } 262 | /** 263 | * focus当前节点 264 | * 265 | * @return {Promise} 266 | */ 267 | focus() { 268 | // console.log('focus: ', this._joinSelfSelector()); 269 | return this.ipc.send("elementHandle.focus", { 270 | selector: this._joinSelfSelector() 271 | }); 272 | } 273 | /** 274 | * 取消聚焦当前节点 275 | * 276 | * @return {Promise} 277 | */ 278 | // @noitce puppeteer不支持blur 279 | blur() { 280 | return this.ipc.send("elementHandle.blur", { 281 | selector: this._joinSelfSelector() 282 | }); 283 | } 284 | /** 285 | * 获取节点的属性集合 286 | * 287 | * @return {Promise>} 288 | */ 289 | getAttributes() { 290 | return this.ipc.send("elementHandle.getAttributes", { 291 | selector: this._joinSelfSelector() 292 | }).then(function (attributes) { 293 | var map = new Map(); 294 | for (var attr of attributes) { 295 | if (attributes.hasOwnProperty(attr)) { 296 | map.set(attr, { 297 | // @notice: 先简单实现 298 | jsonValue: function (value) { 299 | return value; 300 | }(attributes[attr]) 301 | }); 302 | } 303 | } 304 | return map; 305 | }); 306 | } 307 | /** 308 | * 获取节点的指定属性值 309 | * 310 | * @return {Promise} 通过jsonValue()获取属性值 311 | */ 312 | getAttribute(attrName) { 313 | return this.ipc.send("elementHandle.getAttribute", { 314 | selector: this._joinSelfSelector(), 315 | attrName: attrName 316 | }).then(function (value) { 317 | // @notice: 先简单实现 318 | return { 319 | jsonValue: function () { 320 | return value; 321 | } 322 | }; 323 | }); 324 | } 325 | /** 326 | * hover当前节点 327 | * 328 | * @return {Promise} 329 | */ 330 | hover() { 331 | return this.ipc.send("elementHandle.hover", { 332 | selector: this._joinSelfSelector() 333 | }); 334 | } 335 | /** 336 | * todo 337 | */ 338 | isIntersectingViewport() { 339 | return Promise.reject("todo"); 340 | } 341 | /** 342 | * todo 343 | */ 344 | jsonValue() { 345 | return Promise.reject("todo"); 346 | } 347 | /** 348 | * 键入文本 349 | * @param {string} text 输入的文本内容 350 | * @param {*} options 暂不支持 351 | */ 352 | // todo: 暂不支持options 353 | press(text, options) { 354 | var key = _USKeyboardLayout2.default[text]; 355 | 356 | return this.ipc.send("elementHandle.press", { 357 | selector: this._joinSelfSelector(), 358 | keyCode: key && key.keyCode || 0, 359 | text: text, 360 | options: options 361 | }); 362 | } 363 | /** 364 | * todo 365 | */ 366 | screenshot() /* options */{ 367 | return Promise.reject("todo"); 368 | } 369 | /** 370 | * todo 371 | */ 372 | tap() { 373 | return Promise.reject("todo"); 374 | } 375 | /** 376 | * todo 377 | */ 378 | toString() { 379 | return Promise.reject("todo"); 380 | } 381 | /** 382 | * 输入文字 383 | * @param {string} text 输入的文本内容 384 | * @param {Object} options 选项 385 | * @param {number} [options.delay] 输入间隔 386 | */ 387 | type(text, options) { 388 | var _this = this; 389 | 390 | return _asyncToGenerator(function* () { 391 | for (let i = 0; i < text.length; i++) { 392 | yield _this.press(text.charAt(i)); 393 | yield (0, _util.sleep)(options && options.delay); 394 | } 395 | return true; 396 | })(); 397 | } 398 | /** 399 | * todo 400 | */ 401 | uploadFile() /* ...filePaths */{ 402 | return Promise.reject("todo"); 403 | } 404 | } 405 | exports.default = ElementHandle; -------------------------------------------------------------------------------- /renderer_lib/EventEmitter.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /*! 7 | * EventEmitter v5.2.6 - git.io/ee 8 | * Unlicense - http://unlicense.org/ 9 | * Oliver Caldwell - https://oli.me.uk/ 10 | * @preserve 11 | */ 12 | 13 | /** 14 | * Class for managing events. 15 | * Can be extended to provide event functionality in other classes. 16 | * 17 | * @class EventEmitter Manages event registering and emitting. 18 | */ 19 | function EventEmitter() {} 20 | 21 | // Shortcuts to improve speed and size 22 | var proto = EventEmitter.prototype; 23 | 24 | /** 25 | * Finds the index of the listener for the event in its storage array. 26 | * 27 | * @ignore 28 | * @param {Function[]} listeners Array of listeners to search through. 29 | * @param {Function} listener Method to look for. 30 | * @return {Number} Index of the specified listener, -1 if not found 31 | * @api private 32 | */ 33 | function indexOfListener(listeners, listener) { 34 | var i = listeners.length; 35 | while (i--) { 36 | if (listeners[i].listener === listener) { 37 | return i; 38 | } 39 | } 40 | 41 | return -1; 42 | } 43 | 44 | /** 45 | * Alias a method while keeping the context correct, to allow for overwriting of target method. 46 | * 47 | * @ignore 48 | * @param {String} name The name of the target method. 49 | * @return {Function} The aliased method 50 | * @api private 51 | */ 52 | function alias(name) { 53 | return function aliasClosure() { 54 | return this[name].apply(this, arguments); 55 | }; 56 | } 57 | 58 | /** 59 | * Returns the listener array for the specified event. 60 | * Will initialise the event object and listener arrays if required. 61 | * Will return an object if you use a regex search. The object contains keys for each matched event. So /ba[rz]/ might return an object containing bar and baz. But only if you have either defined them with defineEvent or added some listeners to them. 62 | * Each property in the object response is an array of listener functions. 63 | * 64 | * @param {String|RegExp} evt Name of the event to return the listeners from. 65 | * @return {Function[]|Object} All listener functions for the event. 66 | */ 67 | proto.getListeners = function getListeners(evt) { 68 | var events = this._getEvents(); 69 | var response; 70 | var key; 71 | 72 | // Return a concatenated array of all matching events if 73 | // the selector is a regular expression. 74 | if (evt instanceof RegExp) { 75 | response = {}; 76 | for (key in events) { 77 | if (events.hasOwnProperty(key) && evt.test(key)) { 78 | response[key] = events[key]; 79 | } 80 | } 81 | } else { 82 | response = events[evt] || (events[evt] = []); 83 | } 84 | 85 | return response; 86 | }; 87 | 88 | /** 89 | * Takes a list of listener objects and flattens it into a list of listener functions. 90 | * 91 | * @param {Object[]} listeners Raw listener objects. 92 | * @return {Function[]} Just the listener functions. 93 | */ 94 | proto.flattenListeners = function flattenListeners(listeners) { 95 | var flatListeners = []; 96 | var i; 97 | 98 | for (i = 0; i < listeners.length; i += 1) { 99 | flatListeners.push(listeners[i].listener); 100 | } 101 | 102 | return flatListeners; 103 | }; 104 | 105 | /** 106 | * Fetches the requested listeners via getListeners but will always return the results inside an object. This is mainly for internal use but others may find it useful. 107 | * 108 | * @param {String|RegExp} evt Name of the event to return the listeners from. 109 | * @return {Object} All listener functions for an event in an object. 110 | */ 111 | proto.getListenersAsObject = function getListenersAsObject(evt) { 112 | var listeners = this.getListeners(evt); 113 | var response; 114 | 115 | if (listeners instanceof Array) { 116 | response = {}; 117 | response[evt] = listeners; 118 | } 119 | 120 | return response || listeners; 121 | }; 122 | 123 | function isValidListener(listener) { 124 | if (typeof listener === "function" || listener instanceof RegExp) { 125 | return true; 126 | } else if (listener && typeof listener === "object") { 127 | return isValidListener(listener.listener); 128 | } else { 129 | return false; 130 | } 131 | } 132 | 133 | /** 134 | * Adds a listener function to the specified event. 135 | * The listener will not be added if it is a duplicate. 136 | * If the listener returns true then it will be removed after it is called. 137 | * If you pass a regular expression as the event name then the listener will be added to all events that match it. 138 | * 139 | * @param {String|RegExp} evt Name of the event to attach the listener to. 140 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 141 | * @return {Object} Current instance of EventEmitter for chaining. 142 | */ 143 | proto.addListener = function addListener(evt, listener) { 144 | if (!isValidListener(listener)) { 145 | throw new TypeError("listener must be a function"); 146 | } 147 | 148 | var listeners = this.getListenersAsObject(evt); 149 | var listenerIsWrapped = typeof listener === "object"; 150 | var key; 151 | 152 | for (key in listeners) { 153 | if (listeners.hasOwnProperty(key) && indexOfListener(listeners[key], listener) === -1) { 154 | listeners[key].push(listenerIsWrapped ? listener : { 155 | listener: listener, 156 | once: false 157 | }); 158 | } 159 | } 160 | 161 | return this; 162 | }; 163 | 164 | /** 165 | * Alias of addListener 166 | */ 167 | proto.on = alias("addListener"); 168 | 169 | /** 170 | * Semi-alias of addListener. It will add a listener that will be 171 | * automatically removed after its first execution. 172 | * 173 | * @param {String|RegExp} evt Name of the event to attach the listener to. 174 | * @param {Function} listener Method to be called when the event is emitted. If the function returns true then it will be removed after calling. 175 | * @return {Object} Current instance of EventEmitter for chaining. 176 | */ 177 | proto.addOnceListener = function addOnceListener(evt, listener) { 178 | return this.addListener(evt, { 179 | listener: listener, 180 | once: true 181 | }); 182 | }; 183 | 184 | /** 185 | * Alias of addOnceListener. 186 | */ 187 | proto.once = alias("addOnceListener"); 188 | 189 | /** 190 | * Defines an event name. This is required if you want to use a regex to add a listener to multiple events at once. If you don't do this then how do you expect it to know what event to add to? Should it just add to every possible match for a regex? No. That is scary and bad. 191 | * You need to tell it what event names should be matched by a regex. 192 | * 193 | * @param {String} evt Name of the event to create. 194 | * @return {Object} Current instance of EventEmitter for chaining. 195 | */ 196 | proto.defineEvent = function defineEvent(evt) { 197 | this.getListeners(evt); 198 | return this; 199 | }; 200 | 201 | /** 202 | * Uses defineEvent to define multiple events. 203 | * 204 | * @param {String[]} evts An array of event names to define. 205 | * @return {Object} Current instance of EventEmitter for chaining. 206 | */ 207 | proto.defineEvents = function defineEvents(evts) { 208 | for (var i = 0; i < evts.length; i += 1) { 209 | this.defineEvent(evts[i]); 210 | } 211 | return this; 212 | }; 213 | 214 | /** 215 | * Removes a listener function from the specified event. 216 | * When passed a regular expression as the event name, it will remove the listener from all events that match it. 217 | * 218 | * @param {String|RegExp} evt Name of the event to remove the listener from. 219 | * @param {Function} listener Method to remove from the event. 220 | * @return {Object} Current instance of EventEmitter for chaining. 221 | */ 222 | proto.removeListener = function removeListener(evt, listener) { 223 | var listeners = this.getListenersAsObject(evt); 224 | var index; 225 | var key; 226 | 227 | for (key in listeners) { 228 | if (listeners.hasOwnProperty(key)) { 229 | index = indexOfListener(listeners[key], listener); 230 | 231 | if (index !== -1) { 232 | listeners[key].splice(index, 1); 233 | } 234 | } 235 | } 236 | 237 | return this; 238 | }; 239 | 240 | /** 241 | * Alias of removeListener 242 | */ 243 | proto.off = alias("removeListener"); 244 | 245 | /** 246 | * Adds listeners in bulk using the manipulateListeners method. 247 | * If you pass an object as the first argument you can add to multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. You can also pass it an event name and an array of listeners to be added. 248 | * You can also pass it a regular expression to add the array of listeners to all events that match it. 249 | * Yeah, this function does quite a bit. That's probably a bad thing. 250 | * 251 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add to multiple events at once. 252 | * @param {Function[]} [listeners] An optional array of listener functions to add. 253 | * @return {Object} Current instance of EventEmitter for chaining. 254 | */ 255 | proto.addListeners = function addListeners(evt, listeners) { 256 | // Pass through to manipulateListeners 257 | return this.manipulateListeners(false, evt, listeners); 258 | }; 259 | 260 | /** 261 | * Removes listeners in bulk using the manipulateListeners method. 262 | * If you pass an object as the first argument you can remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 263 | * You can also pass it an event name and an array of listeners to be removed. 264 | * You can also pass it a regular expression to remove the listeners from all events that match it. 265 | * 266 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to remove from multiple events at once. 267 | * @param {Function[]} [listeners] An optional array of listener functions to remove. 268 | * @return {Object} Current instance of EventEmitter for chaining. 269 | */ 270 | proto.removeListeners = function removeListeners(evt, listeners) { 271 | // Pass through to manipulateListeners 272 | return this.manipulateListeners(true, evt, listeners); 273 | }; 274 | 275 | /** 276 | * Edits listeners in bulk. The addListeners and removeListeners methods both use this to do their job. You should really use those instead, this is a little lower level. 277 | * The first argument will determine if the listeners are removed (true) or added (false). 278 | * If you pass an object as the second argument you can add/remove from multiple events at once. The object should contain key value pairs of events and listeners or listener arrays. 279 | * You can also pass it an event name and an array of listeners to be added/removed. 280 | * You can also pass it a regular expression to manipulate the listeners of all events that match it. 281 | * 282 | * @param {Boolean} remove True if you want to remove listeners, false if you want to add. 283 | * @param {String|Object|RegExp} evt An event name if you will pass an array of listeners next. An object if you wish to add/remove from multiple events at once. 284 | * @param {Function[]} [listeners] An optional array of listener functions to add/remove. 285 | * @return {Object} Current instance of EventEmitter for chaining. 286 | */ 287 | proto.manipulateListeners = function manipulateListeners(remove, evt, listeners) { 288 | var i; 289 | var value; 290 | var single = remove ? this.removeListener : this.addListener; 291 | var multiple = remove ? this.removeListeners : this.addListeners; 292 | 293 | // If evt is an object then pass each of its properties to this method 294 | if (typeof evt === "object" && !(evt instanceof RegExp)) { 295 | for (i in evt) { 296 | if (evt.hasOwnProperty(i) && (value = evt[i])) { 297 | // Pass the single listener straight through to the singular method 298 | if (typeof value === "function") { 299 | single.call(this, i, value); 300 | } else { 301 | // Otherwise pass back to the multiple function 302 | multiple.call(this, i, value); 303 | } 304 | } 305 | } 306 | } else { 307 | // So evt must be a string 308 | // And listeners must be an array of listeners 309 | // Loop over it and pass each one to the multiple method 310 | i = listeners.length; 311 | while (i--) { 312 | single.call(this, evt, listeners[i]); 313 | } 314 | } 315 | 316 | return this; 317 | }; 318 | 319 | /** 320 | * Removes all listeners from a specified event. 321 | * If you do not specify an event then all listeners will be removed. 322 | * That means every event will be emptied. 323 | * You can also pass a regex to remove all events that match it. 324 | * 325 | * @param {String|RegExp} [evt] Optional name of the event to remove all listeners for. Will remove from every event if not passed. 326 | * @return {Object} Current instance of EventEmitter for chaining. 327 | */ 328 | proto.removeEvent = function removeEvent(evt) { 329 | var type = typeof evt; 330 | var events = this._getEvents(); 331 | var key; 332 | 333 | // Remove different things depending on the state of evt 334 | if (type === "string") { 335 | // Remove all listeners for the specified event 336 | delete events[evt]; 337 | } else if (evt instanceof RegExp) { 338 | // Remove all events matching the regex. 339 | for (key in events) { 340 | if (events.hasOwnProperty(key) && evt.test(key)) { 341 | delete events[key]; 342 | } 343 | } 344 | } else { 345 | // Remove all listeners in all events 346 | delete this._events; 347 | } 348 | 349 | return this; 350 | }; 351 | 352 | /** 353 | * Alias of removeEvent. 354 | * 355 | * Added to mirror the node API. 356 | */ 357 | proto.removeAllListeners = alias("removeEvent"); 358 | 359 | /** 360 | * Emits an event of your choice. 361 | * When emitted, every listener attached to that event will be executed. 362 | * If you pass the optional argument array then those arguments will be passed to every listener upon execution. 363 | * Because it uses `apply`, your array of arguments will be passed as if you wrote them out separately. 364 | * So they will not arrive within the array on the other side, they will be separate. 365 | * You can also pass a regular expression to emit to all events that match it. 366 | * 367 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 368 | * @param {Array} [args] Optional array of arguments to be passed to each listener. 369 | * @return {Object} Current instance of EventEmitter for chaining. 370 | */ 371 | proto.emitEvent = function emitEvent(evt, args) { 372 | var listenersMap = this.getListenersAsObject(evt); 373 | var listeners; 374 | var listener; 375 | var i; 376 | var key; 377 | var response; 378 | 379 | for (key in listenersMap) { 380 | if (listenersMap.hasOwnProperty(key)) { 381 | listeners = listenersMap[key].slice(0); 382 | 383 | for (i = 0; i < listeners.length; i++) { 384 | // If the listener returns true then it shall be removed from the event 385 | // The function is executed either with a basic call or an apply if there is an args array 386 | listener = listeners[i]; 387 | 388 | if (listener.once === true) { 389 | this.removeListener(evt, listener.listener); 390 | } 391 | 392 | response = listener.listener.apply(this, args || []); 393 | 394 | if (response === this._getOnceReturnValue()) { 395 | this.removeListener(evt, listener.listener); 396 | } 397 | } 398 | } 399 | } 400 | 401 | return this; 402 | }; 403 | 404 | /** 405 | * Alias of emitEvent 406 | */ 407 | proto.trigger = alias("emitEvent"); 408 | 409 | /** 410 | * Subtly different from emitEvent in that it will pass its arguments on to the listeners, as opposed to taking a single array of arguments to pass on. 411 | * As with emitEvent, you can pass a regex in place of the event name to emit to all events that match it. 412 | * 413 | * @param {String|RegExp} evt Name of the event to emit and execute listeners for. 414 | * @param {...*} Optional additional arguments to be passed to each listener. 415 | * @return {Object} Current instance of EventEmitter for chaining. 416 | */ 417 | proto.emit = function emit(evt) { 418 | var args = Array.prototype.slice.call(arguments, 1); 419 | return this.emitEvent(evt, args); 420 | }; 421 | 422 | /** 423 | * Sets the current value to check against when executing listeners. If a 424 | * listeners return value matches the one set here then it will be removed 425 | * after execution. This value defaults to true. 426 | * 427 | * @param {*} value The new value to check for when executing listeners. 428 | * @return {Object} Current instance of EventEmitter for chaining. 429 | */ 430 | proto.setOnceReturnValue = function setOnceReturnValue(value) { 431 | this._onceReturnValue = value; 432 | return this; 433 | }; 434 | 435 | /** 436 | * Fetches the current value to check against when executing listeners. If 437 | * the listeners return value matches this one then it should be removed 438 | * automatically. It will return true by default. 439 | * 440 | * @return {*|Boolean} The current value to check for or the default, true. 441 | * @api private 442 | */ 443 | proto._getOnceReturnValue = function _getOnceReturnValue() { 444 | if (this.hasOwnProperty("_onceReturnValue")) { 445 | return this._onceReturnValue; 446 | } else { 447 | return true; 448 | } 449 | }; 450 | 451 | /** 452 | * Fetches the events object and creates one if required. 453 | * 454 | * @return {Object} The events storage object. 455 | * @api private 456 | */ 457 | proto._getEvents = function _getEvents() { 458 | return this._events || (this._events = {}); 459 | }; 460 | 461 | /** 462 | * Reverts the global {@link EventEmitter} to its previous value and returns a reference to this version. 463 | * 464 | * @return {Function} Non conflicting EventEmitter class. 465 | */ 466 | EventEmitter.noConflict = function noConflict() { 467 | return EventEmitter; 468 | }; 469 | 470 | exports.default = EventEmitter; -------------------------------------------------------------------------------- /screenshot/1.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/replace5/electron-puppeteer/7241292db61d4a40ebba53d6289dad2c80528219/screenshot/1.gif --------------------------------------------------------------------------------