feat: 新增包文件
This commit is contained in:
2
packages/electron-chrome-context-menu/.gitignore
vendored
Normal file
2
packages/electron-chrome-context-menu/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
dist
|
||||
*.map
|
||||
1
packages/electron-chrome-context-menu/.npmignore
Normal file
1
packages/electron-chrome-context-menu/.npmignore
Normal file
@@ -0,0 +1 @@
|
||||
tsconfig.json
|
||||
56
packages/electron-chrome-context-menu/README.md
Normal file
56
packages/electron-chrome-context-menu/README.md
Normal file
@@ -0,0 +1,56 @@
|
||||
# electron-chrome-context-menu
|
||||
|
||||
> Chrome context menu for Electron browsers
|
||||
|
||||
Building a modern web browser requires including many features users have grown accustomed to. Context menus are a small, but noticeable feature when done improperly.
|
||||
|
||||
This module aims to provide a context menu with close to feature parity to that of Google Chrome.
|
||||
|
||||
## Install
|
||||
|
||||
> npm install electron-chrome-context-menu
|
||||
|
||||
## Usage
|
||||
|
||||
```ts
|
||||
// ES imports
|
||||
import buildChromeContextMenu from 'electron-chrome-context-menu'
|
||||
// CommonJS
|
||||
const buildChromeContextMenu = require('electron-chrome-context-menu').default
|
||||
|
||||
const { app } = require('electron')
|
||||
|
||||
app.on('web-contents-created', (event, webContents) => {
|
||||
webContents.on('context-menu', (e, params) => {
|
||||
const menu = buildChromeContextMenu({
|
||||
params,
|
||||
webContents,
|
||||
openLink: (url, disposition) => {
|
||||
webContents.loadURL(url)
|
||||
}
|
||||
})
|
||||
|
||||
menu.popup()
|
||||
})
|
||||
})
|
||||
```
|
||||
|
||||
> For a complete example, see the [`electron-browser-shell`](https://github.com/samuelmaddock/electron-browser-shell) project.
|
||||
|
||||
## API
|
||||
|
||||
### `buildChromeContextMenu(options)`
|
||||
|
||||
* `options` Object
|
||||
* `params` Electron.ContextMenuParams - Context menu parameters emitted from the WebContents 'context-menu' event.
|
||||
* `webContents` Electron.WebContents - WebContents which emitted the 'context-menu' event.
|
||||
* `openLink(url, disposition, params)` - Handler for opening links.
|
||||
* `url` String
|
||||
* `disposition` String - Can be `default`, `foreground-tab`, `background-tab`, and `new-window`.
|
||||
* `params` Electron.ContextMenuParams
|
||||
* `extensionMenuItems` Electron.MenuItem[] (optional) - Collection of menu items for active web extensions.
|
||||
* `labels` Object (optional) - Labels used to create menu items. Replace this if localization is needed.
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
249
packages/electron-chrome-context-menu/index.ts
Normal file
249
packages/electron-chrome-context-menu/index.ts
Normal file
@@ -0,0 +1,249 @@
|
||||
import { app, BrowserWindow, clipboard, Menu, MenuItem } from 'electron'
|
||||
|
||||
const LABELS = {
|
||||
openInNewTab: (type: 'link' | Electron.ContextMenuParams['mediaType']) =>
|
||||
`Open ${type} in new tab`,
|
||||
openInNewWindow: (type: 'link' | Electron.ContextMenuParams['mediaType']) =>
|
||||
`Open ${type} in new window`,
|
||||
copyAddress: (type: 'link' | Electron.ContextMenuParams['mediaType']) => `Copy ${type} address`,
|
||||
undo: 'Undo',
|
||||
redo: 'Redo',
|
||||
cut: 'Cut',
|
||||
copy: 'Copy',
|
||||
delete: 'Delete',
|
||||
paste: 'Paste',
|
||||
selectAll: 'Select All',
|
||||
back: 'Back',
|
||||
forward: 'Forward',
|
||||
reload: 'Reload',
|
||||
inspect: 'Inspect',
|
||||
addToDictionary: 'Add to dictionary',
|
||||
exitFullScreen: 'Exit full screen',
|
||||
emoji: 'Emoji',
|
||||
}
|
||||
|
||||
const getBrowserWindowFromWebContents = (webContents: Electron.WebContents) => {
|
||||
return BrowserWindow.getAllWindows().find((win) => {
|
||||
if (win.webContents === webContents) return true
|
||||
|
||||
let browserViews: Electron.BrowserView[]
|
||||
|
||||
if ('getBrowserViews' in win) {
|
||||
browserViews = win.getBrowserViews()
|
||||
} else if ('getBrowserView' in win) {
|
||||
// @ts-ignore
|
||||
browserViews = [win.getBrowserView()]
|
||||
} else {
|
||||
browserViews = []
|
||||
}
|
||||
|
||||
return browserViews.some((view) => view.webContents === webContents)
|
||||
})
|
||||
}
|
||||
|
||||
type ChromeContextMenuLabels = typeof LABELS
|
||||
|
||||
interface ChromeContextMenuOptions {
|
||||
/** Context menu parameters emitted from the WebContents 'context-menu' event. */
|
||||
params: Electron.ContextMenuParams
|
||||
|
||||
/** WebContents which emitted the 'context-menu' event. */
|
||||
webContents: Electron.WebContents
|
||||
|
||||
/** Handler for opening links. */
|
||||
openLink: (
|
||||
url: string,
|
||||
disposition: 'default' | 'foreground-tab' | 'background-tab' | 'new-window',
|
||||
params: Electron.ContextMenuParams,
|
||||
) => void
|
||||
|
||||
/** Chrome extension menu items. */
|
||||
extensionMenuItems?: MenuItem[]
|
||||
|
||||
/** Labels used to create menu items. Replace this if localization is needed. */
|
||||
labels?: ChromeContextMenuLabels
|
||||
|
||||
/**
|
||||
* @deprecated Use 'labels' instead.
|
||||
*/
|
||||
strings?: ChromeContextMenuLabels
|
||||
}
|
||||
|
||||
export const buildChromeContextMenu = (opts: ChromeContextMenuOptions): Menu => {
|
||||
const { params, webContents, openLink, extensionMenuItems } = opts
|
||||
|
||||
const labels = opts.labels || opts.strings || LABELS
|
||||
|
||||
const menu = new Menu()
|
||||
const append = (opts: Electron.MenuItemConstructorOptions) => menu.append(new MenuItem(opts))
|
||||
const appendSeparator = () => menu.append(new MenuItem({ type: 'separator' }))
|
||||
|
||||
if (params.linkURL) {
|
||||
append({
|
||||
label: labels.openInNewTab('link'),
|
||||
click: () => {
|
||||
openLink(params.linkURL, 'default', params)
|
||||
},
|
||||
})
|
||||
append({
|
||||
label: labels.openInNewWindow('link'),
|
||||
click: () => {
|
||||
openLink(params.linkURL, 'new-window', params)
|
||||
},
|
||||
})
|
||||
appendSeparator()
|
||||
append({
|
||||
label: labels.copyAddress('link'),
|
||||
click: () => {
|
||||
clipboard.writeText(params.linkURL)
|
||||
},
|
||||
})
|
||||
appendSeparator()
|
||||
} else if (params.mediaType !== 'none') {
|
||||
// TODO: Loop, Show controls
|
||||
append({
|
||||
label: labels.openInNewTab(params.mediaType),
|
||||
click: () => {
|
||||
openLink(params.srcURL, 'default', params)
|
||||
},
|
||||
})
|
||||
append({
|
||||
label: labels.copyAddress(params.mediaType),
|
||||
click: () => {
|
||||
clipboard.writeText(params.srcURL)
|
||||
},
|
||||
})
|
||||
appendSeparator()
|
||||
}
|
||||
|
||||
if (params.isEditable) {
|
||||
if (params.misspelledWord) {
|
||||
for (const suggestion of params.dictionarySuggestions) {
|
||||
append({
|
||||
label: suggestion,
|
||||
click: () => webContents.replaceMisspelling(suggestion),
|
||||
})
|
||||
}
|
||||
|
||||
if (params.dictionarySuggestions.length > 0) appendSeparator()
|
||||
|
||||
append({
|
||||
label: labels.addToDictionary,
|
||||
click: () => webContents.session.addWordToSpellCheckerDictionary(params.misspelledWord),
|
||||
})
|
||||
} else {
|
||||
if (
|
||||
app.isEmojiPanelSupported() &&
|
||||
!['input-number', 'input-telephone'].includes(params.formControlType)
|
||||
) {
|
||||
append({
|
||||
label: labels.emoji,
|
||||
click: () => app.showEmojiPanel(),
|
||||
})
|
||||
appendSeparator()
|
||||
}
|
||||
|
||||
append({
|
||||
label: labels.redo,
|
||||
enabled: params.editFlags.canRedo,
|
||||
click: () => webContents.redo(),
|
||||
})
|
||||
append({
|
||||
label: labels.undo,
|
||||
enabled: params.editFlags.canUndo,
|
||||
click: () => webContents.undo(),
|
||||
})
|
||||
}
|
||||
|
||||
appendSeparator()
|
||||
|
||||
append({
|
||||
label: labels.cut,
|
||||
enabled: params.editFlags.canCut,
|
||||
click: () => webContents.cut(),
|
||||
})
|
||||
append({
|
||||
label: labels.copy,
|
||||
enabled: params.editFlags.canCopy,
|
||||
click: () => webContents.copy(),
|
||||
})
|
||||
append({
|
||||
label: labels.paste,
|
||||
enabled: params.editFlags.canPaste,
|
||||
click: () => webContents.paste(),
|
||||
})
|
||||
append({
|
||||
label: labels.delete,
|
||||
enabled: params.editFlags.canDelete,
|
||||
click: () => webContents.delete(),
|
||||
})
|
||||
appendSeparator()
|
||||
if (params.editFlags.canSelectAll) {
|
||||
append({
|
||||
label: labels.selectAll,
|
||||
click: () => webContents.selectAll(),
|
||||
})
|
||||
appendSeparator()
|
||||
}
|
||||
} else if (params.selectionText) {
|
||||
append({
|
||||
label: labels.copy,
|
||||
click: () => {
|
||||
clipboard.writeText(params.selectionText)
|
||||
},
|
||||
})
|
||||
appendSeparator()
|
||||
}
|
||||
|
||||
if (menu.items.length === 0) {
|
||||
const browserWindow = getBrowserWindowFromWebContents(webContents)
|
||||
|
||||
// TODO: Electron needs a way to detect whether we're in HTML5 full screen.
|
||||
// Also need to properly exit full screen in Blink rather than just exiting
|
||||
// the Electron BrowserWindow.
|
||||
if (browserWindow?.fullScreen) {
|
||||
append({
|
||||
label: labels.exitFullScreen,
|
||||
click: () => browserWindow.setFullScreen(false),
|
||||
})
|
||||
|
||||
appendSeparator()
|
||||
}
|
||||
|
||||
append({
|
||||
label: labels.back,
|
||||
enabled: webContents.navigationHistory.canGoBack(),
|
||||
click: () => webContents.navigationHistory.goBack(),
|
||||
})
|
||||
append({
|
||||
label: labels.forward,
|
||||
enabled: webContents.navigationHistory.canGoForward(),
|
||||
click: () => webContents.navigationHistory.goForward(),
|
||||
})
|
||||
append({
|
||||
label: labels.reload,
|
||||
click: () => webContents.reload(),
|
||||
})
|
||||
appendSeparator()
|
||||
}
|
||||
|
||||
if (extensionMenuItems) {
|
||||
extensionMenuItems.forEach((item) => menu.append(item))
|
||||
if (extensionMenuItems.length > 0) appendSeparator()
|
||||
}
|
||||
|
||||
append({
|
||||
label: labels.inspect,
|
||||
click: () => {
|
||||
webContents.inspectElement(params.x, params.y)
|
||||
|
||||
if (!webContents.isDevToolsFocused()) {
|
||||
webContents.devToolsWebContents?.focus()
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
return menu
|
||||
}
|
||||
|
||||
export default buildChromeContextMenu
|
||||
25
packages/electron-chrome-context-menu/package.json
Normal file
25
packages/electron-chrome-context-menu/package.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"name": "electron-chrome-context-menu",
|
||||
"version": "1.0.0",
|
||||
"description": "Chrome context menu for Electron browsers",
|
||||
"main": "dist/index.js",
|
||||
"scripts": {
|
||||
"build": "tsc",
|
||||
"prepublishOnly": "yarn build"
|
||||
},
|
||||
"keywords": [
|
||||
"electron",
|
||||
"chrome",
|
||||
"context",
|
||||
"menu"
|
||||
],
|
||||
"repository": "",
|
||||
"author": "Damon <damon.shuwen@gmail.com>",
|
||||
"license": "MIT",
|
||||
"peerDependencies": {
|
||||
"electron": ">=38.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.5.4"
|
||||
}
|
||||
}
|
||||
12
packages/electron-chrome-context-menu/tsconfig.json
Normal file
12
packages/electron-chrome-context-menu/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"extends": "../../tsconfig.base.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"moduleResolution": "node",
|
||||
"outDir": "dist",
|
||||
"declaration": true
|
||||
},
|
||||
|
||||
"include": ["*"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user