src/js/gui.js
/*! @file gui.js */
/*
* part of the 'Transfinite Ordinal Calculator'
* author: Claudio Kressibucher
* license: GNU LGPL
*/
// The gui object will handle the interaction with the gui.
// It works as the basic layer of the application
/* JSHINT global declaration */
/*global oGui:true, util:true, go:true, model:true, config:true, ClassedError:true, xGetElementById, xAddEventListener, xAddClass, xRemoveClass, xGetElementsByClassName, xHasClass */
/**
* The object which communicates with the HTML-GUI. It is
* initialized by the function go.initGui at the start of
* the application.
* @class oGui
* @static
*/
var oGui;
go.initGui = function initGui(){
// this object is returned as "gui object"
var obj = {};
// HTMLElement: Input Keys
var keypad = xGetElementById('keys');
// HTMLElement: the wrapAll div
var wrapAll = xGetElementById('wrapAll');
// HTMLElement: the backspace key
var bsKey = xGetElementById('bspace');
// HTMLElement: input / result field (div#innerResult)
var mainDisplay = xGetElementById('innerResult');
// HTMLElement: div#outerResult
var outerMainDisplay = xGetElementById('outerResult');
// HTMLElement: compare display (div#innerCompLine)
var cmpDisplay = xGetElementById('innerCompLine');
/**
* Method to init the gui console (if an own console object
* is used instead of the browser's console)
* @method initGuiConsole
* @private
*/
var initGuiConsole = function (){
obj.guiConsole = document.createElement('div');
obj.guiConsole.setAttribute('class', 'console');
tmp = document.createElement('h2');
xAddClass(tmp, 'consoleTitle');
tmp.innerHTML = "Console";
document.body.appendChild(tmp);
document.body.appendChild(obj.guiConsole);
obj.out = {
print: function (msg, cls){
var el = document.createElement('p');
var txt = document.createTextNode(msg);
el.appendChild(txt);
if (cls) {
xAddClass(el, cls);
}
obj.guiConsole.insertBefore(el, obj.guiConsole.firstChild);
},
error: function (msg){
this.print(msg, "consoleErr");
},
warn: function (msg){
this.print(msg, "consoleWarn");
},
log: function (msg){
this.print(msg, "consoleLog");
}
};
};
// button to augment display when it was reduced and to reduce when it was augmented
var toggleBtn = document.createElement('span');
var dots = document.createElement('div'); // to append when reduced (indicating reduced state)
// flag to indicate an overflow in mainDisplay
var bOverflow = false;
var mDisContent = ""; // store last content of the display to check if it must be updated or not
var tmp; // temporary variable for anything ...
/**
* Print debug messages of different levels to the console
* object. Is called by the methods dbgErr, dbgWarn and dbgLog;
* don't call it directly...
* @method debug
* @param {string} msg The message to show
* @param {number} level The debug level (see config.debug)
* @private
*/
var debug = function debug(msg, level){
if (!msg) msg = '(no message)';
if (level <= config.debug && oGui.out !== null ) {
switch(level){
case 1: oGui.out.error(msg); break;
case 2: oGui.out.warn(msg); break;
case 3: oGui.out.log(msg); break;
}
}
};
/**
* Event to register a kind of a click... (working with touch devices
* and non-touch devices).
* @property selEvt
* @type string
* @private
*/
var selEvt = "ontouchend" in document ? "touchend" : "mouseup";
/**
* Event used to detect when a key is pressed down
* @property downEvt
* @type string
* @private
*/
var downEvt = "ontouchstart" in document ? "touchstart" : "mousedown";
/**
* Event handler to toggle the main display mode from augmented
* state to reduced or from reduced (or from no-overflow) to
* augmented state. May also be called directly (not as an event
* handler), this method doesn't depend on an event parameter.
* @method fnToggleDMode
* @private
*/
var fnToggleDMode = function (){
// release "hover"-status
xRemoveClass(toggleBtn, 'btnHover');
if (!bOverflow || !xHasClass(outerMainDisplay, 'augmented')){ // expand
if (bOverflow){ // not the first expanding: elements exist
outerMainDisplay.removeChild(dots);
xRemoveClass(mainDisplay, 'rightMarg'); // right margin for dots
} else {
// add toggle button
outerMainDisplay.insertBefore(toggleBtn, mainDisplay);
xAddClass(mainDisplay, 'leftMarg'); // left margin: space for toggle button
}
// add augmented class 5 milliseconds later to enable css animation
setTimeout(function(){
xAddClass(outerMainDisplay, 'augmented');
}, 5);
} else { // reduce
outerMainDisplay.insertBefore(dots, mainDisplay);
xAddClass(mainDisplay, 'rightMarg');
xRemoveClass(outerMainDisplay, 'augmented');
}
};
/**
* When display was overflowed, and now the content is small
* enough again to fit in the display, this method is called
* to remove HTMLElements and formatting used for reduced
* and augmented states. This method is normally called directly
* (not as an event handler).
* @method fnToNormalSizedisp
* @private
*/
var fnToNormalSizeDisp = function (){
if (bOverflow){ // only if it was in reduced or augmented state
xRemoveClass(outerMainDisplay, 'augmented');
xRemoveClass(mainDisplay, 'leftMarg');
outerMainDisplay.removeChild(toggleBtn);
try {
outerMainDisplay.removeChild(dots);
xRemoveClass(mainDisplay, 'rightMarg');
} catch (e){
// node dots is not in the DOM, nothing to do
}
}
};
/**
* Handles a click on one of the input panel's keys
* @method fnKey
* @param evt The event object (the activated key or window.event in IE)
* @private
*/
var fnKey = function fnKey(evt){
// init
var target, cmd, ch;
var err = null;
var hovArr, i, l; // btnHover array, index and length
if (!evt) {
evt = window.event; // IE
}
target = evt.target || evt.srcElement;
// perhaps the user clicked on the inner span element: set target to outer span
if ( !xHasClass(target, 'btn') ){
target = target.parentNode;
if (!xHasClass(target, 'btn')) return; // the original target was an outer element
}
// release "hover"-status
hovArr = xGetElementsByClassName('btnHover', keypad, 'span');
l = hovArr.length;
for (i=l-1; i>=0; i--){
xRemoveClass(hovArr[i], 'btnHover');
}
if (xHasClass(bsKey, 'btnHover')){
xRemoveClass(bsKey, 'btnHover');
}
if (xHasClass(target, 'inactive')){
oGui.dbgLog("User clicked on inactive button...");
return;
}
cmd = target.getAttribute('data-cmd');
ch = target.getAttribute('data-char');
if (cmd === "add-char"){
model.addChar(ch);
} else if (cmd === "calc"){
model.addChar("=");
} else if (cmd === "clear"){
model.resetModel();
} else if (cmd === "clrall"){
model.clearRegister();
} else if (cmd === "stoRcl"){
if (ch === "R1"){ // register 1
if (xHasClass(target, 'sto')){
model.store(0);
} else { // rcl
model.recall(0);
}
} else { // register 2
if (xHasClass(target, 'sto')){
model.store(1);
} else { // rcl
model.recall(1);
}
}
} else if (cmd === "bspace") {
model.backSpace();
} else {
err = new ClassedError("Unknown Operation", ClassedError.types.progError);
err.handleErr();
}
};
/**
* Shows a popup with the complete compare register
* values. Triggered by Click on compare display.
* @method fnPopupCompare
*/
var fnPopupCompare = function(){
var frag; // document fragment
var tDiv; // temp div element
var cmpResStr;
var opnds = model.getCmpOpnds();
var result = model.getCmpResult();
if (opnds !== null && result !== null){
// cmp resutl string
if (result > 0){
cmpResStr = ">";
} else if (result < 0){
cmpResStr = "<";
} else if (result === 0){
cmpResStr = "=";
} else {
oGui.dbgErr("wrong type of resutl in oGui.fnPopupCompare");
cmpResStr = "?";
}
// create and fill fragment...
frag = document.createDocumentFragment();
// Register A: Label
tDiv = document.createElement('div');
tDiv.className = "cmpTitle";
tDiv.innerHTML = "A =";
frag.appendChild(tDiv);
// Register A: Value
tDiv = document.createElement('div');
tDiv.className = "cmpContent";
tDiv.innerHTML = opnds[0].toString(true);
frag.appendChild(tDiv);
// Register B: Label
tDiv = document.createElement('div');
tDiv.className = "cmpTitle";
tDiv.innerHTML = "B =";
frag.appendChild(tDiv);
// Register B: Value
tDiv = document.createElement('div');
tDiv.className = "cmpContent";
tDiv.innerHTML = opnds[1].toString(true);
frag.appendChild(tDiv);
// Comparison: Label
tDiv = document.createElement('div');
tDiv.className = "cmpTitle";
tDiv.innerHTML = "Comparison:";
frag.appendChild(tDiv);
// Comparison: Result
tDiv = document.createElement('div');
tDiv.className = "cmpContent";
tDiv.innerHTML = "<span class=\"it\">A </span>" + cmpResStr + "<span class=\"it\"> B</span>";
frag.appendChild(tDiv);
// show hint
oGui.showHint(frag, "OK");
} // else: do nothing, errors are already handled by model
};
// functions used by showHint to show an lightBox before an overlay layer
/**
* Event Handler to remove the overlay layer again.
* @method fnRemoveOL
* @private
*/
var fnRemoveOL = function (){
var el = xGetElementById('ovlay');
var inner;
if (el){
inner = xGetElementById('innerOvlay');
if (inner){
inner.className = "hidden"; // transition effect
setTimeout(function(){
document.body.removeChild(el);
}, 550);
} else {
document.body.removeChild(el);
}
}
};
/**
* Method to create a semi-transparent overlay layer
* with a lightbox as a smoother alert...
* @method fnOverlay
* @param {HTMLElement} content The Element to show in the lightbox
* @param {string} closeBtnText A short text, e.g. "close" or "OK" or
* similar, which is used on a button inside the lightbox
*/
var fnOverlay = function (content, closeBtnText){
var layer = document.createElement('div');
var inner = document.createElement('div');
var btn = document.createElement('span'); // the close button
var btnLine = document.createElement('div');
content.className = content.className + (content.className ? " " : "") + "ovlContent";
layer.id = "ovlay";
inner.id = "innerOvlay";
inner.className = "hidden";
btnLine.className = "topBorder btnLine";
btn.className = "btn dark singleOnLine"; // the only button on a line
btn.innerHTML = closeBtnText;
xAddEventListener(btn, selEvt, fnRemoveOL, false);
btnLine.appendChild(btn);
content.appendChild(btnLine);
inner.appendChild(content);
layer.appendChild(inner);
document.body.appendChild(layer);
// remove class hidden to start transition effect (needs a little delay)
setTimeout(function(){inner.className = "";}, 5);
};
// init some HTML Elements...
dots.innerHTML = "..."; // dots element for reduced state
dots.id = "reduceDots";
// toggle button
toggleBtn.id = "toggleBtn";
toggleBtn.className = "btn toggleBtn dark";
toggleBtn.innerHTML = "<img alt=\"\" src=\"img/arrow_down.png\" />";
// add event listener to toggle button
xAddEventListener(toggleBtn, selEvt, fnToggleDMode, false);
// === error and debug methods and similar things... (public) ===
/**
* Will show an error message on the gui, indicating
* that some software error has occurred. Should be invoked,
* when the result may be incorrect due to a program error.
* Note: This method is not for development logs, but
* for error notification to the USER, even in
* the productive state.<br />
* This method throws an error that shouldn't be handled anywhere, so
* it executes the script
* @method progError
* @param {String} msg The message to log to the console as error message. (The
* user won't see this message! So it may be verbose...)
*/
obj.progError = function progError(msg){
var err;
this.dbgErr("program error: " + (msg ? msg : ""));
// reset the model, as we don't know what happend, and if the result is correct...
model.resetModel();
// generate an error to inform the user (not the verbose info of msg
err = new ClassedError("A program error has occurred... please retry", ClassedError.types.progError);
err.handleErr();
throw ''; // exit further execution
};
/**
* This is not an error, but can be used to inform the
* user without doing anything else.
* @method showHint
* @param {string / DocumentFragment} msg The message showed to the user.
* @param {string} closeBtnText Optional. The Text of the close button
* of the hint dialog. If omitted, the Text defaults to "Close".
*/
obj.showHint = function showHint(msg, closeBtnText){
var el;
closeBtnText = closeBtnText ? closeBtnText : "Close";
if (msg){
el = document.createElement('div');
if (typeof(msg) === 'string'){
el.innerHTML = msg;
} else {
// msg should be a document fragment
el.appendChild(msg);
}
fnOverlay(el, closeBtnText);
}
};
/**
* Debug an Error. If it will really be showed and where it will be showed
* is defined by the properties of the global config object.
* @method dbgErr
* @param {String} msg The message to show.
*/
obj.dbgErr = function dbgErr(msg){
debug(msg, 1);
};
/**
* Debug a Warning or non-fatal error. If it will really be showed and
* where it will be showed is defined by the properties of the global
* config object.
* @method dbgWarn
* @param {String} msg The message to show.
*/
obj.dbgWarn = function dbgWarn(msg){
debug(msg, 2);
};
/**
* Debug a log message. If it will really be showed and where it will be showed
* is defined by the properties of the global config object.
* @method dbgLog
* @param {String} msg The message to show.
*/
obj.dbgLog = function dbgLog(msg){
debug(msg, 3);
};
// === update methods ====
/**
* Call this method to inform the gui, that the model
* has changed. This method will then look for the needed
* information and update the gui.
* @method update
*/
obj.update = function(){
var state = model.getState();
var stack = model.getData();
var cmp;
if (model.regsDirty()){
cmp = model.compareRegs();
if (cmp !== null)
this.updateCmp(cmp);
}
if (mDisContent !== stack){
mDisContent = stack;
this.updateMainDisplay('<div>' + stack + '</div>');
}
this.updateKeys(state);
oGui.dbgLog("Model State: " + model.getState(true));
};
/**
* Update the keys (depending on the model state given
* as argument, some keys may be set inactive)
* @method updateKeys
* @param {model.enumState} state The state of the model
* @private
*/
obj.updateKeys = function (state){
var keys = keypad;
var eS = model.enumState;
var resultLine; // parent of bsKey
var clearKey = xGetElementById('clearKey');
var inact = []; // array with elements to set inactive
var ta; // temporary array
var i, l; // index and length of ta
// remove keys from DOM while changing classes to improve performance (by avoiding layout reflows)
keys.parentNode.removeChild(keys); // all normal keys
resultLine = bsKey.parentNode;
resultLine.removeChild(bsKey); // the backspace key (not in the keys div)
// helper functions
var setInactive = function(key){
xAddClass(key, 'inactive');
};
var setActive = function(key){
xRemoveClass(key, "inactive");
};
var setAllActive = function(){
ta = xGetElementsByClassName("btn", keys, "span");
l = ta.length;
for (i=0; i<l; i++){
setActive(ta[i]);
}
setActive(bsKey);
};
// type: "c" for clear, "ac" for all-clear
var setClearText = function(type){
var current = clearKey.getAttribute('data-cmd');
if (type === 'ac' && current !== 'clrall'){
clearKey.innerHTML = '<span>AC</span>';
clearKey.setAttribute('data-cmd', 'clrall');
} else if (type === 'c' && current !== 'clear') {
clearKey.innerHTML = '<span>C</span>';
clearKey.setAttribute('data-cmd', 'clear');
}
};
// type: "s" for STO or "r" for RCL
var setStoRegText = function(type){
var stoRcl;
var len, i;
var fn = function(el){
var str;
if (type === "s"){
str = "STO";
xAddClass(el, "sto");
xRemoveClass(el, "rcl");
} else {
str = "RCL";
xAddClass(el, "rcl");
xRemoveClass(el, "sto");
}
el.firstChild.innerHTML = str;
};
stoRcl = xGetElementsByClassName("sto", keys, "span");
len = stoRcl.length;
if (len === 2 && type === "r"){
// iterate: change to RCL
for (i = len-1; i >= 0; i--){
fn(stoRcl[i]);
}
} else if (type === "s" && len === 0) { // iterate: change to STO
stoRcl = xGetElementsByClassName("rcl", keys, "span");
len = stoRcl.length;
for (i=len-1; i>=0; i--){
fn(stoRcl[i]);
}
}
};
// reset: everything is active
setAllActive();
// depending on number of open brackets set calc or rightBracket to inactive
if (model.getNumOfOpenBr() <= 0) {
ta = xGetElementsByClassName("rightBra", keys, "span");
} else {
ta = xGetElementsByClassName("calc", keys, "span");
}
inact = inact.concat(ta);
// text of clear button
if (state !== eS.init){
setClearText("c");
}
// backspace button
if (state === eS.init || state === eS.calc){
setInactive(bsKey);
} // else active (is already active, since setAllActive was called...
switch(state){
case eS.init:
inact = inact.concat(
xGetElementsByClassName("optor", keys, "span"),
xGetElementsByClassName("cmp", keys, "span"),
xGetElementsByClassName("calc", keys, "span") );
if (model.regsEmpty()){
setInactive(clearKey);
} else {
setClearText("ac");
}
setStoRegText("r");
break;
case eS.digit:
inact = inact.concat(
xGetElementsByClassName("const", keys, "span"),
xGetElementsByClassName("leftBra", keys, "span") );
setStoRegText("s");
break;
case eS.optor:
inact = inact.concat(
xGetElementsByClassName("optor", keys, "span"),
xGetElementsByClassName("rightBra", keys, "span"),
xGetElementsByClassName("calc", keys, "span") );
setStoRegText("r");
break;
case eS.constant:
case eS.rightBracket:
inact = inact.concat(
xGetElementsByClassName("const", keys, "span"),
xGetElementsByClassName("num", keys, "span"),
xGetElementsByClassName("leftBra", keys, "span") );
setStoRegText("s");
break;
case eS.calc:
inact = inact.concat( xGetElementsByClassName("calc", keys, "span") );
setStoRegText("s");
break;
case eS.leftBracket:
inact = inact.concat(
xGetElementsByClassName("optor", keys, "span"),
xGetElementsByClassName("rightBra", keys, "span") );
setStoRegText("r");
break;
case eS.stored:
setStoRegText("r");
break;
default:
}
// set all elements from array inact to state 'inactive'
l = inact.length;
for (i=l-1; i>=0; i--){
setInactive(inact[i]);
}
// re-insert elements into dom
wrapAll.appendChild(keys);
resultLine.insertBefore(bsKey, outerMainDisplay);
};
/**
* Method to update the compare line to show the
* relationship between the two registers.
* @method updateCmp
* @param {DocumentFragment} cmpDocFrag The document fragment containing
* the content of the compare line
* @private
*/
obj.updateCmp = function updateCmp(cmpDocFrag){
var l, r; // left and right operand
var el;
var i;
// register fields (labels)
var fields = xGetElementsByClassName("rField", cmpDisplay, "div");
l = cmpDocFrag.childNodes[0];
r = cmpDocFrag.childNodes[2];
cmpDisplay.innerHTML = "";
for (i=fields.length-1; i>=0; i--){
cmpDocFrag.insertBefore( fields[i], cmpDocFrag.firstChild );
}
cmpDisplay.appendChild(cmpDocFrag);
// check height of left operand:
if (l.firstChild.offsetHeight > cmpDisplay.offsetHeight){
oGui.dbgLog("left operand needs to much space");
// overflow: signalise overflow
el = document.createElement('div');
el.innerHTML = "...";
el.className = "cmpDots";
cmpDisplay.insertBefore(el, cmpDisplay.childNodes[3]);
}
if (r.firstChild.offsetHeight > cmpDisplay.offsetHeight){
oGui.dbgLog("right operand needs to much space");
// overflow: signalise overflow
el = document.createElement('div');
el.innerHTML = "...";
el.className = "cmpDots";
cmpDisplay.appendChild(el);
}
};
/**
* Update the result field's content
* @method updateMainDisplay
* @param {string} content The new content (innerHTML) to be set
* @private
*/
obj.updateMainDisplay = function updateMainDisplay(content){
var actOverflow; // overflow detected now
var stateAugm = false;
mainDisplay.innerHTML = content;
// check overflow...
if (xHasClass(outerMainDisplay, 'augmented')){
// the div element must be in normal state (overflow: hidden)
// to ckeck the overflow (use clipped height of outer div).
xRemoveClass(outerMainDisplay, 'augmented');
stateAugm = true;
}
// offsetHeight includes borders, clientHeight not (but padding)
actOverflow = mainDisplay.offsetHeight > outerMainDisplay.clientHeight;
if (stateAugm){ // re-insert class augmented
xAddClass(outerMainDisplay, 'augmented');
}
if (actOverflow){
if (!bOverflow){
// overflow is new
oGui.dbgLog("overflow in mainDisplay");
// handle overflow
fnToggleDMode();
bOverflow = true;
}
} else {
// no overflow
if (bOverflow){ // overflow disappeared..
fnToNormalSizeDisp();
bOverflow = false;
}
}
};
// add event listeners
xAddEventListener(keypad, selEvt, fnKey, false);
xAddEventListener(bsKey, selEvt, fnKey, false);
// when a key is pressed down, change css class to "aktiv"-style (class "btnHover")
xAddEventListener(wrapAll, downEvt, function (evt){
// init
var target;
if (!evt) {
evt = window.event; // IE
}
target = evt.target || evt.srcElement;
// perhaps the user clicked on the inner span element: set target to outer span
if ( !xHasClass(target, 'btn') ){
target = target.parentNode;
if (!xHasClass(target, 'btn')) return; // the original target was an outer element...
}
if (xHasClass(target, 'inactive')){
return;
}
xAddClass(target, 'btnHover');
}, false);
// open the information dialog when 'about' button is pressed
xAddEventListener(xGetElementById('aboutLink'), selEvt, function(){
var msg = document.createDocumentFragment();
var btn = document.createElement('a');
var p = document.createElement('p');
var sub = document.createElement('sub');
sub.innerHTML = "0";
// p element
p.innerHTML = "Ordinal Calculator, Version " + config.projectVersion + "<br />A simple calculator that performs arithmetic operations with ordinal numbers up " +
"to (but not including) ε";
p.appendChild(sub);
p.appendChild( document.createTextNode('.') );
p.className = "leftAlign bottomSpace";
msg.appendChild(p);
// link to project site
btn.className = "btn dark";
btn.href = config.projectUrl;
btn.target = "_blank";
btn.innerHTML = "Go to project site";
msg.appendChild(btn);
oGui.showHint(msg);
}, false);
// compare display: show popup
xAddEventListener(cmpDisplay, selEvt, fnPopupCompare, false);
// determine which console object should be used (if any) and
// set it to oGui.out...
if (config.debug <= 0){
// productive state, no logs
obj.out = null; // oGui.debug checks if oGui.out is null
} else if (config.prohibitOwnConsole) {
if (console && console.log && console.warn && console.error){
// browser console available. Because an own console is
// prohibited anyway, the property config.useOwnConsole doesn't matter
obj.out = console;
} else {
// browser console doesn't exits and an own object isn't allowed:
// logs will be trashed
obj.out = null;
}
} else {
// logging required...
if (config.useOwnConsole || !console ||
!console.log || !console.warn || !console.error){
initGuiConsole();
} else {
// browser console exist
obj.out = console;
}
}
// return the created object
return obj;
};