├── .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 |
--------------------------------------------------------------------------------