diff --git a/filenames.auto.gni b/filenames.auto.gni index 1b58bc2260c6..d8baa35249d6 100644 --- a/filenames.auto.gni +++ b/filenames.auto.gni @@ -196,7 +196,7 @@ auto_filenames = { "lib/browser/api/content-tracing.ts", "lib/browser/api/crash-reporter.ts", "lib/browser/api/desktop-capturer.ts", - "lib/browser/api/dialog.js", + "lib/browser/api/dialog.ts", "lib/browser/api/exports/electron.ts", "lib/browser/api/global-shortcut.ts", "lib/browser/api/in-app-purchase.ts", diff --git a/lib/browser/api/dialog.js b/lib/browser/api/dialog.js deleted file mode 100644 index 7f4685c47c9b..000000000000 --- a/lib/browser/api/dialog.js +++ /dev/null @@ -1,270 +0,0 @@ -'use strict'; - -const { app, BrowserWindow, deprecate } = require('electron'); -const binding = process._linkedBinding('electron_browser_dialog'); -const v8Util = process._linkedBinding('electron_common_v8_util'); - -const DialogType = { - OPEN: 'OPEN', - SAVE: 'SAVE' -}; - -const saveFileDialogProperties = { - createDirectory: 1 << 0, - showHiddenFiles: 1 << 1, - treatPackageAsDirectory: 1 << 2, - showOverwriteConfirmation: 1 << 3, - dontAddToRecent: 1 << 4 -}; - -const openFileDialogProperties = { - openFile: 1 << 0, - openDirectory: 1 << 1, - multiSelections: 1 << 2, - createDirectory: 1 << 3, // macOS - showHiddenFiles: 1 << 4, - promptToCreate: 1 << 5, // Windows - noResolveAliases: 1 << 6, // macOS - treatPackageAsDirectory: 1 << 7, // macOS - dontAddToRecent: 1 << 8 // Windows -}; - -const normalizeAccessKey = (text) => { - if (typeof text !== 'string') return text; - - // macOS does not have access keys so remove single ampersands - // and replace double ampersands with a single ampersand - if (process.platform === 'darwin') { - return text.replace(/&(&?)/g, '$1'); - } - - // Linux uses a single underscore as an access key prefix so escape - // existing single underscores with a second underscore, replace double - // ampersands with a single ampersand, and replace a single ampersand with - // a single underscore - if (process.platform === 'linux') { - return text.replace(/_/g, '__').replace(/&(.?)/g, (match, after) => { - if (after === '&') return after; - return `_${after}`; - }); - } - - return text; -}; - -const checkAppInitialized = function () { - if (!app.isReady()) { - throw new Error('dialog module can only be used after app is ready'); - } -}; - -const setupDialogProperties = (type, properties) => { - const dialogPropertiesTypes = (type === DialogType.OPEN) ? openFileDialogProperties : saveFileDialogProperties; - let dialogProperties = 0; - for (const prop in dialogPropertiesTypes) { - if (properties.includes(prop)) { - dialogProperties |= dialogPropertiesTypes[prop]; - } - } - - return dialogProperties; -}; - -const saveDialog = (sync, window, options) => { - checkAppInitialized(); - - if (window && window.constructor !== BrowserWindow) { - options = window; - window = null; - } - - if (options == null) options = { title: 'Save' }; - - const { - buttonLabel = '', - defaultPath = '', - filters = [], - properties = [], - title = '', - message = '', - securityScopedBookmarks = false, - nameFieldLabel = '', - showsTagField = true - } = options; - - if (typeof title !== 'string') throw new TypeError('Title must be a string'); - if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string'); - if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string'); - if (typeof message !== 'string') throw new TypeError('Message must be a string'); - if (typeof nameFieldLabel !== 'string') throw new TypeError('Name field label must be a string'); - - const settings = { buttonLabel, defaultPath, filters, title, message, securityScopedBookmarks, nameFieldLabel, showsTagField, window }; - settings.properties = setupDialogProperties(DialogType.SAVE, properties); - - return (sync) ? binding.showSaveDialogSync(settings) : binding.showSaveDialog(settings); -}; - -const openDialog = (sync, window, options) => { - checkAppInitialized(); - - if (window && window.constructor !== BrowserWindow) { - options = window; - window = null; - } - - if (options == null) { - options = { - title: 'Open', - properties: ['openFile'] - }; - } - - const { - buttonLabel = '', - defaultPath = '', - filters = [], - properties = ['openFile'], - title = '', - message = '', - securityScopedBookmarks = false - } = options; - - if (!Array.isArray(properties)) throw new TypeError('Properties must be an array'); - - if (typeof title !== 'string') throw new TypeError('Title must be a string'); - if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string'); - if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string'); - if (typeof message !== 'string') throw new TypeError('Message must be a string'); - - const settings = { title, buttonLabel, defaultPath, filters, message, securityScopedBookmarks, window }; - settings.properties = setupDialogProperties(DialogType.OPEN, properties); - - return (sync) ? binding.showOpenDialogSync(settings) : binding.showOpenDialog(settings); -}; - -const messageBox = (sync, window, options) => { - checkAppInitialized(); - - if (window && window.constructor !== BrowserWindow) { - options = window; - window = null; - } - - if (options == null) options = { type: 'none' }; - - const messageBoxTypes = ['none', 'info', 'warning', 'error', 'question']; - const messageBoxOptions = { noLink: 1 << 0 }; - - let { - buttons = [], - cancelId, - checkboxLabel = '', - checkboxChecked, - defaultId = -1, - detail = '', - icon = null, - noLink = false, - message = '', - title = '', - type = 'none' - } = options; - - const messageBoxType = messageBoxTypes.indexOf(type); - if (messageBoxType === -1) throw new TypeError('Invalid message box type'); - if (!Array.isArray(buttons)) throw new TypeError('Buttons must be an array'); - if (options.normalizeAccessKeys) buttons = buttons.map(normalizeAccessKey); - if (typeof title !== 'string') throw new TypeError('Title must be a string'); - if (typeof noLink !== 'boolean') throw new TypeError('noLink must be a boolean'); - if (typeof message !== 'string') throw new TypeError('Message must be a string'); - if (typeof detail !== 'string') throw new TypeError('Detail must be a string'); - if (typeof checkboxLabel !== 'string') throw new TypeError('checkboxLabel must be a string'); - - checkboxChecked = !!checkboxChecked; - if (checkboxChecked && !checkboxLabel) { - throw new Error('checkboxChecked requires that checkboxLabel also be passed'); - } - - // Choose a default button to get selected when dialog is cancelled. - if (cancelId == null) { - // If the defaultId is set to 0, ensure the cancel button is a different index (1) - cancelId = (defaultId === 0 && buttons.length > 1) ? 1 : 0; - for (let i = 0; i < buttons.length; i++) { - const text = buttons[i].toLowerCase(); - if (text === 'cancel' || text === 'no') { - cancelId = i; - break; - } - } - } - - const settings = { - window, - messageBoxType, - buttons, - defaultId, - cancelId, - noLink, - title, - message, - detail, - checkboxLabel, - checkboxChecked, - icon - }; - - if (sync) { - return binding.showMessageBoxSync(settings); - } else { - return binding.showMessageBox(settings); - } -}; - -module.exports = { - showOpenDialog: function (window, options) { - return openDialog(false, window, options); - }, - - showOpenDialogSync: function (window, options) { - return openDialog(true, window, options); - }, - - showSaveDialog: function (window, options) { - return saveDialog(false, window, options); - }, - - showSaveDialogSync: function (window, options) { - return saveDialog(true, window, options); - }, - - showMessageBox: function (window, options) { - return messageBox(false, window, options); - }, - - showMessageBoxSync: function (window, options) { - return messageBox(true, window, options); - }, - - showErrorBox: function (...args) { - return binding.showErrorBox(...args); - }, - - showCertificateTrustDialog: function (window, options) { - if (window && window.constructor !== BrowserWindow) { - options = window; - window = null; - } - - if (options == null || typeof options !== 'object') { - throw new TypeError('options must be an object'); - } - - const { certificate, message = '' } = options; - if (certificate == null || typeof certificate !== 'object') { - throw new TypeError('certificate must be an object'); - } - - if (typeof message !== 'string') throw new TypeError('message must be a string'); - - return binding.showCertificateTrustDialog(window, certificate, message); - } -}; diff --git a/lib/browser/api/dialog.ts b/lib/browser/api/dialog.ts new file mode 100644 index 000000000000..37bd7177c1a9 --- /dev/null +++ b/lib/browser/api/dialog.ts @@ -0,0 +1,306 @@ +import { app, BrowserWindow } from 'electron'; +import { OpenDialogOptions, OpenDialogReturnValue, MessageBoxOptions, SaveDialogOptions, SaveDialogReturnValue, MessageBoxReturnValue, CertificateTrustDialogOptions } from 'electron/main'; +const dialogBinding = process._linkedBinding('electron_browser_dialog'); + +const DialogType = { + OPEN: 'OPEN' as 'OPEN', + SAVE: 'SAVE' as 'SAVE' +}; + +enum SaveFileDialogProperties { + createDirectory = 1 << 0, + showHiddenFiles = 1 << 1, + treatPackageAsDirectory = 1 << 2, + showOverwriteConfirmation = 1 << 3, + dontAddToRecent = 1 << 4 +} + +enum OpenFileDialogProperties { + openFile = 1 << 0, + openDirectory = 1 << 1, + multiSelections = 1 << 2, + createDirectory = 1 << 3, // macOS + showHiddenFiles = 1 << 4, + promptToCreate = 1 << 5, // Windows + noResolveAliases = 1 << 6, // macOS + treatPackageAsDirectory = 1 << 7, // macOS + dontAddToRecent = 1 << 8 // Windows +} + +const normalizeAccessKey = (text: string) => { + if (typeof text !== 'string') return text; + + // macOS does not have access keys so remove single ampersands + // and replace double ampersands with a single ampersand + if (process.platform === 'darwin') { + return text.replace(/&(&?)/g, '$1'); + } + + // Linux uses a single underscore as an access key prefix so escape + // existing single underscores with a second underscore, replace double + // ampersands with a single ampersand, and replace a single ampersand with + // a single underscore + if (process.platform === 'linux') { + return text.replace(/_/g, '__').replace(/&(.?)/g, (match, after) => { + if (after === '&') return after; + return `_${after}`; + }); + } + + return text; +}; + +const checkAppInitialized = function () { + if (!app.isReady()) { + throw new Error('dialog module can only be used after app is ready'); + } +}; + +const setupOpenDialogProperties = (properties: (keyof typeof OpenFileDialogProperties)[]): number => { + let dialogProperties = 0; + for (const property of properties) { + if (Object.prototype.hasOwnProperty.call(OpenFileDialogProperties, property)) { dialogProperties |= OpenFileDialogProperties[property]; } + } + return dialogProperties; +}; + +const setupSaveDialogProperties = (properties: (keyof typeof SaveFileDialogProperties)[]): number => { + let dialogProperties = 0; + for (const property of properties) { + if (Object.prototype.hasOwnProperty.call(SaveFileDialogProperties, property)) { dialogProperties |= SaveFileDialogProperties[property]; } + } + return dialogProperties; +}; + +const setupDialogProperties = (type: keyof typeof DialogType, properties: string[]): number => { + if (type === DialogType.OPEN) { + return setupOpenDialogProperties(properties as (keyof typeof OpenFileDialogProperties)[]); + } else if (type === DialogType.SAVE) { + return setupSaveDialogProperties(properties as (keyof typeof SaveFileDialogProperties)[]); + } else { + return 0; + } +}; + +const saveDialog = (sync: boolean, window: BrowserWindow | null, options?: SaveDialogOptions) => { + checkAppInitialized(); + + if (options == null) options = { title: 'Save' }; + + const { + buttonLabel = '', + defaultPath = '', + filters = [], + properties = [], + title = '', + message = '', + securityScopedBookmarks = false, + nameFieldLabel = '', + showsTagField = true + } = options; + + if (typeof title !== 'string') throw new TypeError('Title must be a string'); + if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string'); + if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string'); + if (typeof message !== 'string') throw new TypeError('Message must be a string'); + if (typeof nameFieldLabel !== 'string') throw new TypeError('Name field label must be a string'); + + const settings = { + buttonLabel, + defaultPath, + filters, + title, + message, + securityScopedBookmarks, + nameFieldLabel, + showsTagField, + window, + properties: setupDialogProperties(DialogType.SAVE, properties) + }; + + return sync ? dialogBinding.showSaveDialogSync(settings) : dialogBinding.showSaveDialog(settings); +}; + +const openDialog = (sync: boolean, window: BrowserWindow | null, options?: OpenDialogOptions) => { + checkAppInitialized(); + + if (options == null) { + options = { + title: 'Open', + properties: ['openFile'] + }; + } + + const { + buttonLabel = '', + defaultPath = '', + filters = [], + properties = ['openFile'], + title = '', + message = '', + securityScopedBookmarks = false + } = options; + + if (!Array.isArray(properties)) throw new TypeError('Properties must be an array'); + + if (typeof title !== 'string') throw new TypeError('Title must be a string'); + if (typeof buttonLabel !== 'string') throw new TypeError('Button label must be a string'); + if (typeof defaultPath !== 'string') throw new TypeError('Default path must be a string'); + if (typeof message !== 'string') throw new TypeError('Message must be a string'); + + const settings = { + title, + buttonLabel, + defaultPath, + filters, + message, + securityScopedBookmarks, + window, + properties: setupDialogProperties(DialogType.OPEN, properties) + }; + + return (sync) ? dialogBinding.showOpenDialogSync(settings) : dialogBinding.showOpenDialog(settings); +}; + +const messageBox = (sync: boolean, window: BrowserWindow | null, options?: MessageBoxOptions) => { + checkAppInitialized(); + + if (options == null) options = { type: 'none', message: '' }; + + const messageBoxTypes = ['none', 'info', 'warning', 'error', 'question']; + + let { + buttons = [], + cancelId, + checkboxLabel = '', + checkboxChecked, + defaultId = -1, + detail = '', + icon = null, + noLink = false, + message = '', + title = '', + type = 'none' + } = options; + + const messageBoxType = messageBoxTypes.indexOf(type); + if (messageBoxType === -1) throw new TypeError('Invalid message box type'); + if (!Array.isArray(buttons)) throw new TypeError('Buttons must be an array'); + if (options.normalizeAccessKeys) buttons = buttons.map(normalizeAccessKey); + if (typeof title !== 'string') throw new TypeError('Title must be a string'); + if (typeof noLink !== 'boolean') throw new TypeError('noLink must be a boolean'); + if (typeof message !== 'string') throw new TypeError('Message must be a string'); + if (typeof detail !== 'string') throw new TypeError('Detail must be a string'); + if (typeof checkboxLabel !== 'string') throw new TypeError('checkboxLabel must be a string'); + + checkboxChecked = !!checkboxChecked; + if (checkboxChecked && !checkboxLabel) { + throw new Error('checkboxChecked requires that checkboxLabel also be passed'); + } + + // Choose a default button to get selected when dialog is cancelled. + if (cancelId == null) { + // If the defaultId is set to 0, ensure the cancel button is a different index (1) + cancelId = (defaultId === 0 && buttons.length > 1) ? 1 : 0; + for (let i = 0; i < buttons.length; i++) { + const text = buttons[i].toLowerCase(); + if (text === 'cancel' || text === 'no') { + cancelId = i; + break; + } + } + } + + const settings = { + window, + messageBoxType, + buttons, + defaultId, + cancelId, + noLink, + title, + message, + detail, + checkboxLabel, + checkboxChecked, + icon + }; + + if (sync) { + return dialogBinding.showMessageBoxSync(settings); + } else { + return dialogBinding.showMessageBox(settings); + } +}; + +// eat dirt, eslint +/* eslint-disable import/export */ +export function showOpenDialog(window: BrowserWindow, options: OpenDialogOptions): OpenDialogReturnValue; +export function showOpenDialog(options: OpenDialogOptions): OpenDialogReturnValue; +export function showOpenDialog (windowOrOptions: BrowserWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + return openDialog(false, window, options); +} + +export function showOpenDialogSync(window: BrowserWindow, options: OpenDialogOptions): OpenDialogReturnValue; +export function showOpenDialogSync(options: OpenDialogOptions): OpenDialogReturnValue; +export function showOpenDialogSync (windowOrOptions: BrowserWindow | OpenDialogOptions, maybeOptions?: OpenDialogOptions): OpenDialogReturnValue { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + return openDialog(true, window, options); +} + +export function showSaveDialog(window: BrowserWindow, options: SaveDialogOptions): SaveDialogReturnValue; +export function showSaveDialog(options: SaveDialogOptions): SaveDialogReturnValue; +export function showSaveDialog (windowOrOptions: BrowserWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + return saveDialog(false, window, options); +} + +export function showSaveDialogSync(window: BrowserWindow, options: SaveDialogOptions): SaveDialogReturnValue; +export function showSaveDialogSync(options: SaveDialogOptions): SaveDialogReturnValue; +export function showSaveDialogSync (windowOrOptions: BrowserWindow | SaveDialogOptions, maybeOptions?: SaveDialogOptions): SaveDialogReturnValue { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + return saveDialog(true, window, options); +} + +export function showMessageBox(window: BrowserWindow, options: MessageBoxOptions): MessageBoxReturnValue; +export function showMessageBox(options: MessageBoxOptions): MessageBoxReturnValue; +export function showMessageBox (windowOrOptions: BrowserWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + return messageBox(false, window, options); +} + +export function showMessageBoxSync(window: BrowserWindow, options: MessageBoxOptions): MessageBoxReturnValue; +export function showMessageBoxSync(options: MessageBoxOptions): MessageBoxReturnValue; +export function showMessageBoxSync (windowOrOptions: BrowserWindow | MessageBoxOptions, maybeOptions?: MessageBoxOptions): MessageBoxReturnValue { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + return messageBox(true, window, options); +} + +export function showErrorBox (...args: any[]) { + return dialogBinding.showErrorBox(...args); +} + +export function showCertificateTrustDialog (windowOrOptions: BrowserWindow | CertificateTrustDialogOptions, maybeOptions?: CertificateTrustDialogOptions) { + const window = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? null : windowOrOptions); + const options = (windowOrOptions && !(windowOrOptions instanceof BrowserWindow) ? windowOrOptions : maybeOptions); + + if (options == null || typeof options !== 'object') { + throw new TypeError('options must be an object'); + } + + const { certificate, message = '' } = options; + if (certificate == null || typeof certificate !== 'object') { + throw new TypeError('certificate must be an object'); + } + + if (typeof message !== 'string') throw new TypeError('message must be a string'); + + return dialogBinding.showCertificateTrustDialog(window, certificate, message); +}