electron-findbar 3.2.0 → 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/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.
@@ -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')
@@ -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.2.0",
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,510 +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 {boolean} */
17
- #propagateVisibilityEvents = true
18
-
19
- /** @type { { active: number, total: number } } */
20
- #matches = { active: 0, total: 0 }
21
-
22
- /** @type {(findbarWindow: BrowserWindow) => void} */
23
- #windowHandler
24
-
25
- /** @type {{parentBounds: Rectangle, findbarBounds: Rectangle} => Rectangle} */
26
- #boundsHandler = Findbar.#setDefaultPosition
27
-
28
- /** @type {BrowserWindowConstructorOptions} */
29
- #customOptions
30
-
31
- /** @type {string} */
32
- #lastText = ''
33
-
34
- /** @type {boolean} */
35
- #matchCase = false
36
-
37
- /** @type {boolean} */
38
- #isMovable = false
39
-
40
- /**
41
- * Workaround to fix "findInPage" bug - double-click to loop
42
- * @type {boolean | null}
43
- */
44
- #fixMove = null
45
-
46
- /**
47
- * Configure the findbar and link to the web contents.
48
- *
49
- * @overload
50
- * @param {WebContents} webContents Findable web contents. The parent window will be defined by using BaseWindow.getAllWindows() and
51
- * matching the webContents with the webContents of the window or its contentView children.
52
- * @returns {Findbar} The findbar instance if it exists.
53
- * @throws {Error} If no webContents is provided.
54
- *
55
- * @overload
56
- * @param {BrowserWindow} browserWindow Parent window.
57
- * @param {WebContents} [customWebContents] Custom findable web contents. If not provided, the browserWindow.webContents will be used.
58
- * @returns {Findbar} The findbar instance if it exists.
59
- *
60
- * @overload
61
- * @param {BaseWindow} baseWindow Parent window.
62
- * @param {WebContents} [webContents] Custom findable web contents. If not provided, the win.contentView.children[0] will be used.
63
- * @returns {Findbar} The findbar instance if it exists.
64
- * @throws {Error} If no webContents is provided.
65
- */
66
- constructor (parent, webContents) {
67
- if (isFindable(parent)) {
68
- this.#findableContents = parent
69
- parent = Findbar.#getBaseWindowFromWebContents(this.#findableContents)
70
- } else {
71
- this.#findableContents = webContents ?? Findbar.#retrieveWebContents(parent)
72
- }
73
-
74
- if (!this.#findableContents) {
75
- throw new Error('There are no searchable web contents.')
76
- }
77
-
78
- this.#findableContents._findbar = this
79
-
80
- this.#findableContents.once('destroyed', () => { this.detach() })
81
- this.updateParentWindow(parent)
82
- }
83
-
84
- /**
85
- * Open the findbar. If the findbar is already opened, focus the input text.
86
- * @returns {void}
87
- */
88
- open() {
89
- if (this.#window) {
90
- this.#focusWindowAndHighlightInput()
91
- return
92
- }
93
- if (!this.#parent) { this.updateParentWindow() }
94
- const options = Findbar.#mergeStandardOptions(this.#customOptions, this.#parent)
95
- this.#isMovable = options.movable
96
- this.#window = new BrowserWindow(options)
97
- this.#window.webContents._findbar = this
98
-
99
- this.#registerListeners()
100
-
101
- this.#windowHandler && this.#windowHandler(this.#window)
102
- this.#window.loadFile(`${__dirname}/web/findbar.html`)
103
- }
104
-
105
- /**
106
- * Close the findbar.
107
- * @returns {void}
108
- */
109
- close() {
110
- if (this.#window && !this.#window.isDestroyed()) {
111
- this.#window.close()
112
- }
113
- }
114
-
115
- /**
116
- * Detach the findbar from the web contents and close it if opened. After detaching, the findbar instance will be unusable.
117
- * @returns {void}
118
- */
119
- detach() {
120
- this.close()
121
- this.#findableContents._findbar = void 0
122
- if (this.#window) { this.#window.webContents._findbar = void 0 }
123
- }
124
-
125
- /**
126
- * Update the parent window of the findbar.
127
- * @param {BaseWindow} [newParent] - The new parent window. If not provided, the parent will be set to the window containing the web contents.
128
- * @returns {void}
129
- */
130
- updateParentWindow(newParent) {
131
- if (this.#parent === newParent) { return }
132
- this.close()
133
- this.#parent = newParent ?? Findbar.#getBaseWindowFromWebContents(this.#findableContents)
134
- if (this.#parent && !this.#parent.isDestroyed()) {
135
- this.#parent.once('closed', () => { this.#removeParent() })
136
- }
137
- }
138
-
139
- /**
140
- * Get the last state of the findbar.
141
- * @returns {{ text: string, matchCase: boolean, movable: boolean }} Last state of the findbar.
142
- */
143
- getLastState() {
144
- return { text: this.#lastText, matchCase: this.#matchCase, movable: this.#isMovable }
145
- }
146
-
147
- /**
148
- * Starts a request to find all matches for the text in the page.
149
- * @param {string} text - Value to find in page.
150
- * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
151
- * @returns {void}
152
- */
153
- startFind(text, skipRendererEvent) {
154
- skipRendererEvent || this.#window?.webContents.send('electron-findbar/text-change', text)
155
- if (this.#lastText = text) {
156
- this.isOpen() && this.#findInContent({ findNext: true })
157
- } else {
158
- this.stopFind()
159
- }
160
- }
161
-
162
- /**
163
- * Whether the search should be case-sensitive. If not set, the search will be case-insensitive.
164
- * @param {boolean} status - Whether the search should be case-sensitive. Default is false.
165
- * @param {boolean} [skipRendererEvent=false] - Skip update renderer event.
166
- * @returns {void}
167
- */
168
- matchCase(status, skipRendererEvent) {
169
- if (this.#matchCase === status) { return }
170
- this.#matchCase = status
171
- skipRendererEvent || this.#window?.webContents.send('electron-findbar/match-case-change', this.#matchCase)
172
- this.#stopFindInContent()
173
- this.startFind(this.#lastText, skipRendererEvent)
174
- }
175
-
176
- /**
177
- * Select previous match if any.
178
- * @returns {void}
179
- */
180
- findPrevious() {
181
- if (this.#matches.total < 2) { return }
182
- this.#matches.active === 1 && (this.#fixMove = false)
183
- this.isOpen() && this.#findInContent({ forward: false })
184
- }
185
-
186
- /**
187
- * Select next match if any.
188
- * @returns {void}
189
- */
190
- findNext() {
191
- if (this.#matches.total < 2) { return }
192
- this.#matches.active === this.#matches.total && (this.#fixMove = true)
193
- this.isOpen() && this.#findInContent({ forward: true })
194
- }
195
-
196
- /**
197
- * Stops the find request and clears selection.
198
- * @returns {void}
199
- */
200
- stopFind() {
201
- this.isOpen() && this.#sendMatchesCount(0, 0)
202
- this.#findableContents.isDestroyed() || this.#stopFindInContent()
203
- }
204
-
205
- /**
206
- * Whether the findbar is opened.
207
- * @returns {boolean} True if the findbar is open, otherwise false.
208
- */
209
- isOpen() {
210
- return !!this.#window
211
- }
212
-
213
- /**
214
- * Whether the findbar is focused. If the findbar is closed, false will be returned.
215
- * @returns {boolean} True if the findbar is focused, otherwise false.
216
- */
217
- isFocused() {
218
- return !!this.#window?.isFocused()
219
- }
220
-
221
- /**
222
- * Whether the findbar is visible to the user in the foreground of the app.
223
- * If the findbar is closed, false will be returned.
224
- * @returns {boolean} True if the findbar is visible, otherwise false.
225
- */
226
- isVisible() {
227
- return !!this.#window?.isVisible()
228
- }
229
-
230
- /**
231
- * Provides a customized set of options to findbar window before open. Note
232
- * that the options below are necessary for the correct functioning and cannot
233
- * be overridden:
234
- * - options.parent (value: parentWindow)
235
- * - options.frame (value: false)
236
- * - options.transparent (value: true)
237
- * - options.maximizable (value: false)
238
- * - options.minimizable (value: false)
239
- * - options.skipTaskbar (value: true)
240
- * - options.fullscreenable (value: false)
241
- * - options.webPreferences.nodeIntegration (value: true)
242
- * - options.webPreferences.contextIsolation (value: false)
243
- *
244
- * @param {BrowserWindowConstructorOptions} customOptions - Custom window options.
245
- * @returns {void}
246
- */
247
- setWindowOptions(customOptions) {
248
- this.#customOptions = customOptions
249
- }
250
-
251
- /**
252
- * Set a window handler capable of changing the findbar window settings after opening.
253
- * @param {(findbarWindow: BrowserWindow) => void} windowHandler - Window handler function.
254
- * @returns {void}
255
- */
256
- setWindowHandler(windowHandler) {
257
- this.#windowHandler = windowHandler
258
- }
259
-
260
- /**
261
- * 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.
262
- * @param {(parentBounds: Rectangle, findbarBounds: Rectangle) => Rectangle} boundsHandler - Bounds handler function.
263
- * @returns {void}
264
- */
265
- setBoundsHandler(boundsHandler) {
266
- this.#boundsHandler = boundsHandler
267
- }
268
-
269
- /**
270
- * Set whether to propagate visibility events to the parent window. If true, when the parent window is shown/hidden, the findbar will also be shown/hidden.
271
- * @param {boolean} shouldPropagate - Whether to propagate visibility events. Default is true.
272
- * @returns {void}
273
- */
274
- propagateVisibilityEvents(shouldPropagate) {
275
- this.#propagateVisibilityEvents = shouldPropagate
276
- }
277
-
278
- #registerKeyboardShortcuts(event, input) {
279
- if (input.meta || input.control || input.alt) { return }
280
-
281
- const key = input.key.toLowerCase()
282
-
283
- if (input.shift) {
284
- if (key === 'enter') {
285
- this.findPrevious()
286
- event.preventDefault()
287
- }
288
- return;
289
- }
290
-
291
- if (key === 'enter') {
292
- this.findNext()
293
- event.preventDefault()
294
- return;
295
- }
296
-
297
- if (key === 'escape') {
298
- if (this.isOpen()) {
299
- this.close()
300
- event.preventDefault()
301
- }
302
- }
303
- }
304
-
305
- #removeParent() {
306
- this.close()
307
- this.#parent = null
308
- }
309
-
310
- /**
311
- * @param {Electron.FindInPageOptions} options
312
- */
313
- #findInContent(options) {
314
- options.matchCase = this.#matchCase
315
- this.#findableContents.findInPage(this.#lastText, options)
316
- }
317
-
318
- #stopFindInContent() {
319
- this.#findableContents.stopFindInPage('clearSelection')
320
- }
321
-
322
- /**
323
- * Register all event listeners.
324
- */
325
- #registerListeners() {
326
- const showCascade = () => this.#window.isVisible() || this.#window.show()
327
- const hideCascade = () => this.#window.isVisible() && this.#window.hide()
328
- const boundsHandler = () => {
329
- const currentBounds = this.#window.getBounds()
330
- const newBounds = this.#boundsHandler(this.#parent.getBounds(), currentBounds)
331
- if (!newBounds.width) { newBounds.width = currentBounds.width }
332
- if (!newBounds.height) { newBounds.height = currentBounds.height }
333
- this.#window.setBounds(newBounds, false)
334
- }
335
-
336
- if (this.#parent && !this.#parent.isDestroyed()) {
337
- boundsHandler()
338
- if (this.#propagateVisibilityEvents) {
339
- this.#parent.prependListener('show', showCascade)
340
- this.#parent.prependListener('hide', hideCascade)
341
- }
342
- this.#parent.prependListener('resize', boundsHandler)
343
- this.#parent.prependListener('move', boundsHandler)
344
- }
345
-
346
- this.#window.once('closed', () => {
347
- if (this.#parent && !this.#parent.isDestroyed()) {
348
- if (this.#propagateVisibilityEvents) {
349
- this.#parent.off('show', showCascade)
350
- this.#parent.off('hide', hideCascade)
351
- }
352
- this.#parent.off('resize', boundsHandler)
353
- this.#parent.off('move', boundsHandler)
354
- }
355
- this.#window = null
356
- this.stopFind()
357
- })
358
-
359
- this.#window.prependOnceListener('ready-to-show', () => { this.#window.show() })
360
- this.#window.webContents.prependListener('before-input-event', this.#registerKeyboardShortcuts.bind(this))
361
-
362
- this.#findableContents.prependOnceListener('destroyed', () => { this.close() })
363
- this.#findableContents.prependListener('found-in-page', (_e, result) => { this.#sendMatchesCount(result.activeMatchOrdinal, result.matches) })
364
- }
365
-
366
- /**
367
- * Send to renderer the active match and the total.
368
- * @param {number} active Active match.
369
- * @param {number} total Total matches.
370
- */
371
- #sendMatchesCount(active, total) {
372
- if (this.#fixMove !== null) {
373
- this.#fixMove ? this.findNext() : this.findPrevious()
374
- this.#fixMove = null
375
- }
376
-
377
- this.#matches.active = active
378
- this.#matches.total = total
379
-
380
- this.#window.webContents.send('electron-findbar/matches', this.#matches)
381
- }
382
-
383
- /**
384
- * Focus the findbar and highlight the input text.
385
- */
386
- #focusWindowAndHighlightInput() {
387
- this.#window.focus()
388
- this.#window.webContents.send('electron-findbar/input-focus')
389
- }
390
-
391
- /**
392
- * Retrieve web contents from a BrowserWindow or BaseWindow.
393
- * @param {BrowserWindow | BaseWindow} window
394
- * @returns {WebContents | undefined} The web contents if any.
395
- */
396
- static #retrieveWebContents(window) {
397
- return window.webContents ?? window.contentView?.children[0]?.webContents
398
- }
399
-
400
- /**
401
- * Get the parent window from web contents.
402
- * @param {WebContents} w
403
- * @returns {BaseWindow | undefined} Parent window if any.
404
- */
405
- static #getBaseWindowFromWebContents(w) {
406
- return BaseWindow.getAllWindows().find(win => win.webContents === w || win.contentView.children.some(child => child.webContents === w))
407
- }
408
-
409
- /**
410
- * Set default findbar position.
411
- * @param {Rectangle} parentBounds
412
- * @param {Rectangle} findbarBounds
413
- * @returns {x: number, y: number} position.
414
- */
415
- static #setDefaultPosition(parentBounds, findbarBounds) {
416
- return {
417
- x: parentBounds.x + parentBounds.width - findbarBounds.width - 20,
418
- y: parentBounds.y - ((findbarBounds.height / 4) | 0)
419
- }
420
- }
421
-
422
- /**
423
- * Merge custom, defaults, and fixed options.
424
- * @param {Electron.BrowserWindowConstructorOptions} options Custom options.
425
- * @param {BaseWindow | void} parent Parent window, if any.
426
- * @returns {Electron.BrowserWindowConstructorOptions} Merged options.
427
- */
428
- static #mergeStandardOptions(options, parent) {
429
- if (!options) { options = {} }
430
- options.width = options.width ?? 372
431
- options.height = options.height ?? 52
432
- options.resizable = options.resizable ?? false
433
- options.movable = options.movable ?? false
434
- options.acceptFirstMouse = options.acceptFirstMouse ?? true
435
- options.parent = parent
436
- options.show = false
437
- options.frame = false
438
- options.roundedCorners = true
439
- options.transparent = process.platform === 'linux'
440
- options.maximizable = false
441
- options.minimizable = false
442
- options.skipTaskbar = true
443
- options.fullscreenable = false
444
- options.autoHideMenuBar = true
445
- if (!options.webPreferences) { options.webPreferences = {} }
446
- options.webPreferences.nodeIntegration = false
447
- options.webPreferences.contextIsolation = true
448
- options.webPreferences.preload = options.webPreferences.preload ?? `${__dirname}/web/preload.js`
449
- return options
450
- }
451
-
452
- /**
453
- * Get the findbar instance for a given BrowserWindow or WebContents.
454
- * If no findbar instance exists, it will return a new one linked to the web contents.
455
- *
456
- * @overload
457
- * @param {WebContents} webContents Findable web contents. The parent window will be defined by using BaseWindow.getAllWindows() and
458
- * matching the webContents with the webContents of the window or its contentView children.
459
- * @returns {Findbar} The findbar instance if it exists.
460
- * @throws {Error} If no webContents is provided.
461
- *
462
- * @overload
463
- * @param {BrowserWindow} browserWindow Parent window.
464
- * @param {WebContents} [customWebContents] Custom findable web contents. If not provided, the browserWindow.webContents will be used.
465
- * @returns {Findbar} The findbar instance if it exists.
466
- *
467
- * @overload
468
- * @param {BaseWindow} baseWindow Parent window.
469
- * @param {WebContents} [webContents] Custom findable web contents. If not provided, the win.contentView.children[0] will be used.
470
- * @returns {Findbar} The findbar instance if it exists.
471
- * @throws {Error} If no webContents is provided.
472
- */
473
- static from(windowOrWebContents, customWebContents) {
474
- const webContents = isFindable(windowOrWebContents) ? windowOrWebContents : customWebContents ?? Findbar.#retrieveWebContents(windowOrWebContents)
475
- if (!webContents) { throw new Error('[Findbar] There are no searchable web contents.') }
476
- return webContents._findbar || new Findbar(windowOrWebContents, customWebContents)
477
- }
478
-
479
- /**
480
- * Get the findbar instance for a given BrowserWindow or WebContents.
481
- * @param {BrowserWindow | WebContents} windowOrWebContents
482
- * @returns {Findbar | undefined} The findbar instance if it exists, otherwise undefined.
483
- */
484
- static fromIfExists(windowOrWebContents) {
485
- const webContents = isFindable(windowOrWebContents) ? windowOrWebContents : Findbar.#retrieveWebContents(windowOrWebContents)
486
- if (!webContents) { throw new Error('[Findbar] There are no searchable web contents.') }
487
- return webContents._findbar
488
- }
489
- }
490
-
491
- const isFindable = (obj) => obj && typeof obj.findInPage === 'function' && typeof obj.stopFindInPage === 'function';
492
-
493
- /**
494
- * Define IPC events.
495
- */
496
- (ipc => {
497
- ipc.handle('electron-findbar/last-state', e => e.sender._findbar.getLastState())
498
- ipc.on('electron-findbar/input-change', (e, text, skip) => e.sender._findbar.startFind(text, skip))
499
- ipc.on('electron-findbar/match-case', (e, status, skip) => e.sender._findbar.matchCase(status, skip))
500
- ipc.on('electron-findbar/previous', e => e.sender._findbar.findPrevious())
501
- ipc.on('electron-findbar/next', e => e.sender._findbar.findNext())
502
- ipc.on('electron-findbar/open', e => e.sender._findbar.open())
503
- ipc.on('electron-findbar/close', e => {
504
- const findbar = e.sender._findbar
505
- findbar.stopFind()
506
- findbar.close()
507
- })
508
- }) (require('electron').ipcMain)
509
-
510
- 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
- })