├── .eslintrc.yml ├── .github └── FUNDING.yml ├── .gitignore ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs ├── README.md ├── browser-window.md ├── communication-plugin-webview.md ├── frameless-window.md ├── opening-links-in-browser.md ├── rectangle.md └── web-contents.md ├── lib ├── __tests__ │ └── execute-javascript.js ├── browser-api.js ├── constants.js ├── dispatch-first-click.js ├── execute-javascript.js ├── fitSubview.js ├── index.js ├── inject-client-messaging.js ├── movable-area.js ├── parseWebArguments.js ├── set-delegates.js └── webview-api.js ├── package.json ├── remote.js └── type.d.ts /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | root: true 2 | extends: 3 | - airbnb-base 4 | - prettier 5 | plugins: 6 | - prettier 7 | 8 | globals: 9 | __command: false 10 | NSApplication: false 11 | NSApp: false 12 | NSMakeRect: false 13 | NSScreen: false 14 | NSColor: false 15 | NSClosableWindowMask: false 16 | NSFullScreenWindowMask: false 17 | NSWindowZoomButton: false 18 | NSFloatingWindowLevel: false 19 | NSMakeSize: false 20 | NSHeight: false 21 | NSMiniaturizableWindowMask: false 22 | NSWindowCollectionBehaviorFullScreenPrimary: false 23 | NSVisualEffectView: false 24 | NSWindowCollectionBehaviorFullScreenAuxiliary: false 25 | NSNormalWindowLevel: false 26 | NSVisualEffectBlendingModeBehindWindow: false 27 | CGWindowLevelForKey: false 28 | kCGMaximumWindowLevelKey: false 29 | kCGMinimumWindowLevelKey: false 30 | NSTornOffMenuWindowLevel: false 31 | NSModalPanelWindowLevel: false 32 | NSMainMenuWindowLevel: false 33 | NSResizableWindowMask: false 34 | NSStatusWindowLevel: false 35 | NSPopUpMenuWindowLevel: false 36 | NSScreenSaverWindowLevel: false 37 | NSDockWindowLevel: false 38 | NSInformationalRequest: false 39 | NSWindowCollectionBehaviorCanJoinAllSpaces: false 40 | NSWindowSharingNone: false 41 | NSWindowSharingReadOnly: false 42 | NSViewWidthSizable: false 43 | NSViewHeightSizable: false 44 | NSVisualEffectStateActive: false 45 | NSWindowBelow: false 46 | NSVisualEffectMaterialLight: false 47 | NSVisualEffectMaterialAppearanceBased: false 48 | NSVisualEffectMaterialDark: false 49 | NSVisualEffectMaterialTitlebar: false 50 | NSVisualEffectMaterialSelection: false 51 | NSVisualEffectMaterialMenu: false 52 | NSVisualEffectMaterialPopover: false 53 | NSVisualEffectMaterialSidebar: false 54 | NSVisualEffectMaterialMediumLight: false 55 | NSVisualEffectMaterialUltraDark: false 56 | NSWindowAbove: false 57 | NSDictionary: false 58 | NSLayoutConstraint: false 59 | NSLayoutRelationEqual: false 60 | NSLayoutAttributeLeft: false 61 | NSLayoutAttributeTop: false 62 | NSLayoutAttributeRight: false 63 | NSLayoutAttributeBottom: false 64 | NSUUID: false 65 | NSThread: false 66 | coscript: false 67 | NSWidth: false 68 | NSTitledWindowMask: false 69 | NSTexturedBackgroundWindowMask: false 70 | NSPanel: false 71 | NSBackingStoreBuffered: false 72 | WKWebView: false 73 | kCGDesktopWindowLevel: false 74 | NSWindowCollectionBehaviorStationary: false 75 | NSWindowCollectionBehaviorIgnoresCycle: false 76 | NSWindowTitleHidden: false 77 | NSToolbar: false 78 | NSWindowFullScreenButton: false 79 | NSWindowMiniaturizeButton: false 80 | NSWindowCloseButton: false 81 | NSEventTypeLeftMouseDown: false 82 | NSMutableDictionary: false 83 | arguments: false 84 | window: false 85 | NSFullSizeContentViewWindowMask: false 86 | CGRectMake: false 87 | WKWebViewConfiguration: false 88 | NSURLRequest: false 89 | NSURL: false 90 | __mocha__: false 91 | CGPointMake: false 92 | WKUserScript: false 93 | NSURLCredential: false 94 | CGSizeMake: false 95 | NSEvent: false 96 | WKInspectorWKWebView: false 97 | NSString: false 98 | NSCharacterSet: false 99 | MSTheme: false 100 | NSKeyValueChangeNewKey: false 101 | 102 | rules: 103 | ########### 104 | # PLUGINS # 105 | ########### 106 | 107 | ########### 108 | # BUILTIN # 109 | ########### 110 | prefer-rest-params: off 111 | prefer-arrow-callback: off 112 | no-var: off 113 | vars-on-top: off 114 | func-names: off 115 | no-param-reassign: off 116 | no-bitwise: off 117 | prefer-destructuring: off 118 | object-shorthand: off 119 | prefer-template: off 120 | no-underscore-dangle: off 121 | class-methods-use-this: warn 122 | no-useless-constructor: warn 123 | eqeqeq: off 124 | strict: off 125 | prefer-spread: off 126 | 127 | ########### 128 | # SPECIAL # 129 | ########### 130 | prettier/prettier: 131 | - error 132 | - singleQuote: true 133 | trailingComma: es5 134 | semi: false 135 | parser: typescript 136 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [mathieudutour] 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pids 2 | logs 3 | npm-debug.log 4 | node_modules 5 | package-lock.json 6 | test 7 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "search.exclude": { 3 | "**/node_modules": true, 4 | "**/build": true, 5 | "**/.awcache": true 6 | }, 7 | "files.exclude": { 8 | "**/.git": true, 9 | "**/.svn": true, 10 | "**/.hg": true, 11 | "**/.DS_Store": true, 12 | "**/node_modules": true, 13 | "**/build": true, 14 | "**/.awcache": true, 15 | "**/_external": true, 16 | "**/_reference": true, 17 | "**/_site": true, 18 | "**/_vendor": true, 19 | "**/.bundle": true, 20 | "**/.sass-cache": true, 21 | "**/css": true, 22 | "**/images": true, 23 | "**/js": true, 24 | ".jekyll-metadata": true 25 | }, 26 | "editor.tabSize": 2, 27 | "files.trimTrailingWhitespace": true, 28 | "javascript.validate.enable": false, 29 | "eslint.enable": true, 30 | "prettier.semi": false, 31 | "prettier.singleQuote": true, 32 | "prettier.trailingComma": "es5", 33 | "prettier.proseWrap": "never", 34 | "editor.formatOnSave": true, 35 | "editor.defaultFormatter": "esbenp.prettier-vscode", 36 | "spellright.language": [ 37 | "en" 38 | ], 39 | "spellright.documentTypes": [ 40 | "markdown", 41 | "latex", 42 | "plaintext" 43 | ] 44 | } 45 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## Version 3.5.1 4 | 5 | - Revert 3.5.0 because it's crashing on macOS 11.0. 6 | 7 | ## Version 3.5.0 8 | 9 | - Fix a memory leak when closing the webview. 10 | 11 | ## Version 3.4.4 12 | 13 | - Add TypeScript definitions. 14 | 15 | ## Version 3.4.3 16 | 17 | - Fix updating the webview theme when changing system theme. 18 | 19 | ## Version 3.4.2 20 | 21 | - Fix `webContents.getURL()` tag. 22 | 23 | ## Version 3.4.1 24 | 25 | - Fix `acceptsFirstMouse` for `SELECT` tag. 26 | 27 | ## Version 3.4.0 28 | 29 | - `window.postMessage` now returns a Promise with the array of results from the plugin listeners. The plugin listeners can return an object or a Promise which will get executed and its result passed down. 30 | 31 | ## Version 3.3.1 32 | 33 | - Fix typo in theme change. 34 | 35 | ## Version 3.3.0 36 | 37 | - Add a `__skpm-light` or `__skpm-dark` class to the body depending on the Sketch's theme. 38 | 39 | ## Version 3.2.2 40 | 41 | - Fix ghost area not draggable when no frame. 42 | 43 | ## Version 3.2.1 44 | 45 | - Fix typo 46 | 47 | ## Version 3.2.0 48 | 49 | - Fix some methods checking for the delegates' properties after rebuilding the window remotely. 50 | - Add `hidesOnDeactivate` option. Whether the window is removed from the screen when Sketch becomes inactive. Default is `true`. 51 | - Add `remembersWindowFrame` option. Whether to remember the position and the size of the window the next time. Defaults is `false`. 52 | 53 | ## Version 3.1.3 54 | 55 | - allow windows to close on MacOS 10.15 (Thanks @robintindale) 56 | - fix production builds using URLs containing spaces (Thanks @robintindale) 57 | 58 | ## Version 3.1.2 59 | 60 | - Emit `uncaughtException` event if available (Sketch >=58) instead of throwing an error if there is an event listener 61 | 62 | ## Version 3.1.1 63 | 64 | - Correctly load file URLs when prepended with `file://` (Thanks @huw) 65 | 66 | ## Version 3.1.0 67 | 68 | - Fix the `webview.focus()` and `webview.blur()` methods not working 69 | - Fix the webview getting stuck to cursor on draggable area (Thanks @xsfour) 70 | 71 | ## Version 3.0.7 72 | 73 | - Fix the y coordinate of the first event of `acceptFirstClick` 74 | 75 | ## Version 3.0.6 76 | 77 | - Fix `webContents.executeJavaScript` when the script contains some escaped double quotes 78 | 79 | ## Version 3.0.5 80 | 81 | - Fix `remote.sendToWebview` when the inspector was opened (Thanks @ig-robstoffers) 82 | 83 | ## Version 3.0.4 84 | 85 | - Fix `webContents.executeJavaScript` 86 | 87 | ## Version 3.0.3 88 | 89 | - Clear the webview context when closing the webview. 90 | 91 | ## Version 3.0.2 92 | 93 | - Fix `sendToWebview` and `fromPanel` when the vibrancy option is set. 94 | 95 | ## Version 3.0.1 96 | 97 | - Fix hiding the background of the webview. This in turn fixes setting the vibrancy option. 98 | 99 | ## Version 3.0.0 100 | 101 | - Add `data-app-region` to be able to drag a div to move the window. 102 | - `executeJavascript` changed a bit: if the result of the executed code is a promise the callback result will be the resolved value of the promise. We recommend that you use the returned Promise to handle code that results in a Promise. 103 | 104 | ```js 105 | contents 106 | .executeJavaScript( 107 | 'fetch("https://jsonplaceholder.typicode.com/users/1").then(resp => resp.json())', 108 | true 109 | ) 110 | .then((result) => { 111 | console.log(result) // Will be the JSON object from the fetch call 112 | }) 113 | ``` 114 | 115 | - Fix bounds methods to handle inverted y as well as the initial y position of the window. 116 | 117 | ## Version 2.1.7 118 | 119 | - Fix remotely executing javascript on a webview. 120 | 121 | ## Version 2.1.6 122 | 123 | - Fix events dispatching to the wrong event emitter. 124 | 125 | ## Version 2.1.5 126 | 127 | - Fix a bug in the 'will-navigate' event. 128 | 129 | ## Version 2.1.4 130 | 131 | - Fix a bug preventing events to be triggered (introduced in v2.1.3). 132 | 133 | ## Version 2.1.3 134 | 135 | - Make sure that `webContents.executeJavascript` is executed after the webview is loaded. 136 | - Wrap event handlers in try/catch to log the error (and avoid crashing Sketch). 137 | 138 | ## Version 2.1.2 139 | 140 | - Fix a crash when loading an https resource. 141 | 142 | ## Version 2.1.1 143 | 144 | - Enable developer tools by setting `options.webPreferences.devTools` to true. 145 | 146 | ## Version 2.1.0 147 | 148 | - Add support for showing the webview as a modal (aka sheet) by setting `options.modal` to `true`. 149 | 150 | ## Version 2.0.1 151 | 152 | - Fix the loading of css/js assets from a local web page 153 | 154 | ## Version 2.0.0 155 | 156 | :warning: This version requires Sketch >= 51. 157 | 158 | - The webview is now backed by `WKWebview`. 159 | - Instead of importing `sketch-module-web-view/client`, you now communicate with the plugin using `window.postMessage`. 160 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Mathieu Dutour 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | docs/README.md -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # sketch-module-web-view 2 | 3 | A Sketch module for creating a complex UI with a webview. The API is mimicking the [BrowserWindow](https://electronjs.org/docs/api/browser-window) API of Electron. 4 | 5 | ## Installation 6 | 7 | To use this module in your Sketch plugin you need a bundler utility like [skpm](https://github.com/skpm/skpm) and add it as a dependency: 8 | 9 | ```bash 10 | npm install -S sketch-module-web-view 11 | ``` 12 | 13 | You can also use the [with-webview](https://github.com/skpm/with-webview) skpm template to have a solid base to start your project with a webview: 14 | 15 | ```bash 16 | skpm create my-plugin-name --template=skpm/with-webview 17 | ``` 18 | 19 | _The version 2.x is only compatible with Sketch >= 51. If you need compatibility with previous versions of Sketch, use the version 1.x_ 20 | 21 | ## Usage 22 | 23 | ```js 24 | import BrowserWindow from 'sketch-module-web-view' 25 | 26 | export default function () { 27 | const options = { 28 | identifier: 'unique.id', 29 | } 30 | 31 | const browserWindow = new BrowserWindow(options) 32 | 33 | browserWindow.loadURL(require('./my-screen.html')) 34 | } 35 | ``` 36 | 37 | ## Documentation 38 | 39 | - [Communicating between the Plugin and the WebView](/docs/communication-plugin-webview.md) 40 | - [Frameless-window](/docs/frameless-window.md) 41 | - [Opening links in browser](/docs/opening-links-in-browser.md) 42 | 43 | ## API References 44 | 45 | - [Browser window](/docs/browser-window.md) 46 | - [Web Contents](/docs/web-contents.md) 47 | 48 | ## License 49 | 50 | MIT 51 | -------------------------------------------------------------------------------- /docs/browser-window.md: -------------------------------------------------------------------------------- 1 | # BrowserWindow 2 | 3 | > Create and control browser windows. 4 | 5 | ```javascript 6 | // In the plugin. 7 | const BrowserWindow = require('sketch-module-web-view') 8 | 9 | let win = new BrowserWindow({ width: 800, height: 600 }) 10 | win.on('closed', () => { 11 | win = null 12 | }) 13 | 14 | // Load a remote URL 15 | win.loadURL('https://github.com') 16 | 17 | // Or load a local HTML file 18 | win.loadURL(require('./index.html')) 19 | ``` 20 | 21 | ## Frameless window 22 | 23 | To create a window without chrome, or a transparent window in arbitrary shape, you can use the [Frameless Window](frameless-window.md) API. 24 | 25 | ## Showing window gracefully 26 | 27 | When loading a page in the window directly, users may see the page load incrementally, which is not a good experience for a native app. To make the window display without visual flash, there are two solutions for different situations. 28 | 29 | ### Using `ready-to-show` event 30 | 31 | While loading the page, the `ready-to-show` event will be emitted when the renderer process has rendered the page for the first time if the window has not been shown yet. Showing the window after this event will have no visual flash: 32 | 33 | ```javascript 34 | const BrowserWindow = require('sketch-module-web-view') 35 | let win = new BrowserWindow({ show: false }) 36 | win.once('ready-to-show', () => { 37 | win.show() 38 | }) 39 | ``` 40 | 41 | This event is usually emitted after the `did-finish-load` event, but for pages with many remote resources, it may be emitted before the `did-finish-load` event. 42 | 43 | ### Setting `backgroundColor` 44 | 45 | For a complex app, the `ready-to-show` event could be emitted too late, making the app feel slow. In this case, it is recommended to show the window immediately, and use a `backgroundColor` close to your app's background: 46 | 47 | ```javascript 48 | const BrowserWindow = require('sketch-module-web-view') 49 | 50 | let win = new BrowserWindow({ backgroundColor: '#2e2c29' }) 51 | win.loadURL('https://github.com') 52 | ``` 53 | 54 | Note that even for apps that use `ready-to-show` event, it is still recommended to set `backgroundColor` to make app feel more native. 55 | 56 | ## Parent and child windows 57 | 58 | A modal window is a child window that disables parent window, to create a modal window, you have to set both `parent` and `modal` options: 59 | 60 | ```javascript 61 | const BrowserWindow = require('sketch-module-web-view') 62 | const sketch = require('sketch') 63 | 64 | let child = new BrowserWindow({ 65 | parent: sketch.getSelectedDocument(), 66 | modal: true, 67 | show: false, 68 | }) 69 | child.loadURL('https://github.com') 70 | child.once('ready-to-show', () => { 71 | child.show() 72 | }) 73 | ``` 74 | 75 | Modal windows will be displayed as sheets attached to the parent Document. 76 | 77 | ## Class: BrowserWindow 78 | 79 | > Create and control browser windows. 80 | 81 | `BrowserWindow` is an [EventEmitter](https://nodejs.org/api/events.html#events_class_events_eventemitter). 82 | 83 | It creates a new `BrowserWindow` with native properties as set by the `options`. 84 | 85 | ### `new BrowserWindow([options])` 86 | 87 | - `options` Object (optional) 88 | - `identifier` String (optional) - Window's unique id. Default is newly generated UUID. 89 | - `width` Integer (optional) - Window's width in pixels. Default is `800`. 90 | - `height` Integer (optional) - Window's height in pixels. Default is `600`. 91 | - `x` Integer (optional) (**required** if y is used) - Window's left offset from screen. Default is to center the window. 92 | - `y` Integer (optional) (**required** if x is used) - Window's top offset from screen. Default is to center the window. 93 | - `hidesOnDeactivate` Boolean (optional) - Whether the window is removed from the screen when Sketch becomes inactive. Default is `true`. 94 | - `remembersWindowFrame` Boolean (optional) - Whether to remember the position and the size of the window the next time. Defaults is `false`. 95 | - `useContentSize` Boolean (optional) - The `width` and `height` would be used as web page's size, which means the actual window's size will include window frame's size and be slightly larger. Default is `false`. 96 | - `center` Boolean (optional) - Show window in the center of the screen. 97 | - `minWidth` Integer (optional) - Window's minimum width. Default is `0`. 98 | - `minHeight` Integer (optional) - Window's minimum height. Default is `0`. 99 | - `maxWidth` Integer (optional) - Window's maximum width. Default is no limit. 100 | - `maxHeight` Integer (optional) - Window's maximum height. Default is no limit. 101 | - `resizable` Boolean (optional) - Whether window is resizable. Default is `true`. 102 | - `movable` Boolean (optional) - Whether window is movable. Default is `true`. 103 | - `minimizable` Boolean (optional) - Whether window is minimizable. Default is `true`. 104 | - `maximizable` Boolean (optional) - Whether window is maximizable. Default is `true`. 105 | - `closable` Boolean (optional) - Whether window is closable. Default is `true`. 106 | 107 | - `alwaysOnTop` Boolean (optional) - Whether the window should always stay on top of other windows. Default is `false`. 108 | - `fullscreen` Boolean (optional) - Whether the window should show in fullscreen. When explicitly set to `false` the fullscreen button will be hidden or disabled. Default is `false`. 109 | - `fullscreenable` Boolean (optional) - Whether the window can be put into fullscreen mode. Also whether the maximize/zoom button should toggle full screen mode or maximize window. Default is `true`. 110 | - `title` String (optional) - Default window title. Default is your plugin name. 111 | - `show` Boolean (optional) - Whether window should be shown when created. Default is `true`. 112 | - `frame` Boolean (optional) - Specify `false` to create a [Frameless Window](frameless-window.md). Default is `true`. 113 | - `parent` Document (optional) - Specify parent [Document](https://developer.sketchapp.com/reference/api/#document). Default is `null`. 114 | - `modal` Boolean (optional) - Whether this is a modal window. This only works when the window is a child window. Default is `false`. 115 | - `acceptsFirstMouse` Boolean (optional) - Whether the web view accepts a single mouse-down event that simultaneously activates the window. Default is `false`. 116 | - `disableAutoHideCursor` Boolean (optional) - Whether to hide cursor when typing. Default is `false`. 117 | - `backgroundColor` String (optional) - Window's background color as a hexadecimal value, like `#66CD00` or `#FFF` or `#80FFFFFF` (alpha is supported). Default is `NSColor.windowBackgroundColor()`. 118 | - `hasShadow` Boolean (optional) - Whether window should have a shadow. Default is `true`. 119 | - `opacity` Number (optional) - Set the initial opacity of the window, between 0.0 (fully transparent) and 1.0 (fully opaque). 120 | - `transparent` Boolean (optional) - Makes the window [transparent](frameless-window.md). Default is `false`. 121 | - `titleBarStyle` String (optional) - The style of window title bar. Default is `default`. Possible values are: 122 | - `default` - Results in the standard gray opaque Mac title bar. 123 | - `hidden` - Results in a hidden title bar and a full size content window, yet the title bar still has the standard window controls ("traffic lights") in the top left. 124 | - `hiddenInset` - Results in a hidden title bar with an alternative look where the traffic light buttons are slightly more inset from the window edge. 125 | - `vibrancy` String (optional) - Add a type of vibrancy effect to the window, only on macOS. Can be `appearance-based`, `light`, `dark`, `titlebar`, `selection`, `menu`, `popover`, `sidebar`, `medium-light` or `ultra-dark`. Please note that using `frame: false` in combination with a vibrancy value requires that you use a non-default `titleBarStyle` as well. 126 | - `webPreferences` Object (optional) - Settings of web page's features. - `devTools` Boolean (optional) - Whether to enable DevTools. If it is set to `false`, can not use `BrowserWindow.webContents.openDevTools()` to open DevTools. Default is `true`. - `javascript` Boolean (optional) - Enables JavaScript support. Default is `true`. - `plugins` Boolean (optional) - Whether plugins should be enabled. Default is `false`. - `minimumFontSize` Integer (optional) - Defaults to `0`. - `zoomFactor` Number (optional) - The default zoom factor of the page, `3.0` represents `300%`. Default is `1.0`. 127 | 164 | 165 | When setting minimum or maximum window size with `minWidth`/`maxWidth`/ `minHeight`/`maxHeight`, it only constrains the users. It won't prevent you from passing a size that does not follow size constraints to `setBounds`/`setSize` or to the constructor of `BrowserWindow`. 166 | 167 | ### Instance Events 168 | 169 | Objects created with `new BrowserWindow` emit the following events: 170 | 171 | #### Event: 'page-title-updated' 172 | 173 | Returns: 174 | 175 | - `event` Event 176 | - `title` String 177 | 178 | Emitted when the document changed its title, calling `event.preventDefault()` will prevent the native window's title from changing. 179 | 180 | #### Event: 'close' 181 | 182 | Returns: 183 | 184 | - `event` Event 185 | 186 | Emitted when the window is going to be closed. It's emitted before the `beforeunload` and `unload` event of the DOM. Calling `event.preventDefault()` will cancel the close. 187 | 188 | 204 | 205 | #### Event: 'closed' 206 | 207 | Emitted when the window is closed. After you have received this event you should remove the reference to the window and avoid using it any more. 208 | 209 | #### Event: 'blur' 210 | 211 | Emitted when the window loses focus. 212 | 213 | #### Event: 'focus' 214 | 215 | Emitted when the window gains focus. 216 | 217 | 224 | 225 | #### Event: 'ready-to-show' 226 | 227 | Emitted when the web page has been rendered (while not being shown) and window can be displayed without a visual flash. 228 | 229 | 236 | 237 | #### Event: 'minimize' 238 | 239 | Emitted when the window is minimized. 240 | 241 | #### Event: 'restore' 242 | 243 | Emitted when the window is restored from a minimized state. 244 | 245 | #### Event: 'resize' 246 | 247 | Emitted when the window is being resized. 248 | 249 | #### Event: 'move' 250 | 251 | Emitted when the window is being moved to a new position. 252 | 253 | **Note**: This event is just an alias of `moved`. 254 | 255 | #### Event: 'moved' _macOS_ 256 | 257 | Emitted once when the window is moved to a new position. 258 | 259 | #### Event: 'enter-full-screen' 260 | 261 | Emitted when the window enters a full-screen state. 262 | 263 | #### Event: 'leave-full-screen' 264 | 265 | Emitted when the window leaves a full-screen state. 266 | 267 | 303 | 304 | ### Static Methods 305 | 306 | The `BrowserWindow` class has the following static methods: 307 | 308 | 327 | 328 | #### `BrowserWindow.fromId(id)` 329 | 330 | - `id` String 331 | 332 | Returns `BrowserWindow` - The window with the given `id`. 333 | 334 | 403 | 404 | ### Instance Properties 405 | 406 | Objects created with `new BrowserWindow` have the following properties: 407 | 408 | ```javascript 409 | const BrowserWindow = require('sketch-module-web-view') 410 | // In this example `win` is our instance 411 | let win = new BrowserWindow({ width: 800, height: 600 }) 412 | win.loadURL('https://github.com') 413 | ``` 414 | 415 | #### `win.webContents` 416 | 417 | A `WebContents` object this window owns. All web page related events and operations will be done via it. 418 | 419 | See the [`webContents` documentation](web-contents.md) for its methods and events. 420 | 421 | #### `win.id` 422 | 423 | A `String` representing the unique ID of the window. 424 | 425 | ### Instance Methods 426 | 427 | Objects created with `new BrowserWindow` have the following instance methods: 428 | 429 | #### `win.destroy()` 430 | 431 | Force closing the window, the `unload` and `beforeunload` event won't be emitted for the web page, and `close` event will also not be emitted for this window, but it guarantees the `closed` event will be emitted. 432 | 433 | #### `win.close()` 434 | 435 | Try to close the window. This has the same effect as a user manually clicking the close button of the window. The web page may cancel the close though. See the [close event](#event-close). 436 | 437 | #### `win.focus()` 438 | 439 | Focuses on the window. 440 | 441 | #### `win.blur()` 442 | 443 | Removes focus from the window. 444 | 445 | #### `win.isFocused()` 446 | 447 | Returns `Boolean` - Whether the window is focused. 448 | 449 | #### `win.isDestroyed()` 450 | 451 | Returns `Boolean` - Whether the window is destroyed. 452 | 453 | #### `win.show()` 454 | 455 | Shows and gives focus to the window. 456 | 457 | #### `win.showInactive()` 458 | 459 | Shows the window but doesn't focus on it. 460 | 461 | #### `win.hide()` 462 | 463 | Hides the window. 464 | 465 | #### `win.isVisible()` 466 | 467 | Returns `Boolean` - Whether the window is visible to the user. 468 | 469 | #### `win.isModal()` 470 | 471 | Returns `Boolean` - Whether current window is a modal window. 472 | 473 | #### `win.maximize()` 474 | 475 | Maximizes the window. This will also show (but not focus) the window if it isn't being displayed already. 476 | 477 | #### `win.unmaximize()` 478 | 479 | Unmaximizes the window. 480 | 481 | #### `win.isMaximized()` 482 | 483 | Returns `Boolean` - Whether the window is maximized. 484 | 485 | #### `win.minimize()` 486 | 487 | Minimizes the window. On some platforms the minimized window will be shown in the Dock. 488 | 489 | #### `win.restore()` 490 | 491 | Restores the window from minimized state to its previous state. 492 | 493 | #### `win.isMinimized()` 494 | 495 | Returns `Boolean` - Whether the window is minimized. 496 | 497 | #### `win.setFullScreen(flag)` 498 | 499 | - `flag` Boolean 500 | 501 | Sets whether the window should be in fullscreen mode. 502 | 503 | #### `win.isFullScreen()` 504 | 505 | Returns `Boolean` - Whether the window is in fullscreen mode. 506 | 507 | #### `win.setAspectRatio(aspectRatio[, extraSize])` 508 | 509 | - `aspectRatio` Float - The aspect ratio to maintain for some portion of the content view. 510 | - `extraSize` [Size](structures/size.md) - The extra size not to be included while maintaining the aspect ratio. 511 | 512 | This will make a window maintain an aspect ratio. The extra size allows a developer to have space, specified in pixels, not included within the aspect ratio calculations. This API already takes into account the difference between a window's size and its content size. 513 | 514 | Consider a normal window with an HD video player and associated controls. Perhaps there are 15 pixels of controls on the left edge, 25 pixels of controls on the right edge and 50 pixels of controls below the player. In order to maintain a 16:9 aspect ratio (standard aspect ratio for HD @1920x1080) within the player itself we would call this function with arguments of 16/9 and [ 40, 50 ]. The second argument doesn't care where the extra width and height are within the content view--only that they exist. Just sum any extra width and height areas you have within the overall content view. 515 | 516 | #### `win.setBounds(bounds[, animate])` 517 | 518 | - `bounds` [Rectangle](rectangle.md) 519 | - `animate` Boolean (optional) 520 | 521 | Resizes and moves the window to the supplied bounds. Any properties that are not supplied will default to their current values. 522 | 523 | ```js 524 | // set all bounds properties 525 | win.setBounds({ x: 440, y: 225, width: 800, height: 600 }) 526 | // set a single bounds property 527 | win.setBounds({ width: 200 }) 528 | // { x: 440, y: 225, width: 200, height: 600 } 529 | console.log(win.getBounds()) 530 | ``` 531 | 532 | #### `win.getBounds()` 533 | 534 | Returns [`Rectangle`](rectangle.md) 535 | 536 | #### `win.setContentBounds(bounds[, animate])` 537 | 538 | - `bounds` [Rectangle](rectangle.md) 539 | - `animate` Boolean (optional) 540 | 541 | Resizes and moves the window's client area (e.g. the web page) to the supplied bounds. 542 | 543 | #### `win.getContentBounds()` 544 | 545 | Returns [`Rectangle`](rectangle.md) 546 | 547 | #### `win.setEnabled(enable)` 548 | 549 | - `enable` Boolean 550 | 551 | Disable or enable the window. 552 | 553 | #### `win.setSize(width, height[, animate])` 554 | 555 | - `width` Integer 556 | - `height` Integer 557 | - `animate` Boolean (optional) 558 | 559 | Resizes the window to `width` and `height`. 560 | 561 | #### `win.getSize()` 562 | 563 | Returns `Integer[]` - Contains the window's width and height. 564 | 565 | #### `win.setContentSize(width, height[, animate])` 566 | 567 | - `width` Integer 568 | - `height` Integer 569 | - `animate` Boolean (optional) 570 | 571 | Resizes the window's client area (e.g. the web page) to `width` and `height`. 572 | 573 | #### `win.getContentSize()` 574 | 575 | Returns `Integer[]` - Contains the window's client area's width and height. 576 | 577 | #### `win.setMinimumSize(width, height)` 578 | 579 | - `width` Integer 580 | - `height` Integer 581 | 582 | Sets the minimum size of window to `width` and `height`. 583 | 584 | #### `win.getMinimumSize()` 585 | 586 | Returns `Integer[]` - Contains the window's minimum width and height. 587 | 588 | #### `win.setMaximumSize(width, height)` 589 | 590 | - `width` Integer 591 | - `height` Integer 592 | 593 | Sets the maximum size of window to `width` and `height`. 594 | 595 | #### `win.getMaximumSize()` 596 | 597 | Returns `Integer[]` - Contains the window's maximum width and height. 598 | 599 | #### `win.setResizable(resizable)` 600 | 601 | - `resizable` Boolean 602 | 603 | Sets whether the window can be manually resized by user. 604 | 605 | #### `win.isResizable()` 606 | 607 | Returns `Boolean` - Whether the window can be manually resized by user. 608 | 609 | #### `win.setMovable(movable)` 610 | 611 | - `movable` Boolean 612 | 613 | Sets whether the window can be moved by user. 614 | 615 | #### `win.isMovable()` 616 | 617 | Returns `Boolean` - Whether the window can be moved by user. 618 | 619 | #### `win.setMinimizable(minimizable)` 620 | 621 | - `minimizable` Boolean 622 | 623 | Sets whether the window can be manually minimized by user. 624 | 625 | #### `win.isMinimizable()` 626 | 627 | Returns `Boolean` - Whether the window can be manually minimized by user. 628 | 629 | #### `win.setMaximizable(maximizable)` 630 | 631 | - `maximizable` Boolean 632 | 633 | Sets whether the window can be manually maximized by user. 634 | 635 | #### `win.isMaximizable()` 636 | 637 | Returns `Boolean` - Whether the window can be manually maximized by user. 638 | 639 | #### `win.setFullScreenable(fullscreenable)` 640 | 641 | - `fullscreenable` Boolean 642 | 643 | Sets whether the maximize/zoom window button toggles fullscreen mode or maximizes the window. 644 | 645 | #### `win.isFullScreenable()` 646 | 647 | Returns `Boolean` - Whether the maximize/zoom window button toggles fullscreen mode or maximizes the window. 648 | 649 | #### `win.setClosable(closable)` 650 | 651 | - `closable` Boolean 652 | 653 | Sets whether the window can be manually closed by user. 654 | 655 | #### `win.isClosable()` 656 | 657 | Returns `Boolean` - Whether the window can be manually closed by user. 658 | 659 | #### `win.setAlwaysOnTop(flag[, level][, relativeLevel])` 660 | 661 | - `flag` Boolean 662 | - `level` String (optional) _macOS_ - Values include `normal`, `floating`, `torn-off-menu`, `modal-panel`, `main-menu`, `status`, `pop-up-menu`, `screen-saver`, and ~~`dock`~~ (Deprecated). The default is `floating`. See the [macOS docs][https://developer.apple.com/documentation/appkit/nswindow/level] for more details. 663 | - `relativeLevel` Integer (optional) - The number of layers higher to set this window relative to the given `level`. The default is `0`. Note that Apple discourages setting levels higher than 1 above `screen-saver`. 664 | 665 | Sets whether the window should show always on top of other windows. After setting this, the window is still a normal window, not a toolbox window which can not be focused on. 666 | 667 | #### `win.isAlwaysOnTop()` 668 | 669 | Returns `Boolean` - Whether the window is always on top of other windows. 670 | 671 | #### `win.moveTop()` 672 | 673 | Moves window to top(z-order) regardless of focus 674 | 675 | #### `win.center()` 676 | 677 | Moves window to the center of the screen. 678 | 679 | #### `win.setPosition(x, y[, animate])` 680 | 681 | - `x` Integer 682 | - `y` Integer 683 | - `animate` Boolean (optional) 684 | 685 | Moves window to `x` and `y`. 686 | 687 | #### `win.getPosition()` 688 | 689 | Returns `Integer[]` - Contains the window's current position. 690 | 691 | #### `win.setTitle(title)` 692 | 693 | - `title` String 694 | 695 | Changes the title of native window to `title`. 696 | 697 | #### `win.getTitle()` 698 | 699 | Returns `String` - The title of the native window. 700 | 701 | **Note:** The title of web page can be different from the title of the native window. 702 | 703 | 717 | 718 | #### `win.flashFrame(flag)` 719 | 720 | - `flag` Boolean 721 | 722 | Starts or stops flashing the window to attract user's attention. 723 | 724 | #### `win.getNativeWindowHandle()` 725 | 726 | Returns `Buffer` - The platform-specific handle of the window. 727 | 728 | The native type of the handle is an `NSPanel`. 729 | 730 | #### `win.loadURL(url)` 731 | 732 | - `url` String 733 | 734 | Same as `webContents.loadURL(url)`. 735 | 736 | The `url` can be a remote address (e.g. `http://`) or a path to a local HTML file using the `file://` protocol. 737 | 738 | To ensure that file URLs are properly formatted, it is recommended to use `require`. 739 | 740 | #### `win.reload()` 741 | 742 | Same as `webContents.reload`. 743 | 744 | #### `win.setHasShadow(hasShadow)` 745 | 746 | - `hasShadow` Boolean 747 | 748 | Sets whether the window should have a shadow. 749 | 750 | #### `win.hasShadow()` 751 | 752 | Returns `Boolean` - Whether the window has a shadow. 753 | 754 | #### `win.setOpacity(opacity)` 755 | 756 | - `opacity` Number - between 0.0 (fully transparent) and 1.0 (fully opaque) 757 | 758 | Sets the opacity of the window. 759 | 760 | #### `win.getOpacity()` 761 | 762 | Returns `Number` - between 0.0 (fully transparent) and 1.0 (fully opaque) 763 | 764 | 767 | 768 | #### `win.setVisibleOnAllWorkspaces(visible)` 769 | 770 | - `visible` Boolean 771 | 772 | Sets whether the window should be visible on all workspaces. 773 | 774 | #### `win.isVisibleOnAllWorkspaces()` 775 | 776 | Returns `Boolean` - Whether the window is visible on all workspaces. 777 | 778 | #### `win.setIgnoreMouseEvents(ignore)` 779 | 780 | - `ignore` Boolean 781 | 782 | Makes the window ignore all mouse events. 783 | 784 | All mouse events happened in this window will be passed to the window below this window, but if this window has focus, it will still receive keyboard events. 785 | 786 | #### `win.setContentProtection(enable)` 787 | 788 | - `enable` Boolean 789 | 790 | Prevents the window contents from being captured by other apps. 791 | 792 | It sets the NSWindow's sharingType to NSWindowSharingNone. 793 | 794 | #### `win.setAutoHideCursor(autoHide)` 795 | 796 | - `autoHide` Boolean 797 | 798 | Controls whether to hide cursor when typing. 799 | 800 | #### `win.setVibrancy(type)` _macOS_ 801 | 802 | - `type` String - Can be `appearance-based`, `light`, `dark`, `titlebar`, `selection`, `menu`, `popover`, `sidebar`, `medium-light` or `ultra-dark`. See the [macOS documentation][vibrancy-docs] for more details. 803 | 804 | Adds a vibrancy effect to the browser window. Passing `null` or an empty string will remove the vibrancy effect on the window. 805 | 806 | [blink-feature-string]: https://cs.chromium.org/chromium/src/third_party/WebKit/Source/platform/runtime_enabled_features.json5?l=70 807 | [page-visibility-api]: https://developer.mozilla.org/en-US/docs/Web/API/Page_Visibility_API 808 | [quick-look]: https://en.wikipedia.org/wiki/Quick_Look 809 | [vibrancy-docs]: https://developer.apple.com/documentation/appkit/nsvisualeffectview?preferredLanguage=objc 810 | [window-levels]: https://developer.apple.com/reference/appkit/nswindow/1664726-window_levels 811 | [chrome-content-scripts]: https://developer.chrome.com/extensions/content_scripts#execution-environment 812 | -------------------------------------------------------------------------------- /docs/communication-plugin-webview.md: -------------------------------------------------------------------------------- 1 | # Communicating with the webview 2 | 3 | When creating a UI, chances are that you will need to communicate between your "frontend" (the webview) and you "backend" (the plugin running in Sketch). 4 | 5 | ## Sending a message to the WebView from your plugin command 6 | 7 | If you want to update the UI when something changes in Sketch (when the selection changes for example), you will need to send a message to the WebView. 8 | 9 | To do so, you need to define a global function in the WebView that you will call from the plugin. 10 | 11 | On the WebView: 12 | 13 | ```js 14 | window.someGlobalFunctionDefinedInTheWebview = function (arg) { 15 | console.log(arg) 16 | } 17 | ``` 18 | 19 | On the plugin: 20 | 21 | ```js 22 | browserWindow.webContents 23 | .executeJavaScript('someGlobalFunctionDefinedInTheWebview("hello")') 24 | .then((res) => { 25 | // do something with the result 26 | }) 27 | ``` 28 | 29 | > Note that values passed to functions within `.executeJavaScript()` must be strings. To pass an object use `JSON.stringify()` 30 | > 31 | > For example : 32 | > 33 | > ```js 34 | > let someObject = { a: 'someValue', b: 5 } 35 | > browserWindow.webContents 36 | > .executeJavaScript( 37 | > `someGlobalFunctionDefinedInTheWebview(${JSON.stringify(someObject)})` 38 | > ) 39 | > .then((res) => { 40 | > // do something with the result 41 | > }) 42 | > ``` 43 | 44 | ## Sending a message to the WebView from another plugin or command 45 | 46 | If you do not have access to the WebView instance (because it was created in another command for example), you can still send a message by using the `id` used to create the WebView. 47 | 48 | ```js 49 | import { isWebviewPresent, sendToWebview } from 'sketch-module-web-view/remote' 50 | 51 | if (isWebviewPresent('unique.id')) { 52 | sendToWebview('unique.id', 'someGlobalFunctionDefinedInTheWebview("hello")') 53 | } 54 | ``` 55 | 56 | ## Sending a message to the plugin from the WebView 57 | 58 | When the user interacts with the WebView, you will probably need to communicate with your plugin. You can do so by listening to the event that the WebView will dispatch. 59 | 60 | For example, if we wanted to log something in the plugin, we could define the `nativeLog` event. 61 | 62 | On the plugin: 63 | 64 | ```js 65 | var sketch = require('sketch') 66 | 67 | browserWindow.webContents.on('nativeLog', function (s) { 68 | sketch.UI.message(s) 69 | 70 | return 'result' 71 | }) 72 | ``` 73 | 74 | On the webview: 75 | 76 | ```js 77 | window.postMessage('nativeLog', 'Called from the webview') 78 | 79 | // you can pass any argument that can be stringified 80 | window.postMessage('nativeLog', { 81 | a: b, 82 | }) 83 | 84 | // you can also pass multiple arguments 85 | window.postMessage('nativeLog', 1, 2, 3) 86 | 87 | // `window.postMessage` returns a Promis with the array of results from plugin listeners 88 | window.postMessage('nativeLog', 'blabla').then((res) => { 89 | // res === ['result'] 90 | }) 91 | ``` 92 | 93 | ##### Note 94 | 95 | If you want to see `console.log` messages from inside your webview you can see this output from `Safari > Develop > {{Your Computer}} > {{Your Plugin}}` 96 | -------------------------------------------------------------------------------- /docs/frameless-window.md: -------------------------------------------------------------------------------- 1 | # Frameless Window 2 | 3 | > Open a window without toolbars, borders, or other graphical "chrome". 4 | 5 | A frameless window is a window that has no [chrome](https://developer.mozilla.org/en-US/docs/Glossary/Chrome), the parts of the window, like toolbars, that are not a part of the web page. These are options on the [`BrowserWindow`](browser-window.md) class. 6 | 7 | ## Create a frameless window 8 | 9 | To create a frameless window, you need to set `frame` to `false` in [BrowserWindow](browser-window.md)'s `options`: 10 | 11 | ```javascript 12 | const BrowserWindow = require('sketch-module-web-view') 13 | let win = new BrowserWindow({ width: 800, height: 600, frame: false }) 14 | win.show() 15 | ``` 16 | 17 | ### Alternatives 18 | 19 | Instead of setting `frame` to `false` which disables both the titlebar and window controls, you may want to have the title bar hidden and your content extend to the full window size, yet still preserve the window controls ("traffic lights") for standard window actions. You can do so by specifying the `titleBarStyle` option: 20 | 21 | #### `hidden` 22 | 23 | Results in a hidden title bar and a full size content window, yet the title bar still has the standard window controls (“traffic lights”) in the top left. 24 | 25 | ```javascript 26 | const BrowserWindow = require('sketch-module-web-view') 27 | let win = new BrowserWindow({ titleBarStyle: 'hidden' }) 28 | win.show() 29 | ``` 30 | 31 | #### `hiddenInset` 32 | 33 | Results in a hidden title bar with an alternative look where the traffic light buttons are slightly more inset from the window edge. 34 | 35 | ```javascript 36 | const BrowserWindow = require('sketch-module-web-view') 37 | let win = new BrowserWindow({ titleBarStyle: 'hiddenInset' }) 38 | win.show() 39 | ``` 40 | 41 | 53 | 54 | ## Transparent window 55 | 56 | By setting the `transparent` option to `true`, you can also make the frameless window transparent: 57 | 58 | ```javascript 59 | const BrowserWindow = require('sketch-module-web-view') 60 | let win = new BrowserWindow({ transparent: true, frame: false }) 61 | win.show() 62 | ``` 63 | 64 | ### Notes 65 | 66 | - Any background color set on or will leak to the whole window, even if they are sized smaller than the window. 67 | - To create a "square" corners effect, size a
5px or so shorter than the actual transparent window height and give it a background color. 68 | 69 | ### Limitations 70 | 71 | - You can not click through the transparent area. We are going to introduce an API to set window shape to solve this, see [our issue](https://github.com/electron/electron/issues/1335) for details. 72 | - Transparent windows are not resizable. Setting `resizable` to `true` may make a transparent window stop working on some platforms. 73 | - The `blur` filter only applies to the web page, so there is no way to apply blur effect to the content below the window (i.e. other applications open on the user's system). 74 | - The native window shadow will not be shown on a transparent window. 75 | 76 | ## Click-through window 77 | 78 | To create a click-through window, i.e. making the window ignore all mouse events, you can call the [win.setIgnoreMouseEvents(ignore)][ignore-mouse-events] API: 79 | 80 | ```javascript 81 | const BrowserWindow = require('sketch-module-web-view') 82 | let win = new BrowserWindow() 83 | win.setIgnoreMouseEvents(true) 84 | ``` 85 | 86 | ## Draggable region 87 | 88 | By default, the frameless window is non-draggable. Apps need to specify the attribute `data-app-region="drag"` to tell which regions are draggable (like the OS's standard titlebar), and apps can also use `data-app-region="no-drag"` to exclude the non-draggable area from the draggable region. 89 | 90 | To make the whole window draggable, you can add `data-app-region="drag"` as `body`'s attribute: 91 | 92 | ```html 93 | 94 | ``` 95 | 96 | ## Text selection 97 | 98 | In a frameless window the dragging behavior may conflict with selecting text. For example, when you drag the titlebar you may accidentally select the text on the titlebar. To prevent this, you need to disable text selection within a draggable area like this: 99 | 100 | ```css 101 | .titlebar { 102 | -webkit-user-select: none; 103 | user-select: none; 104 | } 105 | ``` 106 | -------------------------------------------------------------------------------- /docs/opening-links-in-browser.md: -------------------------------------------------------------------------------- 1 | # Opening an external link in the default browser instead of the WebView 2 | 3 | A link with a `target="_blank"` will have no effect by default in the WebView. In order to open an external link, we will need to go through the Sketch Plugin. 4 | 5 | To achieve that you need 2 parts: 6 | 7 | 1. In the WebView - intercept click events on a link: 8 | 9 | ```js 10 | function interceptClickEvent(event) { 11 | const target = event.target.closest('a') 12 | if (target && target.getAttribute('target') === '_blank') { 13 | event.preventDefault() 14 | window.postMessage('externalLinkClicked', target.href) 15 | } 16 | } 17 | 18 | // listen for link click events at the document level 19 | document.addEventListener('click', interceptClickEvent) 20 | ``` 21 | 22 | 2. In the Sketch Plugin - handle the click: 23 | 24 | ```js 25 | browserWindow.webContent.on('externalLinkClicked', (url) => { 26 | NSWorkspace.sharedWorkspace().openURL(NSURL.URLWithString(url)) 27 | }) 28 | ``` 29 | -------------------------------------------------------------------------------- /docs/rectangle.md: -------------------------------------------------------------------------------- 1 | # Rectangle Object 2 | 3 | - `x` Number - The x coordinate of the origin of the rectangle (must be an integer). 4 | - `y` Number - The y coordinate of the origin of the rectangle (must be an integer). 5 | - `width` Number - The width of the rectangle (must be an integer). 6 | - `height` Number - The height of the rectangle (must be an integer). 7 | -------------------------------------------------------------------------------- /docs/web-contents.md: -------------------------------------------------------------------------------- 1 | # webContents 2 | 3 | > Render and control web pages. 4 | 5 | `webContents` is an [EventEmitter](https://nodejs.org/api/events.html#events_class_eventemitter). It is responsible for rendering and controlling a web page and is a property of the [`BrowserWindow`](browser-window.md) object. An example of accessing the `webContents` object: 6 | 7 | ```javascript 8 | const BrowserWindow = require('sketch-module-web-view') 9 | 10 | let win = new BrowserWindow({ width: 800, height: 1500 }) 11 | win.loadURL('http://github.com') 12 | 13 | let contents = win.webContents 14 | console.log(contents) 15 | ``` 16 | 17 | ## Class: WebContents 18 | 19 | > Render and control the contents of a BrowserWindow instance. 20 | 21 | ### Instance Events 22 | 23 | #### Event: 'did-finish-load' 24 | 25 | Emitted when the navigation is done, i.e. the spinner of the tab has stopped spinning, and the `onload` event was dispatched. 26 | 27 | #### Event: 'did-fail-load' 28 | 29 | Returns: 30 | 31 | - `error` Error 32 | 33 | This event is like `did-finish-load` but emitted when the load failed or was cancelled, e.g. `window.stop()` is invoked. 34 | 35 | #### Event: 'did-frame-finish-load' 36 | 37 | Emitted when a frame has done navigation. 38 | 39 | #### Event: 'did-start-loading' 40 | 41 | Corresponds to the points in time when the spinner of the tab started spinning. 42 | 43 | #### Event: 'did-get-redirect-request' 44 | 45 | Emitted when a redirect is received while requesting a resource. 46 | 47 | #### Event: 'dom-ready' 48 | 49 | Emitted when the document in the given frame is loaded. 50 | 51 | #### Event: 'will-navigate' 52 | 53 | Returns: 54 | 55 | - `event` Event 56 | - `url` String 57 | 58 | Emitted when a user or the page wants to start navigation. It can happen when the `window.location` object is changed or a user clicks a link in the page. 59 | 60 | This event will not emit when the navigation is started programmatically with APIs like `webContents.loadURL` and `webContents.back`. 61 | 62 | It is also not emitted for in-page navigations, such as clicking anchor links or updating the `window.location.hash`. Use `did-navigate-in-page` event for this purpose. 63 | 64 | #### Event: 'did-navigate-in-page' 65 | 66 | Returns: 67 | 68 | - `event` Event 69 | - `url` String 70 | 71 | Emitted when an in-page navigation happened. 72 | 73 | When in-page navigation happens, the page URL changes but does not cause navigation outside of the page. Examples of this occurring are when anchor links are clicked or when the DOM `hashchange` event is triggered. 74 | 75 | ### Instance Methods 76 | 77 | #### `contents.loadURL(url)` 78 | 79 | - `url` String 80 | 81 | The `url` can be a remote address (e.g. `http://`) or a path to a local HTML file using the `file://` protocol. 82 | 83 | To ensure that file URLs are properly formatted, it is recommended to use `require`. 84 | 85 | #### `contents.getURL()` 86 | 87 | Returns `String` - The URL of the current web page. 88 | 89 | ```javascript 90 | const BrowserWindow = require('sketch-module-web-view') 91 | let win = new BrowserWindow({ width: 800, height: 600 }) 92 | win.loadURL('http://github.com') 93 | 94 | let currentURL = win.webContents.getURL() 95 | console.log(currentURL) 96 | ``` 97 | 98 | #### `contents.getTitle()` 99 | 100 | Returns `String` - The title of the current web page. 101 | 102 | #### `contents.isDestroyed()` 103 | 104 | Returns `Boolean` - Whether the web page is destroyed. 105 | 106 | #### `contents.isLoading()` 107 | 108 | Returns `Boolean` - Whether web page is still loading resources. 109 | 110 | #### `contents.stop()` 111 | 112 | Stops any pending navigation. 113 | 114 | #### `contents.reload()` 115 | 116 | Reloads the current web page. 117 | 118 | #### `contents.canGoBack()` 119 | 120 | Returns `Boolean` - Whether the browser can go back to previous web page. 121 | 122 | #### `contents.canGoForward()` 123 | 124 | Returns `Boolean` - Whether the browser can go forward to next web page. 125 | 126 | #### `contents.goBack()` 127 | 128 | Makes the browser go back a web page. 129 | 130 | #### `contents.goForward()` 131 | 132 | Makes the browser go forward a web page. 133 | 134 | #### `contents.executeJavaScript(code[, callback])` 135 | 136 | - `code` String 137 | - `callback` Function (optional) - Called after script has been executed. 138 | - `error` Error | null 139 | - `result` Any 140 | 141 | Returns `Promise` - A promise that resolves with the result of the executed code or is rejected if the result of the code is a rejected promise (or if it fails to execute the code). 142 | 143 | Evaluates `code` in page. 144 | 145 | If the result of the executed code is a promise the callback result will be the resolved value of the promise. We recommend that you use the returned Promise to handle code that results in a Promise. 146 | 147 | ```js 148 | contents 149 | .executeJavaScript( 150 | 'fetch("https://jsonplaceholder.typicode.com/users/1").then(resp => resp.json())', 151 | true 152 | ) 153 | .then((result) => { 154 | console.log(result) // Will be the JSON object from the fetch call 155 | }) 156 | ``` 157 | 158 | #### `contents.undo()` 159 | 160 | Executes the editing command `undo` in web page. 161 | 162 | #### `contents.redo()` 163 | 164 | Executes the editing command `redo` in web page. 165 | 166 | #### `contents.cut()` 167 | 168 | Executes the editing command `cut` in web page. 169 | 170 | #### `contents.copy()` 171 | 172 | Executes the editing command `copy` in web page. 173 | 174 | #### `contents.paste()` 175 | 176 | Executes the editing command `paste` in web page. 177 | 178 | #### `contents.pasteAndMatchStyle()` 179 | 180 | Executes the editing command `pasteAndMatchStyle` in web page. 181 | 182 | #### `contents.delete()` 183 | 184 | Executes the editing command `delete` in web page. 185 | 186 | #### `contents.replace(text)` 187 | 188 | - `text` String 189 | 190 | Executes the editing command `replace` in web page. 191 | 192 | 196 | -------------------------------------------------------------------------------- /lib/__tests__/execute-javascript.js: -------------------------------------------------------------------------------- 1 | /* globals test, expect */ 2 | const { wrapScript } = require('../execute-javascript') 3 | 4 | test('should wrap a script correctly', () => { 5 | expect(wrapScript(`setRandomNumber(3)`, 1)).toBe( 6 | 'window.__skpm_executeJS(1, "setRandomNumber(3)")' 7 | ) 8 | const argument = { json: JSON.stringify({ a: 'a' }) } 9 | const script = `console.log(${JSON.stringify(argument)});` 10 | expect(wrapScript(script, 2)).toBe( 11 | 'window.__skpm_executeJS(2, "console.log({\\"json\\":\\"{\\\\\\"a\\\\\\":\\\\\\"a\\\\\\"}\\"});")' 12 | ) 13 | }) 14 | -------------------------------------------------------------------------------- /lib/browser-api.js: -------------------------------------------------------------------------------- 1 | function parseHexColor(color) { 2 | // Check the string for incorrect formatting. 3 | if (!color || color[0] !== '#') { 4 | if ( 5 | color && 6 | typeof color.isKindOfClass === 'function' && 7 | color.isKindOfClass(NSColor) 8 | ) { 9 | return color 10 | } 11 | throw new Error( 12 | 'Incorrect color formating. It should be an hex color: #RRGGBBAA' 13 | ) 14 | } 15 | 16 | // append FF if alpha channel is not specified. 17 | var source = color.substr(1) 18 | if (source.length === 3) { 19 | source += 'F' 20 | } else if (source.length === 6) { 21 | source += 'FF' 22 | } 23 | // Convert the string from #FFF format to #FFFFFF format. 24 | var hex 25 | if (source.length === 4) { 26 | for (var i = 0; i < 4; i += 1) { 27 | hex += source[i] 28 | hex += source[i] 29 | } 30 | } else if (source.length === 8) { 31 | hex = source 32 | } else { 33 | return NSColor.whiteColor() 34 | } 35 | 36 | var r = parseInt(hex.slice(0, 2), 16) / 255 37 | var g = parseInt(hex.slice(2, 4), 16) / 255 38 | var b = parseInt(hex.slice(4, 6), 16) / 255 39 | var a = parseInt(hex.slice(6, 8), 16) / 255 40 | 41 | return NSColor.colorWithSRGBRed_green_blue_alpha(r, g, b, a) 42 | } 43 | 44 | module.exports = function (browserWindow, panel, webview) { 45 | // keep reference to the subviews 46 | browserWindow._panel = panel 47 | browserWindow._webview = webview 48 | browserWindow._destroyed = false 49 | 50 | browserWindow.destroy = function () { 51 | return panel.close() 52 | } 53 | 54 | browserWindow.close = function () { 55 | if (panel.delegate().utils && panel.delegate().utils.parentWindow) { 56 | var shouldClose = true 57 | browserWindow.emit('close', { 58 | get defaultPrevented() { 59 | return !shouldClose 60 | }, 61 | preventDefault: function () { 62 | shouldClose = false 63 | }, 64 | }) 65 | if (shouldClose) { 66 | panel.delegate().utils.parentWindow.endSheet(panel) 67 | } 68 | return 69 | } 70 | 71 | if (!browserWindow.isClosable()) { 72 | return 73 | } 74 | 75 | panel.performClose(null) 76 | } 77 | 78 | function focus(focused) { 79 | if (!browserWindow.isVisible()) { 80 | return 81 | } 82 | if (focused) { 83 | NSApplication.sharedApplication().activateIgnoringOtherApps(true) 84 | panel.makeKeyAndOrderFront(null) 85 | } else { 86 | panel.orderBack(null) 87 | NSApp.mainWindow().makeKeyAndOrderFront(null) 88 | } 89 | } 90 | 91 | browserWindow.focus = focus.bind(this, true) 92 | browserWindow.blur = focus.bind(this, false) 93 | 94 | browserWindow.isFocused = function () { 95 | return panel.isKeyWindow() 96 | } 97 | 98 | browserWindow.isDestroyed = function () { 99 | return browserWindow._destroyed 100 | } 101 | 102 | browserWindow.show = function () { 103 | // This method is supposed to put focus on window, however if the app does not 104 | // have focus then "makeKeyAndOrderFront" will only show the window. 105 | NSApp.activateIgnoringOtherApps(true) 106 | 107 | if (panel.delegate().utils && panel.delegate().utils.parentWindow) { 108 | return panel.delegate().utils.parentWindow.beginSheet_completionHandler( 109 | panel, 110 | __mocha__.createBlock_function('v16@?0q8', function () { 111 | browserWindow.emit('closed') 112 | }) 113 | ) 114 | } 115 | 116 | return panel.makeKeyAndOrderFront(null) 117 | } 118 | 119 | browserWindow.showInactive = function () { 120 | return panel.orderFrontRegardless() 121 | } 122 | 123 | browserWindow.hide = function () { 124 | return panel.orderOut(null) 125 | } 126 | 127 | browserWindow.isVisible = function () { 128 | return panel.isVisible() 129 | } 130 | 131 | browserWindow.isModal = function () { 132 | return false 133 | } 134 | 135 | browserWindow.maximize = function () { 136 | if (!browserWindow.isMaximized()) { 137 | panel.zoom(null) 138 | } 139 | } 140 | browserWindow.unmaximize = function () { 141 | if (browserWindow.isMaximized()) { 142 | panel.zoom(null) 143 | } 144 | } 145 | 146 | browserWindow.isMaximized = function () { 147 | if ((panel.styleMask() & NSResizableWindowMask) !== 0) { 148 | return panel.isZoomed() 149 | } 150 | var rectScreen = NSScreen.mainScreen().visibleFrame() 151 | var rectWindow = panel.frame() 152 | return ( 153 | rectScreen.origin.x == rectWindow.origin.x && 154 | rectScreen.origin.y == rectWindow.origin.y && 155 | rectScreen.size.width == rectWindow.size.width && 156 | rectScreen.size.height == rectWindow.size.height 157 | ) 158 | } 159 | 160 | browserWindow.minimize = function () { 161 | return panel.miniaturize(null) 162 | } 163 | 164 | browserWindow.restore = function () { 165 | return panel.deminiaturize(null) 166 | } 167 | 168 | browserWindow.isMinimized = function () { 169 | return panel.isMiniaturized() 170 | } 171 | 172 | browserWindow.setFullScreen = function (fullscreen) { 173 | if (fullscreen !== browserWindow.isFullscreen()) { 174 | panel.toggleFullScreen(null) 175 | } 176 | } 177 | 178 | browserWindow.isFullscreen = function () { 179 | return panel.styleMask() & NSFullScreenWindowMask 180 | } 181 | 182 | browserWindow.setAspectRatio = function (aspectRatio /* , extraSize */) { 183 | // Reset the behaviour to default if aspect_ratio is set to 0 or less. 184 | if (aspectRatio > 0.0) { 185 | panel.setAspectRatio(NSMakeSize(aspectRatio, 1.0)) 186 | } else { 187 | panel.setResizeIncrements(NSMakeSize(1.0, 1.0)) 188 | } 189 | } 190 | 191 | browserWindow.setBounds = function (bounds, animate) { 192 | if (!bounds) { 193 | return 194 | } 195 | 196 | // Do nothing if in fullscreen mode. 197 | if (browserWindow.isFullscreen()) { 198 | return 199 | } 200 | 201 | const newBounds = Object.assign(browserWindow.getBounds(), bounds) 202 | 203 | // TODO: Check size constraints since setFrame does not check it. 204 | // var size = bounds.size 205 | // size.SetToMax(GetMinimumSize()); 206 | // gfx::Size max_size = GetMaximumSize(); 207 | // if (!max_size.IsEmpty()) 208 | // size.SetToMin(max_size); 209 | 210 | var cocoaBounds = NSMakeRect( 211 | newBounds.x, 212 | 0, 213 | newBounds.width, 214 | newBounds.height 215 | ) 216 | // Flip Y coordinates based on the primary screen 217 | var screen = NSScreen.screens().firstObject() 218 | cocoaBounds.origin.y = NSHeight(screen.frame()) - newBounds.y 219 | 220 | panel.setFrame_display_animate(cocoaBounds, true, animate) 221 | } 222 | 223 | browserWindow.getBounds = function () { 224 | const cocoaBounds = panel.frame() 225 | var mainScreenRect = NSScreen.screens().firstObject().frame() 226 | return { 227 | x: cocoaBounds.origin.x, 228 | y: Math.round(NSHeight(mainScreenRect) - cocoaBounds.origin.y), 229 | width: cocoaBounds.size.width, 230 | height: cocoaBounds.size.height, 231 | } 232 | } 233 | 234 | browserWindow.setContentBounds = function (bounds, animate) { 235 | // TODO: 236 | browserWindow.setBounds(bounds, animate) 237 | } 238 | 239 | browserWindow.getContentBounds = function () { 240 | // TODO: 241 | return browserWindow.getBounds() 242 | } 243 | 244 | browserWindow.setSize = function (width, height, animate) { 245 | // TODO: handle resizing around center 246 | return browserWindow.setBounds({ width: width, height: height }, animate) 247 | } 248 | 249 | browserWindow.getSize = function () { 250 | var bounds = browserWindow.getBounds() 251 | return [bounds.width, bounds.height] 252 | } 253 | 254 | browserWindow.setContentSize = function (width, height, animate) { 255 | // TODO: handle resizing around center 256 | return browserWindow.setContentBounds( 257 | { width: width, height: height }, 258 | animate 259 | ) 260 | } 261 | 262 | browserWindow.getContentSize = function () { 263 | var bounds = browserWindow.getContentBounds() 264 | return [bounds.width, bounds.height] 265 | } 266 | 267 | browserWindow.setMinimumSize = function (width, height) { 268 | const minSize = CGSizeMake(width, height) 269 | panel.setContentMinSize(minSize) 270 | } 271 | 272 | browserWindow.getMinimumSize = function () { 273 | const size = panel.contentMinSize() 274 | return [size.width, size.height] 275 | } 276 | 277 | browserWindow.setMaximumSize = function (width, height) { 278 | const maxSize = CGSizeMake(width, height) 279 | panel.setContentMaxSize(maxSize) 280 | } 281 | 282 | browserWindow.getMaximumSize = function () { 283 | const size = panel.contentMaxSize() 284 | return [size.width, size.height] 285 | } 286 | 287 | browserWindow.setResizable = function (resizable) { 288 | return browserWindow._setStyleMask(resizable, NSResizableWindowMask) 289 | } 290 | 291 | browserWindow.isResizable = function () { 292 | return panel.styleMask() & NSResizableWindowMask 293 | } 294 | 295 | browserWindow.setMovable = function (movable) { 296 | return panel.setMovable(movable) 297 | } 298 | browserWindow.isMovable = function () { 299 | return panel.isMovable() 300 | } 301 | 302 | browserWindow.setMinimizable = function (minimizable) { 303 | return browserWindow._setStyleMask(minimizable, NSMiniaturizableWindowMask) 304 | } 305 | 306 | browserWindow.isMinimizable = function () { 307 | return panel.styleMask() & NSMiniaturizableWindowMask 308 | } 309 | 310 | browserWindow.setMaximizable = function (maximizable) { 311 | if (panel.standardWindowButton(NSWindowZoomButton)) { 312 | panel.standardWindowButton(NSWindowZoomButton).setEnabled(maximizable) 313 | } 314 | } 315 | 316 | browserWindow.isMaximizable = function () { 317 | return ( 318 | panel.standardWindowButton(NSWindowZoomButton) && 319 | panel.standardWindowButton(NSWindowZoomButton).isEnabled() 320 | ) 321 | } 322 | 323 | browserWindow.setFullScreenable = function (fullscreenable) { 324 | browserWindow._setCollectionBehavior( 325 | fullscreenable, 326 | NSWindowCollectionBehaviorFullScreenPrimary 327 | ) 328 | // On EL Capitan this flag is required to hide fullscreen button. 329 | browserWindow._setCollectionBehavior( 330 | !fullscreenable, 331 | NSWindowCollectionBehaviorFullScreenAuxiliary 332 | ) 333 | } 334 | 335 | browserWindow.isFullScreenable = function () { 336 | var collectionBehavior = panel.collectionBehavior() 337 | return collectionBehavior & NSWindowCollectionBehaviorFullScreenPrimary 338 | } 339 | 340 | browserWindow.setClosable = function (closable) { 341 | browserWindow._setStyleMask(closable, NSClosableWindowMask) 342 | } 343 | 344 | browserWindow.isClosable = function () { 345 | return panel.styleMask() & NSClosableWindowMask 346 | } 347 | 348 | browserWindow.setAlwaysOnTop = function (top, level, relativeLevel) { 349 | var windowLevel = NSNormalWindowLevel 350 | var maxWindowLevel = CGWindowLevelForKey(kCGMaximumWindowLevelKey) 351 | var minWindowLevel = CGWindowLevelForKey(kCGMinimumWindowLevelKey) 352 | 353 | if (top) { 354 | if (level === 'normal') { 355 | windowLevel = NSNormalWindowLevel 356 | } else if (level === 'torn-off-menu') { 357 | windowLevel = NSTornOffMenuWindowLevel 358 | } else if (level === 'modal-panel') { 359 | windowLevel = NSModalPanelWindowLevel 360 | } else if (level === 'main-menu') { 361 | windowLevel = NSMainMenuWindowLevel 362 | } else if (level === 'status') { 363 | windowLevel = NSStatusWindowLevel 364 | } else if (level === 'pop-up-menu') { 365 | windowLevel = NSPopUpMenuWindowLevel 366 | } else if (level === 'screen-saver') { 367 | windowLevel = NSScreenSaverWindowLevel 368 | } else if (level === 'dock') { 369 | // Deprecated by macOS, but kept for backwards compatibility 370 | windowLevel = NSDockWindowLevel 371 | } else { 372 | windowLevel = NSFloatingWindowLevel 373 | } 374 | } 375 | 376 | var newLevel = windowLevel + (relativeLevel || 0) 377 | if (newLevel >= minWindowLevel && newLevel <= maxWindowLevel) { 378 | panel.setLevel(newLevel) 379 | } else { 380 | throw new Error( 381 | 'relativeLevel must be between ' + 382 | minWindowLevel + 383 | ' and ' + 384 | maxWindowLevel 385 | ) 386 | } 387 | } 388 | 389 | browserWindow.isAlwaysOnTop = function () { 390 | return panel.level() !== NSNormalWindowLevel 391 | } 392 | 393 | browserWindow.moveTop = function () { 394 | return panel.orderFrontRegardless() 395 | } 396 | 397 | browserWindow.center = function () { 398 | panel.center() 399 | } 400 | 401 | browserWindow.setPosition = function (x, y, animate) { 402 | return browserWindow.setBounds({ x: x, y: y }, animate) 403 | } 404 | 405 | browserWindow.getPosition = function () { 406 | var bounds = browserWindow.getBounds() 407 | return [bounds.x, bounds.y] 408 | } 409 | 410 | browserWindow.setTitle = function (title) { 411 | panel.setTitle(title) 412 | } 413 | 414 | browserWindow.getTitle = function () { 415 | return String(panel.title()) 416 | } 417 | 418 | var attentionRequestId = 0 419 | browserWindow.flashFrame = function (flash) { 420 | if (flash) { 421 | attentionRequestId = NSApp.requestUserAttention(NSInformationalRequest) 422 | } else { 423 | NSApp.cancelUserAttentionRequest(attentionRequestId) 424 | attentionRequestId = 0 425 | } 426 | } 427 | 428 | browserWindow.getNativeWindowHandle = function () { 429 | return panel 430 | } 431 | 432 | browserWindow.getNativeWebViewHandle = function () { 433 | return webview 434 | } 435 | 436 | browserWindow.loadURL = function (url) { 437 | // When frameLocation is a file, prefix it with the Sketch Resources path 438 | if (/^(?!https?|file).*\.html?$/.test(url)) { 439 | if (typeof __command !== 'undefined' && __command.pluginBundle()) { 440 | url = 441 | 'file://' + __command.pluginBundle().urlForResourceNamed(url).path() 442 | } 443 | } 444 | 445 | if (/^file:\/\/.*\.html?$/.test(url)) { 446 | // ensure URLs containing spaces are properly handled 447 | url = NSString.alloc().initWithString(url) 448 | url = url.stringByAddingPercentEncodingWithAllowedCharacters( 449 | NSCharacterSet.URLQueryAllowedCharacterSet() 450 | ) 451 | webview.loadFileURL_allowingReadAccessToURL( 452 | NSURL.URLWithString(url), 453 | NSURL.URLWithString('file:///') 454 | ) 455 | return 456 | } 457 | 458 | const properURL = NSURL.URLWithString(url) 459 | const urlRequest = NSURLRequest.requestWithURL(properURL) 460 | 461 | webview.loadRequest(urlRequest) 462 | } 463 | 464 | browserWindow.reload = function () { 465 | webview.reload() 466 | } 467 | 468 | browserWindow.setHasShadow = function (hasShadow) { 469 | return panel.setHasShadow(hasShadow) 470 | } 471 | 472 | browserWindow.hasShadow = function () { 473 | return panel.hasShadow() 474 | } 475 | 476 | browserWindow.setOpacity = function (opacity) { 477 | return panel.setAlphaValue(opacity) 478 | } 479 | 480 | browserWindow.getOpacity = function () { 481 | return panel.alphaValue() 482 | } 483 | 484 | browserWindow.setVisibleOnAllWorkspaces = function (visible) { 485 | return browserWindow._setCollectionBehavior( 486 | visible, 487 | NSWindowCollectionBehaviorCanJoinAllSpaces 488 | ) 489 | } 490 | 491 | browserWindow.isVisibleOnAllWorkspaces = function () { 492 | var collectionBehavior = panel.collectionBehavior() 493 | return collectionBehavior & NSWindowCollectionBehaviorCanJoinAllSpaces 494 | } 495 | 496 | browserWindow.setIgnoreMouseEvents = function (ignore) { 497 | return panel.setIgnoresMouseEvents(ignore) 498 | } 499 | 500 | browserWindow.setContentProtection = function (enable) { 501 | panel.setSharingType(enable ? NSWindowSharingNone : NSWindowSharingReadOnly) 502 | } 503 | 504 | browserWindow.setAutoHideCursor = function (autoHide) { 505 | panel.setDisableAutoHideCursor(autoHide) 506 | } 507 | 508 | browserWindow.setVibrancy = function (type) { 509 | var effectView = browserWindow._vibrantView 510 | 511 | if (!type) { 512 | if (effectView == null) { 513 | return 514 | } 515 | 516 | effectView.removeFromSuperview() 517 | panel.setVibrantView(null) 518 | return 519 | } 520 | 521 | if (effectView == null) { 522 | var contentView = panel.contentView() 523 | effectView = NSVisualEffectView.alloc().initWithFrame( 524 | contentView.bounds() 525 | ) 526 | browserWindow._vibrantView = effectView 527 | 528 | effectView.setAutoresizingMask(NSViewWidthSizable | NSViewHeightSizable) 529 | effectView.setBlendingMode(NSVisualEffectBlendingModeBehindWindow) 530 | effectView.setState(NSVisualEffectStateActive) 531 | effectView.setFrame(contentView.bounds()) 532 | contentView.addSubview_positioned_relativeTo( 533 | effectView, 534 | NSWindowBelow, 535 | null 536 | ) 537 | } 538 | 539 | var vibrancyType = NSVisualEffectMaterialLight 540 | 541 | if (type === 'appearance-based') { 542 | vibrancyType = NSVisualEffectMaterialAppearanceBased 543 | } else if (type === 'light') { 544 | vibrancyType = NSVisualEffectMaterialLight 545 | } else if (type === 'dark') { 546 | vibrancyType = NSVisualEffectMaterialDark 547 | } else if (type === 'titlebar') { 548 | vibrancyType = NSVisualEffectMaterialTitlebar 549 | } else if (type === 'selection') { 550 | vibrancyType = NSVisualEffectMaterialSelection 551 | } else if (type === 'menu') { 552 | vibrancyType = NSVisualEffectMaterialMenu 553 | } else if (type === 'popover') { 554 | vibrancyType = NSVisualEffectMaterialPopover 555 | } else if (type === 'sidebar') { 556 | vibrancyType = NSVisualEffectMaterialSidebar 557 | } else if (type === 'medium-light') { 558 | vibrancyType = NSVisualEffectMaterialMediumLight 559 | } else if (type === 'ultra-dark') { 560 | vibrancyType = NSVisualEffectMaterialUltraDark 561 | } 562 | 563 | effectView.setMaterial(vibrancyType) 564 | } 565 | 566 | browserWindow._setBackgroundColor = function (colorName) { 567 | var color = parseHexColor(colorName) 568 | webview.setValue_forKey(false, 'drawsBackground') 569 | panel.backgroundColor = color 570 | } 571 | 572 | browserWindow._invalidate = function () { 573 | panel.flushWindow() 574 | panel.contentView().setNeedsDisplay(true) 575 | } 576 | 577 | browserWindow._setStyleMask = function (on, flag) { 578 | var wasMaximizable = browserWindow.isMaximizable() 579 | if (on) { 580 | panel.setStyleMask(panel.styleMask() | flag) 581 | } else { 582 | panel.setStyleMask(panel.styleMask() & ~flag) 583 | } 584 | // Change style mask will make the zoom button revert to default, probably 585 | // a bug of Cocoa or macOS. 586 | browserWindow.setMaximizable(wasMaximizable) 587 | } 588 | 589 | browserWindow._setCollectionBehavior = function (on, flag) { 590 | var wasMaximizable = browserWindow.isMaximizable() 591 | if (on) { 592 | panel.setCollectionBehavior(panel.collectionBehavior() | flag) 593 | } else { 594 | panel.setCollectionBehavior(panel.collectionBehavior() & ~flag) 595 | } 596 | // Change collectionBehavior will make the zoom button revert to default, 597 | // probably a bug of Cocoa or macOS. 598 | browserWindow.setMaximizable(wasMaximizable) 599 | } 600 | 601 | browserWindow._showWindowButton = function (button) { 602 | var view = panel.standardWindowButton(button) 603 | view.superview().addSubview_positioned_relative(view, NSWindowAbove, null) 604 | } 605 | } 606 | -------------------------------------------------------------------------------- /lib/constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | JS_BRIDGE: '__skpm_sketchBridge', 3 | JS_BRIDGE_RESULT_SUCCESS: '__skpm_sketchBridge_success', 4 | JS_BRIDGE_RESULT_ERROR: '__skpm_sketchBridge_error', 5 | START_MOVING_WINDOW: '__skpm_startMovingWindow', 6 | EXECUTE_JAVASCRIPT: '__skpm_executeJS', 7 | EXECUTE_JAVASCRIPT_SUCCESS: '__skpm_executeJS_success_', 8 | EXECUTE_JAVASCRIPT_ERROR: '__skpm_executeJS_error_', 9 | } 10 | -------------------------------------------------------------------------------- /lib/dispatch-first-click.js: -------------------------------------------------------------------------------- 1 | var tagsToFocus = 2 | '["text", "textarea", "date", "datetime-local", "email", "number", "month", "password", "search", "tel", "time", "url", "week" ]' 3 | 4 | module.exports = function (webView, event) { 5 | var point = webView.convertPoint_fromView(event.locationInWindow(), null) 6 | return ( 7 | 'var el = document.elementFromPoint(' + // get the DOM element that match the event 8 | point.x + 9 | ', ' + 10 | point.y + 11 | '); ' + 12 | 'if (el && el.tagName === "SELECT") {' + // select needs special handling 13 | ' var event = document.createEvent("MouseEvents");' + 14 | ' event.initMouseEvent("mousedown", true, true, window);' + 15 | ' el.dispatchEvent(event);' + 16 | '} else if (el && ' + // some tags need to be focused instead of clicked 17 | tagsToFocus + 18 | '.indexOf(el.type) >= 0 && ' + 19 | 'el.focus' + 20 | ') {' + 21 | 'el.focus();' + // so focus them 22 | '} else if (el) {' + 23 | 'el.dispatchEvent(new Event("click", {bubbles: true}))' + // click the others 24 | '}' 25 | ) 26 | } 27 | -------------------------------------------------------------------------------- /lib/execute-javascript.js: -------------------------------------------------------------------------------- 1 | var CONSTANTS = require('./constants') 2 | 3 | module.exports = function (webview, browserWindow) { 4 | function executeJavaScript(script, userGesture, callback) { 5 | if (typeof userGesture === 'function') { 6 | callback = userGesture 7 | userGesture = false 8 | } 9 | var fiber = coscript.createFiber() 10 | 11 | // if the webview is not ready yet, defer the execution until it is 12 | if ( 13 | webview.navigationDelegate().state && 14 | webview.navigationDelegate().state.wasReady == 0 15 | ) { 16 | return new Promise(function (resolve, reject) { 17 | browserWindow.once('ready-to-show', function () { 18 | executeJavaScript(script, userGesture, callback) 19 | .then(resolve) 20 | .catch(reject) 21 | fiber.cleanup() 22 | }) 23 | }) 24 | } 25 | 26 | return new Promise(function (resolve, reject) { 27 | var requestId = Math.random() 28 | 29 | browserWindow.webContents.on( 30 | CONSTANTS.EXECUTE_JAVASCRIPT_SUCCESS + requestId, 31 | function (res) { 32 | try { 33 | if (callback) { 34 | callback(null, res) 35 | } 36 | resolve(res) 37 | } catch (err) { 38 | reject(err) 39 | } 40 | fiber.cleanup() 41 | } 42 | ) 43 | browserWindow.webContents.on( 44 | CONSTANTS.EXECUTE_JAVASCRIPT_ERROR + requestId, 45 | function (err) { 46 | try { 47 | if (callback) { 48 | callback(err) 49 | resolve() 50 | } else { 51 | reject(err) 52 | } 53 | } catch (err2) { 54 | reject(err2) 55 | } 56 | fiber.cleanup() 57 | } 58 | ) 59 | 60 | webview.evaluateJavaScript_completionHandler( 61 | module.exports.wrapScript(script, requestId), 62 | null 63 | ) 64 | }) 65 | } 66 | 67 | return executeJavaScript 68 | } 69 | 70 | module.exports.wrapScript = function (script, requestId) { 71 | return ( 72 | 'window.' + 73 | CONSTANTS.EXECUTE_JAVASCRIPT + 74 | '(' + 75 | requestId + 76 | ', ' + 77 | JSON.stringify(script) + 78 | ')' 79 | ) 80 | } 81 | 82 | module.exports.injectScript = function (webView) { 83 | var source = 84 | 'window.' + 85 | CONSTANTS.EXECUTE_JAVASCRIPT + 86 | ' = function(id, script) {' + 87 | ' try {' + 88 | ' var res = eval(script);' + 89 | ' if (res && typeof res.then === "function" && typeof res.catch === "function") {' + 90 | ' res.then(function (res2) {' + 91 | ' window.postMessage("' + 92 | CONSTANTS.EXECUTE_JAVASCRIPT_SUCCESS + 93 | '" + id, res2);' + 94 | ' })' + 95 | ' .catch(function (err) {' + 96 | ' window.postMessage("' + 97 | CONSTANTS.EXECUTE_JAVASCRIPT_ERROR + 98 | '" + id, err);' + 99 | ' })' + 100 | ' } else {' + 101 | ' window.postMessage("' + 102 | CONSTANTS.EXECUTE_JAVASCRIPT_SUCCESS + 103 | '" + id, res);' + 104 | ' }' + 105 | ' } catch (err) {' + 106 | ' window.postMessage("' + 107 | CONSTANTS.EXECUTE_JAVASCRIPT_ERROR + 108 | '" + id, err);' + 109 | ' }' + 110 | '}' 111 | var script = WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly( 112 | source, 113 | 0, 114 | true 115 | ) 116 | webView.configuration().userContentController().addUserScript(script) 117 | } 118 | -------------------------------------------------------------------------------- /lib/fitSubview.js: -------------------------------------------------------------------------------- 1 | function addEdgeConstraint(edge, subview, view, constant) { 2 | view.addConstraint( 3 | NSLayoutConstraint.constraintWithItem_attribute_relatedBy_toItem_attribute_multiplier_constant( 4 | subview, 5 | edge, 6 | NSLayoutRelationEqual, 7 | view, 8 | edge, 9 | 1, 10 | constant 11 | ) 12 | ) 13 | } 14 | module.exports = function fitSubviewToView(subview, view, constants) { 15 | constants = constants || [] 16 | subview.setTranslatesAutoresizingMaskIntoConstraints(false) 17 | 18 | addEdgeConstraint(NSLayoutAttributeLeft, subview, view, constants[0] || 0) 19 | addEdgeConstraint(NSLayoutAttributeTop, subview, view, constants[1] || 0) 20 | addEdgeConstraint(NSLayoutAttributeRight, subview, view, constants[2] || 0) 21 | addEdgeConstraint(NSLayoutAttributeBottom, subview, view, constants[3] || 0) 22 | } 23 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* let's try to match the API from Electron's Browser window 2 | (https://github.com/electron/electron/blob/master/docs/api/browser-window.md) */ 3 | var EventEmitter = require('events') 4 | var buildBrowserAPI = require('./browser-api') 5 | var buildWebAPI = require('./webview-api') 6 | var fitSubviewToView = require('./fitSubview') 7 | var dispatchFirstClick = require('./dispatch-first-click') 8 | var injectClientMessaging = require('./inject-client-messaging') 9 | var movableArea = require('./movable-area') 10 | var executeJavaScript = require('./execute-javascript') 11 | var setDelegates = require('./set-delegates') 12 | 13 | function BrowserWindow(options) { 14 | options = options || {} 15 | 16 | var identifier = options.identifier || String(NSUUID.UUID().UUIDString()) 17 | var threadDictionary = NSThread.mainThread().threadDictionary() 18 | 19 | var existingBrowserWindow = BrowserWindow.fromId(identifier) 20 | 21 | // if we already have a window opened, reuse it 22 | if (existingBrowserWindow) { 23 | return existingBrowserWindow 24 | } 25 | 26 | var browserWindow = new EventEmitter() 27 | browserWindow.id = identifier 28 | 29 | if (options.modal && !options.parent) { 30 | throw new Error('A modal needs to have a parent.') 31 | } 32 | 33 | // Long-running script 34 | var fiber = coscript.createFiber() 35 | 36 | // Window size 37 | var width = options.width || 800 38 | var height = options.height || 600 39 | var mainScreenRect = NSScreen.screens().firstObject().frame() 40 | var cocoaBounds = NSMakeRect( 41 | typeof options.x !== 'undefined' 42 | ? options.x 43 | : Math.round((NSWidth(mainScreenRect) - width) / 2), 44 | typeof options.y !== 'undefined' 45 | ? NSHeight(mainScreenRect) - options.y 46 | : Math.round((NSHeight(mainScreenRect) - height) / 2), 47 | width, 48 | height 49 | ) 50 | 51 | if (options.titleBarStyle && options.titleBarStyle !== 'default') { 52 | options.frame = false 53 | } 54 | 55 | var useStandardWindow = options.windowType !== 'textured' 56 | var styleMask = NSTitledWindowMask 57 | 58 | // this is commented out because the toolbar doesn't appear otherwise :thinking-face: 59 | // if (!useStandardWindow || options.frame === false) { 60 | // styleMask = NSFullSizeContentViewWindowMask 61 | // } 62 | if (options.minimizable !== false) { 63 | styleMask |= NSMiniaturizableWindowMask 64 | } 65 | if (options.closable !== false) { 66 | styleMask |= NSClosableWindowMask 67 | } 68 | if (options.resizable !== false) { 69 | styleMask |= NSResizableWindowMask 70 | } 71 | if (!useStandardWindow || options.transparent || options.frame === false) { 72 | styleMask |= NSTexturedBackgroundWindowMask 73 | } 74 | 75 | var panel = NSPanel.alloc().initWithContentRect_styleMask_backing_defer( 76 | cocoaBounds, 77 | styleMask, 78 | NSBackingStoreBuffered, 79 | true 80 | ) 81 | 82 | // this would be nice but it's crashing on macOS 11.0 83 | // panel.releasedWhenClosed = true 84 | 85 | var wkwebviewConfig = WKWebViewConfiguration.alloc().init() 86 | var webView = WKWebView.alloc().initWithFrame_configuration( 87 | CGRectMake(0, 0, options.width || 800, options.height || 600), 88 | wkwebviewConfig 89 | ) 90 | injectClientMessaging(webView) 91 | webView.setAutoresizingMask(NSViewWidthSizable | NSViewHeightSizable) 92 | 93 | buildBrowserAPI(browserWindow, panel, webView) 94 | buildWebAPI(browserWindow, panel, webView) 95 | setDelegates(browserWindow, panel, webView, options) 96 | 97 | if (options.windowType === 'desktop') { 98 | panel.setLevel(kCGDesktopWindowLevel - 1) 99 | // panel.setCanBecomeKeyWindow(false) 100 | panel.setCollectionBehavior( 101 | NSWindowCollectionBehaviorCanJoinAllSpaces | 102 | NSWindowCollectionBehaviorStationary | 103 | NSWindowCollectionBehaviorIgnoresCycle 104 | ) 105 | } 106 | 107 | if ( 108 | typeof options.minWidth !== 'undefined' || 109 | typeof options.minHeight !== 'undefined' 110 | ) { 111 | browserWindow.setMinimumSize(options.minWidth || 0, options.minHeight || 0) 112 | } 113 | 114 | if ( 115 | typeof options.maxWidth !== 'undefined' || 116 | typeof options.maxHeight !== 'undefined' 117 | ) { 118 | browserWindow.setMaximumSize( 119 | options.maxWidth || 10000, 120 | options.maxHeight || 10000 121 | ) 122 | } 123 | 124 | // if (options.focusable === false) { 125 | // panel.setCanBecomeKeyWindow(false) 126 | // } 127 | 128 | if (options.transparent || options.frame === false) { 129 | panel.titlebarAppearsTransparent = true 130 | panel.titleVisibility = NSWindowTitleHidden 131 | panel.setOpaque(0) 132 | panel.isMovableByWindowBackground = true 133 | var toolbar2 = NSToolbar.alloc().initWithIdentifier( 134 | 'titlebarStylingToolbar' 135 | ) 136 | toolbar2.setShowsBaselineSeparator(false) 137 | panel.setToolbar(toolbar2) 138 | } 139 | 140 | if (options.titleBarStyle === 'hiddenInset') { 141 | var toolbar = NSToolbar.alloc().initWithIdentifier('titlebarStylingToolbar') 142 | toolbar.setShowsBaselineSeparator(false) 143 | panel.setToolbar(toolbar) 144 | } 145 | 146 | if (options.frame === false || !options.useContentSize) { 147 | browserWindow.setSize(width, height) 148 | } 149 | 150 | if (options.center) { 151 | browserWindow.center() 152 | } 153 | 154 | if (options.alwaysOnTop) { 155 | browserWindow.setAlwaysOnTop(true) 156 | } 157 | 158 | if (options.fullscreen) { 159 | browserWindow.setFullScreen(true) 160 | } 161 | browserWindow.setFullScreenable(!!options.fullscreenable) 162 | 163 | let title = options.title 164 | if (options.frame === false) { 165 | title = undefined 166 | } else if ( 167 | typeof title === 'undefined' && 168 | typeof __command !== 'undefined' && 169 | __command.pluginBundle() 170 | ) { 171 | title = __command.pluginBundle().name() 172 | } 173 | 174 | if (title) { 175 | browserWindow.setTitle(title) 176 | } 177 | 178 | var backgroundColor = options.backgroundColor 179 | if (options.transparent) { 180 | backgroundColor = NSColor.clearColor() 181 | } 182 | if (!backgroundColor && options.frame === false && options.vibrancy) { 183 | backgroundColor = NSColor.clearColor() 184 | } 185 | 186 | browserWindow._setBackgroundColor( 187 | backgroundColor || NSColor.windowBackgroundColor() 188 | ) 189 | 190 | if (options.hasShadow === false) { 191 | browserWindow.setHasShadow(false) 192 | } 193 | 194 | if (typeof options.opacity !== 'undefined') { 195 | browserWindow.setOpacity(options.opacity) 196 | } 197 | 198 | options.webPreferences = options.webPreferences || {} 199 | 200 | webView 201 | .configuration() 202 | .preferences() 203 | .setValue_forKey( 204 | options.webPreferences.devTools !== false, 205 | 'developerExtrasEnabled' 206 | ) 207 | webView 208 | .configuration() 209 | .preferences() 210 | .setValue_forKey( 211 | options.webPreferences.javascript !== false, 212 | 'javaScriptEnabled' 213 | ) 214 | webView 215 | .configuration() 216 | .preferences() 217 | .setValue_forKey(!!options.webPreferences.plugins, 'plugInsEnabled') 218 | webView 219 | .configuration() 220 | .preferences() 221 | .setValue_forKey( 222 | options.webPreferences.minimumFontSize || 0, 223 | 'minimumFontSize' 224 | ) 225 | 226 | if (options.webPreferences.zoomFactor) { 227 | webView.setMagnification(options.webPreferences.zoomFactor) 228 | } 229 | 230 | var contentView = panel.contentView() 231 | 232 | if (options.frame !== false) { 233 | webView.setFrame(contentView.bounds()) 234 | contentView.addSubview(webView) 235 | } else { 236 | // In OSX 10.10, adding subviews to the root view for the NSView hierarchy 237 | // produces warnings. To eliminate the warnings, we resize the contentView 238 | // to fill the window, and add subviews to that. 239 | // http://crbug.com/380412 240 | contentView.setAutoresizingMask(NSViewWidthSizable | NSViewHeightSizable) 241 | fitSubviewToView(contentView, contentView.superview()) 242 | 243 | webView.setFrame(contentView.bounds()) 244 | contentView.addSubview(webView) 245 | 246 | // The fullscreen button should always be hidden for frameless window. 247 | if (panel.standardWindowButton(NSWindowFullScreenButton)) { 248 | panel.standardWindowButton(NSWindowFullScreenButton).setHidden(true) 249 | } 250 | 251 | if (!options.titleBarStyle || options.titleBarStyle === 'default') { 252 | // Hide the window buttons. 253 | panel.standardWindowButton(NSWindowZoomButton).setHidden(true) 254 | panel.standardWindowButton(NSWindowMiniaturizeButton).setHidden(true) 255 | panel.standardWindowButton(NSWindowCloseButton).setHidden(true) 256 | 257 | // Some third-party macOS utilities check the zoom button's enabled state to 258 | // determine whether to show custom UI on hover, so we disable it here to 259 | // prevent them from doing so in a frameless app window. 260 | panel.standardWindowButton(NSWindowZoomButton).setEnabled(false) 261 | } 262 | } 263 | 264 | if (options.vibrancy) { 265 | browserWindow.setVibrancy(options.vibrancy) 266 | } 267 | 268 | // Set maximizable state last to ensure zoom button does not get reset 269 | // by calls to other APIs. 270 | browserWindow.setMaximizable(options.maximizable !== false) 271 | 272 | panel.setHidesOnDeactivate(options.hidesOnDeactivate !== false) 273 | 274 | if (options.remembersWindowFrame) { 275 | panel.setFrameAutosaveName(identifier) 276 | panel.setFrameUsingName_force(panel.frameAutosaveName(), false) 277 | } 278 | 279 | if (options.acceptsFirstMouse) { 280 | browserWindow.on('focus', function (event) { 281 | if (event.type() === NSEventTypeLeftMouseDown) { 282 | browserWindow.webContents 283 | .executeJavaScript(dispatchFirstClick(webView, event)) 284 | .catch(() => {}) 285 | } 286 | }) 287 | } 288 | 289 | executeJavaScript.injectScript(webView) 290 | movableArea.injectScript(webView) 291 | movableArea.setupHandler(browserWindow) 292 | 293 | if (options.show !== false) { 294 | browserWindow.show() 295 | } 296 | 297 | browserWindow.on('closed', function () { 298 | browserWindow._destroyed = true 299 | threadDictionary.removeObjectForKey(identifier) 300 | var observer = threadDictionary[identifier + '.themeObserver'] 301 | if (observer) { 302 | NSApplication.sharedApplication().removeObserver_forKeyPath( 303 | observer, 304 | 'effectiveAppearance' 305 | ) 306 | threadDictionary.removeObjectForKey(identifier + '.themeObserver') 307 | } 308 | fiber.cleanup() 309 | }) 310 | 311 | threadDictionary[identifier] = panel 312 | 313 | fiber.onCleanup(function () { 314 | if (!browserWindow._destroyed) { 315 | browserWindow.destroy() 316 | } 317 | }) 318 | 319 | return browserWindow 320 | } 321 | 322 | BrowserWindow.fromId = function (identifier) { 323 | var threadDictionary = NSThread.mainThread().threadDictionary() 324 | 325 | if (threadDictionary[identifier]) { 326 | return BrowserWindow.fromPanel(threadDictionary[identifier], identifier) 327 | } 328 | 329 | return undefined 330 | } 331 | 332 | BrowserWindow.fromPanel = function (panel, identifier) { 333 | var browserWindow = new EventEmitter() 334 | browserWindow.id = identifier 335 | 336 | if (!panel || !panel.contentView) { 337 | throw new Error('needs to pass an NSPanel') 338 | } 339 | 340 | var webView = null 341 | var subviews = panel.contentView().subviews() 342 | for (var i = 0; i < subviews.length; i += 1) { 343 | if ( 344 | !webView && 345 | !subviews[i].isKindOfClass(WKInspectorWKWebView) && 346 | subviews[i].isKindOfClass(WKWebView) 347 | ) { 348 | webView = subviews[i] 349 | } 350 | } 351 | 352 | if (!webView) { 353 | throw new Error('The panel needs to have a webview') 354 | } 355 | 356 | buildBrowserAPI(browserWindow, panel, webView) 357 | buildWebAPI(browserWindow, panel, webView) 358 | 359 | return browserWindow 360 | } 361 | 362 | module.exports = BrowserWindow 363 | -------------------------------------------------------------------------------- /lib/inject-client-messaging.js: -------------------------------------------------------------------------------- 1 | var CONSTANTS = require('./constants') 2 | 3 | module.exports = function (webView) { 4 | var source = 5 | 'window.originalPostMessage = window.postMessage;' + 6 | 'window.postMessage = function(actionName) {' + 7 | ' if (!actionName) {' + 8 | " throw new Error('missing action name')" + 9 | ' }' + 10 | ' var id = String(Math.random()).replace(".", "");' + 11 | ' var args = [].slice.call(arguments);' + 12 | ' args.unshift(id);' + 13 | ' return new Promise(function (resolve, reject) {' + 14 | ' window["' + 15 | CONSTANTS.JS_BRIDGE_RESULT_SUCCESS + 16 | '" + id] = resolve;' + 17 | ' window["' + 18 | CONSTANTS.JS_BRIDGE_RESULT_ERROR + 19 | '" + id] = reject;' + 20 | ' window.webkit.messageHandlers.' + 21 | CONSTANTS.JS_BRIDGE + 22 | '.postMessage(JSON.stringify(args));' + 23 | ' });' + 24 | '}' 25 | var script = WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly( 26 | source, 27 | 0, 28 | true 29 | ) 30 | webView.configuration().userContentController().addUserScript(script) 31 | } 32 | -------------------------------------------------------------------------------- /lib/movable-area.js: -------------------------------------------------------------------------------- 1 | var CONSTANTS = require('./constants') 2 | 3 | module.exports.injectScript = function (webView) { 4 | var source = 5 | '(function () {' + 6 | "document.addEventListener('mousedown', onMouseDown);" + 7 | '' + 8 | 'function shouldDrag(target) {' + 9 | ' if (!target || (target.dataset || {}).appRegion === "no-drag") { return false }' + 10 | ' if ((target.dataset || {}).appRegion === "drag") { return true }' + 11 | ' return shouldDrag(target.parentElement)' + 12 | '};' + 13 | '' + 14 | 'function onMouseDown(e) {' + 15 | ' if (e.button !== 0 || !shouldDrag(e.target)) { return }' + 16 | ' window.postMessage("' + 17 | CONSTANTS.START_MOVING_WINDOW + 18 | '");' + 19 | '};' + 20 | '})()' 21 | var script = WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly( 22 | source, 23 | 0, 24 | true 25 | ) 26 | webView.configuration().userContentController().addUserScript(script) 27 | } 28 | 29 | module.exports.setupHandler = function (browserWindow) { 30 | var initialMouseLocation = null 31 | var initialWindowPosition = null 32 | var interval = null 33 | 34 | function moveWindow() { 35 | // if the user released the button, stop moving the window 36 | if (!initialWindowPosition || NSEvent.pressedMouseButtons() !== 1) { 37 | clearInterval(interval) 38 | initialMouseLocation = null 39 | initialWindowPosition = null 40 | return 41 | } 42 | 43 | var mouse = NSEvent.mouseLocation() 44 | browserWindow.setPosition( 45 | initialWindowPosition.x + (mouse.x - initialMouseLocation.x), 46 | initialWindowPosition.y + (initialMouseLocation.y - mouse.y), // y is inverted 47 | false 48 | ) 49 | } 50 | 51 | browserWindow.webContents.on(CONSTANTS.START_MOVING_WINDOW, function () { 52 | initialMouseLocation = NSEvent.mouseLocation() 53 | var position = browserWindow.getPosition() 54 | initialWindowPosition = { 55 | x: position[0], 56 | y: position[1], 57 | } 58 | 59 | interval = setInterval(moveWindow, 1000 / 60) // 60 fps 60 | }) 61 | } 62 | -------------------------------------------------------------------------------- /lib/parseWebArguments.js: -------------------------------------------------------------------------------- 1 | module.exports = function (webArguments) { 2 | var args = null 3 | try { 4 | args = JSON.parse(webArguments) 5 | } catch (e) { 6 | // malformed arguments 7 | } 8 | 9 | if ( 10 | !args || 11 | !args.constructor || 12 | args.constructor !== Array || 13 | args.length == 0 14 | ) { 15 | return null 16 | } 17 | 18 | return args 19 | } 20 | -------------------------------------------------------------------------------- /lib/set-delegates.js: -------------------------------------------------------------------------------- 1 | var ObjCClass = require('mocha-js-delegate') 2 | var parseWebArguments = require('./parseWebArguments') 3 | var CONSTANTS = require('./constants') 4 | 5 | // We create one ObjC class for ourselves here 6 | var WindowDelegateClass 7 | var NavigationDelegateClass 8 | var WebScriptHandlerClass 9 | var ThemeObserverClass 10 | 11 | // TODO: events 12 | // - 'page-favicon-updated' 13 | // - 'new-window' 14 | // - 'did-navigate-in-page' 15 | // - 'will-prevent-unload' 16 | // - 'crashed' 17 | // - 'unresponsive' 18 | // - 'responsive' 19 | // - 'destroyed' 20 | // - 'before-input-event' 21 | // - 'certificate-error' 22 | // - 'found-in-page' 23 | // - 'media-started-playing' 24 | // - 'media-paused' 25 | // - 'did-change-theme-color' 26 | // - 'update-target-url' 27 | // - 'cursor-changed' 28 | // - 'context-menu' 29 | // - 'select-bluetooth-device' 30 | // - 'paint' 31 | // - 'console-message' 32 | 33 | module.exports = function (browserWindow, panel, webview, options) { 34 | if (!ThemeObserverClass) { 35 | ThemeObserverClass = new ObjCClass({ 36 | utils: null, 37 | 38 | 'observeValueForKeyPath:ofObject:change:context:': function ( 39 | keyPath, 40 | object, 41 | change 42 | ) { 43 | const newAppearance = change[NSKeyValueChangeNewKey] 44 | const isDark = 45 | String( 46 | newAppearance.bestMatchFromAppearancesWithNames([ 47 | 'NSAppearanceNameAqua', 48 | 'NSAppearanceNameDarkAqua', 49 | ]) 50 | ) === 'NSAppearanceNameDarkAqua' 51 | 52 | this.utils.executeJavaScript( 53 | "document.body.classList.remove('__skpm-" + 54 | (isDark ? 'light' : 'dark') + 55 | "'); document.body.classList.add('__skpm-" + 56 | (isDark ? 'dark' : 'light') + 57 | "')" 58 | ) 59 | }, 60 | }) 61 | } 62 | 63 | if (!WindowDelegateClass) { 64 | WindowDelegateClass = new ObjCClass({ 65 | utils: null, 66 | panel: null, 67 | 68 | 'windowDidResize:': function () { 69 | this.utils.emit('resize') 70 | }, 71 | 72 | 'windowDidMiniaturize:': function () { 73 | this.utils.emit('minimize') 74 | }, 75 | 76 | 'windowDidDeminiaturize:': function () { 77 | this.utils.emit('restore') 78 | }, 79 | 80 | 'windowDidEnterFullScreen:': function () { 81 | this.utils.emit('enter-full-screen') 82 | }, 83 | 84 | 'windowDidExitFullScreen:': function () { 85 | this.utils.emit('leave-full-screen') 86 | }, 87 | 88 | 'windowDidMove:': function () { 89 | this.utils.emit('move') 90 | this.utils.emit('moved') 91 | }, 92 | 93 | 'windowShouldClose:': function () { 94 | var shouldClose = 1 95 | this.utils.emit('close', { 96 | get defaultPrevented() { 97 | return !shouldClose 98 | }, 99 | preventDefault: function () { 100 | shouldClose = 0 101 | }, 102 | }) 103 | return shouldClose 104 | }, 105 | 106 | 'windowWillClose:': function () { 107 | this.utils.emit('closed') 108 | }, 109 | 110 | 'windowDidBecomeKey:': function () { 111 | this.utils.emit('focus', this.panel.currentEvent()) 112 | }, 113 | 114 | 'windowDidResignKey:': function () { 115 | this.utils.emit('blur') 116 | }, 117 | }) 118 | } 119 | 120 | if (!NavigationDelegateClass) { 121 | NavigationDelegateClass = new ObjCClass({ 122 | state: { 123 | wasReady: 0, 124 | }, 125 | utils: null, 126 | 127 | // // Called when the web view begins to receive web content. 128 | 'webView:didCommitNavigation:': function (webView) { 129 | this.utils.emit('will-navigate', {}, String(String(webView.URL()))) 130 | }, 131 | 132 | // // Called when web content begins to load in a web view. 133 | 'webView:didStartProvisionalNavigation:': function () { 134 | this.utils.emit('did-start-navigation') 135 | this.utils.emit('did-start-loading') 136 | }, 137 | 138 | // Called when a web view receives a server redirect. 139 | 'webView:didReceiveServerRedirectForProvisionalNavigation:': function () { 140 | this.utils.emit('did-get-redirect-request') 141 | }, 142 | 143 | // // Called when the web view needs to respond to an authentication challenge. 144 | // 'webView:didReceiveAuthenticationChallenge:completionHandler:': function( 145 | // webView, 146 | // challenge, 147 | // completionHandler 148 | // ) { 149 | // function callback(username, password) { 150 | // completionHandler( 151 | // 0, 152 | // NSURLCredential.credentialWithUser_password_persistence( 153 | // username, 154 | // password, 155 | // 1 156 | // ) 157 | // ) 158 | // } 159 | // var protectionSpace = challenge.protectionSpace() 160 | // this.utils.emit( 161 | // 'login', 162 | // {}, 163 | // { 164 | // method: String(protectionSpace.authenticationMethod()), 165 | // url: 'not implemented', // TODO: 166 | // referrer: 'not implemented', // TODO: 167 | // }, 168 | // { 169 | // isProxy: !!protectionSpace.isProxy(), 170 | // scheme: String(protectionSpace.protocol()), 171 | // host: String(protectionSpace.host()), 172 | // port: Number(protectionSpace.port()), 173 | // realm: String(protectionSpace.realm()), 174 | // }, 175 | // callback 176 | // ) 177 | // }, 178 | 179 | // Called when an error occurs during navigation. 180 | // 'webView:didFailNavigation:withError:': function( 181 | // webView, 182 | // navigation, 183 | // error 184 | // ) {}, 185 | 186 | // Called when an error occurs while the web view is loading content. 187 | 'webView:didFailProvisionalNavigation:withError:': function ( 188 | webView, 189 | navigation, 190 | error 191 | ) { 192 | this.utils.emit('did-fail-load', error) 193 | }, 194 | 195 | // Called when the navigation is complete. 196 | 'webView:didFinishNavigation:': function () { 197 | if (this.state.wasReady == 0) { 198 | this.state.wasReady = 1 199 | this.utils.emitBrowserEvent('ready-to-show') 200 | } 201 | this.utils.emit('did-navigate') 202 | this.utils.emit('did-frame-navigate') 203 | this.utils.emit('did-stop-loading') 204 | this.utils.emit('did-finish-load') 205 | this.utils.emit('did-frame-finish-load') 206 | }, 207 | 208 | // Called when the web view’s web content process is terminated. 209 | 'webViewWebContentProcessDidTerminate:': function () { 210 | this.utils.emit('dom-ready') 211 | }, 212 | 213 | // Decides whether to allow or cancel a navigation. 214 | // webView:decidePolicyForNavigationAction:decisionHandler: 215 | 216 | // Decides whether to allow or cancel a navigation after its response is known. 217 | // webView:decidePolicyForNavigationResponse:decisionHandler: 218 | }) 219 | } 220 | 221 | if (!WebScriptHandlerClass) { 222 | WebScriptHandlerClass = new ObjCClass({ 223 | utils: null, 224 | 'userContentController:didReceiveScriptMessage:': function (_, message) { 225 | var args = this.utils.parseWebArguments(String(message.body())) 226 | if (!args) { 227 | return 228 | } 229 | if (!args[0] || typeof args[0] !== 'string') { 230 | return 231 | } 232 | args[0] = String(args[0]) 233 | 234 | this.utils.emit.apply(this, args) 235 | }, 236 | }) 237 | } 238 | 239 | var themeObserver = ThemeObserverClass.new({ 240 | utils: { 241 | executeJavaScript(script) { 242 | webview.evaluateJavaScript_completionHandler(script, null) 243 | }, 244 | }, 245 | }) 246 | 247 | var script = WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly( 248 | "document.addEventListener('DOMContentLoaded', function() { document.body.classList.add('__skpm-" + 249 | (typeof MSTheme !== 'undefined' && MSTheme.sharedTheme().isDark() 250 | ? 'dark' 251 | : 'light') + 252 | "') }, false)", 253 | 0, 254 | true 255 | ) 256 | webview.configuration().userContentController().addUserScript(script) 257 | 258 | NSApplication.sharedApplication().addObserver_forKeyPath_options_context( 259 | themeObserver, 260 | 'effectiveAppearance', 261 | NSKeyValueObservingOptionNew, 262 | null 263 | ) 264 | 265 | var threadDictionary = NSThread.mainThread().threadDictionary() 266 | threadDictionary[browserWindow.id + '.themeObserver'] = themeObserver 267 | 268 | var navigationDelegate = NavigationDelegateClass.new({ 269 | utils: { 270 | setTitle: browserWindow.setTitle.bind(browserWindow), 271 | emitBrowserEvent() { 272 | try { 273 | browserWindow.emit.apply(browserWindow, arguments) 274 | } catch (err) { 275 | if ( 276 | typeof process !== 'undefined' && 277 | process.listenerCount && 278 | process.listenerCount('uncaughtException') 279 | ) { 280 | process.emit('uncaughtException', err, 'uncaughtException') 281 | } else { 282 | console.error(err) 283 | throw err 284 | } 285 | } 286 | }, 287 | emit() { 288 | try { 289 | browserWindow.webContents.emit.apply( 290 | browserWindow.webContents, 291 | arguments 292 | ) 293 | } catch (err) { 294 | if ( 295 | typeof process !== 'undefined' && 296 | process.listenerCount && 297 | process.listenerCount('uncaughtException') 298 | ) { 299 | process.emit('uncaughtException', err, 'uncaughtException') 300 | } else { 301 | console.error(err) 302 | throw err 303 | } 304 | } 305 | }, 306 | }, 307 | state: { 308 | wasReady: 0, 309 | }, 310 | }) 311 | 312 | webview.setNavigationDelegate(navigationDelegate) 313 | 314 | var webScriptHandler = WebScriptHandlerClass.new({ 315 | utils: { 316 | emit(id, type) { 317 | if (!type) { 318 | webview.evaluateJavaScript_completionHandler( 319 | CONSTANTS.JS_BRIDGE_RESULT_SUCCESS + id + '()', 320 | null 321 | ) 322 | return 323 | } 324 | 325 | var args = [] 326 | for (var i = 2; i < arguments.length; i += 1) args.push(arguments[i]) 327 | 328 | var listeners = browserWindow.webContents.listeners(type) 329 | 330 | Promise.all( 331 | listeners.map(function (l) { 332 | return Promise.resolve().then(function () { 333 | return l.apply(l, args) 334 | }) 335 | }) 336 | ) 337 | .then(function (res) { 338 | webview.evaluateJavaScript_completionHandler( 339 | CONSTANTS.JS_BRIDGE_RESULT_SUCCESS + 340 | id + 341 | '(' + 342 | JSON.stringify(res) + 343 | ')', 344 | null 345 | ) 346 | }) 347 | .catch(function (err) { 348 | webview.evaluateJavaScript_completionHandler( 349 | CONSTANTS.JS_BRIDGE_RESULT_ERROR + 350 | id + 351 | '(' + 352 | JSON.stringify(err) + 353 | ')', 354 | null 355 | ) 356 | }) 357 | }, 358 | parseWebArguments: parseWebArguments, 359 | }, 360 | }) 361 | 362 | webview 363 | .configuration() 364 | .userContentController() 365 | .addScriptMessageHandler_name(webScriptHandler, CONSTANTS.JS_BRIDGE) 366 | 367 | var utils = { 368 | emit() { 369 | try { 370 | browserWindow.emit.apply(browserWindow, arguments) 371 | } catch (err) { 372 | if ( 373 | typeof process !== 'undefined' && 374 | process.listenerCount && 375 | process.listenerCount('uncaughtException') 376 | ) { 377 | process.emit('uncaughtException', err, 'uncaughtException') 378 | } else { 379 | console.error(err) 380 | throw err 381 | } 382 | } 383 | }, 384 | } 385 | if (options.modal) { 386 | // find the window of the document 387 | var msdocument 388 | if (options.parent.type === 'Document') { 389 | msdocument = options.parent.sketchObject 390 | } else { 391 | msdocument = options.parent 392 | } 393 | if (msdocument && String(msdocument.class()) === 'MSDocumentData') { 394 | // we only have an MSDocumentData instead of a MSDocument 395 | // let's try to get back to the MSDocument 396 | msdocument = msdocument.delegate() 397 | } 398 | utils.parentWindow = msdocument.windowForSheet() 399 | } 400 | 401 | var windowDelegate = WindowDelegateClass.new({ 402 | utils: utils, 403 | panel: panel, 404 | }) 405 | 406 | panel.setDelegate(windowDelegate) 407 | } 408 | -------------------------------------------------------------------------------- /lib/webview-api.js: -------------------------------------------------------------------------------- 1 | var EventEmitter = require('events') 2 | var executeJavaScript = require('./execute-javascript') 3 | 4 | // let's try to match https://github.com/electron/electron/blob/master/docs/api/web-contents.md 5 | module.exports = function buildAPI(browserWindow, panel, webview) { 6 | var webContents = new EventEmitter() 7 | 8 | webContents.loadURL = browserWindow.loadURL 9 | 10 | webContents.loadFile = function (/* filePath */) { 11 | // TODO: 12 | console.warn( 13 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 14 | ) 15 | } 16 | 17 | webContents.downloadURL = function (/* filePath */) { 18 | // TODO: 19 | console.warn( 20 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 21 | ) 22 | } 23 | 24 | webContents.getURL = function () { 25 | return String(webview.URL()) 26 | } 27 | 28 | webContents.getTitle = function () { 29 | return String(webview.title()) 30 | } 31 | 32 | webContents.isDestroyed = function () { 33 | // TODO: 34 | console.warn( 35 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 36 | ) 37 | } 38 | 39 | webContents.focus = browserWindow.focus 40 | webContents.isFocused = browserWindow.isFocused 41 | 42 | webContents.isLoading = function () { 43 | return !!webview.loading() 44 | } 45 | 46 | webContents.isLoadingMainFrame = function () { 47 | // TODO: 48 | return !!webview.loading() 49 | } 50 | 51 | webContents.isWaitingForResponse = function () { 52 | return !webview.loading() 53 | } 54 | 55 | webContents.stop = function () { 56 | webview.stopLoading() 57 | } 58 | webContents.reload = function () { 59 | webview.reload() 60 | } 61 | webContents.reloadIgnoringCache = function () { 62 | webview.reloadFromOrigin() 63 | } 64 | webContents.canGoBack = function () { 65 | return !!webview.canGoBack() 66 | } 67 | webContents.canGoForward = function () { 68 | return !!webview.canGoForward() 69 | } 70 | webContents.canGoToOffset = function (offset) { 71 | return !!webview.backForwardList().itemAtIndex(offset) 72 | } 73 | webContents.clearHistory = function () { 74 | // TODO: 75 | console.warn( 76 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 77 | ) 78 | } 79 | webContents.goBack = function () { 80 | webview.goBack() 81 | } 82 | webContents.goForward = function () { 83 | webview.goForward() 84 | } 85 | webContents.goToIndex = function (index) { 86 | var backForwardList = webview.backForwardList() 87 | var backList = backForwardList.backList() 88 | var backListLength = backList.count() 89 | if (backListLength > index) { 90 | webview.loadRequest(NSURLRequest.requestWithURL(backList[index])) 91 | return 92 | } 93 | var forwardList = backForwardList.forwardList() 94 | if (forwardList.count() > index - backListLength) { 95 | webview.loadRequest( 96 | NSURLRequest.requestWithURL(forwardList[index - backListLength]) 97 | ) 98 | return 99 | } 100 | throw new Error('Cannot go to index ' + index) 101 | } 102 | webContents.goToOffset = function (offset) { 103 | if (!webContents.canGoToOffset(offset)) { 104 | throw new Error('Cannot go to offset ' + offset) 105 | } 106 | webview.loadRequest( 107 | NSURLRequest.requestWithURL(webview.backForwardList().itemAtIndex(offset)) 108 | ) 109 | } 110 | webContents.isCrashed = function () { 111 | // TODO: 112 | console.warn( 113 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 114 | ) 115 | } 116 | webContents.setUserAgent = function (/* userAgent */) { 117 | // TODO: 118 | console.warn( 119 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 120 | ) 121 | } 122 | webContents.getUserAgent = function () { 123 | const userAgent = webview.customUserAgent() 124 | return userAgent ? String(userAgent) : undefined 125 | } 126 | webContents.insertCSS = function (css) { 127 | var source = 128 | "var style = document.createElement('style'); style.innerHTML = " + 129 | css.replace(/"/, '\\"') + 130 | '; document.head.appendChild(style);' 131 | var script = WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly( 132 | source, 133 | 0, 134 | true 135 | ) 136 | webview.configuration().userContentController().addUserScript(script) 137 | } 138 | webContents.insertJS = function (source) { 139 | var script = WKUserScript.alloc().initWithSource_injectionTime_forMainFrameOnly( 140 | source, 141 | 0, 142 | true 143 | ) 144 | webview.configuration().userContentController().addUserScript(script) 145 | } 146 | webContents.executeJavaScript = executeJavaScript(webview, browserWindow) 147 | webContents.setIgnoreMenuShortcuts = function () { 148 | // TODO:?? 149 | console.warn( 150 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 151 | ) 152 | } 153 | webContents.setAudioMuted = function (/* muted */) { 154 | // TODO:?? 155 | console.warn( 156 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 157 | ) 158 | } 159 | webContents.isAudioMuted = function () { 160 | // TODO:?? 161 | console.warn( 162 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 163 | ) 164 | } 165 | webContents.setZoomFactor = function (factor) { 166 | webview.setMagnification_centeredAtPoint(factor, CGPointMake(0, 0)) 167 | } 168 | webContents.getZoomFactor = function (callback) { 169 | callback(Number(webview.magnification())) 170 | } 171 | webContents.setZoomLevel = function (level) { 172 | // eslint-disable-next-line no-restricted-properties 173 | webContents.setZoomFactor(Math.pow(1.2, level)) 174 | } 175 | webContents.getZoomLevel = function (callback) { 176 | // eslint-disable-next-line no-restricted-properties 177 | callback(Math.log(Number(webview.magnification())) / Math.log(1.2)) 178 | } 179 | webContents.setVisualZoomLevelLimits = function (/* minimumLevel, maximumLevel */) { 180 | // TODO:?? 181 | console.warn( 182 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 183 | ) 184 | } 185 | webContents.setLayoutZoomLevelLimits = function (/* minimumLevel, maximumLevel */) { 186 | // TODO:?? 187 | console.warn( 188 | 'Not implemented yet, please open a PR on https://github.com/skpm/sketch-module-web-view :)' 189 | ) 190 | } 191 | 192 | // TODO: 193 | // webContents.undo = function() { 194 | // webview.undoManager().undo() 195 | // } 196 | // webContents.redo = function() { 197 | // webview.undoManager().redo() 198 | // } 199 | // webContents.cut = webview.cut 200 | // webContents.copy = webview.copy 201 | // webContents.paste = webview.paste 202 | // webContents.pasteAndMatchStyle = webview.pasteAsRichText 203 | // webContents.delete = webview.delete 204 | // webContents.replace = webview.replaceSelectionWithText 205 | 206 | webContents.send = function () { 207 | const script = 208 | 'window.postMessage({' + 209 | 'isSketchMessage: true,' + 210 | "origin: '" + 211 | String(__command.identifier()) + 212 | "'," + 213 | 'args: ' + 214 | JSON.stringify([].slice.call(arguments)) + 215 | '}, "*")' 216 | webview.evaluateJavaScript_completionHandler(script, null) 217 | } 218 | 219 | webContents.getNativeWebview = function () { 220 | return webview 221 | } 222 | 223 | browserWindow.webContents = webContents 224 | } 225 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "sketch-module-web-view", 3 | "version": "3.5.1", 4 | "description": "A sketch module for creating an complex UI with a webview", 5 | "main": "lib/index.js", 6 | "types": "./type.d.ts", 7 | "dependencies": { 8 | "mocha-js-delegate": "^0.2.0" 9 | }, 10 | "devDependencies": { 11 | "@skpm/test-runner": "^0.4.0", 12 | "eslint": "^5.16.0", 13 | "eslint-config-airbnb-base": "^13.1.0", 14 | "eslint-config-prettier": "^4.2.0", 15 | "eslint-plugin-import": "^2.17.2", 16 | "eslint-plugin-prettier": "^3.0.1", 17 | "lint-staged": "^8.1.5", 18 | "pre-commit": "^1.2.2", 19 | "prettier": "^1.17.0" 20 | }, 21 | "scripts": { 22 | "test": "npm run lint && skpm-test", 23 | "test:watch": "skpm-test --watch", 24 | "lint": "find . -name \"*.js\" | grep -v -f .gitignore | xargs eslint", 25 | "prettier:base": "prettier --write", 26 | "prettify": "find . -name \"*.js\" | grep -v -f .gitignore | xargs npm run prettier:base", 27 | "lint-staged": "lint-staged" 28 | }, 29 | "repository": { 30 | "type": "git", 31 | "url": "git+https://github.com/mathieudutour/sketch-module-web-view.git" 32 | }, 33 | "keywords": [ 34 | "sketch", 35 | "module", 36 | "webview", 37 | "ui" 38 | ], 39 | "author": "Mathieu Dutour (http://mathieu.dutour.me/)", 40 | "license": "MIT", 41 | "bugs": { 42 | "url": "https://github.com/mathieudutour/sketch-module-web-view/issues" 43 | }, 44 | "homepage": "https://github.com/mathieudutour/sketch-module-web-view#readme", 45 | "pre-commit": [ 46 | "lint-staged" 47 | ], 48 | "lint-staged": { 49 | "*.{js,ts}": [ 50 | "npm run prettier:base", 51 | "eslint", 52 | "git add" 53 | ], 54 | "*.{md}": [ 55 | "npm run prettier:base", 56 | "git add" 57 | ] 58 | }, 59 | "prettier": { 60 | "proseWrap": "never", 61 | "singleQuote": true, 62 | "trailingComma": "es5", 63 | "semi": false 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /remote.js: -------------------------------------------------------------------------------- 1 | /* globals NSThread */ 2 | var threadDictionary = NSThread.mainThread().threadDictionary() 3 | 4 | module.exports.getWebview = function (identifier) { 5 | return require('./lib').fromId(identifier) // eslint-disable-line 6 | } 7 | 8 | module.exports.isWebviewPresent = function isWebviewPresent(identifier) { 9 | return !!threadDictionary[identifier] 10 | } 11 | 12 | module.exports.sendToWebview = function sendToWebview(identifier, evalString) { 13 | if (!module.exports.isWebviewPresent(identifier)) { 14 | return 15 | } 16 | 17 | var panel = threadDictionary[identifier] 18 | var webview = null 19 | var subviews = panel.contentView().subviews() 20 | for (var i = 0; i < subviews.length; i += 1) { 21 | if ( 22 | !webview && 23 | !subviews[i].isKindOfClass(WKInspectorWKWebView) && 24 | subviews[i].isKindOfClass(WKWebView) 25 | ) { 26 | webview = subviews[i] 27 | } 28 | } 29 | 30 | if (!webview || !webview.evaluateJavaScript_completionHandler) { 31 | throw new Error('Webview ' + identifier + ' not found') 32 | } 33 | 34 | webview.evaluateJavaScript_completionHandler(evalString, null) 35 | } 36 | -------------------------------------------------------------------------------- /type.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'sketch-module-web-view' { 2 | import { EventEmitter } from 'events' 3 | 4 | export interface WebPreferences { 5 | /** 6 | * Whether to enable DevTools. If it is set to `false`, can not use 7 | * `BrowserWindow.webContents.openDevTools()` to open DevTools. 8 | * Default is `true`. */ 9 | devTools: boolean 10 | 11 | /** Enables JavaScript support. Default is `true`. */ 12 | javascript?: boolean 13 | 14 | /** Whether plugins should be enabled. Default is `false`. */ 15 | plugins?: boolean 16 | 17 | /** Defaults to `0` */ 18 | minimumFontSize?: number 19 | 20 | /** 21 | * The default zoom factor of the page, `3.0` represents `300%`. 22 | * Default is `1.0`. 23 | */ 24 | zoomFactor?: number 25 | } 26 | 27 | export interface BrowserWindowOptions { 28 | /** Unique identifier of the window */ 29 | identifier?: string 30 | 31 | /** Window's width in pixels. Default is `800`. */ 32 | width?: number 33 | 34 | /** Window's height in pixels. Default is `600`. */ 35 | height?: number 36 | 37 | /** 38 | * (required if y is used) - Window's left offset from screen. 39 | * Default is to center the window. 40 | */ 41 | x?: number 42 | 43 | /** 44 | * (required if x is used) - Window's top offset from screen. 45 | * Default is to center the window. 46 | */ 47 | y?: number 48 | 49 | /** 50 | * Whether the window is removed from the screen when Sketch becomes 51 | * inactive. Default is `true`. 52 | */ 53 | hidesOnDeactivate?: boolean 54 | 55 | /** 56 | * Whether to remember the position and the size of the window the next 57 | * time. Defaults is `false`. 58 | */ 59 | remembersWindowFrame?: boolean 60 | 61 | /** 62 | * The `width` and `height` would be used as web page's size, which means 63 | * the actual window's size will include window frame's size and be slightly 64 | * larger. Default is `false`. 65 | */ 66 | useContentSize?: boolean 67 | 68 | /** Show window in the center of the screen. */ 69 | center?: boolean 70 | 71 | /** Window's minimum width. Default is `0`. */ 72 | minWidth?: number 73 | 74 | /** Window's minimum height. Default is `0`. */ 75 | minHeight?: number 76 | 77 | /** Window's maximum width. Default is no limit. */ 78 | maxWidth?: number 79 | 80 | /** Window's maximum height. Default is no limit. */ 81 | maxHeight?: number 82 | 83 | /** Whether window is resizable. Default is `true`. */ 84 | resizable?: boolean 85 | 86 | /** Whether window is movable. Default is `true`. */ 87 | movable?: boolean 88 | 89 | /** Whether window is minimizable. Default is `true`. */ 90 | minimizable?: boolean 91 | 92 | /** Whether window is maximizable. Default is `true`. */ 93 | maximizable?: boolean 94 | 95 | /** Whether window is closable. Default is `true`. */ 96 | closable?: boolean 97 | 98 | /** 99 | * Whether the window should always stay on top of other windows. 100 | * Default is `false`. 101 | */ 102 | alwaysOnTop?: boolean 103 | 104 | /** 105 | * Whether the window should show in fullscreen. When explicitly set to 106 | * `false` the fullscreen button will be hidden or disabled. 107 | * Default is `false`. 108 | */ 109 | fullscreen?: boolean 110 | 111 | /** 112 | * Whether the window can be put into fullscreen mode. Also whether the 113 | * maximize/zoom button should toggle full screen mode or maximize window. 114 | * Default is `true`. 115 | */ 116 | fullscreenable?: boolean 117 | 118 | /** Default window title. Default is your plugin name. */ 119 | title?: string 120 | 121 | /** Whether window should be shown when created. Default is `true`. */ 122 | show?: boolean 123 | 124 | /** Specify `false` to create a Frameless Window. Default is `true`. */ 125 | frame?: boolean 126 | 127 | /** Specify parent Document. Default is `null`. */ 128 | parent?: any 129 | 130 | /** 131 | * Whether this is a modal window. This only works when the window is a 132 | * child window. Default is `false`. 133 | */ 134 | modal?: boolean 135 | 136 | /** 137 | * Whether the web view accepts a single mouse-down event that 138 | * simultaneously activates the window. Default is `false`. 139 | */ 140 | acceptsFirstMouse?: boolean 141 | 142 | /** Whether to hide cursor when typing. Default is `false`. */ 143 | disableAutoHideCursor?: boolean 144 | 145 | /** 146 | * Window's background color as a hexadecimal value, like `#66CD00` or 147 | * `#FFF` or `#80FFFFFF` (alpha is supported). Default is 148 | * `NSColor.windowBackgroundColor()`. 149 | */ 150 | backgroundColor?: string 151 | 152 | /** Whether window should have a shadow. Default is `true`. */ 153 | hasShadow?: boolean 154 | 155 | /** 156 | * Set the initial opacity of the window, between `0.0` (fully transparent) 157 | * and `1.0` (fully opaque). 158 | */ 159 | opacity?: number 160 | 161 | /** Makes the window transparent. Default is `false`. */ 162 | transparent?: boolean 163 | 164 | /** 165 | * The style of window title bar. Default is `default`. Possible values are: 166 | * - `default` - Results in the standard gray opaque Mac title bar. 167 | * - `hidden` - Results in a hidden title bar and a full size content 168 | * window, yet the title bar still has the standard window controls 169 | * ("traffic lights") in the top left. 170 | * - `hiddenInset` - Results in a hidden title bar with an alternative look 171 | * where the traffic light buttons are slightly more inset from the 172 | * window edge. 173 | */ 174 | titleBarStyle?: 'default' | 'hidden' | 'hiddenInset' 175 | 176 | /** 177 | * Add a type of vibrancy effect to the window, only on macOS. 178 | * Please note that using `frame: false` in combination with a `vibrancy` 179 | * value requires that you use a non-default `titleBarStyle` as well. 180 | */ 181 | vibrancy?: 182 | | 'appearance-based' 183 | | 'light' 184 | | 'dark' 185 | | 'titlebar' 186 | | 'selection' 187 | | 'menu' 188 | | 'popover' 189 | | 'sidebar' 190 | | 'medium-light' 191 | | 'ultra-dark' 192 | 193 | /** Settings of web page's features. */ 194 | webPreferences?: WebPreferences 195 | } 196 | 197 | export interface Rectangle { 198 | /** The height of the rectangle (must be an integer). */ 199 | height: number 200 | 201 | /** The width of the rectangle (must be an integer). */ 202 | width: number 203 | 204 | /** The x coordinate of the origin of the rectangle (must be an integer). */ 205 | x: number 206 | 207 | /** The y coordinate of the origin of the rectangle (must be an integer). */ 208 | y: number 209 | } 210 | 211 | export interface WebContents extends NodeJS.EventEmitter { 212 | /** 213 | * Load web page of given url 214 | * @param url can be a remote address (e.g. `http://`) or a path to a local 215 | * HTML file using the `file://` protocol. 216 | */ 217 | loadURL(url: string): Promise 218 | 219 | /** The URL of the current web page. */ 220 | getURL(): string 221 | 222 | /** The title of the current web page. */ 223 | getTitle(): string 224 | 225 | // todo 226 | /** Whether the web page is destroyed. */ 227 | // isDestroyed(): boolean; 228 | 229 | /** Whether web page is still loading resources. */ 230 | isLoading(): boolean 231 | 232 | /** Whether web page is waiting for response */ 233 | isWaitingForResponse(): boolean 234 | 235 | /** Stops any pending navigation. */ 236 | stop(): void 237 | 238 | /** Reloads the current web page. */ 239 | reload(): void 240 | 241 | /** Reload the current web page by ignoring local cache. */ 242 | reloadIgnoringCache(): void 243 | 244 | /** Whether the browser can go back to previous web page. */ 245 | canGoBack(): boolean 246 | 247 | /** Whether the browser can go forward to next web page. */ 248 | canGoForward(): boolean 249 | 250 | /** Whether the browser can go to any index web package. */ 251 | canGoToOffset(offset: number): boolean 252 | 253 | /** Makes the browser go back a web page. */ 254 | goBack(): void 255 | 256 | /** Makes the browser go forward a web page. */ 257 | goForward(): void 258 | 259 | /** Navigates browser to the specified absolute web page index. */ 260 | goToIndex(index: number): void 261 | 262 | /** Navigates to the specified offset from the "current entry". */ 263 | goToOffset(offset: number): void 264 | 265 | /** Return the user agent string of webview. */ 266 | getUserAgent(): string | undefined 267 | 268 | /** Inject CSS code into a web page. */ 269 | insertCSS(css: string): void 270 | 271 | /** Inject JS code into a web page. */ 272 | insertJS(source: string): void 273 | 274 | /** Evaluates `code` in page. */ 275 | executeJavaScript( 276 | code: string, 277 | callback?: (error: Error | null, result: T) => void 278 | ): Promise 279 | 280 | /** Changes the zoom factor to the specified factor. */ 281 | setZoomFactor(factor: number): void 282 | 283 | /** Returns the current zoom factor. */ 284 | getZoomFactor(callback: (factor: number) => any): void 285 | 286 | /** Changes the zoom level to the specified level. The original size is 0 and each increment above or below represents zooming 20% larger or smaller to default limits. */ 287 | setZoomLevel(level: number): void 288 | 289 | /** Returns the current zoom level. */ 290 | getZoomLevel(callback: (level: number) => any): void 291 | 292 | /** Executes the editing command `undo` in web page. */ 293 | undo(): void 294 | 295 | /** Executes the editing command `redo` in web page. */ 296 | redo(): void 297 | 298 | /** Executes the editing command `cut` in web page. */ 299 | cut(): void 300 | 301 | /** Executes the editing command `copy` in web page. */ 302 | copy(): void 303 | 304 | /** Executes the editing command `paste` in web page. */ 305 | paste(): void 306 | 307 | /** Executes the editing command `pasteAndMatchStyle` in web page. */ 308 | pasteAndMatchStyle(): void 309 | 310 | /** Executes the editing command `delete` in web page. */ 311 | delete(): void 312 | 313 | /** Executes the editing command `replace` in web page. */ 314 | replace(): void 315 | 316 | /** Send message to a web page. */ 317 | send(...args: any[]): void 318 | } 319 | 320 | export class BrowserWindow extends EventEmitter { 321 | /** Create a browser window. */ 322 | constructor(options?: BrowserWindowOptions) 323 | 324 | /** Return window with the given id */ 325 | static fromId(id: string): BrowserWindow 326 | 327 | /** 328 | * A `WebContents` object this window owns. All web page related events and 329 | * operations will be done via it. 330 | */ 331 | webContents: WebContents 332 | 333 | /** An integer representing the unique ID of the window. */ 334 | id: string 335 | 336 | /** 337 | * Force closing the window, the `unload` and `beforeunload` event won't be 338 | * emitted for the web page, and `close` event will also not be emitted for 339 | * this window, but it guarantees the `closed` event will be emitted. 340 | */ 341 | destroy(): void 342 | 343 | /** 344 | * Try to close the window. This has the same effect as a user manually 345 | * clicking the close button of the window. The web page may cancel the 346 | * close though. 347 | */ 348 | close(): void 349 | 350 | /** Focuses on the window. */ 351 | focus(): void 352 | 353 | /** Removes focus from the window. */ 354 | blur(): void 355 | 356 | /** Whether the window is focused. */ 357 | isFocused(): boolean 358 | 359 | /** Whether the window is destroyed. */ 360 | isDestroyed(): boolean 361 | 362 | /** Shows and gives focus to the window. */ 363 | show(): void 364 | 365 | /** Shows the window but doesn't focus on it. */ 366 | showInactive(): void 367 | 368 | /** Hides the window. */ 369 | hide(): void 370 | 371 | /** Whether the window is visible to the user. */ 372 | isVisible(): boolean 373 | 374 | /** Whether current window is a modal window. */ 375 | isModal(): boolean 376 | 377 | /** 378 | * Maximizes the window. This will also show (but not focus) the window if 379 | * it isn't being displayed already. 380 | */ 381 | maximize(): void 382 | 383 | /** Unmaximizes the window. */ 384 | unmaximize(): void 385 | 386 | /** Whether the window is maximized. */ 387 | isMaximized(): boolean 388 | 389 | /** 390 | * Minimizes the window. 391 | * On some platforms the minimized window will be shown in the Dock. 392 | */ 393 | minimize(): void 394 | 395 | /** Restores the window from minimized state to its previous state. */ 396 | restore(): void 397 | 398 | /** Whether the window is minimized. */ 399 | isMinimized(): boolean 400 | 401 | /** Sets whether the window should be in fullscreen mode. */ 402 | setFullScreen(shouldBeFullscreen: boolean): void 403 | 404 | /** Whether the window is in fullscreen mode. */ 405 | isFullScreen(): boolean 406 | 407 | /** 408 | * Make a window maintain an aspect ratio. 409 | * @param aspectRatio The aspect ratio to maintain for some portion of the 410 | * content view. 411 | * @param extraSize The extra size not to be included while maintaining the 412 | * aspect ratio. 413 | */ 414 | setAspectRatio( 415 | aspectRatio: number, 416 | extraSize?: { 417 | width: number 418 | height: number 419 | } 420 | ): void 421 | 422 | /** 423 | * Resizes and moves the window to the supplied bounds. 424 | * @param bounds The supplied bounds. Any properties that are not supplied 425 | * will default to their current values. 426 | * @param animate Whether to show the animation when resizing 427 | */ 428 | setBounds(bounds: Partial, animate: boolean): void 429 | 430 | /** Get the bounds of the browser window */ 431 | getBounds(): Rectangle 432 | 433 | /** Resizes and moves the window's client area to the supplied bounds. */ 434 | setContentBounds(bounds: Rectangle, animate: boolean): void 435 | 436 | /** Get the bounds of the window's client area */ 437 | getContentBounds(): Rectangle 438 | 439 | /** Disable or enable the window. */ 440 | setEnabled(enabled: boolean): void 441 | 442 | /** Resizes the window to given `width` and `height`. */ 443 | setSize(width: number, height: number, animate: boolean): void 444 | 445 | /** Contains the window's width and height. */ 446 | getSize(): [number, number] 447 | 448 | /** Resizes the window's client area to given `width` and `height`. */ 449 | setContentSize(width: number, height: number, animate: boolean): void 450 | 451 | /** Contains the window's client area's width and height. */ 452 | getContentSize(): [number, number] 453 | 454 | /** Sets the minimum size of window to width and height. */ 455 | setMinimumSize(width: number, height: number): void 456 | 457 | /** Contains the window's minimum width and height. */ 458 | getMinimumSize(): [number, number] 459 | 460 | /** Sets the maximum size of window to `width` and `height`. */ 461 | setMaximumSize(width: number, height: number): void 462 | 463 | /** Contains the window's maximum width and height. */ 464 | getMaximumSize(): [number, number] 465 | 466 | /** Sets whether the window can be manually resized by user. */ 467 | setResizable(resizable: boolean): void 468 | 469 | /** Whether the window can be manually resized by user. */ 470 | isResizable(): boolean 471 | 472 | /** Sets whether the window can be moved by user. */ 473 | setMovable(movable: boolean): void 474 | 475 | /** Sets whether the window can be manually minimized by user. */ 476 | setMinimizable(minimizable: boolean): void 477 | 478 | /** Sets whether the window can be manually maximized by user. */ 479 | setMaximizable(maximizable: boolean): void 480 | 481 | /** Whether the window can be manually maximized by user. */ 482 | isMaximizable(): boolean 483 | 484 | /** 485 | * Sets whether the maximize/zoom window button toggles fullscreen mode or 486 | * maximizes the window. 487 | */ 488 | setFullScreenable(fullscreenable: boolean): void 489 | 490 | /** 491 | * Whether the maximize/zoom window button toggles fullscreen mode or 492 | * maximizes the window. 493 | */ 494 | isFullScreenable(): boolean 495 | 496 | /** Sets whether the window can be manually closed by user. */ 497 | setClosable(closable: boolean): void 498 | 499 | /** Whether the window can be manually closed by user. */ 500 | isClosable(): boolean 501 | 502 | /** Sets whether the window should show always on top of other windows. */ 503 | setAlwaysOnTop(flag: boolean, level?: string, relativeLevel?: number): void 504 | 505 | /** Whether the window is always on top of other windows. */ 506 | isAlwaysOnTop(): boolean 507 | 508 | /** Moves window to top(z-order) regardless of focus */ 509 | moveTop(): void 510 | 511 | /** Moves window to the center of the screen. */ 512 | center(): void 513 | 514 | /** Moves window to `x` and `y`. */ 515 | setPosition(x: number, y: number, animate: boolean): void 516 | 517 | /** Contains the window's current position. */ 518 | getPosition(): [number, number] 519 | 520 | /** Changes the title of native window to `title`. */ 521 | setTitle(title: string): void 522 | 523 | /** The title of the native window. */ 524 | getTitle(): string 525 | 526 | /** Starts or stops flashing the window to attract user's attention. */ 527 | flashFrame(flashing: boolean): void 528 | 529 | /** The platform-specific handle of the window. */ 530 | getNativeWindowHandle(): Buffer 531 | 532 | /** Load web page of given url */ 533 | loadURL(url: string): Promise 534 | 535 | /** Reload the page */ 536 | reload(): void 537 | 538 | /** Sets whether the window should have a shadow. */ 539 | setHasShadow(hasShadow: boolean): void 540 | 541 | /** Whether the window has a shadow. */ 542 | hasShadow(): boolean 543 | 544 | /** Sets the opacity of the window. */ 545 | setOpacity(opacity: number): void 546 | 547 | /** Return the opacity of the window */ 548 | getOpacity(): number 549 | 550 | /** Sets whether the window should be visible on all workspaces. */ 551 | setVisibleOnAllWorkspaces(visible: boolean): void 552 | 553 | /** Whether the window is visible on all workspaces. */ 554 | isVisibleOnAllWorkspaces(): boolean 555 | 556 | /** Makes the window ignore all mouse events. */ 557 | setIgnoreMouseEvents(ignore: boolean): void 558 | 559 | /** Prevents the window contents from being captured by other apps. */ 560 | setContentProtection(enabled: boolean): void 561 | 562 | /** Controls whether to hide cursor when typing. */ 563 | setAutoHideCursor(autoHide: boolean): void 564 | 565 | /** Adds a vibrancy effect to the browser window. */ 566 | setVibrancy(type: string | null): void 567 | } 568 | 569 | export default BrowserWindow 570 | } 571 | --------------------------------------------------------------------------------