mirror of
https://github.com/hodgef/simple-keyboard.git
synced 2026-04-30 00:00:04 +08:00
CandidateBox, IE support
This commit is contained in:
@@ -0,0 +1,129 @@
|
||||
import "./css/CandidateBox.css";
|
||||
|
||||
import Utilities from "../services/Utilities";
|
||||
import {
|
||||
CandidateBoxParams,
|
||||
CandidateBoxRenderParams,
|
||||
CandidateBoxShowParams,
|
||||
} from "./../interfaces";
|
||||
|
||||
class CandidateBox {
|
||||
utilities: Utilities;
|
||||
candidateBoxElement: HTMLDivElement;
|
||||
pageIndex = 0;
|
||||
pageSize;
|
||||
|
||||
constructor({ utilities }: CandidateBoxParams) {
|
||||
this.utilities = utilities;
|
||||
Utilities.bindMethods(CandidateBox, this);
|
||||
this.pageSize = this.utilities.getOptions().layoutCandidatesPageSize || 5;
|
||||
}
|
||||
|
||||
destroy() {
|
||||
if (this.candidateBoxElement) {
|
||||
this.candidateBoxElement.remove();
|
||||
this.pageIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
show({
|
||||
candidateValue,
|
||||
targetElement,
|
||||
onSelect,
|
||||
}: CandidateBoxShowParams): void {
|
||||
if (!candidateValue || !candidateValue.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
const candidateListPages = this.utilities.chunkArray(
|
||||
candidateValue.split(" "),
|
||||
this.pageSize
|
||||
);
|
||||
|
||||
this.renderPage({
|
||||
candidateListPages,
|
||||
targetElement,
|
||||
pageIndex: this.pageIndex,
|
||||
nbPages: candidateListPages.length,
|
||||
onItemSelected: (selectedCandidate: string) => {
|
||||
onSelect(selectedCandidate);
|
||||
this.destroy();
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
renderPage({
|
||||
candidateListPages,
|
||||
targetElement,
|
||||
pageIndex,
|
||||
nbPages,
|
||||
onItemSelected,
|
||||
}: CandidateBoxRenderParams) {
|
||||
// Remove current candidate box, if any
|
||||
this.candidateBoxElement?.remove();
|
||||
|
||||
// Create candidate box element
|
||||
this.candidateBoxElement = document.createElement("div");
|
||||
this.candidateBoxElement.className = "hg-candidate-box";
|
||||
|
||||
// Candidate box list
|
||||
const candidateListULElement = document.createElement("ul");
|
||||
candidateListULElement.className = "hg-candidate-box-list";
|
||||
|
||||
// Create Candidate box list items
|
||||
candidateListPages[pageIndex].forEach((candidateListItem) => {
|
||||
const candidateListLIElement = document.createElement("li");
|
||||
candidateListLIElement.className = "hg-candidate-box-list-item";
|
||||
candidateListLIElement.textContent = candidateListItem;
|
||||
candidateListLIElement.onclick = () => onItemSelected(candidateListItem);
|
||||
|
||||
// Append list item to ul
|
||||
candidateListULElement.appendChild(candidateListLIElement);
|
||||
});
|
||||
|
||||
// Add previous button
|
||||
const isPrevBtnElementActive = pageIndex > 0;
|
||||
const prevBtnElement = document.createElement("div");
|
||||
prevBtnElement.classList.add("hg-candidate-box-prev");
|
||||
isPrevBtnElementActive &&
|
||||
prevBtnElement.classList.add("hg-candidate-box-btn-active");
|
||||
prevBtnElement.onclick = () => {
|
||||
if (!isPrevBtnElementActive) return;
|
||||
this.renderPage({
|
||||
candidateListPages,
|
||||
targetElement,
|
||||
pageIndex: pageIndex - 1,
|
||||
nbPages,
|
||||
onItemSelected,
|
||||
});
|
||||
};
|
||||
this.candidateBoxElement.appendChild(prevBtnElement);
|
||||
|
||||
// Add elements to container
|
||||
this.candidateBoxElement.appendChild(candidateListULElement);
|
||||
|
||||
// Add next button
|
||||
const isNextBtnElementActive = pageIndex < nbPages - 1;
|
||||
const nextBtnElement = document.createElement("div");
|
||||
nextBtnElement.classList.add("hg-candidate-box-next");
|
||||
isNextBtnElementActive &&
|
||||
nextBtnElement.classList.add("hg-candidate-box-btn-active");
|
||||
|
||||
nextBtnElement.onclick = () => {
|
||||
if (!isNextBtnElementActive) return;
|
||||
this.renderPage({
|
||||
candidateListPages,
|
||||
targetElement,
|
||||
pageIndex: pageIndex + 1,
|
||||
nbPages,
|
||||
onItemSelected,
|
||||
});
|
||||
};
|
||||
this.candidateBoxElement.appendChild(nextBtnElement);
|
||||
|
||||
// Append candidate box to target element
|
||||
targetElement.prepend(this.candidateBoxElement);
|
||||
}
|
||||
}
|
||||
|
||||
export default CandidateBox;
|
||||
+287
-55
@@ -1,4 +1,4 @@
|
||||
import "./Keyboard.css";
|
||||
import "./css/Keyboard.css";
|
||||
|
||||
// Services
|
||||
import { getDefaultLayout } from "../services/KeyboardLayout";
|
||||
@@ -9,11 +9,13 @@ import {
|
||||
KeyboardInput,
|
||||
KeyboardButtonElements,
|
||||
KeyboardHandlerEvent,
|
||||
KeyboardButton,
|
||||
KeyboardElement,
|
||||
KeyboardParams,
|
||||
} from "../interfaces";
|
||||
import CandidateBox from "./CandidateBox";
|
||||
|
||||
/**
|
||||
* Root class for simple-keyboard
|
||||
* Root class for simple-keyboard.
|
||||
* This class:
|
||||
* - Parses the options
|
||||
* - Renders the rows and buttons
|
||||
@@ -25,7 +27,7 @@ class SimpleKeyboard {
|
||||
utilities: any;
|
||||
caretPosition: number;
|
||||
caretPositionEnd: number;
|
||||
keyboardDOM: KeyboardButton;
|
||||
keyboardDOM: KeyboardElement;
|
||||
keyboardPluginClasses: string;
|
||||
keyboardDOMClass: string;
|
||||
buttonElements: KeyboardButtonElements;
|
||||
@@ -40,12 +42,16 @@ class SimpleKeyboard {
|
||||
holdTimeout: number;
|
||||
isMouseHold: boolean;
|
||||
initialized: boolean;
|
||||
candidateBox: CandidateBox;
|
||||
keyboardRowsDOM: KeyboardElement;
|
||||
|
||||
/**
|
||||
* Creates an instance of SimpleKeyboard
|
||||
* @param {Array} params If first parameter is a string, it is considered the container class. The second parameter is then considered the options object. If first parameter is an object, it is considered the options object.
|
||||
*/
|
||||
constructor(...params: []) {
|
||||
constructor(...params: KeyboardParams) {
|
||||
if (typeof window === "undefined") return;
|
||||
|
||||
const { keyboardDOMClass, keyboardDOM, options = {} } = this.handleParams(
|
||||
params
|
||||
);
|
||||
@@ -115,13 +121,19 @@ class SimpleKeyboard {
|
||||
* @property {boolean} rtl Adds unicode right-to-left control characters to input return values.
|
||||
* @property {function} onKeyReleased Executes the callback function on key release.
|
||||
* @property {array} modules Module classes to be loaded by simple-keyboard.
|
||||
* @property {boolean} enableLayoutCandidates Enable input method editor candidate list support.
|
||||
* @property {object} excludeFromLayout Buttons to exclude from layout
|
||||
* @property {number} layoutCandidatesPageSize Determine size of layout candidate list
|
||||
*/
|
||||
this.options = options;
|
||||
this.options.layoutName = this.options.layoutName || "default";
|
||||
this.options.theme = this.options.theme || "hg-theme-default";
|
||||
this.options.inputName = this.options.inputName || "default";
|
||||
this.options.preventMouseDownDefault =
|
||||
this.options.preventMouseDownDefault || false;
|
||||
this.options = {
|
||||
layoutName: "default",
|
||||
theme: "hg-theme-default",
|
||||
inputName: "default",
|
||||
preventMouseDownDefault: false,
|
||||
enableLayoutCandidates: true,
|
||||
excludeFromLayout: {},
|
||||
...options,
|
||||
};
|
||||
|
||||
/**
|
||||
* @type {object} Classes identifying loaded plugins
|
||||
@@ -186,6 +198,15 @@ class SimpleKeyboard {
|
||||
getOptions: this.getOptions,
|
||||
});
|
||||
|
||||
/**
|
||||
* Initializing CandidateBox
|
||||
*/
|
||||
this.candidateBox = this.options.enableLayoutCandidates
|
||||
? new CandidateBox({
|
||||
utilities: this.utilities,
|
||||
})
|
||||
: null;
|
||||
|
||||
/**
|
||||
* Rendering keyboard
|
||||
*/
|
||||
@@ -206,10 +227,10 @@ class SimpleKeyboard {
|
||||
* parseParams
|
||||
*/
|
||||
handleParams = (
|
||||
params: any[]
|
||||
params: KeyboardParams
|
||||
): {
|
||||
keyboardDOMClass: string;
|
||||
keyboardDOM: KeyboardButton;
|
||||
keyboardDOM: KeyboardElement;
|
||||
options: Partial<KeyboardOptions>;
|
||||
} => {
|
||||
let keyboardDOMClass;
|
||||
@@ -224,11 +245,11 @@ class SimpleKeyboard {
|
||||
keyboardDOMClass = params[0].split(".").join("");
|
||||
keyboardDOM = document.querySelector(
|
||||
`.${keyboardDOMClass}`
|
||||
) as KeyboardButton;
|
||||
) as KeyboardElement;
|
||||
options = params[1];
|
||||
|
||||
/**
|
||||
* If first parameter is an KeyboardButton
|
||||
* If first parameter is an KeyboardElement
|
||||
* Consider it as the keyboard DOM element
|
||||
*/
|
||||
} else if (params[0] instanceof HTMLDivElement) {
|
||||
@@ -251,7 +272,7 @@ class SimpleKeyboard {
|
||||
keyboardDOMClass = "simple-keyboard";
|
||||
keyboardDOM = document.querySelector(
|
||||
`.${keyboardDOMClass}`
|
||||
) as KeyboardButton;
|
||||
) as KeyboardElement;
|
||||
options = params[0];
|
||||
}
|
||||
|
||||
@@ -279,11 +300,85 @@ class SimpleKeyboard {
|
||||
this.caretPositionEnd = endPosition;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the candidates for a given input
|
||||
* @param input The input string to check
|
||||
*/
|
||||
getInputCandidates(
|
||||
input: string
|
||||
): { candidateKey: string; candidateValue: string } | Record<string, never> {
|
||||
const { layoutCandidates: layoutCandidatesObj } = this.options;
|
||||
|
||||
if (!layoutCandidatesObj || typeof layoutCandidatesObj !== "object") {
|
||||
return {};
|
||||
}
|
||||
|
||||
const layoutCandidates = Object.keys(layoutCandidatesObj).filter(
|
||||
(layoutCandidate: string) => {
|
||||
const regexp = new RegExp(`${layoutCandidate}$`, "g");
|
||||
const matches = [...input.matchAll(regexp)];
|
||||
return !!matches.length;
|
||||
}
|
||||
);
|
||||
|
||||
if (layoutCandidates.length > 1) {
|
||||
const candidateKey = layoutCandidates.sort(
|
||||
(a, b) => b.length - a.length
|
||||
)[0];
|
||||
return {
|
||||
candidateKey,
|
||||
candidateValue: layoutCandidatesObj[candidateKey],
|
||||
};
|
||||
} else if (layoutCandidates.length) {
|
||||
const candidateKey = layoutCandidates[0];
|
||||
return {
|
||||
candidateKey,
|
||||
candidateValue: layoutCandidatesObj[candidateKey],
|
||||
};
|
||||
} else {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows a suggestion box with a list of candidate words
|
||||
* @param candidates The chosen candidates string as defined in the layoutCandidates option
|
||||
* @param targetElement The element next to which the candidates box will be shown
|
||||
*/
|
||||
showCandidatesBox(
|
||||
candidateKey: string,
|
||||
candidateValue: string,
|
||||
targetElement: KeyboardElement
|
||||
): void {
|
||||
if (this.candidateBox) {
|
||||
this.candidateBox.show({
|
||||
candidateValue,
|
||||
targetElement,
|
||||
onSelect: (selectedCandidate: string) => {
|
||||
const currentInput = this.getInput(this.options.inputName, true);
|
||||
const regexp = new RegExp(`${candidateKey}$`, "g");
|
||||
const newInput = currentInput.replace(regexp, selectedCandidate);
|
||||
|
||||
this.setInput(newInput, this.options.inputName, true);
|
||||
|
||||
if (typeof this.options.onChange === "function")
|
||||
this.options.onChange(this.getInput(this.options.inputName, true));
|
||||
|
||||
/**
|
||||
* Calling onChangeAll
|
||||
*/
|
||||
if (typeof this.options.onChangeAll === "function")
|
||||
this.options.onChangeAll(this.getAllInputs());
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles clicks made to keyboard buttons
|
||||
* @param {string} button The button's layout name.
|
||||
*/
|
||||
handleButtonClicked(button: string): void {
|
||||
handleButtonClicked(button: string, e?: KeyboardHandlerEvent): void {
|
||||
const debug = this.options.debug;
|
||||
|
||||
/**
|
||||
@@ -292,14 +387,14 @@ class SimpleKeyboard {
|
||||
if (button === "{//}") return;
|
||||
|
||||
/**
|
||||
* Calling onKeyPress
|
||||
* Creating inputName if it doesn't exist
|
||||
*/
|
||||
if (typeof this.options.onKeyPress === "function")
|
||||
this.options.onKeyPress(button);
|
||||
|
||||
if (!this.input[this.options.inputName])
|
||||
this.input[this.options.inputName] = "";
|
||||
|
||||
/**
|
||||
* Calculating new input
|
||||
*/
|
||||
const updatedInput = this.utilities.getUpdatedInput(
|
||||
button,
|
||||
this.input[this.options.inputName],
|
||||
@@ -307,6 +402,12 @@ class SimpleKeyboard {
|
||||
this.caretPositionEnd
|
||||
);
|
||||
|
||||
/**
|
||||
* Calling onKeyPress
|
||||
*/
|
||||
if (typeof this.options.onKeyPress === "function")
|
||||
this.options.onKeyPress(button);
|
||||
|
||||
if (
|
||||
// If input will change as a result of this button press
|
||||
this.input[this.options.inputName] !== updatedInput &&
|
||||
@@ -326,7 +427,10 @@ class SimpleKeyboard {
|
||||
return;
|
||||
}
|
||||
|
||||
this.input[this.options.inputName] = this.utilities.getUpdatedInput(
|
||||
/**
|
||||
* Updating input
|
||||
*/
|
||||
const newInputValue = this.utilities.getUpdatedInput(
|
||||
button,
|
||||
this.input[this.options.inputName],
|
||||
this.caretPosition,
|
||||
@@ -334,6 +438,8 @@ class SimpleKeyboard {
|
||||
true
|
||||
);
|
||||
|
||||
this.setInput(newInputValue, this.options.inputName, true);
|
||||
|
||||
if (debug) console.log("Input changed:", this.getAllInputs());
|
||||
|
||||
if (this.options.debug) {
|
||||
@@ -361,6 +467,25 @@ class SimpleKeyboard {
|
||||
*/
|
||||
if (typeof this.options.onChangeAll === "function")
|
||||
this.options.onChangeAll(this.getAllInputs());
|
||||
|
||||
/**
|
||||
* Check if this new input has candidates (suggested words)
|
||||
*/
|
||||
if (e?.target && this.options.enableLayoutCandidates) {
|
||||
const { candidateKey, candidateValue } = this.getInputCandidates(
|
||||
updatedInput
|
||||
);
|
||||
|
||||
if (candidateKey && candidateValue) {
|
||||
this.showCandidatesBox(
|
||||
candidateKey,
|
||||
candidateValue,
|
||||
this.keyboardDOM
|
||||
);
|
||||
} else {
|
||||
this.candidateBox.destroy();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (debug) {
|
||||
@@ -430,8 +555,27 @@ class SimpleKeyboard {
|
||||
/**
|
||||
* Handle event options
|
||||
*/
|
||||
if (this.options.preventMouseUpDefault) e.preventDefault();
|
||||
if (this.options.stopMouseUpPropagation) e.stopPropagation();
|
||||
if (this.options.preventMouseUpDefault && e.preventDefault)
|
||||
e.preventDefault();
|
||||
if (this.options.stopMouseUpPropagation && e.stopPropagation)
|
||||
e.stopPropagation();
|
||||
|
||||
/* istanbul ignore next */
|
||||
const isKeyboard =
|
||||
e.target === this.keyboardDOM ||
|
||||
(e.target && this.keyboardDOM.contains(e.target)) ||
|
||||
(this.candidateBox &&
|
||||
this.candidateBox.candidateBoxElement &&
|
||||
(e.target === this.candidateBox.candidateBoxElement ||
|
||||
(e.target &&
|
||||
this.candidateBox.candidateBoxElement.contains(e.target))));
|
||||
|
||||
/**
|
||||
* On click outside, remove candidateBox
|
||||
*/
|
||||
if (!isKeyboard && this.candidateBox) {
|
||||
this.candidateBox.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -514,9 +658,7 @@ class SimpleKeyboard {
|
||||
* Get the keyboard’s input (You can also get it from the onChange prop).
|
||||
* @param {string} [inputName] optional - the internal input to select
|
||||
*/
|
||||
getInput(inputName: string, skipSync = false): string {
|
||||
inputName = inputName || this.options.inputName;
|
||||
|
||||
getInput(inputName = this.options.inputName, skipSync = false): string {
|
||||
/**
|
||||
* Enforce syncInstanceInputs, if set
|
||||
*/
|
||||
@@ -553,14 +695,17 @@ class SimpleKeyboard {
|
||||
* @param {string} input the input value
|
||||
* @param {string} inputName optional - the internal input to select
|
||||
*/
|
||||
setInput(input: string, inputName: string): void {
|
||||
inputName = inputName || this.options.inputName;
|
||||
setInput(
|
||||
input: string,
|
||||
inputName = this.options.inputName,
|
||||
skipSync?: boolean
|
||||
): void {
|
||||
this.input[inputName] = input;
|
||||
|
||||
/**
|
||||
* Enforce syncInstanceInputs, if set
|
||||
*/
|
||||
if (this.options.syncInstanceInputs) this.syncInstanceInputs();
|
||||
if (!skipSync && this.options.syncInstanceInputs) this.syncInstanceInputs();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -587,7 +732,7 @@ class SimpleKeyboard {
|
||||
/**
|
||||
* Some option changes require adjustments before re-render
|
||||
*/
|
||||
this.onSetOptions(options);
|
||||
this.onSetOptions(changedOptions);
|
||||
|
||||
/**
|
||||
* Rendering
|
||||
@@ -612,8 +757,11 @@ class SimpleKeyboard {
|
||||
* Executing actions depending on changed options
|
||||
* @param {object} options The options to set
|
||||
*/
|
||||
onSetOptions(options: Partial<KeyboardOptions>): void {
|
||||
if (options.inputName) {
|
||||
onSetOptions(changedOptions: string[] = []): void {
|
||||
/**
|
||||
* Changed: inputName
|
||||
*/
|
||||
if (changedOptions.includes("inputName")) {
|
||||
/**
|
||||
* inputName changed. This requires a caretPosition reset
|
||||
*/
|
||||
@@ -622,14 +770,47 @@ class SimpleKeyboard {
|
||||
}
|
||||
this.setCaretPosition(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changed: layoutName
|
||||
*/
|
||||
if (changedOptions.includes("layoutName")) {
|
||||
/**
|
||||
* Reset candidateBox
|
||||
*/
|
||||
if (this.candidateBox) {
|
||||
this.candidateBox.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Changed: layoutCandidatesPageSize, layoutCandidates
|
||||
*/
|
||||
if (
|
||||
changedOptions.includes("layoutCandidatesPageSize") ||
|
||||
changedOptions.includes("layoutCandidates")
|
||||
) {
|
||||
/**
|
||||
* Reset and recreate candidateBox
|
||||
*/
|
||||
if (this.candidateBox) {
|
||||
this.candidateBox.destroy();
|
||||
this.candidateBox = new CandidateBox({
|
||||
utilities: this.utilities,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all keyboard rows and reset keyboard values.
|
||||
* Used internally between re-renders.
|
||||
*/
|
||||
clear(): void {
|
||||
this.keyboardDOM.innerHTML = "";
|
||||
resetRows(): void {
|
||||
if (this.keyboardRowsDOM) {
|
||||
this.keyboardRowsDOM.remove();
|
||||
}
|
||||
|
||||
this.keyboardDOM.className = this.keyboardDOMClass;
|
||||
this.buttonElements = {};
|
||||
}
|
||||
@@ -759,7 +940,7 @@ class SimpleKeyboard {
|
||||
* Get the DOM Element of a button. If there are several buttons with the same name, an array of the DOM Elements is returned.
|
||||
* @param {string} button The button layout name to select
|
||||
*/
|
||||
getButtonElement(button: string): KeyboardButton | KeyboardButton[] {
|
||||
getButtonElement(button: string): KeyboardElement | KeyboardElement[] {
|
||||
let output;
|
||||
|
||||
const buttonArr = this.buttonElements[button];
|
||||
@@ -830,6 +1011,7 @@ class SimpleKeyboard {
|
||||
document.addEventListener("keydown", this.handleKeyDown);
|
||||
document.addEventListener("mouseup", this.handleMouseUp);
|
||||
document.addEventListener("touchend", this.handleTouchEnd);
|
||||
document.addEventListener("select", this.handleSelect);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -868,6 +1050,14 @@ class SimpleKeyboard {
|
||||
this.caretEventHandler(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Event Handler: Select
|
||||
*/
|
||||
/* istanbul ignore next */
|
||||
handleSelect(event: KeyboardHandlerEvent): void {
|
||||
this.caretEventHandler(event);
|
||||
}
|
||||
|
||||
/**
|
||||
* Called by {@link setEventListeners} when an event that warrants a cursor position update is triggered
|
||||
*/
|
||||
@@ -947,6 +1137,7 @@ class SimpleKeyboard {
|
||||
document.removeEventListener("keydown", this.handleKeyDown);
|
||||
document.removeEventListener("mouseup", this.handleMouseUp);
|
||||
document.removeEventListener("touchend", this.handleTouchEnd);
|
||||
document.removeEventListener("select", this.handleSelect);
|
||||
document.onpointerup = null;
|
||||
document.ontouchend = null;
|
||||
document.ontouchcancel = null;
|
||||
@@ -955,7 +1146,7 @@ class SimpleKeyboard {
|
||||
/**
|
||||
* Remove buttons
|
||||
*/
|
||||
let deleteButton = (buttonElement: KeyboardButton) => {
|
||||
let deleteButton = (buttonElement: KeyboardElement) => {
|
||||
buttonElement.onpointerdown = null;
|
||||
buttonElement.onpointerup = null;
|
||||
buttonElement.onpointercancel = null;
|
||||
@@ -983,9 +1174,22 @@ class SimpleKeyboard {
|
||||
this.keyboardDOM.onmousedown = null;
|
||||
|
||||
/**
|
||||
* Clearing keyboard wrapper
|
||||
* Clearing keyboard rows
|
||||
*/
|
||||
this.clear();
|
||||
this.resetRows();
|
||||
|
||||
/**
|
||||
* Candidate box
|
||||
*/
|
||||
if (this.candidateBox) {
|
||||
this.candidateBox.destroy();
|
||||
this.candidateBox = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clearing keyboardDOM
|
||||
*/
|
||||
this.keyboardDOM.innerHTML = "";
|
||||
|
||||
/**
|
||||
* Remove instance
|
||||
@@ -1117,7 +1321,7 @@ class SimpleKeyboard {
|
||||
*/
|
||||
this.setEventListeners();
|
||||
|
||||
if (typeof this.options.onInit === "function") this.options.onInit();
|
||||
if (typeof this.options.onInit === "function") this.options.onInit(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1172,7 +1376,8 @@ class SimpleKeyboard {
|
||||
* Executes the callback function every time simple-keyboard is rendered (e.g: when you change layouts).
|
||||
*/
|
||||
onRender() {
|
||||
if (typeof this.options.onRender === "function") this.options.onRender();
|
||||
if (typeof this.options.onRender === "function")
|
||||
this.options.onRender(this);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -1198,8 +1403,8 @@ class SimpleKeyboard {
|
||||
loadModules() {
|
||||
if (Array.isArray(this.options.modules)) {
|
||||
this.options.modules.forEach((KeyboardModule) => {
|
||||
const keyboardModule = new KeyboardModule();
|
||||
keyboardModule.init(this);
|
||||
const keyboardModule = new KeyboardModule(this);
|
||||
keyboardModule.init && keyboardModule.init(this);
|
||||
});
|
||||
|
||||
this.keyboardPluginClasses = "modules-loaded";
|
||||
@@ -1329,7 +1534,7 @@ class SimpleKeyboard {
|
||||
/**
|
||||
* Clear keyboard
|
||||
*/
|
||||
this.clear();
|
||||
this.resetRows();
|
||||
|
||||
/**
|
||||
* Calling beforeFirstRender
|
||||
@@ -1360,11 +1565,29 @@ class SimpleKeyboard {
|
||||
useTouchEventsClass
|
||||
);
|
||||
|
||||
/**
|
||||
* Create row wrapper
|
||||
*/
|
||||
this.keyboardRowsDOM = document.createElement("div");
|
||||
this.keyboardRowsDOM.className = "hg-rows";
|
||||
|
||||
/**
|
||||
* Iterating through each row
|
||||
*/
|
||||
layout[this.options.layoutName].forEach((row, rIndex) => {
|
||||
const rowArray = row.split(" ");
|
||||
let rowArray = row.split(" ");
|
||||
|
||||
/**
|
||||
* Enforce excludeFromLayout
|
||||
*/
|
||||
if (this.options.excludeFromLayout[this.options.layoutName]) {
|
||||
rowArray = rowArray.filter(
|
||||
(buttonName) =>
|
||||
!this.options.excludeFromLayout[this.options.layoutName].includes(
|
||||
buttonName
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creating empty row
|
||||
@@ -1465,7 +1688,7 @@ class SimpleKeyboard {
|
||||
* Handle PointerEvents
|
||||
*/
|
||||
buttonDOM.onpointerdown = (e: KeyboardHandlerEvent) => {
|
||||
this.handleButtonClicked(button);
|
||||
this.handleButtonClicked(button, e);
|
||||
this.handleButtonMouseDown(button, e);
|
||||
};
|
||||
buttonDOM.onpointerup = (e: KeyboardHandlerEvent) => {
|
||||
@@ -1483,7 +1706,7 @@ class SimpleKeyboard {
|
||||
* Handle touch events
|
||||
*/
|
||||
buttonDOM.ontouchstart = (e: KeyboardHandlerEvent) => {
|
||||
this.handleButtonClicked(button);
|
||||
this.handleButtonClicked(button, e);
|
||||
this.handleButtonMouseDown(button, e);
|
||||
};
|
||||
buttonDOM.ontouchend = (e: KeyboardHandlerEvent) => {
|
||||
@@ -1496,9 +1719,9 @@ class SimpleKeyboard {
|
||||
/**
|
||||
* Handle mouse events
|
||||
*/
|
||||
buttonDOM.onclick = () => {
|
||||
buttonDOM.onclick = (e: KeyboardHandlerEvent) => {
|
||||
this.isMouseHold = false;
|
||||
this.handleButtonClicked(button);
|
||||
this.handleButtonClicked(button, e);
|
||||
};
|
||||
buttonDOM.onmousedown = (e: KeyboardHandlerEvent) => {
|
||||
this.handleButtonMouseDown(button, e);
|
||||
@@ -1552,11 +1775,16 @@ class SimpleKeyboard {
|
||||
);
|
||||
|
||||
/**
|
||||
* Appending row to keyboard
|
||||
* Appending row to hg-rows
|
||||
*/
|
||||
this.keyboardDOM.appendChild(rowDOM);
|
||||
this.keyboardRowsDOM.appendChild(rowDOM);
|
||||
});
|
||||
|
||||
/**
|
||||
* Appending row to keyboard
|
||||
*/
|
||||
this.keyboardDOM.appendChild(this.keyboardRowsDOM);
|
||||
|
||||
/**
|
||||
* Calling onRender
|
||||
*/
|
||||
@@ -1577,15 +1805,18 @@ class SimpleKeyboard {
|
||||
!useTouchEvents &&
|
||||
!useMouseEvents
|
||||
) {
|
||||
document.onpointerup = () => this.handleButtonMouseUp();
|
||||
document.onpointerup = (e: KeyboardHandlerEvent) =>
|
||||
this.handleButtonMouseUp(null, e);
|
||||
this.keyboardDOM.onpointerdown = (e: KeyboardHandlerEvent) =>
|
||||
this.handleKeyboardContainerMouseDown(e);
|
||||
} else if (useTouchEvents) {
|
||||
/**
|
||||
* Handling ontouchend, ontouchcancel
|
||||
*/
|
||||
document.ontouchend = () => this.handleButtonMouseUp();
|
||||
document.ontouchcancel = () => this.handleButtonMouseUp();
|
||||
document.ontouchend = (e: KeyboardHandlerEvent) =>
|
||||
this.handleButtonMouseUp(null, e);
|
||||
document.ontouchcancel = (e: KeyboardHandlerEvent) =>
|
||||
this.handleButtonMouseUp(null, e);
|
||||
|
||||
this.keyboardDOM.ontouchstart = (e: KeyboardHandlerEvent) =>
|
||||
this.handleKeyboardContainerMouseDown(e);
|
||||
@@ -1593,7 +1824,8 @@ class SimpleKeyboard {
|
||||
/**
|
||||
* Handling mouseup
|
||||
*/
|
||||
document.onmouseup = () => this.handleButtonMouseUp();
|
||||
document.onmouseup = (e: KeyboardHandlerEvent) =>
|
||||
this.handleButtonMouseUp(null, e);
|
||||
this.keyboardDOM.onmousedown = (e: KeyboardHandlerEvent) =>
|
||||
this.handleKeyboardContainerMouseDown(e);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
.hg-candidate-box {
|
||||
display: inline-flex;
|
||||
border-radius: 5px;
|
||||
position: absolute;
|
||||
background: #ececec;
|
||||
border-bottom: 2px solid #b5b5b5;
|
||||
user-select: none;
|
||||
min-width: 272px;
|
||||
transform: translateY(-100%);
|
||||
margin-top: -10px;
|
||||
}
|
||||
|
||||
ul.hg-candidate-box-list {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
li.hg-candidate-box-list-item {
|
||||
height: 40px;
|
||||
width: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
li.hg-candidate-box-list-item:hover {
|
||||
background: rgba(0, 0, 0, 0.03);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
li.hg-candidate-box-list-item:active {
|
||||
background: rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.hg-candidate-box-prev::before {
|
||||
content: "◄";
|
||||
}
|
||||
|
||||
.hg-candidate-box-next::before {
|
||||
content: "►";
|
||||
}
|
||||
|
||||
.hg-candidate-box-next,
|
||||
.hg-candidate-box-prev {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 10px;
|
||||
background: #d0d0d0;
|
||||
color: #969696;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.hg-candidate-box-next {
|
||||
border-top-right-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
}
|
||||
|
||||
.hg-candidate-box-prev {
|
||||
border-top-left-radius: 5px;
|
||||
border-bottom-left-radius: 5px;
|
||||
}
|
||||
|
||||
.hg-candidate-box-btn-active {
|
||||
color: #444;
|
||||
}
|
||||
@@ -0,0 +1,371 @@
|
||||
import Keyboard from '../Keyboard';
|
||||
import { setDOM, clearDOM } from '../../../utils/TestUtility';
|
||||
import CandidateBox from '../CandidateBox';
|
||||
|
||||
beforeEach(() => {
|
||||
setDOM();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
clearDOM();
|
||||
});
|
||||
|
||||
it('CandidateBox class will be instantiated by default', () => {
|
||||
document.querySelector("body").innerHTML = `
|
||||
<input class="input" placeholder="Tap on the virtual keyboard to start" />
|
||||
<div class="simple-keyboard"></div>
|
||||
|
||||
<input class="input2" placeholder="Tap on the virtual keyboard to start" />
|
||||
<div class="keyboard2"></div>
|
||||
`;
|
||||
|
||||
const keyboard1 = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b"
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
const keyboard2 = new Keyboard(".keyboard2", {
|
||||
layout: {
|
||||
default: [
|
||||
"a b"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2"
|
||||
}
|
||||
});
|
||||
|
||||
expect(keyboard1.candidateBox).toBeInstanceOf(CandidateBox);
|
||||
expect(keyboard2.candidateBox).toBeInstanceOf(CandidateBox);
|
||||
|
||||
keyboard1.destroy();
|
||||
keyboard2.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox class will not be instantiated on enableLayoutCandidates: false', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2"
|
||||
},
|
||||
enableLayoutCandidates: false
|
||||
});
|
||||
|
||||
expect(keyboard.candidateBox).toBeNull();
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox will respect layoutCandidatesPageSize', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
},
|
||||
layoutCandidatesPageSize: 3
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelectorAll("li").length).toBe(3);
|
||||
|
||||
keyboard.getButtonElement("{bksp}").click();
|
||||
keyboard.setOptions({
|
||||
layoutCandidatesPageSize: 6
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelectorAll("li").length).toBe(6);
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox will respect layoutCandidatesPageSize', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
},
|
||||
layoutCandidatesPageSize: 3
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelectorAll("li").length).toBe(3);
|
||||
|
||||
keyboard.getButtonElement("{bksp}").click();
|
||||
keyboard.setOptions({
|
||||
layoutCandidatesPageSize: 6
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelectorAll("li").length).toBe(6);
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox will reset on layoutCandidates change', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
},
|
||||
layoutCandidatesPageSize: 3
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("li").textContent).toBe("1");
|
||||
|
||||
keyboard.getButtonElement("{bksp}").click();
|
||||
keyboard.setOptions({
|
||||
layoutCandidates: {
|
||||
a: "s1 s2 s3 s4 s5 s6"
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("li").textContent).toBe("s1");
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox show will return early if candidateValue is not provided', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.candidateBox.renderPage = jest.fn();
|
||||
keyboard.candidateBox.show({
|
||||
candidateValue: null,
|
||||
targetElement: document.createElement("div"),
|
||||
onSelect: () => {}
|
||||
});
|
||||
|
||||
expect(keyboard.candidateBox.renderPage).not.toBeCalled();
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox show will call renderPage if candidateValue is provided', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.candidateBox.renderPage = jest.fn();
|
||||
keyboard.candidateBox.show({
|
||||
candidateValue: "a b",
|
||||
targetElement: document.createElement("div"),
|
||||
onSelect: () => {}
|
||||
});
|
||||
|
||||
expect(keyboard.candidateBox.renderPage).toBeCalled();
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox select candidate will work', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
}
|
||||
});
|
||||
|
||||
let candidateBoxOnItemSelected;
|
||||
const onSelect = jest.fn().mockImplementation((selectedCandidate) => {
|
||||
candidateBoxOnItemSelected(selectedCandidate);
|
||||
keyboard.candidateBox.destroy();
|
||||
});
|
||||
|
||||
const candidateBoxRenderFn = keyboard.candidateBox.renderPage;
|
||||
jest.spyOn(keyboard.candidateBox, "renderPage").mockImplementation((params) => {
|
||||
candidateBoxOnItemSelected = params.onItemSelected;
|
||||
params.onItemSelected = onSelect;
|
||||
candidateBoxRenderFn(params);
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector("li").click();
|
||||
|
||||
expect(onSelect).toBeCalledWith("1");
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox select next and previous page will work', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
},
|
||||
layoutCandidatesPageSize: 3
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector(".hg-candidate-box-next").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("456");
|
||||
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector(".hg-candidate-box-prev").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("123");
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox selecting next and previous page when not available will do nothing', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
},
|
||||
layoutCandidatesPageSize: 3
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector(".hg-candidate-box-prev").click();
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("123");
|
||||
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector(".hg-candidate-box-next").click();
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("456");
|
||||
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector(".hg-candidate-box-next").click();
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("456");
|
||||
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox will not show anything when there is no candidates', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("b").click();
|
||||
|
||||
expect(keyboard.candidateBox.candidateBoxElement).toBeUndefined();
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox will propose the better matching result, regardless of layoutCandidates order', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3",
|
||||
aa: "6 7 8"
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("123");
|
||||
|
||||
keyboard.getButtonElement("a").click(); // This will get you the 'aa' layoutCandidates instead of the 'a' ones.
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("678");
|
||||
|
||||
keyboard.getButtonElement("{bksp}").click();
|
||||
expect(keyboard.candidateBox.candidateBoxElement.querySelector("ul").textContent).toBe("123");
|
||||
|
||||
keyboard.destroy();
|
||||
});
|
||||
|
||||
it('CandidateBox show not be called if keyboard.candidateBox is undefined upon showCandidatesBox call', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3",
|
||||
aa: "6 7 8"
|
||||
}
|
||||
});
|
||||
|
||||
keyboard.candidateBox = null;
|
||||
keyboard.showCandidatesBox();
|
||||
});
|
||||
|
||||
it('CandidateBox selection should trigger onChange', () => {
|
||||
const keyboard = new Keyboard({
|
||||
layout: {
|
||||
default: [
|
||||
"a b {bksp}"
|
||||
]
|
||||
},
|
||||
layoutCandidates: {
|
||||
a: "1 2 3 4 5 6"
|
||||
},
|
||||
onChange: jest.fn(),
|
||||
onChangeAll: jest.fn()
|
||||
});
|
||||
|
||||
let candidateBoxOnItemSelected;
|
||||
|
||||
const onSelect = jest.fn().mockImplementation((selectedCandidate) => {
|
||||
candidateBoxOnItemSelected(selectedCandidate);
|
||||
keyboard.candidateBox.destroy();
|
||||
});
|
||||
|
||||
const candidateBoxRenderFn = keyboard.candidateBox.renderPage;
|
||||
jest.spyOn(keyboard.candidateBox, "renderPage").mockImplementation((params) => {
|
||||
candidateBoxOnItemSelected = params.onItemSelected;
|
||||
params.onItemSelected = onSelect;
|
||||
candidateBoxRenderFn(params);
|
||||
});
|
||||
|
||||
keyboard.getButtonElement("a").click();
|
||||
keyboard.candidateBox.candidateBoxElement.querySelector("li").click();
|
||||
|
||||
expect(keyboard.options.onChange).toBeCalledWith("a");
|
||||
expect(keyboard.options.onChangeAll).toBeCalledWith({"default": "a"});
|
||||
|
||||
expect(keyboard.options.onChange).toBeCalledWith("1");
|
||||
expect(keyboard.options.onChangeAll).toBeCalledWith({"default": "1"});
|
||||
keyboard.destroy();
|
||||
});
|
||||
Reference in New Issue
Block a user