From 980ffb45e927335b66ab950751707e283be48674 Mon Sep 17 00:00:00 2001 From: Araxeus Date: Tue, 6 Apr 2021 21:57:16 +0300 Subject: [PATCH] Create readme.md refactor and css fix xo --fix add inline doc fix typo --- index.js | 18 +- menu.js | 9 +- package.json | 12 +- plugins/in-app-menu/back.js | 9 +- providers/logger.js | 20 +- providers/prompt/custom-titlebar.js | 14 ++ providers/prompt/customTitlebar.js | 19 -- .../{darkPrompt.css => dark-prompt.css} | 4 +- providers/prompt/index.js | 73 +++--- providers/prompt/page/prompt.js | 232 ++++++++++-------- providers/prompt/readme.md | 79 ++++++ 11 files changed, 306 insertions(+), 183 deletions(-) create mode 100644 providers/prompt/custom-titlebar.js delete mode 100644 providers/prompt/customTitlebar.js rename providers/prompt/{darkPrompt.css => dark-prompt.css} (94%) create mode 100644 providers/prompt/readme.md diff --git a/index.js b/index.js index 9e8b7233..7fd88754 100644 --- a/index.js +++ b/index.js @@ -110,8 +110,8 @@ function createMainWindow() { titleBarStyle: useInlineMenu ? "hidden" : is.macOS() - ? "hiddenInset" - : "default", + ? "hiddenInset" + : "default", autoHideMenuBar: config.get("options.hideMenu"), }); if (windowPosition) { @@ -156,7 +156,7 @@ function createMainWindow() { let createdWindow = false; app.on("browser-window-created", (event, win) => { - //Ensure listeners aren't registered when creating input dialog + //Ensures listeners are registered only once if (createdWindow) { return; } @@ -172,7 +172,7 @@ app.on("browser-window-created", (event, win) => { frameProcessId, frameRoutingId, ) => { - let log = { + const log = { error: "did-fail-load", event, errorCode, @@ -183,7 +183,7 @@ app.on("browser-window-created", (event, win) => { frameRoutingId, }; if (is.dev()) { - console.log(log); + console.log(log.toString()); } win.webContents.send("log", log); win.webContents.loadFile(path.join(__dirname, "error.html")); @@ -306,13 +306,11 @@ app.on("ready", () => { } // Optimized for Mac OS X - if (is.macOS()) { - if (!config.get("options.appVisible")) { - app.dock.hide(); - } + if (is.macOS() && !config.get("options.appVisible")) { + app.dock.hide(); } - var forceQuit = false; + let forceQuit = false; app.on("before-quit", () => { forceQuit = true; }); diff --git a/menu.js b/menu.js index 3754d14c..90487970 100644 --- a/menu.js +++ b/menu.js @@ -12,6 +12,7 @@ const pluginEnabledMenu = (win, plugin, label = "", hasSubmenu = false) => ({ label: label || plugin, type: "checkbox", checked: config.plugins.isEnabled(plugin), + //Submenu check used in in-app-menu hasSubmenu: hasSubmenu || undefined, click: (item) => { if (item.checked) { @@ -321,13 +322,13 @@ function setProxy(item, win) { }, type: 'input', icon: iconPath, - customStylesheet: path.join(__dirname, "providers", "prompt", "darkPrompt.css"), + customStylesheet: path.join(__dirname, "providers", "prompt", "dark-prompt.css"), }; //TODO: custom bar on prompt need testing on macOS if(!is.macOS()) { Object.assign(options, { frame: false, - customScript: path.join(__dirname, "providers", "prompt", "customTitlebar.js"), + customScript: path.join(__dirname, "providers", "prompt", "custom-titlebar.js"), enableRemoteModule: true, height: 200, width: 450, @@ -337,10 +338,10 @@ function setProxy(item, win) { .then((input) => { if (input !== null && input !== example) { config.set("options.proxy", input); - item.checked = (input === "") ? false : true; + item.checked = input !== ""; } else { //user pressed cancel item.checked = !item.checked; //reset checkbox } }) .catch(console.error); -} \ No newline at end of file +} diff --git a/package.json b/package.json index 36591897..144ac53f 100644 --- a/package.json +++ b/package.json @@ -101,6 +101,16 @@ "envs": [ "node", "browser" - ] + ], + "rules": { + "quotes": [ + "error", + "double", + { + "avoidEscape": true, + "allowTemplateLiterals": true + } + ] + } } } diff --git a/plugins/in-app-menu/back.js b/plugins/in-app-menu/back.js index b17ee032..938c5475 100644 --- a/plugins/in-app-menu/back.js +++ b/plugins/in-app-menu/back.js @@ -9,8 +9,6 @@ const { injectCSS } = require("../utils"); //check that menu doesn't get created twice let calledReadyToShow = false; -//check menu state isn't changed twice -let calledFinishedLoad = false //tracks menu visibility let visible = true; // win hook for fixing menu @@ -37,6 +35,7 @@ module.exports = (winImport) => { if (calledReadyToShow) { return; } + calledReadyToShow = true; setApplicationMenu(win); @@ -53,7 +52,7 @@ module.exports = (winImport) => { win.webContents.on("did-finish-load", () => { // fix bug with menu not applying on start when no internet connection available setMenuVisibility(!config.get("options.hideMenu")); - }) + }); }; function switchMenuVisibility() { @@ -81,10 +80,10 @@ function updateTemplate(template) { for (let item of template) { // Change onClick of checkbox+radio if ((item.type === "checkbox" || item.type === "radio") && !item.fixed) { - let originalOnclick = item.click; + const originalOnclick = item.click; item.click = (itemClicked) => { originalOnclick(itemClicked); - updateCheckboxesAndRadioButtons(itemClicked, item.type === 'radio', item.hasSubmenu); + updateCheckboxesAndRadioButtons(itemClicked, item.type === "radio", item.hasSubmenu); }; item.fixed = true; } diff --git a/providers/logger.js b/providers/logger.js index 08a940f3..730a3dfa 100644 --- a/providers/logger.js +++ b/providers/logger.js @@ -1,11 +1,15 @@ const { ipcRenderer } = require("electron"); +function logToString(log) { + let string = (typeof log === "string") ? log : log.toString(); + if (!string || string.includes("[object Object]")) { + string = JSON.stringify(log); + } + return string; +} + module.exports = () => { - ipcRenderer.on("log", (event, log) => { - let string = log || log.toString(); - if (!string || string === "[object Object]") { - string = JSON.stringify(log); - } - console.log(string); - }) -}; \ No newline at end of file + ipcRenderer.on("log", (event, log) => { + console.log(logToString(log)); + }); +}; diff --git a/providers/prompt/custom-titlebar.js b/providers/prompt/custom-titlebar.js new file mode 100644 index 00000000..c36ce5f5 --- /dev/null +++ b/providers/prompt/custom-titlebar.js @@ -0,0 +1,14 @@ +const customTitlebar = require("custom-electron-titlebar"); + +module.exports = () => { + const bar = new customTitlebar.Titlebar({ + backgroundColor: customTitlebar.Color.fromHex("#050505"), + minimizable: false, + maximizable: false, + menu: null + }); + const mainStyle = document.querySelector("#container").style; + mainStyle.width = "100%"; + mainStyle.position = "fixed"; + mainStyle.border = "unset"; +}; diff --git a/providers/prompt/customTitlebar.js b/providers/prompt/customTitlebar.js deleted file mode 100644 index 116aa56c..00000000 --- a/providers/prompt/customTitlebar.js +++ /dev/null @@ -1,19 +0,0 @@ -const customTitlebar = require("custom-electron-titlebar"); - -module.exports = () => { - const bar = new customTitlebar.Titlebar({ - backgroundColor: customTitlebar.Color.fromHex("#050505"), - minimizable: false, - maximizable: false, - unfocusEffect: true, - }); - try { - bar.updateMenu(null); - } catch (e) { - //will always throw type error - null isn't menu, but it works - } - let container = document.querySelector('#container'); - container.style.width = '100%'; - container.style.position = 'fixed'; - container.style.border = 'unset'; -} \ No newline at end of file diff --git a/providers/prompt/darkPrompt.css b/providers/prompt/dark-prompt.css similarity index 94% rename from providers/prompt/darkPrompt.css rename to providers/prompt/dark-prompt.css index 8b343a25..49e0f88e 100644 --- a/providers/prompt/darkPrompt.css +++ b/providers/prompt/dark-prompt.css @@ -34,7 +34,9 @@ body { } #ok:hover, -#cancel:hover { +#ok:focus, +#cancel:hover, +#cancel:focus { outline: rgba(60, 0, 0, 0.4) solid 2px; } diff --git a/providers/prompt/index.js b/providers/prompt/index.js index d6664305..bbba8b78 100644 --- a/providers/prompt/index.js +++ b/providers/prompt/index.js @@ -1,17 +1,19 @@ -const electron = require('electron'); +const electron = require("electron"); const BrowserWindow = electron.BrowserWindow || electron.remote.BrowserWindow; const ipcMain = electron.ipcMain || electron.remote.ipcMain; -const url = require('url'); -const path = require('path'); +const url = require("url"); +const path = require("path"); const DEFAULT_WIDTH = 370; const DEFAULT_HEIGHT = 160; function electronPrompt(options, parentWindow) { return new Promise((resolve, reject) => { + //id used to ensure unique listeners per window const id = `${Date.now()}-${Math.random()}`; + //custom options override default const options_ = Object.assign( { width: DEFAULT_WIDTH, @@ -19,12 +21,12 @@ function electronPrompt(options, parentWindow) { minWidth: DEFAULT_WIDTH, minHeight: DEFAULT_HEIGHT, resizable: false, - title: 'Prompt', - label: 'Please input a value:', + title: "Prompt", + label: "Please input a value:", buttonLabels: null, alwaysOnTop: false, value: null, - type: 'input', + type: "input", selectOptions: null, icon: null, useHtmlLabel: false, @@ -38,7 +40,7 @@ function electronPrompt(options, parentWindow) { options || {} ); - if (options_.type === 'select' && (options_.selectOptions === null || typeof options_.selectOptions !== 'object')) { + if (options_.type === "select" && (options_.selectOptions === null || typeof options_.selectOptions !== "object")) { reject(new Error('"selectOptions" must be an object')); return; } @@ -50,9 +52,9 @@ function electronPrompt(options, parentWindow) { minWidth: options_.minWidth, minHeight: options_.minHeight, resizable: options_.resizable, - minimizable: false, - fullscreenable: false, - maximizable: false, + minimizable: !options_.skipTaskbar && !parentWindow && !options_.alwaysOnTop, + fullscreenable: options_.resizable, + maximizable: options_.resizable, parent: parentWindow, skipTaskbar: options_.skipTaskbar, alwaysOnTop: options_.alwaysOnTop, @@ -70,14 +72,11 @@ function electronPrompt(options, parentWindow) { promptWindow.setMenu(null); promptWindow.setMenuBarVisibility(options_.menuBarVisible); - const getOptionsListener = event => { - event.returnValue = JSON.stringify(options_); - }; - + //called on exit const cleanup = () => { - ipcMain.removeListener('prompt-get-options:' + id, getOptionsListener); - ipcMain.removeListener('prompt-post-data:' + id, postDataListener); - ipcMain.removeListener('prompt-error:' + id, errorListener); + ipcMain.removeListener("prompt-get-options:" + id, getOptionsListener); + ipcMain.removeListener("prompt-post-data:" + id, postDataListener); + ipcMain.removeListener("prompt-error:" + id, errorListener); if (promptWindow) { promptWindow.close(); @@ -85,6 +84,12 @@ function electronPrompt(options, parentWindow) { } }; + ///transfer options to front + const getOptionsListener = event => { + event.returnValue = JSON.stringify(options_); + }; + + //get input from front const postDataListener = (event, value) => { resolve(value); event.returnValue = null; @@ -92,34 +97,46 @@ function electronPrompt(options, parentWindow) { }; const unresponsiveListener = () => { - reject(new Error('Window was unresponsive')); + reject(new Error("Window was unresponsive")); cleanup(); }; + //get error from front const errorListener = (event, message) => { reject(new Error(message)); event.returnValue = null; cleanup(); }; - ipcMain.on('prompt-get-options:' + id, getOptionsListener); - ipcMain.on('prompt-post-data:' + id, postDataListener); - ipcMain.on('prompt-error:' + id, errorListener); - promptWindow.on('unresponsive', unresponsiveListener); + //attach listeners + ipcMain.on("prompt-get-options:" + id, getOptionsListener); + ipcMain.on("prompt-post-data:" + id, postDataListener); + ipcMain.on("prompt-error:" + id, errorListener); + promptWindow.on("unresponsive", unresponsiveListener); - promptWindow.on('closed', () => { + promptWindow.on("closed", () => { promptWindow = null; cleanup(); resolve(null); }); - const promptUrl = url.format({ - protocol: 'file', - slashes: true, - pathname: path.join(__dirname, 'page', 'prompt.html'), - hash: id + //should never happen + promptWindow.webContents.on("did-fail-load", ( + event, + errorCode, + errorDescription, + validatedURL, + ) => { + const log = { + error: "did-fail-load", + errorCode, + errorDescription, + validatedURL, + }; + reject(new Error("prompt.html did-fail-load, log:\n", + log.toString())); }); + //Finally, load prompt promptWindow.loadURL(promptUrl); }); } diff --git a/providers/prompt/page/prompt.js b/providers/prompt/page/prompt.js index 754d0e74..b7a54120 100644 --- a/providers/prompt/page/prompt.js +++ b/providers/prompt/page/prompt.js @@ -1,126 +1,47 @@ -const fs = require('fs'); -const {ipcRenderer} = require('electron'); -const docReady = require('doc-ready'); +const fs = require("fs"); +const {ipcRenderer} = require("electron"); +const docReady = require("doc-ready"); let promptId = null; let promptOptions = null; -function promptError(error) { - if (error instanceof Error) { - error = error.message; - } - - ipcRenderer.sendSync('prompt-error:' + promptId, error); -} - -function promptCancel() { - ipcRenderer.sendSync('prompt-post-data:' + promptId, null); -} - -function promptSubmit() { - const dataElement = document.querySelector('#data'); - let data = null; - - if (promptOptions.type === 'input') { - data = dataElement.value; - } else if (promptOptions.type === 'select') { - if (promptOptions.selectMultiple) { - data = dataElement.querySelectorAll('option[selected]').map(o => o.getAttribute('value')); - } else { - data = dataElement.value; - } - } - - ipcRenderer.sendSync('prompt-post-data:' + promptId, data); -} - -function promptCreateInput() { - const dataElement = document.createElement('input'); - dataElement.setAttribute('type', 'text'); - - if (promptOptions.value) { - dataElement.value = promptOptions.value; - } else { - dataElement.value = ''; - } - - if (promptOptions.inputAttrs && typeof (promptOptions.inputAttrs) === 'object') { - for (const k in promptOptions.inputAttrs) { - if (!Object.prototype.hasOwnProperty.call(promptOptions.inputAttrs, k)) { - continue; - } - - dataElement.setAttribute(k, promptOptions.inputAttrs[k]); - } - } - - dataElement.addEventListener('keyup', event => { - if (event.key === 'Escape') { - promptCancel(); - } - }); - - dataElement.addEventListener('keypress', event => { - if (event.key === 'Enter') { - event.preventDefault(); - document.querySelector('#ok').click(); - } - }); - - return dataElement; -} - -function promptCreateSelect() { - const dataElement = document.createElement('select'); - let optionElement; - - for (const k in promptOptions.selectOptions) { - if (!Object.prototype.hasOwnProperty.call(promptOptions.selectOptions, k)) { - continue; - } - - optionElement = document.createElement('option'); - optionElement.setAttribute('value', k); - optionElement.textContent = promptOptions.selectOptions[k]; - if (k === promptOptions.value) { - optionElement.setAttribute('selected', 'selected'); - } - - dataElement.append(optionElement); - } - - return dataElement; -} +docReady(promptRegister); +//start here function promptRegister() { - promptId = document.location.hash.replace('#', ''); + //get custom session id + promptId = document.location.hash.replace("#", ""); + //get options from back try { - promptOptions = JSON.parse(ipcRenderer.sendSync('prompt-get-options:' + promptId)); + promptOptions = JSON.parse(ipcRenderer.sendSync("prompt-get-options:" + promptId)); } catch (error) { return promptError(error); } + //set label if (promptOptions.useHtmlLabel) { - document.querySelector('#label').innerHTML = promptOptions.label; + document.querySelector("#label").innerHTML = promptOptions.label; } else { - document.querySelector('#label').textContent = promptOptions.label; + document.querySelector("#label").textContent = promptOptions.label; } + //set button label if (promptOptions.buttonLabels && promptOptions.buttonLabels.ok) { - document.querySelector('#ok').textContent = promptOptions.buttonLabels.ok; + document.querySelector("#ok").textContent = promptOptions.buttonLabels.ok; } if (promptOptions.buttonLabels && promptOptions.buttonLabels.cancel) { - document.querySelector('#cancel').textContent = promptOptions.buttonLabels.cancel; + document.querySelector("#cancel").textContent = promptOptions.buttonLabels.cancel; } + //inject custom stylesheet from options if (promptOptions.customStylesheet) { try { const customStyleContent = fs.readFileSync(promptOptions.customStylesheet); if (customStyleContent) { - const customStyle = document.createElement('style'); - customStyle.setAttribute('rel', 'stylesheet'); + const customStyle = document.createElement("style"); + customStyle.setAttribute("rel", "stylesheet"); customStyle.append(document.createTextNode(customStyleContent)); document.head.append(customStyle); } @@ -129,28 +50,31 @@ function promptRegister() { } } - document.querySelector('#form').addEventListener('submit', promptSubmit); - document.querySelector('#cancel').addEventListener('click', promptCancel); + //add button listeners + document.querySelector("#form").addEventListener("submit", promptSubmit); + document.querySelector("#cancel").addEventListener("click", promptCancel); - const dataContainerElement = document.querySelector('#data-container'); + //create input/select + const dataContainerElement = document.querySelector("#data-container"); let dataElement; - if (promptOptions.type === 'input') { + if (promptOptions.type === "input") { dataElement = promptCreateInput(); - } else if (promptOptions.type === 'select') { + } else if (promptOptions.type === "select") { dataElement = promptCreateSelect(); } else { return promptError(`Unhandled input type '${promptOptions.type}'`); } dataContainerElement.append(dataElement); - dataElement.setAttribute('id', 'data'); + dataElement.setAttribute("id", "data"); dataElement.focus(); - if (promptOptions.type === 'input') { + if (promptOptions.type === "input") { dataElement.select(); } + //load custom script from options if (promptOptions.customScript) { try { const customScript = require(promptOptions.customScript); @@ -161,10 +85,104 @@ function promptRegister() { } } -window.addEventListener('error', error => { +window.addEventListener("error", error => { if (promptId) { - promptError('An error has occured on the prompt window: \n' + error); + promptError("An error has occured on the prompt window: \n" + error); } }); -docReady(promptRegister); \ No newline at end of file +//send error to back +function promptError(error) { + if (error instanceof Error) { + error = error.message; + } + + ipcRenderer.sendSync("prompt-error:" + promptId, error); +} + +//send to back: input=null +function promptCancel() { + ipcRenderer.sendSync("prompt-post-data:" + promptId, null); +} + +//transfer input data to back +function promptSubmit() { + const dataElement = document.querySelector("#data"); + let data = null; + + if (promptOptions.type === "input") { + data = dataElement.value; + } else if (promptOptions.type === "select") { + data = promptOptions.selectMultiple ? + dataElement.querySelectorAll("option[selected]").map(o => o.getAttribute("value")) : + dataElement.value; + } + + ipcRenderer.sendSync("prompt-post-data:" + promptId, data); +} + +//creates input box +function promptCreateInput() { + const dataElement = document.createElement("input"); + dataElement.setAttribute("type", "text"); + + if (promptOptions.value) { + dataElement.value = promptOptions.value; + } else { + dataElement.value = ""; + } + + //insert custom input attributes if in options + if (promptOptions.inputAttrs && typeof (promptOptions.inputAttrs) === "object") { + for (const k in promptOptions.inputAttrs) { + if (!Object.prototype.hasOwnProperty.call(promptOptions.inputAttrs, k)) { + continue; + } + + dataElement.setAttribute(k, promptOptions.inputAttrs[k]); + } + } + + //Cancel/Exit on 'Escape' + dataElement.addEventListener("keyup", event => { + if (event.key === "Escape") { + promptCancel(); + } + }); + + //Confrim on 'Enter' + dataElement.addEventListener("keypress", event => { + if (event.key === "Enter") { + event.preventDefault(); + document.querySelector("#ok").click(); + } + }); + + return dataElement; +} + +//create multiple select +function promptCreateSelect() { + const dataElement = document.createElement("select"); + let optionElement; + + for (const k in promptOptions.selectOptions) { + if (!Object.prototype.hasOwnProperty.call(promptOptions.selectOptions, k)) { + continue; + } + + optionElement = document.createElement("option"); + optionElement.setAttribute("value", k); + optionElement.textContent = promptOptions.selectOptions[k]; + if (k === promptOptions.value) { + optionElement.setAttribute("selected", "selected"); + } + + dataElement.append(optionElement); + } + + return dataElement; +} + + + diff --git a/providers/prompt/readme.md b/providers/prompt/readme.md new file mode 100644 index 00000000..6683aced --- /dev/null +++ b/providers/prompt/readme.md @@ -0,0 +1,79 @@ +# Prompt Documentation + +

prompt-preview

+ +## Usage +```js +prompt([options, parentBrowserWindow]).then(...).catch(...) +``` +Promise resolve returns input + +If user presses cancel/exit window, input = null; + +On error, Prompise reject returns custom error message +## Example + +```js +const prompt = require('./providers/prompt'); + +prompt({ + title: 'Prompt example', + label: 'URL:', + value: 'http://example.org', + inputAttrs: { + type: 'url' + }, + type: 'input' +}) +.then((r) => { + if(r === null) { + console.log('user cancelled'); + } else { + console.log('result', r); + } +}) +.catch(console.error); +``` + +### Options object (optional) + +| Key | Explanation | +| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| title | (optional, string) The title of the prompt window. Defaults to 'Prompt'. | +| label | (optional, string) The label which appears on the prompt for the input field. Defaults to 'Please input a value:'. | +| buttonLabels | (optional, object) The text for the OK/cancel buttons. Properties are 'ok' and 'cancel'. Defaults to null. | +| value | (optional, string) The default value for the input field. Defaults to null. | +| type | (optional, string) The type of input field, either 'input' for a standard text input field or 'select' for a dropdown type input. Defaults to 'input'. | +| inputAttrs | (optional, object) The attributes of the input field, analagous to the HTML attributes: `{type: 'text', required: true}` -> ``. Used if the type is 'input' | +| selectOptions | (optional, object) The items for the select dropdown if using the 'select' type in the format 'value': 'display text', where the value is what will be given to the then block and the display text is what the user will see. | +| useHtmlLabel | (optional, boolean) Whether the label should be interpreted as HTML or not. Defaults to false. | +| width | (optional, integer) The width of the prompt window. Defaults to 370. | +| minWidth | (optional, integer) The minimum allowed width for the prompt window. Same default value as width. | +| height | (optional, integer) The height of the prompt window. Defaults to 130. | +| minHeight | (optional, integer) The minimum allowed height for the prompt window. Same default value as height. | +| resizable | (optional, boolean) Whether the prompt window can be resized or not (also sets useContentSize). Defaults to false. | +| alwaysOnTop | (optional, boolean) Whether the window should always stay on top of other windows. Defaults to false | +| icon | (optional, string) The path to an icon image to use in the title bar. Defaults to null and uses electron's icon. | +| customStylesheet | (optional, string) The local path of a CSS file to stylize the prompt window. Defaults to null. | +| menuBarVisible | (optional, boolean) Whether to show the menubar or not. Defaults to false. | +| skipTaskbar | (optional, boolean) Whether to show the prompt window icon in taskbar. Defaults to true. | +| frame | (optional, boolean) Wether to create prompt with frame. Defaults to true. | +| customScript | (optional, string) The local path of a JS file to run on preload. Defaults to null. | +| enableRemoteModule | (optional, boolean) Wether the prompt window have remote modules activated, Defaults to false. | + +If not supplied, it uses the defaults listed in the table above. + +### parentBrowserWindow (optional) + +The window in which to display the prompt on. If not supplied, the parent window of the prompt will be null. + +### customScript (optional) + +Create the script with the following template: + +```node +module.exports = () => { + // This function will be called as a preload script + // So you can use front features like `document.querySelector` +}; +```