electron-findbar 3.1.1 → 3.3.2

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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Emerson Capuchi Romaneli
3
+ Copyright (c) 2026 Emerson Capuchi Romaneli
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -19,6 +19,23 @@ Install the `electron-findbar` package via [npm](https://www.npmjs.com/package/e
19
19
  npm install electron-findbar
20
20
  ```
21
21
 
22
+ ## Development & Build
23
+
24
+ This project is written in **TypeScript** and uses **Webpack** for bundling and minification.
25
+
26
+ ### Prerequisites
27
+
28
+ - Node.js >= 12.0.0
29
+ - npm
30
+
31
+ ### Setup
32
+
33
+ Install dependencies:
34
+
35
+ ```sh
36
+ npm install
37
+ ```
38
+
22
39
  ## Overview
23
40
 
24
41
  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.
@@ -62,7 +79,7 @@ It is also possible to create a findbar providing only the web contents. The Bas
62
79
  const findbar = Findbar.from(webContents)
63
80
  ```
64
81
 
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.
82
+ **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.
66
83
 
67
84
  #### Retrieve if exists
68
85
 
@@ -241,7 +258,7 @@ Once open, the Findbar appears by default in the top-right corner of the parent
241
258
  ```js
242
259
  /**
243
260
  * Get the last state of the findbar.
244
- * @returns {{ text: string, matchCase: boolean, movable: boolean }} Last state of the findbar.
261
+ * @returns {{ text: string, matchCase: boolean, movable: boolean, theme: 'light' | 'dark' | 'system' }} Last state of the findbar.
245
262
  */
246
263
  getLastState()
247
264
 
@@ -292,6 +309,36 @@ isFocused()
292
309
  * @returns {boolean} True if the findbar is visible, otherwise false.
293
310
  */
294
311
  isVisible()
312
+
313
+ /**
314
+ * Get the current theme of this findbar instance.
315
+ * @returns {'light' | 'dark' | 'system'} The current theme setting.
316
+ */
317
+ getTheme()
318
+
319
+ /**
320
+ * Update the theme of the findbar. Only affects the current instance.
321
+ * @param {'light' | 'dark' | 'system'} theme - The theme to set. If not provided, uses the default theme.
322
+ */
323
+ updateTheme(theme)
324
+
325
+ /**
326
+ * Set whether the findbar will follow the parent window visibility events. Default is true.
327
+ * If false, the findbar will not hide with the parent window automatically.
328
+ */
329
+ followVisibilityEvents(shouldFollow: boolean = true)
330
+
331
+ /**
332
+ * Get the default theme for new findbar instances.
333
+ * @returns {'light' | 'dark' | 'system'} The default theme setting.
334
+ */
335
+ static getDefaultTheme()
336
+
337
+ /**
338
+ * Set the default theme for new findbar instances.
339
+ * @param {'light' | 'dark' | 'system'} theme - The theme to set as default.
340
+ */
341
+ static setDefaultTheme(theme)
295
342
  ```
296
343
 
297
344
  ## IPC Events
@@ -305,13 +352,17 @@ If the `contextIsolation` is enabled, the `electron-findbar/remote` will not be
305
352
  ```js
306
353
  const $remote = (ipc => ({
307
354
  getLastState: async () => ipc.invoke('electron-findbar/last-state'),
308
- inputChange: (text) => { ipc.send('electron-findbar/input-change', text) },
309
- matchCase: (value) => { ipc.send('electron-findbar/match-case', value) },
355
+ inputChange: (value: string) => { ipc.send('electron-findbar/input-change', value, true) },
356
+ matchCase: (value: boolean) => { ipc.send('electron-findbar/match-case', value, true) },
310
357
  previous: () => { ipc.send('electron-findbar/previous') },
311
358
  next: () => { ipc.send('electron-findbar/next') },
312
- open: () => { ipc.send('electron-findbar/open') },
313
- close: () => { ipc.send('electron-findbar/close') }
314
- })) (require('electron').ipcRenderer)
359
+ close: () => { ipc.send('electron-findbar/close') },
360
+ onMatchesChange: (listener: Function) => { ipc.on('electron-findbar/matches', listener) },
361
+ onInputFocus: (listener: Function) => { ipc.on('electron-findbar/input-focus', listener) },
362
+ onTextChange: (listener: Function) => { ipc.on('electron-findbar/text-change', listener) },
363
+ onMatchCaseChange: (listener: Function) => { ipc.on('electron-findbar/match-case-change', listener) },
364
+ onForceTheme: (listener: Function) => { ipc.on('electron-findbar/force-theme', listener) },
365
+ })) (require('electron').ipcRenderer);
315
366
 
316
367
  $remote.open()
317
368
  $remote.inputChange('findIt')
@@ -338,7 +389,7 @@ The `updateParentWindow` method allows you to change the parent window while pre
338
389
 
339
390
  ```javascript
340
391
  // Create a findbar for the initial window
341
- const findbar = Findbar.from(oldWindow, webContents)
392
+ const findbar = Findbar.from([oldWindow, ]webContents)
342
393
 
343
394
  // Later, when you need to change the parent:
344
395
  findbar.updateParentWindow(newWindow)
@@ -0,0 +1 @@
1
+ <!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><meta http-equiv="Content-Security-Policy" content="script-src 'self'; style-src 'self' 'sha256-xegrQ38nadcaxMgXTqofPqPhLTW4XM5uk6u7cigFZb4='; default-src 'self'; connect-src 'none'; frame-src 'none'; object-src 'none'; font-src 'self'; img-src 'self'"><title>Find in page</title><style>*,:after,:before{box-sizing:border-box}body{margin:0;padding:0;font-weight:400;height:100vh;--bg-color:#fff;--border:#ddd;--color:#626262;--input-color:#111;--btn-hover-color:#ccc;--btn-active-color:#bbb;--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";--font-size:.75rem;--spacing:.5rem}body.dark{--bg-color:#1f1f1f;--border:#3f3f3f;--color:#a9a9aa;--input-color:#eee;--btn-hover-color:#333;--btn-active-color:#444}@media (prefers-color-scheme:dark){body:not(.light){--bg-color:#1f1f1f;--border:#3f3f3f;--color:#a9a9aa;--input-color:#eee;--btn-hover-color:#333;--btn-active-color:#444}}#match-case{fill:var(--color);user-select:none}#match-case>svg{width:20px}#close,#next,#previous{fill:none;stroke:var(--color);stroke-width:2;stroke-linecap:round;stroke-linejoin:round}nav{display:flex;align-items:center;width:100%;height:100%;padding:var(--spacing);background-color:var(--bg-color);color:var(--color)}.linux nav{border-radius:10px;border:1px solid var(--border)}.btn-group,nav{gap:var(--spacing)}input,span{font-family:var(--font-family);font-size:var(--font-size)}span{color:var(--input-color);user-select:none}input{width:100%;background-color:transparent;color:var(--input-color);font-weight:500;border:none;outline:0}.divider{width:2px;height:100%;background-color:var(--btn-hover-color)}.btn-group{display:flex}button{background-color:transparent;border:none;border-radius:50%;cursor:default;width:26px;height:26px;padding:3px;text-align:center;transition:.2s linear all}button:hover{background-color:var(--btn-hover-color)}button:active{background-color:var(--btn-active-color)}button:focus{outline:0}button.disabled{opacity:.4}#next.disabled,#previous.disabled{background-color:transparent!important}.movable nav{-webkit-app-region:drag;app-region:drag}button,input{-webkit-app-region:no-drag;app-region:no-drag}</style></head><body class="movable"><nav><input id="input" spellcheck="false"> <span id="matches"></span> <button id="match-case" class="disabled" title="Match case"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg" stroke="none"><text x="4" y="15" font-size="14" font-family="monospace" font-weight="bold">A</text><text x="12" y="19" font-size="14" font-family="monospace">a</text></svg></button><div class="divider"></div><div class="btn-group"><button id="previous" class="disabled" title="Previous"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 15L12 10L17 15"/></svg></button> <button id="next" class="disabled" title="Next"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 10L12 15L17 10"/></svg></button> <button id="close" title="Close"><svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path d="M7 17.5L12 12.5L17 17.5"/><path d="M7 7.5L12 12.5L17 7.5"/></svg></button></div></nav></body></html>
package/dist/main.d.ts ADDED
@@ -0,0 +1,150 @@
1
+ import { BaseWindow, BrowserWindow, WebContents, BrowserWindowConstructorOptions, Rectangle } from 'electron';
2
+ interface LastState {
3
+ text: string;
4
+ matchCase: boolean;
5
+ movable: boolean;
6
+ theme: 'light' | 'dark' | 'system';
7
+ }
8
+ /**
9
+ * Chrome-like findbar for Electron applications.
10
+ */
11
+ declare class Findbar {
12
+ private static defaultTheme;
13
+ private static defaultWindowHandler?;
14
+ private static defaultBoundsHandler;
15
+ private parent?;
16
+ private window?;
17
+ private findableContents;
18
+ private followVisibilityEventsFlag;
19
+ private matches;
20
+ private windowHandler;
21
+ private boundsHandler;
22
+ private customOptions?;
23
+ private lastText;
24
+ private theme;
25
+ private matchCaseFlag;
26
+ private isMovableFlag;
27
+ private fixMove?;
28
+ /**
29
+ * Configure the findbar and link to the web contents.
30
+ *
31
+ * @param parent - Parent window or web contents
32
+ * @param webContents - Custom findable web contents (optional)
33
+ */
34
+ constructor(parent: BaseWindow | BrowserWindow | WebContents, webContents?: WebContents);
35
+ /**
36
+ * Open the findbar. If the findbar is already opened, focus the input text.
37
+ */
38
+ open(): void;
39
+ /**
40
+ * Close the findbar.
41
+ */
42
+ close(): void;
43
+ /**
44
+ * Detach the findbar from the web contents and close it if opened.
45
+ */
46
+ detach(): void;
47
+ /**
48
+ * Update the parent window of the findbar.
49
+ */
50
+ updateParentWindow(newParent?: BaseWindow): void;
51
+ /**
52
+ * Get the last state of the findbar.
53
+ */
54
+ getLastState(): LastState;
55
+ /**
56
+ * Starts a request to find all matches for the text in the page.
57
+ */
58
+ startFind(text: string, skipRendererEvent?: boolean): void;
59
+ /**
60
+ * Whether the search should be case-sensitive.
61
+ */
62
+ matchCase(status: boolean, skipRendererEvent?: boolean): void;
63
+ /**
64
+ * Select previous match if any.
65
+ */
66
+ findPrevious(): void;
67
+ /**
68
+ * Select next match if any.
69
+ */
70
+ findNext(): void;
71
+ /**
72
+ * Stops the find request and clears selection.
73
+ */
74
+ stopFind(): void;
75
+ /**
76
+ * Whether the findbar is opened.
77
+ */
78
+ isOpen(): boolean;
79
+ /**
80
+ * Whether the findbar is focused.
81
+ */
82
+ isFocused(): boolean;
83
+ /**
84
+ * Whether the findbar is visible to the user.
85
+ */
86
+ isVisible(): boolean;
87
+ /**
88
+ * Set custom options for the findbar window.
89
+ */
90
+ setWindowOptions(customOptions: BrowserWindowConstructorOptions): void;
91
+ /**
92
+ * Set a window handler for the findbar window.
93
+ */
94
+ setWindowHandler(windowHandler: (findbarWindow: BrowserWindow) => void): void;
95
+ /**
96
+ * Set a bounds handler to calculate findbar bounds.
97
+ */
98
+ setBoundsHandler(boundsHandler: (parentBounds: Rectangle, findbarBounds: Rectangle) => Rectangle): void;
99
+ /**
100
+ * Set whether the findbar will follow the parent window "show" and "hide" events. Default is true.
101
+ * If false, the findbar will not hide automatically with the parent window.
102
+ */
103
+ followVisibilityEvents(shouldFollow: boolean): void;
104
+ /**
105
+ * Get the current theme of this findbar instance.
106
+ * @returns The current theme setting ('light', 'dark', or 'system').
107
+ */
108
+ getTheme(): 'light' | 'dark' | 'system';
109
+ /**
110
+ * Update the theme of the findbar. Only affects the current instance.
111
+ * @param theme - The theme to set. If not provided, uses the default theme.
112
+ */
113
+ updateTheme(theme?: 'light' | 'dark' | 'system'): void;
114
+ /**
115
+ * Get the default theme.
116
+ */
117
+ static getDefaultTheme(): 'light' | 'dark' | 'system';
118
+ /**
119
+ * Set the default theme for new findbar instances.
120
+ */
121
+ static setDefaultTheme(theme: 'light' | 'dark' | 'system'): void;
122
+ /**
123
+ * Set the default window handler for new findbar instances.
124
+ */
125
+ static setDefaultWindowHandler(windowHandler: (findbarWindow: BrowserWindow) => void): void;
126
+ /**
127
+ * Set the default bounds handler for new findbar instances.
128
+ */
129
+ static setDefaultBoundsHandler(boundsHandler: (parentBounds: Rectangle, findbarBounds: Rectangle) => Rectangle): void;
130
+ private registerKeyboardShortcuts;
131
+ private removeParent;
132
+ private findInContent;
133
+ private stopFindInContent;
134
+ private registerListeners;
135
+ private sendMatchesCount;
136
+ private focusWindowAndHighlightInput;
137
+ private static retrieveWebContents;
138
+ private static getBaseWindowFromWebContents;
139
+ private static setDefaultPosition;
140
+ private static mergeStandardOptions;
141
+ /**
142
+ * Get the findbar instance for a given BrowserWindow or WebContents.
143
+ */
144
+ static from(windowOrWebContents: BaseWindow | BrowserWindow | WebContents, customWebContents?: WebContents): Findbar;
145
+ /**
146
+ * Get the findbar instance for a given BrowserWindow or WebContents if it exists.
147
+ */
148
+ static fromIfExists(windowOrWebContents: BaseWindow | BrowserWindow | WebContents): Findbar | undefined;
149
+ }
150
+ export = Findbar;
package/dist/main.js ADDED
@@ -0,0 +1 @@
1
+ (()=>{"use strict";var e={288(e){e.exports=require("electron")},927(e,t,i){const n=i(288);class s{constructor(e,t){var i;if(this.followVisibilityEventsFlag=!0,this.matches={active:0,total:0},this.windowHandler=s.defaultWindowHandler,this.boundsHandler=s.defaultBoundsHandler,this.lastText="",this.theme=s.defaultTheme,this.matchCaseFlag=!1,this.isMovableFlag=!1,o(e)?(this.findableContents=e,e=s.getBaseWindowFromWebContents(this.findableContents)):this.findableContents=null!==(i=t)&&void 0!==i?i:s.retrieveWebContents(e),!this.findableContents)throw new Error("There are no searchable web contents.");this.findableContents._findbar=this,this.findableContents.once("destroyed",()=>{this.detach()}),this.updateParentWindow(e)}open(){var e,t;if(this.window)return void this.focusWindowAndHighlightInput();this.parent||this.updateParentWindow();const i=s.mergeStandardOptions(this.customOptions,this.parent);this.isMovableFlag=null!==(e=i.movable)&&void 0!==e&&e,this.window=new n.BrowserWindow(i),this.window.webContents._findbar=this,this.registerListeners(),null===(t=this.windowHandler)||void 0===t||t.call(this,this.window),this.window.loadFile(`${__dirname}/index.html`)}close(){this.window&&!this.window.isDestroyed()&&this.window.close()}detach(){this.close(),this.findableContents._findbar=void 0,this.window&&(this.window.webContents._findbar=void 0)}updateParentWindow(e){var t;this.parent!==e&&(this.close(),this.parent=null!==(t=e)&&void 0!==t?t:s.getBaseWindowFromWebContents(this.findableContents),this.parent&&!this.parent.isDestroyed()&&this.parent.once("closed",()=>{this.removeParent()}))}getLastState(){return{text:this.lastText,matchCase:this.matchCaseFlag,movable:this.isMovableFlag,theme:this.theme}}startFind(e,t){var i;t||null===(i=this.window)||void 0===i||i.webContents.send("electron-findbar/text-change",e),(this.lastText=e)?this.isOpen()&&this.findInContent({findNext:!0}):this.stopFind()}matchCase(e,t){var i;this.matchCaseFlag!==e&&(this.matchCaseFlag=e,t||null===(i=this.window)||void 0===i||i.webContents.send("electron-findbar/match-case-change",this.matchCaseFlag),this.stopFindInContent(),this.startFind(this.lastText,t))}findPrevious(){this.matches.total<2||(1===this.matches.active&&(this.fixMove=!1),this.isOpen()&&this.findInContent({forward:!1}))}findNext(){this.matches.total<2||(this.matches.active===this.matches.total&&(this.fixMove=!0),this.isOpen()&&this.findInContent({forward:!0}))}stopFind(){this.isOpen()&&this.sendMatchesCount(0,0),this.findableContents.isDestroyed()||this.stopFindInContent()}isOpen(){return!!this.window}isFocused(){var e;return!!(null===(e=this.window)||void 0===e?void 0:e.isFocused())}isVisible(){var e;return!!(null===(e=this.window)||void 0===e?void 0:e.isVisible())}setWindowOptions(e){this.customOptions=e}setWindowHandler(e){this.windowHandler=e}setBoundsHandler(e){this.boundsHandler=e}followVisibilityEvents(e){this.followVisibilityEventsFlag=e}getTheme(){return this.theme}updateTheme(e=s.defaultTheme){this.theme=e,this.window&&!this.window.isDestroyed()&&this.window.webContents.send("electron-findbar/force-theme",e)}static getDefaultTheme(){return s.defaultTheme}static setDefaultTheme(e){s.defaultTheme=e}static setDefaultWindowHandler(e){s.defaultWindowHandler=e}static setDefaultBoundsHandler(e){s.defaultBoundsHandler=e}registerKeyboardShortcuts(e,t){if(t.meta||t.control||t.alt)return;const i=t.key.toLowerCase();if(!t.shift)return"enter"===i?(this.findNext(),void e.preventDefault()):void("escape"===i&&this.isOpen()&&(this.close(),e.preventDefault()));"enter"===i&&(this.findPrevious(),e.preventDefault())}removeParent(){this.close(),this.parent=void 0}findInContent(e){e.matchCase=this.matchCaseFlag,this.findableContents.findInPage(this.lastText,e)}stopFindInContent(){this.findableContents.stopFindInPage("clearSelection")}registerListeners(){const e=()=>{this.window.isVisible()||this.window.show()},t=()=>{this.window.isVisible()&&this.window.hide()},i=()=>{const e=this.window.getBounds(),t=this.boundsHandler(this.parent.getBounds(),e);t.width||(t.width=e.width),t.height||(t.height=e.height),this.window.setBounds(t,!1)};this.parent&&!this.parent.isDestroyed()&&(i(),this.followVisibilityEventsFlag&&(this.parent.prependListener("show",e),this.parent.prependListener("hide",t)),this.parent.prependListener("resize",i),this.parent.prependListener("move",i)),this.window.once("closed",()=>{this.parent&&!this.parent.isDestroyed()&&(this.followVisibilityEventsFlag&&(this.parent.off("show",e),this.parent.off("hide",t)),this.parent.off("resize",i),this.parent.off("move",i)),this.window=void 0,this.stopFind()}),this.window.prependOnceListener("ready-to-show",()=>{this.window.show()}),this.window.webContents.prependListener("before-input-event",(e,t)=>{this.registerKeyboardShortcuts(e,t)}),this.findableContents.prependOnceListener("destroyed",()=>{this.close()}),this.findableContents.prependListener("found-in-page",(e,t)=>{this.sendMatchesCount(t.activeMatchOrdinal,t.matches)})}sendMatchesCount(e,t){void 0!==this.fixMove&&(this.fixMove?this.findNext():this.findPrevious(),this.fixMove=void 0),this.matches.active=e,this.matches.total=t,this.window.webContents.send("electron-findbar/matches",this.matches)}focusWindowAndHighlightInput(){this.window.focus(),this.window.webContents.send("electron-findbar/input-focus")}static retrieveWebContents(e){var t,i,n;return null!==(t=e.webContents)&&void 0!==t?t:null===(n=null===(i=e.contentView)||void 0===i?void 0:i.children[0])||void 0===n?void 0:n.webContents}static getBaseWindowFromWebContents(e){return n.BaseWindow.getAllWindows().find(t=>{var i;return t.webContents===e||(null===(i=t.contentView)||void 0===i?void 0:i.children.some(t=>t.webContents===e))})}static setDefaultPosition(e,t){return{x:e.x+e.width-t.width-20,y:e.y-(t.height/4|0),width:t.width,height:t.height}}static mergeStandardOptions(e,t){var i,n,s,o,r,a;return e||(e={}),e.width=null!==(i=e.width)&&void 0!==i?i:372,e.height=null!==(n=e.height)&&void 0!==n?n:52,e.resizable=null!==(s=e.resizable)&&void 0!==s&&s,e.movable=null!==(o=e.movable)&&void 0!==o&&o,e.acceptFirstMouse=null===(r=e.acceptFirstMouse)||void 0===r||r,e.parent=t,e.show=!1,e.frame=!1,e.roundedCorners=!0,e.transparent="linux"===process.platform,e.maximizable=!1,e.minimizable=!1,e.skipTaskbar=!0,e.fullscreenable=!1,e.autoHideMenuBar=!0,e.webPreferences||(e.webPreferences={}),e.webPreferences.nodeIntegration=!1,e.webPreferences.contextIsolation=!0,e.webPreferences.preload=null!==(a=e.webPreferences.preload)&&void 0!==a?a:`${__dirname}/preload.js`,e}static from(e,t){const i=o(e)?e:null!=t?t:s.retrieveWebContents(e);if(!i)throw new Error("[Findbar] There are no searchable web contents.");return i._findbar||new s(e,t)}static fromIfExists(e){const t=o(e)?e:s.retrieveWebContents(e);if(!t)throw new Error("[Findbar] There are no searchable web contents.");return t._findbar}}s.defaultTheme="system",s.defaultBoundsHandler=s.setDefaultPosition;const o=e=>e&&"function"==typeof e.findInPage&&"function"==typeof e.stopFindInPage;n.ipcMain.handle("electron-findbar/last-state",e=>{var t;return null===(t=s.fromIfExists(e.sender))||void 0===t?void 0:t.getLastState()}),n.ipcMain.on("electron-findbar/input-change",(e,t,i)=>{var n;return null===(n=s.fromIfExists(e.sender))||void 0===n?void 0:n.startFind(t,i)}),n.ipcMain.on("electron-findbar/match-case",(e,t,i)=>{var n;return null===(n=s.fromIfExists(e.sender))||void 0===n?void 0:n.matchCase(t,i)}),n.ipcMain.on("electron-findbar/previous",e=>{var t;return null===(t=s.fromIfExists(e.sender))||void 0===t?void 0:t.findPrevious()}),n.ipcMain.on("electron-findbar/next",e=>{var t;return null===(t=s.fromIfExists(e.sender))||void 0===t?void 0:t.findNext()}),n.ipcMain.on("electron-findbar/open",e=>s.from(e.sender).open()),n.ipcMain.on("electron-findbar/close",e=>{const t=s.fromIfExists(e.sender);t&&(t.stopFind(),t.close())}),e.exports=s}},t={},i=function i(n){var s=t[n];if(void 0!==s)return s.exports;var o=t[n]={exports:{}};return e[n](o,o.exports,i),o.exports}(927);module.exports=i})();
@@ -0,0 +1 @@
1
+ (()=>{"use strict";var e={213(e,n,t){var o=this&&this.__awaiter||function(e,n,t,o){return new(t||(t=Promise))(function(c,a){function s(e){try{r(o.next(e))}catch(e){a(e)}}function i(e){try{r(o.throw(e))}catch(e){a(e)}}function r(e){var n;e.done?c(e.value):(n=e.value,n instanceof t?n:new t(function(e){e(n)})).then(s,i)}r((o=o.apply(e,n||[])).next())})};const c=(a=t(288).ipcRenderer,{getLastState:()=>o(void 0,void 0,void 0,function*(){return a.invoke("electron-findbar/last-state")}),inputChange:e=>{a.send("electron-findbar/input-change",e,!0)},matchCase:e=>{a.send("electron-findbar/match-case",e,!0)},previous:()=>{a.send("electron-findbar/previous")},next:()=>{a.send("electron-findbar/next")},close:()=>{a.send("electron-findbar/close")},onMatchesChange:e=>{a.on("electron-findbar/matches",e)},onInputFocus:e=>{a.on("electron-findbar/input-focus",e)},onTextChange:e=>{a.on("electron-findbar/text-change",e)},onMatchCaseChange:e=>{a.on("electron-findbar/match-case-change",e)},onForceTheme:e=>{a.on("electron-findbar/force-theme",e)}});var a;let s=!0,i=!1;function r(e){const n=e.target;s=!1,c.inputChange(n.value)}function d(e){s&&i&&(s=!1,e?c.next():c.previous())}function l(e,n){e.classList[n?"remove":"add"]("disabled")}function u(e){document.body.classList.remove("light","dark"),"system"!==e&&document.body.classList.add(e)}document.addEventListener("DOMContentLoaded",()=>o(void 0,void 0,void 0,function*(){const e=document.getElementById("input"),n=document.getElementById("match-case"),t=document.getElementById("previous"),o=document.getElementById("next"),a=document.getElementById("close"),v=document.getElementById("matches"),f=[t,o].filter(e=>null!==e);if(!(e&&n&&t&&o&&a&&v))return void console.error("Required elements not found in the DOM");n.addEventListener("click",()=>function(e){const n=function(e){return e.classList.contains("disabled")}(e);l(e,n),c.matchCase(n)}(n)),t.addEventListener("click",()=>d(!1)),o.addEventListener("click",()=>d(!0)),a.addEventListener("click",()=>c.close()),e.addEventListener("input",r),c.onTextChange((n,t)=>{e.value=t}),c.onInputFocus(()=>{e.setSelectionRange(0,e.value.length),e.focus()}),c.onMatchCaseChange((e,t)=>l(n,t)),c.onMatchesChange((n,t)=>{s=!0,v.innerText=e.value?t.active+"/"+t.total:"";for(const e of f)l(e,i=t.total>1)}),c.onForceTheme((e,n)=>{u(n)});const h=yield c.getLastState();e.value=h.text||"",h.movable||document.body.classList.remove("movable"),"linux"===process.platform&&document.body.classList.add("linux"),u(h.theme),l(n,h.matchCase),c.inputChange(e.value),e.setSelectionRange(0,e.value.length),e.focus()}))},288(e){e.exports=require("electron")}},n={};!function t(o){var c=n[o];if(void 0!==c)return c.exports;var a=n[o]={exports:{}};return e[o].call(a.exports,a,a.exports,t),a.exports}(213)})();
@@ -0,0 +1,65 @@
1
+ import { IpcRendererEvent } from 'electron';
2
+ /**
3
+ * Remote IPC events to control the findbar through the renderer.
4
+ */
5
+ interface Remote {
6
+ /**
7
+ * Get last queried text and the "match case" status.
8
+ */
9
+ getLastState(): Promise<{
10
+ text: string;
11
+ matchCase: boolean;
12
+ movable: boolean;
13
+ }>;
14
+ /**
15
+ * Change the input value and find it.
16
+ * @param text - The text to search for
17
+ */
18
+ inputChange(text: string): void;
19
+ /**
20
+ * Toggle case sensitive search
21
+ * @param value - Whether to match case or not
22
+ */
23
+ matchCase(value: boolean): void;
24
+ /**
25
+ * Navigate to the previous match
26
+ */
27
+ previous(): void;
28
+ /**
29
+ * Navigate to the next match
30
+ */
31
+ next(): void;
32
+ /**
33
+ * Open the findbar
34
+ */
35
+ open(): void;
36
+ /**
37
+ * Close the findbar
38
+ */
39
+ close(): void;
40
+ /**
41
+ * Listen for matches change event
42
+ */
43
+ onMatchesChange(listener: (event: IpcRendererEvent, matches: {
44
+ active: number;
45
+ total: number;
46
+ }) => void): void;
47
+ /**
48
+ * Listen for input focus event
49
+ */
50
+ onInputFocus(listener: (event: IpcRendererEvent) => void): void;
51
+ /**
52
+ * Listen for text change event
53
+ */
54
+ onTextChange(listener: (event: IpcRendererEvent, text: string) => void): void;
55
+ /**
56
+ * Listen for match case change event
57
+ */
58
+ onMatchCaseChange(listener: (event: IpcRendererEvent, status: boolean) => void): void;
59
+ /**
60
+ * Listen for force theme change event
61
+ */
62
+ onForceTheme(listener: (event: IpcRendererEvent, theme: 'light' | 'dark' | 'system') => void): void;
63
+ }
64
+ declare const Remote: Remote;
65
+ export = Remote;
package/dist/remote.js ADDED
@@ -0,0 +1 @@
1
+ (()=>{"use strict";var e={288(e){e.exports=require("electron")},684(e,n,r){var t=this&&this.__awaiter||function(e,n,r,t){return new(r||(r=Promise))(function(o,c){function i(e){try{d(t.next(e))}catch(e){c(e)}}function a(e){try{d(t.throw(e))}catch(e){c(e)}}function d(e){var n;e.done?o(e.value):(n=e.value,n instanceof r?n:new r(function(e){e(n)})).then(i,a)}d((t=t.apply(e,n||[])).next())})};const o=r(288),c={getLastState:()=>t(void 0,void 0,void 0,function*(){return o.ipcRenderer.invoke("electron-findbar/last-state")}),inputChange:e=>{o.ipcRenderer.send("electron-findbar/input-change",e)},matchCase:e=>{o.ipcRenderer.send("electron-findbar/match-case",e)},previous:()=>{o.ipcRenderer.send("electron-findbar/previous")},next:()=>{o.ipcRenderer.send("electron-findbar/next")},open:()=>{o.ipcRenderer.send("electron-findbar/open")},close:()=>{o.ipcRenderer.send("electron-findbar/close")},onMatchesChange:e=>{o.ipcRenderer.on("electron-findbar/matches",e)},onInputFocus:e=>{o.ipcRenderer.on("electron-findbar/input-focus",e)},onTextChange:e=>{o.ipcRenderer.on("electron-findbar/text-change",e)},onMatchCaseChange:e=>{o.ipcRenderer.on("electron-findbar/match-case-change",e)},onForceTheme:e=>{o.ipcRenderer.on("electron-findbar/force-theme",e)}};e.exports=c}},n={},r=function r(t){var o=n[t];if(void 0!==o)return o.exports;var c=n[t]={exports:{}};return e[t].call(c.exports,c,c.exports,r),c.exports}(684);module.exports=r})();
package/package.json CHANGED
@@ -1,28 +1,44 @@
1
1
  {
2
2
  "name": "electron-findbar",
3
- "version": "3.1.1",
3
+ "version": "3.3.2",
4
4
  "description": "Chrome-like findbar for your Electron app.",
5
- "main": "index.js",
5
+ "exports": {
6
+ ".": {
7
+ "types": "./dist/main.d.ts",
8
+ "default": "./dist/main.js"
9
+ },
10
+ "./remote": {
11
+ "types": "./dist/remote.d.ts",
12
+ "default": "./dist/remote.js"
13
+ }
14
+ },
6
15
  "files": [
7
- "index.js",
8
- "remote.js",
16
+ "dist/**/*",
9
17
  "LICENSE",
10
- "web/**"
18
+ "!dist/preload.d.ts"
11
19
  ],
12
20
  "scripts": {
13
- "sample": "electron test/sample.js"
21
+ "clean": "rm -rf dist",
22
+ "build": "npm run clean && webpack",
23
+ "build:dev": "npm run clean && NODE_ENV=development webpack",
24
+ "dev": "webpack --mode development --watch",
25
+ "sample": "electron test/sample.js",
26
+ "publish": "npm run build && npm publish --access public"
14
27
  },
15
28
  "repository": {
16
29
  "type": "git",
17
30
  "url": "git+https://github.com/ECRomaneli/electron-findbar.git"
18
31
  },
19
32
  "keywords": [
33
+ "find",
20
34
  "findbar",
21
35
  "find-bar",
22
36
  "find bar",
37
+ "electron-find",
23
38
  "electron-findbar",
24
39
  "electron-find-bar",
25
40
  "electron",
41
+ "chrome-find",
26
42
  "chrome-findbar",
27
43
  "chrome-find-bar",
28
44
  "search",
@@ -38,6 +54,19 @@
38
54
  "node": ">=12.0.0"
39
55
  },
40
56
  "devDependencies": {
41
- "electron": ">=12.0.0"
57
+ "@types/electron": "^1.6.12",
58
+ "@types/node": "^25.1.0",
59
+ "clean-css": "^5.3.3",
60
+ "css-loader": "^7.1.3",
61
+ "cssnano": "^7.1.2",
62
+ "electron": ">=12.0.0",
63
+ "html-webpack-plugin": "^5.6.6",
64
+ "mini-css-extract-plugin": "^2.10.0",
65
+ "postcss-loader": "^8.2.0",
66
+ "style-loader": "^4.0.0",
67
+ "ts-loader": "^9.5.4",
68
+ "typescript": "^5.9.3",
69
+ "webpack": "^5.104.1",
70
+ "webpack-cli": "^6.0.1"
42
71
  }
43
72
  }
package/index.js DELETED
@@ -1,484 +0,0 @@
1
- const { BaseWindow, BrowserWindow, WebContents, BrowserWindowConstructorOptions, Rectangle } = require('electron')
2
-
3
- /**
4
- * Chrome-like findbar for Electron applications.
5
- */
6
- class Findbar {
7
- /** @type {BaseWindow} */
8
- #parent
9
-
10
- /** @type {BrowserWindow} */
11
- #window
12
-
13
- /** @type {WebContents} */
14
- #findableContents
15
-
16
- /** @type { { active: number, total: number } } */
17
- #matches = { active: 0, total: 0 }
18
-
19
- /** @type {(findbarWindow: BrowserWindow) => void} */
20
- #windowHandler
21
-
22
- /** @type {{parentBounds: Rectangle, findbarBounds: Rectangle} => Rectangle} */
23
- #boundsHandler = Findbar.#setDefaultPosition
24
-
25
- /** @type {BrowserWindowConstructorOptions} */
26
- #customOptions
27
-
28
- /** @type {string} */
29
- #lastText = ''
30
-
31
- /** @type {boolean} */
32
- #matchCase = false
33
-
34
- /** @type {boolean} */
35
- #isMovable = false
36
-
37
- /**
38
- * Workaround to fix "findInPage" bug - double-click to loop
39
- * @type {boolean | null}
40
- */
41
- #fixMove = null
42
-
43
- /**
44
- * Configure the findbar and link to the web contents.
45
- *
46
- * @overload
47
- * @param {WebContents} webContents Findable web contents. The parent window will be defined by using BaseWindow.getAllWindows() and
48
- * matching the webContents with the webContents of the window or its contentView children.
49
- * @returns {Findbar} The findbar instance if it exists.
50
- * @throws {Error} If no webContents is provided.
51
- *
52
- * @overload
53
- * @param {BrowserWindow} browserWindow Parent window.
54
- * @param {WebContents} [customWebContents] Custom findable web contents. If not provided, the browserWindow.webContents will be used.
55
- * @returns {Findbar} The findbar instance if it exists.
56
- *
57
- * @overload
58
- * @param {BaseWindow} baseWindow Parent window.
59
- * @param {WebContents} [webContents] Custom findable web contents. If not provided, the win.contentView.children[0] will be used.
60
- * @returns {Findbar} The findbar instance if it exists.
61
- * @throws {Error} If no webContents is provided.
62
- */
63
- constructor (parent, webContents) {
64
- if (isFindable(parent)) {
65
- this.#findableContents = parent
66
- this.#parent = Findbar.#getBaseWindowFromWebContents(this.#findableContents)
67
- } else {
68
- this.#parent = parent
69
- this.#findableContents = webContents ?? Findbar.#retrieveWebContents(parent)
70
- }
71
-
72
- if (!this.#findableContents) {
73
- throw new Error('There are no searchable web contents.')
74
- }
75
-
76
- this.#findableContents._findbar = this
77
-
78
- this.#findableContents.once('destroyed', () => { this.detach() })
79
- }
80
-
81
- /**
82
- * Open the findbar. If the findbar is already opened, focus the input text.
83
- * @returns {void}
84
- */
85
- open() {
86
- if (this.#window) {
87
- this.#focusWindowAndHighlightInput()
88
- return
89
- }
90
- const options = Findbar.#mergeStandardOptions(this.#customOptions, this.#parent)
91
- this.#isMovable = options.movable
92
- this.#window = new BrowserWindow(options)
93
- this.#window.webContents._findbar = this
94
-
95
- this.#registerListeners()
96
-
97
- this.#windowHandler && this.#windowHandler(this.#window)
98
- this.#window.loadFile(`${__dirname}/web/findbar.html`)
99
- }
100
-
101
- /**
102
- * Close the findbar.
103
- * @returns {void}
104
- */
105
- close() {
106
- if (this.#window && !this.#window.isDestroyed()) {
107
- this.#window.close()
108
- }
109
- }
110
-
111
- /**
112
- * Detach the findbar from the web contents and close it if opened. After detaching, the findbar instance will be unusable.
113
- * @returns {void}
114
- */
115
- detach() {
116
- this.close()
117
- this.#findableContents._findbar = void 0
118
- if (this.#window) { this.#window.webContents._findbar = void 0 }
119
- }
120
-
121
- /**
122
- * Update the parent window of the findbar.
123
- * @param {BaseWindow} [newParent] - The new parent window. If not provided, the parent will be set to the window containing the web contents.
124
- * @returns {void}
125
- */
126
- updateParentWindow(newParent) {
127
- if (this.#parent === newParent) { return }
128
- this.close()
129
- this.#parent = newParent ?? Findbar.#getBaseWindowFromWebContents(this.#findableContents)
130
- }
131
-
132
- /**
133
- * Get the last state of the findbar.
134
- * @returns {{ text: string, matchCase: boolean, movable: boolean }} Last state of the findbar.
135
- */
136
- getLastState() {
137
- return { text: this.#lastText, matchCase: this.#matchCase, movable: this.#isMovable }
138
- }
139
-
140
- /**
141
- * Starts a request to find all matches for the text in the page.
142
- * @param {string} text - Value to find in page.
143
- * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
144
- * @returns {void}
145
- */
146
- startFind(text, skipRendererEvent) {
147
- skipRendererEvent || this.#window?.webContents.send('electron-findbar/text-change', text)
148
- if (this.#lastText = text) {
149
- this.isOpen() && this.#findInContent({ findNext: true })
150
- } else {
151
- this.stopFind()
152
- }
153
- }
154
-
155
- /**
156
- * Whether the search should be case-sensitive. If not set, the search will be case-insensitive.
157
- * @param {boolean} status - Whether the search should be case-sensitive. Default is false.
158
- * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
159
- * @returns {void}
160
- */
161
- matchCase(status, skipRendererEvent) {
162
- if (this.#matchCase === status) { return }
163
- this.#matchCase = status
164
- skipRendererEvent || this.#window?.webContents.send('electron-findbar/match-case-change', this.#matchCase)
165
- this.#stopFindInContent()
166
- this.startFind(this.#lastText, skipRendererEvent)
167
- }
168
-
169
- /**
170
- * Select previous match if any.
171
- * @returns {void}
172
- */
173
- findPrevious() {
174
- if (this.#matches.total < 2) { return }
175
- this.#matches.active === 1 && (this.#fixMove = false)
176
- this.isOpen() && this.#findInContent({ forward: false })
177
- }
178
-
179
- /**
180
- * Select next match if any.
181
- * @returns {void}
182
- */
183
- findNext() {
184
- if (this.#matches.total < 2) { return }
185
- this.#matches.active === this.#matches.total && (this.#fixMove = true)
186
- this.isOpen() && this.#findInContent({ forward: true })
187
- }
188
-
189
- /**
190
- * Stops the find request and clears selection.
191
- * @returns {void}
192
- */
193
- stopFind() {
194
- this.isOpen() && this.#sendMatchesCount(0, 0)
195
- this.#findableContents.isDestroyed() || this.#stopFindInContent()
196
- }
197
-
198
- /**
199
- * Whether the findbar is opened.
200
- * @returns {boolean} True if the findbar is open, otherwise false.
201
- */
202
- isOpen() {
203
- return !!this.#window
204
- }
205
-
206
- /**
207
- * Whether the findbar is focused. If the findbar is closed, false will be returned.
208
- * @returns {boolean} True if the findbar is focused, otherwise false.
209
- */
210
- isFocused() {
211
- return !!this.#window?.isFocused()
212
- }
213
-
214
- /**
215
- * Whether the findbar is visible to the user in the foreground of the app.
216
- * If the findbar is closed, false will be returned.
217
- * @returns {boolean} True if the findbar is visible, otherwise false.
218
- */
219
- isVisible() {
220
- return !!this.#window?.isVisible()
221
- }
222
-
223
- /**
224
- * Provides a customized set of options to findbar window before open. Note
225
- * that the options below are necessary for the correct functioning and cannot
226
- * be overridden:
227
- * - options.parent (value: parentWindow)
228
- * - options.frame (value: false)
229
- * - options.transparent (value: true)
230
- * - options.maximizable (value: false)
231
- * - options.minimizable (value: false)
232
- * - options.skipTaskbar (value: true)
233
- * - options.fullscreenable (value: false)
234
- * - options.webPreferences.nodeIntegration (value: true)
235
- * - options.webPreferences.contextIsolation (value: false)
236
- *
237
- * @param {BrowserWindowConstructorOptions} customOptions - Custom window options.
238
- * @returns {void}
239
- */
240
- setWindowOptions(customOptions) {
241
- this.#customOptions = customOptions
242
- }
243
-
244
- /**
245
- * Set a window handler capable of changing the findbar window settings after opening.
246
- * @param {(findbarWindow: BrowserWindow) => void} windowHandler - Window handler function.
247
- * @returns {void}
248
- */
249
- setWindowHandler(windowHandler) {
250
- this.#windowHandler = windowHandler
251
- }
252
-
253
- /**
254
- * 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.
255
- * @param {(parentBounds: Rectangle, findbarBounds: Rectangle) => Rectangle} boundsHandler - Bounds handler function.
256
- * @returns {void}
257
- */
258
- setBoundsHandler(boundsHandler) {
259
- this.#boundsHandler = boundsHandler
260
- }
261
-
262
- #registerKeyboardShortcuts(event, input) {
263
- if (input.meta || input.control || input.alt) { return }
264
-
265
- const key = input.key.toLowerCase()
266
-
267
- if (input.shift) {
268
- if (key === 'enter') {
269
- this.findPrevious()
270
- event.preventDefault()
271
- }
272
- return;
273
- }
274
-
275
- if (key === 'enter') {
276
- this.findNext()
277
- event.preventDefault()
278
- return;
279
- }
280
-
281
- if (key === 'escape') {
282
- if (this.isOpen()) {
283
- this.close()
284
- event.preventDefault()
285
- }
286
- }
287
- }
288
- /**
289
- * @param {Electron.FindInPageOptions} options
290
- */
291
- #findInContent(options) {
292
- options.matchCase = this.#matchCase
293
- this.#findableContents.findInPage(this.#lastText, options)
294
- }
295
-
296
- #stopFindInContent() {
297
- this.#findableContents.stopFindInPage('clearSelection')
298
- }
299
-
300
- /**
301
- * Register all event listeners.
302
- */
303
- #registerListeners() {
304
- const showCascade = () => this.#window.isVisible() || this.#window.show()
305
- const hideCascade = () => this.#window.isVisible() && this.#window.hide()
306
- const boundsHandler = () => {
307
- const currentBounds = this.#window.getBounds()
308
- const newBounds = this.#boundsHandler(this.#parent.getBounds(), currentBounds)
309
- if (!newBounds.width) { newBounds.width = currentBounds.width }
310
- if (!newBounds.height) { newBounds.height = currentBounds.height }
311
- this.#window.setBounds(newBounds, false)
312
- }
313
-
314
- if (this.#parent && !this.#parent.isDestroyed()) {
315
- boundsHandler()
316
- this.#parent.prependListener('show', showCascade)
317
- this.#parent.prependListener('hide', hideCascade)
318
- this.#parent.prependListener('resize', boundsHandler)
319
- this.#parent.prependListener('move', boundsHandler)
320
- }
321
-
322
- this.#window.once('closed', () => {
323
- if (this.#parent && !this.#parent.isDestroyed()) {
324
- this.#parent.off('show', showCascade)
325
- this.#parent.off('hide', hideCascade)
326
- this.#parent.off('resize', boundsHandler)
327
- this.#parent.off('move', boundsHandler)
328
- }
329
- this.#window = null
330
- this.stopFind()
331
- })
332
-
333
- this.#window.prependOnceListener('ready-to-show', () => { this.#window.show() })
334
- this.#window.webContents.prependListener('before-input-event', this.#registerKeyboardShortcuts.bind(this))
335
-
336
- this.#findableContents.prependOnceListener('destroyed', () => { this.close() })
337
- this.#findableContents.prependListener('found-in-page', (_e, result) => { this.#sendMatchesCount(result.activeMatchOrdinal, result.matches) })
338
- }
339
-
340
- /**
341
- * Send to renderer the active match and the total.
342
- * @param {number} active Active match.
343
- * @param {number} total Total matches.
344
- */
345
- #sendMatchesCount(active, total) {
346
- if (this.#fixMove !== null) {
347
- this.#fixMove ? this.findNext() : this.findPrevious()
348
- this.#fixMove = null
349
- }
350
-
351
- this.#matches.active = active
352
- this.#matches.total = total
353
-
354
- this.#window.webContents.send('electron-findbar/matches', this.#matches)
355
- }
356
-
357
- /**
358
- * Focus the findbar and highlight the input text.
359
- */
360
- #focusWindowAndHighlightInput() {
361
- this.#window.focus()
362
- this.#window.webContents.send('electron-findbar/input-focus')
363
- }
364
-
365
- /**
366
- * Retrieve web contents from a BrowserWindow or BaseWindow.
367
- * @param {BrowserWindow | BaseWindow} window
368
- * @returns {WebContents | undefined} The web contents if any.
369
- */
370
- static #retrieveWebContents(window) {
371
- return window.webContents ?? window.contentView?.children[0]?.webContents
372
- }
373
-
374
- /**
375
- * Get the parent window from web contents.
376
- * @param {WebContents} w
377
- * @returns {BaseWindow | undefined} Parent window if any.
378
- */
379
- static #getBaseWindowFromWebContents(w) {
380
- return BaseWindow.getAllWindows().find(win => win.webContents === w || win.contentView.children.some(child => child.webContents === w))
381
- }
382
-
383
- /**
384
- * Set default findbar position.
385
- * @param {Rectangle} parentBounds
386
- * @param {Rectangle} findbarBounds
387
- * @returns {x: number, y: number} position.
388
- */
389
- static #setDefaultPosition(parentBounds, findbarBounds) {
390
- return {
391
- x: parentBounds.x + parentBounds.width - findbarBounds.width - 20,
392
- y: parentBounds.y - ((findbarBounds.height / 4) | 0)
393
- }
394
- }
395
-
396
- /**
397
- * Merge custom, defaults, and fixed options.
398
- * @param {Electron.BrowserWindowConstructorOptions} options Custom options.
399
- * @param {BaseWindow | void} parent Parent window, if any.
400
- * @returns {Electron.BrowserWindowConstructorOptions} Merged options.
401
- */
402
- static #mergeStandardOptions(options, parent) {
403
- if (!options) { options = {} }
404
- options.width = options.width ?? 372
405
- options.height = options.height ?? 52
406
- options.resizable = options.resizable ?? false
407
- options.movable = options.movable ?? false
408
- options.acceptFirstMouse = options.acceptFirstMouse ?? true
409
- options.parent = parent
410
- options.show = false
411
- options.frame = false
412
- options.roundedCorners = true
413
- options.transparent = process.platform === 'linux'
414
- options.maximizable = false
415
- options.minimizable = false
416
- options.skipTaskbar = true
417
- options.fullscreenable = false
418
- options.autoHideMenuBar = true
419
- if (!options.webPreferences) { options.webPreferences = {} }
420
- options.webPreferences.nodeIntegration = false
421
- options.webPreferences.contextIsolation = true
422
- options.webPreferences.preload = options.webPreferences.preload ?? `${__dirname}/web/preload.js`
423
- return options
424
- }
425
-
426
- /**
427
- * Get the findbar instance for a given BrowserWindow or WebContents.
428
- * If no findbar instance exists, it will return a new one linked to the web contents.
429
- *
430
- * @overload
431
- * @param {WebContents} webContents Findable web contents. The parent window will be defined by using BaseWindow.getAllWindows() and
432
- * matching the webContents with the webContents of the window or its contentView children.
433
- * @returns {Findbar} The findbar instance if it exists.
434
- * @throws {Error} If no webContents is provided.
435
- *
436
- * @overload
437
- * @param {BrowserWindow} browserWindow Parent window.
438
- * @param {WebContents} [customWebContents] Custom findable web contents. If not provided, the browserWindow.webContents will be used.
439
- * @returns {Findbar} The findbar instance if it exists.
440
- *
441
- * @overload
442
- * @param {BaseWindow} baseWindow Parent window.
443
- * @param {WebContents} [webContents] Custom findable web contents. If not provided, the win.contentView.children[0] will be used.
444
- * @returns {Findbar} The findbar instance if it exists.
445
- * @throws {Error} If no webContents is provided.
446
- */
447
- static from(windowOrWebContents, customWebContents) {
448
- const webContents = isFindable(windowOrWebContents) ? windowOrWebContents : customWebContents ?? Findbar.#retrieveWebContents(windowOrWebContents)
449
- if (!webContents) { throw new Error('[Findbar] There are no searchable web contents.') }
450
- return webContents._findbar || new Findbar(windowOrWebContents, customWebContents)
451
- }
452
-
453
- /**
454
- * Get the findbar instance for a given BrowserWindow or WebContents.
455
- * @param {BrowserWindow | WebContents} windowOrWebContents
456
- * @returns {Findbar | undefined} The findbar instance if it exists, otherwise undefined.
457
- */
458
- static fromIfExists(windowOrWebContents) {
459
- const webContents = isFindable(windowOrWebContents) ? windowOrWebContents : Findbar.#retrieveWebContents(windowOrWebContents)
460
- if (!webContents) { throw new Error('[Findbar] There are no searchable web contents.') }
461
- return webContents._findbar
462
- }
463
- }
464
-
465
- const isFindable = (obj) => obj && typeof obj.findInPage === 'function' && typeof obj.stopFindInPage === 'function';
466
-
467
- /**
468
- * Define IPC events.
469
- */
470
- (ipc => {
471
- ipc.handle('electron-findbar/last-state', e => e.sender._findbar.getLastState())
472
- ipc.on('electron-findbar/input-change', (e, text, skip) => e.sender._findbar.startFind(text, skip))
473
- ipc.on('electron-findbar/match-case', (e, status, skip) => e.sender._findbar.matchCase(status, skip))
474
- ipc.on('electron-findbar/previous', e => e.sender._findbar.findPrevious())
475
- ipc.on('electron-findbar/next', e => e.sender._findbar.findNext())
476
- ipc.on('electron-findbar/open', e => e.sender._findbar.open())
477
- ipc.on('electron-findbar/close', e => {
478
- const findbar = e.sender._findbar
479
- findbar.stopFind()
480
- findbar.close()
481
- })
482
- }) (require('electron').ipcMain)
483
-
484
- module.exports = Findbar
package/remote.js DELETED
@@ -1,44 +0,0 @@
1
- /**
2
- * Remote IPC events to control the findbar through the renderer.
3
- */
4
- const Remote = (ipc => ({
5
- /**
6
- * Get last queried text and the "match case" status.
7
- * @returns {Promise<{ text: string, matchCase: boolean, movable: boolean }>}
8
- */
9
- getLastState: async () => ipc.invoke('electron-findbar/last-state'),
10
-
11
- /**
12
- * Change the input value and find it.
13
- * @param {string} text - The text to search for
14
- */
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
- */
26
- previous: () => { ipc.send('electron-findbar/previous') },
27
-
28
- /**
29
- * Navigate to the next match
30
- */
31
- next: () => { ipc.send('electron-findbar/next') },
32
-
33
- /**
34
- * Open the findbar
35
- */
36
- open: () => { ipc.send('electron-findbar/open') },
37
-
38
- /**
39
- * Close the findbar
40
- */
41
- close: () => { ipc.send('electron-findbar/close') },
42
- })) (require('electron').ipcRenderer)
43
-
44
- module.exports = Remote
package/web/app.css DELETED
@@ -1,141 +0,0 @@
1
- *, *:before, *:after {
2
- box-sizing: border-box;
3
- }
4
-
5
- body {
6
- margin: 0;
7
- padding: 0;
8
- font-weight: normal;
9
- height: 100vh;
10
- }
11
-
12
- nav {
13
- --bg-color: #fff;
14
- --border: #ddd;
15
- --color: #626262;
16
- --input-color: #111;
17
- --btn-hover-color: #ccc;
18
- --btn-active-color: #bbb;
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
- --font-size: .75rem;
21
- --spacing: .5rem;
22
- }
23
-
24
- @media (prefers-color-scheme: dark) {
25
- nav {
26
- --bg-color: #1f1f1f;
27
- --border: #3f3f3f;
28
- --color: #a9a9aa;
29
- --input-color: #eee;
30
- --btn-hover-color: #333;
31
- --btn-active-color: #444;
32
- }
33
- }
34
-
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 {
45
- fill: none;
46
- stroke: var(--color);
47
- stroke-width: 2;
48
- stroke-linecap: round;
49
- stroke-linejoin: round;
50
- }
51
-
52
- nav {
53
- display: flex;
54
- align-items: center;
55
- width: 100%;
56
- height: 100%;
57
- padding: var(--spacing);
58
- background-color: var(--bg-color);
59
- color: var(--color);
60
- }
61
-
62
- .linux nav {
63
- border-radius: 10px;
64
- border: 1px solid var(--border);
65
- }
66
-
67
- nav, .btn-group {
68
- gap: var(--spacing);
69
- }
70
-
71
- span, input {
72
- font-family: var(--font-family);
73
- font-size: var(--font-size);
74
- }
75
-
76
- span {
77
- color: var(--input-color);
78
- user-select: none;
79
- }
80
-
81
- input {
82
- width: 100%;
83
- background-color: transparent;
84
- color: var(--input-color);
85
- font-weight: 500;
86
- border: none;
87
- outline: none;
88
- }
89
-
90
- .divider {
91
- width: 2px;
92
- height: 100%;
93
- background-color: var(--btn-hover-color);
94
- }
95
-
96
- .btn-group {
97
- display: flex;
98
- }
99
-
100
- button {
101
- background-color: transparent;
102
- border: none;
103
- border-radius: 50%;
104
- cursor: default;
105
- width: 26px;
106
- height: 26px;
107
- padding: 3px;
108
- text-align: center;
109
- transition: .2s linear all;
110
- }
111
-
112
- button:hover {
113
- background-color: var(--btn-hover-color);
114
- }
115
-
116
- button:active {
117
- background-color: var(--btn-active-color);
118
- }
119
-
120
- button:focus {
121
- outline: none;
122
- }
123
-
124
- button.disabled {
125
- opacity: .4;
126
- }
127
-
128
- #previous.disabled, #next.disabled {
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
-
141
- }
package/web/findbar.html DELETED
@@ -1,43 +0,0 @@
1
- <!doctype html>
2
- <html>
3
-
4
- <head>
5
- <meta charset="utf-8">
6
- <meta name="viewport" content="width=device-width, initial-scale=1">
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>
9
- <link rel="stylesheet" href="app.css">
10
- <body class="movable">
11
- <nav>
12
- <input id="input" type="text" spellcheck="false">
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>
20
- <div class="divider"></div>
21
- <div class="btn-group">
22
-
23
- <button id="previous" class="disabled" title="Previous">
24
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
25
- <path d="M7 15L12 10L17 15"/>
26
- </svg>
27
- </button>
28
- <button id="next" class="disabled" title="Next">
29
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
30
- <path d="M7 10L12 15L17 10"/>
31
- </svg>
32
- </button>
33
- <button id="close" title="Close">
34
- <svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
35
- <path d="M7 17.5L12 12.5L17 17.5"/>
36
- <path d="M7 7.5L12 12.5L17 7.5"/>
37
- </svg>
38
- </button>
39
- </div>
40
- </nav>
41
- </body>
42
-
43
- </html>
package/web/preload.js DELETED
@@ -1,82 +0,0 @@
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
- })