suranadira-keyboard
Version:
A component for displaying Suranadira texts.
406 lines (346 loc) • 10.6 kB
JavaScript
/**
* Suranadira Keyboard
*
* Copyright 2019, Armands Strazds <armands@strazds.com>
*
*/
import React, { useState, useEffect } from "react";
import suranadiraCore from "../../suranadira-core";
import Debug, { info } from "suranadira-utils/debug";
import { getClickXY } from "suranadira-utils/dom";
import { encode } from "suranadira-utils/arrays";
import { useKeyDown } from "suranadira-utils/keyboard";
import { removeUID } from "suranadira-utils/string";
import { createPath, getPathIndex } from "suranadira-utils/canvas";
import { updateComposition } from "./keyboard-layout";
const strokes = suranadiraCore.suranadiraStrokes;
const elements = suranadiraCore.suranadiraElements;
const { getWordID } = suranadiraCore.suranadiraWords;
// Control commands
const showKeyNames = false;
// The Keyboard app
const SuranadiraKeyboard = args => {
// Defaults
let defaults = {
width: window.innerWidth,
maxWidth: 900,
height: 80,
color: "#000",
colorPressed: "#ff0000",
background: "#ddd",
keyWidth: 7,
keyHeight: 7,
onClickAllowed: true,
onKeyDownAllowed: true,
onComposition: () => {},
onLayoutSet: () => {},
layoutDefault: new Set(["Z", "J"]),
unit: 20,
lineWidth: 5,
charSpaceX: 6,
charSpaceY: 3,
focusRectPaddingX: -2,
focusRectPaddingY: 0,
locked: false,
visible: true,
resize: "",
styles: {}
};
// Properties
let properties = Object.assign(defaults, args);
if (typeof args.styles !== "undefined") {
properties = Object.assign(properties, args.styles);
}
properties.height = properties.unit * 4;
// Limit component's width
if (properties.width > properties.maxWidth)
properties.width = properties.maxWidth;
// Create canvas reference
let refCanvas = React.createRef();
// External hooks
let keyDown = useKeyDown();
// Define state hooks
const [position, setPosition] = useState(-1); // as in "positional number system"
const [composition, setComposition] = useState([]);
const [layout, setLayout] = useState(properties.layoutDefault);
const [paths, setPaths] = useState(null);
// Set the default layout
useEffect(() => {
drawLayout(properties.layoutDefault);
setLayout(properties.layoutDefault);
// eslint-disable-next-line
}, []);
// Dispatcher for externale commands
useEffect(() => {
if (!properties.action) return;
// Send out the script
switch (removeUID(properties.action)) {
// On Composition done
case "DONE":
Debug(
info({
module: "SuranadiraKeyboard.js",
func: "useEffect [properties.action]",
param: "composition",
value: composition
})
);
properties.onDone(composition);
resetKeyboard();
break;
// On Composition cancelled
case "CANCEL":
properties.onCancel();
resetKeyboard();
break;
// On Message sent
case "SENT":
resetKeyboard();
break;
default:
// ignore
break;
}
// eslint-disable-next-line
}, [properties.action]);
// Map keyDown to virtual keyDown
useEffect(() => {
if (properties.locked) return;
// Unserialze the keyDown return
let keyID = removeUID(keyDown);
// Show the keyID in console
if (showKeyNames) console.log(keyID);
// Map a physical numeric key to virtual keyboard
if (!isNaN(parseFloat(keyID)) && isFinite(keyID)) {
onKeyDown(keyID);
}
// Finalize Composition
else if (keyID === "Enter") {
properties.onDone(composition);
resetKeyboard();
}
// Cancel Composition
else if (keyID === "Escape") {
properties.onCancel();
resetKeyboard();
}
// eslint-disable-next-line
}, [keyDown]);
// Handle the device dimensions change
useEffect(() => {
Debug(
info({
module: "SuranadiraKeyboard.js",
func: "useEffect [properties.resize]",
param: "layout",
value: layout
})
);
drawLayout(layout);
// eslint-disable-next-line
}, [properties.resize]);
// Fire event onLayoutSet
useEffect(() => {
let diff = new Set(
[...layout].filter(x => !properties.layoutDefault.has(x))
);
properties.onLayoutSet(layout, layout.size !== 0 && diff.size === 0);
// eslint-disable-next-line
}, [layout]);
// Map mouseClick to virtual keyDown
const onClick = e => {
if (!properties.onClickAllowed) return;
let keyID = getKeyIDByMouseEvent(e);
// Trigger key down, if a key was pressed
if (keyID !== false) onKeyDown(keyID);
};
const getKeyIDByMouseEvent = e => {
// Get CTX
let canvas = refCanvas.current;
let ctx = canvas.getContext("2d");
// Get point coordinates
let point = getClickXY(e);
let x = point.x;
let y = point.y;
// Get path index
let keyID = getPathIndex({ ctx, paths, x, y });
return keyID;
};
const onMouseDown = e => {
let keyID = getKeyIDByMouseEvent(e);
console.log("onMouseDown keyID", keyID);
drawElement(keyID, properties.colorPressed);
};
const onMouseUp = e => {
let keyID = getKeyIDByMouseEvent(e);
console.log("onMouseUp keyID", keyID);
drawElement(keyID, properties.color);
};
// Send the virtual keyDown to Callback
const onKeyDown = keyID => {
if (!properties.onKeyDownAllowed) return;
// Compose character according to the key command
composeCharacter(keyID);
};
// Set Suranadirakeyboard layout
const drawLayout = lo => {
if (typeof lo === "undefined") return;
// Clear keyboard
clearKeyboard();
let _paths = [],
_path;
// Draw Suranadira Elements
for (let keyID = 0; keyID < lo.size; keyID++) {
_path = drawElement(keyID, properties.color, lo);
_paths.push(_path);
}
setPaths(_paths);
setLayout(lo);
};
// Draw an Element / a Keyboard Key
const drawElement = (keyID, color, lo) => {
if (typeof lo === "undefined") lo = layout;
if (typeof color === "undefined") color = properties.color;
let ctx = getCTX();
if (!ctx) return;
var _lo = Array.from(lo);
let element = _lo[keyID];
let marginLeft = 2;
let offset = keyID * properties.keyWidth;
// Stroke properties
strokes.properties.ctx = ctx;
strokes.properties.unit = properties.unit;
strokes.properties.lineWidth = properties.lineWidth;
strokes.properties.left = 0;
strokes.properties.top = properties.unit * 1.5;
strokes.properties.strokeStyle = color;
let x, shape;
try {
for (let stroke of elements[element].strokes) {
shape = stroke[0];
x = stroke[1];
strokes.draw(shape, x + offset + marginLeft, 0);
}
} catch (e) {
// ignore
}
// Set path properties
let pathProperties = {
left:
(offset + marginLeft) * strokes.properties.unit -
properties.focusRectPaddingX,
top: strokes.properties.top - properties.focusRectPaddingY,
width:
strokes.properties.unit * properties.charSpaceX +
2 * properties.focusRectPaddingX,
height:
strokes.properties.unit * properties.charSpaceY +
-2 * properties.focusRectPaddingY
};
// Create path for events for the composition
let path = createPath({
ctx: ctx,
properties: pathProperties,
invisible: true
});
return path;
};
// Compose character
const composeCharacter = keyID => {
let _layout = layout;
let _position = position;
let ret = updateComposition(
keyID, // numeric key 0..n for choice selection
position, // position within the positional number system
layout, // a set of element choices
composition // current composition of the character
);
// Send composition and character ID to callback
if (ret !== false) {
var [co, cp] = ret;
// Draw layout
if (typeof co !== undefined) {
drawLayout(co);
_layout = co;
}
// Save state
setComposition(cp);
_position = position + 1;
// Callback composition
if (cp.length < 4) {
properties.onComposition(encode(cp), null);
}
// Callback composition and character ID
else {
Debug(
info({
module: "SuranadiraKeyboard.js",
func: "composeCharacter",
param: "cp for getWordID(cp)",
value: cp
})
);
let ch = getWordID(cp);
if (ch !== false) {
properties.onComposition(encode(cp), ch);
clearKeyboard();
// setPosition(-1);
_position = -1;
_layout = new Set();
}
}
}
setPosition(_position);
setLayout(new Set(_layout));
};
// Get canvas context
const getCTX = () => {
let canvas = refCanvas.current;
let ctx = canvas.getContext("2d");
return ctx;
};
// Clear the keyboard canvas
const clearKeyboard = () => {
// let canvas = refCanvas.current;
// let ctx = canvas.getContext("2d");
let ctx = getCTX();
if (!ctx) return;
ctx.clearRect(0, 0, window.innerWidth, window.innerHeight);
return ctx;
};
// Reset keyboard after composition is done
const resetKeyboard = () => {
drawLayout(properties.layoutDefault);
setPosition(-1);
setComposition([]);
};
return (
<div
className="SuranadiraKeyboard"
style={{
background: "none", // properties.background,
position: "relative",
display: properties.visible ? "inline-block" : "none",
width: properties.width,
height: properties.height,
// boxSizing: "border-box",
overflow: "hidden"
}}
>
<canvas
ref={refCanvas}
style={{
background: properties.background, // "none",
position: "relative",
display: "block"
}}
width={properties.width + "px"}
height={properties.height + "px"}
onClick={onClick}
onMouseDown={onMouseDown}
onMouseUp={onMouseUp}
/>
</div>
);
};
export default SuranadiraKeyboard;