Allow CandidateBox caret positioning, passing event though onChange. Per https://github.com/hodgef/react-simple-keyboard/issues/1298

This commit is contained in:
Francisco Hodge 2021-06-28 23:26:43 -07:00
parent c6f12f0f92
commit 3032bef324
13 changed files with 201 additions and 25 deletions

View File

@ -1,6 +1,6 @@
/*!
*
* simple-keyboard v3.1.57
* simple-keyboard v3.2.0
* https://github.com/hodgef/simple-keyboard
*
* Copyright (c) Francisco Hodge (https://github.com/hodgef) and project contributors.

File diff suppressed because one or more lines are too long

View File

@ -22,14 +22,14 @@ export declare type CandidateBoxParams = {
export declare type CandidateBoxShowParams = {
candidateValue: string;
targetElement: KeyboardElement;
onSelect: (selectedCandidate: string) => void;
onSelect: (selectedCandidate: string, e: MouseEvent) => void;
};
export declare type CandidateBoxRenderParams = {
candidateListPages: string[][];
targetElement: KeyboardElement;
pageIndex: number;
nbPages: number;
onItemSelected: (selectedCandidate: string) => void;
onItemSelected: (selectedCandidate: string, e: MouseEvent) => void;
};
export declare type KeyboardElement = HTMLDivElement | HTMLButtonElement;
export declare type KeyboardHandlerEvent = any;
@ -195,6 +195,14 @@ export interface KeyboardOptions {
* Executes the callback function once simple-keyboard is rendered for the first time (on initialization).
*/
onInit?: (instance?: SimpleKeyboard) => void;
/**
* Retrieves the current input
*/
onChange?: (input: string, e?: MouseEvent) => any;
/**
* Retrieves all inputs
*/
onChangeAll?: (inputObj: KeyboardInput, e?: MouseEvent) => any;
/**
* Module options can have any format
*/

2
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "simple-keyboard",
"version": "3.1.57",
"version": "3.2.0",
"lockfileVersion": 1,
"requires": true,
"dependencies": {

View File

@ -1,6 +1,6 @@
{
"name": "simple-keyboard",
"version": "3.1.57",
"version": "3.2.0",
"description": "On-screen Javascript Virtual Keyboard",
"main": "build/index.js",
"types": "build/types/index.d.ts",

View File

@ -0,0 +1,81 @@
import Keyboard from "../lib";
import "./css/CandidateBoxDemo.css";
const setDOM = () => {
document.querySelector("body").innerHTML = `
<input class="input" placeholder="Tap on the virtual keyboard to start" />
<div class="simple-keyboard"></div>
`;
};
class Demo {
constructor() {
setDOM();
/**
* Demo Start
*/
this.keyboard = new Keyboard({
onChange: input => this.onChange(input),
onKeyPress: button => this.onKeyPress(button),
preventMouseDownDefault: true,
layoutCandidates: {
ni: "你 尼",
hao: "好 号"
}
});
/**
* Update simple-keyboard when input is changed directly
*/
document.querySelector(".input").addEventListener("input", event => {
this.keyboard.setInput(event.target.value);
});
}
onChange(input) {
const inputElement = document.querySelector(".input");
/**
* Updating input's value
*/
inputElement.value = input;
console.log("Input changed", input);
/**
* Synchronizing input caret position
*/
const caretPosition = this.keyboard.caretPosition;
if (caretPosition !== null)
this.setInputCaretPosition(inputElement, caretPosition);
console.log("caretPosition", caretPosition);
}
setInputCaretPosition(elem, pos) {
if (elem.setSelectionRange) {
elem.focus();
elem.setSelectionRange(pos, pos);
}
}
onKeyPress(button) {
console.log("Button pressed", button);
/**
* If you want to handle the shift and caps lock buttons
*/
if (button === "{shift}" || button === "{lock}") this.handleShift();
}
handleShift() {
const currentLayout = this.keyboard.options.layoutName;
const shiftToggle = currentLayout === "default" ? "shift" : "default";
this.keyboard.setOptions({
layoutName: shiftToggle
});
}
}
export default Demo;

View File

@ -0,0 +1,12 @@
input {
width: 100%;
height: 100px;
padding: 20px;
font-size: 20px;
border: none;
box-sizing: border-box;
}
.simple-keyboard {
max-width: 850px;
}

View File

@ -9,6 +9,7 @@ import BasicDemo from "./BasicDemo";
//import DOMElementDemo from "./DOMElementDemo";
//import FullKeyboardDemo from "./FullKeyboardDemo";
//import MultipleKeyboardsDemo from "./MultipleKeyboardsDestroyDemo";
//import CandidateBoxDemo from "./CandidateBoxDemo";
/**
* Selected demo

View File

@ -0,0 +1,36 @@
import { setDOM } from '../../utils/TestUtility';
import CandidateBoxDemo from '../CandidateBoxDemo';
it('Demo will load', () => {
setDOM();
new CandidateBoxDemo();
});
it('Demo caret positioning will adjust accordingly', () => {
setDOM();
const demo = new CandidateBoxDemo();
demo.keyboard.setCaretPosition(0);
demo.keyboard.getButtonElement("n").click();
demo.keyboard.getButtonElement("h").click();
demo.keyboard.getButtonElement("a").click();
demo.keyboard.getButtonElement("o").click();
expect(demo.keyboard.getCaretPosition()).toBe(4);
demo.keyboard.candidateBox.candidateBoxElement.querySelector("li").click();
expect(demo.keyboard.getCaretPosition()).toBe(2);
demo.keyboard.setCaretPosition(1);
demo.keyboard.getButtonElement("i").click();
expect(demo.keyboard.getCaretPosition()).toBe(2);
demo.keyboard.candidateBox.candidateBoxElement.querySelector("li").click();
expect(demo.keyboard.getCaretPosition()).toBe(1);
expect(demo.keyboard.getInput()).toBe("你好");
});

View File

@ -45,8 +45,8 @@ class CandidateBox {
targetElement,
pageIndex: this.pageIndex,
nbPages: candidateListPages.length,
onItemSelected: (selectedCandidate: string) => {
onSelect(selectedCandidate);
onItemSelected: (selectedCandidate: string, e: MouseEvent) => {
onSelect(selectedCandidate, e);
this.destroy();
},
});
@ -73,9 +73,18 @@ class CandidateBox {
// Create Candidate box list items
candidateListPages[pageIndex].forEach((candidateListItem) => {
const candidateListLIElement = document.createElement("li");
const getMouseEvent = () => {
const mouseEvent = new MouseEvent("click");
Object.defineProperty(mouseEvent, "target", {
value: candidateListLIElement,
});
return mouseEvent;
};
candidateListLIElement.className = "hg-candidate-box-list-item";
candidateListLIElement.textContent = candidateListItem;
candidateListLIElement.onclick = () => onItemSelected(candidateListItem);
candidateListLIElement.onclick = (e = getMouseEvent()) =>
onItemSelected(candidateListItem, e);
// Append list item to ul
candidateListULElement.appendChild(candidateListLIElement);

View File

@ -320,8 +320,10 @@ class SimpleKeyboard {
const layoutCandidates = Object.keys(layoutCandidatesObj).filter(
(layoutCandidate: string) => {
const inputSubstr =
input.substring(0, this.getCaretPositionEnd() || 0) || input;
const regexp = new RegExp(`${layoutCandidate}$`, "g");
const matches = [...input.matchAll(regexp)];
const matches = [...inputSubstr.matchAll(regexp)];
return !!matches.length;
}
);
@ -359,21 +361,37 @@ class SimpleKeyboard {
this.candidateBox.show({
candidateValue,
targetElement,
onSelect: (selectedCandidate: string) => {
onSelect: (selectedCandidate: string, e: MouseEvent) => {
const currentInput = this.getInput(this.options.inputName, true);
const initialCaretPosition = this.getCaretPositionEnd() || 0;
const inputSubstr =
currentInput.substring(0, initialCaretPosition || 0) ||
currentInput;
const regexp = new RegExp(`${candidateKey}$`, "g");
const newInput = currentInput.replace(regexp, selectedCandidate);
const newInputSubstr = inputSubstr.replace(regexp, selectedCandidate);
const newInput = currentInput.replace(inputSubstr, newInputSubstr);
const caretPositionDiff = newInputSubstr.length - inputSubstr.length;
let newCaretPosition =
(initialCaretPosition || currentInput.length) + caretPositionDiff;
if (newCaretPosition < 0) newCaretPosition = 0;
this.setInput(newInput, this.options.inputName, true);
this.setCaretPosition(newCaretPosition);
if (typeof this.options.onChange === "function")
this.options.onChange(this.getInput(this.options.inputName, true));
this.options.onChange(
this.getInput(this.options.inputName, true),
e
);
/**
* Calling onChangeAll
*/
if (typeof this.options.onChangeAll === "function")
this.options.onChangeAll(this.getAllInputs());
this.options.onChangeAll(this.getAllInputs(), e);
},
});
}
@ -429,7 +447,7 @@ class SimpleKeyboard {
* Calling onKeyPress
*/
if (typeof this.options.onKeyPress === "function")
this.options.onKeyPress(button);
this.options.onKeyPress(button, e);
if (
// If input will change as a result of this button press
@ -483,13 +501,13 @@ class SimpleKeyboard {
* Calling onChange
*/
if (typeof this.options.onChange === "function")
this.options.onChange(this.getInput(this.options.inputName, true));
this.options.onChange(this.getInput(this.options.inputName, true), e);
/**
* Calling onChangeAll
*/
if (typeof this.options.onChangeAll === "function")
this.options.onChangeAll(this.getAllInputs());
this.options.onChangeAll(this.getAllInputs(), e);
/**
* Check if this new input has candidates (suggested words)

View File

@ -216,7 +216,7 @@ it('CandidateBox select candidate will work', () => {
keyboard.getButtonElement("a").click();
keyboard.candidateBox.candidateBoxElement.querySelector("li").click();
expect(onSelect).toBeCalledWith("1");
expect(onSelect).toBeCalledWith("1", expect.anything());
keyboard.destroy();
});
@ -353,6 +353,7 @@ it('CandidateBox selection should trigger onChange', () => {
});
const candidateBoxRenderFn = keyboard.candidateBox.renderPage;
jest.spyOn(keyboard.candidateBox, "renderPage").mockImplementation((params) => {
candidateBoxOnItemSelected = params.onItemSelected;
params.onItemSelected = onSelect;
@ -362,10 +363,10 @@ it('CandidateBox selection should trigger onChange', () => {
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.mock.calls[0][0]).toBe("a");
expect(keyboard.options.onChangeAll.mock.calls[0][0]).toMatchObject({"default": "a"});
expect(keyboard.options.onChange).toBeCalledWith("1");
expect(keyboard.options.onChangeAll).toBeCalledWith({"default": "1"});
expect(keyboard.options.onChange.mock.calls[1][0]).toBe("1");
expect(keyboard.options.onChangeAll.mock.calls[1][0]).toMatchObject({"default": "1"});
keyboard.destroy();
});

View File

@ -31,7 +31,7 @@ export type CandidateBoxShowParams = {
candidateValue: string,
targetElement: KeyboardElement,
// eslint-disable-next-line no-unused-vars
onSelect: (selectedCandidate: string) => void
onSelect: (selectedCandidate: string, e: MouseEvent) => void
}
export type CandidateBoxRenderParams = {
@ -40,7 +40,7 @@ export type CandidateBoxRenderParams = {
pageIndex: number;
nbPages: number;
// eslint-disable-next-line no-unused-vars
onItemSelected: (selectedCandidate: string) => void
onItemSelected: (selectedCandidate: string, e: MouseEvent) => void
}
export type KeyboardElement = HTMLDivElement | HTMLButtonElement;
@ -240,6 +240,16 @@ export interface KeyboardOptions {
*/
onInit?: (instance?: SimpleKeyboard) => void;
/**
* Retrieves the current input
*/
onChange?: (input: string, e?: MouseEvent) => any;
/**
* Retrieves all inputs
*/
onChangeAll?: (inputObj: KeyboardInput, e?: MouseEvent) => any;
/**
* Module options can have any format
*/