electron-findbar 1.1.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,8 +1,5 @@
1
1
  <p align='center'>
2
- <a href="https://github.com/ECRomaneli/handbook" style='text-decoration:none'>
3
- <img src="https://i.postimg.cc/0QR0s0Z1/findbar-light.png" alt='Findbar Light Theme'>
4
- <img src="https://i.postimg.cc/LXtB6g0Y/findbar-dark.png" alt='Findbar Dark Theme'>
5
- </a>
2
+ <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>
6
3
  </p>
7
4
  <p align='center'>
8
5
  Chrome-like findbar for your Electron application
@@ -47,13 +44,35 @@ const Findbar = require('electron-findbar')
47
44
  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:
48
45
 
49
46
  ```js
50
- const findbar = new Findbar(browserWindow)
47
+ // Create or retrieve the findbar associated to the browserWindow.webContents. If a new findbar is created, the browserWindow is used as parent.
48
+ const findbar = Findbar.from(browserWindow)
51
49
  ```
52
50
 
53
51
  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:
54
52
 
55
53
  ```js
56
- const findbar = new Findbar(baseWindow, customWebContents)
54
+ // Create or retrieve the findbar associated to the webContents. If a new findbar is created, the baseWindow is used as parent.
55
+ const findbar = Findbar.from(baseWindow, webContents)
56
+ ```
57
+
58
+ Is also possible to create a findbar without a parent window (even though it is not recommended):
59
+
60
+ ```js
61
+ // Create or retrieve the findbar associated to the webContents. If a new findbar is created, it will be displayed in the middle of the screen without a parent to connect to.
62
+ const findbar = Findbar.from(webContents)
63
+ ```
64
+
65
+ **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.
66
+
67
+ #### Retrieve if exists
68
+
69
+ If there is no intention to create a new findbar in case it does not exist, use:
70
+
71
+ ```js
72
+ // Get the existing findbar or undefined.
73
+ const existingFindbar = Findbar.fromIfExists(browserWindow)
74
+ /* OR */
75
+ const existingFindbar = Findbar.fromIfExists(webContents)
57
76
  ```
58
77
 
59
78
  ### Configuring the Findbar
@@ -61,7 +80,7 @@ const findbar = new Findbar(baseWindow, customWebContents)
61
80
  You can customize the Findbar window options using the `setWindowOptions` method:
62
81
 
63
82
  ```js
64
- findbar.setWindowOptions({ movable: true, resizable: true, alwaysOnTop: true })
83
+ findbar.setWindowOptions({ resizable: true, alwaysOnTop: true, height: 100 })
65
84
  ```
66
85
 
67
86
  To handle the Findbar window directly after it is opened, use the `setWindowHandler` method:
@@ -72,12 +91,14 @@ findbar.setWindowHandler(win => {
72
91
  });
73
92
  ```
74
93
 
75
- The findbar has a default position handler which moves the findbar to the top-right corner. To change the position handler, use the `setPositionHandler` method. The position handler is called when the parent window moves or resizes and provides both the parent and findbar bounds as parameters.
94
+ 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.
76
95
 
77
96
  ```js
78
- findbar.setPositionHandler((parentBounds, findbarBounds) => ({
97
+ findbar.setBoundsHandler((parentBounds, findbarBounds) => ({
79
98
  x: parentBounds.x + parentBounds.width - findbarBounds.width - 20,
80
99
  y: parentBounds.y - ((findbarBounds.height / 4) | 0)
100
+ /* width: OPTIONAL, current value will be used */
101
+ /* height: OPTIONAL, current value will be used */
81
102
  }))
82
103
  ```
83
104
 
@@ -89,38 +110,6 @@ The Findbar is a child window of the `BaseWindow` passed during construction. To
89
110
  findbar.open()
90
111
  ```
91
112
 
92
- ### Finding Text in the Page
93
-
94
- 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:
95
-
96
- ```js
97
- /**
98
- * Retrieve the last queried value.
99
- */
100
- getLastValue()
101
-
102
- /**
103
- * Initiate a request to find all matches for the specified text on the page.
104
- * @param {string} text - The text to search for.
105
- */
106
- startFind(text)
107
-
108
- /**
109
- * Select the previous match, if available.
110
- */
111
- findPrevious()
112
-
113
- /**
114
- * Select the next match, if available.
115
- */
116
- findNext()
117
-
118
- /**
119
- * Stop the find request.
120
- */
121
- stopFind()
122
- ```
123
-
124
113
  ### Closing the Findbar
125
114
 
126
115
  When the Findbar is closed, its window is destroyed to free memory resources. Use the following method to close the Findbar:
@@ -144,21 +133,148 @@ app.whenReady().then(() => {
144
133
  window.loadURL('https://github.com/ECRomaneli/electron-findbar')
145
134
 
146
135
  // Create and configure the Findbar object
147
- const findbar = new Findbar(window)
136
+ const findbar = Findbar.from(window)
148
137
 
149
138
  // [OPTIONAL] Customize window options
150
139
  findbar.setWindowOptions({ movable: true, resizable: true })
151
140
 
152
141
  // [OPTIONAL] Handle the window object when the Findbar is opened
153
- findbar.setWindowHandler(win => {
154
- win.webContents.openDevTools()
155
- })
142
+ findbar.setWindowHandler(win => { win.webContents.openDevTools() })
156
143
 
157
144
  // Open the Findbar
158
145
  findbar.open()
159
146
  })
160
147
  ```
161
148
 
149
+ ### Configuring Keyboard Shortcuts
150
+
151
+ The Findbar component can be controlled using keyboard shortcuts. Below are two implementation approaches to help you integrate search functionality seamlessly into your application's user experience.
152
+
153
+ **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.
154
+
155
+ #### Using Before Input Event
156
+
157
+ 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:
158
+
159
+ ```js
160
+ window.webContents.on('before-input-event', (event, input) => {
161
+ // Detect Ctrl+F (Windows/Linux) or Command+F (macOS)
162
+ if ((input.control || input.meta) && input.key.toLowerCase() === 'f') {
163
+ // Prevent default browser behavior
164
+ event.preventDefault()
165
+
166
+ // Access and open the findbar
167
+ const findbar = Findbar.from(window)
168
+ if (findbar) {
169
+ findbar.open()
170
+ }
171
+ }
172
+
173
+ // Handle Escape key to close the findbar
174
+ if (input.key === 'Escape') {
175
+ const findbar = Findbar.from(window)
176
+ if (findbar && findbar.isOpen()) {
177
+ event.preventDefault()
178
+ findbar.close()
179
+ }
180
+ }
181
+ })
182
+ ```
183
+
184
+ #### Using Menu Accelerators
185
+
186
+ 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:
187
+
188
+ ```js
189
+ // Get reference to the parent window
190
+ const parent = currentBrowserWindowOrWebContents
191
+
192
+ // Get or create application menu
193
+ const appMenu = Menu.getApplicationMenu() ?? new Menu()
194
+
195
+ // Add Findbar controls to menu
196
+ appMenu.append(new MenuItem({
197
+ label: 'Find',
198
+ submenu: [
199
+ {
200
+ label: 'Find in Page',
201
+ click: () => Findbar.from(parent)?.open(),
202
+ accelerator: 'CommandOrControl+F'
203
+ },
204
+ {
205
+ label: 'Close Find',
206
+ click: () => Findbar.from(parent)?.close(),
207
+ accelerator: 'Esc'
208
+ }
209
+ ]
210
+ }))
211
+
212
+ // Apply the updated menu
213
+ Menu.setApplicationMenu(appMenu)
214
+ ```
215
+
216
+ 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.
217
+
218
+ ### Finding Text using the main process
219
+
220
+ 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:
221
+
222
+ ```js
223
+ /**
224
+ * Get the last state of the findbar.
225
+ * @returns {{ text: string, matchCase: boolean, movable: boolean }} Last state of the findbar.
226
+ */
227
+ getLastState()
228
+
229
+ /**
230
+ * Initiate a request to find all matches for the specified text on the page.
231
+ * @param {string} text - The text to search for.
232
+ * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
233
+ */
234
+ startFind(text, skipRendererEvent)
235
+
236
+ /**
237
+ * Whether the search should be case-sensitive.
238
+ * @param {boolean} status - Whether the search should be case-sensitive. Default is false.
239
+ * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
240
+ */
241
+ matchCase(status, skipRendererEvent)
242
+
243
+ /**
244
+ * Select the previous match, if available.
245
+ */
246
+ findPrevious()
247
+
248
+ /**
249
+ * Select the next match, if available.
250
+ */
251
+ findNext()
252
+
253
+ /**
254
+ * Stop the find request and clears selection.
255
+ */
256
+ stopFind()
257
+
258
+ /**
259
+ * Whether the findbar is opened.
260
+ * @returns {boolean} True if the findbar is open, otherwise false.
261
+ */
262
+ isOpen()
263
+
264
+ /**
265
+ * Whether the findbar is focused. If the findbar is closed, false will be returned.
266
+ * @returns {boolean} True if the findbar is focused, otherwise false.
267
+ */
268
+ isFocused()
269
+
270
+ /**
271
+ * Whether the findbar is visible to the user in the foreground of the app.
272
+ * If the findbar is closed, false will be returned.
273
+ * @returns {boolean} True if the findbar is visible, otherwise false.
274
+ */
275
+ isVisible()
276
+ ```
277
+
162
278
  ## IPC Events
163
279
 
164
280
  As an alternative, the findbar can be controlled using IPC events in the `renderer` process of the `WebContents` provided during the findbar construction.
@@ -169,12 +285,12 @@ If the `contextIsolation` is enabled, the `electron-findbar/remote` will not be
169
285
 
170
286
  ```js
171
287
  const $remote = (ipc => ({
172
- getLastText: async () => ipc.invoke('electron-findbar/last-text'),
288
+ getLastState: async () => ipc.invoke('electron-findbar/last-state'),
173
289
  inputChange: (value) => { ipc.send('electron-findbar/input-change', value) },
290
+ matchCase: (value) => { ipc.send('electron-findbar/match-case', value) },
174
291
  previous: () => { ipc.send('electron-findbar/previous') },
175
292
  next: () => { ipc.send('electron-findbar/next') },
176
- open: () => { ipc.send('electron-findbar/open') },
177
- close: () => { ipc.send('electron-findbar/close') },
293
+ close: () => { ipc.send('electron-findbar/close') }
178
294
  })) (require('electron').ipcRenderer)
179
295
 
180
296
  $remote.open()
package/index.js CHANGED
@@ -1,5 +1,8 @@
1
1
  const { BaseWindow, BrowserWindow, WebContents, BrowserWindowConstructorOptions, Rectangle } = require('electron')
2
2
 
3
+ /**
4
+ * Chrome-like findbar for Electron applications.
5
+ */
3
6
  class Findbar {
4
7
  /** @type {BaseWindow} */
5
8
  #parent
@@ -10,14 +13,14 @@ class Findbar {
10
13
  /** @type {WebContents} */
11
14
  #findableContents
12
15
 
13
- /** */
16
+ /** @type { { active: number, total: number } } */
14
17
  #matches
15
18
 
16
19
  /** @type {(findbarWindow: BrowserWindow) => void} */
17
20
  #windowHandler
18
21
 
19
- /** @type {{parentBounds: Rectangle, findbarBounds: Rectangle} => {x: number, y: number}} */
20
- #positionHandler = Findbar.#setDefaultPosition
22
+ /** @type {{parentBounds: Rectangle, findbarBounds: Rectangle} => Rectangle} */
23
+ #boundsHandler = Findbar.#setDefaultPosition
21
24
 
22
25
  /** @type {BrowserWindowConstructorOptions} */
23
26
  #customOptions
@@ -25,6 +28,12 @@ class Findbar {
25
28
  /** @type {string} */
26
29
  #lastText = ''
27
30
 
31
+ /** @type {boolean} */
32
+ #matchCase = false
33
+
34
+ /** @type {boolean} */
35
+ #isMovable = false
36
+
28
37
  /**
29
38
  * Workaround to fix "findInPage" bug - double-click to loop
30
39
  * @type {boolean | null}
@@ -32,37 +41,56 @@ class Findbar {
32
41
  #fixMove = null
33
42
 
34
43
  /**
35
- * Prepare the findbar.
36
- * @param {BaseWindow} parent Parent window.
37
- * @param {WebContents | void} webContents Searchable web contents. If not set and the parent is a BrowserWindow,
38
- * the web contents of the parent will be used. Otherwise, an error will be triggered.
44
+ * Configure the findbar and link to the web contents.
45
+ *
46
+ * @overload
47
+ * @param {BrowserWindow} browserWindow Parent window.
48
+ * @param {WebContents} [customWebContents] Custom findable web contents. If not provided, the web contents of the BrowserWindow will be used.
49
+ * @returns {Findbar} The findbar instance if it exists.
50
+ *
51
+ * @overload
52
+ * @param {BaseWindow} baseWindow Parent window.
53
+ * @param {WebContents} webContents Findable web contents.
54
+ * @returns {Findbar} The findbar instance if it exists.
55
+ * @throws {Error} If no webContents is provided.
56
+ * *
57
+ * @overload
58
+ * @param {WebContents} webContents Findable web contents. The parent window will be undefined.
59
+ * @returns {Findbar} The findbar instance if it exists.
60
+ * @throws {Error} If no webContents is provided.
39
61
  */
40
62
  constructor (parent, webContents) {
41
- this.#parent = parent
42
- this.#findableContents = webContents ?? parent.webContents
43
- this.#findableContents._findbar = this
44
-
63
+ if (isFindable(parent)) {
64
+ this.#parent = void 0
65
+ this.#findableContents = parent
66
+ } else {
67
+ this.#parent = parent
68
+ this.#findableContents = webContents ?? parent.webContents
69
+ }
70
+
45
71
  if (!this.#findableContents) {
46
72
  throw new Error('There are no searchable web contents.')
47
73
  }
74
+
75
+ this.#findableContents._findbar = this
48
76
  }
49
77
 
50
78
  /**
51
79
  * Open the findbar. If the findbar is already opened, focus the input text.
80
+ * @returns {void}
52
81
  */
53
82
  open() {
54
83
  if (this.#window) {
55
84
  this.#focusWindowAndHighlightInput()
56
85
  return
57
86
  }
58
- this.#window = new BrowserWindow(Findbar.#mergeStandardOptions(this.#customOptions, this.#parent))
87
+ const options = Findbar.#mergeStandardOptions(this.#customOptions, this.#parent)
88
+ this.#isMovable = options.movable
89
+ this.#window = new BrowserWindow(options)
59
90
  this.#window.webContents._findbar = this
60
91
 
61
92
  this.#registerListeners()
62
93
 
63
- const pos = this.#positionHandler(this.#parent.getBounds(), this.#window.getBounds())
64
- this.#window.setPosition(pos.x, pos.y)
65
-
66
94
  this.#windowHandler && this.#windowHandler(this.#window)
67
95
 
68
96
  this.#window.loadFile(`${__dirname}/web/findbar.html`)
@@ -70,6 +98,7 @@ class Findbar {
70
98
 
71
99
  /**
72
100
  * Close the findbar.
101
+ * @returns {void}
73
102
  */
74
103
  close() {
75
104
  if (!this.#window || this.#window.isDestroyed()) { return }
@@ -79,53 +108,72 @@ class Findbar {
79
108
  }
80
109
 
81
110
  /**
82
- * Get last queried text.
111
+ * Get the last state of the findbar.
112
+ * @returns {{ text: string, matchCase: boolean, movable: boolean }} Last state of the findbar.
83
113
  */
84
- getLastText() {
85
- return this.#lastText
114
+ getLastState() {
115
+ return { text: this.#lastText, matchCase: this.#matchCase, movable: this.#isMovable }
86
116
  }
87
117
 
88
118
  /**
89
119
  * Starts a request to find all matches for the text in the page.
90
- * @param {string} text Value to find in page.
91
- * @param {boolean | void} skipInputUpdate Skip findbar input update.
120
+ * @param {string} text - Value to find in page.
121
+ * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
122
+ * @returns {void}
92
123
  */
93
- startFind(text, skipInputUpdate) {
94
- skipInputUpdate || this.#window?.webContents.send('electron-findbar/text-change', text)
124
+ startFind(text, skipRendererEvent) {
125
+ skipRendererEvent || this.#window?.webContents.send('electron-findbar/text-change', text)
95
126
  if (this.#lastText = text) {
96
- this.isOpen() && this.#findableContents.findInPage(this.#lastText, { findNext: true })
127
+ this.isOpen() && this.#findInContent({ findNext: true })
97
128
  } else {
98
129
  this.stopFind()
99
130
  }
100
131
  }
101
132
 
133
+ /**
134
+ * Whether the search should be case-sensitive. If not set, the search will be case-insensitive.
135
+ * @param {boolean} status - Whether the search should be case-sensitive. Default is false.
136
+ * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
137
+ * @returns {void}
138
+ */
139
+ matchCase(status, skipRendererEvent) {
140
+ if (this.#matchCase === status) { return }
141
+ this.#matchCase = status
142
+ skipRendererEvent || this.#window?.webContents.send('electron-findbar/match-case-change', this.#matchCase)
143
+ this.#stopFindInContent()
144
+ this.startFind(this.#lastText, skipRendererEvent)
145
+ }
146
+
102
147
  /**
103
148
  * Select previous match if any.
149
+ * @returns {void}
104
150
  */
105
151
  findPrevious() {
106
152
  this.#matches.active === 1 && (this.#fixMove = false)
107
- this.isOpen() && this.#findableContents.findInPage(this.#lastText, { forward: false })
153
+ this.isOpen() && this.#findInContent({ forward: false })
108
154
  }
109
155
 
110
156
  /**
111
157
  * Select next match if any.
158
+ * @returns {void}
112
159
  */
113
160
  findNext() {
114
161
  this.#matches.active === this.#matches.total && (this.#fixMove = true)
115
- this.isOpen() && this.#findableContents.findInPage(this.#lastText, { forward: true })
162
+ this.isOpen() && this.#findInContent({ forward: true })
116
163
  }
117
164
 
118
165
  /**
119
- * Stops the find request.
166
+ * Stops the find request and clears selection.
167
+ * @returns {void}
120
168
  */
121
169
  stopFind() {
122
170
  this.isOpen() && this.#sendMatchesCount(0, 0)
123
- this.#findableContents.isDestroyed() || this.#findableContents.stopFindInPage("clearSelection")
171
+ this.#findableContents.isDestroyed() || this.#stopFindInContent()
124
172
  }
125
173
 
126
174
  /**
127
175
  * Whether the findbar is opened.
128
- * @returns {boolean} True, if the findbar is open. Otherwise, false.
176
+ * @returns {boolean} True if the findbar is open, otherwise false.
129
177
  */
130
178
  isOpen() {
131
179
  return !!this.#window
@@ -133,15 +181,16 @@ class Findbar {
133
181
 
134
182
  /**
135
183
  * Whether the findbar is focused. If the findbar is closed, false will be returned.
136
- * @returns {boolean} True, if the findbar is focused. Otherwise, false.
184
+ * @returns {boolean} True if the findbar is focused, otherwise false.
137
185
  */
138
186
  isFocused() {
139
187
  return !!this.#window?.isFocused()
140
188
  }
141
189
 
142
190
  /**
143
- * Whether the findbar is visible to the user in the foreground of the app. If the findbar is closed, false will be returned.
144
- * @returns {boolean} True, if the findbar is visible. Otherwise, false.
191
+ * Whether the findbar is visible to the user in the foreground of the app.
192
+ * If the findbar is closed, false will be returned.
193
+ * @returns {boolean} True if the findbar is visible, otherwise false.
145
194
  */
146
195
  isVisible() {
147
196
  return !!this.#window?.isVisible()
@@ -160,7 +209,9 @@ class Findbar {
160
209
  * - options.fullscreenable (value: false)
161
210
  * - options.webPreferences.nodeIntegration (value: true)
162
211
  * - options.webPreferences.contextIsolation (value: false)
163
- * @param {BrowserWindowConstructorOptions} customOptions Custom window options.
212
+ *
213
+ * @param {BrowserWindowConstructorOptions} customOptions - Custom window options.
214
+ * @returns {void}
164
215
  */
165
216
  setWindowOptions(customOptions) {
166
217
  this.#customOptions = customOptions
@@ -168,18 +219,32 @@ class Findbar {
168
219
 
169
220
  /**
170
221
  * Set a window handler capable of changing the findbar window settings after opening.
171
- * @param {(findbarWindow: BrowserWindow) => void} windowHandler Window handler.
222
+ * @param {(findbarWindow: BrowserWindow) => void} windowHandler - Window handler function.
223
+ * @returns {void}
172
224
  */
173
225
  setWindowHandler(windowHandler) {
174
226
  this.#windowHandler = windowHandler
175
227
  }
176
228
 
177
229
  /**
178
- * Set a bounds handler to calculate the findbar bounds when the parent resizes.
179
- * @param {{parentBounds: Rectangle, findbarBounds: Rectangle} => Rectangle} boundsHandler Bounds handler.
230
+ * Set a bounds handler to calculate the findbar bounds when the parent window resizes. If width and/or height are not provided, the current value will be used.
231
+ * @param {(parentBounds: Rectangle, findbarBounds: Rectangle) => Rectangle} boundsHandler - Bounds handler function.
232
+ * @returns {void}
180
233
  */
181
234
  setBoundsHandler(boundsHandler) {
182
- this.#positionHandler = boundsHandler
235
+ this.#boundsHandler = boundsHandler
236
+ }
237
+
238
+ /**
239
+ * @param {Electron.FindInPageOptions} options
240
+ */
241
+ #findInContent(options) {
242
+ options.matchCase = this.#matchCase
243
+ this.#findableContents.findInPage(this.#lastText, options)
244
+ }
245
+
246
+ #stopFindInContent() {
247
+ this.#findableContents.stopFindInPage('clearSelection')
183
248
  }
184
249
 
185
250
  /**
@@ -188,21 +253,29 @@ class Findbar {
188
253
  #registerListeners() {
189
254
  const showCascade = () => this.#window.isVisible() || this.#window.show()
190
255
  const hideCascade = () => this.#window.isVisible() && this.#window.hide()
191
- const positionHandler = () => {
192
- const pos = this.#positionHandler(this.#parent.getBounds(), this.#window.getBounds())
193
- this.#window.setPosition(pos.x, pos.y)
256
+ const boundsHandler = () => {
257
+ const currentBounds = this.#window.getBounds()
258
+ const newBounds = this.#boundsHandler(this.#parent.getBounds(), currentBounds)
259
+ if (!newBounds.width) { newBounds.width = currentBounds.width }
260
+ if (!newBounds.height) { newBounds.height = currentBounds.height }
261
+ this.#window.setBounds(newBounds, false)
194
262
  }
195
263
 
196
- this.#parent.prependListener('show', showCascade)
197
- this.#parent.prependListener('hide', hideCascade)
198
- this.#parent.prependListener('resize', positionHandler)
199
- this.#parent.prependListener('move', positionHandler)
264
+ if (this.#parent && !this.#parent.isDestroyed()) {
265
+ boundsHandler()
266
+ this.#parent.prependListener('show', showCascade)
267
+ this.#parent.prependListener('hide', hideCascade)
268
+ this.#parent.prependListener('resize', boundsHandler)
269
+ this.#parent.prependListener('move', boundsHandler)
270
+ }
200
271
 
201
272
  this.#window.once('close', () => {
202
- this.#parent.off('show', showCascade)
203
- this.#parent.off('hide', hideCascade)
204
- this.#parent.off('resize', positionHandler)
205
- this.#parent.off('move', positionHandler)
273
+ if (this.#parent && !this.#parent.isDestroyed()) {
274
+ this.#parent.off('show', showCascade)
275
+ this.#parent.off('hide', hideCascade)
276
+ this.#parent.off('resize', boundsHandler)
277
+ this.#parent.off('move', boundsHandler)
278
+ }
206
279
  this.#window = null
207
280
  this.stopFind()
208
281
  })
@@ -279,14 +352,53 @@ class Findbar {
279
352
  options.webPreferences.preload = options.webPreferences.preload ?? `${__dirname}/web/preload.js`
280
353
  return options
281
354
  }
355
+
356
+ /**
357
+ * Get the findbar instance for a given BrowserWindow or WebContents.
358
+ * If no findbar instance exists, it will return a new one linked to the web contents.
359
+ *
360
+ * @overload
361
+ * @param {BrowserWindow} browserWindow Parent window.
362
+ * @param {WebContents} [customWebContents] Custom findable web contents. If not provided, the web contents of the BrowserWindow will be used.
363
+ * @returns {Findbar} The findbar instance if it exists.
364
+ *
365
+ * @overload
366
+ * @param {WebContents} webContents Findable web contents. The parent window will be undefined.
367
+ * @returns {Findbar} The findbar instance if it exists.
368
+ * @throws {Error} If no webContents is provided.
369
+ *
370
+ * @overload
371
+ * @param {BaseWindow} baseWindow Parent window.
372
+ * @param {WebContents} webContents Findable web contents.
373
+ * @returns {Findbar} The findbar instance if it exists.
374
+ * @throws {Error} If no webContents is provided.
375
+ */
376
+ static from(windowOrWebContents, customWebContents) {
377
+ let webContents = isFindable(windowOrWebContents) ?
378
+ windowOrWebContents : customWebContents ?? windowOrWebContents.webContents
379
+
380
+ return webContents._findbar || new Findbar(windowOrWebContents, customWebContents)
381
+ }
382
+
383
+ /**
384
+ * Get the findbar instance for a given BrowserWindow or WebContents.
385
+ * @param {BrowserWindow | WebContents} windowOrWebContents
386
+ * @returns {Findbar | undefined} The findbar instance if it exists, otherwise undefined.
387
+ */
388
+ static fromIfExists(windowOrWebContents) {
389
+ return (isFindable(windowOrWebContents) ? windowOrWebContents : windowOrWebContents.webContents)._findbar
390
+ }
282
391
  }
283
392
 
393
+ const isFindable = (obj) => obj && typeof obj.findInPage === 'function' && typeof obj.stopFindInPage === 'function';
394
+
284
395
  /**
285
396
  * Define IPC events.
286
397
  */
287
398
  (ipc => {
288
- ipc.handle('electron-findbar/last-text', e => e.sender._findbar.getLastText())
399
+ ipc.handle('electron-findbar/last-state', e => e.sender._findbar.getLastState())
289
400
  ipc.on('electron-findbar/input-change', (e, text, skip) => e.sender._findbar.startFind(text, skip))
401
+ ipc.on('electron-findbar/match-case', (e, status, skip) => e.sender._findbar.matchCase(status, skip))
290
402
  ipc.on('electron-findbar/previous', e => e.sender._findbar.findPrevious())
291
403
  ipc.on('electron-findbar/next', e => e.sender._findbar.findNext())
292
404
  ipc.on('electron-findbar/open', e => e.sender._findbar.open())
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "electron-findbar",
3
- "version": "1.1.0",
3
+ "version": "2.0.1",
4
4
  "description": "Chrome-like findbar for your Electron app.",
5
5
  "main": "index.js",
6
6
  "files": [
package/remote.js CHANGED
@@ -3,19 +3,41 @@
3
3
  */
4
4
  const Remote = (ipc => ({
5
5
  /**
6
- * Get last queried text.
7
- * @returns {string}
6
+ * Get last queried text and the "match case" status.
7
+ * @returns {Promise<{ text: string, matchCase: boolean, movable: boolean }>}
8
8
  */
9
- getLastText: async () => ipc.invoke('electron-findbar/last-text'),
9
+ getLastState: async () => ipc.invoke('electron-findbar/last-state'),
10
10
 
11
11
  /**
12
12
  * Change the input value and find it.
13
- * @param {string} text
13
+ * @param {string} text - The text to search for
14
14
  */
15
15
  inputChange: (text) => { ipc.send('electron-findbar/input-change', text) },
16
+
17
+ /**
18
+ * Toggle case sensitive search
19
+ * @param {boolean} value - Whether to match case or not
20
+ */
21
+ matchCase: (value) => { ipc.send('electron-findbar/match-case', value) },
22
+
23
+ /**
24
+ * Navigate to the previous match
25
+ */
16
26
  previous: () => { ipc.send('electron-findbar/previous') },
27
+
28
+ /**
29
+ * Navigate to the next match
30
+ */
17
31
  next: () => { ipc.send('electron-findbar/next') },
32
+
33
+ /**
34
+ * Open the findbar
35
+ */
18
36
  open: () => { ipc.send('electron-findbar/open') },
37
+
38
+ /**
39
+ * Close the findbar
40
+ */
19
41
  close: () => { ipc.send('electron-findbar/close') },
20
42
  })) (require('electron').ipcRenderer)
21
43
 
package/web/app.css CHANGED
@@ -11,28 +11,37 @@ body {
11
11
 
12
12
  nav {
13
13
  --bg-color: #fff;
14
- --border: #eee;
14
+ --border: #ddd;
15
15
  --color: #626262;
16
- --input-color: #1f1f1f;
16
+ --input-color: #111;
17
17
  --btn-hover-color: #ccc;
18
18
  --btn-active-color: #bbb;
19
19
  --font-family: system-ui,-apple-system,"Segoe UI",Roboto,"Helvetica Neue","Noto Sans","Liberation Sans",Arial,sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
20
20
  --font-size: .75rem;
21
- --spacing: .75rem;
21
+ --spacing: .5rem;
22
22
  }
23
23
 
24
24
  @media (prefers-color-scheme: dark) {
25
25
  nav {
26
26
  --bg-color: #1f1f1f;
27
- --border: #2f2f2f;
27
+ --border: #3f3f3f;
28
28
  --color: #a9a9aa;
29
- --input-color: #e3e3e3;
29
+ --input-color: #eee;
30
30
  --btn-hover-color: #333;
31
31
  --btn-active-color: #444;
32
32
  }
33
33
  }
34
34
 
35
- svg {
35
+ #match-case {
36
+ fill: var(--color);
37
+ user-select: none;
38
+ }
39
+
40
+ #match-case > svg {
41
+ width: 20px;
42
+ }
43
+
44
+ #previous, #next, #close {
36
45
  fill: none;
37
46
  stroke: var(--color);
38
47
  stroke-width: 2;
@@ -48,15 +57,15 @@ nav {
48
57
  padding: var(--spacing);
49
58
  background-color: var(--bg-color);
50
59
  color: var(--color);
60
+ }
61
+
62
+ .linux nav {
51
63
  border-radius: 10px;
52
64
  border: 1px solid var(--border);
53
- -webkit-app-region: drag;
54
- app-region: drag;
55
65
  }
56
66
 
57
- nav > *:not(:last-child),
58
- .btn-group > *:not(:last-child) {
59
- margin-right: var(--spacing);
67
+ nav, .btn-group {
68
+ gap: var(--spacing);
60
69
  }
61
70
 
62
71
  span, input {
@@ -65,6 +74,7 @@ span, input {
65
74
  }
66
75
 
67
76
  span {
77
+ color: var(--input-color);
68
78
  user-select: none;
69
79
  }
70
80
 
@@ -75,8 +85,6 @@ input {
75
85
  font-weight: 500;
76
86
  border: none;
77
87
  outline: none;
78
- -webkit-app-region: no-drag;
79
- app-region: no-drag;
80
88
  }
81
89
 
82
90
  .divider {
@@ -89,27 +97,45 @@ input {
89
97
  display: flex;
90
98
  }
91
99
 
92
- .btn-group > div {
100
+ button {
101
+ background-color: transparent;
102
+ border: none;
93
103
  border-radius: 50%;
94
104
  cursor: default;
95
105
  width: 26px;
96
106
  height: 26px;
97
107
  padding: 3px;
98
108
  text-align: center;
99
- transition: .1s linear all;
100
- -webkit-app-region: no-drag;
101
- app-region: no-drag;
109
+ transition: .2s linear all;
102
110
  }
103
111
 
104
- .btn-group > div:hover {
112
+ button:hover {
105
113
  background-color: var(--btn-hover-color);
106
114
  }
107
115
 
108
- .btn-group > div:active {
116
+ button:active {
109
117
  background-color: var(--btn-active-color);
110
118
  }
111
119
 
112
- .btn-group > .disabled {
120
+ button:focus {
121
+ outline: none;
122
+ }
123
+
124
+ button.disabled {
113
125
  opacity: .4;
126
+ }
127
+
128
+ #previous.disabled, #next.disabled {
114
129
  background-color: transparent !important;
130
+ }
131
+
132
+ .movable nav {
133
+ -webkit-app-region: drag;
134
+ app-region: drag;
135
+ }
136
+
137
+ input, button {
138
+ -webkit-app-region: no-drag;
139
+ app-region: no-drag;
140
+
115
141
  }
package/web/findbar.html CHANGED
@@ -5,30 +5,37 @@
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
7
  <meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self'; default-src 'self'; connect-src 'none'; frame-src 'none'; object-src 'none'; font-src 'self'; img-src 'self'">
8
- <title>Findbar</title>
8
+ <title>Find in page</title>
9
9
  <link rel="stylesheet" href="app.css">
10
- <body>
10
+ <body class="movable">
11
11
  <nav>
12
- <input id='input' type="text" spellcheck="false">
12
+ <input id="input" type="text" spellcheck="false">
13
13
  <span id="matches"></span>
14
+ <button id="match-case" class="disabled" title="Match case">
15
+ <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" stroke="none">
16
+ <text x="4" y="15" font-size="14" font-family="monospace" font-weight="bold">A</text>
17
+ <text x="12" y="19" font-size="14" font-family="monospace">a</text>
18
+ </svg>
19
+ </button>
14
20
  <div class="divider"></div>
15
21
  <div class="btn-group">
16
- <div id="previous" class="disabled">
22
+
23
+ <button id="previous" class="disabled" title="Previous">
17
24
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
18
25
  <path d="M7 15L12 10L17 15"/>
19
26
  </svg>
20
- </div>
21
- <div id="next" class="disabled">
27
+ </button>
28
+ <button id="next" class="disabled" title="Next">
22
29
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
23
30
  <path d="M7 10L12 15L17 10"/>
24
31
  </svg>
25
- </div>
26
- <div id="close">
32
+ </button>
33
+ <button id="close" title="Close">
27
34
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
28
35
  <path d="M7 17.5L12 12.5L17 17.5"/>
29
36
  <path d="M7 7.5L12 12.5L17 7.5"/>
30
37
  </svg>
31
- </div>
38
+ </button>
32
39
  </div>
33
40
  </nav>
34
41
  </body>
package/web/preload.js CHANGED
@@ -1,12 +1,14 @@
1
1
  const $remote = (ipc => ({
2
- getLastText: async () => ipc.invoke('electron-findbar/last-text'),
2
+ getLastState: async () => ipc.invoke('electron-findbar/last-state'),
3
3
  inputChange: (value) => { ipc.send('electron-findbar/input-change', value, true) },
4
+ matchCase: (value) => { ipc.send('electron-findbar/match-case', value, true) },
4
5
  previous: () => { ipc.send('electron-findbar/previous') },
5
6
  next: () => { ipc.send('electron-findbar/next') },
6
7
  close: () => { ipc.send('electron-findbar/close') },
7
8
  onMatchesChange: (listener) => { ipc.on('electron-findbar/matches', listener) },
8
9
  onInputFocus: (listener) => { ipc.on('electron-findbar/input-focus', listener) },
9
- onTextChange: (listener) => { ipc.on('electron-findbar/text-change', listener) }
10
+ onTextChange: (listener) => { ipc.on('electron-findbar/text-change', listener) },
11
+ onMatchCaseChange: (listener) => { ipc.on('electron-findbar/match-case-change', listener) }
10
12
  })) (require('electron').ipcRenderer)
11
13
 
12
14
  let canRequest = true, canMove = false
@@ -16,6 +18,12 @@ function inputChange(e) {
16
18
  $remote.inputChange(e.target.value)
17
19
  }
18
20
 
21
+ function matchCaseChange(btn) {
22
+ const newStatus = buttonIsDisabled(btn)
23
+ toggleButton(btn, newStatus)
24
+ $remote.matchCase(newStatus)
25
+ }
26
+
19
27
  function move(next) {
20
28
  if (canRequest && canMove) {
21
29
  canRequest = false
@@ -23,38 +31,51 @@ function move(next) {
23
31
  }
24
32
  }
25
33
 
34
+ function toggleButton(btn, status) {
35
+ btn.classList[status ? 'remove' : 'add']('disabled')
36
+ }
37
+
38
+ function buttonIsDisabled(btn) {
39
+ return btn.classList.contains('disabled')
40
+ }
41
+
26
42
  document.addEventListener('DOMContentLoaded', async () => {
27
43
  const inputEl = document.getElementById('input')
44
+ const matchCaseBtn = document.getElementById('match-case')
28
45
  const previousBtn = document.getElementById('previous')
29
46
  const nextBtn = document.getElementById('next')
30
47
  const closeBtn = document.getElementById('close')
31
48
  const matchesEl = document.getElementById('matches')
32
- const moveBtns = [...document.getElementsByClassName('disabled')]
49
+ const moveBtns = [previousBtn, nextBtn]
33
50
 
51
+ matchCaseBtn.addEventListener('click', () => matchCaseChange(matchCaseBtn))
34
52
  previousBtn.addEventListener('click', () => move(false))
35
53
  nextBtn.addEventListener('click', () => move(true))
36
54
  closeBtn.addEventListener('click', () => $remote.close())
37
55
  inputEl.addEventListener('input', inputChange)
38
56
 
39
- $remote.onMatchesChange((_, m) => {
40
- canRequest = true
41
- matchesEl.innerText = inputEl.value ? m.active + '/' + m.total : ''
42
-
43
- for (var moveBtn of moveBtns) {
44
- (canMove = m.total > 1) ?
45
- moveBtn.classList.remove('disabled') :
46
- moveBtn.classList.add('disabled')
47
- }
48
- })
57
+ $remote.onTextChange((_, text) => { inputEl.value = text })
49
58
 
50
59
  $remote.onInputFocus(() => {
51
60
  inputEl.setSelectionRange(0, inputEl.value.length)
52
61
  inputEl.focus()
53
62
  })
54
63
 
55
- $remote.onTextChange((_, text) => { inputEl.value = text })
64
+ $remote.onMatchCaseChange((_, status) => { console.log('Match case:', status);toggleButton(matchCaseBtn, status) })
56
65
 
57
- inputEl.value = await $remote.getLastText()
66
+ $remote.onMatchesChange((_, m) => {
67
+ canRequest = true
68
+ matchesEl.innerText = inputEl.value ? m.active + '/' + m.total : ''
69
+ for (var moveBtn of moveBtns) { toggleButton(moveBtn, canMove = m.total > 1) }
70
+ })
71
+
72
+ const lastState = await $remote.getLastState()
73
+ inputEl.value = lastState.text || ''
74
+ lastState.movable || document.body.classList.remove('movable')
75
+ if (process.platform === 'linux') {
76
+ document.body.classList.add('linux')
77
+ }
78
+ toggleButton(matchCaseBtn, lastState.matchCase)
58
79
  $remote.inputChange(inputEl.value)
59
80
  inputEl.setSelectionRange(0, inputEl.value.length)
60
81
  inputEl.focus()