/*jshint esversion: 6*/
import math from '../lib/math/math.js';
import Cluster from '../../cluster.js';
import Component from '../component';
/**
* @class Input
* @desc User Interface input for math expressions
* @extends Component
*/
export default class Input extends Component{
constructor(){
super();
this.compiled = null;
}
connectedCallback(){
this._initInput();
this._initConfig();
}
_initConfig(){
this.options = {
typename: 'math',
knowMethods: [
'sin',
'cos',
'asin',
'acos',
'log',
'sqrt',
'random',
'round'
],
knowVariables:{
'pi': {
symbol: 'π',
value: 3.141592653589793
},
'phi': {
symbol: 'φ',
value: 1.618033988749894
},
'euler':{
symbol: 'ℇ',
value: 2.718281828459045
},
'tau':{
symbol: 'τ',
value: 6.283185307179586
}
},
knowAtoms: [
'infinity',
'null',
'not',
'and',
'or',
'xor'
]
};
this.setKnowVariables(this.options.knowVariables);
this.setKnowAtoms(this.options.knowAtoms);
}
_initInput(){
var build = this;
this.spellcheck = false;
this.setAttribute('contenteditable', true);
this.addEventListener('input', (e) => this.update(e));
this.addEventListener('keydown', (e) => this.keydown(e));
this.addEventListener('blur', (e) => this._checkChange(e));
this.addEventListener('change', (e) => {
this.value_last_change = this.textContent;
this.compile();
});
this.addEventListener('error', (e) => this.className = "error");
this.addEventListener('success', (e) => this.className = "");
}
keydown(e){
if(e.keyCode == 13){
this._checkChange();
this.blur();
}
else if(e.key == '('){
document.execCommand('insertHTML', false, ')');
setCurrentCursorPosition(this, getCurrentCursorPosition(this) - 1);
}
else if(e.key == '['){
document.execCommand('insertHTML', false, ']');
setCurrentCursorPosition(this, getCurrentCursorPosition(this) - 1);
}
}
_checkChange(){
if(this.value_last_change != this.textContent){
this.dispatchEvent(new Event('change'));
}
}
update(e, value){
var pos = getCurrentCursorPosition(this);
var length = this.textContent.length;
this.updateContent(value);
if(pos-1 <= this.textContent.length){
if(this.textContent.length == 0 || pos == 0) return;
pos = pos - (length - this.textContent.length);
setCurrentCursorPosition(this, pos);
}
else{
setCurrentCursorPosition(this, this.textContent.length);
}
}
/**
* updateContent - update highlight of current content, or specified content
* @memberof Input
*
* @param {type} [value] A specified value
* @return {void}
*/
updateContent(value){
// Store a value, value of the textContent of this.
value = value || this.textContent;
if(this.knowVariablesReg) {
value = value.replace(new RegExp(this.knowVariablesAliasReg,"gi"), (m,r) => Input.getSymbolKnowAlias(this, m));
value = value.replace(new RegExp(this.knowVariablesReg,"gi"), (m,r) => Input.getHighlightKnowVariables(this, m));
}
if(this.knowAtomsReg){
value = value.replace(new RegExp(this.knowAtomsReg,"gi"), (m) => Input.getHighlightKnowAtoms(this, m));
}
value = value.replace(/([0-9]+)/g,'<span class="cui-number">$1</span>');
value = value.replace(/([a-zA-Z]+)\(/g, (m,r) => Input.getHighlightKnowMethods(this, r));
this.innerHTML = value;
}
/**
* setKnowVariables - set know variables to aliasing and highlight
* @memberof Input
*
* @param {type} list list of know variables
* @return {void}
*/
setKnowVariables(list){
if(list){
var knowVariablesReg=[], knowVariablesAliasReg=[];
for ( var i in list ) {
knowVariablesReg.push(/*'([^a-zA-Z])'+*/list[i].symbol);
knowVariablesAliasReg.push(i);
}
this.knowVariablesReg = knowVariablesReg.join('|');
this.knowVariablesAliasReg = knowVariablesAliasReg.join('|');
}
else{
this.knowVariablesReg = false;
this.knowVariablesAliasReg = false;
}
this.options.knowVariables = list;
this.update();
}
/**
* setKnowAtoms - set know "atom" keywords to highlight
* @memberof Input
*
* @param {array} list A formatted list of know atoms
* @return {void}
*/
setKnowAtoms(list){
if(list){
var knowAtomsReg=[];
for ( var i in list ) {
knowAtomsReg.push(list[i]);
}
this.knowAtomsReg = "([^a-zA-Z])("+knowAtomsReg.join('|')+")([^a-zA-Z]|\b)";
}
else{
this.knowAtomsReg = false;
}
this.options.knowAtoms = list;
this.update();
}
/**
* compile
* @description After each change of the input field, this function is called and compiles the entered expression.
If it returns an error the event "error" is launched, otherwise if all happens normally the event "success" is launched.
The method will store a value in "this.compiled" that can be evaluated by the getter "value",
thus offering a much higher computation speed.
The method will check the entered unit and if it does not match the quantity entered in the "type" attribute,
it will return an "invalid measure" error.
* @memberof Input
*
* @return {void}
*/
compile(){
try{
var build = this;
// "out" is a pre-compiled value for Math library can compile
var out;
// "compiled" store the compiled returned by Math library
var compiled;
// "_eval" store the evaluation of Math Library, here he is use for error debugging
var _eval;
// If the field is empty
if(this.textContent == ""){
// If a default value is define, compile and store it.
if(this.default && this.default != "false") {
compiled = math.compile(this.default);
_eval = compiled.eval();
}
else{ // Else throw error
throw new Error("The field is empty but no default value has been set.");
}
}
else{
// Precompile, Compile and Eval for debugging
out = this.textContent.replace(new RegExp(this.knowVariablesReg, 'gi'), (r) => Input.getVariableKnowSymbol(this, r).value);
compiled = math.compile(out);
_eval = compiled.eval();
}
if(compiled){
// Check if the compiled its unit hashmap
if(_eval.units){
// Store the type key of the compiled, example: "length", "power", "energi"...
var typekey = _eval.units[0].unit.base.key.toLowerCase();
if(this.type == 'number'){
throw new TypeError('Invalid measure: Need "number" but the entry is "'+typekey+'"');
}
else if( typekey != this.type.toLowerCase() ) {
// If the key not matches, throw error message
throw new TypeError('Invalid measure: Need "'+this.type.toLowerCase()+'" but the entry is "'+typekey+'"');
}
}
// Otherwise if the compiled value is a unit while the expected type is a number, throw an error.
else if( typeof(_eval) != 'number' ){
throw new TypeError('Invalid measure: Need "'+this.type.toLowerCase()+'" but the entry is "number"');
}
}
// Dispatch a Success event
this.dispatchEvent(new Event('success'));
// Store the compiled for the getter "value" of this object for future evaluations.
this.compiled = compiled;
// Set error message to false
this.error = false;
this.title = '';
// Dispatch a Compile event
this.dispatchEvent(new Event('compile'));
}
catch(e){
// If a error is throw, the message is store in this.error
this.error = e.message;
// And a "error" event is dispatch
this.dispatchEvent(new Event('error', {
cancelable: true,
details:{
message: e.message,
type: e.type
}
}));
// Set the title according to the error message to give the user a return of his error.
this.title = this.error;
// Dispatch compile event
this.dispatchEvent(new Event('compile'));
}
}
set type(v){
this.setAttribute('type', v || 'number');
this.compile();
}
get type(){
return this.getAttribute('type') || number;
}
set unit(v){
this.setAttribute('unit', v || false);
this.compile();
}
get unit(){
return this.getAttribute('unit') || 'number';
}
/**
* get value - Return a evaluation of the compiled expression
* @memberof Input
*
* @return {object|number} The evaluated value
*/
get value(){
// If the compiled is store
if(this.compiled){
// If a unit is define, try to convert value
if(this.unit != 'number' || !this.unit && this.unit != 'false'){
try{
return this.compiled.eval().to(this.unit);
}
catch(e){
this.error = "Conversion failed: Unable to convert to "+this.unit;
this.dispatchEvent(new Event('error', {
cancelable: true,
details:{
message: e.message,
type: e.type
}
}));
this.title = this.error;
}
}
// [Else without block] return evaluated value or false
return this.compiled.eval() || false;
}
}
set value(v){
this.update(false, v);
this._checkChange();
}
get default(){
return this.getAttribute('default') || false;
}
set default(v){
this.setAttribute('default', v || false);
// Update the compilation
this.compile();
}
/**
* @static getHighlightKnowMethods - get html for method name
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {string} r method name
* @return {string} html content
*/
static getHighlightKnowMethods(input, r){
if(input.options.knowMethods){
if(input.options.knowMethods.indexOf(r)!=-1){
return '<span class="cui-method know">'+r+'</span>(';
}
}
return '<span class="cui-method unknow">'+r+'</span>(';
}
/**
* @static getSymbolKnowAlias - get symbol of knowVariables from it alias
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {string} alias alias like 'pi'
* @return {char} utf8 char symbol like 'π'
*/
static getSymbolKnowAlias(input, alias){
alias = alias.toLowerCase();
if(input.options.knowVariables[alias]){
return input.options.knowVariables[alias].symbol;
}
return false;
}
/**
* @static getVariableKnowAlias - get variable object from alias
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {string} alias alias like 'pi'
* @return {object} the variable object
*/
static getVariableKnowAlias(input, alias){
alias = alias.toLowerCase();
var r;
if(r = input.options.knowVariables[alias]) return r;
return false;
}
/**
* @static getVariableKnowSymbol - get variable from symbol
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {char} symbol utf8 char symbol like 'π'
* @return {object} the variable object
*/
static getVariableKnowSymbol(input, symbol){
var knowVariables = input.options.knowVariables;
for (var i in knowVariables){
if(knowVariables[i].symbol == symbol){
return knowVariables[i];
}
}
return false;
}
/**
* @static getAliasKnowSymbol - get alias from symbol
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {char} symbol utf8 char symbol like 'π'
* @return {string} alias like 'pi'
*/
static getAliasKnowSymbol(input, symbol){
var knowVariables = input.options.knowVariables;
for (var i in knowVariables){
if(knowVariables[i].symbol == symbol){
return i;
}
}
return false;
}
/**
* @static getHighlightKnowVariables - get html for variable name
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {string} r variable name
* @return {string} html content
*/
static getHighlightKnowVariables(input, r){
if(input.options.knowVariables){
if(Input.getAliasKnowSymbol(input, r)){
return '<span class="cui-var">'+r+'</span>';
}
}
return false;
}
/**
* @static getHighlightKnowAtoms - get html for atom name
* @memberof Input
*
* @param {Cluster.Ui.Input} input cluster-input object
* @param {string} name atom name
* @return {string} html content
*/
static getHighlightKnowAtoms(input, name){
if(input.options.knowAtoms){
return name[0]+'<span class="cui-atom">'+name.substring(1, name.length-1)+'</span>'+name[name.length-1];
}
return false;
}
}
Cluster.css('cluster-ui/cluster-ui-input/stylesheets/component.css');
Input.tag = 'input';
Cluster.Ui.define(Input);
Input.classSuffix = 'input';
Input.className = Cluster.Ui.className+'-'+Input.classSuffix;
function createRange(node, chars, range) {
if (!range) {
range = document.createRange()
range.selectNode(node);
range.setStart(node, 0);
}
if (chars.count === 0) {
range.setEnd(node, chars.count);
} else if (node && chars.count >0) {
if (node.nodeType === Node.TEXT_NODE) {
if (node.textContent.length < chars.count) {
chars.count -= node.textContent.length;
} else {
range.setEnd(node, chars.count);
chars.count = 0;
}
} else {
for (var lp = 0; lp < node.childNodes.length; lp++) {
range = createRange(node.childNodes[lp], chars, range);
if (chars.count === 0) {
break;
}
}
}
}
return range;
};
function setCurrentCursorPosition(element,chars) {
if (chars >= 0) {
var selection = window.getSelection();
var range = createRange(element, { count: chars });
if (range) {
range.collapse(false);
selection.removeAllRanges();
selection.addRange(range);
}
}
};
function isChildOf(node, parent) {
while (node !== null) {
if (node === parent) {
return true;
}
node = node.parentNode;
}
return false;
};
function getCurrentCursorPosition(parent) {
//var parentId = parent.id;
var selection = window.getSelection(),
charCount = -1,
node;
if (selection.focusNode) {
if (isChildOf(selection.focusNode, parent)) {
node = selection.focusNode;
charCount = selection.focusOffset;
while (node) {
if (node === parent) {
break;
}
if (node.previousSibling) {
node = node.previousSibling;
charCount += node.textContent.length;
} else {
node = node.parentNode;
if (node === null) {
break;
}
}
}
}
}
return charCount;
};