electron-findbar 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.vscode/launch.json +17 -0
- package/LICENSE +21 -0
- package/README.md +176 -0
- package/index.js +241 -0
- package/package.json +33 -0
- package/sample.js +23 -0
- package/test.js +0 -0
- package/web/app.css +106 -0
- package/web/app.js +51 -0
- package/web/findbar.html +37 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "0.2.0",
|
|
3
|
+
"configurations": [
|
|
4
|
+
{
|
|
5
|
+
"name": "Debug Main Process",
|
|
6
|
+
"type": "node",
|
|
7
|
+
"request": "launch",
|
|
8
|
+
"cwd": "${workspaceFolder}",
|
|
9
|
+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron",
|
|
10
|
+
"windows": {
|
|
11
|
+
"runtimeExecutable": "${workspaceFolder}/node_modules/.bin/electron.cmd"
|
|
12
|
+
},
|
|
13
|
+
"args" : ["sample.js"],
|
|
14
|
+
"outputCapture": "std"
|
|
15
|
+
}
|
|
16
|
+
]
|
|
17
|
+
}
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Emerson Capuchi Romaneli
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
### Documentation Improvements and Report
|
|
2
|
+
|
|
3
|
+
---
|
|
4
|
+
|
|
5
|
+
<p align='center'>
|
|
6
|
+
<a href="https://github.com/ECRomaneli/handbook" style='text-decoration:none'>
|
|
7
|
+
<img src="https://i.postimg.cc/0QR0s0Z1/findbar-light.png" alt='Findbar Light Theme'>
|
|
8
|
+
<img src="https://i.postimg.cc/LXtB6g0Y/findbar-dark.png" alt='Findbar Dark Theme'>
|
|
9
|
+
</a>
|
|
10
|
+
</p>
|
|
11
|
+
<p align='center'>
|
|
12
|
+
Chrome-like findbar for your Electron application
|
|
13
|
+
</p>
|
|
14
|
+
<p align='center'>
|
|
15
|
+
<a href="https://github.com/ECRomaneli/electron-findbar/tags" style='text-decoration:none'>
|
|
16
|
+
<img src="https://img.shields.io/github/v/tag/ecromaneli/electron-findbar?label=version&sort=semver&style=for-the-badge" alt="Version">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://github.com/ECRomaneli/electron-findbar/commits/master" style='text-decoration:none'>
|
|
19
|
+
<img src="https://img.shields.io/github/last-commit/ecromaneli/electron-findbar?style=for-the-badge" alt="Last Commit">
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://github.com/ECRomaneli/electron-findbar/blob/master/LICENSE" style='text-decoration:none'>
|
|
22
|
+
<img src="https://img.shields.io/github/license/ecromaneli/electron-findbar?style=for-the-badge" alt="License">
|
|
23
|
+
</a>
|
|
24
|
+
<a href="https://github.com/ECRomaneli/electron-findbar/issues" style='text-decoration:none'>
|
|
25
|
+
<img src="https://img.shields.io/badge/contributions-welcome-brightgreen.svg?style=for-the-badge" alt="Contributions Welcome">
|
|
26
|
+
</a>
|
|
27
|
+
</p>
|
|
28
|
+
|
|
29
|
+
## Installation
|
|
30
|
+
|
|
31
|
+
Install the `electron-findbar` package via npm:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
npm install electron-findbar
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Overview
|
|
38
|
+
|
|
39
|
+
The `electron-findbar` is a `BrowserWindow` 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.
|
|
40
|
+
|
|
41
|
+
### Memory Usage
|
|
42
|
+
|
|
43
|
+
To optimize memory usage, the Findbar window is created only when the findbar is open. The implementation is lightweight, including only essential code.
|
|
44
|
+
|
|
45
|
+
## Usage
|
|
46
|
+
|
|
47
|
+
All public methods are documented with JSDoc and can be referenced during import.
|
|
48
|
+
|
|
49
|
+
### Importing the Findbar
|
|
50
|
+
|
|
51
|
+
To import the Findbar class:
|
|
52
|
+
|
|
53
|
+
```js
|
|
54
|
+
const { Findbar } = require('electron-findbar');
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
### Creating the Findbar Instance
|
|
58
|
+
|
|
59
|
+
You can pass a `BrowserWindow` instance as a single parameter to use it as the parent window. The `BrowserWindow.WebContents` will be used as the searchable content:
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
const findbar = new Findbar(browserWindow);
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
Alternatively, you can provide a custom `WebContents` as the second parameter. In this case, the first parameter can be any `BaseWindow`, and the second parameter will be the searchable content:
|
|
66
|
+
|
|
67
|
+
```js
|
|
68
|
+
const findbar = new Findbar(baseWindow, customWebContents);
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Configuring the Findbar
|
|
72
|
+
|
|
73
|
+
You can customize the Findbar window options using the `setWindowOptions` method:
|
|
74
|
+
|
|
75
|
+
```js
|
|
76
|
+
findbar.setWindowOptions({ movable: true, resizable: true, alwaysOnTop: true });
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
To handle the Findbar window directly after it is opened, use the `setWindowHandler` method:
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
findbar.setWindowHandler(win => {
|
|
83
|
+
win.setVisibleOnAllWorkspaces(true, { visibleOnFullScreen: true });
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Opening the Findbar
|
|
88
|
+
|
|
89
|
+
The Findbar is a child window of the `BaseWindow` passed during construction. It will follow the relative movement of the parent window and must be visible for the Findbar to display:
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
findbar.open();
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Finding Text in the Page
|
|
96
|
+
|
|
97
|
+
Once open, the Findbar appears by default in the top-right corner of the parent window and can be used without additional coding. Alternatively, you can use the following methods to trigger `findInPage` and navigate through matches in the main process:
|
|
98
|
+
|
|
99
|
+
```js
|
|
100
|
+
/**
|
|
101
|
+
* Retrieve the last queried value.
|
|
102
|
+
*/
|
|
103
|
+
getLastValue();
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Initiate a request to find all matches for the specified text on the page.
|
|
107
|
+
* @param {string} text - The text to search for.
|
|
108
|
+
*/
|
|
109
|
+
startFind(text);
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Select the previous match, if available.
|
|
113
|
+
*/
|
|
114
|
+
findPrevious();
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Select the next match, if available.
|
|
118
|
+
*/
|
|
119
|
+
findNext();
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Stop the find request.
|
|
123
|
+
*/
|
|
124
|
+
stopFind();
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### Closing the Findbar
|
|
128
|
+
|
|
129
|
+
When the Findbar is closed, its window is destroyed to free memory resources. Use the following method to close the Findbar:
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
findbar.close();
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
A new internal window will be created the next time the `open` method is called. There is no need to instantiate another Findbar for the same parent window.
|
|
136
|
+
|
|
137
|
+
### Quick Example
|
|
138
|
+
|
|
139
|
+
Here is a quick example demonstrating how to use the `electron-findbar`:
|
|
140
|
+
|
|
141
|
+
```js
|
|
142
|
+
const { app, BrowserWindow } = require('electron');
|
|
143
|
+
const { Findbar } = require('electron-findbar');
|
|
144
|
+
|
|
145
|
+
app.whenReady().then(() => {
|
|
146
|
+
const window = new BrowserWindow();
|
|
147
|
+
window.loadURL('https://github.com/ECRomaneli/electron-findbar');
|
|
148
|
+
|
|
149
|
+
// Create and configure the Findbar object
|
|
150
|
+
const findbar = new Findbar(window);
|
|
151
|
+
|
|
152
|
+
// [OPTIONAL] Customize window options
|
|
153
|
+
findbar.setWindowOptions({ movable: true, resizable: true });
|
|
154
|
+
|
|
155
|
+
// [OPTIONAL] Handle the window object when the Findbar is opened
|
|
156
|
+
findbar.setWindowHandler(win => {
|
|
157
|
+
win.webContents.openDevTools();
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
// Open the Findbar
|
|
161
|
+
findbar.open();
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## Notes
|
|
166
|
+
|
|
167
|
+
There are some intentional differences from the Chrome findbar, such as the horizontal margins of the divider and the input text, which has been replaced by a search input to include a clear button (the "x" on the right side).
|
|
168
|
+
|
|
169
|
+
## Author
|
|
170
|
+
|
|
171
|
+
Created by [Emerson Capuchi Romaneli](https://github.com/ECRomaneli) (@ECRomaneli).
|
|
172
|
+
|
|
173
|
+
## License
|
|
174
|
+
|
|
175
|
+
This project is licensed under the [MIT License](https://github.com/ECRomaneli/handbook/blob/master/LICENSE).
|
|
176
|
+
|
package/index.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const { BaseWindow, BrowserWindow, WebContents, BrowserWindowConstructorOptions, Rectangle } = require('electron')
|
|
2
|
+
const path = require('node:path')
|
|
3
|
+
|
|
4
|
+
class Findbar {
|
|
5
|
+
/** @type {BaseWindow} */
|
|
6
|
+
#parent
|
|
7
|
+
|
|
8
|
+
/** @type {BrowserWindow} */
|
|
9
|
+
#window
|
|
10
|
+
|
|
11
|
+
/** @type {WebContents} */
|
|
12
|
+
#searchableContents
|
|
13
|
+
|
|
14
|
+
/** */
|
|
15
|
+
#matches
|
|
16
|
+
|
|
17
|
+
/** @type {(findbarWindow: BrowserWindow) => void} */
|
|
18
|
+
#windowHandler
|
|
19
|
+
|
|
20
|
+
/** @type {BrowserWindowConstructorOptions} */
|
|
21
|
+
#customOptions
|
|
22
|
+
|
|
23
|
+
/** @type {string} */
|
|
24
|
+
#lastValue = ''
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Prepare the findbar.
|
|
28
|
+
* @param {BaseWindow} parent Parent window.
|
|
29
|
+
* @param {WebContents | void} webContents Searchable web contents. If not set and the parent is a BrowserWindow,
|
|
30
|
+
* the web contents of the parent will be used. Otherwise, an error will be triggered.
|
|
31
|
+
*/
|
|
32
|
+
constructor (parent, webContents) {
|
|
33
|
+
this.#parent = parent
|
|
34
|
+
this.#searchableContents = webContents ?? parent.webContents
|
|
35
|
+
|
|
36
|
+
if (!this.#searchableContents) {
|
|
37
|
+
throw new Error('There are no searchable web contents.')
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Open the findbar.
|
|
43
|
+
*/
|
|
44
|
+
open() {
|
|
45
|
+
if (this.#window) {
|
|
46
|
+
this.#focusInput()
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
this.#window = new BrowserWindow(Findbar.#mergeStandardOptions(this.#customOptions, this.#parent))
|
|
50
|
+
this.#window.webContents.findbar = this
|
|
51
|
+
|
|
52
|
+
this.#registerListeners()
|
|
53
|
+
this.#setDefaultPosition(this.#parent.getBounds())
|
|
54
|
+
|
|
55
|
+
this.#windowHandler && this.#windowHandler(this.#window)
|
|
56
|
+
|
|
57
|
+
this.#window.loadFile(path.join('web', 'findbar.html'))
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Close the findbar.
|
|
62
|
+
*/
|
|
63
|
+
close() {
|
|
64
|
+
this.#window?.close()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Get last queried value.
|
|
69
|
+
*/
|
|
70
|
+
getLastValue() {
|
|
71
|
+
return this.#lastValue
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Starts a request to find all matches for the text in the page.
|
|
76
|
+
* @param {string} text Value to find in page.
|
|
77
|
+
*/
|
|
78
|
+
startFind(text) {
|
|
79
|
+
if (this.#lastValue = text) {
|
|
80
|
+
this.#searchableContents.findInPage(this.#lastValue, { findNext: true })
|
|
81
|
+
} else {
|
|
82
|
+
this.stopFind()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Select previous match if any.
|
|
88
|
+
*/
|
|
89
|
+
findPrevious() {
|
|
90
|
+
this.#searchableContents.findInPage(this.#lastValue, { forward: false })
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Select next match if any.
|
|
95
|
+
*/
|
|
96
|
+
findNext() {
|
|
97
|
+
this.#searchableContents.findInPage(this.#lastValue, { forward: true })
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Stops the find request.
|
|
102
|
+
*/
|
|
103
|
+
stopFind() {
|
|
104
|
+
this.isOpen() && this.#sendMatchesCount(0, 0)
|
|
105
|
+
this.#searchableContents.isDestroyed() || this.#searchableContents.stopFindInPage("clearSelection")
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Return if the findbar is open or not.
|
|
110
|
+
* @returns {boolean} True, if the findbar is open. Otherwise, false.
|
|
111
|
+
*/
|
|
112
|
+
isOpen() {
|
|
113
|
+
return !!this.#window
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Provides a customized set of options to findbar window before open. Note
|
|
118
|
+
* that the options below are necessary for the correct functioning and cannot
|
|
119
|
+
* be overridden:
|
|
120
|
+
* - options.parent (value: parentWindow)
|
|
121
|
+
* - options.frame (value: false)
|
|
122
|
+
* - options.transparent (value: true)
|
|
123
|
+
* - options.maximizable (value: false)
|
|
124
|
+
* - options.minimizable (value: false)
|
|
125
|
+
* - options.skipTaskbar (value: true)
|
|
126
|
+
* - options.fullscreenable (value: false)
|
|
127
|
+
* - options.webPreferences.nodeIntegration (value: true)
|
|
128
|
+
* - options.webPreferences.contextIsolation (value: false)
|
|
129
|
+
* @param {BrowserWindowConstructorOptions} customOptions Custom window options.
|
|
130
|
+
*/
|
|
131
|
+
setWindowOptions(customOptions) {
|
|
132
|
+
this.#customOptions = customOptions
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Set a window handler capable of changing the findbar window settings after opening.
|
|
137
|
+
* @param {(findbarWindow: BrowserWindow) => void} windowHandler Window handler.
|
|
138
|
+
*/
|
|
139
|
+
setWindowHandler(windowHandler) {
|
|
140
|
+
this.#windowHandler = windowHandler
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Merge custom, defaults, and fixed options.
|
|
145
|
+
* @param {Electron.BrowserWindowConstructorOptions} options Custom options.
|
|
146
|
+
* @param {BaseWindow | void} parent Parent window, if any.
|
|
147
|
+
* @returns {Electron.BrowserWindowConstructorOptions} Merged options.
|
|
148
|
+
*/
|
|
149
|
+
static #mergeStandardOptions(options, parent) {
|
|
150
|
+
if (!options) { options = {} }
|
|
151
|
+
options.width = options.width ?? 372
|
|
152
|
+
options.height = options.height ?? 52
|
|
153
|
+
options.resizable = options.resizable ?? false
|
|
154
|
+
options.movable = options.movable ?? false
|
|
155
|
+
options.parent = parent
|
|
156
|
+
options.frame = false
|
|
157
|
+
options.transparent = true
|
|
158
|
+
options.maximizable = false
|
|
159
|
+
options.minimizable = false
|
|
160
|
+
options.skipTaskbar = true
|
|
161
|
+
options.fullscreenable = false
|
|
162
|
+
if (!options.webPreferences) { options.webPreferences = {} }
|
|
163
|
+
options.webPreferences.nodeIntegration = true
|
|
164
|
+
options.webPreferences.contextIsolation = false
|
|
165
|
+
return options
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Register all event listeners.
|
|
170
|
+
*/
|
|
171
|
+
#registerListeners() {
|
|
172
|
+
const showCascade = () => this.#window.isVisible() || this.#window.show()
|
|
173
|
+
const hideCascade = () => this.#window.isVisible() && this.#window.hide()
|
|
174
|
+
|
|
175
|
+
this.#parent.prependListener('show', showCascade)
|
|
176
|
+
this.#parent.prependListener('hide', hideCascade)
|
|
177
|
+
|
|
178
|
+
this.#window.once('closed', () => {
|
|
179
|
+
this.#parent.off('show', showCascade)
|
|
180
|
+
this.#parent.off('hide', hideCascade)
|
|
181
|
+
this.#window = null
|
|
182
|
+
this.stopFind()
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
this.#searchableContents.prependOnceListener('destroyed', () => { this.close() })
|
|
186
|
+
this.#searchableContents.prependListener('found-in-page', (_e, result) => {
|
|
187
|
+
this.#matches = result
|
|
188
|
+
this.#sendMatchesCount(result.activeMatchOrdinal, result.matches)
|
|
189
|
+
})
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Set default findbar position.
|
|
194
|
+
* @param {Rectangle} parentBounds
|
|
195
|
+
*/
|
|
196
|
+
#setDefaultPosition(parentBounds) {
|
|
197
|
+
const s = this.#window.getSize()
|
|
198
|
+
this.#window.setBounds({
|
|
199
|
+
x: parentBounds.x + parentBounds.width - s[0] - 20,
|
|
200
|
+
y: parentBounds.y - ((s[1] / 4) | 0)
|
|
201
|
+
})
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Send to renderer the active match and the total.
|
|
206
|
+
* @param {number} active Active match.
|
|
207
|
+
* @param {number} total Total matches.
|
|
208
|
+
*/
|
|
209
|
+
#sendMatchesCount(active, total) {
|
|
210
|
+
this.#window.webContents.send('electron-findbar/matches', { active, total })
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Select input text.
|
|
215
|
+
*/
|
|
216
|
+
#focusInput() {
|
|
217
|
+
this.#window.webContents.send('electron-findbar/input-focus')
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Define IPC events.
|
|
223
|
+
*/
|
|
224
|
+
(({ ipcMain }) => {
|
|
225
|
+
ipcMain.handle('electron-findbar/initial-input', e => {
|
|
226
|
+
const findbar = e.sender.findbar
|
|
227
|
+
findbar.startFind(findbar.getLastValue())
|
|
228
|
+
return findbar.getLastValue()
|
|
229
|
+
})
|
|
230
|
+
|
|
231
|
+
ipcMain.on('electron-findbar/input-change', (e, value) => e.sender.findbar.startFind(value))
|
|
232
|
+
ipcMain.on('electron-findbar/previous', e => e.sender.findbar.findPrevious())
|
|
233
|
+
ipcMain.on('electron-findbar/next', e => e.sender.findbar.findNext())
|
|
234
|
+
ipcMain.on('electron-findbar/close', e => {
|
|
235
|
+
const findbar = e.sender.findbar
|
|
236
|
+
findbar.stopFind()
|
|
237
|
+
findbar.close()
|
|
238
|
+
})
|
|
239
|
+
}) (require('electron'))
|
|
240
|
+
|
|
241
|
+
module.exports = { Findbar }
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "electron-findbar",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Chrome-like findbar for your Electron app.",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"sample": "electron sample.js",
|
|
8
|
+
"test": "node test.js"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/ECRomaneli/electron-findbar.git"
|
|
13
|
+
},
|
|
14
|
+
"keywords": [
|
|
15
|
+
"findbar",
|
|
16
|
+
"electron-findbar",
|
|
17
|
+
"electron",
|
|
18
|
+
"chrome-findbar",
|
|
19
|
+
"search"
|
|
20
|
+
],
|
|
21
|
+
"author": "ECRomaneli",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/ECRomaneli/electron-findbar/issues"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://github.com/ECRomaneli/electron-findbar#readme",
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=12.0.0"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"electron": ">=12.0.0"
|
|
32
|
+
}
|
|
33
|
+
}
|
package/sample.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
const { BrowserWindow, app, Menu } = require('electron')
|
|
2
|
+
const { Findbar } = require('./index')
|
|
3
|
+
|
|
4
|
+
app.whenReady().then(() => {
|
|
5
|
+
const window = new BrowserWindow()
|
|
6
|
+
window.loadURL('https://github.com/ECRomaneli/electron-findbar#readme')
|
|
7
|
+
|
|
8
|
+
const findbar = new Findbar(window)
|
|
9
|
+
findbar.setWindowOptions({ movable: !true, resizable: true })
|
|
10
|
+
findbar.setWindowHandler(win => {
|
|
11
|
+
win.webContents.openDevTools()
|
|
12
|
+
})
|
|
13
|
+
findbar.open()
|
|
14
|
+
|
|
15
|
+
const contextMenu = Menu.buildFromTemplate([
|
|
16
|
+
{ role: 'separator' },
|
|
17
|
+
{ label: 'Open findbar', click: () => findbar.open(), accelerator: 'CommandOrControl+F' },
|
|
18
|
+
{ label: 'Close findbar', click: () => findbar.isOpen() && findbar.close(), accelerator: 'Esc', registerAccelerator: true, acceleratorWorksWhenHidden: true }
|
|
19
|
+
])
|
|
20
|
+
|
|
21
|
+
Menu.setApplicationMenu(contextMenu)
|
|
22
|
+
|
|
23
|
+
})
|
package/test.js
ADDED
|
File without changes
|
package/web/app.css
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
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: #eee;
|
|
15
|
+
--color: #626262;
|
|
16
|
+
--input-color: #1f1f1f;
|
|
17
|
+
--btn-hover-color: #ccc;
|
|
18
|
+
--btn-active-color: #bbb;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
@media (prefers-color-scheme: dark) {
|
|
22
|
+
nav {
|
|
23
|
+
--bg-color: #1f1f1f;
|
|
24
|
+
--border: #2f2f2f;
|
|
25
|
+
--color: #a9a9aa;
|
|
26
|
+
--input-color: #e3e3e3;
|
|
27
|
+
--btn-hover-color: #333;
|
|
28
|
+
--btn-active-color: #444;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
svg {
|
|
33
|
+
fill: none;
|
|
34
|
+
stroke: var(--color);
|
|
35
|
+
stroke-width: 2;
|
|
36
|
+
stroke-linecap: round;
|
|
37
|
+
stroke-linejoin: round;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
nav {
|
|
41
|
+
display: flex;
|
|
42
|
+
align-items: center;
|
|
43
|
+
width: 100%;
|
|
44
|
+
height: 100%;
|
|
45
|
+
padding: .75rem;
|
|
46
|
+
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";
|
|
47
|
+
background-color: var(--bg-color);
|
|
48
|
+
color: var(--color);
|
|
49
|
+
font-size: .75rem;
|
|
50
|
+
border-radius: 10px;
|
|
51
|
+
border: 1px solid var(--border);
|
|
52
|
+
-webkit-app-region: drag;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
nav > span,
|
|
56
|
+
nav > .divider {
|
|
57
|
+
margin-right: .75rem;
|
|
58
|
+
user-select: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
input {
|
|
62
|
+
width: 100%;
|
|
63
|
+
background-color: transparent;
|
|
64
|
+
color: var(--input-color);
|
|
65
|
+
border: none;
|
|
66
|
+
outline: none;
|
|
67
|
+
-webkit-app-region: no-drag;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
.divider {
|
|
71
|
+
width: 2px;
|
|
72
|
+
height: 100%;
|
|
73
|
+
background-color: var(--btn-hover-color);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
.btn-group {
|
|
77
|
+
display: flex;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.btn-group > :not(:last-child) {
|
|
81
|
+
margin-right: .5rem;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.btn-group > div {
|
|
85
|
+
border-radius: 50%;
|
|
86
|
+
cursor: default;
|
|
87
|
+
width: 26px;
|
|
88
|
+
height: 26px;
|
|
89
|
+
padding: 3px;
|
|
90
|
+
text-align: center;
|
|
91
|
+
transition: .1s linear all;
|
|
92
|
+
-webkit-app-region: no-drag;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.btn-group > div:hover {
|
|
96
|
+
background-color: var(--btn-hover-color);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
.btn-group > div:active {
|
|
100
|
+
background-color: var(--btn-active-color);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
.btn-group > .disabled {
|
|
104
|
+
opacity: .4;
|
|
105
|
+
background-color: transparent !important;
|
|
106
|
+
}
|
package/web/app.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
const { ipcRenderer } = require('electron')
|
|
2
|
+
|
|
3
|
+
const $remote = {
|
|
4
|
+
getInitialInput: async () => ipcRenderer.invoke('electron-findbar/initial-input'),
|
|
5
|
+
inputChange: (value) => { ipcRenderer.send('electron-findbar/input-change', value) },
|
|
6
|
+
previous: () => { ipcRenderer.send('electron-findbar/previous') },
|
|
7
|
+
next: () => { ipcRenderer.send('electron-findbar/next') },
|
|
8
|
+
close: () => { ipcRenderer.send('electron-findbar/close') },
|
|
9
|
+
onMatchesChange: (listener) => { ipcRenderer.on('electron-findbar/matches', listener) },
|
|
10
|
+
onInputFocus: (listener) => { ipcRenderer.on('electron-findbar/input-focus', listener) }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let canRequest = true, canMove = false
|
|
14
|
+
|
|
15
|
+
function inputChange(e) {
|
|
16
|
+
canRequest = false
|
|
17
|
+
$remote.inputChange(e.target.value)
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function move(next) {
|
|
21
|
+
if (canRequest && canMove) {
|
|
22
|
+
canRequest = false
|
|
23
|
+
next ? $remote.next() : $remote.previous()
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
document.addEventListener('DOMContentLoaded', async () => {
|
|
28
|
+
const inputEl = document.getElementById('input')
|
|
29
|
+
const matchesEl = document.getElementById('matches')
|
|
30
|
+
const moveBtns = [...document.getElementsByClassName('disabled')]
|
|
31
|
+
|
|
32
|
+
$remote.onMatchesChange((_, m) => {
|
|
33
|
+
canRequest = true
|
|
34
|
+
matchesEl.innerText = inputEl.value ? m.active + '/' + m.total : ''
|
|
35
|
+
|
|
36
|
+
for (var moveBtn of moveBtns) {
|
|
37
|
+
(canMove = m.total > 1) ?
|
|
38
|
+
moveBtn.classList.remove('disabled') :
|
|
39
|
+
moveBtn.classList.add('disabled')
|
|
40
|
+
}
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
$remote.onInputFocus(async () => {
|
|
44
|
+
inputEl.setSelectionRange(0, inputEl.value.length)
|
|
45
|
+
inputEl.focus()
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
inputEl.value = await $remote.getInitialInput()
|
|
49
|
+
inputEl.setSelectionRange(0, inputEl.value.length)
|
|
50
|
+
inputEl.focus()
|
|
51
|
+
})
|
package/web/findbar.html
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
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
|
+
<title>Findbar</title>
|
|
8
|
+
<link rel="stylesheet" href="app.css">
|
|
9
|
+
<body>
|
|
10
|
+
<nav>
|
|
11
|
+
<input id='input' oninput="inputChange(event)" type="search" spellcheck="false">
|
|
12
|
+
<span id="matches"></span>
|
|
13
|
+
<div class="divider"></div>
|
|
14
|
+
<div class="btn-group">
|
|
15
|
+
<div onclick="move(false)" class="disabled">
|
|
16
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
17
|
+
<path d="M7 15L12 10L17 15"/>
|
|
18
|
+
</svg>
|
|
19
|
+
</div>
|
|
20
|
+
<div onclick="move(true)" class="disabled">
|
|
21
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
22
|
+
<path d="M7 10L12 15L17 10"/>
|
|
23
|
+
</svg>
|
|
24
|
+
</div>
|
|
25
|
+
<div onclick="$remote.close()">
|
|
26
|
+
<svg viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
|
|
27
|
+
<path d="M7 17.5L12 12.5L17 17.5"/>
|
|
28
|
+
<path d="M7 7.5L12 12.5L17 7.5"/>
|
|
29
|
+
</svg>
|
|
30
|
+
</div>
|
|
31
|
+
</div>
|
|
32
|
+
</nav>
|
|
33
|
+
|
|
34
|
+
<script src="app.js"></script>
|
|
35
|
+
</body>
|
|
36
|
+
|
|
37
|
+
</html>
|