<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).

