implement keybind prompt

This commit is contained in:
Araxeus
2021-04-27 23:48:06 +03:00
parent eae4cca148
commit 5dc1179d54
6 changed files with 578 additions and 73 deletions

View File

@ -18,7 +18,8 @@ body {
overflow: hidden;
}
#data {
#data,
.keybindData {
background: unset;
color: whitesmoke;
border: 1px solid rgb(54, 54, 54);
@ -41,12 +42,35 @@ body {
}
#ok,
#cancel {
#cancel,
.clearButton {
background-color: rgb(0, 0, 0);
color: whitesmoke;
}
/* For Counter Prompt */
.minus,
.plus {
background: rgb(0, 0, 0);
}
/* For Select Prompt */
option {
background-color: #07070C;
}
/* For Keybind Prompt */
.clearButton:focus {
outline: none;
}
.clearButton:hover {
background-color: rgb(5, 5, 5);
}
.keybindData:hover {
border: 1px solid rgb(56, 0, 0);
}
.keybindData:focus {
outline: 3px solid #1E0919;
border: 1px solid rgb(56, 0, 0);
}

View File

@ -6,9 +6,10 @@ const url = require("url");
const path = require("path");
const DEFAULT_WIDTH = 370;
const DEFAULT_KEYBIND_WIDTH = 420;
const DEFAULT_COUNTER_WIDTH = 300;
const DEFAULT_HEIGHT = 160;
const DEFAULT_COUNTER_HEIGHT = 150;
const DEFAULT_HEIGHT = 150;
const DEFAULT_KEYBIND_HEIGHT = options => (options.length * 40) + 100;
function electronPrompt(options, parentWindow) {
return new Promise((resolve, reject) => {
@ -18,8 +19,8 @@ function electronPrompt(options, parentWindow) {
//custom options override default
const options_ = Object.assign(
{
width: options?.type === "counter" ? DEFAULT_COUNTER_WIDTH : DEFAULT_WIDTH,
height: options?.type === "counter" ? DEFAULT_COUNTER_HEIGHT : DEFAULT_HEIGHT,
width: options?.type === "counter" ? DEFAULT_COUNTER_WIDTH : options?.type === "keybind" ? DEFAULT_KEYBIND_WIDTH : DEFAULT_WIDTH,
height: options?.type === "keybind" && options?.keybindOptions ? DEFAULT_KEYBIND_HEIGHT(options.keybindOptions) : DEFAULT_HEIGHT,
resizable: false,
title: "Prompt",
label: "Please input a value:",
@ -28,6 +29,7 @@ function electronPrompt(options, parentWindow) {
value: null,
type: "input",
selectOptions: null,
keybindOptions: null,
counterOptions: { minimum: null, maximum: null, multiFire: false },
icon: null,
useHtmlLabel: false,
@ -41,22 +43,21 @@ function electronPrompt(options, parentWindow) {
options || {}
);
options_.minWidth = options?.minWidth || options?.width || options_.width;
options_.minHeight = options?.minHeight || options?.height || options_.height;
if (options_.customStylesheet === "dark") {
options_.customStylesheet = require("path").join(__dirname, "dark-prompt.css");
}
if (options_.type === "counter" && (options_.counterOptions !== null && typeof options_.selectOptions !== "object")) {
reject(new Error('"counterOptions" must be an object if specified'));
return;
for (let type of ["counter", "select", "keybind"]) {
if (options_.type === type && (!options_[`${type}Options`] || typeof options_[`${type}Options`] !== "object")) {
reject(new Error(`"${type}Options" must be an object if type = ${type}`));
return;
}
}
if (options_.type === "select" && (options_.selectOptions === null || typeof options_.selectOptions !== "object")) {
reject(new Error('"selectOptions" must be an object'));
return;
}
options_.minWidth = options?.minWidth || options?.width || options_.width;
options_.minHeight = options?.minHeight || options?.height || options_.height;
let promptWindow = new BrowserWindow({
frame: options_.frame,
@ -104,6 +105,11 @@ function electronPrompt(options, parentWindow) {
//get input from front
const postDataListener = (event, value) => {
if (options_.type === "keybind" && value) {
for (let i=0; i < value.length ;i++) {
value[i] = JSON.parse(value[i])
}
}
resolve(value);
event.returnValue = null;
cleanup();
@ -135,7 +141,7 @@ function electronPrompt(options, parentWindow) {
//should never happen
promptWindow.webContents.on("did-fail-load", (
event,
_event,
errorCode,
errorDescription,
validatedURL

View File

@ -0,0 +1,140 @@
const { promptCreateInput } = require("./prompt");
module.exports = { promptCreateCounter , validateCounterInput }
let options;
function promptCreateCounter(promptOptions, parentElement) {
options = promptOptions;
if (options.counterOptions?.multiFire) {
document.onmouseup = () => {
if (nextTimeoutID) {
clearTimeout(nextTimeoutID)
nextTimeoutID = null;
}
};
}
options.value = validateCounterInput(options.value);
const dataElement = promptCreateInput();
dataElement.onkeypress = function isNumberKey(e) {
if (Number.isNaN(parseInt(e.key)) && e.key !== "Backspace" && e.key !== "Delete")
return false;
return true;
}
dataElement.style.width = "unset";
dataElement.style["text-align"] = "center";
parentElement.append(createMinusButton(dataElement));
parentElement.append(dataElement);
parentElement.append(createPlusButton(dataElement));
return dataElement;
}
let nextTimeoutID = null;
/** Function execute callback in 3 accelerated intervals based on timer.
* Terminated from document.onmouseup() that is registered from promptCreateCounter()
* @param {function} callback function to execute
* @param {object} timer {
* * time: First delay in miliseconds
* * scaleSpeed: Speed change per tick on first acceleration
* * limit: First Speed Limit, gets divided by 2 after $20 calls. $number change exponentially
* }
* @param {int} stepArgs argument for callback representing Initial steps per click, default to 1
* steps starts to increase when speed is too fast to notice
* @param {int} counter used internally to decrease timer.limit
*/
function multiFire(callback, timer = { time: 300, scaleSpeed: 100, limit: 100 }, stepsArg = 1, counter = 0) {
callback(stepsArg);
const nextTimeout = timer.time;
if (counter > 20) {
counter = 0 - stepsArg;
if (timer.limit > 1) {
timer.limit /= 2;
} else {
stepsArg *= 2;
}
}
if (timer.time !== timer.limit) {
timer.time = Math.max(timer.time - timer.scaleSpeed, timer.limit)
}
nextTimeoutID = setTimeout(
multiFire, //callback
nextTimeout, //timer
//multiFire args:
callback,
timer,
stepsArg,
counter + 1
);
}
function createMinusButton(dataElement) {
function doMinus(steps) {
dataElement.value = validateCounterInput(parseInt(dataElement.value) - steps);
}
const minusBtn = document.createElement("span");
minusBtn.textContent = "-";
minusBtn.classList.add("minus");
if (options.counterOptions?.multiFire) {
minusBtn.onmousedown = () => {
multiFire(doMinus);
};
} else {
minusBtn.onmousedown = () => {
doMinus();
};
}
return minusBtn;
}
function createPlusButton(dataElement) {
function doPlus(steps) {
dataElement.value = validateCounterInput(parseInt(dataElement.value) + steps);
}
const plusBtn = document.createElement("span");
plusBtn.textContent = "+";
plusBtn.classList.add("plus");
if (options.counterOptions?.multiFire) {
plusBtn.onmousedown = () => {
multiFire(doPlus);
};
} else {
plusBtn.onmousedown = () => {
doPlus();
};
}
return plusBtn;
}
//validate counter
function validateCounterInput(input) {
const min = options.counterOptions?.minimum;
const max = options.counterOptions?.maximum;
//note that !min/max would proc if min/max are 0
if (min !== null && min !== undefined && input < min) {
return min;
}
if (max !== null && max !== undefined && input > max) {
return max;
}
return input;
}

View File

@ -0,0 +1,305 @@
/* HTML
<div class="keybind" , id="div">
<label id="label" class="keybindLabel">Example</label>
<input readonly type="text" id="txt" class="keybindData">
<button id="clear" class="clearButton">
Clear
</button>
</div>
*/
/* CSS
div.keybind {
display: grid;
grid-template-columns: max-content max-content max-content;
grid-gap: 5px;
}
div.keybind button {
width: auto;
}
div.keybind label {
text-align: right;
}
div.keybind label:after {
content: ":";
}
*/
const { promptError } = require("./prompt")
class KeybindGetter {
value = null;
modifiers = null;
key = "";
label = null;
txt = null;
clearButton = null;
constructor(options, parentElement) {
if (!options.label || !options.value) {
promptError("keybind option must contain label and value");
return;
}
this.value = options.value
this.modifiers = new Set();
this.key = "";
this.label = document.createElement("label");
this.label.classList.add("keybindLabel");
this.txt = document.createElement("input");
this.txt.setAttribute('readonly', true);
this.txt.classList.add("keybindData");
this.clearButton = document.createElement("button");
this.clearButton.classList.add("clearButton");
this.clearButton.textContent = "Clear";
this.clearButton.onclick = (e) => e.preventDefault();
parentElement.append(this.label, this.txt, this.clearButton);
this.setup(options);
if (options.default) {
this.setDefault(options.default)
}
}
focus() {
this.txt.focus();
}
output() {
const output = {value: this.value, accelerator: this.txt.value.replaceAll(" ", "")}
return JSON.stringify(output);
}
updateText() {
let result = "";
for (let modifier of this.modifiers) {
result += modifier + " + ";
}
this.txt.value = result + this.key;
}
setDefault(defaultValue) {
const accelerator = parseAccelerator(defaultValue).split("+");
for (let key of accelerator) {
if (isModifier(key))
this.modifiers.add(key);
else
this.key = key;
}
this.updateText();
}
clear() {
this.modifiers.clear();
this.key = "";
this.txt.value = "";
}
setup(options) {
this.txt.addEventListener("keydown", (event) => {
event.preventDefault();
if (event.repeat) {
return
}
let key = event.code || event.key;
if (key in virtualKeyCodes)
key = virtualKeyCodes[event.code];
else {
console.log('Error, key "' + event.code + '" was not found');
return;
}
if (isModifier(key)) {
if (this.modifiers.size < 3)
this.modifiers.add(key);
} else { // is key
this.key = key;
}
this.updateText();
});
this.clearButton.addEventListener("click", () => {
this.clear()
});
this.label.textContent = options.label + " ";
}
}
class keybindContainer {
elements = [];
constructor(options, parentElement) {
parentElement.classList.add("keybind");
this.elements = options.map(option => new KeybindGetter(option, parentElement));
document.querySelector("#buttons").style["padding-top"] = "20px";
}
focus() {
if (this.elements.length > 0)
this.elements[0].focus();
}
submit() {
return this.elements.map(element => element.output());
}
}
function parseAccelerator(a) {
let accelerator = a.toString();
if (process.platform !== 'darwin') {
accelerator = accelerator.replace(/(Cmd)|(Command)/gi, '');
} else {
accelerator = accelerator.replace(/(Ctrl)|(Control)/gi, '');
}
accelerator = accelerator.replace(/(Or)/gi, '');
return accelerator;
}
function isModifier(key) {
for (let modifier of ["Shift", "Control", "Ctrl", "Command", "Cmd", "Alt", "AltGr", "Super"]) {
if (key === modifier)
return true;
}
return false;
}
const virtualKeyCodes = {
ShiftLeft: "Shift",
ShiftRight: "Shift",
ControlLeft: "Ctrl",
ControlRight: "Ctrl",
AltLeft: "Alt",
AltRight: "Alt",
MetaLeft: "Super",
MetaRight: "Super",
NumLock: "NumLock",
NumpadDivide: "NumDiv",
NumpadMultiply: "NumMult",
NumpadSubtract: "NumSub",
NumpadAdd: "NumAdd",
NumpadDecimal: "NumDec ",
Numpad0: "Num0",
Numpad1: "Num1",
Numpad2: "Num2",
Numpad3: "Num3",
Numpad4: "Num4",
Numpad5: "Num5",
Numpad6: "Num6",
Numpad7: "Num7",
Numpad8: "Num8",
Numpad9: "Num9",
Digit0: "0",
Digit1: "1",
Digit2: "2",
Digit3: "3",
Digit4: "4",
Digit5: "5",
Digit6: "6",
Digit7: "7",
Digit8: "8",
Digit9: "9",
Minus: "-",
Equal: "=",
KeyQ: "Q",
KeyW: "W",
KeyE: "E",
KeyR: "R",
KeyT: "T",
KeyY: "Y",
KeyU: "U",
KeyI: "I",
KeyO: "O",
KeyP: "P",
KeyA: "A",
KeyS: "S",
KeyD: "D",
KeyF: "F",
KeyG: "G",
KeyH: "H",
KeyJ: "J",
KeyK: "K",
KeyL: "L",
KeyZ: "Z",
KeyX: "X",
KeyC: "C",
KeyV: "V",
KeyB: "B",
KeyN: "N",
KeyM: "M",
BracketLeft: "[",
BracketRight: "]",
Semicolon: ";",
Quote: "'",
Backquote: '"',
Backslash: "\\",
Comma: ",",
Period: "'.'",
Slash: "/",
plus: '+',
Space: "Space",
Tab: "Tab",
Backspace: "Backspace",
Delete: "Delete",
Insert: "Insert",
Return: "Return",
Enter: "Enter",
ArrowUp: "Up",
ArrowDown: "Down",
ArrowLeft: "Left",
ArrowRight: "Right",
Home: "Home",
End: "End",
PageUp: "PageUp",
PageDown: "PageDown",
Escape: "Escape",
AudioVolumeUp: "VolumeUp",
AudioVolumeDown: "VolumeDown",
AudioVolumeMute: "VolumeMute",
MediaTrackNext: "MediaNextTrack",
MediaTrackPrevious: "MediaPreviousTrack",
MediaStop: "MediaStop",
MediaPlayPause: "MediaPlayPause",
ScrollLock: "ScrollLock",
PrintScreen: "PrintScreen",
F1: "F1",
F2: "F2",
F3: "F3",
F4: "F4",
F5: "F5",
F6: "F6",
F7: "F7",
F8: "F8",
F9: "F9",
F10: "F10",
F11: "F11",
F12: "F12",
F13: "F13",
F14: "F14",
F15: "F15",
F16: "F16",
F17: "F17",
F18: "F18",
F19: "F19",
F20: "F20",
F21: "F21",
F22: "F22",
F23: "F23",
F24: "F24",
};
module.exports = function promptCreateKeybind(options, parentElement) {
return new keybindContainer(options, parentElement);
}

View File

@ -79,7 +79,7 @@ select#data {
color: black;
}
/* Counter mode css */
/* Counter mode */
span {
cursor: pointer;
}
@ -96,3 +96,25 @@ span {
vertical-align: middle;
text-align: center;
}
/** Keybind mode */
div.keybind {
display: grid;
grid-template-columns: max-content max-content max-content;
row-gap: 20px;
column-gap: 10px;
margin: auto 0;
justify-content: center;
}
div.keybind button {
width: auto;
}
div.keybind label {
text-align: right;
}
div.keybind label:after {
content: ":";
}

View File

@ -1,16 +1,15 @@
const fs = require("fs");
const { ipcRenderer } = require("electron");
let promptId = null;
let promptOptions = null;
let dataElement = null;
function $(selector) {
return document.querySelector(selector);
}
function $(selector) { return document.querySelector(selector); }
document.addEventListener("DOMContentLoaded", promptRegister);
function promptRegister() {
//get custom session id
promptId = document.location.hash.replace("#", "");
@ -56,13 +55,12 @@ function promptRegister() {
$("#form").addEventListener("submit", promptSubmit);
$("#cancel").addEventListener("click", promptCancel);
//create input/select
//create input/select/counter/keybind
const dataContainerElement = $("#data-container");
let dataElement;
switch (promptOptions.type) {
case "counter":
dataElement = promptCreateCounter();
dataElement = promptCreateCounter(dataContainerElement);
break;
case "input":
dataElement = promptCreateInput();
@ -70,22 +68,24 @@ function promptRegister() {
case "select":
dataElement = promptCreateSelect();
break;
case "keybind":
dataElement = require("./keybind")(promptOptions.keybindOptions, dataContainerElement);
break;
default:
return promptError(`Unhandled input type '${promptOptions.type}'`);
}
if (promptOptions.type === "counter") {
dataContainerElement.append(createMinusButton(dataElement));
dataContainerElement.append(dataElement);
dataContainerElement.append(createPlusButton(dataElement));
} else {
dataContainerElement.append(dataElement);
if (promptOptions.type != "keybind") {
dataElement.setAttribute("id", "data");
if (promptOptions.type !== "counter") {
dataContainerElement.append(dataElement);
}
}
dataElement.setAttribute("id", "data");
dataElement.focus();
if (promptOptions.type === "input" || promptOptions.type === "counter") {
if (promptOptions.type !== "select" && promptOptions.type !== "keybind") {
dataElement.select();
}
@ -100,10 +100,10 @@ function promptRegister() {
}
}
window.addEventListener("error", error => {
window.addEventListener("error", event => {
if (promptId) {
promptError("An error has occured on the prompt window: \n" +
JSON.stringify(error, ["message", "arguments", "type", "name"])
`Message: ${event.message}\nURL: ${event.url}\nLine: ${event.lineNo}, Column: ${event.columnNo}\nStack: ${event.error.stack}`
);
}
});
@ -111,7 +111,7 @@ window.addEventListener("error", error => {
//send error to back
function promptError(error) {
if (error instanceof Error) {
error = error.message;
error = error.message + "\n" + error.stack;
}
ipcRenderer.sendSync("prompt-error:" + promptId, error);
@ -124,20 +124,18 @@ function promptCancel() {
//transfer input data to back
function promptSubmit() {
const dataElement = $("#data");
let data = null;
switch (promptOptions.type) {
case "input":
case "select":
data = dataElement.value;
break;
case "counter":
data = validateCounterInput(dataElement.value);
break;
case "select":
data = promptOptions.selectMultiple ?
dataElement.querySelectorAll("option[selected]").map(o => o.getAttribute("value")) :
dataElement.value;
case "keybind":
data = dataElement.submit();
break;
default: //will never happen
return promptError(`Unhandled input type '${promptOptions.type}'`);
@ -152,9 +150,6 @@ function promptCreateInput() {
dataElement.setAttribute("type", "text");
if (promptOptions.value) {
if (promptOptions.type === "counter") {
promptOptions.value = validateCounterInput(promptOptions.value);
}
dataElement.value = promptOptions.value;
} else {
dataElement.value = "";
@ -212,21 +207,50 @@ function promptCreateSelect() {
return dataElement;
}
function promptCreateCounter(parentElement) {
if (promptOptions.counterOptions?.multiFire) {
document.onmouseup = () => {
if (nextTimeoutID) {
clearTimeout(nextTimeoutID)
nextTimeoutID = null;
}
};
}
promptOptions.value = validateCounterInput(promptOptions.value);
const dataElement = promptCreateInput();
dataElement.onkeypress = function isNumberKey(e) {
if (Number.isNaN(parseInt(e.key)) && e.key !== "Backspace" && e.key !== "Delete")
return false;
return true;
}
dataElement.style.width = "unset";
dataElement.style["text-align"] = "center";
parentElement.append(createMinusButton(dataElement));
parentElement.append(dataElement);
parentElement.append(createPlusButton(dataElement));
return dataElement;
}
let nextTimeoutID = null;
/* Function execute callback in 3 accelerated intervals based on timer.
/** Function execute callback in 3 accelerated intervals based on timer.
* Terminated from document.onmouseup() that is registered from promptCreateCounter()
* @param {function} callback: function to execute
* @param {object} timer: {
* * time: First delay in miliseconds.
* * limit: First Speed Limit, gets divided by 2 after $20 calls. $number change exponentially
* @param {function} callback function to execute
* @param {object} timer {
* * time: First delay in miliseconds
* * scaleSpeed: Speed change per tick on first acceleration
* }
* @param {int} stepArgs: argument for callback representing Initial steps per click, default to 1
* * limit: First Speed Limit, gets divided by 2 after $20 calls. $number change exponentially
* }
* @param {int} stepArgs argument for callback representing Initial steps per click, default to 1
* steps starts to increase when speed is too fast to notice
* @param {int} counter: used internally to decrease timer.limit
* @param {int} counter used internally to decrease timer.limit
*/
function multiFire(callback, timer = { time: 500, scaleSpeed: 140, limit: 100 }, stepsArg = 1, counter = 0) {
function multiFire(callback, timer = { time: 300, scaleSpeed: 100, limit: 100 }, stepsArg = 1, counter = 0) {
callback(stepsArg);
const nextTimeout = timer.time;
@ -241,9 +265,7 @@ function multiFire(callback, timer = { time: 500, scaleSpeed: 140, limit: 100 },
}
if (timer.time !== timer.limit) {
timer.time = timer.time > timer.limit ?
timer.time - timer.scaleSpeed :
timer.limit;
timer.time = Math.max(timer.time - timer.scaleSpeed, timer.limit)
}
nextTimeoutID = setTimeout(
@ -301,26 +323,9 @@ function createPlusButton(dataElement) {
return plusBtn;
}
function promptCreateCounter() {
if (promptOptions.counterOptions?.multiFire) {
document.onmouseup = () => {
if (nextTimeoutID) {
clearTimeout(nextTimeoutID)
nextTimeoutID = null;
}
};
}
const dataElement = promptCreateInput();
dataElement.style.width = "unset";
dataElement.style["text-align"] = "center";
return dataElement;
}
//validate counter
function validateCounterInput(input) {
const min = promptOptions.counterOptions?.minimum;
const max = promptOptions.counterOptions?.maximum;
//note that !min/max would proc if min/max are 0
@ -334,3 +339,6 @@ function validateCounterInput(input) {
return input;
}
module.exports.promptError = promptError;