├── .eslintrc.yml ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── lib ├── index.js ├── message-box.js ├── open-dialog.js ├── run-delegate.js ├── save-dialog.js └── utils.js ├── package.json └── type.d.ts /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | root: true 3 | extends: 4 | - airbnb-base 5 | - plugin:import/errors 6 | - prettier 7 | plugins: 8 | - no-not-accumulator-reassign 9 | - prettier 10 | env: 11 | node: true 12 | rules: 13 | no-param-reassign: 0 14 | import/no-dynamic-require: 0 15 | no-unused-expressions: 0 16 | no-restricted-syntax: 1 17 | no-await-in-loop: 0 18 | no-loop-func: 0 19 | global-require: 0 20 | no-console: 0 21 | no-not-accumulator-reassign/no-not-accumulator-reassign: 22 | [2, ['reduce'], { props: true }] 23 | prefer-arrow-callback: 0 24 | no-var: 0 25 | vars-on-top: 0 26 | eqeqeq: 0 27 | object-shorthand: 0 28 | globals: 29 | coscript: 0 30 | NSOnState: 0 31 | NSButton: 0 32 | NSSavePanel: 0 33 | NSOKButton: 0 34 | NSURL: 0 35 | NSSelectorFromString: 0 36 | NSApp: 0 37 | NSDictionary: 0 38 | NSAlert: 0 39 | NSImage: 0 40 | NSOffState: 0 41 | NSOpenPanel: 0 42 | __command: 0 43 | NSString: 0 44 | __mocha__: 0 45 | -------------------------------------------------------------------------------- /.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 | }, 5 | "files.exclude": { 6 | "**/.git": true, 7 | "**/.svn": true, 8 | "**/.hg": true, 9 | "**/.DS_Store": true, 10 | "**/node_modules": true 11 | }, 12 | "editor.tabSize": 2, 13 | "prettier.semi": false, 14 | "prettier.singleQuote": true, 15 | "prettier.trailingComma": "es5", 16 | "editor.formatOnSave": true 17 | } 18 | 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Sketch dialogs 2 | 3 | A Sketch module for displaying native system dialogs for opening and saving 4 | files, alerting, etc. The API is the mimicking the [Electron dialog API](https://github.com/electron/electron/blob/master/docs/api/dialog.md). 5 | 6 | ## Installation 7 | 8 | To use this module in your Sketch plugin you need a bundler utility like 9 | [skpm](https://github.com/skpm/skpm) and add it as a dependency: 10 | 11 | ```bash 12 | npm i -S @skpm/dialog 13 | ``` 14 | 15 | ## Usage 16 | 17 | An example of showing a dialog to select multiple files and directories: 18 | 19 | ```javascript 20 | import dialog from '@skpm/dialog' 21 | console.log( 22 | dialog.showOpenDialogSync({ 23 | properties: ['openFile', 'openDirectory', 'multiSelections'] 24 | }) 25 | ) 26 | ``` 27 | 28 | ## Methods 29 | 30 | The `dialog` module has the following methods: 31 | 32 | * `showOpenDialog` 33 | * `showOpenDialogSync` 34 | * `showSaveDialog` 35 | * `showSaveDialogSync` 36 | * `showMessageBox` 37 | * `showMessageBoxSync` 38 | 39 | ### `dialog.showOpenDialogSync([document, ]options)` 40 | 41 | * `document` Document (optional) 42 | * `options` Object 43 | * `title` String (optional) 44 | * `defaultPath` String (optional) 45 | * `buttonLabel` String (optional) - Custom label for the confirmation button, 46 | when left empty the default label will be used. 47 | * `filters` FileFilter\[] (optional) 48 | * `properties` String\[] (optional) - Contains which features the dialog should 49 | use. The following values are supported: 50 | * `openFile` - Allow files to be selected. 51 | * `openDirectory` - Allow directories to be selected. 52 | * `multiSelections` - Allow multiple paths to be selected. 53 | * `showHiddenFiles` - Show hidden files in dialog. 54 | * `createDirectory` - Allow creating new directories from dialog. 55 | * `noResolveAliases` - Disable the automatic alias (symlink) path 56 | resolution. Selected aliases will now return the alias path instead of 57 | their target path. 58 | * `treatPackageAsDirectory` - Treat packages, such as `.app` folders, as a 59 | directory instead of a file. 60 | * `message` String (optional) - Message to display above input boxes. 61 | 62 | Returns `String[]`, an array of file paths chosen by the user, if the callback 63 | is provided it returns an empty array. 64 | 65 | The `document` argument allows the dialog to attach itself to a Sketch document 66 | window, making it a sheet. 67 | 68 | The `filters` specifies an array of file types that can be displayed or selected 69 | when you want to limit the user to a specific type. For example: 70 | 71 | ```javascript 72 | { 73 | filters: [ 74 | { name: 'Images', extensions: ['jpg', 'png', 'gif'] }, 75 | { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] }, 76 | { name: 'Custom File Type', extensions: ['as'] } 77 | ] 78 | } 79 | ``` 80 | 81 | The `extensions` array should contain extensions without wildcards or dots (e.g. 82 | `'png'` is good but `'.png'` and `'*.png'` are bad). 83 | 84 | ### `dialog.showOpenDialog([document, ]options)` 85 | 86 | * `document` Document (optional) 87 | * `options` Object 88 | * `title` String (optional) 89 | * `defaultPath` String (optional) 90 | * `buttonLabel` String (optional) - Custom label for the confirmation button, 91 | when left empty the default label will be used. 92 | * `filters` FileFilter\[] (optional) 93 | * `properties` String\[] (optional) - Contains which features the dialog should 94 | use. The following values are supported: 95 | * `openFile` - Allow files to be selected. 96 | * `openDirectory` - Allow directories to be selected. 97 | * `multiSelections` - Allow multiple paths to be selected. 98 | * `showHiddenFiles` - Show hidden files in dialog. 99 | * `createDirectory` - Allow creating new directories from dialog. 100 | * `noResolveAliases` - Disable the automatic alias (symlink) path 101 | resolution. Selected aliases will now return the alias path instead of 102 | their target path. 103 | * `treatPackageAsDirectory` - Treat packages, such as `.app` folders, as a 104 | directory instead of a file. 105 | * `message` String (optional) - Message to display above input boxes. 106 | 107 | Returns `Promise` - Resolve with an object containing the following: 108 | 109 | * `canceled` Boolean - whether or not the dialog was canceled. 110 | * `filePaths` String[] - An array of file paths chosen by the user. If the dialog is cancelled this will be an empty array. 111 | 112 | Returns `String[]`, an array of file paths chosen by the user, if the callback 113 | is provided it returns `undefined`. 114 | 115 | The `document` argument allows the dialog to attach itself to a Sketch document 116 | window, making it a sheet. 117 | 118 | The `filters` specifies an array of file types that can be displayed or selected 119 | when you want to limit the user to a specific type. For example: 120 | 121 | ```javascript 122 | { 123 | filters: [ 124 | { name: 'Images', extensions: ['jpg', 'png', 'gif'] }, 125 | { name: 'Movies', extensions: ['mkv', 'avi', 'mp4'] }, 126 | { name: 'Custom File Type', extensions: ['as'] } 127 | ] 128 | } 129 | ``` 130 | 131 | The `extensions` array should contain extensions without wildcards or dots (e.g. 132 | `'png'` is good but `'.png'` and `'*.png'` are bad). 133 | 134 | ### `dialog.showSaveDialogSync([document, ]options)` 135 | 136 | * `document` Document (optional) 137 | * `options` Object 138 | * `title` String (optional) 139 | * `defaultPath` String (optional) - Absolute directory path, absolute file 140 | path, or file name to use by default. 141 | * `buttonLabel` String (optional) - Custom label for the confirmation button, 142 | when left empty the default label will be used. 143 | * `filters` FileFilter\[] (optional) 144 | * `message` String (optional) - Message to display above text fields. 145 | * `nameFieldLabel` String (optional) - Custom label for the text displayed in 146 | front of the filename text field. 147 | * `showsTagField` Boolean (optional) - Show the tags input box, defaults to 148 | `true`. 149 | 150 | Returns `String`, the path of the file chosen by the user, if a callback is 151 | provided it returns `undefined`. 152 | 153 | The `document` argument allows the dialog to attach itself to a Sketch document 154 | window, making it a sheet. 155 | 156 | The `filters` specifies an array of file types that can be displayed, see 157 | `dialog.showOpenDialog` for an example. 158 | 159 | ### `dialog.showSaveDialog([document, ]options)` 160 | 161 | * `document` Document (optional) 162 | * `options` Object 163 | * `title` String (optional) 164 | * `defaultPath` String (optional) - Absolute directory path, absolute file 165 | path, or file name to use by default. 166 | * `buttonLabel` String (optional) - Custom label for the confirmation button, 167 | when left empty the default label will be used. 168 | * `filters` FileFilter\[] (optional) 169 | * `message` String (optional) - Message to display above text fields. 170 | * `nameFieldLabel` String (optional) - Custom label for the text displayed in 171 | front of the filename text field. 172 | * `showsTagField` Boolean (optional) - Show the tags input box, defaults to 173 | `true`. 174 | 175 | Returns `Promise` - Resolve with an object containing the following: 176 | 177 | * `canceled` Boolean - whether or not the dialog was canceled. 178 | * `filePath` String (optional) - If the dialog is canceled, this will be undefined. 179 | 180 | The `document` argument allows the dialog to attach itself to a Sketch document 181 | window, making it a sheet. 182 | 183 | The `filters` specifies an array of file types that can be displayed, see 184 | `dialog.showOpenDialog` for an example. 185 | 186 | _Note_: Using the asynchronous version is recommended to avoid issues when expanding and collapsing the dialog. 187 | 188 | ### `dialog.showMessageBox([document, ]options)` 189 | 190 | * `document` Document (optional) 191 | * `options` Object 192 | * `type` String (optional) - Can be `"none"`, `"info"`, `"error"`, 193 | `"question"` or `"warning"`. Both `"warning"` and `"error"` display the same 194 | warning icon. 195 | * `buttons` String\[] (optional) - Array of texts for buttons. 196 | * `defaultId` Integer (optional) - Index of the button in the buttons array 197 | which will be selected by default when the message box opens. 198 | * `title` String (optional) - Title of the message box, some platforms will 199 | not show it. 200 | * `message` String - Content of the message box. 201 | * `detail` String (optional) - Extra information of the message. 202 | * `checkboxLabel` String (optional) - If provided, the message box will 203 | include a checkbox with the given label. 204 | * `checkboxChecked` Boolean (optional) - Initial checked state of the 205 | checkbox. `false` by default. 206 | * `icon` String (optional) - path to the image (if you use `skpm`, you can 207 | just `require('./path/to/my/icon.png')`) 208 | 209 | Returns `Integer` - the index of the clicked button. 210 | 211 | Shows a message box, it will block the process until the message box is closed. 212 | 213 | The `document` argument allows the dialog to attach itself to a Sketch document 214 | window, making it a sheet. 215 | 216 | ### `dialog.showMessageBoxSync([document, ]options)` 217 | 218 | * `document` Document (optional) 219 | * `options` Object 220 | * `type` String (optional) - Can be `"none"`, `"info"`, `"error"`, 221 | `"question"` or `"warning"`. Both `"warning"` and `"error"` display the same 222 | warning icon. 223 | * `buttons` String\[] (optional) - Array of texts for buttons. 224 | * `defaultId` Integer (optional) - Index of the button in the buttons array 225 | which will be selected by default when the message box opens. 226 | * `title` String (optional) - Title of the message box, some platforms will 227 | not show it. 228 | * `message` String - Content of the message box. 229 | * `detail` String (optional) - Extra information of the message. 230 | * `checkboxLabel` String (optional) - If provided, the message box will 231 | include a checkbox with the given label. 232 | * `checkboxChecked` Boolean (optional) - Initial checked state of the 233 | checkbox. `false` by default. 234 | * `icon` String (optional) - path to the image (if you use `skpm`, you can 235 | just `require('./path/to/my/icon.png')`) 236 | 237 | Returns `Promise` - resolves with a promise containing the following properties: 238 | 239 | * `response` Number - The index of the clicked button. 240 | * `checkboxChecked` Boolean - The checked state of the checkbox if checkboxLabel was set. Otherwise false. 241 | 242 | Shows a message box, it will block the process until the message box is closed. 243 | 244 | The `document` argument allows the dialog to attach itself to a Sketch document 245 | window, making it a sheet. 246 | 247 | ## Sheets 248 | 249 | Dialogs are presented as sheets attached to a window if you provide a `document` 250 | reference in the `document` parameter, or modals if no document is provided. 251 | -------------------------------------------------------------------------------- /lib/index.js: -------------------------------------------------------------------------------- 1 | /* let's try to match the API from Electron's Dialog 2 | (https://github.com/electron/electron/blob/master/docs/api/dialog.md) */ 3 | 4 | module.exports = { 5 | showOpenDialog: require('./open-dialog').openDialog, 6 | showOpenDialogSync: require('./open-dialog').openDialogSync, 7 | showSaveDialog: require('./save-dialog').saveDialog, 8 | showSaveDialogSync: require('./save-dialog').saveDialogSync, 9 | showMessageBox: require('./message-box').messageBox, 10 | showMessageBoxSync: require('./message-box').messageBoxSync, 11 | // showErrorBox: require('./error-box'), 12 | } 13 | -------------------------------------------------------------------------------- /lib/message-box.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-not-accumulator-reassign/no-not-accumulator-reassign */ 2 | var utils = require('./utils') 3 | 4 | var typeMap = { 5 | none: 0, 6 | info: 1, 7 | error: 2, 8 | question: 1, 9 | warning: 2, 10 | } 11 | 12 | function setupOptions(document, options) { 13 | if ( 14 | !document || 15 | (typeof document.isKindOfClass !== 'function' && !document.sketchObject) 16 | ) { 17 | options = document 18 | document = undefined 19 | } else if (document.sketchObject) { 20 | document = document.sketchObject 21 | } 22 | if (!options) { 23 | options = {} 24 | } 25 | 26 | var dialog = NSAlert.alloc().init() 27 | 28 | if (options.type) { 29 | dialog.alertStyle = typeMap[options.type] || 0 30 | } 31 | 32 | if (options.buttons && options.buttons.length) { 33 | options.buttons.forEach(function addButton(button) { 34 | dialog.addButtonWithTitle( 35 | options.normalizeAccessKeys ? button.replace(/&/g, '') : button 36 | ) 37 | // TODO: add keyboard shortcut if options.normalizeAccessKeys 38 | }) 39 | } 40 | 41 | if (typeof options.defaultId !== 'undefined') { 42 | var buttons = dialog.buttons() 43 | if (options.defaultId < buttons.length) { 44 | // Focus the button at defaultId if the user opted to do so. 45 | // The first button added gets set as the default selected. 46 | // So remove that default, and make the requested button the default. 47 | buttons[0].setKeyEquivalent('') 48 | buttons[options.defaultId].setKeyEquivalent('\r') 49 | } 50 | } 51 | 52 | if (options.title) { 53 | // not shown on macOS 54 | } 55 | 56 | if (options.message) { 57 | dialog.messageText = options.message 58 | } 59 | 60 | if (options.detail) { 61 | dialog.informativeText = options.detail 62 | } 63 | 64 | if (options.checkboxLabel) { 65 | dialog.showsSuppressionButton = true 66 | dialog.suppressionButton().title = options.checkboxLabel 67 | 68 | if (typeof options.checkboxChecked !== 'undefined') { 69 | dialog.suppressionButton().state = options.checkboxChecked 70 | ? NSOnState 71 | : NSOffState 72 | } 73 | } 74 | 75 | if (options.icon) { 76 | if (typeof options.icon === 'string') { 77 | options.icon = NSImage.alloc().initWithContentsOfFile(options.icon) 78 | } 79 | dialog.icon = options.icon 80 | } else if ( 81 | typeof __command !== 'undefined' && 82 | __command.pluginBundle() && 83 | __command.pluginBundle().icon() 84 | ) { 85 | dialog.icon = __command.pluginBundle().icon() 86 | } else { 87 | var icon = NSImage.imageNamed('plugins') 88 | if (icon) { 89 | dialog.icon = icon 90 | } 91 | } 92 | 93 | return { 94 | document: document, 95 | options: options, 96 | dialog: dialog, 97 | } 98 | } 99 | 100 | // https://github.com/electron/electron/blob/master/docs/api/dialog.md#dialogshowmessageboxbrowserwindow-options 101 | module.exports.messageBox = function messageBox(document, options) { 102 | var setup = setupOptions(document, options) 103 | 104 | return utils.runDialog( 105 | setup.dialog, 106 | function getResult(_dialog, returnCode) { 107 | return { 108 | response: 109 | setup.options.buttons && setup.options.buttons.length 110 | ? Number(returnCode) - 1000 111 | : Number(returnCode), 112 | checkboxChecked: _dialog.suppressionButton().state() == NSOnState, 113 | } 114 | }, 115 | setup.document 116 | ) 117 | } 118 | 119 | // https://github.com/electron/electron/blob/master/docs/api/dialog.md#dialogshowmessageboxsyncbrowserwindow-options 120 | module.exports.messageBoxSync = function messageBoxSync(document, options) { 121 | var setup = setupOptions(document, options) 122 | 123 | return utils.runDialogSync( 124 | setup.dialog, 125 | function getResult(_dialog, returnCode) { 126 | return setup.options.buttons && setup.options.buttons.length 127 | ? Number(returnCode) - 1000 128 | : Number(returnCode) 129 | }, 130 | setup.document 131 | ) 132 | } 133 | -------------------------------------------------------------------------------- /lib/open-dialog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-not-accumulator-reassign/no-not-accumulator-reassign */ 2 | var utils = require('./utils') 3 | 4 | function setupOptions(document, options) { 5 | if ( 6 | !document || 7 | (typeof document.isKindOfClass !== 'function' && !document.sketchObject) 8 | ) { 9 | options = document 10 | document = undefined 11 | } 12 | if (!options) { 13 | options = {} 14 | } 15 | 16 | var dialog = NSOpenPanel.openPanel() 17 | 18 | if (options.title) { 19 | dialog.title = options.title 20 | } 21 | 22 | if (options.defaultPath) { 23 | dialog.setDirectoryURL(utils.getURL(options.defaultPath)) 24 | } 25 | 26 | if (options.buttonLabel) { 27 | dialog.prompt = options.buttonLabel 28 | } 29 | 30 | if (options.filters && options.filters.length) { 31 | var exts = [] 32 | options.filters.forEach(function setFilter(filter) { 33 | filter.extensions.forEach(function setExtension(ext) { 34 | exts.push(ext) 35 | }) 36 | }) 37 | 38 | dialog.allowedFileTypes = exts 39 | } 40 | 41 | var hasProperty = 42 | Array.isArray(options.properties) && options.properties.length > 0 43 | dialog.canChooseFiles = 44 | hasProperty && options.properties.indexOf('openFile') !== -1 45 | dialog.canChooseDirectories = 46 | hasProperty && options.properties.indexOf('openDirectory') !== -1 47 | dialog.allowsMultipleSelection = 48 | hasProperty && options.properties.indexOf('multiSelections') !== -1 49 | dialog.showsHiddenFiles = 50 | hasProperty && options.properties.indexOf('showHiddenFiles') !== -1 51 | dialog.canCreateDirectories = 52 | hasProperty && options.properties.indexOf('createDirectory') !== -1 53 | dialog.resolvesAliases = 54 | !hasProperty || options.properties.indexOf('noResolveAliases') === -1 55 | dialog.treatsFilePackagesAsDirectories = 56 | hasProperty && options.properties.indexOf('treatPackageAsDirectory') !== -1 57 | 58 | if (options.message) { 59 | dialog.message = options.message 60 | } 61 | 62 | return { 63 | document: document, 64 | options: options, 65 | dialog: dialog, 66 | } 67 | } 68 | 69 | // https://github.com/electron/electron/blob/master/docs/api/dialog.md#dialogshowopendialogbrowserwindow-options 70 | module.exports.openDialog = function openDialog(document, options) { 71 | var setup = setupOptions(document, options) 72 | 73 | return utils.runDialog( 74 | setup.dialog, 75 | function getResult(_dialog, returnCode) { 76 | if (returnCode != NSOKButton) { 77 | return { 78 | canceled: true, 79 | filePaths: [], 80 | } 81 | } 82 | var result = [] 83 | var urls = _dialog.URLs() 84 | for (var k = 0; k < urls.length; k += 1) { 85 | result.push(String(urls[k].path())) 86 | } 87 | return { 88 | canceled: false, 89 | filePaths: result, 90 | } 91 | }, 92 | setup.document 93 | ) 94 | } 95 | 96 | // https://github.com/electron/electron/blob/master/docs/api/dialog.md#dialogshowopendialogsyncbrowserwindow-options 97 | module.exports.openDialogSync = function openDialogSync(document, options) { 98 | var setup = setupOptions(document, options) 99 | 100 | return utils.runDialogSync( 101 | setup.dialog, 102 | function getResult(_dialog, returnCode) { 103 | if (returnCode != NSOKButton) { 104 | return [] 105 | } 106 | var result = [] 107 | var urls = _dialog.URLs() 108 | for (var k = 0; k < urls.length; k += 1) { 109 | result.push(String(urls[k].path())) 110 | } 111 | return result 112 | }, 113 | setup.document 114 | ) 115 | } 116 | -------------------------------------------------------------------------------- /lib/run-delegate.js: -------------------------------------------------------------------------------- 1 | var ObjCClass = require('mocha-js-delegate') 2 | 3 | var delegate = new ObjCClass({ 4 | options: null, 5 | 6 | 'buttonClicked:': function handleButtonClicked(sender) { 7 | if (this.options.onClicked) { 8 | this.options.onClicked(sender.tag()) 9 | } 10 | this.release() 11 | }, 12 | 13 | 'button0Clicked:': function handleButtonClicked() { 14 | if (this.options.onClicked) { 15 | this.options.onClicked(0) 16 | } 17 | this.release() 18 | }, 19 | 20 | 'button1Clicked:': function handleButtonClicked() { 21 | if (this.options.onClicked) { 22 | this.options.onClicked(1) 23 | } 24 | this.release() 25 | }, 26 | }) 27 | 28 | module.exports = delegate 29 | -------------------------------------------------------------------------------- /lib/save-dialog.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-not-accumulator-reassign/no-not-accumulator-reassign */ 2 | var utils = require('./utils') 3 | 4 | function setupOptions(document, options) { 5 | if ( 6 | !document || 7 | (typeof document.isKindOfClass !== 'function' && !document.sketchObject) 8 | ) { 9 | options = document 10 | document = undefined 11 | } 12 | if (!options) { 13 | options = {} 14 | } 15 | 16 | var dialog = NSSavePanel.savePanel() 17 | 18 | if (options.title) { 19 | dialog.title = options.title 20 | } 21 | 22 | if (options.defaultPath) { 23 | // that's a path 24 | dialog.setDirectoryURL(utils.getURL(options.defaultPath)) 25 | 26 | if ( 27 | options.defaultPath[0] === '.' || 28 | options.defaultPath[0] === '~' || 29 | options.defaultPath[0] === '/' 30 | ) { 31 | var parts = options.defaultPath.split('/') 32 | if (parts.length > 1 && parts[parts.length - 1]) { 33 | dialog.setNameFieldStringValue(parts[parts.length - 1]) 34 | } 35 | } else { 36 | dialog.setNameFieldStringValue(options.defaultPath) 37 | } 38 | } 39 | 40 | if (options.buttonLabel) { 41 | dialog.prompt = options.buttonLabel 42 | } 43 | 44 | if (options.filters && options.filters.length) { 45 | var exts = [] 46 | options.filters.forEach(function setFilter(filter) { 47 | filter.extensions.forEach(function setExtension(ext) { 48 | exts.push(ext) 49 | }) 50 | }) 51 | 52 | if (dialog.allowedContentTypes) { 53 | // Big Sur and newer. 54 | dialog.allowedContentTypes = exts.map((ext) => UTType.typeWithFilenameExtension(ext)) 55 | } else { 56 | // Catalina and older. 57 | dialog.allowedFileTypes = exts 58 | } 59 | } 60 | 61 | if (options.message) { 62 | dialog.message = options.message 63 | } 64 | 65 | if (options.nameFieldLabel) { 66 | dialog.nameFieldLabel = options.nameFieldLabel 67 | } 68 | 69 | if (options.showsTagField) { 70 | dialog.showsTagField = options.showsTagField 71 | } 72 | 73 | return { 74 | document: document, 75 | options: options, 76 | dialog: dialog, 77 | } 78 | } 79 | 80 | // https://github.com/electron/electron/blob/master/docs/api/dialog.md#dialogshowsavedialogbrowserwindow-options 81 | module.exports.saveDialog = function saveDialog(document, options) { 82 | var setup = setupOptions(document, options) 83 | 84 | return utils.runDialog( 85 | setup.dialog, 86 | function getResult(_dialog, returnCode) { 87 | return { 88 | canceled: returnCode != NSOKButton, 89 | filePath: 90 | returnCode == NSOKButton ? String(_dialog.URL().path()) : undefined, 91 | } 92 | }, 93 | setup.document 94 | ) 95 | } 96 | 97 | // https://github.com/electron/electron/blob/master/docs/api/dialog.md#dialogshowsavedialogsyncbrowserwindow-options 98 | module.exports.saveDialogSync = function saveDialogSync(document, options) { 99 | var setup = setupOptions(document, options) 100 | 101 | return utils.runDialogSync( 102 | setup.dialog, 103 | function getResult(_dialog, returnCode) { 104 | return returnCode == NSOKButton ? String(_dialog.URL().path()) : undefined 105 | }, 106 | setup.document 107 | ) 108 | } 109 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | module.exports.getURL = function getURL(path) { 2 | return NSURL.URLWithString( 3 | String( 4 | NSString.stringWithString(path).stringByExpandingTildeInPath() 5 | ).replace(/ /g, '%20') 6 | ) 7 | } 8 | 9 | module.exports.runDialog = function runDialog(dialog, getResult, document) { 10 | if (!document) { 11 | var returnCode = dialog.runModal() 12 | return Promise.resolve(getResult(dialog, returnCode)) 13 | } 14 | 15 | var fiber = coscript.createFiber() 16 | 17 | var window = (document.sketchObject || document).documentWindow() 18 | 19 | return new Promise(function p(resolve, reject) { 20 | dialog.beginSheetModalForWindow_completionHandler( 21 | window, 22 | __mocha__.createBlock_function('v16@?0q8', function onCompletion( 23 | _returnCode 24 | ) { 25 | try { 26 | resolve(getResult(dialog, _returnCode)) 27 | } catch (err) { 28 | reject(err) 29 | } 30 | NSApp.endSheet(dialog) 31 | if (fiber) { 32 | fiber.cleanup() 33 | } else { 34 | coscript.shouldKeepAround = false 35 | } 36 | }) 37 | ) 38 | }) 39 | } 40 | 41 | module.exports.runDialogSync = function runDialog(dialog, getResult, document) { 42 | var returnCode 43 | 44 | if (!document) { 45 | returnCode = dialog.runModal() 46 | return getResult(dialog, returnCode) 47 | } 48 | 49 | var window = (document.sketchObject || document).documentWindow() 50 | 51 | dialog.beginSheetModalForWindow_completionHandler( 52 | window, 53 | __mocha__.createBlock_function('v16@?0q8', function onCompletion( 54 | _returnCode 55 | ) { 56 | NSApp.stopModalWithCode(_returnCode) 57 | }) 58 | ) 59 | 60 | returnCode = NSApp.runModalForWindow(window) 61 | NSApp.endSheet(dialog) 62 | return getResult(dialog, returnCode) 63 | } 64 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@skpm/dialog", 3 | "version": "0.4.2", 4 | "description": "A sketch module for showing dialog", 5 | "main": "lib/index.js", 6 | "types": "./type.d.ts", 7 | "devDependencies": { 8 | "eslint": "^4.8.0", 9 | "eslint-config-airbnb-base": "^12.0.1", 10 | "eslint-config-prettier": "^2.6.0", 11 | "eslint-plugin-import": "^2.7.0", 12 | "eslint-plugin-no-not-accumulator-reassign": "^0.1.0", 13 | "eslint-plugin-prettier": "^2.3.1", 14 | "lint-staged": "^4.2.3", 15 | "pre-commit": "^1.2.2", 16 | "prettier": "^1.7.3", 17 | "rimraf": "^2.6.2" 18 | }, 19 | "scripts": { 20 | "test": "eslint . --ignore-path .gitignore", 21 | "lint-staged": "lint-staged", 22 | "prettier:base": "prettier --single-quote --trailing-comma es5 --no-semi --write", 23 | "prettify": "npm run prettier:base \"./lib/**/*.js\"" 24 | }, 25 | "repository": { 26 | "type": "git", 27 | "url": "git+https://github.com/skpm/dialog.git" 28 | }, 29 | "keywords": [ 30 | "sketch", 31 | "module", 32 | "dialog", 33 | "ui" 34 | ], 35 | "author": "Mathieu Dutour (http://mathieu.dutour.me/)", 36 | "license": "MIT", 37 | "bugs": { 38 | "url": "https://github.com/skpm/dialog/issues" 39 | }, 40 | "homepage": "https://github.com/skpm/dialog#readme", 41 | "dependencies": { 42 | "mocha-js-delegate": "^0.2.0" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /type.d.ts: -------------------------------------------------------------------------------- 1 | declare module "@skpm/dialog" { 2 | namespace dialog { 3 | export type DialogType = "none" | "info" | "error" | "question" | "warning"; 4 | 5 | // An array of file types that can be displayed or selected when you want to limit the user to a specific type. 6 | export interface FileFilter { 7 | name: string; 8 | /** 9 | * The extensions array should contain extensions without wildcards or dots (e.g. 'png' is good but '.png' and '*.png' are bad). 10 | */ 11 | extensions: string[]; 12 | } 13 | 14 | export type OpenDialogProperty = 15 | // Allow files to be selected. 16 | "openFile" 17 | // Allow directories to be selected. 18 | | "openDirectory" 19 | // Allow multiple paths to be selected. 20 | | "multiSelections" 21 | // Show hidden files in dialog. 22 | | "showHiddenFiles" 23 | // Allow creating new directories from dialog. 24 | | "createDirectory" 25 | // Disable the automatic alias (symlink) path resolution. Selected aliases will now return the alias path instead of their target path. 26 | | "noResolveAliases" 27 | // Treat packages, such as `.app` folders, as a directory instead of a file. 28 | | "treatPackageAsDirectory"; 29 | 30 | // open diaglog 31 | export interface OpenDialogOptions { 32 | title?: string; 33 | defaultPath?: string; 34 | /** Custom label for the confirmation button, when left empty the default label will be used. */ 35 | buttonLabel?: string; 36 | filters?: FileFilter[]; 37 | /** Contains which features the dialog should use. */ 38 | properties?: OpenDialogProperty[]; 39 | /** Message to display above input boxes. */ 40 | message?: string; 41 | } 42 | 43 | export interface SaveDialogOptions { 44 | title?: string; 45 | // Absolute directory path, absolute file path, or file name to use by default. 46 | defaultPath?: string; 47 | // Custom label for the confirmation button, when left empty the default label will be used. 48 | buttonLabel?: string; 49 | filters?: FileFilter[]; 50 | // Message to display above text fields. 51 | message?: string; 52 | // Custom label for the text displayed in front of the filename text field. 53 | nameFieldLabel?: string; 54 | // Show the tags input box, defaults to true. 55 | showTagField?: boolean; 56 | } 57 | 58 | // message box 59 | export interface MessageBoxOptions { 60 | // Can be "none", "info", "error", "question" or "warning". Both "warning" and "error" display the same warning icon. 61 | type: DialogType; 62 | // Array of texts for buttons. 63 | buttons?: string[]; 64 | // Index of the button in the buttons array which will be selected by default when the message box opens. 65 | defaultId?: number; 66 | // Title of the message box, some platforms will not show it. 67 | title?: string; 68 | // Content of the message box. 69 | message: string; 70 | // Extra information of the message. 71 | detail?: string; 72 | // If provided, the message box will include a checkbox with the given label. 73 | checkboxLabel?: string; 74 | // Initial checked state of the checkbox. `false` by default. 75 | checkboxChecked?: boolean; 76 | // path to the image (if you use `skpm`, you can just `require('./path/to/my/icon.png'))` 77 | icon: string; 78 | } 79 | 80 | export interface OpenDialogReturn { 81 | /** whether or not the dialog was canceled. */ 82 | canceled: boolean; 83 | /** An array of file paths chosen by the user. If the dialog is cancelled this will be an empty array. */ 84 | filePaths: string[]; 85 | } 86 | 87 | export interface SaveDialogReturn { 88 | /** whether or not the dialog was canceled. */ 89 | canceled: boolean; 90 | /** If the dialog is canceled, this will be undefined. */ 91 | filePath: string | undefined; 92 | } 93 | 94 | /** 95 | * Shows a open dialog box, it will block the process until 96 | * the message box is closed. 97 | */ 98 | export function showOpenDialogSync( 99 | document?: any, 100 | options?: OpenDialogOptions 101 | ): string[]; 102 | 103 | /** 104 | * Shows a open dialog box 105 | */ 106 | export function showOpenDialog( 107 | document?: any, 108 | options?: OpenDialogOptions 109 | ): Promise; 110 | 111 | /** 112 | * Shows a save dialog box, it will block the process until 113 | * the message box is closed. 114 | * 115 | * Return the path of the file chosen by the user. 116 | */ 117 | export function showSaveDialogSync( 118 | document?: any, 119 | options?: SaveDialogOptions 120 | ): string; 121 | 122 | /** 123 | * Shows a save dialog box. 124 | */ 125 | export function showSaveDialog( 126 | document?: any, 127 | options?: SaveDialogOptions 128 | ): Promise; 129 | 130 | /** 131 | * Shows a message box, it will block the process until 132 | * the message box is closed. 133 | */ 134 | export function showMessageBoxSync( 135 | document?: any, 136 | options?: MessageBoxOptions 137 | ): number; 138 | 139 | /** 140 | * Shows a message box 141 | */ 142 | export function showMessageBox( 143 | document?: any, 144 | options?: MessageBoxOptions 145 | ): Promise<{ 146 | response: number; 147 | checkboxChecked: boolean; 148 | }>; 149 | } 150 | 151 | export default dialog; 152 | } 153 | --------------------------------------------------------------------------------