diff --git a/app/js/index.js b/app/js/index.js index 936b64dc..41dc6a8d 100644 --- a/app/js/index.js +++ b/app/js/index.js @@ -1,5 +1,8 @@ (function($) { const ipcRenderer = require('electron').ipcRenderer; + const remote = require('electron').remote; + const electronFind = require('electron-find'); + let findInPage = new electronFind.FindInPage(remote.getCurrentWebContents()); //events ipcRenderer.on('fileList', (event, files) => { @@ -71,6 +74,11 @@ }); $(document).on('keypress', (ev) => { + //ctrl + f + if (ev.ctrlKey && ev.charCode == 6) { + findInPage.openFindWindow(); + } + //ctrl + r if (ev.ctrlKey && ev.keyCode == 18) { window.location.reload(false); diff --git a/app/node_modules/electron-find/README.md b/app/node_modules/electron-find/README.md new file mode 100644 index 00000000..be99b479 --- /dev/null +++ b/app/node_modules/electron-find/README.md @@ -0,0 +1,126 @@ +# electron-find + +English | [简体中文](./README.zh-CN.md) + +## Introduction +Find all matches for the text in electron app + +## Features +- depend on the API of electron's findInPage +- support user config UI of find interface +- support case-sensitive +- Auto find when user inputing is change +- The find interface is separated from electron view +- support electron version ^1.8.0, ^2.0.0, ^3.0.0, ^4.0.0 +- support platform of Windows, Linux, Mac + +## Demo + +### Default UI +![demo](./find.gif) + +### Custom UI +![demo](./find2.png) + +## Install +``` +$ npm install electron-find --save +``` + +## Usage +``` +# import module +import { remote, ipcRenderer } from 'electron' +import { FindInPage } from 'electron-find' + +# create instance of FindInPage with default config +let findInPage = new FindInPage(remote.getCurrentWebContents()) +findInPage.openFindWindow() + +# use preload option, the find interface will be loaded when create instance +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + preload: true +}) +findInPage.openFindWindow() + +# config parentElement of find interface, default is document.body +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + parentElement: document.querySelector('#id') +}) +findInPage.openFindWindow() + +# config duration of find interface moving, default is 300 (ms) +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + duration: 200 +}) +findInPage.openFindWindow() + +# config offset relative to parentElement +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + offsetTop: 20, + offsetRight: 30 +}) +findInPage.openFindWindow() + +# config UI of find interface +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + boxBgColor: '#333', + boxShadowColor: '#000', + inputColor: '#aaa', + inputBgColor: '#222', + inputFocusColor: '#555', + textColor: '#aaa', + textHoverBgColor: '#555', + caseSelectedColor: '#555' +}) +findInPage.openFindWindow() + +# there is a simply demo for reference +npm install +npm run e +``` +## Shortcut +| keys | function | +| ------ | ------ | +| Enter | find next | +| Shift + Enter| find back | +| Esc | close | + + Besides, you can also register global shortcut to open the find window, just like the demo. + + ## API + ### Class: FindInPage + ` new FindInPage(webContents, [options]) ` +- ` webContents ` Object(required) - The webContents of renderer process +- ` options ` Object(optional) + - ` preload ` Boolean - Whether load the find interface when create instance. Default is `false`. + - ` parentElement ` Object - Specify parent dom of the find interface. Default is `document.body`. + - ` duration ` Number - Specify moving time when the find window open or close. Default is `300` (ms). + - ` offsetTop ` Number - Specify offset relative to the top of parentElement. Default is `5`. + - ` offsetRight ` Number - Specify offset relative to the right of parentElement. Default is `5`. + - ` boxBgColor ` String - Specify background color of the find interface. Default is `"#ffffff"`. + - ` boxShadowColor ` String - Specify shadow color of the find interface. Default is `"#909399"`. + - ` inputColor ` String - Specify text color of the input form. Default is "#606266". + - ` inputBgColor ` String - Specify background color of the input form. Default is `"#f0f0f0"`. + - ` inputFocusColor ` String - Specify border color of the input form when focusing. Default is `"#c5ade0"`. + - ` textColor ` String - Specify color of the text in find interface. Default is `"#606266"`. + - ` textHoverBgColor ` String - Specify background color of text in find interface when hovering. Default is `"#eaeaea"`. + - ` caseSelectedColor ` String - Specify border color of the matchCase button when selected. Default is `"#c5ade0"`. + + ### Instance Methods + Objects created with new FindInPage have the following instance methods: +   + ` findInPage.openFindWindow() ` + Open the find window when it is closed. Focus input form when the find window has opened. +   + ` findInPage.closeFindWindow() ` + Close the find window when it has opened. +   + ` findInPage.destroy() ` + Close the find window, and release memery. + + + + + + diff --git a/app/node_modules/electron-find/README.zh-CN.md b/app/node_modules/electron-find/README.zh-CN.md new file mode 100644 index 00000000..54a08868 --- /dev/null +++ b/app/node_modules/electron-find/README.zh-CN.md @@ -0,0 +1,126 @@ +# electron-find + +简体中文 | [English](./README.md) + +## 简介 +在Electron app页内查找匹配关键字的所有文本字符 + +## 特征 +- 依赖于Electron的findInPage API +- 支持使用者灵活配置UI界面 +- 支持区分大小写 +- 当用户输入时自动查找 +- 查找输入框文本隔离,不会被匹配到 +- 支持以下Electron版本 ^1.8.7, ^2.0.0, ^3.0.0, ^4.0.0 +- 支持以下平台 Windows, Linux, Mac + +## 演示 + +### 默认UI +![demo](./find.gif) + +### 定制化UI +![demo](./find2.png) + +## 安装 +``` +$ npm install electron-find --save +``` + +## 使用 +``` +# 引入模块 +import { remote, ipcRenderer } from 'electron' +import { FindInPage } from 'electron-find' + +# 使用默认配置来创建实例 +let findInPage = new FindInPage(remote.getCurrentWebContents()) +findInPage.openFindWindow() + +# 开启预加载选项,创建实例的时候会同时加载查找窗口相关dom +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + preload: true +}) +findInPage.openFindWindow() + +# 配置父节点元素, 默认为 document.body +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + parentElement: document.querySelector('#id') +}) +findInPage.openFindWindow() + +# 配置查找窗口显示或隐藏的过渡周期, 默认为 300 (ms) +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + duration: 200 +}) +findInPage.openFindWindow() + +# 配置查找窗口相对于父级定位节点的偏移量 +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + offsetTop: 20, + offsetRight: 30 +}) +findInPage.openFindWindow() + +# 自定义UI界面颜色 +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + boxBgColor: '#333', + boxShadowColor: '#000', + inputColor: '#aaa', + inputBgColor: '#222', + inputFocusColor: '#555', + textColor: '#aaa', + textHoverBgColor: '#555', + caseSelectedColor: '#555' +}) +findInPage.openFindWindow() + +# 参考demo +npm install +npm run e +``` +## 快捷键 +| 键 | 功能 | +| ------ | ------ | +| Enter | 查找上一个 | +| Shift + Enter| 查找下一个 | +| Esc | 关闭窗口 | + + 另外, 可以参考demo,使用全局快捷键来打开窗口。 + + ## API + ### Class: FindInPage + ` new FindInPage(webContents, [options]) ` +- ` webContents ` Object(required) - 渲染进程的webContents对象 +- ` options ` Object(optional) + - ` preload ` Boolean - 创建实例的时候是否预加载查找窗口。 默认为 `false`。 + - ` parentElement ` Object - 指定查找窗口的父级节点。 默认为 `document.body`。 + - ` duration ` Number - 指定查找窗口显示或隐藏的过渡周期。 默认为 `300` (ms)。 + - ` offsetTop ` Number - 指定查找窗口相对于父级定位元素顶部偏移量。 默认为 `5`。 + - ` offsetRight ` Number - 指定查找窗口相对于父级定位元素右边偏移量。 默认为 `5`。 + - ` boxBgColor ` String - 配置查找窗口背景色。 默认为 `"#ffffff"`。 + - ` boxShadowColor ` String - 配置查找窗口阴影色。 默认为 `"#909399"`。 + - ` inputColor ` String - 配置输入框文本颜色。 默认为 "#606266"。 + - ` inputBgColor ` String - 配置输入框背景颜色。 默认为 `"#f0f0f0"`。 + - ` inputFocusColor ` String - 配置输入框聚焦时的边框颜色。 默认为 `"#c5ade0"`。 + - ` textColor ` String - 配置查找窗口中文本颜色。 默认为 `"#606266"`。 + - ` textHoverBgColor ` String - 配置鼠标悬停文本时的背景色。 默认为 `"#eaeaea"`。 + - ` caseSelectedColor ` String - 配置区分大小写选项选中时的边框颜色。 默认为 `"#c5ade0"`。 + + ### Instance Methods + 使用new FindInPage 创建的实例具有以下方法: +   + ` findInPage.openFindWindow() ` + 当查找窗口关闭时,打开窗口。 当查找窗口已经打开时,聚焦输入框。 +   + ` findInPage.closeFindWindow() ` + 关闭窗口。 +   + ` findInPage.destroy() ` + 关闭窗口,清除对象的引用,释放内存。 + + + + + + diff --git a/app/node_modules/electron-find/example/example.css b/app/node_modules/electron-find/example/example.css new file mode 100644 index 00000000..d128d7ae --- /dev/null +++ b/app/node_modules/electron-find/example/example.css @@ -0,0 +1,14 @@ +html, +body{ + margin: 0; + padding: 0; + box-sizing: border-box; + overflow: auto; +} +section{ + padding: 10px 20px; +} +h3{ + margin-top: 30px; +} + diff --git a/app/node_modules/electron-find/example/example.js b/app/node_modules/electron-find/example/example.js new file mode 100644 index 00000000..c858d23a --- /dev/null +++ b/app/node_modules/electron-find/example/example.js @@ -0,0 +1,26 @@ +const { remote, ipcRenderer } = require('electron') +const { FindInPage } = require('../src/index.js') + + +let findInPage = new FindInPage(remote.getCurrentWebContents(), { + preload: true, + offsetTop: 6, + offsetRight: 10 +}) + +// let findInPage = new FindInPage(remote.getCurrentWebContents(), { +// boxBgColor: '#333', +// boxShadowColor: '#000', +// inputColor: '#aaa', +// inputBgColor: '#222', +// inputFocusColor: '#555', +// textColor: '#aaa', +// textHoverBgColor: '#555', +// caseSelectedColor: '#555', +// offsetTop: 8, +// offsetRight: 12 +// }) + +ipcRenderer.on('on-find', (e, args) => { + findInPage.openFindWindow() +}) diff --git a/app/node_modules/electron-find/example/index.html b/app/node_modules/electron-find/example/index.html new file mode 100644 index 00000000..766b7d36 --- /dev/null +++ b/app/node_modules/electron-find/example/index.html @@ -0,0 +1,54 @@ + + + + + Example + + + +
+

关于 Electron

+

+ Electron是由Github开发,用HTML,CSS和JavaScript来构建跨平台桌面应用程序的一个开源库。 +

+

+ Electron通过将Chromium和Node.js合并到同一个运行时环境中,并将其打包为Mac,Windows和Linux系统下的应用来实现这一目的。 +

+

+ Electron于2013年作为构建Github上可编程的文本编辑器Atom的框架而被开发出来。这两个项目在2014春季开源。 +

+

+ 目前它已成为开源开发者、初创企业和老牌公司常用的开发工具。 +

+ +

长期支持

+

+ 当前并不存在对Electron旧版本的长期支持 +

+

+ 如果现在你使用的Electron版本跑得不错,你就可以一直使用这个版本。 +

+

+ 如果你想使用新发布的特性,那就升级到更新的版本。 +

+

+ 版本v1.0.0发布了重大的更新。 如果你现在没有在用这个版本,你应该了解更多关于v1.0.0的改变。 +

+ +

核心理念

+

+ 为了保持Electron的小巧 (文件体积) 和可持续性开发 (以防依赖库和API的泛滥) ,Electron限制了所使用的核心项目的数量。 +

+

+ 比如Electron只用了Chromium的渲染库而不是其全部组件。 +

+

+ 这使得升级Chromium更加容易,但也意味着Electron缺少了Google Chrome里的一些浏览器相关的特性 +

+

+ 添加到Electron的新功能应该主要是原生 API。 如果可以的话,一个功能应该尽可能的成为一个Node.js模块。 +

+
+ + + diff --git a/app/node_modules/electron-find/example/main.js b/app/node_modules/electron-find/example/main.js new file mode 100644 index 00000000..32fdf376 --- /dev/null +++ b/app/node_modules/electron-find/example/main.js @@ -0,0 +1,47 @@ +const electron = require('electron') +const { app, BrowserWindow, globalShortcut } = electron +const path = require('path') +let win +const winURL = 'file://' + path.normalize(`${__dirname}/index.html`) + +function createWindow () { + win = new BrowserWindow({ + width: 1280, + height: 1040, + center: false, + webPreferences: { + nodeIntegration: true, + plugins: true, + } + }) + win.loadURL(winURL) + //win.webContents.openDevTools() + win.on('closed', () => { + win = null + }) + + win.on('focus', () => { + globalShortcut.register('CommandOrControl+F', function () { + if (win && win.webContents) { + win.webContents.send('on-find', '') + } + }) + }) + win.on('blur', () => { + globalShortcut.unregister('CommandOrControl+F') + }) + +} + +app.on('ready', createWindow) +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } + globalShortcut.unregister('CommandOrControl+F') +}) + +app.on('activate', () => { + if (win === null) createWindow() +}) + diff --git a/app/node_modules/electron-find/example2/inner1.html b/app/node_modules/electron-find/example2/inner1.html new file mode 100644 index 00000000..e96d9823 --- /dev/null +++ b/app/node_modules/electron-find/example2/inner1.html @@ -0,0 +1,22 @@ + + + + + webview 1 + + + +
+

In webview 1

+
content 1
+
content 1
+
+ + diff --git a/app/node_modules/electron-find/example2/inner2.html b/app/node_modules/electron-find/example2/inner2.html new file mode 100644 index 00000000..0403a381 --- /dev/null +++ b/app/node_modules/electron-find/example2/inner2.html @@ -0,0 +1,22 @@ + + + + + webview 2 + + + +
+

In webview 2

+
content 2
+
content 2
+
+ + diff --git a/app/node_modules/electron-find/example2/main.js b/app/node_modules/electron-find/example2/main.js new file mode 100644 index 00000000..324a80a2 --- /dev/null +++ b/app/node_modules/electron-find/example2/main.js @@ -0,0 +1,47 @@ +const electron = require('electron') +const { app, BrowserWindow, globalShortcut } = electron +const path = require('path') +let win +const winURL = 'file://' + path.normalize(`${__dirname}/outer.html`) + +function createWindow () { + win = new BrowserWindow({ + width: 1280, + height: 1040, + center: false, + webPreferences: { + nodeIntegration: true, + plugins: true, + } + }) + win.loadURL(winURL) + //win.webContents.openDevTools() + win.on('closed', () => { + win = null + }) + + win.on('focus', () => { + globalShortcut.register('CommandOrControl+F', function () { + if (win && win.webContents) { + win.webContents.send('on-find', '') + } + }) + }) + win.on('blur', () => { + globalShortcut.unregister('CommandOrControl+F') + }) + +} + +app.on('ready', createWindow) +app.on('window-all-closed', () => { + if (process.platform !== 'darwin') { + app.quit() + } + globalShortcut.unregister('CommandOrControl+F') +}) + +app.on('activate', () => { + if (win === null) createWindow() +}) + diff --git a/app/node_modules/electron-find/example2/outer.html b/app/node_modules/electron-find/example2/outer.html new file mode 100644 index 00000000..dbb7d3a7 --- /dev/null +++ b/app/node_modules/electron-find/example2/outer.html @@ -0,0 +1,27 @@ + + + + + Outer + + + +
+
+

Is in app

+
Main content
+
+
+ + +
+ + + diff --git a/app/node_modules/electron-find/example2/outer.js b/app/node_modules/electron-find/example2/outer.js new file mode 100644 index 00000000..bc780e36 --- /dev/null +++ b/app/node_modules/electron-find/example2/outer.js @@ -0,0 +1,37 @@ +const { remote, ipcRenderer } = require('electron') +const { FindInPage } = require('../src/index.js') + +let findInPage = null +const webview1 = document.querySelector('#webview1') +webview1.addEventListener('dom-ready', () => { + findInPage = new FindInPage(webview1.getWebContents()) + ipcRenderer.on('on-find', (e, args) => { + findInPage.openFindWindow() + }) +}) +webview1.addEventListener('close', () => { + console.log('webview1 close', ) + if (findInPage) { + findInPage.destroy() + findInPage = null + } +}) +webview1.addEventListener('destroyed', () => { + console.log('webview1 destroyed', ) + if (findInPage) { + findInPage.destroy() + findInPage = null + } +}) + +webview1.addEventListener('crashed', () => { + console.log('webview1 crashed', ) + if (findInPage) { + findInPage.destroy() + findInPage = null + } +}) + +ipcRenderer.on('on-find', (e, args) => { + findInPage ? findInPage.openFindWindow() : '' +}) diff --git a/app/node_modules/electron-find/find.gif b/app/node_modules/electron-find/find.gif new file mode 100644 index 00000000..02cd873e Binary files /dev/null and b/app/node_modules/electron-find/find.gif differ diff --git a/app/node_modules/electron-find/find2.png b/app/node_modules/electron-find/find2.png new file mode 100644 index 00000000..213a63f2 Binary files /dev/null and b/app/node_modules/electron-find/find2.png differ diff --git a/app/node_modules/electron-find/package.json b/app/node_modules/electron-find/package.json new file mode 100644 index 00000000..88073b26 --- /dev/null +++ b/app/node_modules/electron-find/package.json @@ -0,0 +1,57 @@ +{ + "_from": "electron-find", + "_id": "electron-find@1.0.6", + "_inBundle": false, + "_integrity": "sha512-RenjzlCCzX7edLywLy+qRYvzds11sBv8+SrJu/3l3eVLt9d9uNqCPk+uFZ525uAhSUaUalgZWDlhQdxIgT1khg==", + "_location": "/electron-find", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "electron-find", + "name": "electron-find", + "escapedName": "electron-find", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER", + "/" + ], + "_resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.6.tgz", + "_shasum": "e10e4be3eb0b634ed0d3acf383720fef39698cdb", + "_spec": "electron-find", + "_where": "F:\\projects\\p\\gitlit\\app", + "author": { + "name": "TheoXiong" + }, + "bugs": { + "url": "https://github.com/TheoXiong/electron-find/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Find all matches for the text in electron app", + "devDependencies": { + "electron": "^2.0.0" + }, + "homepage": "https://github.com/TheoXiong/electron-find#readme", + "keywords": [ + "find", + "electron", + "search", + "match" + ], + "license": "MIT", + "main": "src/index.js", + "name": "electron-find", + "repository": { + "type": "git", + "url": "git+https://github.com/TheoXiong/electron-find.git" + }, + "scripts": { + "e": "electron example/main.js", + "e2": "electron example2/main.js" + }, + "version": "1.0.6" +} diff --git a/app/node_modules/electron-find/src/find.js b/app/node_modules/electron-find/src/find.js new file mode 100644 index 00000000..3e5dab89 --- /dev/null +++ b/app/node_modules/electron-find/src/find.js @@ -0,0 +1,93 @@ +const EventEmitter = require('events') +const { print } = require('./utils.js') + +const stopActions = ['clearSelection', 'keepSelection', 'activateSelection'] +const wcs = Symbol('webContents') +const opts = Symbol('options') +const requestId = Symbol('requestId') +const activeMatch = Symbol('activeMatch') +const matches = Symbol('matches') +const initd = Symbol('initd') +const preText = Symbol('preText') + +class Find extends EventEmitter { + constructor (webContents, options = {}) { + super() + this[wcs] = webContents + this[opts] = options + this[requestId] = null + this[activeMatch] = 0 + this[matches] = 0 + this[initd] = false + this[preText] = '' + } + initFind () { + if (this[initd]) return false + if (isWebContents.call(this)) { + bindFound.call(this) + return this[initd] = true + } else { + throw new Error('[Find] In need of a valid webContents !') + } + } + destroyFind () { + this[wcs] = null + this[opts] = null + this[requestId] = null + this[activeMatch] = 0 + this[matches] = 0 + this[initd] = false + this[preText] = '' + } + isFinding () { + return !!this[requestId] + } + startFind (text = '', forward = true, matchCase = false) { + if (!text) return + this[activeMatch] = 0 + this[matches] = 0 + this[preText] = text + this[requestId] = this[wcs].findInPage(this[preText], { + forward, + matchCase + }) + print(`[Find] startFind text=${text} forward=${forward} matchCase=${matchCase}`) + } + findNext (forward, matchCase = false) { + if (!this.isFinding()) throw new Error('Finding did not start yet !') + this[requestId] = this[wcs].findInPage(this[preText], { + forward, + matchCase, + findNext: true + }) + print(`[Find] findNext text=${this[preText]} forward=${forward} matchCase=${matchCase}`) + } + stopFind (action) { + stopActions.includes(action) ? '' : action = 'clearSelection' + this[wcs].stopFindInPage(action) + print(`[Find] stopFind action=${action}`) + } +} +function isWebContents () { + return (this[wcs] && + typeof this[wcs].findInPage === 'function' && + typeof this[wcs].stopFindInPage === 'function') +} +function bindFound () { + this[wcs].on('found-in-page', (e, r) => { + onFoundInPage.call(this, r) + }) +} +function onFoundInPage (result) { + print('[Find] onFoundInPage, ', result) + if (this[requestId] !== result.requestId) return + typeof result.activeMatchOrdinal === 'number' ? this[activeMatch] = result.activeMatchOrdinal : '' + typeof result.matches === 'number' ? this[matches] = result.matches : '' + result.finalUpdate ? reportResult.call(this) : '' +} +function reportResult () { + this.emit('result', this[activeMatch], this[matches]) + typeof this[opts].onResult === 'function' ? this[opts].onResult(this[activeMatch], this[matches]) : '' +} + +module.exports = Find diff --git a/app/node_modules/electron-find/src/findInPage.js b/app/node_modules/electron-find/src/findInPage.js new file mode 100644 index 00000000..0f5ec221 --- /dev/null +++ b/app/node_modules/electron-find/src/findInPage.js @@ -0,0 +1,470 @@ +const Find = require('./find.js') +const { print, on, off, move } = require('./utils.js') + +const INPUT_INTERVAL_THRESHOLD = 360 + +const findBox = Symbol('findBox') +const findInput = Symbol('findInput') +const findMatches = Symbol('findMatches') +const findCase = Symbol('findCase') +const findBack = Symbol('findBack') +const findForward = Symbol('findForward') +const findClose = Symbol('findClose') +const hasOpened = Symbol('hasOpened') +const matchCase = Symbol('matchCase') + +const documentKeydown = Symbol('documentKeydown') +const inputFocus = Symbol('inputFocus') +const inputBlur = Symbol('inputBlur') +const inputEvent = Symbol('inputEvent') +const compositionstart = Symbol('compositionstart') +const compositionend = Symbol('compositionend') +const caseMouseenter = Symbol('caseMouseenter') +const caseMouseleave = Symbol('caseMouseleave') +const caseClick = Symbol('caseClick') +const backMouseenter = Symbol('backMouseenter') +const backMouseleave = Symbol('backMouseleave') +const backClick = Symbol('backClick') +const forwardMouseenter = Symbol('forwardMouseenter') +const forwardMouseleave = Symbol('forwardMouseleave') +const forwardClick = Symbol('forwardClick') +const closeMouseenter = Symbol('closeMouseenter') +const closeMouseleave = Symbol('closeMouseleave') +const closeClick = Symbol('closeClick') +const events = Symbol('events') + +const inComposition = Symbol('inComposition') +const action = Symbol('action') +const lastText = Symbol('lastText') +const inputCnt = Symbol('inputCnt') +const initialized = Symbol('initialized') +const config = Symbol('config') + +class FindInPage extends Find{ + constructor (webContents, options = {}) { + super(webContents) + this[findBox] = null + this[findInput] = null + this[findMatches] = null + this[findCase] = null + this[findBack] = null + this[findForward] = null + this[findClose] = null + this[hasOpened] = false + this[matchCase] = false + this[inComposition] = false + this[action] = '' + this[lastText] = '' + this[inputCnt] = 0 + this[initialized] = false + this[config] = {} + this[events] = [] + this.parentElement = options.parentElement ? options.parentElement : document.body + this.duration = (typeof options.duration === 'number' && options.duration > 0) ? options.duration : 300 + this.options = options + this.options.preload ? this.initialize() : '' + } + initialize () { + if (this[initialized]) { + print('[FindInPage] Has initialize.') + return true + } + if (!this.initFind()) { + print('[FindInPage] Failed to initialize.') + return false + } + this[findBox] = creatElement('find-box') + this[findInput] = creatElement('find-input', 'input') + this[findMatches] = creatElement('find-matches') + this[findCase] = creatElement('find-case') + this[findBack] = creatElement('find-back') + this[findForward] = creatElement('find-forward') + this[findClose] = creatElement('find-close') + getUserConfig.call(this, this.options) + setBoxStyle.call(this) + setInputStyle.call(this) + setMatchesStyle.call(this) + setCaseStyle.call(this) + setBackStyle.call(this) + setForwardStyle.call(this) + setCloseStyle.call(this) + lockNext.call(this) + creatEventHandler.call(this) + bindEvents.call(this) + appendElement.call(this) + onResult.call(this) + move(this[findBox], (0 - this[findBox].offsetHeight - 10), this.duration) + return this[initialized] = true + } + openFindWindow () { + if (this[hasOpened]) { + focusInput.call(this) + return false + } + if (!this.initialize()) return false + setTimeout(() => { + this[findBox].style['visibility'] = 'visible' + lockNext.call(this) + focusInput.call(this) + }, 10) + move(this[findBox], parseInt(this[config].offsetTop), this.duration) + .then(() => {}) + .catch(err => { throw err }) + return this[hasOpened] = true + } + closeFindWindow () { + if (!this[hasOpened]) return false + this[findInput].value = '' + this[action] = '' + this[lastText] = '' + this[findMatches].innerText = '0/0' + this[hasOpened] = false + lockNext.call(this) + move(this[findBox], (0 - this[findBox].offsetHeight - 10), this.duration) + .then(() => { this[findBox].style['visibility'] = 'hidden' }) + .catch(err => { throw err }) + return true + } + destroy () { + this.destroyFind() + unbindEvents.call(this) + this.closeFindWindow() + removeElement.call(this) + } +} + +function creatElement (className = '', tag = 'div') { + const ele = document.createElement(tag) + ele.classList.add(className) + return ele +} +function getUserConfig (options) { + this[config].offsetTop = typeof options.offsetTop === 'number' ? `${options.offsetTop}px` : '5px' + this[config].offsetRight = typeof options.offsetRight === 'number' ? `${options.offsetRight}px` : '5px' + this[config].boxBgColor = typeof options.boxBgColor === 'string' ? options.boxBgColor : '#fff' + this[config].boxShadowColor = typeof options.boxShadowColor === 'string' ? options.boxShadowColor : '#909399' + this[config].inputColor = typeof options.inputColor === 'string' ? options.inputColor : '#606266' + this[config].inputBgColor = typeof options.inputBgColor === 'string' ? options.inputBgColor : '#f0f0f0' + this[config].inputFocusColor = typeof options.inputFocusColor === 'string' ? options.inputFocusColor : '#c5ade0' + this[config].textColor = typeof options.textColor === 'string' ? options.textColor : '#606266' + this[config].textHoverBgColor = typeof options.textHoverBgColor === 'string' ? options.textHoverBgColor : '#eaeaea' + this[config].caseSelectedColor = typeof options.caseSelectedColor === 'string' ? options.caseSelectedColor : '#c5ade0' +} +function setBoxStyle () { + this[findBox].style.cssText = `position:fixed; top:-110%; z-index: 3001; max-height:48px; min-height:30px; + right:${this[config].offsetRight}; display:flex; align-items:center; box-sizing:border-box !important; + padding:6px; visibility: hidden; background:${this[config].boxBgColor}; + box-shadow: 1px 1px 2px 0.5px ${this[config].boxShadowColor};` +} +function setInputStyle () { + this[findInput].style.cssText = `width:168px; outline:0; border:1px solid ${this[config].inputBgColor}; + background:${this[config].inputBgColor}; margin-right:6px; border-radius:2px; color:${this[config].inputColor}` +} +function setMatchesStyle () { + this[findMatches].innerText = '0/0' + this[findMatches].style.cssText = `color:${this[config].textColor}; font-size:14px; display:flex; align-items:center; + justify-content:center; min-width:40px; max-width:64px; overflow:hidden; margin-right:4px;` +} +function setCaseStyle () { + this[findCase].innerText = 'Aa' + this[findCase].style.cssText = `font-size:14px; font-weight:700; cursor:pointer; -webkit-user-select:none; color:${this[config].textColor}; + padding:0px 2px; border-radius:2px; border:1px solid transparent; margin-right:4px; display:flex; align-items:center;` +} +function setBackStyle () { + this[findBack].style.cssText = `cursor:pointer; -webkit-user-select:none; position: relative; height: 20px; width: 20px; border-radius:2px; + overflow: hidden; display: inline-block; background:${this[config].boxBgColor}; border:0px solid ${this[config].boxBgColor};` + + let backLine = creatElement('find-back-line') + backLine.style.cssText = `width:0; height:0; border:7px solid transparent; border-right-color:${this[config].textColor}; + position: absolute; top:3px; left:-1px;` + this[findBack].appendChild(backLine) + + let backCover = creatElement('find-back-cover') + backCover.style.cssText = `width:0; height:0; border:7px solid transparent; border-right-color:inherit; + position: absolute; top:3px; left:2px; z-index:1001;` + this[findBack].appendChild(backCover) +} +function setForwardStyle () { + this[findForward].style.cssText = `cursor:pointer; -webkit-user-select:none; position: relative; height: 20px; width: 20px; border-radius:2px; + overflow: hidden; display: inline-block; background:${this[config].boxBgColor}; border:0px solid ${this[config].boxBgColor};` + + let forwardLine = creatElement('find-forward-line') + forwardLine.style.cssText = `width:0; height:0; border:7px solid transparent; border-left-color:${this[config].textColor}; + position: absolute; top:3px; left:6px;` + this[findForward].appendChild(forwardLine) + + let forwardCover = creatElement('find-forward-cover') + forwardCover.style.cssText = `width:0; height:0; border:7px solid transparent; border-left-color:inherit; + position: absolute; top:3px; left:3px; z-index:1001;` + this[findForward].appendChild(forwardCover) +} +function setCloseStyle () { + this[findClose].style.cssText = `cursor:pointer; -webkit-user-select:none; position: relative; height: 20px; width: 20px; + overflow: hidden; display: inline-block; background:${this[config].boxBgColor}; border-radius:2px;` + + let closeInner1 = creatElement('find-close-inner1') + closeInner1.style.cssText = `width:14px; height:2px; background:${this[config].textColor}; transform:rotate(45deg); + position: absolute; top:9px; left:3px;` + this[findClose].appendChild(closeInner1) + + let closeInner2 = creatElement('find-close-inner2') + closeInner2.style.cssText = `width:14px; height:2px; background:${this[config].textColor}; transform:rotate(-45deg); + position: absolute; top:9px; left:3px;` + this[findClose].appendChild(closeInner2) +} +function appendElement () { + [this[findInput], this[findMatches], this[findCase], this[findBack], this[findForward], this[findClose]].forEach((item) => { + this[findBox].appendChild(item) + }) + this.parentElement.appendChild(this[findBox]) +} +function removeElement () { + this.parentElement.removeChild(this[findBox]) +} +function creatEventHandler () { + this[documentKeydown] = (function (e) { + if (!this[hasOpened]) return + onKeydown.call(this, e) + }).bind(this) + this[events].push({ ele: document, name: 'keydown', fn: this[documentKeydown] }) + + this[inputFocus] = (function () { + this[findInput].style.border = `1px solid ${this[config].inputFocusColor}` + }).bind(this) + this[events].push({ ele: this[findInput], name: 'focus', fn: this[inputFocus] }) + + this[inputBlur] = (function () { + this[findInput].style.border = `1px solid ${this[config].inputBgColor}` + }).bind(this) + this[events].push({ ele: this[findInput], name: 'blur', fn: this[inputBlur] }) + + this[inputEvent] = (function () { + updateCnt.call(this) + isInputing.call(this) + .then(res => { + res ? '' : onInput.call(this) + }) + }).bind(this) + this[events].push({ ele: this[findInput], name: 'input', fn: this[inputEvent] }) + + this[compositionstart] = (function () { + print('compositionstart') + this[inComposition] = true + }).bind(this) + this[events].push({ ele: this[findInput], name: 'compositionstart', fn: this[compositionstart] }) + + this[compositionend] = (function () { + print('compositionend') + this[inComposition] = false + }).bind(this) + this[events].push({ ele: this[findInput], name: 'compositionend', fn: this[compositionend] }) + + this[caseMouseenter] = (function () { + this[findCase].style['background'] = this[config].textHoverBgColor + }).bind(this) + this[events].push({ ele: this[findCase], name: 'mouseenter', fn: this[caseMouseenter] }) + + this[caseMouseleave] = (function () { + this[findCase].style['background'] = this[config].boxBgColor + }).bind(this) + this[events].push({ ele: this[findCase], name: 'mouseleave', fn: this[caseMouseleave] }) + + this[caseClick] = (function () { + onCaseClick.call(this) + }).bind(this) + this[events].push({ ele: this[findCase], name: 'click', fn: this[caseClick] }) + + this[backMouseenter] = (function () { + this[findBack].style['background'] = this[config].textHoverBgColor + this[findBack].style['border'] = `0px solid ${this[config].textHoverBgColor}` + }).bind(this) + this[events].push({ ele: this[findBack], name: 'mouseenter', fn: this[backMouseenter] }) + + this[backMouseleave] = (function () { + this[findBack].style['background'] = this[config].boxBgColor + this[findBack].style['border'] = `0px solid ${this[config].boxBgColor}` + }).bind(this) + this[events].push({ ele: this[findBack], name: 'mouseleave', fn: this[backMouseleave] }) + + this[backClick] = (function () { + onBackClick.call(this) + }).bind(this) + this[events].push({ ele: this[findBack], name: 'click', fn: this[backClick] }) + + this[forwardMouseenter] = (function () { + this[findForward].style['background'] = this[config].textHoverBgColor + this[findForward].style['border'] = `0px solid ${this[config].textHoverBgColor}` + }).bind(this) + this[events].push({ ele: this[findForward], name: 'mouseenter', fn: this[forwardMouseenter] }) + + this[forwardMouseleave] = (function () { + this[findForward].style['background'] = this[config].boxBgColor + this[findForward].style['border'] = `0px solid ${this[config].boxBgColor}` + }).bind(this) + this[events].push({ ele: this[findForward], name: 'mouseleave', fn: this[forwardMouseleave] }) + + this[forwardClick] = (function () { + onForwardClick.call(this) + }).bind(this) + this[events].push({ ele: this[findForward], name: 'click', fn: this[forwardClick] }) + + this[closeMouseenter] = (function () { + this[findClose].style['background'] = this[config].textHoverBgColor + }).bind(this) + this[events].push({ ele: this[findClose], name: 'mouseenter', fn: this[closeMouseenter] }) + + this[closeMouseleave] = (function () { + this[findClose].style['background'] = this[config].boxBgColor + }).bind(this) + this[events].push({ ele: this[findClose], name: 'mouseleave', fn: this[closeMouseleave] }) + + this[closeClick] = (function () { + onCloseClick.call(this) + }).bind(this) + this[events].push({ ele: this[findClose], name: 'click', fn: this[closeClick] }) +} + +function bindEvents () { + this[events].forEach((item) => { + on(item.ele, item.name, item.fn) + }) +} +function unbindEvents () { + this[events].forEach((item) => { + off(item.ele, item.name, item.fn) + }) +} + +function updateCnt () { + if (this[inputCnt] >= 0xFFFFFFFE) { + this[inputCnt] = 0 + } + this[inputCnt]++ +} + +function isInputing () { + return new Promise((resolve, reject) => { + let currCnt = this[inputCnt] + setTimeout(() => { + currCnt !== this[inputCnt] ? resolve(true) : resolve(false) + }, INPUT_INTERVAL_THRESHOLD) + }) +} + +function focusInput (doBlur = false) { + setTimeout(() => { + doBlur ? this[findInput].blur() : '' + this[findInput].focus() + }, 50) +} + +function wrapInput (inputEle, caseEle, timeout = 50) { + inputEle.type = 'password' + caseEle.style['visibility'] = 'hidden' + + setTimeout(() => { + if (inputEle.type !== 'text') { + print('[FindInPage] wrapInput timeout..') + unwrapInput(inputEle, caseEle) + } + }, timeout) +} +function unwrapInput (inputEle, caseEle) { + inputEle.type = 'text' + caseEle.style['visibility'] = 'visible' +} + +function onInput () { + setTimeout(() => { + if (this[inComposition]) return + this[action] = 'input' + let text = this[findInput].value + if (text && text !== this[lastText]) { + this[lastText] = text + wrapInput(this[findInput], this[findCase], 100) + this.startFind(text, true, this[matchCase]) + } else if (this[lastText] && text === '') { + this.stopFind() + this[findMatches].innerText = '0/0' + lockNext.call(this) + focusInput.call(this, true) + } + }, 50) +} + +function onKeydown (e) { + if (this[inComposition] || !e) return + switch (e.code) { + case 'Enter': + case 'NumpadEnter': + let text = this[findInput].value + if (!text) return + e.shiftKey ? findKeep.call(this, false) : findKeep.call(this, true) + break + case 'Escape': + onCloseClick.call(this) + break + default: + break + } +} + +function findKeep (forward) { + if (!this.isFinding()) return + forward ? onForwardClick.call(this) : onBackClick.call(this) +} + +function onCaseClick () { + if (!this[matchCase]) { + this[matchCase] = true + this[findCase].style['border-color'] = this[config].caseSelectedColor + wrapInput(this[findInput], this[findCase], 100) + this.startFind(this[findInput].value, true, this[matchCase]) + } else { + this[matchCase] = false + this[findCase].style['border-color'] = 'transparent' + wrapInput(this[findInput], this[findCase], 100) + this.startFind(this[findInput].value, true, this[matchCase]) + } +} + +function onBackClick () { + this[action] = 'back' + wrapInput(this[findInput], this[findCase], 100) + this.findNext(false, this[matchCase]) +} + +function onForwardClick () { + this[action] = 'forward' + wrapInput(this[findInput], this[findCase], 100) + this.findNext(true, this[matchCase]) +} + +function onCloseClick () { + this.closeFindWindow() ? this.stopFind() : '' +} + +function onResult () { + this.on('result', (activeMatch, matches) => { + unwrapInput(this[findInput], this[findCase]) + this[findMatches].innerText = `${activeMatch}/${matches}` + matches > 0 ? unlockNext.call(this) : lockNext.call(this) + this[action] === 'input' ? focusInput.call(this) : '' + }) +} + +function lockNext () { + this[findBack].style['opacity'] = 0.6 + this[findBack].style['pointer-events'] = 'none' + this[findForward].style['opacity'] = 0.6 + this[findForward].style['pointer-events'] = 'none' +} + +function unlockNext () { + this[findBack].style['opacity'] = 1 + this[findBack].style['pointer-events'] = 'auto' + this[findForward].style['opacity'] = 1 + this[findForward].style['pointer-events'] = 'auto' +} + +module.exports = FindInPage diff --git a/app/node_modules/electron-find/src/index.js b/app/node_modules/electron-find/src/index.js new file mode 100644 index 00000000..84d03064 --- /dev/null +++ b/app/node_modules/electron-find/src/index.js @@ -0,0 +1,8 @@ +const FindInPage =require('./findInPage') +const Find = require('./find.js') + + +module.exports = { + FindInPage, + Find +} \ No newline at end of file diff --git a/app/node_modules/electron-find/src/utils.js b/app/node_modules/electron-find/src/utils.js new file mode 100644 index 00000000..9bf44006 --- /dev/null +++ b/app/node_modules/electron-find/src/utils.js @@ -0,0 +1,107 @@ +const debug = false +const print = debug ? console.log.bind(console) : () => {} + +const on = (() => { + if (document && document.addEventListener) { + return (element, event, handler) => { + if (element && event && handler) { + element.addEventListener(event, handler, false) + } + } + } else if (document && document.attachEvent) { + return (element, event, handler) => { + if (element && event && handler) { + element.attachEvent(`on${event}`, handler) + } + } + } else { + return () => {} + } +})() + +const off = (() => { + if (document && document.removeEventListener) { + return (element, event, handler) => { + if (element && event && handler) { + element.removeEventListener(event, handler, false) + } + } + } else if (document && document.detachEvent) { + return (element, event, handler) => { + if (element && event && handler) { + element.detachEvent(`on${event}`, handler) + } + } + } else { + return () => {} + } +})() + +const once = (element, event, fn) => { + let listener = function () { + if (typeof fn === 'function') { + fn.apply(this, arguments) + } + off(element, event, listener) + } + on(element, event, listener) +} + +window.requestAnimationFrame = window.requestAnimationFrame || + window.webkitRequestAnimationFrame || + window.mozRequestAnimationFrame || + window.msRequestAnimationFrame || + window.oRequestAnimationFrame || + function (callback) { + return window.setTimeout(callback, 1000 / 60) + } + +window.cancelAnimationFrame = window.cancelAnimationFrame || + window.webkitCancelAnimationFrame || + window.mozCancelAnimationFrame || + window.msCancelAnimationFrame || + window.oCancelAnimationFrame || + function (id) { + window.clearTimeout(id) + } + +const move = (element, end, duration = 300) => { + return new Promise((resolve, reject) => { + try { + let winFrameId = null + let stepTime = duration / (1000 / 60) + let curr = parseInt(element.style.top) + let stepDist = (end - curr) / stepTime + let stepCnt = 0 + + const step = function () { + curr += stepDist + stepCnt++ + if (stepCnt >= stepTime || (Math.abs(end - curr) <= (stepDist + 1))) { + element.style.top = `${end}px` + if (winFrameId) { + window.cancelAnimationFrame(winFrameId) + winFrameId = null + } + resolve() + } else { + element.style.top = `${curr}px` + winFrameId = window.requestAnimationFrame(step) + } + } + step() + } catch (error) { + reject(error) + } + }) + + +} + +module.exports = { + print, + on, + off, + once, + move +} diff --git a/app/package-lock.json b/app/package-lock.json index c804e903..a441629f 100644 --- a/app/package-lock.json +++ b/app/package-lock.json @@ -419,6 +419,11 @@ } } }, + "electron-find": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/electron-find/-/electron-find-1.0.6.tgz", + "integrity": "sha512-RenjzlCCzX7edLywLy+qRYvzds11sBv8+SrJu/3l3eVLt9d9uNqCPk+uFZ525uAhSUaUalgZWDlhQdxIgT1khg==" + }, "electron-is-accelerator": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/electron-is-accelerator/-/electron-is-accelerator-0.1.2.tgz", diff --git a/app/package.json b/app/package.json index 2e06f909..e523ed92 100644 --- a/app/package.json +++ b/app/package.json @@ -7,6 +7,7 @@ "animate.css": "^3.5.2", "bootstrap": "^4.1.3", "ejs": "^2.6.1", + "electron-find": "^1.0.6", "electron-localshortcut": "^3.1.0", "jquery": "^3.3.1", "material-design-icons": "^3.0.1",