electron-findbar
Version:
Chrome-like findbar for your Electron app.
417 lines (302 loc) • 14.8 kB
Markdown
<p align='center'>
<a href="https://github.com/ECRomaneli/electron-findbar" style='text-decoration:none'><img src="https://i.postimg.cc/sXwqJP59/findbar-v2-light.png" alt='Findbar Light Theme'><img src="https://i.postimg.cc/j26XXRVV/findbar-v2-dark.png" alt='Findbar Dark Theme'></a>
</p>
<p align='center'>
Chrome-like findbar for your Electron application
</p>
<p align='center'>
<a href="https://github.com/ECRomaneli/electron-findbar/tags"><img src="https://img.shields.io/github/v/tag/ecromaneli/electron-findbar?label=version&sort=semver&style=for-the-badge" alt="Version"></a>
<a href="https://github.com/ECRomaneli/electron-findbar/commits/master"><img src="https://img.shields.io/github/last-commit/ecromaneli/electron-findbar?style=for-the-badge" alt="Last Commit"></a>
<a href="https://github.com/ECRomaneli/electron-findbar/blob/master/LICENSE"><img src="https://img.shields.io/github/license/ecromaneli/electron-findbar?style=for-the-badge" alt="License"></a>
<a href="https://github.com/ECRomaneli/electron-findbar/issues"><img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=for-the-badge" alt="Contributions Welcome"></a>
</p>
## Installation
Install the `electron-findbar` package via [npm](https://www.npmjs.com/package/electron-findbar):
```sh
npm install electron-findbar
```
## Overview
The `electron-findbar` package creates a `BrowserWindow`-based component designed to emulate the Chrome findbar layout, leveraging the `webContents.findInPage` method to navigate through matches. Inter-process communication (IPC) is used for interaction between the `main` and `renderer` processes.
### Memory Usage
To optimize memory usage, the Findbar window is created only when the findbar is open. The implementation is lightweight, including only essential code.
## Usage
All public methods are documented with JSDoc and can be referenced during import.
### Importing the Findbar
To import the Findbar class:
```js
const Findbar = require('electron-findbar')
```
### Creating the Findbar Instance
You can pass a `BrowserWindow` instance as a single parameter to use it as the parent window. The `BrowserWindow.WebContents` will be used as the findable content:
```js
// Create or retrieve the findbar associated to the browserWindow.webContents or baseWindow.contentView.children[0]. If a new findbar is created, the browserWindow is used as parent.
const findbar = Findbar.from(browserWindow)
```
Alternatively, you can provide a custom `WebContents` as the second parameter. In this case, the first parameter can be any `BaseWindow`, and the second parameter will be the findable content:
```js
// Create or retrieve the findbar associated to the webContents. If a new findbar is created, the baseWindow is used as parent.
const findbar = Findbar.from(baseWindow, webContents)
```
It is also possible to create a findbar providing only the web contents. The BaseWindow.getAllWindows() will be used to query for the parent window:
```js
// Create or retrieve the findbar associated to the webContents.
const findbar = Findbar.from(webContents)
```
**Note:** The findbar is ALWAYS linked to the webContents, not the window. The parent is only the window to connect the events and stay on top. If the `.from(webContents)` is used to retrieve an existing findbar previously created with a parent, the findbar will stay connected to the parent. If a different parent is used, the parent window is updated automatically.
#### Retrieve if exists
If there is no intention to create a new findbar in case it does not exist, use:
```js
// Get the existing findbar or undefined.
const existingFindbar = Findbar.fromIfExists(browserWindow)
/* OR */
const existingFindbar = Findbar.fromIfExists(webContents)
```
### Configuring the Findbar
You can customize the Findbar window options using the `setWindowOptions` method:
```js
findbar.setWindowOptions({ resizable: true, alwaysOnTop: true, height: 100 })
```
To handle the Findbar window directly after it is opened, use the `setWindowHandler` method:
```js
findbar.setWindowHandler(win => {
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true })
});
```
The findbar has a default position handler which moves the findbar to the top-right corner. To change the position handler, use the `setBoundsHandler` method. The bounds handler is called when the parent window moves or resizes and provides both the parent and findbar bounds as parameters.
```js
findbar.setBoundsHandler((parentBounds, findbarBounds) => ({
x: parentBounds.x + parentBounds.width - findbarBounds.width - 20,
y: parentBounds.y - ((findbarBounds.height / 4) | 0)
/* width: OPTIONAL, current value will be used */
/* height: OPTIONAL, current value will be used */
}))
```
### Opening the Findbar
The Findbar is a child window of the `BaseWindow` passed during construction. To open it use:
```js
findbar.open()
```
### Closing the Findbar
When the Findbar is closed, its window is destroyed to free memory resources. Use the following method to close the Findbar:
```js
findbar.close()
```
A new internal window will be created the next time the `open` method is called. There is no need to instantiate another Findbar for the same parent window.
### Quick Example
Here is a quick example demonstrating how to use the `electron-findbar`:
```js
const { app, BrowserWindow } = require('electron')
const Findbar = require('electron-findbar')
app.whenReady().then(() => {
const window = new BrowserWindow()
window.loadURL('https://github.com/ECRomaneli/electron-findbar')
// Create and configure the Findbar object
const findbar = Findbar.from(window)
// [OPTIONAL] Customize window options
findbar.setWindowOptions({ movable: true, resizable: true })
// [OPTIONAL] Handle the window object when the Findbar is opened
findbar.setWindowHandler(win => { win.webContents.openDevTools() })
// Open the Findbar
findbar.open()
})
```
### Keyboard Shortcuts
The Findbar component can be controlled using keyboard shortcuts. The following shortcuts are available by default:
| Shortcut | Description |
|----------|-------------|
| Enter | Move to next match |
| Shift+Enter | Move to previous match |
| Esc | Close the findbar |
### Configuring Other Shortcuts
Below are two implementation approaches to help you integrate search functionality seamlessly into your application's user experience.
**Note:** The following examples demonstrate only the ideal (happy path) scenarios. For production use, make sure to thoroughly validate all inputs and handle edge cases appropriately.
#### Using Before Input Event
The `before-input-event` approach allows you to capture keyboard events directly in the main process before they're processed by the web contents, giving you precise control:
```js
webContents.on('before-input-event', (event, input) => {
if (input.shift || input.alt) { return }
const key = input.key.toLowerCase()
// Detect Ctrl+F (Windows/Linux) or Command+F (macOS)
const isMac = process.platform === 'darwin'
if ((isMac && input.meta) || (!isMac && input.control)) {
if (key === 'f') {
// Prevent default behavior
event.preventDefault()
// Access and open the findbar
Findbar.from(webContents).open()
}
return
}
// Handle Escape key to close the findbar
if (key === 'escape') {
const findbar = Findbar.fromIfExists(webContents)
if (findbar?.isOpen()) {
// Prevent default behavior
event.preventDefault()
// Close the findbar
findbar.close()
}
}
})
```
#### Using Menu Accelerators
For a more integrated approach, you can modify your application's menu system to include findbar controls with keyboard accelerators. This method makes shortcuts available throughout your application:
```js
// Get reference to the parent window
const parent = currentBrowserWindowOrWebContents
// Get or create application menu
const appMenu = Menu.getApplicationMenu() ?? new Menu()
// Add Findbar controls to menu
appMenu.append(new MenuItem({
label: 'Find',
submenu: [
{
label: 'Find in Page',
click: () => Findbar.from(parent).open(),
accelerator: 'CommandOrControl+F'
},
{
label: 'Close Find',
click: () => Findbar.from(parent).close(),
accelerator: 'Esc'
}
]
}))
// Apply the updated menu
Menu.setApplicationMenu(appMenu)
```
Both approaches have their advantages - the first offers fine-grained control over exactly when shortcuts are activated, while the second provides better integration with standard application menu conventions.
### Finding Text using the main process
Once open, the Findbar appears by default in the top-right corner of the parent window and can be used without additional coding. Alternatively, you can use the following methods to trigger `findInPage` and navigate through matches in the main process:
```js
/**
* Get the last state of the findbar.
* @returns {{ text: string, matchCase: boolean, movable: boolean, theme: 'light' | 'dark' | 'system' }} Last state of the findbar.
*/
getLastState()
/**
* Initiate a request to find all matches for the specified text on the page.
* @param {string} text - The text to search for.
* @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
*/
startFind(text, skipRendererEvent)
/**
* Whether the search should be case-sensitive.
* @param {boolean} status - Whether the search should be case-sensitive. Default is false.
* @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
*/
matchCase(status, skipRendererEvent)
/**
* Select the previous match, if available.
*/
findPrevious()
/**
* Select the next match, if available.
*/
findNext()
/**
* Stop the find request and clears selection.
*/
stopFind()
/**
* Whether the findbar is opened.
* @returns {boolean} True if the findbar is open, otherwise false.
*/
isOpen()
/**
* Whether the findbar is focused. If the findbar is closed, false will be returned.
* @returns {boolean} True if the findbar is focused, otherwise false.
*/
isFocused()
/**
* Whether the findbar is visible to the user in the foreground of the app.
* If the findbar is closed, false will be returned.
* @returns {boolean} True if the findbar is visible, otherwise false.
*/
isVisible()
/**
* Get the current theme of this findbar instance.
* @returns {'light' | 'dark' | 'system'} The current theme setting.
*/
getTheme()
/**
* Update the theme of the findbar. Only affects the current instance.
* @param {'light' | 'dark' | 'system'} theme - The theme to set. If not provided, uses the default theme.
*/
updateTheme(theme)
/**
* Set whether the findbar will follow the parent window visibility events. Default is true.
* If false, the findbar will not hide with the parent window automatically.
*/
followVisibilityEvents(shouldFollow: boolean = true)
/**
* Get the default theme for new findbar instances.
* @returns {'light' | 'dark' | 'system'} The default theme setting.
*/
static getDefaultTheme()
/**
* Set the default theme for new findbar instances.
* @param {'light' | 'dark' | 'system'} theme - The theme to set as default.
*/
static setDefaultTheme(theme)
```
## IPC Events
As an alternative, the findbar can be controlled using IPC events in the `renderer` process of the `WebContents` provided during the findbar construction.
### ipcRenderer
If the `contextIsolation` is enabled, the `electron-findbar/remote` will not be available, but the IPC events can be used directly through the preload script:
```js
const $remote = (ipc => ({
getLastState: async () => ipc.invoke('electron-findbar/last-state'),
inputChange: (value: string) => { ipc.send('electron-findbar/input-change', value, true) },
matchCase: (value: boolean) => { ipc.send('electron-findbar/match-case', value, true) },
previous: () => { ipc.send('electron-findbar/previous') },
next: () => { ipc.send('electron-findbar/next') },
close: () => { ipc.send('electron-findbar/close') },
onMatchesChange: (listener: Function) => { ipc.on('electron-findbar/matches', listener) },
onInputFocus: (listener: Function) => { ipc.on('electron-findbar/input-focus', listener) },
onTextChange: (listener: Function) => { ipc.on('electron-findbar/text-change', listener) },
onMatchCaseChange: (listener: Function) => { ipc.on('electron-findbar/match-case-change', listener) },
onForceTheme: (listener: Function) => { ipc.on('electron-findbar/force-theme', listener) },
})) (require('electron').ipcRenderer);
$remote.open()
$remote.inputChange('findIt')
```
### Remote module
With the `contextIsolation` disabled, the remote library is available to use:
```js
const FindbarRemote = require('electron-findbar/remote')
FindbarRemote.open()
FindbarRemote.inputChange('findIt')
```
## Changing the Parent Window
There are scenarios where you might need to change the parent window.
### Using updateParentWindow
The `updateParentWindow` method allows you to change the parent window while preserving the findbar instance and its state:
```javascript
// Create a findbar for the initial window
const findbar = Findbar.from([oldWindow, ]webContents)
// Later, when you need to change the parent:
findbar.updateParentWindow(newWindow)
```
This approach keeps the same findbar instance connected to the same webContents, but changes which window it's attached to. The findbar will close immediately.
### Using detach
Alternatively, the `detach` method disconnects a findbar instance from its webContents, allowing you to create a new instance in the next `Findbar.from` call:
```javascript
// Get the existing findbar
const oldFindbar = Findbar.fromIfExists(webContents)
// Detach it to free the association
if (oldFindbar) {
oldFindbar.detach()
}
// Now create a new findbar with a different parent
const newFindbar = Findbar.from([newWindow, ]webContents)
```
This approach is useful when you want to completely reset the findbar's configuration or when moving between very different window configurations.
### Important Considerations
- If the findbar is currently open when you change the parent window, it will automatically close.
- Window options and handlers will be preserved when using `updateParentWindow`.
- After calling `detach`, the old findbar instance can no longer be used.
## Author
Created by [Emerson Capuchi Romaneli](https://github.com/ECRomaneli) (@ECRomaneli).
## License
This project is licensed under the [MIT License](https://github.com/ECRomaneli/electron-findbar/blob/master/LICENSE).