electron-findbar 1.0.1 → 2.0.0

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,7 +1,7 @@
1
1
  <p align='center'>
2
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'>
3
+ <img src="https://i.postimg.cc/sXwqJP59/findbar-v2-light.png" alt='Findbar Light Theme'>
4
+ <img src="https://i.postimg.cc/j26XXRVV/findbar-v2-dark.png" alt='Findbar Dark Theme'>
5
5
  </a>
6
6
  </p>
7
7
  <p align='center'>
@@ -47,13 +47,35 @@ const Findbar = require('electron-findbar')
47
47
  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
48
 
49
49
  ```js
50
- const findbar = new Findbar(browserWindow)
50
+ // Create or retrieve the findbar associated to the browserWindow.webContents. If a new findbar is created, the browserWindow is used as parent.
51
+ const findbar = Findbar.from(browserWindow)
51
52
  ```
52
53
 
53
54
  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
55
 
55
56
  ```js
56
- const findbar = new Findbar(baseWindow, customWebContents)
57
+ // Create or retrieve the findbar associated to the webContents. If a new findbar is created, the baseWindow is used as parent.
58
+ const findbar = Findbar.from(baseWindow, webContents)
59
+ ```
60
+
61
+ Is also possible to create a findbar without a parent window (even though it is not recommended):
62
+
63
+ ```js
64
+ // 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.
65
+ const findbar = Findbar.from(webContents)
66
+ ```
67
+
68
+ **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.
69
+
70
+ #### Retrieve if exists
71
+
72
+ If there is no intention to create a new findbar in case it does not exist, use:
73
+
74
+ ```js
75
+ // Get the existing findbar or undefined.
76
+ const existingFindbar = Findbar.fromIfExists(browserWindow)
77
+ /* OR */
78
+ const existingFindbar = Findbar.fromIfExists(webContents)
57
79
  ```
58
80
 
59
81
  ### Configuring the Findbar
@@ -61,7 +83,7 @@ const findbar = new Findbar(baseWindow, customWebContents)
61
83
  You can customize the Findbar window options using the `setWindowOptions` method:
62
84
 
63
85
  ```js
64
- findbar.setWindowOptions({ movable: true, resizable: true, alwaysOnTop: true })
86
+ findbar.setWindowOptions({ resizable: true, alwaysOnTop: true, height: 100 })
65
87
  ```
66
88
 
67
89
  To handle the Findbar window directly after it is opened, use the `setWindowHandler` method:
@@ -72,12 +94,14 @@ findbar.setWindowHandler(win => {
72
94
  });
73
95
  ```
74
96
 
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.
97
+ 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
98
 
77
99
  ```js
78
- findbar.setPositionHandler((parentBounds, findbarBounds) => ({
100
+ findbar.setBoundsHandler((parentBounds, findbarBounds) => ({
79
101
  x: parentBounds.x + parentBounds.width - findbarBounds.width - 20,
80
102
  y: parentBounds.y - ((findbarBounds.height / 4) | 0)
103
+ /* width: OPTIONAL, current value will be used */
104
+ /* height: OPTIONAL, current value will be used */
81
105
  }))
82
106
  ```
83
107
 
@@ -89,38 +113,6 @@ The Findbar is a child window of the `BaseWindow` passed during construction. To
89
113
  findbar.open()
90
114
  ```
91
115
 
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
116
  ### Closing the Findbar
125
117
 
126
118
  When the Findbar is closed, its window is destroyed to free memory resources. Use the following method to close the Findbar:
@@ -144,21 +136,148 @@ app.whenReady().then(() => {
144
136
  window.loadURL('https://github.com/ECRomaneli/electron-findbar')
145
137
 
146
138
  // Create and configure the Findbar object
147
- const findbar = new Findbar(window)
139
+ const findbar = Findbar.from(window)
148
140
 
149
141
  // [OPTIONAL] Customize window options
150
142
  findbar.setWindowOptions({ movable: true, resizable: true })
151
143
 
152
144
  // [OPTIONAL] Handle the window object when the Findbar is opened
153
- findbar.setWindowHandler(win => {
154
- win.webContents.openDevTools()
155
- })
145
+ findbar.setWindowHandler(win => { win.webContents.openDevTools() })
156
146
 
157
147
  // Open the Findbar
158
148
  findbar.open()
159
149
  })
160
150
  ```
161
151
 
152
+ ### Configuring Keyboard Shortcuts
153
+
154
+ 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.
155
+
156
+ **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.
157
+
158
+ #### Using Before Input Event
159
+
160
+ 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:
161
+
162
+ ```js
163
+ window.webContents.on('before-input-event', (event, input) => {
164
+ // Detect Ctrl+F (Windows/Linux) or Command+F (macOS)
165
+ if ((input.control || input.meta) && input.key.toLowerCase() === 'f') {
166
+ // Prevent default browser behavior
167
+ event.preventDefault()
168
+
169
+ // Access and open the findbar
170
+ const findbar = Findbar.from(window)
171
+ if (findbar) {
172
+ findbar.open()
173
+ }
174
+ }
175
+
176
+ // Handle Escape key to close the findbar
177
+ if (input.key === 'Escape') {
178
+ const findbar = Findbar.from(window)
179
+ if (findbar && findbar.isOpen()) {
180
+ event.preventDefault()
181
+ findbar.close()
182
+ }
183
+ }
184
+ })
185
+ ```
186
+
187
+ #### Using Menu Accelerators
188
+
189
+ 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:
190
+
191
+ ```js
192
+ // Get reference to the parent window
193
+ const parent = currentBrowserWindowOrWebContents
194
+
195
+ // Get or create application menu
196
+ const appMenu = Menu.getApplicationMenu() ?? new Menu()
197
+
198
+ // Add Findbar controls to menu
199
+ appMenu.append(new MenuItem({
200
+ label: 'Find',
201
+ submenu: [
202
+ {
203
+ label: 'Find in Page',
204
+ click: () => Findbar.from(parent)?.open(),
205
+ accelerator: 'CommandOrControl+F'
206
+ },
207
+ {
208
+ label: 'Close Find',
209
+ click: () => Findbar.from(parent)?.close(),
210
+ accelerator: 'Esc'
211
+ }
212
+ ]
213
+ }))
214
+
215
+ // Apply the updated menu
216
+ Menu.setApplicationMenu(appMenu)
217
+ ```
218
+
219
+ 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.
220
+
221
+ ### Finding Text using the main process
222
+
223
+ 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:
224
+
225
+ ```js
226
+ /**
227
+ * Get the last state of the findbar.
228
+ * @returns {{ text: string, matchCase: boolean, movable: boolean }} Last state of the findbar.
229
+ */
230
+ getLastState()
231
+
232
+ /**
233
+ * Initiate a request to find all matches for the specified text on the page.
234
+ * @param {string} text - The text to search for.
235
+ * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
236
+ */
237
+ startFind(text, skipRendererEvent)
238
+
239
+ /**
240
+ * Whether the search should be case-sensitive.
241
+ * @param {boolean} status - Whether the search should be case-sensitive. Default is false.
242
+ * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
243
+ */
244
+ matchCase(status, skipRendererEvent)
245
+
246
+ /**
247
+ * Select the previous match, if available.
248
+ */
249
+ findPrevious()
250
+
251
+ /**
252
+ * Select the next match, if available.
253
+ */
254
+ findNext()
255
+
256
+ /**
257
+ * Stop the find request and clears selection.
258
+ */
259
+ stopFind()
260
+
261
+ /**
262
+ * Whether the findbar is opened.
263
+ * @returns {boolean} True if the findbar is open, otherwise false.
264
+ */
265
+ isOpen()
266
+
267
+ /**
268
+ * Whether the findbar is focused. If the findbar is closed, false will be returned.
269
+ * @returns {boolean} True if the findbar is focused, otherwise false.
270
+ */
271
+ isFocused()
272
+
273
+ /**
274
+ * Whether the findbar is visible to the user in the foreground of the app.
275
+ * If the findbar is closed, false will be returned.
276
+ * @returns {boolean} True if the findbar is visible, otherwise false.
277
+ */
278
+ isVisible()
279
+ ```
280
+
162
281
  ## IPC Events
163
282
 
164
283
  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 +288,12 @@ If the `contextIsolation` is enabled, the `electron-findbar/remote` will not be
169
288
 
170
289
  ```js
171
290
  const $remote = (ipc => ({
172
- getLastText: async () => ipc.invoke('electron-findbar/last-text'),
291
+ getLastState: async () => ipc.invoke('electron-findbar/last-state'),
173
292
  inputChange: (value) => { ipc.send('electron-findbar/input-change', value) },
293
+ matchCase: (value) => { ipc.send('electron-findbar/match-case', value) },
174
294
  previous: () => { ipc.send('electron-findbar/previous') },
175
295
  next: () => { ipc.send('electron-findbar/next') },
176
- open: () => { ipc.send('electron-findbar/open') },
177
- close: () => { ipc.send('electron-findbar/close') },
296
+ close: () => { ipc.send('electron-findbar/close') }
178
297
  })) (require('electron').ipcRenderer)
179
298
 
180
299
  $remote.open()
@@ -198,5 +317,5 @@ Created by [Emerson Capuchi Romaneli](https://github.com/ECRomaneli) (@ECRomanel
198
317
 
199
318
  ## License
200
319
 
201
- This project is licensed under the [MIT License](https://github.com/ECRomaneli/handbook/blob/master/LICENSE).
320
+ This project is licensed under the [MIT License](https://github.com/ECRomaneli/electron-findbar/blob/master/LICENSE).
202
321
 
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,59 +98,82 @@ class Findbar {
70
98
 
71
99
  /**
72
100
  * Close the findbar.
101
+ * @returns {void}
73
102
  */
74
103
  close() {
75
- this.#window?.close()
104
+ if (!this.#window || this.#window.isDestroyed()) { return }
105
+ if (!this.#window.isVisible()) { this.#window.close(); return }
106
+ this.#window.on('hide', () => { this.#window.close() })
107
+ this.#window.hide()
76
108
  }
77
109
 
78
110
  /**
79
- * 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.
80
113
  */
81
- getLastText() {
82
- return this.#lastText
114
+ getLastState() {
115
+ return { text: this.#lastText, matchCase: this.#matchCase, movable: this.#isMovable }
83
116
  }
84
117
 
85
118
  /**
86
119
  * Starts a request to find all matches for the text in the page.
87
- * @param {string} text Value to find in page.
88
- * @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}
89
123
  */
90
- startFind(text, skipInputUpdate) {
91
- 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)
92
126
  if (this.#lastText = text) {
93
- this.isOpen() && this.#findableContents.findInPage(this.#lastText, { findNext: true })
127
+ this.isOpen() && this.#findInContent({ findNext: true })
94
128
  } else {
95
129
  this.stopFind()
96
130
  }
97
131
  }
98
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
+
99
147
  /**
100
148
  * Select previous match if any.
149
+ * @returns {void}
101
150
  */
102
151
  findPrevious() {
103
152
  this.#matches.active === 1 && (this.#fixMove = false)
104
- this.isOpen() && this.#findableContents.findInPage(this.#lastText, { forward: false })
153
+ this.isOpen() && this.#findInContent({ forward: false })
105
154
  }
106
155
 
107
156
  /**
108
157
  * Select next match if any.
158
+ * @returns {void}
109
159
  */
110
160
  findNext() {
111
161
  this.#matches.active === this.#matches.total && (this.#fixMove = true)
112
- this.isOpen() && this.#findableContents.findInPage(this.#lastText, { forward: true })
162
+ this.isOpen() && this.#findInContent({ forward: true })
113
163
  }
114
164
 
115
165
  /**
116
- * Stops the find request.
166
+ * Stops the find request and clears selection.
167
+ * @returns {void}
117
168
  */
118
169
  stopFind() {
119
170
  this.isOpen() && this.#sendMatchesCount(0, 0)
120
- this.#findableContents.isDestroyed() || this.#findableContents.stopFindInPage("clearSelection")
171
+ this.#findableContents.isDestroyed() || this.#stopFindInContent()
121
172
  }
122
173
 
123
174
  /**
124
175
  * Whether the findbar is opened.
125
- * @returns {boolean} True, if the findbar is open. Otherwise, false.
176
+ * @returns {boolean} True if the findbar is open, otherwise false.
126
177
  */
127
178
  isOpen() {
128
179
  return !!this.#window
@@ -130,15 +181,16 @@ class Findbar {
130
181
 
131
182
  /**
132
183
  * Whether the findbar is focused. If the findbar is closed, false will be returned.
133
- * @returns {boolean} True, if the findbar is focused. Otherwise, false.
184
+ * @returns {boolean} True if the findbar is focused, otherwise false.
134
185
  */
135
186
  isFocused() {
136
187
  return !!this.#window?.isFocused()
137
188
  }
138
189
 
139
190
  /**
140
- * Whether the findbar is visible to the user in the foreground of the app. If the findbar is closed, false will be returned.
141
- * @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.
142
194
  */
143
195
  isVisible() {
144
196
  return !!this.#window?.isVisible()
@@ -157,7 +209,9 @@ class Findbar {
157
209
  * - options.fullscreenable (value: false)
158
210
  * - options.webPreferences.nodeIntegration (value: true)
159
211
  * - options.webPreferences.contextIsolation (value: false)
160
- * @param {BrowserWindowConstructorOptions} customOptions Custom window options.
212
+ *
213
+ * @param {BrowserWindowConstructorOptions} customOptions - Custom window options.
214
+ * @returns {void}
161
215
  */
162
216
  setWindowOptions(customOptions) {
163
217
  this.#customOptions = customOptions
@@ -165,18 +219,32 @@ class Findbar {
165
219
 
166
220
  /**
167
221
  * Set a window handler capable of changing the findbar window settings after opening.
168
- * @param {(findbarWindow: BrowserWindow) => void} windowHandler Window handler.
222
+ * @param {(findbarWindow: BrowserWindow) => void} windowHandler - Window handler function.
223
+ * @returns {void}
169
224
  */
170
225
  setWindowHandler(windowHandler) {
171
226
  this.#windowHandler = windowHandler
172
227
  }
173
228
 
174
229
  /**
175
- * Set a bounds handler to calculate the findbar bounds when the parent resizes.
176
- * @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}
177
233
  */
178
234
  setBoundsHandler(boundsHandler) {
179
- 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')
180
248
  }
181
249
 
182
250
  /**
@@ -185,25 +253,35 @@ class Findbar {
185
253
  #registerListeners() {
186
254
  const showCascade = () => this.#window.isVisible() || this.#window.show()
187
255
  const hideCascade = () => this.#window.isVisible() && this.#window.hide()
188
- const positionHandler = () => {
189
- const pos = this.#positionHandler(this.#parent.getBounds(), this.#window.getBounds())
190
- 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)
191
262
  }
192
263
 
193
- this.#parent.prependListener('show', showCascade)
194
- this.#parent.prependListener('hide', hideCascade)
195
- this.#parent.prependListener('resize', positionHandler)
196
- 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
+ }
197
271
 
198
272
  this.#window.once('close', () => {
199
- this.#parent.off('show', showCascade)
200
- this.#parent.off('hide', hideCascade)
201
- this.#parent.off('resize', positionHandler)
202
- 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
+ }
203
279
  this.#window = null
204
280
  this.stopFind()
205
281
  })
206
282
 
283
+ this.#window.prependOnceListener('ready-to-show', () => { this.#window.show() })
284
+
207
285
  this.#findableContents.prependOnceListener('destroyed', () => { this.close() })
208
286
  this.#findableContents.prependListener('found-in-page', (_e, result) => { this.#sendMatchesCount(result.activeMatchOrdinal, result.matches) })
209
287
  }
@@ -259,6 +337,7 @@ class Findbar {
259
337
  options.movable = options.movable ?? false
260
338
  options.acceptFirstMouse = options.acceptFirstMouse ?? true
261
339
  options.parent = parent
340
+ options.show = false
262
341
  options.frame = false
263
342
  options.roundedCorners = true
264
343
  options.transparent = process.platform === 'linux'
@@ -266,19 +345,60 @@ class Findbar {
266
345
  options.minimizable = false
267
346
  options.skipTaskbar = true
268
347
  options.fullscreenable = false
348
+ options.autoHideMenuBar = true
269
349
  if (!options.webPreferences) { options.webPreferences = {} }
270
- options.webPreferences.nodeIntegration = true
271
- options.webPreferences.contextIsolation = false
350
+ options.webPreferences.nodeIntegration = false
351
+ options.webPreferences.contextIsolation = true
352
+ options.webPreferences.preload = options.webPreferences.preload ?? `${__dirname}/web/preload.js`
272
353
  return options
273
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
+ }
274
391
  }
275
392
 
393
+ const isFindable = (obj) => obj && typeof obj.findInPage === 'function' && typeof obj.stopFindInPage === 'function';
394
+
276
395
  /**
277
396
  * Define IPC events.
278
397
  */
279
398
  (ipc => {
280
- ipc.handle('electron-findbar/last-text', e => e.sender._findbar.getLastText())
399
+ ipc.handle('electron-findbar/last-state', e => e.sender._findbar.getLastState())
281
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))
282
402
  ipc.on('electron-findbar/previous', e => e.sender._findbar.findPrevious())
283
403
  ipc.on('electron-findbar/next', e => e.sender._findbar.findNext())
284
404
  ipc.on('electron-findbar/open', e => e.sender._findbar.open())
package/package.json CHANGED
@@ -1,8 +1,14 @@
1
1
  {
2
2
  "name": "electron-findbar",
3
- "version": "1.0.1",
3
+ "version": "2.0.0",
4
4
  "description": "Chrome-like findbar for your Electron app.",
5
5
  "main": "index.js",
6
+ "files": [
7
+ "index.js",
8
+ "remote.js",
9
+ "LICENSE",
10
+ "web/**"
11
+ ],
6
12
  "scripts": {
7
13
  "sample": "electron test/sample.js"
8
14
  },
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,14 +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
65
  }
55
66
 
56
- nav > *:not(:last-child),
57
- .btn-group > *:not(:last-child) {
58
- margin-right: var(--spacing);
67
+ nav, .btn-group {
68
+ gap: var(--spacing);
59
69
  }
60
70
 
61
71
  span, input {
@@ -64,6 +74,7 @@ span, input {
64
74
  }
65
75
 
66
76
  span {
77
+ color: var(--input-color);
67
78
  user-select: none;
68
79
  }
69
80
 
@@ -74,7 +85,6 @@ input {
74
85
  font-weight: 500;
75
86
  border: none;
76
87
  outline: none;
77
- -webkit-app-region: no-drag;
78
88
  }
79
89
 
80
90
  .divider {
@@ -87,26 +97,45 @@ input {
87
97
  display: flex;
88
98
  }
89
99
 
90
- .btn-group > div {
100
+ button {
101
+ background-color: transparent;
102
+ border: none;
91
103
  border-radius: 50%;
92
104
  cursor: default;
93
105
  width: 26px;
94
106
  height: 26px;
95
107
  padding: 3px;
96
108
  text-align: center;
97
- transition: .1s linear all;
98
- -webkit-app-region: no-drag;
109
+ transition: .2s linear all;
99
110
  }
100
111
 
101
- .btn-group > div:hover {
112
+ button:hover {
102
113
  background-color: var(--btn-hover-color);
103
114
  }
104
115
 
105
- .btn-group > div:active {
116
+ button:active {
106
117
  background-color: var(--btn-active-color);
107
118
  }
108
119
 
109
- .btn-group > .disabled {
120
+ button:focus {
121
+ outline: none;
122
+ }
123
+
124
+ button.disabled {
110
125
  opacity: .4;
126
+ }
127
+
128
+ #previous.disabled, #next.disabled {
111
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
+
112
141
  }
package/web/findbar.html CHANGED
@@ -4,34 +4,40 @@
4
4
  <head>
5
5
  <meta charset="utf-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1">
7
- <title>Findbar</title>
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>Find in page</title>
8
9
  <link rel="stylesheet" href="app.css">
9
- <body>
10
+ <body class="movable">
10
11
  <nav>
11
- <input id='input' oninput="inputChange(event)" type="text" spellcheck="false">
12
+ <input id="input" type="text" spellcheck="false">
12
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>
13
20
  <div class="divider"></div>
14
21
  <div class="btn-group">
15
- <div onclick="move(false)" class="disabled">
22
+
23
+ <button id="previous" class="disabled" title="Previous">
16
24
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
17
25
  <path d="M7 15L12 10L17 15"/>
18
26
  </svg>
19
- </div>
20
- <div onclick="move(true)" class="disabled">
27
+ </button>
28
+ <button id="next" class="disabled" title="Next">
21
29
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
22
30
  <path d="M7 10L12 15L17 10"/>
23
31
  </svg>
24
- </div>
25
- <div onclick="$remote.close()">
32
+ </button>
33
+ <button id="close" title="Close">
26
34
  <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
27
35
  <path d="M7 17.5L12 12.5L17 17.5"/>
28
36
  <path d="M7 7.5L12 12.5L17 7.5"/>
29
37
  </svg>
30
- </div>
38
+ </button>
31
39
  </div>
32
40
  </nav>
33
-
34
- <script src="app.js"></script>
35
41
  </body>
36
42
 
37
43
  </html>
package/web/preload.js ADDED
@@ -0,0 +1,82 @@
1
+ const $remote = (ipc => ({
2
+ getLastState: async () => ipc.invoke('electron-findbar/last-state'),
3
+ inputChange: (value) => { ipc.send('electron-findbar/input-change', value, true) },
4
+ matchCase: (value) => { ipc.send('electron-findbar/match-case', value, true) },
5
+ previous: () => { ipc.send('electron-findbar/previous') },
6
+ next: () => { ipc.send('electron-findbar/next') },
7
+ close: () => { ipc.send('electron-findbar/close') },
8
+ onMatchesChange: (listener) => { ipc.on('electron-findbar/matches', listener) },
9
+ onInputFocus: (listener) => { ipc.on('electron-findbar/input-focus', listener) },
10
+ onTextChange: (listener) => { ipc.on('electron-findbar/text-change', listener) },
11
+ onMatchCaseChange: (listener) => { ipc.on('electron-findbar/match-case-change', listener) }
12
+ })) (require('electron').ipcRenderer)
13
+
14
+ let canRequest = true, canMove = false
15
+
16
+ function inputChange(e) {
17
+ canRequest = false
18
+ $remote.inputChange(e.target.value)
19
+ }
20
+
21
+ function matchCaseChange(btn) {
22
+ const newStatus = buttonIsDisabled(btn)
23
+ toggleButton(btn, newStatus)
24
+ $remote.matchCase(newStatus)
25
+ }
26
+
27
+ function move(next) {
28
+ if (canRequest && canMove) {
29
+ canRequest = false
30
+ next ? $remote.next() : $remote.previous()
31
+ }
32
+ }
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
+
42
+ document.addEventListener('DOMContentLoaded', async () => {
43
+ const inputEl = document.getElementById('input')
44
+ const matchCaseBtn = document.getElementById('match-case')
45
+ const previousBtn = document.getElementById('previous')
46
+ const nextBtn = document.getElementById('next')
47
+ const closeBtn = document.getElementById('close')
48
+ const matchesEl = document.getElementById('matches')
49
+ const moveBtns = [previousBtn, nextBtn]
50
+
51
+ matchCaseBtn.addEventListener('click', () => matchCaseChange(matchCaseBtn))
52
+ previousBtn.addEventListener('click', () => move(false))
53
+ nextBtn.addEventListener('click', () => move(true))
54
+ closeBtn.addEventListener('click', () => $remote.close())
55
+ inputEl.addEventListener('input', inputChange)
56
+
57
+ $remote.onTextChange((_, text) => { inputEl.value = text })
58
+
59
+ $remote.onInputFocus(() => {
60
+ inputEl.setSelectionRange(0, inputEl.value.length)
61
+ inputEl.focus()
62
+ })
63
+
64
+ $remote.onMatchCaseChange((_, status) => { console.log('Match case:', status);toggleButton(matchCaseBtn, status) })
65
+
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)
79
+ $remote.inputChange(inputEl.value)
80
+ inputEl.setSelectionRange(0, inputEl.value.length)
81
+ inputEl.focus()
82
+ })
@@ -1,17 +0,0 @@
1
- {
2
- "version": "0.2.0",
3
- "configurations": [
4
- {
5
- "name": "Debug Main Process",
6
- "type": "node",
7
- "request": "launch",
8
- "cwd": "${workspaceFolder}",
9
- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
10
- "windows": {
11
- "runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
12
- },
13
- "args" : ["test/sample.js"],
14
- "outputCapture": "std"
15
- }
16
- ]
17
- }
package/test/sample.html DELETED
@@ -1,28 +0,0 @@
1
- <!DOCTYPE html>
2
- <html>
3
- <head>
4
- <title>Sample</title>
5
- </head>
6
- <body>
7
- <input type="text" oninput="$remote.inputChange(event.target.value)">
8
- <button onclick="$remote.open()">Open</button>
9
- <button onclick="$remote.previous()">Previous</button>
10
- <button onclick="$remote.next()">Next</button>
11
- <button onclick="$remote.close()">Close</button>
12
- <br>
13
- <span>
14
- Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec volutpat massa et suscipit tincidunt. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam dictum massa id sapien tristique, in venenatis neque sollicitudin. Fusce accumsan augue arcu, sed rhoncus libero pretium vitae. Phasellus sed imperdiet ante. Maecenas ultrices, elit vitae aliquet tincidunt, elit enim maximus libero, id lacinia ex enim consequat justo. Sed sodales tristique augue sed maximus. Integer tincidunt mi ac arcu tempus, sed accumsan lorem bibendum. Sed in dictum nibh. Pellentesque posuere dui pulvinar sodales scelerisque. Ut nisi magna, vulputate ornare bibendum pharetra, accumsan sed tortor. Proin rhoncus interdum tincidunt. Suspendisse diam eros, ultrices eu volutpat in, vestibulum quis nunc.
15
-
16
- Sed sit amet dapibus eros. Quisque porttitor mi a nisl pretium molestie. Praesent tellus dui, vehicula a ex sed, faucibus congue mauris. Cras dictum, sapien tempus consequat luctus, dolor magna vestibulum risus, nec blandit diam nulla eget est. Curabitur vitae posuere dolor, accumsan vulputate felis. Cras eget iaculis ante. Nulla velit felis, aliquet vitae convallis nec, vehicula et nunc. Fusce dapibus vel eros non viverra. Fusce elit arcu, tempus eu enim in, rutrum mattis justo.
17
-
18
- Cras justo tellus, imperdiet et felis sed, iaculis varius lacus. Pellentesque posuere feugiat nisl, eu vulputate tortor. Proin volutpat tortor erat, feugiat pretium justo aliquam non. Maecenas nec neque ultricies diam rhoncus ullamcorper a vitae mauris. Integer ultricies euismod leo, nec facilisis diam volutpat et. Nulla eleifend ante egestas, imperdiet elit at, malesuada velit. Donec tincidunt eleifend libero. Integer congue pharetra scelerisque. In egestas lacus erat. Quisque aliquam massa lectus, eu semper massa ornare auctor. Cras sit amet auctor sem. Curabitur vitae tellus eu risus ultrices accumsan. Curabitur egestas eu lorem et efficitur. Sed nec turpis felis.
19
-
20
- Suspendisse vel euismod ante. Nunc sagittis quam ut gravida pulvinar. Sed at semper nisl, eu porttitor ante. Cras vitae dolor massa. Fusce tincidunt turpis at egestas pharetra. Donec vitae vestibulum ante. Cras erat dolor, finibus vitae auctor vel, varius dictum arcu. Nam porta arcu consectetur, posuere tellus at, laoreet metus. Ut faucibus tincidunt mi placerat fermentum.
21
-
22
- Aliquam ut pellentesque tellus, quis vulputate nisl. Phasellus vitae blandit nunc, eu sodales velit. Duis enim tellus, faucibus id arcu vitae, consectetur tempus mauris. Praesent commodo commodo dolor non malesuada. Donec sed dolor eget arcu tincidunt efficitur sit amet vel nisi. Sed consectetur tincidunt molestie. Nam at magna a odio rhoncus convallis id eu nunc. Nam luctus ut leo et viverra. Proin tempor libero vitae arcu laoreet, sed pharetra sapien rutrum. Nam nunc orci, aliquet sit amet dignissim nec, aliquet quis quam. Nulla facilisi.
23
- </span>
24
- <script>
25
- const $remote = require('../remote')
26
- </script>
27
- </body>
28
- </html>
package/test/sample.js DELETED
@@ -1,43 +0,0 @@
1
- const { BrowserWindow, app, Menu, MenuItem } = require('electron')
2
- const Findbar = require('../index')
3
-
4
- app.whenReady().then(() => {
5
- const window = setupWindow()
6
- const findbar = setupFindbar(window)
7
- setupApplicationMenu(findbar)
8
- })
9
-
10
- function setupWindow() {
11
- const window = new BrowserWindow({
12
- webPreferences: {
13
- nodeIntegration: true,
14
- contextIsolation: false
15
- }
16
- })
17
- window.loadFile(`${__dirname}/sample.html`)
18
- return window
19
- }
20
-
21
- function setupFindbar(window) {
22
- const findbar = new Findbar(window)
23
- findbar.setWindowOptions({ movable: true, resizable: true })
24
- findbar.setWindowHandler(win => { /* handle the findbar window */ })
25
- return findbar
26
- }
27
-
28
- function setupApplicationMenu(findbar) {
29
- const appMenu = Menu.getApplicationMenu()
30
- appMenu.append(new MenuItem({ label: 'Findbar', submenu: [
31
- { label: 'Open', click: () => findbar.open(), accelerator: 'CommandOrControl+F' },
32
- { label: 'Close', click: () => findbar.isOpen() && findbar.close(), accelerator: 'Esc' },
33
- { role: 'toggleDevTools', accelerator: 'CommandOrControl+Shift+I' },
34
- { label: 'Test input propagation', click: () => {
35
- let count = 0
36
- setInterval(() => {
37
- findbar.startFind('count: ' + count++)
38
- findbar.startFind('cannot show this', true)
39
- }, 1000)
40
- }}
41
- ]}))
42
- Menu.setApplicationMenu(appMenu)
43
- }
package/web/app.js DELETED
@@ -1,53 +0,0 @@
1
- const $remote = (ipc => ({
2
- getLastText: async () => ipc.invoke('electron-findbar/last-text'),
3
- inputChange: (value) => { ipc.send('electron-findbar/input-change', value, true) },
4
- previous: () => { ipc.send('electron-findbar/previous') },
5
- next: () => { ipc.send('electron-findbar/next') },
6
- close: () => { ipc.send('electron-findbar/close') },
7
- onMatchesChange: (listener) => { ipc.on('electron-findbar/matches', listener) },
8
- onInputFocus: (listener) => { ipc.on('electron-findbar/input-focus', listener) },
9
- onTextChange: (listener) => { ipc.on('electron-findbar/text-change', listener) }
10
- })) (require('electron').ipcRenderer)
11
-
12
- let canRequest = true, canMove = false
13
-
14
- function inputChange(e) {
15
- canRequest = false
16
- $remote.inputChange(e.target.value)
17
- }
18
-
19
- function move(next) {
20
- if (canRequest && canMove) {
21
- canRequest = false
22
- next ? $remote.next() : $remote.previous()
23
- }
24
- }
25
-
26
- document.addEventListener('DOMContentLoaded', async () => {
27
- const inputEl = document.getElementById('input')
28
- const matchesEl = document.getElementById('matches')
29
- const moveBtns = [...document.getElementsByClassName('disabled')]
30
-
31
- $remote.onMatchesChange((_, m) => {
32
- canRequest = true
33
- matchesEl.innerText = inputEl.value ? m.active + '/' + m.total : ''
34
-
35
- for (var moveBtn of moveBtns) {
36
- (canMove = m.total > 1) ?
37
- moveBtn.classList.remove('disabled') :
38
- moveBtn.classList.add('disabled')
39
- }
40
- })
41
-
42
- $remote.onInputFocus(() => {
43
- inputEl.setSelectionRange(0, inputEl.value.length)
44
- inputEl.focus()
45
- })
46
-
47
- $remote.onTextChange((_, text) => { inputEl.value = text })
48
-
49
- inputEl.value = await $remote.getLastText()
50
- $remote.inputChange(inputEl.value)
51
- inputEl.setSelectionRange(0, inputEl.value.length)
52
- inputEl.focus()
53
- })