From 8ae7720866e60f95ea000b7b48a93c349932d676 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 1 Jul 2017 08:04:53 -0400 Subject: [PATCH 01/79] minor --- A | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 A diff --git a/A b/A deleted file mode 100644 index e69de29..0000000 From 326638b2e1947a56a6cfb5243c354ed5f5e15cf0 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 1 Jul 2017 08:17:21 -0400 Subject: [PATCH 02/79] improved RANDOM.random --- .travis.yml | 1 - lib/random.js | 33 ++++++++++++++++++++++++--------- 2 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index b3c4280..8fc1005 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ sudo: false language: node_js node_js: - - 0.12 - 4.5 before_install: diff --git a/lib/random.js b/lib/random.js index 06b7e23..9ab8bf7 100644 --- a/lib/random.js +++ b/lib/random.js @@ -1,6 +1,6 @@ /** * # RANDOM - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Generates pseudo-random numbers @@ -14,18 +14,31 @@ /** * ## RANDOM.random * - * Generates a pseudo-random floating point number between - * [a,b), a inclusive and b exclusive. + * Generates a pseudo-random floating point number in interval [a,b) * - * @param {number} a The lower limit - * @param {number} b The upper limit + * Interval is a inclusive and b exclusive. + * + * If b is undefined, the interval is [0, a). + * + * If both a and b are undefined the interval is [0, 1) + * + * @param {number} a Optional. The lower limit, or the upper limit + * if b is undefined + * @param {number} b Optional. The upper limit * * @return {number} A random floating point number in [a,b) */ RANDOM.random = function(a, b) { var c; - a = ('undefined' === typeof a) ? 0 : a; - b = ('undefined' === typeof b) ? 0 : b; + if ('undefined' === typeof b) { + if ('undefined' === typeof a) { + return Math.random(); + } + else { + b = a; + a = 0; + } + } if (a === b) return a; if (b < a) { @@ -41,6 +54,8 @@ * * Generates a pseudo-random integer between (a,b] a exclusive, b inclusive * + * @TODO: Change to interval [a,b], and allow 1 parameter for [0,a) + * * @param {number} a The lower limit * @param {number} b The upper limit * @@ -56,9 +71,9 @@ /** * ## RANDOM.sample * - * Generates a randomly shuffled sequence of numbers in (a,b) + * Generates a randomly shuffled sequence of numbers in [a,b)] * - * Both _a_ and _b_ are inclued in the interval. + * Both _a_ and _b_ are included in the interval. * * @param {number} a The lower limit * @param {number} b The upper limit From d6023f6bddd5969782a9bd02731054e3c289bc65 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 1 Jul 2017 09:42:25 -0400 Subject: [PATCH 03/79] transforming DOM --- build/jsus.js | 1394 ++++++++++++++++++++++++++++++++++++++- build/jsus.min.js | 2 +- jsus.js | 6 +- lib/dom.js | 251 ++------ lib/dom_legacy.js | 1573 +++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 3026 insertions(+), 200 deletions(-) create mode 100644 lib/dom_legacy.js diff --git a/build/jsus.js b/build/jsus.js index 652a77d..0345bf7 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -1522,6 +1522,1367 @@ //return scanDocuments(prefix); }; + /** + * ### DOM.getLabel + * + */ + DOM.getLabel = function(forElem, id, labelText, attributes) { + if (!forElem) return false; + var label = document.createElement('label'); + label.id = id; + label.appendChild(document.createTextNode(labelText)); + + if ('undefined' === typeof forElem.id) { + forElem.id = this.generateUniqueId(); + } + + label.setAttribute('for', forElem.id); + this.addAttributes2Elem(label, attributes); + return label; + }; + + /** + * ### DOM.addLabel + * + */ + DOM.addLabel = function(root, forElem, id, labelText, attributes) { + if (!root || !forElem || !labelText) return false; + var l = this.getLabel(forElem, id, labelText, attributes); + root.insertBefore(l, forElem); + return l; + }; + + // ## CSS / JS + + /** + * ### DOM.addCSS + * + * If no root element is passed, it tries to add the CSS + * link element to document.head, document.body, and + * finally document. If it fails, returns FALSE. + * + */ + DOM.addCSS = function(root, css, id, attributes) { + root = root || document.head || document.body || document; + if (!root) return false; + + attributes = attributes || {}; + + attributes = JSUS.merge(attributes, {rel : 'stylesheet', + type: 'text/css', + href: css + }); + + return this.addElement('link', root, id, attributes); + }; + + /** + * ### DOM.addJS + * + */ + DOM.addJS = function(root, js, id, attributes) { + root = root || document.head || document.body || document; + if (!root) return false; + + attributes = attributes || {}; + + attributes = JSUS.merge(attributes, {charset : 'utf-8', + type: 'text/javascript', + src: js + }); + + return this.addElement('script', root, id, attributes); + }; + + /** + * ### DOM.highlight + * + * Provides a simple way to highlight an HTML element + * by adding a colored border around it. + * + * Three pre-defined modes are implemented: + * + * - OK: green + * - WARN: yellow + * - ERR: red (default) + * + * Alternatively, it is possible to specify a custom + * color as HEX value. Examples: + * + * ```javascript + * highlight(myDiv, 'WARN'); // yellow border + * highlight(myDiv); // red border + * highlight(myDiv, '#CCC'); // grey border + * ``` + * + * @param {HTMLElement} elem The element to highlight + * @param {string} code The type of highlight + * + * @see DOM.addBorder + * @see DOM.style + */ + DOM.highlight = function(elem, code) { + var color; + if (!elem) return; + + // default value is ERR + switch (code) { + case 'OK': + color = 'green'; + break; + case 'WARN': + color = 'yellow'; + break; + case 'ERR': + color = 'red'; + break; + default: + if (code.charAt(0) === '#') { + color = code; + } + else { + color = 'red'; + } + } + + return this.addBorder(elem, color); + }; + + /** + * ### DOM.addBorder + * + * Adds a border around the specified element. Color, + * width, and type can be specified. + */ + DOM.addBorder = function(elem, color, width, type) { + var properties; + if (!elem) return; + + color = color || 'red'; + width = width || '5px'; + type = type || 'solid'; + + properties = { border: width + ' ' + type + ' ' + color }; + return DOM.style(elem, properties); + }; + + /** + * ### DOM.style + * + * Styles an element as an in-line css. + * + * Existing style properties are maintained, and new ones added. + * + * @param {HTMLElement} elem The element to style + * @param {object} Objects containing the properties to add. + * + * @return {HTMLElement} The styled element + */ + DOM.style = function(elem, properties) { + var i; + if (!elem || !properties) return; + if (!DOM.isElement(elem)) return; + + for (i in properties) { + if (properties.hasOwnProperty(i)) { + elem.style[i] = properties[i]; + } + } + return elem; + }; + + /** + * ### DOM.removeClass + * + * Removes a specific class from the classNamex attribute of a given element + * + * @param {HTMLElement} el An HTML element + * @param {string} c The name of a CSS class already in the element + * + * @return {HTMLElement|undefined} The HTML element with the removed + * class, or undefined if the inputs are misspecified + */ + DOM.removeClass = function(el, c) { + var regexpr, o; + if (!el || !c) return; + regexpr = new RegExp('(?:^|\\s)' + c + '(?!\\S)'); + o = el.className = el.className.replace( regexpr, '' ); + return el; + }; + + /** + * ### DOM.addClass + * + * Adds one or more classes to the className attribute of the given element + * + * Takes care not to overwrite already existing classes. + * + * @param {HTMLElement} el An HTML element + * @param {string|array} c The name/s of CSS class/es + * + * @return {HTMLElement|undefined} The HTML element with the additional + * class, or undefined if the inputs are misspecified + */ + DOM.addClass = function(el, c) { + if (!el) return; + if (c instanceof Array) c = c.join(' '); + else if ('string' !== typeof c) return; + if (!el.className || el.className === '') el.className = c; + else el.className += (' ' + c); + return el; + }; + + /** + * ### DOM.getElementsByClassName + * + * Returns an array of elements with requested class name + * + * @param {object} document The document object of a window or iframe + * @param {string} className The requested className + * @param {string} nodeName Optional. If set only elements with + * the specified tag name will be searched + * + * @return {array} Array of elements with the requested class name + * + * @see https://gist.github.com/E01T/6088383 + * @see http://stackoverflow.com/ + * questions/8808921/selecting-a-css-class-with-xpath + */ + DOM.getElementsByClassName = function(document, className, nodeName) { + var result, node, tag, seek, i, rightClass; + result = []; + tag = nodeName || '*'; + if (document.evaluate) { + seek = '//' + tag + + '[contains(concat(" ", normalize-space(@class), " "), "' + + className + ' ")]'; + seek = document.evaluate(seek, document, null, 0, null ); + while ((node = seek.iterateNext())) { + result.push(node); + } + } + else { + rightClass = new RegExp( '(^| )'+ className +'( |$)' ); + seek = document.getElementsByTagName(tag); + for (i = 0; i < seek.length; i++) + if (rightClass.test((node = seek[i]).className )) { + result.push(seek[i]); + } + } + return result; + }; + + // ## IFRAME + + /** + * ### DOM.getIFrameDocument + * + * Returns a reference to the document of an iframe object + * + * @param {HTMLIFrameElement} iframe The iframe object + * + * @return {HTMLDocument|null} The document of the iframe, or + * null if not found. + */ + DOM.getIFrameDocument = function(iframe) { + if (!iframe) return null; + return iframe.contentDocument || + iframe.contentWindow ? iframe.contentWindow.document : null; + }; + + /** + * ### DOM.getIFrameAnyChild + * + * Gets the first available child of an IFrame + * + * Tries head, body, lastChild and the HTML element + * + * @param {HTMLIFrameElement} iframe The iframe object + * + * @return {HTMLElement|undefined} The child, or undefined if none is found + */ + DOM.getIFrameAnyChild = function(iframe) { + var contentDocument; + if (!iframe) return; + contentDocument = W.getIFrameDocument(iframe); + return contentDocument.head || contentDocument.body || + contentDocument.lastChild || + contentDocument.getElementsByTagName('html')[0]; + }; + + // ## RIGHT-CLICK + + /** + * ### DOM.disableRightClick + * + * Disables the popup of the context menu by right clicking with the mouse + * + * @param {Document} Optional. A target document object. Defaults, document + * + * @see DOM.enableRightClick + */ + DOM.disableRightClick = function(doc) { + doc = doc || document; + if (doc.layers) { + doc.captureEvents(Event.MOUSEDOWN); + doc.onmousedown = function clickNS4(e) { + if (doc.layers || doc.getElementById && !doc.all) { + if (e.which == 2 || e.which == 3) { + return false; + } + } + }; + } + else if (doc.all && !doc.getElementById) { + doc.onmousedown = function clickIE4() { + if (event.button == 2) { + return false; + } + }; + } + doc.oncontextmenu = new Function("return false"); + }; + + /** + * ### DOM.enableRightClick + * + * Enables the popup of the context menu by right clicking with the mouse + * + * It unregisters the event handlers created by `DOM.disableRightClick` + * + * @param {Document} Optional. A target document object. Defaults, document + * + * @see DOM.disableRightClick + */ + DOM.enableRightClick = function(doc) { + doc = doc || document; + if (doc.layers) { + doc.releaseEvents(Event.MOUSEDOWN); + doc.onmousedown = null; + } + else if (doc.all && !doc.getElementById) { + doc.onmousedown = null; + } + doc.oncontextmenu = null; + }; + + /** + * ### DOM.addEvent + * + * Adds an event listener to an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to handle + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event will initiate a capture. + * Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.removeEvent + * + * Kudos: + * http://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick + */ + DOM.addEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.attachEvent) return element.attachEvent('on' + event, func); + else return element.addEventListener(event, func, capture); + }; + + /** + * ### DOM.removeEvent + * + * Removes an event listener from an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to remove + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event was registered + * as a capture. Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.addEvent + */ + DOM.removeEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.detachEvent) return element.detachEvent('on' + event, func); + else return element.removeEventListener(event, func, capture); + }; + + /** + * ### DOM.disableBackButton + * + * Disables/re-enables backward navigation in history of browsed pages + * + * When disabling, it inserts twice the current url. + * + * It will still be possible to manually select the uri in the + * history pane and nagivate to it. + * + * @param {boolean} disable Optional. If TRUE disables back button, + * if FALSE, re-enables it. Default: TRUE. + * + * @return {boolean} The state of the back button (TRUE = disabled), + * or NULL if the method is not supported by browser. + */ + DOM.disableBackButton = (function(isDisabled) { + return function(disable) { + disable = 'undefined' === typeof disable ? true : disable; + if (disable && !isDisabled) { + if (!history.pushState || !history.go) { + node.warn('DOM.disableBackButton: method not ' + + 'supported by browser.'); + return null; + } + history.pushState(null, null, location.href); + window.onpopstate = function(event) { + history.go(1); + }; + } + else if (isDisabled) { + window.onpopstate = null; + } + isDisabled = disable; + return disable; + }; + })(false); + + /** + * ### DOM.playSound + * + * Plays a sound + * + * @param {various} sound Audio tag or path to audio file to be played + */ + DOM.playSound = 'undefined' === typeof Audio ? + function() { + console.log('JSUS.playSound: Audio tag not supported in your' + + ' browser. Cannot play sound.'); + } : + function(sound) { + var audio; + if ('string' === typeof sound) { + audio = new Audio(sound); + } + else if ('object' === typeof sound && + 'function' === typeof sound.play) { + audio = sound; + } + else { + throw new TypeError('JSUS.playSound: sound must be string' + + ' or audio element.'); + } + audio.play(); + }; + + /** + * ### DOM.onFocusIn + * + * Registers a callback to be executed when the page acquires focus + * + * @param {function|null} cb Callback executed if page acquires focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusIn = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusIn: cb must be function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + + onFocusChange(cb); + }; + + /** + * ### DOM.onFocusOut + * + * Registers a callback to be executed when the page loses focus + * + * @param {function} cb Callback executed if page loses focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusOut = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusOut: cb must be ' + + 'function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + onFocusChange(undefined, cb); + }; + + + /** + * ### DOM.blinkTitle + * + * Changes the title of the page in regular intervals + * + * Calling the function without any arguments stops the blinking + * If an array of strings is provided, that array will be cycled through. + * If a signle string is provided, the title will alternate between '!!!' + * and that string. + * + * @param {mixed} titles New title to blink + * @param {object} options Optional. Configuration object. + * Accepted values and default in parenthesis: + * + * - stopOnFocus (false): Stop blinking if user switched to tab + * - stopOnClick (false): Stop blinking if user clicks on the + * specified element + * - finalTitle (document.title): Title to set after blinking is done + * - repeatFor (undefined): Show each element in titles at most + * N times -- might be stopped earlier by other events. + * - startOnBlur(false): Start blinking if user switches + * away from tab + * - period (1000) How much time between two blinking texts in the title + * + * @return {function|null} A function to clear the blinking of texts, + * or NULL, if the interval was not created yet (e.g. with startOnBlur + * option), or just destroyed. + */ + DOM.blinkTitle = (function(id) { + var clearBlinkInterval, finalTitle, elem; + clearBlinkInterval = function(opts) { + clearInterval(id); + id = null; + if (elem) { + elem.removeEventListener('click', clearBlinkInterval); + elem = null; + } + if (finalTitle) { + document.title = finalTitle; + finalTitle = null; + } + }; + return function(titles, options) { + var period, where, rotation; + var rotationId, nRepeats; + + if (null !== id) clearBlinkInterval(); + if ('undefined' === typeof titles) return null; + + where = 'JSUS.blinkTitle: '; + options = options || {}; + + // Option finalTitle. + if ('undefined' === typeof options.finalTitle) { + finalTitle = document.title; + } + else if ('string' === typeof options.finalTitle) { + finalTitle = options.finalTitle; + } + else { + throw new TypeError(where + 'options.finalTitle must be ' + + 'string or undefined. Found: ' + + options.finalTitle); + } + + // Option repeatFor. + if ('undefined' !== typeof options.repeatFor) { + nRepeats = JSUS.isInt(options.repeatFor, 0); + if (false === nRepeats) { + throw new TypeError(where + 'options.repeatFor must be ' + + 'a positive integer. Found: ' + + options.repeatFor); + } + } + + // Option stopOnFocus. + if (options.stopOnFocus) { + JSUS.onFocusIn(function() { + clearBlinkInterval(); + onFocusChange(null, null); + }); + } + + // Option stopOnClick. + if ('undefined' !== typeof options.stopOnClick) { + if ('object' !== typeof options.stopOnClick || + !options.stopOnClick.addEventListener) { + + throw new TypeError(where + 'options.stopOnClick must be ' + + 'an HTML element with method ' + + 'addEventListener. Found: ' + + options.stopOnClick); + } + elem = options.stopOnClick; + elem.addEventListener('click', clearBlinkInterval); + } + + // Option startOnBlur. + if (options.startOnBlur) { + options.startOnBlur = null; + JSUS.onFocusOut(function() { + JSUS.blinkTitle(titles, options); + }); + return null; + } + + // Prepare the rotation. + if ('string' === typeof titles) { + titles = [titles, '!!!']; + } + else if (!JSUS.isArray(titles)) { + throw new TypeError(where + 'titles must be string, ' + + 'array of strings or undefined.'); + } + rotationId = 0; + period = options.period || 1000; + // Function to be executed every period. + rotation = function() { + changeTitle(titles[rotationId]); + rotationId = (rotationId+1) % titles.length; + // Control the number of times it should be cycled through. + if ('number' === typeof nRepeats) { + if (rotationId === 0) { + nRepeats--; + if (nRepeats === 0) clearBlinkInterval(); + } + } + }; + // Perform first rotation right now. + rotation(); + id = setInterval(rotation, period); + + // Return clear function. + return clearBlinkInterval; + }; + })(null); + + /** + * ### DOM.cookieSupport + * + * Tests for cookie support + * + * @return {boolean|null} The type of support for cookies. Values: + * + * - null: no cookies + * - false: only session cookies + * - true: session cookies and persistent cookies (although + * the browser might clear them on exit) + * + * Kudos: http://stackoverflow.com/questions/2167310/ + * how-to-show-a-message-only-if-cookies-are-disabled-in-browser + */ + DOM.cookieSupport = function() { + var c, persist; + persist = true; + do { + c = 'gCStest=' + Math.floor(Math.random()*100000000); + document.cookie = persist ? c + + ';expires=Tue, 01-Jan-2030 00:00:00 GMT' : c; + + if (document.cookie.indexOf(c) !== -1) { + document.cookie= c + ';expires=Sat, 01-Jan-2000 00:00:00 GMT'; + return persist; + } + } while (!(persist = !persist)); + + return null; + }; + + /** + * ### DOM.viewportSize + * + * Returns the current size of the viewport in pixels + * + * The viewport's size is the actual visible part of the browser's + * window. This excludes, for example, the area occupied by the + * JavaScript console. + * + * @param {string} dim Optional. Controls the return value ('x', or 'y') + * + * @return {object|number} An object containing x and y property, or + * number specifying the value for x or y + * + * Kudos: http://stackoverflow.com/questions/3437786/ + * get-the-size-of-the-screen-current-web-page-and-browser-window + */ + DOM.viewportSize = function(dim) { + var w, d, e, g, x, y; + if (dim && dim !== 'x' && dim !== 'y') { + throw new TypeError('DOM.viewportSize: dim must be "x","y" or ' + + 'undefined. Found: ' + dim); + } + w = window; + d = document; + e = d.documentElement; + g = d.getElementsByTagName('body')[0]; + x = w.innerWidth || e.clientWidth || g.clientWidth, + y = w.innerHeight|| e.clientHeight|| g.clientHeight; + return !dim ? {x: x, y: y} : dim === 'x' ? x : y; + }; + + // ## Helper methods + + /** + * ### onFocusChange + * + * Helper function for DOM.onFocusIn and DOM.onFocusOut (cross-browser) + * + * Expects only one callback, either inCb, or outCb. + * + * @param {function|null} inCb Optional. Executed if page acquires focus, + * or NULL, to delete an existing callback. + * @param {function|null} outCb Optional. Executed if page loses focus, + * or NULL, to delete an existing callback. + * + * Kudos: http://stackoverflow.com/questions/1060008/ + * is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active + * + * @see http://www.w3.org/TR/page-visibility/ + */ + onFocusChange = (function(document) { + var inFocusCb, outFocusCb, event, hidden, evtMap; + + if (!document) { + return function() { + JSUS.log('onFocusChange: no document detected.'); + return; + }; + } + + if ('hidden' in document) { + hidden = 'hidden'; + event = 'visibilitychange'; + } + else if ('mozHidden' in document) { + hidden = 'mozHidden'; + event = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + hidden = 'webkitHidden'; + event = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + hidden = 'msHidden'; + event = 'msvisibilitychange'; + } + + evtMap = { + focus: true, focusin: true, pageshow: true, + blur: false, focusout: false, pagehide: false + }; + + function onchange(evt) { + var isHidden; + evt = evt || window.event; + // If event is defined as one from event Map. + if (evt.type in evtMap) isHidden = evtMap[evt.type]; + // Or use the hidden property. + else isHidden = this[hidden] ? true : false; + // Call the callback, if defined. + if (!isHidden) { if (inFocusCb) inFocusCb(); } + else { if (outFocusCb) outFocusCb(); } + } + + return function(inCb, outCb) { + var onchangeCb; + + if ('undefined' !== typeof inCb) inFocusCb = inCb; + else outFocusCb = outCb; + + onchangeCb = !inFocusCb && !outFocusCb ? null : onchange; + + // Visibility standard detected. + if (event) { + if (onchangeCb) document.addEventListener(event, onchange); + else document.removeEventListener(event, onchange); + } + else if ('onfocusin' in document) { + document.onfocusin = document.onfocusout = onchangeCb; + } + // All others. + else { + window.onpageshow = window.onpagehide + = window.onfocus = window.onblur = onchangeCb; + } + }; + })('undefined' !== typeof document ? document : null); + + /** + * ### changeTitle + * + * Changes title of page + * + * @param {string} title New title of the page + */ + changeTitle = function(title) { + if ('string' === typeof title) { + document.title = title; + } + else { + throw new TypeError('JSUS.changeTitle: title must be string. ' + + 'Found: ' + title); + } + }; + + JSUS.extend(DOM); + +})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS); + +/** + * # DOM + * + * Copyright(c) 2016 Stefano Balietti + * MIT Licensed + * + * Collection of static functions related to DOM manipulation + * + * Helper library to perform generic operation with DOM elements. + * + * The general syntax is the following: Every HTML element has associated + * a get* and a add* method, whose syntax is very similar. + * + * - The get* method creates the element and returns it. + * - The add* method creates the element, append it as child to a root element, + * and then returns it. + * + * The syntax of both method is the same, but the add* method + * needs the root element as first parameter. E.g. + * + * - getButton(id, text, attributes); + * - addButton(root, id, text, attributes); + * + * The last parameter is generally an object containing a list of + * of key-values pairs as additional attributes to set to the element. + * + * Only the methods which do not follow the above-mentioned syntax + * will receive further explanation. + */ +(function(JSUS) { + + "use strict"; + + var onFocusChange, changeTitle; + + function DOM() {} + + // ## GENERAL + + /** + * ### DOM.write + * + * Write a text, or append an HTML element or node, into a root element + * + * @param {Element} root The HTML element where to write into + * @param {mixed} text The text to write. Default, an ampty string + * + * @return {TextNode} The text node inserted in the root element + * + * @see DOM.writeln + */ + DOM.write = function(root, text) { + var content; + if ('undefined' === typeof text || text === null) text = ""; + if (JSUS.isNode(text) || JSUS.isElement(text)) content = text; + else content = document.createTextNode(text); + root.appendChild(content); + return content; + }; + + /** + * ### DOM.writeln + * + * Write a text and a break into a root element + * + * Default break element is
tag + * + * @param {Element} root The HTML element where to write into + * @param {mixed} text The text to write. Default, an ampty string + * @param {string} rc the name of the tag to use as a break element + * + * @return {TextNode} The text node inserted in the root element + * + * @see DOM.write + * @see DOM.addBreak + */ + DOM.writeln = function(root, text, rc) { + var content; + content = DOM.write(root, text); + this.addBreak(root, rc); + return content; + }; + + /** + * ### DOM.sprintf + * + * Builds up a decorated HTML text element + * + * Performs string substitution from an args object where the first + * character of the key bears the following semantic: + * + * - '@': variable substitution with escaping + * - '!': variable substitution without variable escaping + * - '%': wraps a portion of string into a _span_ element to which is + * possible to associate a css class or id. Alternatively, + * it also possible to add in-line style. E.g.: + * + * ```javascript + * sprintf('%sImportant!%s An error has occurred: %pre@err%pre', { + * '%pre': { + * style: 'font-size: 12px; font-family: courier;' + * }, + * '%s': { + * id: 'myId', + * 'class': 'myClass', + * }, + * '@err': 'file not found', + * }, document.body); + * ``` + * + * Special span elements are %strong and %em, which add + * respectively a _strong_ and _em_ tag instead of the default + * _span_ tag. They cannot be styled. + * + * @param {string} string A text to transform + * @param {object} args Optional. An object containing string + * transformations + * @param {Element} root Optional. An HTML element to which append the + * string. Defaults, a new _span_ element + * + * @return {Element} The root element. + */ + DOM.sprintf = function(string, args, root) { + + var text, span, idx_start, idx_finish, idx_replace, idxs; + var spans, key, i; + + root = root || document.createElement('span'); + spans = {}; + + // Create an args object, if none is provided. + // Defaults %em and %strong are added. + args = args || {}; + args['%strong'] = ''; + args['%em'] = ''; + + // Transform arguments before inserting them. + for (key in args) { + if (args.hasOwnProperty(key)) { + + switch(key.charAt(0)) { + + case '%': // Span/Strong/Emph . + + idx_start = string.indexOf(key); + + // Pattern not found. No error. + if (idx_start === -1) continue; + + idx_replace = idx_start + key.length; + idx_finish = string.indexOf(key, idx_replace); + + if (idx_finish === -1) { + JSUS.log('Error. Could not find closing key: ' + key); + continue; + } + + // Can be strong, emph or a generic span. + spans[idx_start] = key; + + break; + + case '@': // Replace and sanitize. + string = string.replace(key, escape(args[key])); + break; + + case '!': // Replace and not sanitize. + string = string.replace(key, args[key]); + break; + + default: + JSUS.log('Identifier not in [!,@,%]: ' + key[0]); + + } + } + } + + // No span to create, return what we have. + if (!JSUS.size(spans)) { + return root.appendChild(document.createTextNode(string)); + } + + // Re-assamble the string. + + idxs = JSUS.keys(spans).sort(function(a, b){ return a - b; }); + idx_finish = 0; + for (i = 0; i < idxs.length; i++) { + + // Add span. + key = spans[idxs[i]]; + idx_start = string.indexOf(key); + + // Add fragments of string. + if (idx_finish !== idx_start-1) { + root.appendChild(document.createTextNode( + string.substring(idx_finish, idx_start))); + } + + idx_replace = idx_start + key.length; + idx_finish = string.indexOf(key, idx_replace); + + if (key === '%strong') { + span = document.createElement('strong'); + } + else if (key === '%em') { + span = document.createElement('em'); + } + else { + span = JSUS.getElement('span', null, args[key]); + } + + text = string.substring(idx_replace, idx_finish); + + span.appendChild(document.createTextNode(text)); + + root.appendChild(span); + idx_finish = idx_finish + key.length; + } + + // Add the final part of the string. + if (idx_finish !== string.length) { + root.appendChild(document.createTextNode( + string.substring(idx_finish))); + } + + return root; + }; + + /** + * ### DOM.isNode + * + * Returns TRUE if the object is a DOM node + * + * @param {mixed} The variable to check + * + * @return {boolean} TRUE, if the the object is a DOM node + */ + DOM.isNode = function(o) { + if (!o || 'object' !== typeof o) return false; + return 'object' === typeof Node ? o instanceof Node : + 'number' === typeof o.nodeType && + 'string' === typeof o.nodeName; + }; + + /** + * ### DOM.isElement + * + * Returns TRUE if the object is a DOM element + * + * Notice: instanceof HTMLElement is not reliable in Safari, even if + * the method is defined. + * + * @param {mixed} The variable to check + * + * @return {boolean} TRUE, if the the object is a DOM element + */ + DOM.isElement = function(o) { + return o && 'object' === typeof o && o.nodeType === 1 && + 'string' === typeof o.nodeName; + }; + + /** + * ### DOM.shuffleElements + * + * Shuffles the order of children of a parent Element + * + * All children *must* have the id attribute (live list elements cannot + * be identified by position). + * + * Notice the difference between Elements and Nodes: + * + * http://stackoverflow.com/questions/7935689/ + * what-is-the-difference-between-children-and-childnodes-in-javascript + * + * @param {Node} parent The parent node + * @param {array} order Optional. A pre-specified order. Defaults, random + * + * @return {array} The order used to shuffle the nodes + */ + DOM.shuffleElements = function(parent, order) { + var i, len, idOrder, children, child; + var id, forceId, missId; + if (!JSUS.isNode(parent)) { + throw new TypeError('DOM.shuffleElements: parent must be a node. ' + + 'Found: ' + parent); + } + if (!parent.children || !parent.children.length) { + JSUS.log('DOM.shuffleElements: parent has no children.', 'ERR'); + return false; + } + if (order) { + if (!JSUS.isArray(order)) { + throw new TypeError('DOM.shuffleElements: order must array.' + + 'Found: ' + order); + } + if (order.length !== parent.children.length) { + throw new Error('DOM.shuffleElements: order length must ' + + 'match the number of children nodes.'); + } + } + + // DOM4 compliant browsers. + children = parent.children; + + //https://developer.mozilla.org/en/DOM/Element.children + //[IE lt 9] IE < 9 + if ('undefined' === typeof children) { + child = this.firstChild; + while (child) { + if (child.nodeType == 1) children.push(child); + child = child.nextSibling; + } + } + + len = children.length; + idOrder = new Array(len); + if (!order) order = JSUS.sample(0, (len-1)); + for (i = 0 ; i < len; i++) { + id = children[order[i]].id; + if ('string' !== typeof id || id === "") { + throw new Error('DOM.shuffleElements: no id found on ' + + 'child n. ' + order[i] + '.'); + } + idOrder[i] = id; + } + + // Two fors are necessary to follow the real sequence (Live List). + // However, parent.children is a special object, so the sequence + // could be unreliable. + for (i = 0 ; i < len; i++) { + parent.appendChild(children[idOrder[i]]); + } + return idOrder; + }; + + /** + * ### DOM.shuffleNodes + * + * It actually shuffles Elements. + * + * @deprecated + */ + DOM.shuffleNodes = function(parent, order) { + console.log('***DOM.shuffleNodes is deprecated. ' + + 'Use Dom.shuffleElements instead.***'); + return DOM.shuffleElements(parent, order); + }; + + /** + * ### DOM.getElement + * + * Creates a generic HTML element with id and attributes as specified + * + * @param {string} elem The name of the tag + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.addAttributes2Elem + */ + DOM.getElement = function(elem, id, attributes) { + var e = document.createElement(elem); + if ('undefined' !== typeof id) { + e.id = id; + } + return this.addAttributes2Elem(e, attributes); + }; + + /** + * ### DOM.addElement + * + * Creates and appends a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {HTMLElement} root The root element to which the new element will + * be appended + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.getElement + * @see DOM.addAttributes2Elem + */ + DOM.addElement = function(elem, root, id, attributes) { + var el = this.getElement(elem, id, attributes); + return root.appendChild(el); + }; + + /** + * ### DOM.addAttributes2Elem + * + * Adds attributes to an HTML element and returns it. + * + * Attributes are defined as key-values pairs. + * Attributes 'label' is ignored, attribute 'className' ('class') and + * 'style' are special and are delegated to special methods. + * + * @param {HTMLElement} e The element to decorate + * @param {object} a Object containing attributes to add to the element + * + * @return {HTMLElement} The decorated element + * + * @see DOM.addLabel + * @see DOM.addClass + * @see DOM.style + */ + DOM.addAttributes2Elem = function(e, a) { + var key; + if (!e || !a) return e; + if ('object' != typeof a) return e; + for (key in a) { + if (a.hasOwnProperty(key)) { + if (key === 'id') { + e.id = a[key]; + } + else if (key === 'class' || key === 'className') { + DOM.addClass(e, a[key]); + } + else if (key === 'style') { + DOM.style(e, a[key]); + } + else if (key === 'label') { + // Handle the case. + JSUS.log('DOM.addAttributes2Elem: label attribute is not ' + + 'supported. Use DOM.addLabel instead.'); + } + else { + e.setAttribute(key, a[key]); + } + + + // TODO: handle special cases + // + } + } + return e; + }; + + /** + * ### DOM.populateSelect + * + * Appends a list of options into a HTML select element. + * The second parameter list is an object containing + * a list of key-values pairs as text-value attributes for + * the option. + * + * @param {HTMLElement} select HTML select element + * @param {object} list Options to add to the select element + */ + DOM.populateSelect = function(select, list) { + var key, opt; + if (!select || !list) return; + for (key in list) { + if (list.hasOwnProperty(key)) { + opt = document.createElement('option'); + opt.value = list[key]; + opt.appendChild(document.createTextNode(key)); + select.appendChild(opt); + } + } + }; + + /** + * ### DOM.removeChildrenFromNode + * + * Removes all children from a node. + * + * @param {HTMLElement} e HTML element. + */ + DOM.removeChildrenFromNode = function(e) { + while (e.hasChildNodes()) { + e.removeChild(e.firstChild); + } + }; + + /** + * ### DOM.insertAfter + * + * Insert a node element after another one. + * + * The first parameter is the node to add. + * + */ + DOM.insertAfter = function(node, referenceNode) { + referenceNode.insertBefore(node, referenceNode.nextSibling); + }; + + /** + * ### DOM.generateUniqueId + * + * Generate a unique id for the page (frames included). + * + * TODO: now it always create big random strings, it does not actually + * check if the string exists. + * + */ + DOM.generateUniqueId = function(prefix) { + var search = [window]; + if (window.frames) { + search = search.concat(window.frames); + } + + function scanDocuments(id) { + var found = true; + while (found) { + for (var i=0; i < search.length; i++) { + found = search[i].document.getElementById(id); + if (found) { + id = '' + id + '_' + JSUS.randomInt(0, 1000); + break; + } + } + } + return id; + } + + + return scanDocuments(prefix + '_' + JSUS.randomInt(0, 10000000)); + //return scanDocuments(prefix); + }; + // ## GET/ADD /** @@ -5007,7 +6368,7 @@ /** * # RANDOM - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Generates pseudo-random numbers @@ -5021,18 +6382,31 @@ /** * ## RANDOM.random * - * Generates a pseudo-random floating point number between - * [a,b), a inclusive and b exclusive. + * Generates a pseudo-random floating point number in interval [a,b) * - * @param {number} a The lower limit - * @param {number} b The upper limit + * Interval is a inclusive and b exclusive. + * + * If b is undefined, the interval is [0, a). + * + * If both a and b are undefined the interval is [0, 1) + * + * @param {number} a Optional. The lower limit, or the upper limit + * if b is undefined + * @param {number} b Optional. The upper limit * * @return {number} A random floating point number in [a,b) */ RANDOM.random = function(a, b) { var c; - a = ('undefined' === typeof a) ? 0 : a; - b = ('undefined' === typeof b) ? 0 : b; + if ('undefined' === typeof b) { + if ('undefined' === typeof a) { + return Math.random(); + } + else { + b = a; + a = 0; + } + } if (a === b) return a; if (b < a) { @@ -5048,6 +6422,8 @@ * * Generates a pseudo-random integer between (a,b] a exclusive, b inclusive * + * @TODO: Change to interval [a,b], and allow 1 parameter for [0,a) + * * @param {number} a The lower limit * @param {number} b The upper limit * @@ -5063,9 +6439,9 @@ /** * ## RANDOM.sample * - * Generates a randomly shuffled sequence of numbers in (a,b) + * Generates a randomly shuffled sequence of numbers in [a,b)] * - * Both _a_ and _b_ are inclued in the interval. + * Both _a_ and _b_ are included in the interval. * * @param {number} a The lower limit * @param {number} b The upper limit diff --git a/build/jsus.min.js b/build/jsus.min.js index 74c7bfe..661f9cf 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=t.get=function(e){return"undefined"==typeof t.clone?(t.log("JSUS.clone not found. Cannot continue."),!1):"undefined"==typeof e?t.clone(t._classes):"undefined"==typeof t._classes[e]?(t.log("Could not find class "+e),!1):t.clone(t._classes[e])},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;i1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;i1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o tag + * + * @param {Element} root The HTML element where to write into + * @param {mixed} text The text to write. Default, an ampty string + * @param {string} rc the name of the tag to use as a break element + * + * @return {TextNode} The text node inserted in the root element + * + * @see DOM.write + * @see DOM.addBreak + */ + DOM.writeln = function(root, text, rc) { + var content; + content = DOM.write(root, text); + this.addBreak(root, rc); + return content; + }; + + /** + * ### DOM.sprintf + * + * Builds up a decorated HTML text element + * + * Performs string substitution from an args object where the first + * character of the key bears the following semantic: + * + * - '@': variable substitution with escaping + * - '!': variable substitution without variable escaping + * - '%': wraps a portion of string into a _span_ element to which is + * possible to associate a css class or id. Alternatively, + * it also possible to add in-line style. E.g.: + * + * ```javascript + * sprintf('%sImportant!%s An error has occurred: %pre@err%pre', { + * '%pre': { + * style: 'font-size: 12px; font-family: courier;' + * }, + * '%s': { + * id: 'myId', + * 'class': 'myClass', + * }, + * '@err': 'file not found', + * }, document.body); + * ``` + * + * Special span elements are %strong and %em, which add + * respectively a _strong_ and _em_ tag instead of the default + * _span_ tag. They cannot be styled. + * + * @param {string} string A text to transform + * @param {object} args Optional. An object containing string + * transformations + * @param {Element} root Optional. An HTML element to which append the + * string. Defaults, a new _span_ element + * + * @return {Element} The root element. + */ + DOM.sprintf = function(string, args, root) { + + var text, span, idx_start, idx_finish, idx_replace, idxs; + var spans, key, i; + + root = root || document.createElement('span'); + spans = {}; + + // Create an args object, if none is provided. + // Defaults %em and %strong are added. + args = args || {}; + args['%strong'] = ''; + args['%em'] = ''; + + // Transform arguments before inserting them. + for (key in args) { + if (args.hasOwnProperty(key)) { + + switch(key.charAt(0)) { + + case '%': // Span/Strong/Emph . + + idx_start = string.indexOf(key); + + // Pattern not found. No error. + if (idx_start === -1) continue; + + idx_replace = idx_start + key.length; + idx_finish = string.indexOf(key, idx_replace); + + if (idx_finish === -1) { + JSUS.log('Error. Could not find closing key: ' + key); + continue; + } + + // Can be strong, emph or a generic span. + spans[idx_start] = key; + + break; + + case '@': // Replace and sanitize. + string = string.replace(key, escape(args[key])); + break; + + case '!': // Replace and not sanitize. + string = string.replace(key, args[key]); + break; + + default: + JSUS.log('Identifier not in [!,@,%]: ' + key[0]); + + } + } + } + + // No span to create, return what we have. + if (!JSUS.size(spans)) { + return root.appendChild(document.createTextNode(string)); + } + + // Re-assamble the string. + + idxs = JSUS.keys(spans).sort(function(a, b){ return a - b; }); + idx_finish = 0; + for (i = 0; i < idxs.length; i++) { + + // Add span. + key = spans[idxs[i]]; + idx_start = string.indexOf(key); + + // Add fragments of string. + if (idx_finish !== idx_start-1) { + root.appendChild(document.createTextNode( + string.substring(idx_finish, idx_start))); + } + + idx_replace = idx_start + key.length; + idx_finish = string.indexOf(key, idx_replace); + + if (key === '%strong') { + span = document.createElement('strong'); + } + else if (key === '%em') { + span = document.createElement('em'); + } + else { + span = JSUS.getElement('span', null, args[key]); + } + + text = string.substring(idx_replace, idx_finish); + + span.appendChild(document.createTextNode(text)); + + root.appendChild(span); + idx_finish = idx_finish + key.length; + } + + // Add the final part of the string. + if (idx_finish !== string.length) { + root.appendChild(document.createTextNode( + string.substring(idx_finish))); + } + + return root; + }; + + /** + * ### DOM.isNode + * + * Returns TRUE if the object is a DOM node + * + * @param {mixed} The variable to check + * + * @return {boolean} TRUE, if the the object is a DOM node + */ + DOM.isNode = function(o) { + if (!o || 'object' !== typeof o) return false; + return 'object' === typeof Node ? o instanceof Node : + 'number' === typeof o.nodeType && + 'string' === typeof o.nodeName; + }; + + /** + * ### DOM.isElement + * + * Returns TRUE if the object is a DOM element + * + * Notice: instanceof HTMLElement is not reliable in Safari, even if + * the method is defined. + * + * @param {mixed} The variable to check + * + * @return {boolean} TRUE, if the the object is a DOM element + */ + DOM.isElement = function(o) { + return o && 'object' === typeof o && o.nodeType === 1 && + 'string' === typeof o.nodeName; + }; + + /** + * ### DOM.shuffleElements + * + * Shuffles the order of children of a parent Element + * + * All children *must* have the id attribute (live list elements cannot + * be identified by position). + * + * Notice the difference between Elements and Nodes: + * + * http://stackoverflow.com/questions/7935689/ + * what-is-the-difference-between-children-and-childnodes-in-javascript + * + * @param {Node} parent The parent node + * @param {array} order Optional. A pre-specified order. Defaults, random + * + * @return {array} The order used to shuffle the nodes + */ + DOM.shuffleElements = function(parent, order) { + var i, len, idOrder, children, child; + var id, forceId, missId; + if (!JSUS.isNode(parent)) { + throw new TypeError('DOM.shuffleElements: parent must be a node. ' + + 'Found: ' + parent); + } + if (!parent.children || !parent.children.length) { + JSUS.log('DOM.shuffleElements: parent has no children.', 'ERR'); + return false; + } + if (order) { + if (!JSUS.isArray(order)) { + throw new TypeError('DOM.shuffleElements: order must array.' + + 'Found: ' + order); + } + if (order.length !== parent.children.length) { + throw new Error('DOM.shuffleElements: order length must ' + + 'match the number of children nodes.'); + } + } + + // DOM4 compliant browsers. + children = parent.children; + + //https://developer.mozilla.org/en/DOM/Element.children + //[IE lt 9] IE < 9 + if ('undefined' === typeof children) { + child = this.firstChild; + while (child) { + if (child.nodeType == 1) children.push(child); + child = child.nextSibling; + } + } + + len = children.length; + idOrder = new Array(len); + if (!order) order = JSUS.sample(0, (len-1)); + for (i = 0 ; i < len; i++) { + id = children[order[i]].id; + if ('string' !== typeof id || id === "") { + throw new Error('DOM.shuffleElements: no id found on ' + + 'child n. ' + order[i] + '.'); + } + idOrder[i] = id; + } + + // Two fors are necessary to follow the real sequence (Live List). + // However, parent.children is a special object, so the sequence + // could be unreliable. + for (i = 0 ; i < len; i++) { + parent.appendChild(children[idOrder[i]]); + } + return idOrder; + }; + + /** + * ### DOM.shuffleNodes + * + * It actually shuffles Elements. + * + * @deprecated + */ + DOM.shuffleNodes = function(parent, order) { + console.log('***DOM.shuffleNodes is deprecated. ' + + 'Use Dom.shuffleElements instead.***'); + return DOM.shuffleElements(parent, order); + }; + + /** + * ### DOM.getElement + * + * Creates a generic HTML element with id and attributes as specified + * + * @param {string} elem The name of the tag + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.addAttributes2Elem + */ + DOM.getElement = function(elem, id, attributes) { + var e = document.createElement(elem); + if ('undefined' !== typeof id) { + e.id = id; + } + return this.addAttributes2Elem(e, attributes); + }; + + /** + * ### DOM.addElement + * + * Creates and appends a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {HTMLElement} root The root element to which the new element will + * be appended + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.getElement + * @see DOM.addAttributes2Elem + */ + DOM.addElement = function(elem, root, id, attributes) { + var el = this.getElement(elem, id, attributes); + return root.appendChild(el); + }; + + /** + * ### DOM.addAttributes2Elem + * + * Adds attributes to an HTML element and returns it. + * + * Attributes are defined as key-values pairs. + * Attributes 'label' is ignored, attribute 'className' ('class') and + * 'style' are special and are delegated to special methods. + * + * @param {HTMLElement} e The element to decorate + * @param {object} a Object containing attributes to add to the element + * + * @return {HTMLElement} The decorated element + * + * @see DOM.addLabel + * @see DOM.addClass + * @see DOM.style + */ + DOM.addAttributes2Elem = function(e, a) { + var key; + if (!e || !a) return e; + if ('object' != typeof a) return e; + for (key in a) { + if (a.hasOwnProperty(key)) { + if (key === 'id') { + e.id = a[key]; + } + else if (key === 'class' || key === 'className') { + DOM.addClass(e, a[key]); + } + else if (key === 'style') { + DOM.style(e, a[key]); + } + else if (key === 'label') { + // Handle the case. + JSUS.log('DOM.addAttributes2Elem: label attribute is not ' + + 'supported. Use DOM.addLabel instead.'); + } + else { + e.setAttribute(key, a[key]); + } + + + // TODO: handle special cases + // + } + } + return e; + }; + + /** + * ### DOM.populateSelect + * + * Appends a list of options into a HTML select element. + * The second parameter list is an object containing + * a list of key-values pairs as text-value attributes for + * the option. + * + * @param {HTMLElement} select HTML select element + * @param {object} list Options to add to the select element + */ + DOM.populateSelect = function(select, list) { + var key, opt; + if (!select || !list) return; + for (key in list) { + if (list.hasOwnProperty(key)) { + opt = document.createElement('option'); + opt.value = list[key]; + opt.appendChild(document.createTextNode(key)); + select.appendChild(opt); + } + } + }; + + /** + * ### DOM.removeChildrenFromNode + * + * Removes all children from a node. + * + * @param {HTMLElement} e HTML element. + */ + DOM.removeChildrenFromNode = function(e) { + while (e.hasChildNodes()) { + e.removeChild(e.firstChild); + } + }; + + /** + * ### DOM.insertAfter + * + * Insert a node element after another one. + * + * The first parameter is the node to add. + * + */ + DOM.insertAfter = function(node, referenceNode) { + referenceNode.insertBefore(node, referenceNode.nextSibling); + }; + + /** + * ### DOM.generateUniqueId + * + * Generate a unique id for the page (frames included). + * + * TODO: now it always create big random strings, it does not actually + * check if the string exists. + * + */ + DOM.generateUniqueId = function(prefix) { + var search = [window]; + if (window.frames) { + search = search.concat(window.frames); + } + + function scanDocuments(id) { + var found = true; + while (found) { + for (var i=0; i < search.length; i++) { + found = search[i].document.getElementById(id); + if (found) { + id = '' + id + '_' + JSUS.randomInt(0, 1000); + break; + } + } + } + return id; + } + + + return scanDocuments(prefix + '_' + JSUS.randomInt(0, 10000000)); + //return scanDocuments(prefix); + }; + + // ## GET/ADD + + /** + * ### DOM.getButton + * + */ + DOM.getButton = function(id, text, attributes) { + var sb; + sb = document.createElement('button'); + if ('undefined' !== typeof id) sb.id = id; + sb.appendChild(document.createTextNode(text || 'Send')); + return this.addAttributes2Elem(sb, attributes); + }; + + /** + * ### DOM.addButton + * + */ + DOM.addButton = function(root, id, text, attributes) { + var b = this.getButton(id, text, attributes); + return root.appendChild(b); + }; + + /** + * ### DOM.getFieldset + * + */ + DOM.getFieldset = function(id, legend, attributes) { + var f = this.getElement('fieldset', id, attributes); + var l = document.createElement('Legend'); + l.appendChild(document.createTextNode(legend)); + f.appendChild(l); + return f; + }; + + /** + * ### DOM.addFieldset + * + */ + DOM.addFieldset = function(root, id, legend, attributes) { + var f = this.getFieldset(id, legend, attributes); + return root.appendChild(f); + }; + + /** + * ### DOM.getTextInput + * + */ + DOM.getTextInput = function(id, attributes) { + var ti = document.createElement('input'); + if ('undefined' !== typeof id) ti.id = id; + ti.setAttribute('type', 'text'); + return this.addAttributes2Elem(ti, attributes); + }; + + /** + * ### DOM.addTextInput + * + */ + DOM.addTextInput = function(root, id, attributes) { + var ti = this.getTextInput(id, attributes); + return root.appendChild(ti); + }; + + /** + * ### DOM.getTextArea + * + */ + DOM.getTextArea = function(id, attributes) { + var ta = document.createElement('textarea'); + if ('undefined' !== typeof id) ta.id = id; + return this.addAttributes2Elem(ta, attributes); + }; + + /** + * ### DOM.addTextArea + * + */ + DOM.addTextArea = function(root, id, attributes) { + var ta = this.getTextArea(id, attributes); + return root.appendChild(ta); + }; + + /** + * ### DOM.getCanvas + * + */ + DOM.getCanvas = function(id, attributes) { + var canvas = document.createElement('canvas'); + var context = canvas.getContext('2d'); + + if (!context) { + alert('Canvas is not supported'); + return false; + } + + canvas.id = id; + return this.addAttributes2Elem(canvas, attributes); + }; + + /** + * ### DOM.addCanvas + * + */ + DOM.addCanvas = function(root, id, attributes) { + var c = this.getCanvas(id, attributes); + return root.appendChild(c); + }; + + /** + * ### DOM.getSlider + * + */ + DOM.getSlider = function(id, attributes) { + var slider = document.createElement('input'); + slider.id = id; + slider.setAttribute('type', 'range'); + return this.addAttributes2Elem(slider, attributes); + }; + + /** + * ### DOM.addSlider + * + */ + DOM.addSlider = function(root, id, attributes) { + var s = this.getSlider(id, attributes); + return root.appendChild(s); + }; + + /** + * ### DOM.getRadioButton + * + */ + DOM.getRadioButton = function(id, attributes) { + var radio = document.createElement('input'); + radio.id = id; + radio.setAttribute('type', 'radio'); + return this.addAttributes2Elem(radio, attributes); + }; + + /** + * ### DOM.addRadioButton + * + */ + DOM.addRadioButton = function(root, id, attributes) { + var rb = this.getRadioButton(id, attributes); + return root.appendChild(rb); + }; + + /** + * ### DOM.getLabel + * + */ + DOM.getLabel = function(forElem, id, labelText, attributes) { + if (!forElem) return false; + var label = document.createElement('label'); + label.id = id; + label.appendChild(document.createTextNode(labelText)); + + if ('undefined' === typeof forElem.id) { + forElem.id = this.generateUniqueId(); + } + + label.setAttribute('for', forElem.id); + this.addAttributes2Elem(label, attributes); + return label; + }; + + /** + * ### DOM.addLabel + * + */ + DOM.addLabel = function(root, forElem, id, labelText, attributes) { + if (!root || !forElem || !labelText) return false; + var l = this.getLabel(forElem, id, labelText, attributes); + root.insertBefore(l, forElem); + return l; + }; + + /** + * ### DOM.getSelect + * + */ + DOM.getSelect = function(id, attributes) { + return this.getElement('select', id, attributes); + }; + + /** + * ### DOM.addSelect + * + */ + DOM.addSelect = function(root, id, attributes) { + return this.addElement('select', root, id, attributes); + }; + + /** + * ### DOM.getIFrame + * + */ + DOM.getIFrame = function(id, attributes) { + attributes = attributes || {}; + if (!attributes.name) { + attributes.name = id; // For Firefox + } + return this.getElement('iframe', id, attributes); + }; + + /** + * ### DOM.addIFrame + * + */ + DOM.addIFrame = function(root, id, attributes) { + var ifr = this.getIFrame(id, attributes); + return root.appendChild(ifr); + }; + + /** + * ### DOM.addBreak + * + */ + DOM.addBreak = function(root, rc) { + var RC = rc || 'br'; + var br = document.createElement(RC); + return root.appendChild(br); + //return this.insertAfter(br,root); + }; + + /** + * ### DOM.getDiv + * + */ + DOM.getDiv = function(id, attributes) { + return this.getElement('div', id, attributes); + }; + + /** + * ### DOM.addDiv + * + */ + DOM.addDiv = function(root, id, attributes) { + return this.addElement('div', root, id, attributes); + }; + + // ## CSS / JS + + /** + * ### DOM.addCSS + * + * If no root element is passed, it tries to add the CSS + * link element to document.head, document.body, and + * finally document. If it fails, returns FALSE. + * + */ + DOM.addCSS = function(root, css, id, attributes) { + root = root || document.head || document.body || document; + if (!root) return false; + + attributes = attributes || {}; + + attributes = JSUS.merge(attributes, {rel : 'stylesheet', + type: 'text/css', + href: css + }); + + return this.addElement('link', root, id, attributes); + }; + + /** + * ### DOM.addJS + * + */ + DOM.addJS = function(root, js, id, attributes) { + root = root || document.head || document.body || document; + if (!root) return false; + + attributes = attributes || {}; + + attributes = JSUS.merge(attributes, {charset : 'utf-8', + type: 'text/javascript', + src: js + }); + + return this.addElement('script', root, id, attributes); + }; + + /** + * ### DOM.highlight + * + * Provides a simple way to highlight an HTML element + * by adding a colored border around it. + * + * Three pre-defined modes are implemented: + * + * - OK: green + * - WARN: yellow + * - ERR: red (default) + * + * Alternatively, it is possible to specify a custom + * color as HEX value. Examples: + * + * ```javascript + * highlight(myDiv, 'WARN'); // yellow border + * highlight(myDiv); // red border + * highlight(myDiv, '#CCC'); // grey border + * ``` + * + * @param {HTMLElement} elem The element to highlight + * @param {string} code The type of highlight + * + * @see DOM.addBorder + * @see DOM.style + */ + DOM.highlight = function(elem, code) { + var color; + if (!elem) return; + + // default value is ERR + switch (code) { + case 'OK': + color = 'green'; + break; + case 'WARN': + color = 'yellow'; + break; + case 'ERR': + color = 'red'; + break; + default: + if (code.charAt(0) === '#') { + color = code; + } + else { + color = 'red'; + } + } + + return this.addBorder(elem, color); + }; + + /** + * ### DOM.addBorder + * + * Adds a border around the specified element. Color, + * width, and type can be specified. + */ + DOM.addBorder = function(elem, color, width, type) { + var properties; + if (!elem) return; + + color = color || 'red'; + width = width || '5px'; + type = type || 'solid'; + + properties = { border: width + ' ' + type + ' ' + color }; + return DOM.style(elem, properties); + }; + + /** + * ### DOM.style + * + * Styles an element as an in-line css. + * + * Existing style properties are maintained, and new ones added. + * + * @param {HTMLElement} elem The element to style + * @param {object} Objects containing the properties to add. + * + * @return {HTMLElement} The styled element + */ + DOM.style = function(elem, properties) { + var i; + if (!elem || !properties) return; + if (!DOM.isElement(elem)) return; + + for (i in properties) { + if (properties.hasOwnProperty(i)) { + elem.style[i] = properties[i]; + } + } + return elem; + }; + + /** + * ### DOM.removeClass + * + * Removes a specific class from the classNamex attribute of a given element + * + * @param {HTMLElement} el An HTML element + * @param {string} c The name of a CSS class already in the element + * + * @return {HTMLElement|undefined} The HTML element with the removed + * class, or undefined if the inputs are misspecified + */ + DOM.removeClass = function(el, c) { + var regexpr, o; + if (!el || !c) return; + regexpr = new RegExp('(?:^|\\s)' + c + '(?!\\S)'); + o = el.className = el.className.replace( regexpr, '' ); + return el; + }; + + /** + * ### DOM.addClass + * + * Adds one or more classes to the className attribute of the given element + * + * Takes care not to overwrite already existing classes. + * + * @param {HTMLElement} el An HTML element + * @param {string|array} c The name/s of CSS class/es + * + * @return {HTMLElement|undefined} The HTML element with the additional + * class, or undefined if the inputs are misspecified + */ + DOM.addClass = function(el, c) { + if (!el) return; + if (c instanceof Array) c = c.join(' '); + else if ('string' !== typeof c) return; + if (!el.className || el.className === '') el.className = c; + else el.className += (' ' + c); + return el; + }; + + /** + * ### DOM.getElementsByClassName + * + * Returns an array of elements with requested class name + * + * @param {object} document The document object of a window or iframe + * @param {string} className The requested className + * @param {string} nodeName Optional. If set only elements with + * the specified tag name will be searched + * + * @return {array} Array of elements with the requested class name + * + * @see https://gist.github.com/E01T/6088383 + * @see http://stackoverflow.com/ + * questions/8808921/selecting-a-css-class-with-xpath + */ + DOM.getElementsByClassName = function(document, className, nodeName) { + var result, node, tag, seek, i, rightClass; + result = []; + tag = nodeName || '*'; + if (document.evaluate) { + seek = '//' + tag + + '[contains(concat(" ", normalize-space(@class), " "), "' + + className + ' ")]'; + seek = document.evaluate(seek, document, null, 0, null ); + while ((node = seek.iterateNext())) { + result.push(node); + } + } + else { + rightClass = new RegExp( '(^| )'+ className +'( |$)' ); + seek = document.getElementsByTagName(tag); + for (i = 0; i < seek.length; i++) + if (rightClass.test((node = seek[i]).className )) { + result.push(seek[i]); + } + } + return result; + }; + + // ## IFRAME + + /** + * ### DOM.getIFrameDocument + * + * Returns a reference to the document of an iframe object + * + * @param {HTMLIFrameElement} iframe The iframe object + * + * @return {HTMLDocument|null} The document of the iframe, or + * null if not found. + */ + DOM.getIFrameDocument = function(iframe) { + if (!iframe) return null; + return iframe.contentDocument || + iframe.contentWindow ? iframe.contentWindow.document : null; + }; + + /** + * ### DOM.getIFrameAnyChild + * + * Gets the first available child of an IFrame + * + * Tries head, body, lastChild and the HTML element + * + * @param {HTMLIFrameElement} iframe The iframe object + * + * @return {HTMLElement|undefined} The child, or undefined if none is found + */ + DOM.getIFrameAnyChild = function(iframe) { + var contentDocument; + if (!iframe) return; + contentDocument = W.getIFrameDocument(iframe); + return contentDocument.head || contentDocument.body || + contentDocument.lastChild || + contentDocument.getElementsByTagName('html')[0]; + }; + + // ## RIGHT-CLICK + + /** + * ### DOM.disableRightClick + * + * Disables the popup of the context menu by right clicking with the mouse + * + * @param {Document} Optional. A target document object. Defaults, document + * + * @see DOM.enableRightClick + */ + DOM.disableRightClick = function(doc) { + doc = doc || document; + if (doc.layers) { + doc.captureEvents(Event.MOUSEDOWN); + doc.onmousedown = function clickNS4(e) { + if (doc.layers || doc.getElementById && !doc.all) { + if (e.which == 2 || e.which == 3) { + return false; + } + } + }; + } + else if (doc.all && !doc.getElementById) { + doc.onmousedown = function clickIE4() { + if (event.button == 2) { + return false; + } + }; + } + doc.oncontextmenu = new Function("return false"); + }; + + /** + * ### DOM.enableRightClick + * + * Enables the popup of the context menu by right clicking with the mouse + * + * It unregisters the event handlers created by `DOM.disableRightClick` + * + * @param {Document} Optional. A target document object. Defaults, document + * + * @see DOM.disableRightClick + */ + DOM.enableRightClick = function(doc) { + doc = doc || document; + if (doc.layers) { + doc.releaseEvents(Event.MOUSEDOWN); + doc.onmousedown = null; + } + else if (doc.all && !doc.getElementById) { + doc.onmousedown = null; + } + doc.oncontextmenu = null; + }; + + /** + * ### DOM.addEvent + * + * Adds an event listener to an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to handle + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event will initiate a capture. + * Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.removeEvent + * + * Kudos: + * http://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick + */ + DOM.addEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.attachEvent) return element.attachEvent('on' + event, func); + else return element.addEventListener(event, func, capture); + }; + + /** + * ### DOM.removeEvent + * + * Removes an event listener from an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to remove + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event was registered + * as a capture. Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.addEvent + */ + DOM.removeEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.detachEvent) return element.detachEvent('on' + event, func); + else return element.removeEventListener(event, func, capture); + }; + + /** + * ### DOM.disableBackButton + * + * Disables/re-enables backward navigation in history of browsed pages + * + * When disabling, it inserts twice the current url. + * + * It will still be possible to manually select the uri in the + * history pane and nagivate to it. + * + * @param {boolean} disable Optional. If TRUE disables back button, + * if FALSE, re-enables it. Default: TRUE. + * + * @return {boolean} The state of the back button (TRUE = disabled), + * or NULL if the method is not supported by browser. + */ + DOM.disableBackButton = (function(isDisabled) { + return function(disable) { + disable = 'undefined' === typeof disable ? true : disable; + if (disable && !isDisabled) { + if (!history.pushState || !history.go) { + node.warn('DOM.disableBackButton: method not ' + + 'supported by browser.'); + return null; + } + history.pushState(null, null, location.href); + window.onpopstate = function(event) { + history.go(1); + }; + } + else if (isDisabled) { + window.onpopstate = null; + } + isDisabled = disable; + return disable; + }; + })(false); + + /** + * ### DOM.playSound + * + * Plays a sound + * + * @param {various} sound Audio tag or path to audio file to be played + */ + DOM.playSound = 'undefined' === typeof Audio ? + function() { + console.log('JSUS.playSound: Audio tag not supported in your' + + ' browser. Cannot play sound.'); + } : + function(sound) { + var audio; + if ('string' === typeof sound) { + audio = new Audio(sound); + } + else if ('object' === typeof sound && + 'function' === typeof sound.play) { + audio = sound; + } + else { + throw new TypeError('JSUS.playSound: sound must be string' + + ' or audio element.'); + } + audio.play(); + }; + + /** + * ### DOM.onFocusIn + * + * Registers a callback to be executed when the page acquires focus + * + * @param {function|null} cb Callback executed if page acquires focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusIn = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusIn: cb must be function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + + onFocusChange(cb); + }; + + /** + * ### DOM.onFocusOut + * + * Registers a callback to be executed when the page loses focus + * + * @param {function} cb Callback executed if page loses focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusOut = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusOut: cb must be ' + + 'function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + onFocusChange(undefined, cb); + }; + + + /** + * ### DOM.blinkTitle + * + * Changes the title of the page in regular intervals + * + * Calling the function without any arguments stops the blinking + * If an array of strings is provided, that array will be cycled through. + * If a signle string is provided, the title will alternate between '!!!' + * and that string. + * + * @param {mixed} titles New title to blink + * @param {object} options Optional. Configuration object. + * Accepted values and default in parenthesis: + * + * - stopOnFocus (false): Stop blinking if user switched to tab + * - stopOnClick (false): Stop blinking if user clicks on the + * specified element + * - finalTitle (document.title): Title to set after blinking is done + * - repeatFor (undefined): Show each element in titles at most + * N times -- might be stopped earlier by other events. + * - startOnBlur(false): Start blinking if user switches + * away from tab + * - period (1000) How much time between two blinking texts in the title + * + * @return {function|null} A function to clear the blinking of texts, + * or NULL, if the interval was not created yet (e.g. with startOnBlur + * option), or just destroyed. + */ + DOM.blinkTitle = (function(id) { + var clearBlinkInterval, finalTitle, elem; + clearBlinkInterval = function(opts) { + clearInterval(id); + id = null; + if (elem) { + elem.removeEventListener('click', clearBlinkInterval); + elem = null; + } + if (finalTitle) { + document.title = finalTitle; + finalTitle = null; + } + }; + return function(titles, options) { + var period, where, rotation; + var rotationId, nRepeats; + + if (null !== id) clearBlinkInterval(); + if ('undefined' === typeof titles) return null; + + where = 'JSUS.blinkTitle: '; + options = options || {}; + + // Option finalTitle. + if ('undefined' === typeof options.finalTitle) { + finalTitle = document.title; + } + else if ('string' === typeof options.finalTitle) { + finalTitle = options.finalTitle; + } + else { + throw new TypeError(where + 'options.finalTitle must be ' + + 'string or undefined. Found: ' + + options.finalTitle); + } + + // Option repeatFor. + if ('undefined' !== typeof options.repeatFor) { + nRepeats = JSUS.isInt(options.repeatFor, 0); + if (false === nRepeats) { + throw new TypeError(where + 'options.repeatFor must be ' + + 'a positive integer. Found: ' + + options.repeatFor); + } + } + + // Option stopOnFocus. + if (options.stopOnFocus) { + JSUS.onFocusIn(function() { + clearBlinkInterval(); + onFocusChange(null, null); + }); + } + + // Option stopOnClick. + if ('undefined' !== typeof options.stopOnClick) { + if ('object' !== typeof options.stopOnClick || + !options.stopOnClick.addEventListener) { + + throw new TypeError(where + 'options.stopOnClick must be ' + + 'an HTML element with method ' + + 'addEventListener. Found: ' + + options.stopOnClick); + } + elem = options.stopOnClick; + elem.addEventListener('click', clearBlinkInterval); + } + + // Option startOnBlur. + if (options.startOnBlur) { + options.startOnBlur = null; + JSUS.onFocusOut(function() { + JSUS.blinkTitle(titles, options); + }); + return null; + } + + // Prepare the rotation. + if ('string' === typeof titles) { + titles = [titles, '!!!']; + } + else if (!JSUS.isArray(titles)) { + throw new TypeError(where + 'titles must be string, ' + + 'array of strings or undefined.'); + } + rotationId = 0; + period = options.period || 1000; + // Function to be executed every period. + rotation = function() { + changeTitle(titles[rotationId]); + rotationId = (rotationId+1) % titles.length; + // Control the number of times it should be cycled through. + if ('number' === typeof nRepeats) { + if (rotationId === 0) { + nRepeats--; + if (nRepeats === 0) clearBlinkInterval(); + } + } + }; + // Perform first rotation right now. + rotation(); + id = setInterval(rotation, period); + + // Return clear function. + return clearBlinkInterval; + }; + })(null); + + /** + * ### DOM.cookieSupport + * + * Tests for cookie support + * + * @return {boolean|null} The type of support for cookies. Values: + * + * - null: no cookies + * - false: only session cookies + * - true: session cookies and persistent cookies (although + * the browser might clear them on exit) + * + * Kudos: http://stackoverflow.com/questions/2167310/ + * how-to-show-a-message-only-if-cookies-are-disabled-in-browser + */ + DOM.cookieSupport = function() { + var c, persist; + persist = true; + do { + c = 'gCStest=' + Math.floor(Math.random()*100000000); + document.cookie = persist ? c + + ';expires=Tue, 01-Jan-2030 00:00:00 GMT' : c; + + if (document.cookie.indexOf(c) !== -1) { + document.cookie= c + ';expires=Sat, 01-Jan-2000 00:00:00 GMT'; + return persist; + } + } while (!(persist = !persist)); + + return null; + }; + + /** + * ### DOM.viewportSize + * + * Returns the current size of the viewport in pixels + * + * The viewport's size is the actual visible part of the browser's + * window. This excludes, for example, the area occupied by the + * JavaScript console. + * + * @param {string} dim Optional. Controls the return value ('x', or 'y') + * + * @return {object|number} An object containing x and y property, or + * number specifying the value for x or y + * + * Kudos: http://stackoverflow.com/questions/3437786/ + * get-the-size-of-the-screen-current-web-page-and-browser-window + */ + DOM.viewportSize = function(dim) { + var w, d, e, g, x, y; + if (dim && dim !== 'x' && dim !== 'y') { + throw new TypeError('DOM.viewportSize: dim must be "x","y" or ' + + 'undefined. Found: ' + dim); + } + w = window; + d = document; + e = d.documentElement; + g = d.getElementsByTagName('body')[0]; + x = w.innerWidth || e.clientWidth || g.clientWidth, + y = w.innerHeight|| e.clientHeight|| g.clientHeight; + return !dim ? {x: x, y: y} : dim === 'x' ? x : y; + }; + + // ## Helper methods + + /** + * ### onFocusChange + * + * Helper function for DOM.onFocusIn and DOM.onFocusOut (cross-browser) + * + * Expects only one callback, either inCb, or outCb. + * + * @param {function|null} inCb Optional. Executed if page acquires focus, + * or NULL, to delete an existing callback. + * @param {function|null} outCb Optional. Executed if page loses focus, + * or NULL, to delete an existing callback. + * + * Kudos: http://stackoverflow.com/questions/1060008/ + * is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active + * + * @see http://www.w3.org/TR/page-visibility/ + */ + onFocusChange = (function(document) { + var inFocusCb, outFocusCb, event, hidden, evtMap; + + if (!document) { + return function() { + JSUS.log('onFocusChange: no document detected.'); + return; + }; + } + + if ('hidden' in document) { + hidden = 'hidden'; + event = 'visibilitychange'; + } + else if ('mozHidden' in document) { + hidden = 'mozHidden'; + event = 'mozvisibilitychange'; + } + else if ('webkitHidden' in document) { + hidden = 'webkitHidden'; + event = 'webkitvisibilitychange'; + } + else if ('msHidden' in document) { + hidden = 'msHidden'; + event = 'msvisibilitychange'; + } + + evtMap = { + focus: true, focusin: true, pageshow: true, + blur: false, focusout: false, pagehide: false + }; + + function onchange(evt) { + var isHidden; + evt = evt || window.event; + // If event is defined as one from event Map. + if (evt.type in evtMap) isHidden = evtMap[evt.type]; + // Or use the hidden property. + else isHidden = this[hidden] ? true : false; + // Call the callback, if defined. + if (!isHidden) { if (inFocusCb) inFocusCb(); } + else { if (outFocusCb) outFocusCb(); } + } + + return function(inCb, outCb) { + var onchangeCb; + + if ('undefined' !== typeof inCb) inFocusCb = inCb; + else outFocusCb = outCb; + + onchangeCb = !inFocusCb && !outFocusCb ? null : onchange; + + // Visibility standard detected. + if (event) { + if (onchangeCb) document.addEventListener(event, onchange); + else document.removeEventListener(event, onchange); + } + else if ('onfocusin' in document) { + document.onfocusin = document.onfocusout = onchangeCb; + } + // All others. + else { + window.onpageshow = window.onpagehide + = window.onfocus = window.onblur = onchangeCb; + } + }; + })('undefined' !== typeof document ? document : null); + + /** + * ### changeTitle + * + * Changes title of page + * + * @param {string} title New title of the page + */ + changeTitle = function(title) { + if ('string' === typeof title) { + document.title = title; + } + else { + throw new TypeError('JSUS.changeTitle: title must be string. ' + + 'Found: ' + title); + } + }; + + JSUS.extend(DOM); + +})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS); From c7a6c6470f1d9cb736d3c5df04c14d8c5cc402ab Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 1 Jul 2017 11:45:07 -0400 Subject: [PATCH 04/79] improved DOM --- build/jsus.js | 159 ++++++++++++++++++++++++++++++---------------- build/jsus.min.js | 2 +- lib/dom.js | 85 ++++++------------------- 3 files changed, 125 insertions(+), 121 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index 0345bf7..38d554b 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -112,7 +112,7 @@ * @return {function|boolean} The copy of the JSUS library, or * FALSE if the library does not exist */ - JSUS.require = JSUS.get = function(className) { + JSUS.require = function(className) { if ('undefined' === typeof JSUS.clone) { JSUS.log('JSUS.clone not found. Cannot continue.'); return false; @@ -139,7 +139,6 @@ }; // ## Node.JS includes - // if node if (JSUS.isNodeJS()) { require('./lib/compatibility'); require('./lib/obj'); @@ -153,10 +152,9 @@ require('./lib/fs'); } else { - // Also exports J in the browser. + // Exports J in the browser. exports.J = exports.JSUS; } - // end node })( 'undefined' !== typeof module && 'undefined' !== typeof module.exports ? @@ -1028,8 +1026,9 @@ * * Write a text, or append an HTML element or node, into a root element * - * @param {Element} root The HTML element where to write into - * @param {mixed} text The text to write. Default, an ampty string + * @param {HTMLElement} root The HTML element where to write into + * @param {string|HTMLElement} text The text to write or an element + * to append. Default: an ampty string * * @return {TextNode} The text node inserted in the root element * @@ -1051,19 +1050,19 @@ * * Default break element is
tag * - * @param {Element} root The HTML element where to write into - * @param {mixed} text The text to write. Default, an ampty string + * @param {HTMLElement} root The HTML element where to write into + * @param {string|HTMLElement} text The text to write or an element + * to append. Default: an ampty string * @param {string} rc the name of the tag to use as a break element * * @return {TextNode} The text node inserted in the root element * * @see DOM.write - * @see DOM.addBreak */ DOM.writeln = function(root, text, rc) { var content; content = DOM.write(root, text); - this.addBreak(root, rc); + this.add(rc || 'br', root); return content; }; @@ -1332,50 +1331,6 @@ return DOM.shuffleElements(parent, order); }; - /** - * ### DOM.getElement - * - * Creates a generic HTML element with id and attributes as specified - * - * @param {string} elem The name of the tag - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.addAttributes2Elem - */ - DOM.getElement = function(elem, id, attributes) { - var e = document.createElement(elem); - if ('undefined' !== typeof id) { - e.id = id; - } - return this.addAttributes2Elem(e, attributes); - }; - - /** - * ### DOM.addElement - * - * Creates and appends a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {HTMLElement} root The root element to which the new element will - * be appended - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.getElement - * @see DOM.addAttributes2Elem - */ - DOM.addElement = function(elem, root, id, attributes) { - var el = this.getElement(elem, id, attributes); - return root.appendChild(el); - }; - /** * ### DOM.addAttributes2Elem * @@ -1518,8 +1473,100 @@ } - return scanDocuments(prefix + '_' + JSUS.randomInt(0, 10000000)); - //return scanDocuments(prefix); + return scanDocuments(prefix + '_' + JSUS.randomInt(10000000)); + }; + + /** + * ### DOM.get + * + * Creates a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.add + * @see DOM.addAttributes2Elem + */ + DOM.get = function(name, attributes) { + var el; + el = document.createElement(name); + if ('string' === typeof attributes) el.id = attributes; + else if (attributes) this.addAttributes2Elem(el, attributes); + return el; + }; + + /** + * ### DOM.add|append + * + * Creates and append an element with specified attributes to a root + * + * @param {string} name The name of the HTML tag + * @param {HTMLElement} root The root element to which the new element + * will be appended + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.get + */ + DOM.add = DOM.append = function(name, root, attributes) { + var el; + el = this.get(name, attributes); + return root.appendChild(el); + }; + + /** + * ### DOM.getElement + * + * Creates a generic HTML element with id and attributes as specified + * + * @param {string} elem The name of the tag + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.addAttributes2Elem + * + * @deprecated + */ + DOM.getElement = function(elem, id, attributes) { + var e; + console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); + e = document.createElement(elem); + if ('undefined' !== typeof id) e.id = id; + return this.addAttributes2Elem(e, attributes); + }; + + /** + * ### DOM.addElement + * + * Creates and appends a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {HTMLElement} root The root element to which the new element will + * be appended + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.getElement + * @see DOM.addAttributes2Elem + * + * @deprecated + */ + DOM.addElement = function(elem, root, id, attributes) { + var el; + console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); + el = this.getElement(elem, id, attributes); + return root.appendChild(el); }; /** diff --git a/build/jsus.min.js b/build/jsus.min.js index 661f9cf..8105923 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=t.get=function(e){return"undefined"==typeof t.clone?(t.log("JSUS.clone not found. Cannot continue."),!1):"undefined"==typeof e?t.clone(t._classes):"undefined"==typeof t._classes[e]?(t.log("Could not find class "+e),!1):t.clone(t._classes[e])},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;i1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;i1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o tag * - * @param {Element} root The HTML element where to write into - * @param {mixed} text The text to write. Default, an ampty string + * @param {HTMLElement} root The HTML element where to write into + * @param {string|HTMLElement} text The text to write or an element + * to append. Default: an ampty string * @param {string} rc the name of the tag to use as a break element * * @return {TextNode} The text node inserted in the root element * * @see DOM.write - * @see DOM.addBreak */ DOM.writeln = function(root, text, rc) { var content; content = DOM.write(root, text); - this.addBreak(root, rc); + this.add(rc || 'br', root); return content; }; @@ -346,50 +347,6 @@ return DOM.shuffleElements(parent, order); }; - /** - * ### DOM.getElement - * - * Creates a generic HTML element with id and attributes as specified - * - * @param {string} elem The name of the tag - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.addAttributes2Elem - */ - DOM.getElement = function(elem, id, attributes) { - var e = document.createElement(elem); - if ('undefined' !== typeof id) { - e.id = id; - } - return this.addAttributes2Elem(e, attributes); - }; - - /** - * ### DOM.addElement - * - * Creates and appends a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {HTMLElement} root The root element to which the new element will - * be appended - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.getElement - * @see DOM.addAttributes2Elem - */ - DOM.addElement = function(elem, root, id, attributes) { - var el = this.getElement(elem, id, attributes); - return root.appendChild(el); - }; - /** * ### DOM.addAttributes2Elem * @@ -532,18 +489,17 @@ } - return scanDocuments(prefix + '_' + JSUS.randomInt(0, 10000000)); - //return scanDocuments(prefix); + return scanDocuments(prefix + '_' + JSUS.randomInt(10000000)); }; - + /** * ### DOM.get * * Creates a generic HTML element with specified attributes * * @param {string} elem The name of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element * * @return {HTMLElement} The newly created HTML element * @@ -553,19 +509,21 @@ DOM.get = function(name, attributes) { var el; el = document.createElement(name); - return this.addAttributes2Elem(el, attributes); + if ('string' === typeof attributes) el.id = attributes; + else if (attributes) this.addAttributes2Elem(el, attributes); + return el; }; - + /** - * ### DOM.append|add + * ### DOM.add|append * * Creates and append an element with specified attributes to a root * * @param {string} name The name of the HTML tag - * @param {HTMLElement} root The root element to which the new element + * @param {HTMLElement} root The root element to which the new element * will be appended - * @param {object} attributes Optional. Object containing attributes for - * the newly created element + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element * * @return {HTMLElement} The newly created HTML element * @@ -597,7 +555,7 @@ var e; console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); e = document.createElement(elem); - if ('undefined' !== typeof id) e.id = id; + if ('undefined' !== typeof id) e.id = id; return this.addAttributes2Elem(e, attributes); }; @@ -626,8 +584,7 @@ el = this.getElement(elem, id, attributes); return root.appendChild(el); }; - - + /** * ### DOM.getLabel * From 2d8fd1cddc9035882f1fb75714678be9e6eed975 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sun, 2 Jul 2017 12:33:39 -0400 Subject: [PATCH 05/79] DOM.generateUniqueId --- build/jsus.js | 160 ++++++++++++++++++++++++---------------------- build/jsus.min.js | 2 +- jsus.js | 6 +- lib/dom.js | 154 ++++++++++++++++++++++++-------------------- 4 files changed, 171 insertions(+), 151 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index 38d554b..337ea96 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -29,9 +29,7 @@ * * @param {string} txt Text to output */ - JSUS.log = function(txt) { - console.log(txt); - }; + JSUS.log = function(txt) { console.log(txt); }; /** * ## JSUS.extend @@ -49,8 +47,6 @@ * @param {object|function} target The object to extend * * @return {object|function} target The extended object - * - * @see JSUS.get */ JSUS.extend = function(additional, target) { var name, prop; @@ -984,32 +980,10 @@ /** * # DOM - * - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * - * Collection of static functions related to DOM manipulation - * * Helper library to perform generic operation with DOM elements. - * - * The general syntax is the following: Every HTML element has associated - * a get* and a add* method, whose syntax is very similar. - * - * - The get* method creates the element and returns it. - * - The add* method creates the element, append it as child to a root element, - * and then returns it. - * - * The syntax of both method is the same, but the add* method - * needs the root element as first parameter. E.g. - * - * - getButton(id, text, attributes); - * - addButton(root, id, text, attributes); - * - * The last parameter is generally an object containing a list of - * of key-values pairs as additional attributes to set to the element. - * - * Only the methods which do not follow the above-mentioned syntax - * will receive further explanation. */ (function(JSUS) { @@ -1318,23 +1292,10 @@ return idOrder; }; - /** - * ### DOM.shuffleNodes - * - * It actually shuffles Elements. - * - * @deprecated - */ - DOM.shuffleNodes = function(parent, order) { - console.log('***DOM.shuffleNodes is deprecated. ' + - 'Use Dom.shuffleElements instead.***'); - return DOM.shuffleElements(parent, order); - }; - /** * ### DOM.addAttributes2Elem * - * Adds attributes to an HTML element and returns it. + * Adds attributes to an HTML element and returns it * * Attributes are defined as key-values pairs. * Attributes 'label' is ignored, attribute 'className' ('class') and @@ -1397,7 +1358,8 @@ /** * ### DOM.populateSelect * - * Appends a list of options into a HTML select element. + * Appends a list of options into a HTML select element + * * The second parameter list is an object containing * a list of key-values pairs as text-value attributes for * the option. @@ -1421,61 +1383,105 @@ /** * ### DOM.removeChildrenFromNode * - * Removes all children from a node. + * Removes all children from a node * * @param {HTMLElement} e HTML element. */ - DOM.removeChildrenFromNode = function(e) { - while (e.hasChildNodes()) { - e.removeChild(e.firstChild); + DOM.removeChildrenFromNode = function(elem) { + while (elem.hasChildNodes()) { + elem.removeChild(elem.firstChild); } }; /** * ### DOM.insertAfter * - * Insert a node element after another one. + * Inserts a node element after another one * - * The first parameter is the node to add. + * @param {Node} node The node element to insert + * @param {Node} referenceNode The node element after which the + * the insertion is performed * + * @return {Node} The inserted node */ DOM.insertAfter = function(node, referenceNode) { - referenceNode.insertBefore(node, referenceNode.nextSibling); + return referenceNode.insertBefore(node, referenceNode.nextSibling); }; /** * ### DOM.generateUniqueId * - * Generate a unique id for the page (frames included). + * Generates a unique id for the whole page, frames included + * + * The resulting id is of the type: prefix_randomdigits. * - * TODO: now it always create big random strings, it does not actually - * check if the string exists. + * @param {string} prefix Optional. A given prefix. Default: a random + * string of 8 characters. + * @param {boolean} checkFrames Optional. If TRUE, the id will be unique + * all frames as well. Default: TRUE * + * @return {string} id The unique id */ - DOM.generateUniqueId = function(prefix) { - var search = [window]; - if (window.frames) { - search = search.concat(window.frames); + DOM.generateUniqueId = (function() { + var limit; + limit = 100; + + // Returns TRUE if id is NOT found in all docs (optimized). + function scanDocuments(docs, id) { + var i, len; + len = docs.length + if (len === 1) { + return !docs[0].document.getElementById(id); + } + if (len === 2) { + return !!(docs[0].document.getElementById(id) && + docs[1].document.getElementById(id)); + } + i = -1; + for ( ; ++i < len ; ) { + if (docs[i].document.getElementById(id)) return false; + } + return true; } + + return function(prefix, checkFrames) { + var id, windows; + var found, i, len, counter; - function scanDocuments(id) { - var found = true; + if (prefix) { + if ('string' !== typeof prefix && 'number' !== typeof prefix) { + throw new TypeError('DOM.generateUniqueId: prefix must ' + + 'be string or number. Found: ' + + prefix); + } + } + else { + prefix = JSUS.randomString(8, 'a'); + } + id = prefix + '_'; + + windows = [ window ]; + if ((checkFrames || 'undefined' === typeof checkFrames) && + window.frames) { + + windows = windows.concat(window.frames); + } + + found = true; + counter = -1; while (found) { - for (var i=0; i < search.length; i++) { - found = search[i].document.getElementById(id); - if (found) { - id = '' + id + '_' + JSUS.randomInt(0, 1000); - break; - } + id = prefix + '_' + JSUS.randomInt(1000); + found = scanDocuments(windows, id); + if (++counter > limit) { + throw new Error('DOM.generateUniqueId: could not ' + + 'find unique id within ' + limit + + ' trials.'); } } return id; - } - - - return scanDocuments(prefix + '_' + JSUS.randomInt(10000000)); - }; - + }; + })(); + /** * ### DOM.get * @@ -1615,10 +1621,11 @@ attributes = attributes || {}; - attributes = JSUS.merge(attributes, {rel : 'stylesheet', - type: 'text/css', - href: css - }); + attributes = JSUS.merge(attributes, { + rel : 'stylesheet', + type: 'text/css', + href: css + }); return this.addElement('link', root, id, attributes); }; @@ -1665,6 +1672,8 @@ * @param {HTMLElement} elem The element to highlight * @param {string} code The type of highlight * + * @return {HTMLElement} elem The styled element + * * @see DOM.addBorder * @see DOM.style */ @@ -1700,6 +1709,7 @@ * * Adds a border around the specified element. Color, * width, and type can be specified. + * */ DOM.addBorder = function(elem, color, width, type) { var properties; diff --git a/build/jsus.min.js b/build/jsus.min.js index 8105923..298dadd 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e){return"undefined"==typeof t.clone?(t.log("JSUS.clone not found. Cannot continue."),!1):"undefined"==typeof e?t.clone(t._classes):"undefined"==typeof t._classes[e]?(t.log("Could not find class "+e),!1):t.clone(t._classes[e])},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;i1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.get=function(e,t){var n;return n=document.createElement(e),"string"==typeof t?n.id=t:t&&this.addAttributes2Elem(n,t),n},r.add=r.append=function(e,t,n){var r;return r=this.get(e,n),t.appendChild(r)},r.getElement=function(e,t,n){var r;return console.log("***DOM.getElement is deprecated. Use DOM.get instead.***"),r=document.createElement(e),"undefined"!=typeof t&&(r.id=t),this.addAttributes2Elem(r,n)},r.addElement=function(e,t,n,r){var i;return console.log("***DOM.addElement is deprecated. Use DOM.add instead.***"),i=this.getElement(e,n,r),t.appendChild(i)},r.getLabel=function(e,t,n,r){if(!e)return!1;var i=document.createElement("label");return i.id=t,i.appendChild(document.createTextNode(n)),"undefined"==typeof e.id&&(e.id=this.generateUniqueId()),i.setAttribute("for",e.id),this.addAttributes2Elem(i,r),i},r.addLabel=function(e,t,n,r,i){if(!e||!t||!r)return!1;var s=this.getLabel(t,n,r,i);return e.insertBefore(s,t),s},r.addCSS=function(t,n,r,i){return t=t||document.head||document.body||document,t?(i=i||{},i=e.merge(i,{rel:"stylesheet",type:"text/css",href:n}),this.addElement("link",t,r,i)):!1},r.addJS=function(t,n,r,i){return t=t||document.head||document.body||document,t?(i=i||{},i=e.merge(i,{charset:"utf-8",type:"text/javascript",src:n}),this.addElement("script",t,r,i)):!1},r.highlight=function(e,t){var n;if(!e)return;switch(t){case"OK":n="green";break;case"WARN":n="yellow";break;case"ERR":n="red";break;default:t.charAt(0)==="#"?n=t:n="red"}return this.addBorder(e,n)},r.addBorder=function(e,t,n,i){var s;if(!e)return;return t=t||"red",n=n||"5px",i=i||"solid",s={border:n+" "+i+" "+t},r.style(e,s)},r.style=function(e,t){var n;if(!e||!t)return;if(!r.isElement(e))return;for(n in t)t.hasOwnProperty(n)&&(e.style[n]=t[n]);return e},r.removeClass=function(e,t){var n,r;if(!e||!t)return;return n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),r=e.className=e.className.replace(n,""),e},r.addClass=function(e,t){if(!e)return;if(t instanceof Array)t=t.join(" ");else if("string"!=typeof t)return;return!e.className||e.className===""?e.className=t:e.className+=" "+t,e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o * MIT Licensed * - * Collection of static functions related to DOM manipulation - * * Helper library to perform generic operation with DOM elements. - * - * The general syntax is the following: Every HTML element has associated - * a get* and a add* method, whose syntax is very similar. - * - * - The get* method creates the element and returns it. - * - The add* method creates the element, append it as child to a root element, - * and then returns it. - * - * The syntax of both method is the same, but the add* method - * needs the root element as first parameter. E.g. - * - * - getButton(id, text, attributes); - * - addButton(root, id, text, attributes); - * - * The last parameter is generally an object containing a list of - * of key-values pairs as additional attributes to set to the element. - * - * Only the methods which do not follow the above-mentioned syntax - * will receive further explanation. */ (function(JSUS) { @@ -334,23 +312,10 @@ return idOrder; }; - /** - * ### DOM.shuffleNodes - * - * It actually shuffles Elements. - * - * @deprecated - */ - DOM.shuffleNodes = function(parent, order) { - console.log('***DOM.shuffleNodes is deprecated. ' + - 'Use Dom.shuffleElements instead.***'); - return DOM.shuffleElements(parent, order); - }; - /** * ### DOM.addAttributes2Elem * - * Adds attributes to an HTML element and returns it. + * Adds attributes to an HTML element and returns it * * Attributes are defined as key-values pairs. * Attributes 'label' is ignored, attribute 'className' ('class') and @@ -413,7 +378,8 @@ /** * ### DOM.populateSelect * - * Appends a list of options into a HTML select element. + * Appends a list of options into a HTML select element + * * The second parameter list is an object containing * a list of key-values pairs as text-value attributes for * the option. @@ -437,61 +403,105 @@ /** * ### DOM.removeChildrenFromNode * - * Removes all children from a node. + * Removes all children from a node * * @param {HTMLElement} e HTML element. */ - DOM.removeChildrenFromNode = function(e) { - while (e.hasChildNodes()) { - e.removeChild(e.firstChild); + DOM.removeChildrenFromNode = function(elem) { + while (elem.hasChildNodes()) { + elem.removeChild(elem.firstChild); } }; /** * ### DOM.insertAfter * - * Insert a node element after another one. + * Inserts a node element after another one * - * The first parameter is the node to add. + * @param {Node} node The node element to insert + * @param {Node} referenceNode The node element after which the + * the insertion is performed * + * @return {Node} The inserted node */ DOM.insertAfter = function(node, referenceNode) { - referenceNode.insertBefore(node, referenceNode.nextSibling); + return referenceNode.insertBefore(node, referenceNode.nextSibling); }; /** * ### DOM.generateUniqueId * - * Generate a unique id for the page (frames included). + * Generates a unique id for the whole page, frames included + * + * The resulting id is of the type: prefix_randomdigits. * - * TODO: now it always create big random strings, it does not actually - * check if the string exists. + * @param {string} prefix Optional. A given prefix. Default: a random + * string of 8 characters. + * @param {boolean} checkFrames Optional. If TRUE, the id will be unique + * all frames as well. Default: TRUE * + * @return {string} id The unique id */ - DOM.generateUniqueId = function(prefix) { - var search = [window]; - if (window.frames) { - search = search.concat(window.frames); + DOM.generateUniqueId = (function() { + var limit; + limit = 100; + + // Returns TRUE if id is NOT found in all docs (optimized). + function scanDocuments(docs, id) { + var i, len; + len = docs.length + if (len === 1) { + return !docs[0].document.getElementById(id); + } + if (len === 2) { + return !!(docs[0].document.getElementById(id) && + docs[1].document.getElementById(id)); + } + i = -1; + for ( ; ++i < len ; ) { + if (docs[i].document.getElementById(id)) return false; + } + return true; } + + return function(prefix, checkFrames) { + var id, windows; + var found, i, len, counter; + + if (prefix) { + if ('string' !== typeof prefix && 'number' !== typeof prefix) { + throw new TypeError('DOM.generateUniqueId: prefix must ' + + 'be string or number. Found: ' + + prefix); + } + } + else { + prefix = JSUS.randomString(8, 'a'); + } + id = prefix + '_'; + + windows = [ window ]; + if ((checkFrames || 'undefined' === typeof checkFrames) && + window.frames) { + + windows = windows.concat(window.frames); + } - function scanDocuments(id) { - var found = true; + found = true; + counter = -1; while (found) { - for (var i=0; i < search.length; i++) { - found = search[i].document.getElementById(id); - if (found) { - id = '' + id + '_' + JSUS.randomInt(0, 1000); - break; - } + id = prefix + '_' + JSUS.randomInt(1000); + found = scanDocuments(windows, id); + if (++counter > limit) { + throw new Error('DOM.generateUniqueId: could not ' + + 'find unique id within ' + limit + + ' trials.'); } } return id; - } - - - return scanDocuments(prefix + '_' + JSUS.randomInt(10000000)); - }; - + }; + })(); + /** * ### DOM.get * @@ -631,10 +641,11 @@ attributes = attributes || {}; - attributes = JSUS.merge(attributes, {rel : 'stylesheet', - type: 'text/css', - href: css - }); + attributes = JSUS.merge(attributes, { + rel : 'stylesheet', + type: 'text/css', + href: css + }); return this.addElement('link', root, id, attributes); }; @@ -681,6 +692,8 @@ * @param {HTMLElement} elem The element to highlight * @param {string} code The type of highlight * + * @return {HTMLElement} elem The styled element + * * @see DOM.addBorder * @see DOM.style */ @@ -716,6 +729,7 @@ * * Adds a border around the specified element. Color, * width, and type can be specified. + * */ DOM.addBorder = function(elem, color, width, type) { var properties; From 2413837dd8bcc1b7e7b5533c3b0dc7e66116f759 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sun, 2 Jul 2017 12:41:47 -0400 Subject: [PATCH 06/79] minor --- lib/dom.js | 281 +++++++++++++++++++++++++++-------------------------- 1 file changed, 145 insertions(+), 136 deletions(-) diff --git a/lib/dom.js b/lib/dom.js index b8df092..b9e8e37 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -312,6 +312,99 @@ return idOrder; }; + /** + * ### DOM.get + * + * Creates a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.add + * @see DOM.addAttributes2Elem + */ + DOM.get = function(name, attributes) { + var el; + el = document.createElement(name); + if ('string' === typeof attributes) el.id = attributes; + else if (attributes) this.addAttributes2Elem(el, attributes); + return el; + }; + + /** + * ### DOM.add|append + * + * Creates and append an element with specified attributes to a root + * + * @param {string} name The name of the HTML tag + * @param {HTMLElement} root The root element to which the new element + * will be appended + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.get + */ + DOM.add = DOM.append = function(name, root, attributes) { + var el; + el = this.get(name, attributes); + return root.appendChild(el); + }; + + /** + * ### DOM.getElement + * + * Creates a generic HTML element with id and attributes as specified + * + * @param {string} elem The name of the tag + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.addAttributes2Elem + * + * @deprecated + */ + DOM.getElement = function(elem, id, attributes) { + var e; + console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); + e = document.createElement(elem); + if ('undefined' !== typeof id) e.id = id; + return this.addAttributes2Elem(e, attributes); + }; + + /** + * ### DOM.addElement + * + * Creates and appends a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {HTMLElement} root The root element to which the new element will + * be appended + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.getElement + * @see DOM.addAttributes2Elem + * + * @deprecated + */ + DOM.addElement = function(elem, root, id, attributes) { + var el; + console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); + el = this.getElement(elem, id, attributes); + return root.appendChild(el); + }; + /** * ### DOM.addAttributes2Elem * @@ -330,20 +423,27 @@ * @see DOM.addClass * @see DOM.style */ - DOM.addAttributes2Elem = function(e, a) { + DOM.addAttributes2Elem = function(elem, attributes) { var key; - if (!e || !a) return e; - if ('object' != typeof a) return e; - for (key in a) { - if (a.hasOwnProperty(key)) { + if (!J.isElement(elem)) { + throw new TypeError('DOM.addAttributes2Elem: elem must be ' + + 'HTMLElement. Found: ' + elem); + } + if ('undefined' === typeof attributes) return elem; + if ('object' !== typeof attributes) { + throw new TypeError('DOM.addAttributes2Elem: attributes must be ' + + 'object or undefined. Found: ' + attributes); + } + for (key in attributes) { + if (attributes.hasOwnProperty(key)) { if (key === 'id') { - e.id = a[key]; + elem.id = attributes[key]; } else if (key === 'class' || key === 'className') { - DOM.addClass(e, a[key]); + DOM.addClass(elem, attributes[key]); } else if (key === 'style') { - DOM.style(e, a[key]); + DOM.style(elem, attributes[key]); } else if (key === 'label') { // Handle the case. @@ -351,7 +451,7 @@ 'supported. Use DOM.addLabel instead.'); } else { - e.setAttribute(key, a[key]); + elem.setAttribute(key, attributes[key]); } @@ -372,8 +472,39 @@ // --> } } - return e; + return elem; }; + + /** + * ### DOM.getLabel + * + */ + DOM.getLabel = function(forElem, id, labelText, attributes) { + if (!forElem) return false; + var label = document.createElement('label'); + label.id = id; + label.appendChild(document.createTextNode(labelText)); + + if ('undefined' === typeof forElem.id) { + forElem.id = this.generateUniqueId(); + } + + label.setAttribute('for', forElem.id); + this.addAttributes2Elem(label, attributes); + return label; + }; + + /** + * ### DOM.addLabel + * + */ + DOM.addLabel = function(root, forElem, id, labelText, attributes) { + if (!root || !forElem || !labelText) return false; + var l = this.getLabel(forElem, id, labelText, attributes); + root.insertBefore(l, forElem); + return l; + }; + /** * ### DOM.populateSelect @@ -432,7 +563,7 @@ * ### DOM.generateUniqueId * * Generates a unique id for the whole page, frames included - * + * * The resulting id is of the type: prefix_randomdigits. * * @param {string} prefix Optional. A given prefix. Default: a random @@ -463,7 +594,7 @@ } return true; } - + return function(prefix, checkFrames) { var id, windows; var found, i, len, counter; @@ -479,11 +610,11 @@ prefix = JSUS.randomString(8, 'a'); } id = prefix + '_'; - + windows = [ window ]; if ((checkFrames || 'undefined' === typeof checkFrames) && window.frames) { - + windows = windows.concat(window.frames); } @@ -501,129 +632,7 @@ return id; }; })(); - - /** - * ### DOM.get - * - * Creates a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {object|string} attributes Optional. Object containing - * attributes for the element. If string, the id of the element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.add - * @see DOM.addAttributes2Elem - */ - DOM.get = function(name, attributes) { - var el; - el = document.createElement(name); - if ('string' === typeof attributes) el.id = attributes; - else if (attributes) this.addAttributes2Elem(el, attributes); - return el; - }; - /** - * ### DOM.add|append - * - * Creates and append an element with specified attributes to a root - * - * @param {string} name The name of the HTML tag - * @param {HTMLElement} root The root element to which the new element - * will be appended - * @param {object|string} attributes Optional. Object containing - * attributes for the element. If string, the id of the element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.get - */ - DOM.add = DOM.append = function(name, root, attributes) { - var el; - el = this.get(name, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.getElement - * - * Creates a generic HTML element with id and attributes as specified - * - * @param {string} elem The name of the tag - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.getElement = function(elem, id, attributes) { - var e; - console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); - e = document.createElement(elem); - if ('undefined' !== typeof id) e.id = id; - return this.addAttributes2Elem(e, attributes); - }; - - /** - * ### DOM.addElement - * - * Creates and appends a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {HTMLElement} root The root element to which the new element will - * be appended - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.getElement - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.addElement = function(elem, root, id, attributes) { - var el; - console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); - el = this.getElement(elem, id, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.getLabel - * - */ - DOM.getLabel = function(forElem, id, labelText, attributes) { - if (!forElem) return false; - var label = document.createElement('label'); - label.id = id; - label.appendChild(document.createTextNode(labelText)); - - if ('undefined' === typeof forElem.id) { - forElem.id = this.generateUniqueId(); - } - - label.setAttribute('for', forElem.id); - this.addAttributes2Elem(label, attributes); - return label; - }; - - /** - * ### DOM.addLabel - * - */ - DOM.addLabel = function(root, forElem, id, labelText, attributes) { - if (!root || !forElem || !labelText) return false; - var l = this.getLabel(forElem, id, labelText, attributes); - root.insertBefore(l, forElem); - return l; - }; // ## CSS / JS From 30fcc4aefaf5688d90fa76eb017e254deab9a02a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sun, 2 Jul 2017 14:28:55 -0400 Subject: [PATCH 07/79] improving DOM --- lib/dom.js | 74 +++++++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/lib/dom.js b/lib/dom.js index b9e8e37..b8ef005 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -478,34 +478,78 @@ /** * ### DOM.getLabel * + * Returns a label element for a given element + * + * @param {string} text The text of the label + * @param {HTMLElement|string} forElem The HTML element for which the + * label is, or its id. If the element has no id, a random one is + * created and assigned. + * @param {object|string} attributes Optional. Additional attributes + * for the label or its id + * + * @return {HTMLElement} label The label + * + * @see DOM.addAttributes2Elem */ - DOM.getLabel = function(forElem, id, labelText, attributes) { - if (!forElem) return false; - var label = document.createElement('label'); - label.id = id; - label.appendChild(document.createTextNode(labelText)); - - if ('undefined' === typeof forElem.id) { - forElem.id = this.generateUniqueId(); + DOM.getLabel = function(text, forElem, attributes) { + var label; + if ('string' !== typeof text) { + throw new TypeError('DOM.getLabel: text must be string. Found: ' + + text); } + // Create Label and text. + label = document.createElement('label'); + label.appendChild(document.createTextNode(text)); + if (forElem) { + if ('string' === typeof forElem) { + label.id = forElem; + } + else if (J.isElement(forElem)) { + // Add unique id for the referenced element, if missing. + if (!forElem.id) forElem.id = this.generateUniqueId(); + label.setAttribute('for', forElem.id); + } + else { + throw new TypeError('DOM.getLabel: forElem must be ' + + 'HTMLElement or string. Found: ' + + forElem); + } - label.setAttribute('for', forElem.id); - this.addAttributes2Elem(label, attributes); + } + + // Label attributes. + if ('string' === typeof attributes) label.id = attributes; + else if (attributes) this.addAttributes2Elem(label, attributes); return label; }; /** * ### DOM.addLabel * + * Appends a label element for a given element to a root + * + * @param {string} text The text of the label + * @param {HTMLElement|string} forElem The HTML element for which the + * label is, or its id. If the element has no id, a random one is + * created and assigned. + * @param {HTMLElement} root The root element to which the label will + * be appended + * @param {object|string} attributes Optional. Additional attributes + * for the label or its id + * + * @return {HTMLElement} label The label + * + * @see DOM.addAttributes2Elem + * + * @TODO: continue here. what is the best behavior? */ - DOM.addLabel = function(root, forElem, id, labelText, attributes) { - if (!root || !forElem || !labelText) return false; - var l = this.getLabel(forElem, id, labelText, attributes); - root.insertBefore(l, forElem); + DOM.addLabel = function(text, forElem, root, attributes) { + var label; + label = this.getLabel(text, forElem, attributes); + root.insertBefore(label, forElem); return l; }; - /** * ### DOM.populateSelect * From 403812ff51a7e127844a44afd25c2053d96d67bf Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sun, 2 Jul 2017 21:32:45 -0400 Subject: [PATCH 08/79] DOM work in progress --- lib/dom.js | 495 +++++++++++++++++++++++++++-------------------------- 1 file changed, 256 insertions(+), 239 deletions(-) diff --git a/lib/dom.js b/lib/dom.js index b8ef005..81af5af 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -13,8 +13,263 @@ function DOM() {} - // ## GENERAL + // ## GET/ADD + /** + * ### DOM.get + * + * Creates a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.add + * @see DOM.addAttributes2Elem + */ + DOM.get = function(name, attributes) { + var el; + if (name === 'label') { + el = DOM.getLabel(arguments[2], arguments[3], attributes); + } + else { + el = document.createElement(name); + } + if ('string' === typeof attributes) el.id = attributes; + else if (attributes) this.addAttributes2Elem(el, attributes); + return el; + }; + + W.add('label', forEl.parentNode, { 'for': forEl.id }, 'my label', forEl); + W.add('label', 'my label', forEl.parentNode); + + W.add('span', root, { innerHTML + + /** + * ### DOM.add|append + * + * Creates and append an element with specified attributes to a root + * + * Special elements with extra parameters + * + * - 'label': accepts two extra parameter + * + * @param {string} name The name of the HTML tag + * @param {HTMLElement} root The root element to which the new element + * will be appended + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.get + */ + DOM.add = DOM.append = function(name, root, attributes) { + var el; + if (name === 'label') { + // TODO: continue here. + el = DOM.getLabel(arguments[2], arguments[3], attributes); + if (arguments[3]) root.insertBefore(el, forElem); + else root.appendChild(el); + } + else { + el = this.get(name, attributes); + root.appendChild(el); + } + return el; + }; + + /** + * ### DOM.getElement + * + * Creates a generic HTML element with id and attributes as specified + * + * @param {string} elem The name of the tag + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.addAttributes2Elem + * + * @deprecated + */ + DOM.getElement = function(elem, id, attributes) { + var e; + console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); + e = document.createElement(elem); + if ('undefined' !== typeof id) e.id = id; + return this.addAttributes2Elem(e, attributes); + }; + + /** + * ### DOM.addElement + * + * Creates and appends a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {HTMLElement} root The root element to which the new element will + * be appended + * @param {string} id Optional. The id of the tag + * @param {object} attributes Optional. Object containing attributes for + * the newly created element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.getElement + * @see DOM.addAttributes2Elem + * + * @deprecated + */ + DOM.addElement = function(elem, root, id, attributes) { + var el; + console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); + el = this.getElement(elem, id, attributes); + return root.appendChild(el); + }; + + /** + * ### DOM.addAttributes2Elem + * + * Adds attributes to an HTML element and returns it + * + * Attributes are defined as key-values pairs and added + * + * Special cases: + * + * - 'className': alias for class + * - 'class': add a class to the className property (does not overwrite) + * - 'style': adds property to the style property (see DOM.style) + * - 'id': the id of the element + * - 'innerHTML': the innerHTML property of the element (overwrites) + * - 'label': adds a element in front (element.parentNode must exist) + * + * @param {HTMLElement} elem The element to decorate + * @param {object} attributes Object containing attributes to + * add to the element + * + * @return {HTMLElement} The element with attributes + * + * @see DOM.addLabel + * @see DOM.addClass + * @see DOM.style + */ + DOM.addAttributes2Elem = function(elem, attributes) { + var key; + if (!J.isElement(elem)) { + throw new TypeError('DOM.addAttributes2Elem: elem must be ' + + 'HTMLElement. Found: ' + elem); + } + if ('undefined' === typeof attributes) return elem; + if ('object' !== typeof attributes) { + throw new TypeError('DOM.addAttributes2Elem: attributes must be ' + + 'object or undefined. Found: ' + attributes); + } + for (key in attributes) { + if (attributes.hasOwnProperty(key)) { + if (key === 'id' || key === 'innerHTML') { + elem[key] = attributes[key]; + } + else if (key === 'class' || key === 'className') { + DOM.addClass(elem, attributes[key]); + } + else if (key === 'style') { + DOM.style(elem, attributes[key]); + } + else if (key === 'label') { + if (!elem.parentNode) { + // TODO: continue here! + // this.addLabel(e.parentNode, e, a[key]); + } + } + else { + elem.setAttribute(key, attributes[key]); + } + } + } + return elem; + }; + + /** + * ### DOM.getLabel + * + * Returns a label element for a given element + * + * @param {string} text The text of the label + * @param {HTMLElement|string} forElem The HTML element for which the + * label is, or its id. If the element has no id, a random one is + * created and assigned. + * @param {object|string} attributes Optional. Additional attributes + * for the label or its id + * + * @return {HTMLElement} label The label + * + * @see DOM.addAttributes2Elem + */ + DOM.getLabel = function(text, forElem, attributes) { + var label; + if ('string' !== typeof text) { + throw new TypeError('DOM.getLabel: text must be string. Found: ' + + text); + } + // Create label and text. + label = document.createElement('label'); + label.appendChild(document.createTextNode(text)); + if (forElem) { + if ('string' === typeof forElem) { + label.id = forElem; + } + else if (J.isElement(forElem)) { + // Add unique id for the referenced element, if missing. + if (!forElem.id) forElem.id = this.generateUniqueId(); + label.setAttribute('for', forElem.id); + } + else { + throw new TypeError('DOM.getLabel: forElem must be ' + + 'HTMLElement or string. Found: ' + + forElem); + } + + } + + // Label attributes. + if ('string' === typeof attributes) label.id = attributes; + else if (attributes) this.addAttributes2Elem(label, attributes); + return label; + }; + + /** + * ### DOM.addLabel + * + * Appends a label element for a given element to a root + * + * @param {string} text The text of the label + * @param {HTMLElement|string} forElem The HTML element for which the + * label is, or its id. If the element has no id, a random one is + * created and assigned. + * @param {HTMLElement} root The root element to which the label will + * be appended + * @param {object|string} attributes Optional. Additional attributes + * for the label or its id + * + * @return {HTMLElement} label The label element + * + * @see DOM.addAttributes2Elem + * + * @TODO: continue here. what is the best behavior? + */ + DOM.addLabel = function(text, forElem, root, attributes) { + var label; + if ('undefined' === typeof root) { + root = forElem + label = this.getLabel(text, forElem, attributes); + root.insertBefore(label, forElem); + return l; + }; + /** * ### DOM.write * @@ -312,244 +567,6 @@ return idOrder; }; - /** - * ### DOM.get - * - * Creates a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {object|string} attributes Optional. Object containing - * attributes for the element. If string, the id of the element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.add - * @see DOM.addAttributes2Elem - */ - DOM.get = function(name, attributes) { - var el; - el = document.createElement(name); - if ('string' === typeof attributes) el.id = attributes; - else if (attributes) this.addAttributes2Elem(el, attributes); - return el; - }; - - /** - * ### DOM.add|append - * - * Creates and append an element with specified attributes to a root - * - * @param {string} name The name of the HTML tag - * @param {HTMLElement} root The root element to which the new element - * will be appended - * @param {object|string} attributes Optional. Object containing - * attributes for the element. If string, the id of the element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.get - */ - DOM.add = DOM.append = function(name, root, attributes) { - var el; - el = this.get(name, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.getElement - * - * Creates a generic HTML element with id and attributes as specified - * - * @param {string} elem The name of the tag - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.getElement = function(elem, id, attributes) { - var e; - console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); - e = document.createElement(elem); - if ('undefined' !== typeof id) e.id = id; - return this.addAttributes2Elem(e, attributes); - }; - - /** - * ### DOM.addElement - * - * Creates and appends a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {HTMLElement} root The root element to which the new element will - * be appended - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.getElement - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.addElement = function(elem, root, id, attributes) { - var el; - console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); - el = this.getElement(elem, id, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.addAttributes2Elem - * - * Adds attributes to an HTML element and returns it - * - * Attributes are defined as key-values pairs. - * Attributes 'label' is ignored, attribute 'className' ('class') and - * 'style' are special and are delegated to special methods. - * - * @param {HTMLElement} e The element to decorate - * @param {object} a Object containing attributes to add to the element - * - * @return {HTMLElement} The decorated element - * - * @see DOM.addLabel - * @see DOM.addClass - * @see DOM.style - */ - DOM.addAttributes2Elem = function(elem, attributes) { - var key; - if (!J.isElement(elem)) { - throw new TypeError('DOM.addAttributes2Elem: elem must be ' + - 'HTMLElement. Found: ' + elem); - } - if ('undefined' === typeof attributes) return elem; - if ('object' !== typeof attributes) { - throw new TypeError('DOM.addAttributes2Elem: attributes must be ' + - 'object or undefined. Found: ' + attributes); - } - for (key in attributes) { - if (attributes.hasOwnProperty(key)) { - if (key === 'id') { - elem.id = attributes[key]; - } - else if (key === 'class' || key === 'className') { - DOM.addClass(elem, attributes[key]); - } - else if (key === 'style') { - DOM.style(elem, attributes[key]); - } - else if (key === 'label') { - // Handle the case. - JSUS.log('DOM.addAttributes2Elem: label attribute is not ' + - 'supported. Use DOM.addLabel instead.'); - } - else { - elem.setAttribute(key, attributes[key]); - } - - - // TODO: handle special cases - // - } - } - return elem; - }; - - /** - * ### DOM.getLabel - * - * Returns a label element for a given element - * - * @param {string} text The text of the label - * @param {HTMLElement|string} forElem The HTML element for which the - * label is, or its id. If the element has no id, a random one is - * created and assigned. - * @param {object|string} attributes Optional. Additional attributes - * for the label or its id - * - * @return {HTMLElement} label The label - * - * @see DOM.addAttributes2Elem - */ - DOM.getLabel = function(text, forElem, attributes) { - var label; - if ('string' !== typeof text) { - throw new TypeError('DOM.getLabel: text must be string. Found: ' + - text); - } - // Create Label and text. - label = document.createElement('label'); - label.appendChild(document.createTextNode(text)); - if (forElem) { - if ('string' === typeof forElem) { - label.id = forElem; - } - else if (J.isElement(forElem)) { - // Add unique id for the referenced element, if missing. - if (!forElem.id) forElem.id = this.generateUniqueId(); - label.setAttribute('for', forElem.id); - } - else { - throw new TypeError('DOM.getLabel: forElem must be ' + - 'HTMLElement or string. Found: ' + - forElem); - } - - } - - // Label attributes. - if ('string' === typeof attributes) label.id = attributes; - else if (attributes) this.addAttributes2Elem(label, attributes); - return label; - }; - - /** - * ### DOM.addLabel - * - * Appends a label element for a given element to a root - * - * @param {string} text The text of the label - * @param {HTMLElement|string} forElem The HTML element for which the - * label is, or its id. If the element has no id, a random one is - * created and assigned. - * @param {HTMLElement} root The root element to which the label will - * be appended - * @param {object|string} attributes Optional. Additional attributes - * for the label or its id - * - * @return {HTMLElement} label The label - * - * @see DOM.addAttributes2Elem - * - * @TODO: continue here. what is the best behavior? - */ - DOM.addLabel = function(text, forElem, root, attributes) { - var label; - label = this.getLabel(text, forElem, attributes); - root.insertBefore(label, forElem); - return l; - }; - /** * ### DOM.populateSelect * From b0be509437c8c7a668f1e9616c5d078d0212efc5 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 3 Jul 2017 12:03:48 -0400 Subject: [PATCH 09/79] updated DOM --- lib/dom.js | 294 +++++++++++++++++++++++------------------------------ 1 file changed, 126 insertions(+), 168 deletions(-) diff --git a/lib/dom.js b/lib/dom.js index 81af5af..d9a6d9b 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -27,112 +27,79 @@ * @return {HTMLElement} The newly created HTML element * * @see DOM.add - * @see DOM.addAttributes2Elem + * @see DOM.addAttributes */ DOM.get = function(name, attributes) { var el; - if (name === 'label') { - el = DOM.getLabel(arguments[2], arguments[3], attributes); - } - else { - el = document.createElement(name); - } + el = document.createElement(name); if ('string' === typeof attributes) el.id = attributes; - else if (attributes) this.addAttributes2Elem(el, attributes); + else if (attributes) this.addAttributes(el, attributes); return el; }; - - W.add('label', forEl.parentNode, { 'for': forEl.id }, 'my label', forEl); - W.add('label', 'my label', forEl.parentNode); - - W.add('span', root, { innerHTML /** * ### DOM.add|append * * Creates and append an element with specified attributes to a root * - * Special elements with extra parameters - * - * - 'label': accepts two extra parameter - * * @param {string} name The name of the HTML tag * @param {HTMLElement} root The root element to which the new element * will be appended - * @param {object|string} attributes Optional. Object containing - * attributes for the element. If string, the id of the element + * @param {object|string} options Optional. Object containing + * attributes for the element and rules about how to insert it relative + * to root. Available options: insertAfter, insertBefore (default: + * child of root). If string, it is the id of the element. Examples: + * + * ```javascript + * // Appends a new new to the body. + * var div = DOM.add('div', document.body); + * // Appends a new new to the body with id 'myid'. + * var div1 = DOM.add('div', document.body, 'myid'); + * // Appends a new new to the body with id 'myid2' and class name 'c'. + * var div2 = DOM.add('div', document.body, { id: 'myid2', className: 'c'}); + * // Appends a new div after div1 with id 'myid'. + * var div3 = DOM.add('div', div1, { id: 'myid3', insertAfter: true }); + * // Appends a new div before div2 with id 'myid'. + * var div3 = DOM.add('div', div2, { id: 'myid3', insertBefore: true }); + * ``` * * @return {HTMLElement} The newly created HTML element * * @see DOM.get + * @see DOM.addAttributes */ - DOM.add = DOM.append = function(name, root, attributes) { + DOM.add = DOM.append = function(name, root, options) { var el; - if (name === 'label') { - // TODO: continue here. - el = DOM.getLabel(arguments[2], arguments[3], attributes); - if (arguments[3]) root.insertBefore(el, forElem); - else root.appendChild(el); + el = this.get(name, attributes); + if (attributes) { + if (attributes.insertBefore) { + if (attributes.insertAfter) { + throw new Error('DOM.add: attributes.insertBefore and ' + + 'attributes.insertBefore cannot be ' + + 'both set.'); + } + if (!root.parentNode) { + throw new Error('DOM.add: root.parentNode not found. ' + + 'Cannot insert before.'); + } + root.parentNode.insertBefore(el, root); + } + else if (attributes.insertAfter) { + if (!root.parentNode) { + throw new Error('DOM.add: root.parentNode not found. ' + + 'Cannot insert before.'); + } + DOM.insertAfter(el, root); + } } else { - el = this.get(name, attributes); root.appendChild(el); - } + } return el; }; /** - * ### DOM.getElement - * - * Creates a generic HTML element with id and attributes as specified - * - * @param {string} elem The name of the tag - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.getElement = function(elem, id, attributes) { - var e; - console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); - e = document.createElement(elem); - if ('undefined' !== typeof id) e.id = id; - return this.addAttributes2Elem(e, attributes); - }; - - /** - * ### DOM.addElement - * - * Creates and appends a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {HTMLElement} root The root element to which the new element will - * be appended - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.getElement - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.addElement = function(elem, root, id, attributes) { - var el; - console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); - el = this.getElement(elem, id, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.addAttributes2Elem + * ### DOM.addAttributes * * Adds attributes to an HTML element and returns it * @@ -145,27 +112,27 @@ * - 'style': adds property to the style property (see DOM.style) * - 'id': the id of the element * - 'innerHTML': the innerHTML property of the element (overwrites) - * - 'label': adds a element in front (element.parentNode must exist) + * - 'insertBefore': ignored + * - 'insertAfter': ignored * * @param {HTMLElement} elem The element to decorate * @param {object} attributes Object containing attributes to * add to the element * - * @return {HTMLElement} The element with attributes + * @return {HTMLElement} The element with speficied attributes added * - * @see DOM.addLabel * @see DOM.addClass * @see DOM.style */ - DOM.addAttributes2Elem = function(elem, attributes) { + DOM.addAttributes = function(elem, attributes) { var key; if (!J.isElement(elem)) { - throw new TypeError('DOM.addAttributes2Elem: elem must be ' + + throw new TypeError('DOM.addAttributes: elem must be ' + 'HTMLElement. Found: ' + elem); } if ('undefined' === typeof attributes) return elem; if ('object' !== typeof attributes) { - throw new TypeError('DOM.addAttributes2Elem: attributes must be ' + + throw new TypeError('DOM.addAttributes: attributes must be ' + 'object or undefined. Found: ' + attributes); } for (key in attributes) { @@ -179,97 +146,14 @@ else if (key === 'style') { DOM.style(elem, attributes[key]); } - else if (key === 'label') { - if (!elem.parentNode) { - // TODO: continue here! - // this.addLabel(e.parentNode, e, a[key]); - } - } - else { + else if (key !== 'insertBefore' && key !== 'insertAfter') { elem.setAttribute(key, attributes[key]); } } } return elem; }; - - /** - * ### DOM.getLabel - * - * Returns a label element for a given element - * - * @param {string} text The text of the label - * @param {HTMLElement|string} forElem The HTML element for which the - * label is, or its id. If the element has no id, a random one is - * created and assigned. - * @param {object|string} attributes Optional. Additional attributes - * for the label or its id - * - * @return {HTMLElement} label The label - * - * @see DOM.addAttributes2Elem - */ - DOM.getLabel = function(text, forElem, attributes) { - var label; - if ('string' !== typeof text) { - throw new TypeError('DOM.getLabel: text must be string. Found: ' + - text); - } - // Create label and text. - label = document.createElement('label'); - label.appendChild(document.createTextNode(text)); - if (forElem) { - if ('string' === typeof forElem) { - label.id = forElem; - } - else if (J.isElement(forElem)) { - // Add unique id for the referenced element, if missing. - if (!forElem.id) forElem.id = this.generateUniqueId(); - label.setAttribute('for', forElem.id); - } - else { - throw new TypeError('DOM.getLabel: forElem must be ' + - 'HTMLElement or string. Found: ' + - forElem); - } - - } - - // Label attributes. - if ('string' === typeof attributes) label.id = attributes; - else if (attributes) this.addAttributes2Elem(label, attributes); - return label; - }; - /** - * ### DOM.addLabel - * - * Appends a label element for a given element to a root - * - * @param {string} text The text of the label - * @param {HTMLElement|string} forElem The HTML element for which the - * label is, or its id. If the element has no id, a random one is - * created and assigned. - * @param {HTMLElement} root The root element to which the label will - * be appended - * @param {object|string} attributes Optional. Additional attributes - * for the label or its id - * - * @return {HTMLElement} label The label element - * - * @see DOM.addAttributes2Elem - * - * @TODO: continue here. what is the best behavior? - */ - DOM.addLabel = function(text, forElem, root, attributes) { - var label; - if ('undefined' === typeof root) { - root = forElem - label = this.getLabel(text, forElem, attributes); - root.insertBefore(label, forElem); - return l; - }; - /** * ### DOM.write * @@ -1488,6 +1372,80 @@ } }; + + + /** + * ### getLabel + * + * Returns a label element for a given element + * + * @param {string} text The text of the label + * @param {HTMLElement|string} forElem The HTML element for which the + * label is, or its id. If the element has no id, a random one is + * created and assigned. + * + * @return {HTMLElement} label The label + * + * @see DOM.addAttributes + */ + function getLabel(text, forElem) { + var label, forId; + if ('string' !== typeof text) { + throw new TypeError('DOM.get: label text must be string. Found: ' + + text); + } + // Create label and text. + label = document.createElement('label'); + label.innerHTML = text; + if (forElem) { + if ('string' === typeof forElem) { + label['for'] = forElem; + } + else if (J.isElement(forElem)) { + // Add unique id for the referenced element, if missing. + if (!forElem.id) forElem.id = this.generateUniqueId(); + forId = forElem.id; + } + else { + throw new TypeError('DOM.get: label forElem must be ' + + 'HTMLElement or string. Found: ' + + forElem); + } + label.setAttribute('for', forId); + } + return label; + } + + /** + * ### DOM.addLabel + * + * Appends a label element for a given element to a root + * + * @param {string} text The text of the label + * @param {HTMLElement|string} forElem The HTML element for which the + * label is, or its id. If the element has no id, a random one is + * created and assigned. + * @param {HTMLElement} root The root element to which the label will + * be appended + * @param {object|string} attributes Optional. Additional attributes + * for the label or its id + * + * @return {HTMLElement} label The label element + * + * @see DOM.addAttributes + * + * @TODO: continue here. what is the best behavior? + */ + //DOM.addLabel = function(text, forElem, root, attributes) { + // var label; + // if ('undefined' === typeof root) { + // root = forElem + // label = this.getLabel(text, forElem, attributes); + // root.insertBefore(label, forElem); + // return l; + //}; + + JSUS.extend(DOM); })('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS); From 5ddc4c751ad69ea9c4c3116ec13589e061f384ae Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 3 Jul 2017 12:12:16 -0400 Subject: [PATCH 10/79] cleanup new DOM --- lib/dom.js | 86 ++++-------------------------------------------------- 1 file changed, 6 insertions(+), 80 deletions(-) diff --git a/lib/dom.js b/lib/dom.js index d9a6d9b..c708a1b 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -525,7 +525,7 @@ // Returns TRUE if id is NOT found in all docs (optimized). function scanDocuments(docs, id) { var i, len; - len = docs.length + len = docs.length; if (len === 1) { return !docs[0].document.getElementById(id); } @@ -871,7 +871,7 @@ } }; } - doc.oncontextmenu = new Function("return false"); + doc.oncontextmenu = function() { return false; }; }; /** @@ -1263,9 +1263,9 @@ d = document; e = d.documentElement; g = d.getElementsByTagName('body')[0]; - x = w.innerWidth || e.clientWidth || g.clientWidth, + x = w.innerWidth || e.clientWidth || g.clientWidth; y = w.innerHeight|| e.clientHeight|| g.clientHeight; - return !dim ? {x: x, y: y} : dim === 'x' ? x : y; + return !dim ? { x: x, y: y } : dim === 'x' ? x : y; }; // ## Helper methods @@ -1349,8 +1349,8 @@ } // All others. else { - window.onpageshow = window.onpagehide - = window.onfocus = window.onblur = onchangeCb; + window.onpageshow = window.onpagehide = + window.onfocus = window.onblur = onchangeCb; } }; })('undefined' !== typeof document ? document : null); @@ -1372,80 +1372,6 @@ } }; - - - /** - * ### getLabel - * - * Returns a label element for a given element - * - * @param {string} text The text of the label - * @param {HTMLElement|string} forElem The HTML element for which the - * label is, or its id. If the element has no id, a random one is - * created and assigned. - * - * @return {HTMLElement} label The label - * - * @see DOM.addAttributes - */ - function getLabel(text, forElem) { - var label, forId; - if ('string' !== typeof text) { - throw new TypeError('DOM.get: label text must be string. Found: ' + - text); - } - // Create label and text. - label = document.createElement('label'); - label.innerHTML = text; - if (forElem) { - if ('string' === typeof forElem) { - label['for'] = forElem; - } - else if (J.isElement(forElem)) { - // Add unique id for the referenced element, if missing. - if (!forElem.id) forElem.id = this.generateUniqueId(); - forId = forElem.id; - } - else { - throw new TypeError('DOM.get: label forElem must be ' + - 'HTMLElement or string. Found: ' + - forElem); - } - label.setAttribute('for', forId); - } - return label; - } - - /** - * ### DOM.addLabel - * - * Appends a label element for a given element to a root - * - * @param {string} text The text of the label - * @param {HTMLElement|string} forElem The HTML element for which the - * label is, or its id. If the element has no id, a random one is - * created and assigned. - * @param {HTMLElement} root The root element to which the label will - * be appended - * @param {object|string} attributes Optional. Additional attributes - * for the label or its id - * - * @return {HTMLElement} label The label element - * - * @see DOM.addAttributes - * - * @TODO: continue here. what is the best behavior? - */ - //DOM.addLabel = function(text, forElem, root, attributes) { - // var label; - // if ('undefined' === typeof root) { - // root = forElem - // label = this.getLabel(text, forElem, attributes); - // root.insertBefore(label, forElem); - // return l; - //}; - - JSUS.extend(DOM); })('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS); From 215a430d24ffafad1476a9391e126694e66a2553 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 3 Jul 2017 12:27:50 -0400 Subject: [PATCH 11/79] writing CHANGELOG --- CHANGELOG | 4 +++- lib/dom.js | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 778ac80..35a6337 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ # ChangeLog -## Current +## 1.0.0 - `J` is exported as a top level object in the browser's window. +- `#JSUS.get` alias of `JSUS.require` removed. `#JSUS.get` is now a method of the DOM library. +- DOM library rewritten. Several methods removed and code optimized. Methods removed: getElement, addElement, addAttributes2Elem, getButton, addButton, getFieldset, addFieldset, getTextInput, addTextInput, getTextArea, addTextArea, getCanvas, addCanvas, getSlider, addSlider, getRadioButton, addRadioButton, getLabel, addLabel, getSelect, addSelect, getIFrame, addIFrame ... [CONTINUE HERE] ## 0.18.0 - `#OBJ.mixin|mixout|mixcommon` return the mixed object. diff --git a/lib/dom.js b/lib/dom.js index c708a1b..fec79ba 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -34,6 +34,8 @@ el = document.createElement(name); if ('string' === typeof attributes) el.id = attributes; else if (attributes) this.addAttributes(el, attributes); + // For firefox, name of iframe must be set as well. + if (name === 'iframe' && el.id && !el.name) el.name = el.id; return el; }; From 27493550900b786fb26b5ec97e038ee0d099c909 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 3 Jul 2017 17:46:20 -0400 Subject: [PATCH 12/79] CHANGELOG and DOM improvements --- CHANGELOG | 2 +- lib/dom.js | 626 ++++++++++++++++++++++++++++----------------------- lib/obj.js | 2 +- package.json | 2 +- 4 files changed, 348 insertions(+), 284 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 35a6337..0d87a24 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ ## 1.0.0 - `J` is exported as a top level object in the browser's window. - `#JSUS.get` alias of `JSUS.require` removed. `#JSUS.get` is now a method of the DOM library. -- DOM library rewritten. Several methods removed and code optimized. Methods removed: getElement, addElement, addAttributes2Elem, getButton, addButton, getFieldset, addFieldset, getTextInput, addTextInput, getTextArea, addTextArea, getCanvas, addCanvas, getSlider, addSlider, getRadioButton, addRadioButton, getLabel, addLabel, getSelect, addSelect, getIFrame, addIFrame ... [CONTINUE HERE] +- DOM library rewritten. Several methods removed and code optimized. Methods _added_: get, add|append, addAttributes. Methods _modified_: generateUniqueId, addCSS, addJS. Methods _removed_: getElement, addElement, addAttributes2Elem, getButton, addButton, getFieldset, addFieldset, getTextInput, addTextInput, getTextArea, addTextArea, getCanvas, addCanvas, getSlider, addSlider, getRadioButton, addRadioButton, getLabel, addLabel, getSelect, addSelect, getIFrame, addIFrame, addBreak, getDiv, addDiv. ## 0.18.0 - `#OBJ.mixin|mixout|mixcommon` return the mixed object. diff --git a/lib/dom.js b/lib/dom.js index fec79ba..3f8c615 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -31,14 +31,14 @@ */ DOM.get = function(name, attributes) { var el; - el = document.createElement(name); + el = document.createElement(name); if ('string' === typeof attributes) el.id = attributes; else if (attributes) this.addAttributes(el, attributes); // For firefox, name of iframe must be set as well. if (name === 'iframe' && el.id && !el.name) el.name = el.id; return el; }; - + /** * ### DOM.add|append * @@ -49,7 +49,7 @@ * will be appended * @param {object|string} options Optional. Object containing * attributes for the element and rules about how to insert it relative - * to root. Available options: insertAfter, insertBefore (default: + * to root. Available options: insertAfter, insertBefore (default: * child of root). If string, it is the id of the element. Examples: * * ```javascript @@ -72,13 +72,13 @@ */ DOM.add = DOM.append = function(name, root, options) { var el; - el = this.get(name, attributes); - if (attributes) { - if (attributes.insertBefore) { - if (attributes.insertAfter) { - throw new Error('DOM.add: attributes.insertBefore and ' + - 'attributes.insertBefore cannot be ' + - 'both set.'); + el = this.get(name, options); + if (options) { + if (options.insertBefore) { + if (options.insertAfter) { + throw new Error('DOM.add: options.insertBefore and ' + + 'options.insertBefore cannot be ' + + 'both set.'); } if (!root.parentNode) { throw new Error('DOM.add: root.parentNode not found. ' + @@ -86,17 +86,17 @@ } root.parentNode.insertBefore(el, root); } - else if (attributes.insertAfter) { + else if (options.insertAfter) { if (!root.parentNode) { throw new Error('DOM.add: root.parentNode not found. ' + 'Cannot insert before.'); } - DOM.insertAfter(el, root); + DOM.insertAfter(el, root); } } else { root.appendChild(el); - } + } return el; }; @@ -116,7 +116,7 @@ * - 'innerHTML': the innerHTML property of the element (overwrites) * - 'insertBefore': ignored * - 'insertAfter': ignored - * + * * @param {HTMLElement} elem The element to decorate * @param {object} attributes Object containing attributes to * add to the element @@ -128,7 +128,7 @@ */ DOM.addAttributes = function(elem, attributes) { var key; - if (!J.isElement(elem)) { + if (!DOM.isElement(elem)) { throw new TypeError('DOM.addAttributes: elem must be ' + 'HTMLElement. Found: ' + elem); } @@ -156,6 +156,8 @@ return elem; }; + // ## WRITE + /** * ### DOM.write * @@ -346,6 +348,8 @@ return root; }; + // ## ELEMENTS + /** * ### DOM.isNode * @@ -399,7 +403,7 @@ */ DOM.shuffleElements = function(parent, order) { var i, len, idOrder, children, child; - var id, forceId, missId; + var id; if (!JSUS.isNode(parent)) { throw new TypeError('DOM.shuffleElements: parent must be a node. ' + 'Found: ' + parent); @@ -458,24 +462,34 @@ * * Appends a list of options into a HTML select element * - * The second parameter list is an object containing - * a list of key-values pairs as text-value attributes for - * the option. - * * @param {HTMLElement} select HTML select element - * @param {object} list Options to add to the select element + * @param {object} options Optional. List of options to add to + * the select element. List is in the format of key-values pairs + * as innerHTML and value attributes of the option. + * + * @return {HTMLElement} select The updated select element */ - DOM.populateSelect = function(select, list) { + DOM.populateSelect = function(select, options) { var key, opt; - if (!select || !list) return; - for (key in list) { - if (list.hasOwnProperty(key)) { - opt = document.createElement('option'); - opt.value = list[key]; - opt.appendChild(document.createTextNode(key)); - select.appendChild(opt); + if (!DOM.isElement(select)) { + throw new TypeError('DOM.populateSelect: select must be ' + + 'HTMLElement. Found: ' + select); + } + if (options) { + if ('object' !== typeof options) { + throw new TypeError('DOM.populateSelect: options must be ' + + 'object or undefined. Found: ' + options); + } + for (key in options) { + if (options.hasOwnProperty(key)) { + opt = document.createElement('option'); + opt.value = key; + opt.innerHTML = options[key]; + select.appendChild(opt); + } } } + return select; }; /** @@ -483,11 +497,11 @@ * * Removes all children from a node * - * @param {HTMLElement} e HTML element. + * @param {HTMLNode} node HTML node. */ - DOM.removeChildrenFromNode = function(elem) { - while (elem.hasChildNodes()) { - elem.removeChild(elem.firstChild); + DOM.removeChildrenFromNode = function(node) { + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); } }; @@ -506,129 +520,79 @@ return referenceNode.insertBefore(node, referenceNode.nextSibling); }; - /** - * ### DOM.generateUniqueId - * - * Generates a unique id for the whole page, frames included - * - * The resulting id is of the type: prefix_randomdigits. - * - * @param {string} prefix Optional. A given prefix. Default: a random - * string of 8 characters. - * @param {boolean} checkFrames Optional. If TRUE, the id will be unique - * all frames as well. Default: TRUE - * - * @return {string} id The unique id - */ - DOM.generateUniqueId = (function() { - var limit; - limit = 100; - - // Returns TRUE if id is NOT found in all docs (optimized). - function scanDocuments(docs, id) { - var i, len; - len = docs.length; - if (len === 1) { - return !docs[0].document.getElementById(id); - } - if (len === 2) { - return !!(docs[0].document.getElementById(id) && - docs[1].document.getElementById(id)); - } - i = -1; - for ( ; ++i < len ; ) { - if (docs[i].document.getElementById(id)) return false; - } - return true; - } - - return function(prefix, checkFrames) { - var id, windows; - var found, i, len, counter; - - if (prefix) { - if ('string' !== typeof prefix && 'number' !== typeof prefix) { - throw new TypeError('DOM.generateUniqueId: prefix must ' + - 'be string or number. Found: ' + - prefix); - } - } - else { - prefix = JSUS.randomString(8, 'a'); - } - id = prefix + '_'; - - windows = [ window ]; - if ((checkFrames || 'undefined' === typeof checkFrames) && - window.frames) { - - windows = windows.concat(window.frames); - } - - found = true; - counter = -1; - while (found) { - id = prefix + '_' + JSUS.randomInt(1000); - found = scanDocuments(windows, id); - if (++counter > limit) { - throw new Error('DOM.generateUniqueId: could not ' + - 'find unique id within ' + limit + - ' trials.'); - } - } - return id; - }; - })(); - - // ## CSS / JS /** * ### DOM.addCSS * - * If no root element is passed, it tries to add the CSS - * link element to document.head, document.body, and - * finally document. If it fails, returns FALSE. + * Adds a CSS link to the page * + * @param {string} cssPath The path to the css + * @param {HTMLElement} root Optional. The root element. If no root + * element is passed, it tries document.head, document.body, and + * document. If it fails, it throws an error. + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The link element */ - DOM.addCSS = function(root, css, id, attributes) { + DOM.addCSS = function(cssPath, root, attributes) { + if ('string' !== typeof cssPath || cssPath.trim() === '') { + throw new TypeError('DOM.addCSS: cssPath must be a non-empty ' + + 'string. Found: ' + cssPath); + } root = root || document.head || document.body || document; - if (!root) return false; - - attributes = attributes || {}; - - attributes = JSUS.merge(attributes, { + if (!root) { + throw new Error('DOM.addCSS: root is undefined, and could not ' + + 'detect a valid root for css: ' + cssPath); + } + attributes = JSUS.mixin({ rel : 'stylesheet', type: 'text/css', - href: css - }); - - return this.addElement('link', root, id, attributes); + href: cssPath + }, attributes); + return this.add('link', root, attributes); }; /** * ### DOM.addJS * + * Adds a JavaScript script to the page + * + * @param {string} cssPath The path to the css + * @param {HTMLElement} root Optional. The root element. If no root + * element is passed, it tries document.head, document.body, and + * document. If it fails, it throws an error. + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The link element + * */ - DOM.addJS = function(root, js, id, attributes) { + DOM.addJS = function(jsPath, root, attributes) { + if ('string' !== typeof jsPath || jsPath.trim() === '') { + throw new TypeError('DOM.addCSS: jsPath must be a non-empty ' + + 'string. Found: ' + jsPath); + } root = root || document.head || document.body || document; - if (!root) return false; - - attributes = attributes || {}; - - attributes = JSUS.merge(attributes, {charset : 'utf-8', - type: 'text/javascript', - src: js - }); - - return this.addElement('script', root, id, attributes); + if (!root) { + throw new Error('DOM.addCSS: root is undefined, and could not ' + + 'detect a valid root for css: ' + jsPath); + } + attributes = JSUS.mixin({ + charset : 'utf-8', + type: 'text/javascript', + src: jsPath + }, attributes); + return this.add('script', root, attributes); }; + // ## STYLE + /** * ### DOM.highlight * - * Provides a simple way to highlight an HTML element - * by adding a colored border around it. + * Highlights an element by adding a custom border around it * * Three pre-defined modes are implemented: * @@ -640,9 +604,9 @@ * color as HEX value. Examples: * * ```javascript - * highlight(myDiv, 'WARN'); // yellow border + * highlight(myDiv, 'WARN'); // yellow border * highlight(myDiv); // red border - * highlight(myDiv, '#CCC'); // grey border + * highlight(myDiv, '#CCC'); // grey border * ``` * * @param {HTMLElement} elem The element to highlight @@ -655,9 +619,7 @@ */ DOM.highlight = function(elem, code) { var color; - if (!elem) return; - - // default value is ERR + // Default value is ERR. switch (code) { case 'OK': color = 'green'; @@ -669,32 +631,29 @@ color = 'red'; break; default: - if (code.charAt(0) === '#') { - color = code; - } - else { - color = 'red'; - } + if (code.charAt(0) === '#') color = code; + else color = 'red'; } - return this.addBorder(elem, color); }; /** * ### DOM.addBorder * - * Adds a border around the specified element. Color, - * width, and type can be specified. + * Adds a border around the specified element + * + * @param {HTMLElement} elem The element to which adding the borders + * @param {string} color Optional. The color of border. Default: 'red'. + * @param {string} width Optional. The width of border. Default: '5px'. + * @param {string} type Optional. The type of border. Default: 'solid'. * + * @return {HTMLElement} The element to which a border has been added */ DOM.addBorder = function(elem, color, width, type) { var properties; - if (!elem) return; - color = color || 'red'; width = width || '5px'; type = type || 'solid'; - properties = { border: width + ' ' + type + ' ' + color }; return DOM.style(elem, properties); }; @@ -713,34 +672,128 @@ */ DOM.style = function(elem, properties) { var i; - if (!elem || !properties) return; - if (!DOM.isElement(elem)) return; - - for (i in properties) { - if (properties.hasOwnProperty(i)) { - elem.style[i] = properties[i]; + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.style: elem must be HTMLElement. ' + + 'Found: ' + elem); + } + if (properties) { + if ('object' !== typeof properties) { + throw new TypeError('DOM.style: properties must be object or ' + + 'undefined. Found: ' + properties); + } + for (i in properties) { + if (properties.hasOwnProperty(i)) { + elem.style[i] = properties[i]; + } } } return elem; }; + // ## ID + + /** + * ### DOM.generateUniqueId + * + * Generates a unique id for the whole page, frames included + * + * The resulting id is of the type: prefix_randomdigits. + * + * @param {string} prefix Optional. A given prefix. Default: a random + * string of 8 characters. + * @param {boolean} checkFrames Optional. If TRUE, the id will be unique + * all frames as well. Default: TRUE + * + * @return {string} id The unique id + */ + DOM.generateUniqueId = (function() { + var limit; + limit = 100; + + // Returns TRUE if id is NOT found in all docs (optimized). + function scanDocuments(docs, id) { + var i, len; + len = docs.length; + if (len === 1) { + return !docs[0].document.getElementById(id); + } + if (len === 2) { + return !!(docs[0].document.getElementById(id) && + docs[1].document.getElementById(id)); + } + i = -1; + for ( ; ++i < len ; ) { + if (docs[i].document.getElementById(id)) return false; + } + return true; + } + + return function(prefix, checkFrames) { + var id, windows; + var found, counter; + + if (prefix) { + if ('string' !== typeof prefix && 'number' !== typeof prefix) { + throw new TypeError('DOM.generateUniqueId: prefix must ' + + 'be string or number. Found: ' + + prefix); + } + } + else { + prefix = JSUS.randomString(8, 'a'); + } + id = prefix + '_'; + + windows = [ window ]; + if ((checkFrames || 'undefined' === typeof checkFrames) && + window.frames) { + + windows = windows.concat(window.frames); + } + + found = true; + counter = -1; + while (found) { + id = prefix + '_' + JSUS.randomInt(1000); + found = scanDocuments(windows, id); + if (++counter > limit) { + throw new Error('DOM.generateUniqueId: could not ' + + 'find unique id within ' + limit + + ' trials.'); + } + } + return id; + }; + })(); + + // ## CLASSES + /** * ### DOM.removeClass * * Removes a specific class from the classNamex attribute of a given element * * @param {HTMLElement} el An HTML element - * @param {string} c The name of a CSS class already in the element + * @param {string} className The name of a CSS class already in the element * * @return {HTMLElement|undefined} The HTML element with the removed * class, or undefined if the inputs are misspecified */ - DOM.removeClass = function(el, c) { + DOM.removeClass = function(elem, className) { var regexpr, o; - if (!el || !c) return; - regexpr = new RegExp('(?:^|\\s)' + c + '(?!\\S)'); - o = el.className = el.className.replace( regexpr, '' ); - return el; + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.removeClass: elem must be HTMLElement. ' + + 'Found: ' + elem); + } + if (className) { + if ('string' !== typeof className || className.trim() === '') { + throw new TypeError('DOM.removeClass: className must be ' + + 'HTMLElement. Found: ' + className); + } + regexpr = new RegExp('(?:^|\\s)' + className + '(?!\\S)'); + o = elem.className = elem.className.replace(regexpr, '' ); + } + return elem; }; /** @@ -750,19 +803,27 @@ * * Takes care not to overwrite already existing classes. * - * @param {HTMLElement} el An HTML element - * @param {string|array} c The name/s of CSS class/es + * @param {HTMLElement} elem An HTML element + * @param {string|array} className The name/s of CSS class/es * - * @return {HTMLElement|undefined} The HTML element with the additional + * @return {HTMLElement} The HTML element with the additional * class, or undefined if the inputs are misspecified */ - DOM.addClass = function(el, c) { - if (!el) return; - if (c instanceof Array) c = c.join(' '); - else if ('string' !== typeof c) return; - if (!el.className || el.className === '') el.className = c; - else el.className += (' ' + c); - return el; + DOM.addClass = function(elem, className) { + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.addClass: elem must be HTMLElement. ' + + 'Found: ' + elem); + } + if (className) { + if (className instanceof Array) className = className.join(' '); + if ('string' !== typeof className || className.trim() === '') { + throw new TypeError('DOM.addClass: className must be ' + + 'HTMLElement. Found: ' + className); + } + if (!elem.className) elem.className = className; + else elem.className += (' ' + className); + } + return elem; }; /** @@ -837,13 +898,118 @@ DOM.getIFrameAnyChild = function(iframe) { var contentDocument; if (!iframe) return; - contentDocument = W.getIFrameDocument(iframe); + contentDocument = DOM.getIFrameDocument(iframe); return contentDocument.head || contentDocument.body || contentDocument.lastChild || contentDocument.getElementsByTagName('html')[0]; }; - // ## RIGHT-CLICK + // ## EVENTS + + /** + * ### DOM.addEvent + * + * Adds an event listener to an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to handle + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event will initiate a capture. + * Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.removeEvent + * + * Kudos: + * http://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick + */ + DOM.addEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.attachEvent) return element.attachEvent('on' + event, func); + else return element.addEventListener(event, func, capture); + }; + + /** + * ### DOM.removeEvent + * + * Removes an event listener from an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to remove + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event was registered + * as a capture. Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.addEvent + */ + DOM.removeEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.detachEvent) return element.detachEvent('on' + event, func); + else return element.removeEventListener(event, func, capture); + }; + + /** + * ### DOM.onFocusIn + * + * Registers a callback to be executed when the page acquires focus + * + * @param {function|null} cb Callback executed if page acquires focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusIn = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusIn: cb must be function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + + onFocusChange(cb); + }; + + /** + * ### DOM.onFocusOut + * + * Registers a callback to be executed when the page loses focus + * + * @param {function} cb Callback executed if page loses focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusOut = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusOut: cb must be ' + + 'function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + onFocusChange(undefined, cb); + }; + + // ## UI /** * ### DOM.disableRightClick @@ -899,53 +1065,6 @@ doc.oncontextmenu = null; }; - /** - * ### DOM.addEvent - * - * Adds an event listener to an element (cross-browser) - * - * @param {Element} element A target element - * @param {string} event The name of the event to handle - * @param {function} func The event listener - * @param {boolean} Optional. If TRUE, the event will initiate a capture. - * Available only in some browsers. Default, FALSE - * - * @return {boolean} TRUE, on success. However, the return value is - * browser dependent. - * - * @see DOM.removeEvent - * - * Kudos: - * http://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick - */ - DOM.addEvent = function(element, event, func, capture) { - capture = !!capture; - if (element.attachEvent) return element.attachEvent('on' + event, func); - else return element.addEventListener(event, func, capture); - }; - - /** - * ### DOM.removeEvent - * - * Removes an event listener from an element (cross-browser) - * - * @param {Element} element A target element - * @param {string} event The name of the event to remove - * @param {function} func The event listener - * @param {boolean} Optional. If TRUE, the event was registered - * as a capture. Available only in some browsers. Default, FALSE - * - * @return {boolean} TRUE, on success. However, the return value is - * browser dependent. - * - * @see DOM.addEvent - */ - DOM.removeEvent = function(element, event, func, capture) { - capture = !!capture; - if (element.detachEvent) return element.detachEvent('on' + event, func); - else return element.removeEventListener(event, func, capture); - }; - /** * ### DOM.disableBackButton * @@ -967,8 +1086,8 @@ disable = 'undefined' === typeof disable ? true : disable; if (disable && !isDisabled) { if (!history.pushState || !history.go) { - node.warn('DOM.disableBackButton: method not ' + - 'supported by browser.'); + JSUS.log('DOM.disableBackButton: method not ' + + 'supported by browser.'); return null; } history.pushState(null, null, location.href); @@ -984,6 +1103,8 @@ }; })(false); + // ## EXTRA + /** * ### DOM.playSound * @@ -1012,63 +1133,6 @@ audio.play(); }; - /** - * ### DOM.onFocusIn - * - * Registers a callback to be executed when the page acquires focus - * - * @param {function|null} cb Callback executed if page acquires focus, - * or NULL, to delete an existing callback. - * @param {object|function} ctx Optional. Context of execution for cb - * - * @see onFocusChange - */ - DOM.onFocusIn = function(cb, ctx) { - var origCb; - if ('function' !== typeof cb && null !== cb) { - throw new TypeError('JSUS.onFocusIn: cb must be function or null.'); - } - if (ctx) { - if ('object' !== typeof ctx && 'function' !== typeof ctx) { - throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + - 'function or undefined.'); - } - origCb = cb; - cb = function() { origCb.call(ctx); }; - } - - onFocusChange(cb); - }; - - /** - * ### DOM.onFocusOut - * - * Registers a callback to be executed when the page loses focus - * - * @param {function} cb Callback executed if page loses focus, - * or NULL, to delete an existing callback. - * @param {object|function} ctx Optional. Context of execution for cb - * - * @see onFocusChange - */ - DOM.onFocusOut = function(cb, ctx) { - var origCb; - if ('function' !== typeof cb && null !== cb) { - throw new TypeError('JSUS.onFocusOut: cb must be ' + - 'function or null.'); - } - if (ctx) { - if ('object' !== typeof ctx && 'function' !== typeof ctx) { - throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + - 'function or undefined.'); - } - origCb = cb; - cb = function() { origCb.call(ctx); }; - } - onFocusChange(undefined, cb); - }; - - /** * ### DOM.blinkTitle * diff --git a/lib/obj.js b/lib/obj.js index 35607cc..aa09e31 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -1,6 +1,6 @@ /** * # OBJ - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Collection of static functions to manipulate JavaScript objects diff --git a/package.json b/package.json index b1b019d..e3886b7 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "JSUS", "description": "JavaScript UtilS. Extended functional programming support. JSUS helps!", - "version": "0.17.0", + "version": "1.0.0", "keywords": [ "functional", "functional programming", "util", "general", "array", "eval", "time", "date", "object", "nodeGame"], "main": "jsus.js", "author": "Stefano Balietti ", From e47973ca2fad0147d0c4f6508b500d66469a8bac Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 3 Jul 2017 18:55:54 -0400 Subject: [PATCH 13/79] minor --- build/jsus.js | 932 +++++++++++++++++++++++----------------------- build/jsus.min.js | 2 +- lib/dom.js | 36 +- 3 files changed, 493 insertions(+), 477 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index 337ea96..1866bd5 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -993,7 +993,148 @@ function DOM() {} - // ## GENERAL + // ## GET/ADD + + /** + * ### DOM.get + * + * Creates a generic HTML element with specified attributes + * + * @param {string} elem The name of the tag + * @param {object|string} attributes Optional. Object containing + * attributes for the element. If string, the id of the element + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.add + * @see DOM.addAttributes + */ + DOM.get = function(name, attributes) { + var el; + el = document.createElement(name); + if ('string' === typeof attributes) el.id = attributes; + else if (attributes) this.addAttributes(el, attributes); + // For firefox, name of iframe must be set as well. + if (name === 'iframe' && el.id && !el.name) el.name = el.id; + return el; + }; + + /** + * ### DOM.add|append + * + * Creates and append an element with specified attributes to a root + * + * @param {string} name The name of the HTML tag + * @param {HTMLElement} root The root element to which the new element + * will be appended + * @param {object|string} options Optional. Object containing + * attributes for the element and rules about how to insert it relative + * to root. Available options: insertAfter, insertBefore (default: + * child of root). If string, it is the id of the element. Examples: + * + * ```javascript + * // Appends a new new to the body. + * var div = DOM.add('div', document.body); + * // Appends a new new to the body with id 'myid'. + * var div1 = DOM.add('div', document.body, 'myid'); + * // Appends a new new to the body with id 'myid2' and class name 'c'. + * var div2 = DOM.add('div', document.body, { id: 'myid2', className: 'c'}); + * // Appends a new div after div1 with id 'myid'. + * var div3 = DOM.add('div', div1, { id: 'myid3', insertAfter: true }); + * // Appends a new div before div2 with id 'myid'. + * var div3 = DOM.add('div', div2, { id: 'myid3', insertBefore: true }); + * ``` + * + * @return {HTMLElement} The newly created HTML element + * + * @see DOM.get + * @see DOM.addAttributes + */ + DOM.add = DOM.append = function(name, root, options) { + var el; + el = this.get(name, options); + if (options && options.insertBefore) { + if (options.insertAfter) { + throw new Error('DOM.add: options.insertBefore and ' + + 'options.insertBefore cannot be ' + + 'both set.'); + } + if (!root.parentNode) { + throw new Error('DOM.add: root.parentNode not found. ' + + 'Cannot insert before.'); + } + root.parentNode.insertBefore(el, root); + } + else if (options && options.insertAfter) { + if (!root.parentNode) { + throw new Error('DOM.add: root.parentNode not found. ' + + 'Cannot insert after.'); + } + DOM.insertAfter(el, root); + } + else { + root.appendChild(el); + } + return el; + }; + + /** + * ### DOM.addAttributes + * + * Adds attributes to an HTML element and returns it + * + * Attributes are defined as key-values pairs and added + * + * Special cases: + * + * - 'className': alias for class + * - 'class': add a class to the className property (does not overwrite) + * - 'style': adds property to the style property (see DOM.style) + * - 'id': the id of the element + * - 'innerHTML': the innerHTML property of the element (overwrites) + * - 'insertBefore': ignored + * - 'insertAfter': ignored + * + * @param {HTMLElement} elem The element to decorate + * @param {object} attributes Object containing attributes to + * add to the element + * + * @return {HTMLElement} The element with speficied attributes added + * + * @see DOM.addClass + * @see DOM.style + */ + DOM.addAttributes = function(elem, attributes) { + var key; + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.addAttributes: elem must be ' + + 'HTMLElement. Found: ' + elem); + } + if ('undefined' === typeof attributes) return elem; + if ('object' !== typeof attributes) { + throw new TypeError('DOM.addAttributes: attributes must be ' + + 'object or undefined. Found: ' + attributes); + } + for (key in attributes) { + if (attributes.hasOwnProperty(key)) { + if (key === 'id' || key === 'innerHTML') { + elem[key] = attributes[key]; + } + else if (key === 'class' || key === 'className') { + DOM.addClass(elem, attributes[key]); + } + else if (key === 'style') { + DOM.style(elem, attributes[key]); + } + else if (key !== 'insertBefore' && key !== 'insertAfter') { + elem.setAttribute(key, attributes[key]); + } + } + } + return elem; + }; + + // ## WRITE /** * ### DOM.write @@ -1165,7 +1306,7 @@ span = document.createElement('em'); } else { - span = JSUS.getElement('span', null, args[key]); + span = DOM.get('span', args[key]); } text = string.substring(idx_replace, idx_finish); @@ -1185,6 +1326,8 @@ return root; }; + // ## ELEMENTS + /** * ### DOM.isNode * @@ -1238,7 +1381,7 @@ */ DOM.shuffleElements = function(parent, order) { var i, len, idOrder, children, child; - var id, forceId, missId; + var id; if (!JSUS.isNode(parent)) { throw new TypeError('DOM.shuffleElements: parent must be a node. ' + 'Found: ' + parent); @@ -1292,92 +1435,39 @@ return idOrder; }; - /** - * ### DOM.addAttributes2Elem - * - * Adds attributes to an HTML element and returns it - * - * Attributes are defined as key-values pairs. - * Attributes 'label' is ignored, attribute 'className' ('class') and - * 'style' are special and are delegated to special methods. - * - * @param {HTMLElement} e The element to decorate - * @param {object} a Object containing attributes to add to the element - * - * @return {HTMLElement} The decorated element - * - * @see DOM.addLabel - * @see DOM.addClass - * @see DOM.style - */ - DOM.addAttributes2Elem = function(e, a) { - var key; - if (!e || !a) return e; - if ('object' != typeof a) return e; - for (key in a) { - if (a.hasOwnProperty(key)) { - if (key === 'id') { - e.id = a[key]; - } - else if (key === 'class' || key === 'className') { - DOM.addClass(e, a[key]); - } - else if (key === 'style') { - DOM.style(e, a[key]); - } - else if (key === 'label') { - // Handle the case. - JSUS.log('DOM.addAttributes2Elem: label attribute is not ' + - 'supported. Use DOM.addLabel instead.'); - } - else { - e.setAttribute(key, a[key]); - } - - - // TODO: handle special cases - // - } - } - return e; - }; - /** * ### DOM.populateSelect * * Appends a list of options into a HTML select element * - * The second parameter list is an object containing - * a list of key-values pairs as text-value attributes for - * the option. - * * @param {HTMLElement} select HTML select element - * @param {object} list Options to add to the select element + * @param {object} options Optional. List of options to add to + * the select element. List is in the format of key-values pairs + * as innerHTML and value attributes of the option. + * + * @return {HTMLElement} select The updated select element */ - DOM.populateSelect = function(select, list) { + DOM.populateSelect = function(select, options) { var key, opt; - if (!select || !list) return; - for (key in list) { - if (list.hasOwnProperty(key)) { - opt = document.createElement('option'); - opt.value = list[key]; - opt.appendChild(document.createTextNode(key)); - select.appendChild(opt); + if (!DOM.isElement(select)) { + throw new TypeError('DOM.populateSelect: select must be ' + + 'HTMLElement. Found: ' + select); + } + if (options) { + if ('object' !== typeof options) { + throw new TypeError('DOM.populateSelect: options must be ' + + 'object or undefined. Found: ' + options); + } + for (key in options) { + if (options.hasOwnProperty(key)) { + opt = document.createElement('option'); + opt.value = key; + opt.innerHTML = options[key]; + select.appendChild(opt); + } } } + return select; }; /** @@ -1385,11 +1475,11 @@ * * Removes all children from a node * - * @param {HTMLElement} e HTML element. + * @param {HTMLNode} node HTML node. */ - DOM.removeChildrenFromNode = function(elem) { - while (elem.hasChildNodes()) { - elem.removeChild(elem.firstChild); + DOM.removeChildrenFromNode = function(node) { + while (node.hasChildNodes()) { + node.removeChild(node.firstChild); } }; @@ -1408,251 +1498,79 @@ return referenceNode.insertBefore(node, referenceNode.nextSibling); }; - /** - * ### DOM.generateUniqueId - * - * Generates a unique id for the whole page, frames included - * - * The resulting id is of the type: prefix_randomdigits. - * - * @param {string} prefix Optional. A given prefix. Default: a random - * string of 8 characters. - * @param {boolean} checkFrames Optional. If TRUE, the id will be unique - * all frames as well. Default: TRUE - * - * @return {string} id The unique id - */ - DOM.generateUniqueId = (function() { - var limit; - limit = 100; - - // Returns TRUE if id is NOT found in all docs (optimized). - function scanDocuments(docs, id) { - var i, len; - len = docs.length - if (len === 1) { - return !docs[0].document.getElementById(id); - } - if (len === 2) { - return !!(docs[0].document.getElementById(id) && - docs[1].document.getElementById(id)); - } - i = -1; - for ( ; ++i < len ; ) { - if (docs[i].document.getElementById(id)) return false; - } - return true; - } - - return function(prefix, checkFrames) { - var id, windows; - var found, i, len, counter; - - if (prefix) { - if ('string' !== typeof prefix && 'number' !== typeof prefix) { - throw new TypeError('DOM.generateUniqueId: prefix must ' + - 'be string or number. Found: ' + - prefix); - } - } - else { - prefix = JSUS.randomString(8, 'a'); - } - id = prefix + '_'; - - windows = [ window ]; - if ((checkFrames || 'undefined' === typeof checkFrames) && - window.frames) { - - windows = windows.concat(window.frames); - } + // ## CSS / JS - found = true; - counter = -1; - while (found) { - id = prefix + '_' + JSUS.randomInt(1000); - found = scanDocuments(windows, id); - if (++counter > limit) { - throw new Error('DOM.generateUniqueId: could not ' + - 'find unique id within ' + limit + - ' trials.'); - } - } - return id; - }; - })(); - /** - * ### DOM.get + * ### DOM.addCSS * - * Creates a generic HTML element with specified attributes + * Adds a CSS link to the page * - * @param {string} elem The name of the tag + * @param {string} cssPath The path to the css + * @param {HTMLElement} root Optional. The root element. If no root + * element is passed, it tries document.head, document.body, and + * document. If it fails, it throws an error. * @param {object|string} attributes Optional. Object containing * attributes for the element. If string, the id of the element * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.add - * @see DOM.addAttributes2Elem + * @return {HTMLElement} The link element */ - DOM.get = function(name, attributes) { - var el; - el = document.createElement(name); - if ('string' === typeof attributes) el.id = attributes; - else if (attributes) this.addAttributes2Elem(el, attributes); - return el; + DOM.addCSS = function(cssPath, root, attributes) { + if ('string' !== typeof cssPath || cssPath.trim() === '') { + throw new TypeError('DOM.addCSS: cssPath must be a non-empty ' + + 'string. Found: ' + cssPath); + } + root = root || document.head || document.body || document; + if (!root) { + throw new Error('DOM.addCSS: root is undefined, and could not ' + + 'detect a valid root for css: ' + cssPath); + } + attributes = JSUS.mixin({ + rel : 'stylesheet', + type: 'text/css', + href: cssPath + }, attributes); + return this.add('link', root, attributes); }; /** - * ### DOM.add|append + * ### DOM.addJS * - * Creates and append an element with specified attributes to a root + * Adds a JavaScript script to the page * - * @param {string} name The name of the HTML tag - * @param {HTMLElement} root The root element to which the new element - * will be appended + * @param {string} cssPath The path to the css + * @param {HTMLElement} root Optional. The root element. If no root + * element is passed, it tries document.head, document.body, and + * document. If it fails, it throws an error. * @param {object|string} attributes Optional. Object containing * attributes for the element. If string, the id of the element * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.get - */ - DOM.add = DOM.append = function(name, root, attributes) { - var el; - el = this.get(name, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.getElement - * - * Creates a generic HTML element with id and attributes as specified - * - * @param {string} elem The name of the tag - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.getElement = function(elem, id, attributes) { - var e; - console.log('***DOM.getElement is deprecated. Use DOM.get instead.***'); - e = document.createElement(elem); - if ('undefined' !== typeof id) e.id = id; - return this.addAttributes2Elem(e, attributes); - }; - - /** - * ### DOM.addElement - * - * Creates and appends a generic HTML element with specified attributes - * - * @param {string} elem The name of the tag - * @param {HTMLElement} root The root element to which the new element will - * be appended - * @param {string} id Optional. The id of the tag - * @param {object} attributes Optional. Object containing attributes for - * the newly created element - * - * @return {HTMLElement} The newly created HTML element - * - * @see DOM.getElement - * @see DOM.addAttributes2Elem - * - * @deprecated - */ - DOM.addElement = function(elem, root, id, attributes) { - var el; - console.log('***DOM.addElement is deprecated. Use DOM.add instead.***'); - el = this.getElement(elem, id, attributes); - return root.appendChild(el); - }; - - /** - * ### DOM.getLabel + * @return {HTMLElement} The link element * */ - DOM.getLabel = function(forElem, id, labelText, attributes) { - if (!forElem) return false; - var label = document.createElement('label'); - label.id = id; - label.appendChild(document.createTextNode(labelText)); - - if ('undefined' === typeof forElem.id) { - forElem.id = this.generateUniqueId(); + DOM.addJS = function(jsPath, root, attributes) { + if ('string' !== typeof jsPath || jsPath.trim() === '') { + throw new TypeError('DOM.addCSS: jsPath must be a non-empty ' + + 'string. Found: ' + jsPath); } - - label.setAttribute('for', forElem.id); - this.addAttributes2Elem(label, attributes); - return label; - }; - - /** - * ### DOM.addLabel - * - */ - DOM.addLabel = function(root, forElem, id, labelText, attributes) { - if (!root || !forElem || !labelText) return false; - var l = this.getLabel(forElem, id, labelText, attributes); - root.insertBefore(l, forElem); - return l; - }; - - // ## CSS / JS - - /** - * ### DOM.addCSS - * - * If no root element is passed, it tries to add the CSS - * link element to document.head, document.body, and - * finally document. If it fails, returns FALSE. - * - */ - DOM.addCSS = function(root, css, id, attributes) { root = root || document.head || document.body || document; - if (!root) return false; - - attributes = attributes || {}; - - attributes = JSUS.merge(attributes, { - rel : 'stylesheet', - type: 'text/css', - href: css - }); - - return this.addElement('link', root, id, attributes); + if (!root) { + throw new Error('DOM.addCSS: root is undefined, and could not ' + + 'detect a valid root for css: ' + jsPath); + } + attributes = JSUS.mixin({ + charset : 'utf-8', + type: 'text/javascript', + src: jsPath + }, attributes); + return this.add('script', root, attributes); }; - /** - * ### DOM.addJS - * - */ - DOM.addJS = function(root, js, id, attributes) { - root = root || document.head || document.body || document; - if (!root) return false; - - attributes = attributes || {}; - - attributes = JSUS.merge(attributes, {charset : 'utf-8', - type: 'text/javascript', - src: js - }); - - return this.addElement('script', root, id, attributes); - }; + // ## STYLE /** * ### DOM.highlight * - * Provides a simple way to highlight an HTML element - * by adding a colored border around it. + * Highlights an element by adding a custom border around it * * Three pre-defined modes are implemented: * @@ -1664,9 +1582,9 @@ * color as HEX value. Examples: * * ```javascript - * highlight(myDiv, 'WARN'); // yellow border + * highlight(myDiv, 'WARN'); // yellow border * highlight(myDiv); // red border - * highlight(myDiv, '#CCC'); // grey border + * highlight(myDiv, '#CCC'); // grey border * ``` * * @param {HTMLElement} elem The element to highlight @@ -1679,9 +1597,7 @@ */ DOM.highlight = function(elem, code) { var color; - if (!elem) return; - - // default value is ERR + // Default value is ERR. switch (code) { case 'OK': color = 'green'; @@ -1693,32 +1609,29 @@ color = 'red'; break; default: - if (code.charAt(0) === '#') { - color = code; - } - else { - color = 'red'; - } + if (code.charAt(0) === '#') color = code; + else color = 'red'; } - return this.addBorder(elem, color); }; /** * ### DOM.addBorder * - * Adds a border around the specified element. Color, - * width, and type can be specified. + * Adds a border around the specified element + * + * @param {HTMLElement} elem The element to which adding the borders + * @param {string} color Optional. The color of border. Default: 'red'. + * @param {string} width Optional. The width of border. Default: '5px'. + * @param {string} type Optional. The type of border. Default: 'solid'. * + * @return {HTMLElement} The element to which a border has been added */ DOM.addBorder = function(elem, color, width, type) { var properties; - if (!elem) return; - color = color || 'red'; width = width || '5px'; type = type || 'solid'; - properties = { border: width + ' ' + type + ' ' + color }; return DOM.style(elem, properties); }; @@ -1737,16 +1650,101 @@ */ DOM.style = function(elem, properties) { var i; - if (!elem || !properties) return; - if (!DOM.isElement(elem)) return; + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.style: elem must be HTMLElement. ' + + 'Found: ' + elem); + } + if (properties) { + if ('object' !== typeof properties) { + throw new TypeError('DOM.style: properties must be object or ' + + 'undefined. Found: ' + properties); + } + for (i in properties) { + if (properties.hasOwnProperty(i)) { + elem.style[i] = properties[i]; + } + } + } + return elem; + }; - for (i in properties) { - if (properties.hasOwnProperty(i)) { - elem.style[i] = properties[i]; + // ## ID + + /** + * ### DOM.generateUniqueId + * + * Generates a unique id for the whole page, frames included + * + * The resulting id is of the type: prefix_randomdigits. + * + * @param {string} prefix Optional. A given prefix. Default: a random + * string of 8 characters. + * @param {boolean} checkFrames Optional. If TRUE, the id will be unique + * all frames as well. Default: TRUE + * + * @return {string} id The unique id + */ + DOM.generateUniqueId = (function() { + var limit; + limit = 100; + + // Returns TRUE if id is NOT found in all docs (optimized). + function scanDocuments(docs, id) { + var i, len; + len = docs.length; + if (len === 1) { + return !docs[0].document.getElementById(id); + } + if (len === 2) { + return !!(docs[0].document.getElementById(id) && + docs[1].document.getElementById(id)); + } + i = -1; + for ( ; ++i < len ; ) { + if (docs[i].document.getElementById(id)) return false; + } + return true; + } + + return function(prefix, checkFrames) { + var id, windows; + var found, counter; + + if (prefix) { + if ('string' !== typeof prefix && 'number' !== typeof prefix) { + throw new TypeError('DOM.generateUniqueId: prefix must ' + + 'be string or number. Found: ' + + prefix); + } + } + else { + prefix = JSUS.randomString(8, 'a'); + } + id = prefix + '_'; + + windows = [ window ]; + if ((checkFrames || 'undefined' === typeof checkFrames) && + window.frames) { + + windows = windows.concat(window.frames); + } + + found = true; + counter = -1; + while (found) { + id = prefix + '_' + JSUS.randomInt(1000); + found = scanDocuments(windows, id); + if (++counter > limit) { + throw new Error('DOM.generateUniqueId: could not ' + + 'find unique id within ' + limit + + ' trials.'); + } } - } - return elem; - }; + return id; + }; + })(); + + // ## CLASSES /** * ### DOM.removeClass @@ -1754,17 +1752,26 @@ * Removes a specific class from the classNamex attribute of a given element * * @param {HTMLElement} el An HTML element - * @param {string} c The name of a CSS class already in the element + * @param {string} className The name of a CSS class already in the element * * @return {HTMLElement|undefined} The HTML element with the removed * class, or undefined if the inputs are misspecified */ - DOM.removeClass = function(el, c) { + DOM.removeClass = function(elem, className) { var regexpr, o; - if (!el || !c) return; - regexpr = new RegExp('(?:^|\\s)' + c + '(?!\\S)'); - o = el.className = el.className.replace( regexpr, '' ); - return el; + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.removeClass: elem must be HTMLElement. ' + + 'Found: ' + elem); + } + if (className) { + if ('string' !== typeof className || className.trim() === '') { + throw new TypeError('DOM.removeClass: className must be ' + + 'HTMLElement. Found: ' + className); + } + regexpr = new RegExp('(?:^|\\s)' + className + '(?!\\S)'); + o = elem.className = elem.className.replace(regexpr, '' ); + } + return elem; }; /** @@ -1774,19 +1781,27 @@ * * Takes care not to overwrite already existing classes. * - * @param {HTMLElement} el An HTML element - * @param {string|array} c The name/s of CSS class/es + * @param {HTMLElement} elem An HTML element + * @param {string|array} className The name/s of CSS class/es * - * @return {HTMLElement|undefined} The HTML element with the additional + * @return {HTMLElement} The HTML element with the additional * class, or undefined if the inputs are misspecified */ - DOM.addClass = function(el, c) { - if (!el) return; - if (c instanceof Array) c = c.join(' '); - else if ('string' !== typeof c) return; - if (!el.className || el.className === '') el.className = c; - else el.className += (' ' + c); - return el; + DOM.addClass = function(elem, className) { + if (!DOM.isElement(elem)) { + throw new TypeError('DOM.addClass: elem must be HTMLElement. ' + + 'Found: ' + elem); + } + if (className) { + if (className instanceof Array) className = className.join(' '); + if ('string' !== typeof className || className.trim() === '') { + throw new TypeError('DOM.addClass: className must be ' + + 'HTMLElement. Found: ' + className); + } + if (!elem.className) elem.className = className; + else elem.className += (' ' + className); + } + return elem; }; /** @@ -1861,13 +1876,118 @@ DOM.getIFrameAnyChild = function(iframe) { var contentDocument; if (!iframe) return; - contentDocument = W.getIFrameDocument(iframe); + contentDocument = DOM.getIFrameDocument(iframe); return contentDocument.head || contentDocument.body || contentDocument.lastChild || contentDocument.getElementsByTagName('html')[0]; }; - // ## RIGHT-CLICK + // ## EVENTS + + /** + * ### DOM.addEvent + * + * Adds an event listener to an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to handle + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event will initiate a capture. + * Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.removeEvent + * + * Kudos: + * http://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick + */ + DOM.addEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.attachEvent) return element.attachEvent('on' + event, func); + else return element.addEventListener(event, func, capture); + }; + + /** + * ### DOM.removeEvent + * + * Removes an event listener from an element (cross-browser) + * + * @param {Element} element A target element + * @param {string} event The name of the event to remove + * @param {function} func The event listener + * @param {boolean} Optional. If TRUE, the event was registered + * as a capture. Available only in some browsers. Default, FALSE + * + * @return {boolean} TRUE, on success. However, the return value is + * browser dependent. + * + * @see DOM.addEvent + */ + DOM.removeEvent = function(element, event, func, capture) { + capture = !!capture; + if (element.detachEvent) return element.detachEvent('on' + event, func); + else return element.removeEventListener(event, func, capture); + }; + + /** + * ### DOM.onFocusIn + * + * Registers a callback to be executed when the page acquires focus + * + * @param {function|null} cb Callback executed if page acquires focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusIn = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusIn: cb must be function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + + onFocusChange(cb); + }; + + /** + * ### DOM.onFocusOut + * + * Registers a callback to be executed when the page loses focus + * + * @param {function} cb Callback executed if page loses focus, + * or NULL, to delete an existing callback. + * @param {object|function} ctx Optional. Context of execution for cb + * + * @see onFocusChange + */ + DOM.onFocusOut = function(cb, ctx) { + var origCb; + if ('function' !== typeof cb && null !== cb) { + throw new TypeError('JSUS.onFocusOut: cb must be ' + + 'function or null.'); + } + if (ctx) { + if ('object' !== typeof ctx && 'function' !== typeof ctx) { + throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + + 'function or undefined.'); + } + origCb = cb; + cb = function() { origCb.call(ctx); }; + } + onFocusChange(undefined, cb); + }; + + // ## UI /** * ### DOM.disableRightClick @@ -1897,7 +2017,7 @@ } }; } - doc.oncontextmenu = new Function("return false"); + doc.oncontextmenu = function() { return false; }; }; /** @@ -1923,53 +2043,6 @@ doc.oncontextmenu = null; }; - /** - * ### DOM.addEvent - * - * Adds an event listener to an element (cross-browser) - * - * @param {Element} element A target element - * @param {string} event The name of the event to handle - * @param {function} func The event listener - * @param {boolean} Optional. If TRUE, the event will initiate a capture. - * Available only in some browsers. Default, FALSE - * - * @return {boolean} TRUE, on success. However, the return value is - * browser dependent. - * - * @see DOM.removeEvent - * - * Kudos: - * http://stackoverflow.com/questions/6348494/addeventlistener-vs-onclick - */ - DOM.addEvent = function(element, event, func, capture) { - capture = !!capture; - if (element.attachEvent) return element.attachEvent('on' + event, func); - else return element.addEventListener(event, func, capture); - }; - - /** - * ### DOM.removeEvent - * - * Removes an event listener from an element (cross-browser) - * - * @param {Element} element A target element - * @param {string} event The name of the event to remove - * @param {function} func The event listener - * @param {boolean} Optional. If TRUE, the event was registered - * as a capture. Available only in some browsers. Default, FALSE - * - * @return {boolean} TRUE, on success. However, the return value is - * browser dependent. - * - * @see DOM.addEvent - */ - DOM.removeEvent = function(element, event, func, capture) { - capture = !!capture; - if (element.detachEvent) return element.detachEvent('on' + event, func); - else return element.removeEventListener(event, func, capture); - }; - /** * ### DOM.disableBackButton * @@ -1991,8 +2064,8 @@ disable = 'undefined' === typeof disable ? true : disable; if (disable && !isDisabled) { if (!history.pushState || !history.go) { - node.warn('DOM.disableBackButton: method not ' + - 'supported by browser.'); + JSUS.log('DOM.disableBackButton: method not ' + + 'supported by browser.'); return null; } history.pushState(null, null, location.href); @@ -2008,6 +2081,8 @@ }; })(false); + // ## EXTRA + /** * ### DOM.playSound * @@ -2036,63 +2111,6 @@ audio.play(); }; - /** - * ### DOM.onFocusIn - * - * Registers a callback to be executed when the page acquires focus - * - * @param {function|null} cb Callback executed if page acquires focus, - * or NULL, to delete an existing callback. - * @param {object|function} ctx Optional. Context of execution for cb - * - * @see onFocusChange - */ - DOM.onFocusIn = function(cb, ctx) { - var origCb; - if ('function' !== typeof cb && null !== cb) { - throw new TypeError('JSUS.onFocusIn: cb must be function or null.'); - } - if (ctx) { - if ('object' !== typeof ctx && 'function' !== typeof ctx) { - throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + - 'function or undefined.'); - } - origCb = cb; - cb = function() { origCb.call(ctx); }; - } - - onFocusChange(cb); - }; - - /** - * ### DOM.onFocusOut - * - * Registers a callback to be executed when the page loses focus - * - * @param {function} cb Callback executed if page loses focus, - * or NULL, to delete an existing callback. - * @param {object|function} ctx Optional. Context of execution for cb - * - * @see onFocusChange - */ - DOM.onFocusOut = function(cb, ctx) { - var origCb; - if ('function' !== typeof cb && null !== cb) { - throw new TypeError('JSUS.onFocusOut: cb must be ' + - 'function or null.'); - } - if (ctx) { - if ('object' !== typeof ctx && 'function' !== typeof ctx) { - throw new TypeError('JSUS.onFocusIn: ctx must be object, ' + - 'function or undefined.'); - } - origCb = cb; - cb = function() { origCb.call(ctx); }; - } - onFocusChange(undefined, cb); - }; - - /** * ### DOM.blinkTitle * @@ -2289,9 +2307,9 @@ d = document; e = d.documentElement; g = d.getElementsByTagName('body')[0]; - x = w.innerWidth || e.clientWidth || g.clientWidth, + x = w.innerWidth || e.clientWidth || g.clientWidth; y = w.innerHeight|| e.clientHeight|| g.clientHeight; - return !dim ? {x: x, y: y} : dim === 'x' ? x : y; + return !dim ? { x: x, y: y } : dim === 'x' ? x : y; }; // ## Helper methods @@ -2375,8 +2393,8 @@ } // All others. else { - window.onpageshow = window.onpagehide - = window.onfocus = window.onblur = onchangeCb; + window.onpageshow = window.onpagehide = + window.onfocus = window.onblur = onchangeCb; } }; })('undefined' !== typeof document ? document : null); @@ -4316,7 +4334,7 @@ /** * # OBJ - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Collection of static functions to manipulate JavaScript objects diff --git a/build/jsus.min.js b/build/jsus.min.js index 298dadd..da727d2 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e){return"undefined"==typeof t.clone?(t.log("JSUS.clone not found. Cannot continue."),!1):"undefined"==typeof e?t.clone(t._classes):"undefined"==typeof t._classes[e]?(t.log("Could not find class "+e),!1):t.clone(t._classes[e])},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.get=function(e,t){var n;return n=document.createElement(e),"string"==typeof t?n.id=t:t&&this.addAttributes2Elem(n,t),n},r.add=r.append=function(e,t,n){var r;return r=this.get(e,n),t.appendChild(r)},r.getElement=function(e,t,n){var r;return console.log("***DOM.getElement is deprecated. Use DOM.get instead.***"),r=document.createElement(e),"undefined"!=typeof t&&(r.id=t),this.addAttributes2Elem(r,n)},r.addElement=function(e,t,n,r){var i;return console.log("***DOM.addElement is deprecated. Use DOM.add instead.***"),i=this.getElement(e,n,r),t.appendChild(i)},r.getLabel=function(e,t,n,r){if(!e)return!1;var i=document.createElement("label");return i.id=t,i.appendChild(document.createTextNode(n)),"undefined"==typeof e.id&&(e.id=this.generateUniqueId()),i.setAttribute("for",e.id),this.addAttributes2Elem(i,r),i},r.addLabel=function(e,t,n,r,i){if(!e||!t||!r)return!1;var s=this.getLabel(t,n,r,i);return e.insertBefore(s,t),s},r.addCSS=function(t,n,r,i){return t=t||document.head||document.body||document,t?(i=i||{},i=e.merge(i,{rel:"stylesheet",type:"text/css",href:n}),this.addElement("link",t,r,i)):!1},r.addJS=function(t,n,r,i){return t=t||document.head||document.body||document,t?(i=i||{},i=e.merge(i,{charset:"utf-8",type:"text/javascript",src:n}),this.addElement("script",t,r,i)):!1},r.highlight=function(e,t){var n;if(!e)return;switch(t){case"OK":n="green";break;case"WARN":n="yellow";break;case"ERR":n="red";break;default:t.charAt(0)==="#"?n=t:n="red"}return this.addBorder(e,n)},r.addBorder=function(e,t,n,i){var s;if(!e)return;return t=t||"red",n=n||"5px",i=i||"solid",s={border:n+" "+i+" "+t},r.style(e,s)},r.style=function(e,t){var n;if(!e||!t)return;if(!r.isElement(e))return;for(n in t)t.hasOwnProperty(n)&&(e.style[n]=t[n]);return e},r.removeClass=function(e,t){var n,r;if(!e||!t)return;return n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),r=e.className=e.className.replace(n,""),e},r.addClass=function(e,t){if(!e)return;if(t instanceof Array)t=t.join(" ");else if("string"!=typeof t)return;return!e.className||e.className===""?e.className=t:e.className+=" "+t,e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o Date: Tue, 4 Jul 2017 13:10:55 -0400 Subject: [PATCH 14/79] minor --- lib/parse.js | 28 ++++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 3e0f2c6..309e972 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -1,6 +1,6 @@ /** * # PARSE - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Collection of static functions related to parsing strings @@ -50,7 +50,7 @@ var regex, results; if (referer && 'string' !== typeof referer) { throw new TypeError('JSUS.getQueryString: referer must be string ' + - 'or undefined.'); + 'or undefined. Found: ' + referer); } referer = referer || window.location.search; if ('undefined' === typeof name) return referer; @@ -418,7 +418,7 @@ if ('number' === typeof expr) expr = '' + expr; else if ('string' !== typeof expr) { throw new TypeError('PARSE.range: expr must be string, number, ' + - 'undefined.'); + 'undefined. Found: ' + expr); } // If no available numbers defined, assumes all possible are allowed. if ('undefined' === typeof available) { @@ -432,19 +432,20 @@ else if ('object' === typeof available) { if ('function' !== typeof available.next) { throw new TypeError('PARSE.range: available.next must be ' + - 'function.'); + 'function. Found: ' + available.next); } if ('function' !== typeof available.isFinished) { throw new TypeError('PARSE.range: available.isFinished must ' + - 'be function.'); + 'be function. Found: ' + + available.isFinished); } if ('number' !== typeof available.begin) { throw new TypeError('PARSE.range: available.begin must be ' + - 'number.'); + 'number. Found: ' + available.begin); } if ('number' !== typeof available.end) { throw new TypeError('PARSE.range: available.end must be ' + - 'number.'); + 'number. Found: ' + available.end); } begin = available.begin; @@ -457,8 +458,8 @@ numbers = available.match(/([-+]?\d+)/g); if (numbers === null) { - throw new Error( - 'PARSE.range: no numbers in available: ' + available); + throw new Error('PARSE.range: no numbers in available: ' + + available); } lowerBound = Math.min.apply(null, numbers); @@ -478,7 +479,8 @@ } else { throw new TypeError('PARSE.range: available must be string, ' + - 'array, object or undefined.'); + 'array, object or undefined. Found: ' + + available); } // end -> maximal available value. @@ -642,7 +644,8 @@ if ('undefined' !== typeof Function.prototype.name) { PARSE.funcName = function(func) { if ('function' !== typeof func) { - throw new TypeError('PARSE.funcName: func must be function.'); + throw new TypeError('PARSE.funcName: func must be function. ' + + 'Found: ' + func); } return func.name; }; @@ -651,7 +654,8 @@ PARSE.funcName = function(func) { var funcNameRegex, res; if ('function' !== typeof func) { - throw new TypeError('PARSE.funcName: func must be function.'); + throw new TypeError('PARSE.funcName: func must be function. ' + + 'Found: ' + func); } funcNameRegex = /function\s([^(]{1,})\(/; res = (funcNameRegex).exec(func.toString()); From d988d258968f456db4d7925e777cbd320b3bae7b Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 10 Jul 2017 19:04:59 -0400 Subject: [PATCH 15/79] minor on array --- lib/array.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/lib/array.js b/lib/array.js index b615566..029d1a2 100644 --- a/lib/array.js +++ b/lib/array.js @@ -1,6 +1,6 @@ /** * # ARRAY - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Collection of static functions to manipulate arrays @@ -132,20 +132,27 @@ * If an error occurs returns FALSE. * * @param {array} array The array to loop in - * @param {Function} func The callback for each element in the array + * @param {Function} cb The callback for each element in the array * @param {object} context Optional. The context of execution of the * callback. Defaults ARRAY.each * * @return {boolean} TRUE, if execution was successful */ - ARRAY.each = function(array, func, context) { - if ('object' !== typeof array) return false; - if (!func) return false; + ARRAY.each = function(array, cb, context) { + var i, len; + if ('object' !== typeof array) { + throw new TypeError('ARRAY.each: array must be object. Found: ' + + array); + } + if ('function' !== typeof cb) { + throw new TypeError('ARRAY.each: cb must be function. Found: ' + + cb); + } context = context || this; - var i, len = array.length; + len = array.length; for (i = 0 ; i < len; i++) { - func.call(context, array[i]); + cb.call(context, array[i]); } return true; }; @@ -181,8 +188,8 @@ } len = arguments.length; - if (len === 3) args = [null, arguments[2]]; - else if (len === 4) args = [null, arguments[2], arguments[3]]; + if (len === 3) args = [ null, arguments[2] ]; + else if (len === 4) args = [ null, arguments[2], arguments[3] ]; else { len = len - 1; args = new Array(len); From 9c0e4bd1a8bf80f66be9d61075c4dd7df5f181a8 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 4 Aug 2017 10:56:33 -0400 Subject: [PATCH 16/79] README --- README.md | 39 +++++++++++++++------------------------ 1 file changed, 15 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index bd42b87..f6ee070 100644 --- a/README.md +++ b/README.md @@ -17,48 +17,39 @@ Collection of general purpose javascript functions. JSUS helps! 9. COMPATIBILITY 10. QUEUE -Full API description available [here](http://nodegame.github.io/JSUS/docs/jsus.js.html). -## Build - -Create your customized build of JSUS.js using the make file in the bin directory +All methods of all libraries are available directly as +JSUS.[methodName]. ```javascript -node make.js build -a // Full build -node make.js build -l obj,array -o jsus-oa.js // Only object and array libs. +JSUS.seq(1,10,2); // [ 1, 3, 5, 7, 9 ]; ``` -## Extend - -JSUS is designed to be modular and easy to extend. - -Just use: +To get a copy of one of the registered JSUS libraries do: ```javascript -JSUS.extend(myClass); +var ARRAY = JSUS.require('ARRAY'); +ARRAY.seq(1,10,2); // [ 1, 3, 5, 7, 9 ]; ``` -to extend the functionalities of JSUS. All the methods of myClass -are immediately added to JSUS, and a reference to myClass is stored -in `JSUS._classes`. +## Browser -`MyClass` can be either of type Object or Function. +In the browser, two objects are exported: `JSUS` and its shorthand `J`. -JSUS can also extend other objects. Just pass a second parameter: +## Full Documentation. -```javascript -JSUS.extend(myClass, mySecondClass); -``` +Full API description available [here](http://nodegame.github.io/JSUS/docs/jsus.js.html). -and `mySecondClass` will receive all the methods of `myClass`. In this case, -no reference of myClass is stored. +## Build -To get a copy of one of the registered JSUS libraries do: +Create your customized build of JSUS.js using the make file in the bin directory ```javascript -var myClass = JSUS.require('myClass'); +node make.js build -a // Full build +node make.js build -l obj,array -o jsus-oa.js // Only object and array libs. ``` + ## API Documentation Create html API documentation using the make file in the bin directory From 017e673ac04ef6d17781a7dca82753df30938515 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 2 Oct 2017 22:02:08 -0400 Subject: [PATCH 17/79] updated --- build/jsus.js | 92 ++++++++++++++++++++++++++++------------------- build/jsus.min.js | 2 +- jsus.js | 39 +++++++++++--------- lib/dom.js | 4 ++- 4 files changed, 82 insertions(+), 55 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index 1866bd5..def0cb8 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -97,28 +97,35 @@ /** * ## JSUS.require * - * Returns a copy of one / all the objects extending JSUS + * Returns a copy/reference of one/all the JSUS components * - * The first parameter is a string representation of the name of - * the requested extending object. If no parameter is passed a copy - * of all the extending objects is returned. + * @param {string} component The name of the requested JSUS library. + * If undefined, all JSUS components are returned. Default: undefined. + * @param {boolean} clone If TRUE, the requested component is cloned + * before being returned. Default: FALSE * - * @param {string} className The name of the requested JSUS library - * - * @return {function|boolean} The copy of the JSUS library, or - * FALSE if the library does not exist + * @return {function|boolean} The copy of the JSUS component, or + * FALSE if the library does not exist, or cloning is not possible */ - JSUS.require = function(className) { - if ('undefined' === typeof JSUS.clone) { - JSUS.log('JSUS.clone not found. Cannot continue.'); + JSUS.require = function(component, clone) { + var out; + clone = 'undefined' === typeof clone ? true : clone; + if (clone && 'undefined' === typeof JSUS.clone) { + JSUS.log('JSUS.require: JSUS.clone not found, but clone ' + + 'requested. Cannot continue.'); return false; } - if ('undefined' === typeof className) return JSUS.clone(JSUS._classes); - if ('undefined' === typeof JSUS._classes[className]) { - JSUS.log('Could not find class ' + className); - return false; + if ('undefined' === typeof component) { + out = JSUS._classes; + } + else { + out = JSUS._classes[component] + if ('undefined' === typeof out) { + JSUS.log('JSUS.require: could not find component ' + component); + return false; + } } - return JSUS.clone(JSUS._classes[className]); + return clone ? JSUS.clone(out) : out; }; /** @@ -159,7 +166,7 @@ /** * # ARRAY - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Collection of static functions to manipulate arrays @@ -291,20 +298,27 @@ * If an error occurs returns FALSE. * * @param {array} array The array to loop in - * @param {Function} func The callback for each element in the array + * @param {Function} cb The callback for each element in the array * @param {object} context Optional. The context of execution of the * callback. Defaults ARRAY.each * * @return {boolean} TRUE, if execution was successful */ - ARRAY.each = function(array, func, context) { - if ('object' !== typeof array) return false; - if (!func) return false; + ARRAY.each = function(array, cb, context) { + var i, len; + if ('object' !== typeof array) { + throw new TypeError('ARRAY.each: array must be object. Found: ' + + array); + } + if ('function' !== typeof cb) { + throw new TypeError('ARRAY.each: cb must be function. Found: ' + + cb); + } context = context || this; - var i, len = array.length; + len = array.length; for (i = 0 ; i < len; i++) { - func.call(context, array[i]); + cb.call(context, array[i]); } return true; }; @@ -340,8 +354,8 @@ } len = arguments.length; - if (len === 3) args = [null, arguments[2]]; - else if (len === 4) args = [null, arguments[2], arguments[3]]; + if (len === 3) args = [ null, arguments[2] ]; + else if (len === 4) args = [ null, arguments[2], arguments[3] ]; else { len = len - 1; args = new Array(len); @@ -5631,7 +5645,7 @@ /** * # PARSE - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2017 Stefano Balietti * MIT Licensed * * Collection of static functions related to parsing strings @@ -5681,7 +5695,7 @@ var regex, results; if (referer && 'string' !== typeof referer) { throw new TypeError('JSUS.getQueryString: referer must be string ' + - 'or undefined.'); + 'or undefined. Found: ' + referer); } referer = referer || window.location.search; if ('undefined' === typeof name) return referer; @@ -6049,7 +6063,7 @@ if ('number' === typeof expr) expr = '' + expr; else if ('string' !== typeof expr) { throw new TypeError('PARSE.range: expr must be string, number, ' + - 'undefined.'); + 'undefined. Found: ' + expr); } // If no available numbers defined, assumes all possible are allowed. if ('undefined' === typeof available) { @@ -6063,19 +6077,20 @@ else if ('object' === typeof available) { if ('function' !== typeof available.next) { throw new TypeError('PARSE.range: available.next must be ' + - 'function.'); + 'function. Found: ' + available.next); } if ('function' !== typeof available.isFinished) { throw new TypeError('PARSE.range: available.isFinished must ' + - 'be function.'); + 'be function. Found: ' + + available.isFinished); } if ('number' !== typeof available.begin) { throw new TypeError('PARSE.range: available.begin must be ' + - 'number.'); + 'number. Found: ' + available.begin); } if ('number' !== typeof available.end) { throw new TypeError('PARSE.range: available.end must be ' + - 'number.'); + 'number. Found: ' + available.end); } begin = available.begin; @@ -6088,8 +6103,8 @@ numbers = available.match(/([-+]?\d+)/g); if (numbers === null) { - throw new Error( - 'PARSE.range: no numbers in available: ' + available); + throw new Error('PARSE.range: no numbers in available: ' + + available); } lowerBound = Math.min.apply(null, numbers); @@ -6109,7 +6124,8 @@ } else { throw new TypeError('PARSE.range: available must be string, ' + - 'array, object or undefined.'); + 'array, object or undefined. Found: ' + + available); } // end -> maximal available value. @@ -6273,7 +6289,8 @@ if ('undefined' !== typeof Function.prototype.name) { PARSE.funcName = function(func) { if ('function' !== typeof func) { - throw new TypeError('PARSE.funcName: func must be function.'); + throw new TypeError('PARSE.funcName: func must be function. ' + + 'Found: ' + func); } return func.name; }; @@ -6282,7 +6299,8 @@ PARSE.funcName = function(func) { var funcNameRegex, res; if ('function' !== typeof func) { - throw new TypeError('PARSE.funcName: func must be function.'); + throw new TypeError('PARSE.funcName: func must be function. ' + + 'Found: ' + func); } funcNameRegex = /function\s([^(]{1,})\(/; res = (funcNameRegex).exec(func.toString()); diff --git a/build/jsus.min.js b/build/jsus.min.js index da727d2..0da5469 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e){return"undefined"==typeof t.clone?(t.log("JSUS.clone not found. Cannot continue."),!1):"undefined"==typeof e?t.clone(t._classes):"undefined"==typeof t._classes[e]?(t.log("Could not find class "+e),!1):t.clone(t._classes[e])},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){if("object"!=typeof e)return!1;if(!t)return!1;n=n||this;var r,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined.");if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function.");if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function.");if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number.");if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number.");f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined.");i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o Date: Tue, 24 Oct 2017 16:47:42 -0400 Subject: [PATCH 18/79] shuffleElements accepts cb --- build/jsus.js | 57 ++++++++++++++++++++++++++++++++++------------- build/jsus.min.js | 2 +- lib/dom.js | 40 ++++++++++++++++++++++++--------- lib/fs.js | 9 +++++--- 4 files changed, 78 insertions(+), 30 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index def0cb8..a3b559f 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -101,8 +101,8 @@ * * @param {string} component The name of the requested JSUS library. * If undefined, all JSUS components are returned. Default: undefined. - * @param {boolean} clone If TRUE, the requested component is cloned - * before being returned. Default: FALSE + * @param {boolean} clone Optional. If TRUE, the requested component + * is cloned before being returned. Default: TRUE * * @return {function|boolean} The copy of the JSUS component, or * FALSE if the library does not exist, or cloning is not possible @@ -1016,7 +1016,9 @@ * * @param {string} elem The name of the tag * @param {object|string} attributes Optional. Object containing - * attributes for the element. If string, the id of the element + * attributes for the element. If string, the id of the element. If + * the request element is an 'iframe', the `name` attribute is set + * equal to the `id` attribute. * * @return {HTMLElement} The newly created HTML element * @@ -1390,11 +1392,18 @@ * * @param {Node} parent The parent node * @param {array} order Optional. A pre-specified order. Defaults, random + * @param {function} cb Optional. A callback to execute one each shuffled + * element (after re-positioning). This is always the last parameter, + * so if order is omitted, it goes second. The callback takes as input: + * - the element + * - the new order + * - the old order + * * * @return {array} The order used to shuffle the nodes */ - DOM.shuffleElements = function(parent, order) { - var i, len, idOrder, children, child; + DOM.shuffleElements = function(parent, order, cb) { + var i, len, numOrder, idOrder, children, child; var id; if (!JSUS.isNode(parent)) { throw new TypeError('DOM.shuffleElements: parent must be a node. ' + @@ -1405,16 +1414,25 @@ return false; } if (order) { - if (!JSUS.isArray(order)) { - throw new TypeError('DOM.shuffleElements: order must array.' + - 'Found: ' + order); + if ('undefined' === typeof cb && 'function' === typeof order) { + cb = order; } - if (order.length !== parent.children.length) { - throw new Error('DOM.shuffleElements: order length must ' + - 'match the number of children nodes.'); + else { + if (!JSUS.isArray(order)) { + throw new TypeError('DOM.shuffleElements: order must be ' + + 'array. Found: ' + order); + } + if (order.length !== parent.children.length) { + throw new Error('DOM.shuffleElements: order length must ' + + 'match the number of children nodes.'); + } } } - + if (cb && 'function' !== typeof cb) { + throw new TypeError('DOM.shuffleElements: order must be ' + + 'array. Found: ' + order); + } + // DOM4 compliant browsers. children = parent.children; @@ -1428,16 +1446,19 @@ } } + // Get all ids. len = children.length; idOrder = new Array(len); + if (cb) numOrder = new Array(len); if (!order) order = JSUS.sample(0, (len-1)); for (i = 0 ; i < len; i++) { id = children[order[i]].id; if ('string' !== typeof id || id === "") { throw new Error('DOM.shuffleElements: no id found on ' + - 'child n. ' + order[i] + '.'); + 'child n. ' + order[i]); } idOrder[i] = id; + if (cb) numOrder[i] = order[i]; } // Two fors are necessary to follow the real sequence (Live List). @@ -1445,6 +1466,7 @@ // could be unreliable. for (i = 0 ; i < len; i++) { parent.appendChild(children[idOrder[i]]); + if (cb) cb(children[idOrder[i]], i, numOrder[i]); } return idOrder; }; @@ -4069,7 +4091,9 @@ * Copyright(c) 2016 Stefano Balietti * MIT Licensed * - * Collection of static functions related to file system operations + * Collection of static functions related to file system operations. + * + * TODO: see if we need to keep this file at all. * * @see http://nodejs.org/api/fs.html * @see https://github.com/jprichardson/node-fs-extra @@ -4123,11 +4147,12 @@ FS.resolveModuleDir = function(module, basedir) { var str, stop; if ('string' !== typeof module) { - throw new TypeError('FS.resolveModuleDir: module must be string.'); + throw new TypeError('FS.resolveModuleDir: module must be ' + + 'string. Found: ' + module); } if (basedir && 'string' !== typeof basedir) { throw new TypeError('FS.resolveModuleDir: basedir must be ' + - 'string or undefined.'); + 'string or undefined. Found: ' + basedir); } // Added this line because it might fail. if (module === 'JSUS') return path.resolve(__dirname, '..') + '/'; diff --git a/build/jsus.min.js b/build/jsus.min.js index 0da5469..b4810d2 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e,n){var r;n="undefined"==typeof n?!0:n;if(n&&"undefined"==typeof t.clone)return t.log("JSUS.require: JSUS.clone not found, but clone requested. Cannot continue."),!1;if("undefined"==typeof e)r=t._classes;else{r=t._classes[e];if("undefined"==typeof r)return t.log("JSUS.require: could not find component "+e),!1}return n?t.clone(r):r},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o Date: Tue, 24 Oct 2017 16:48:17 -0400 Subject: [PATCH 19/79] mods --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 0d87a24..3e7cc7d 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,7 @@ - `J` is exported as a top level object in the browser's window. - `#JSUS.get` alias of `JSUS.require` removed. `#JSUS.get` is now a method of the DOM library. - DOM library rewritten. Several methods removed and code optimized. Methods _added_: get, add|append, addAttributes. Methods _modified_: generateUniqueId, addCSS, addJS. Methods _removed_: getElement, addElement, addAttributes2Elem, getButton, addButton, getFieldset, addFieldset, getTextInput, addTextInput, getTextArea, addTextArea, getCanvas, addCanvas, getSlider, addSlider, getRadioButton, addRadioButton, getLabel, addLabel, getSelect, addSelect, getIFrame, addIFrame, addBreak, getDiv, addDiv. +- `#DOM.shuffleElements` accepts a callback. ## 0.18.0 - `#OBJ.mixin|mixout|mixcommon` return the mixed object. From c26cb2bf42e2eb21d6e68c24205b392f787a5d2b Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Oct 2017 11:14:16 -0400 Subject: [PATCH 20/79] travis --- .travis.yml | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8fc1005..0db8769 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,31 +1,21 @@ sudo: false language: node_js node_js: - - 4.5 + - 6 + - 7 + - 8 before_install: # Get installer script. - - cd bin - - wget https://github.com/nodeGame/nodegame/raw/master/bin/install-nodegame-for-module.sh - - chmod a+x install-nodegame-for-module.sh - - cd .. + - wget https://github.com/nodeGame/nodegame-test/raw/master/nodegame-installer.js + - chmod a+x nodegame-installer.js install: -# Share node_modules with nodeGame. - - mkdir -p nodegame node_modules - - ln -s ../node_modules nodegame/node_modules - - ln -s .. node_modules/JSUS -# If "install-nodegame-for-module.sh" is called before "npm install", -# it gives an error. - - npm install - - bin/install-nodegame-for-module.sh -g "nodegame-window nodegame-widgets NDDB shelf.js nodegame-client nodegame-server descil-mturk nodegame-requirements nodegame-monitor nodegame-db nodegame-mongodb" - + - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 script: -# Test JSUS. +# Add JSUS tests here. - npm test # Test Ultimatum game. - - cd nodegame/games - - git clone https://github.com/nodeGame/ultimatum.git - - cd ultimatum - - ./bin/run-standalone-test.sh + - cd node_modules/nodegame-test/games/ultimatum-game + - ./bin/run-standalone-test-v4.sh From ce2f8cbe3a5b2a2bf824d06a31c8af2358bdbc2e Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Oct 2017 19:05:28 -0400 Subject: [PATCH 21/79] travis --- .travis.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.travis.yml b/.travis.yml index 0db8769..b7df888 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,10 +12,13 @@ before_install: install: - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 + - npm install -g should + - npm install -g mocha script: # Add JSUS tests here. - npm test + # Test Ultimatum game. - cd node_modules/nodegame-test/games/ultimatum-game - ./bin/run-standalone-test-v4.sh From f151c715384c53de10837bbcc2c41d4f7a8d517a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Oct 2017 20:18:32 -0400 Subject: [PATCH 22/79] travis --- .travis.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index b7df888..803e7f8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,8 @@ before_install: - chmod a+x nodegame-installer.js install: - - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 - - npm install -g should - - npm install -g mocha + - npm install --only=dev + - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 --yes script: # Add JSUS tests here. From e7e91aa64b6b9336a873ddec6bfae993aa5ce5ec Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Oct 2017 20:18:47 -0400 Subject: [PATCH 23/79] travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 803e7f8..00caec2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ node_js: before_install: # Get installer script. - - wget https://github.com/nodeGame/nodegame-test/raw/master/nodegame-installer.js + - wget https://github.com/nodeGame/nodegame/raw/v4/nodegame-installer.js - chmod a+x nodegame-installer.js install: From fa04ab7172dd1d515976f7efa92e4e3f4176c47c Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Oct 2017 20:21:49 -0400 Subject: [PATCH 24/79] travis --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index e3886b7..d435afe 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,8 @@ }, "devDependencies": { "mocha": ">= 0.3.0", - "should": ">= 0.5.1" + "should": ">= 0.5.1", + "fs-extra": "*" }, "engines": { "node": ">=0.10.0" }, "repository": { From 1af24f06b0b8fe04db1b4b06b9fb8fafcdc819df Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 26 Oct 2017 20:26:43 -0400 Subject: [PATCH 25/79] travis --- package.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index d435afe..2494d49 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "devDependencies": { "mocha": ">= 0.3.0", "should": ">= 0.5.1", - "fs-extra": "*" + "fs-extra": "*", + "resolve": "*" }, "engines": { "node": ">=0.10.0" }, "repository": { From a29d165b803a14b7471ce158dbd414a80c246ccd Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 30 Oct 2017 13:53:24 -0400 Subject: [PATCH 26/79] travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 00caec2..a498c09 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,7 @@ node_js: before_install: # Get installer script. - - wget https://github.com/nodeGame/nodegame/raw/v4/nodegame-installer.js + - wget https://raw.githubusercontent.com/nodeGame/nodegame/master/bin/nodegame-installer.js - chmod a+x nodegame-installer.js install: From 691dc588f3edda390c3dd4f5e057731a4c2d0189 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 30 Oct 2017 14:08:51 -0400 Subject: [PATCH 27/79] travis --- .travis.yml | 6 ++++-- package.json | 4 +--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index a498c09..759e410 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,8 +1,6 @@ sudo: false language: node_js node_js: - - 6 - - 7 - 8 before_install: @@ -13,9 +11,13 @@ before_install: install: - npm install --only=dev - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 --yes + - mv node_modules/JSUS/node_modules/resolve node_modules/ + script: # Add JSUS tests here. + - ls -la node_modules/JSUS/node_modules/ + - ls -la node_modules/ - npm test # Test Ultimatum game. diff --git a/package.json b/package.json index 2494d49..e3886b7 100644 --- a/package.json +++ b/package.json @@ -14,9 +14,7 @@ }, "devDependencies": { "mocha": ">= 0.3.0", - "should": ">= 0.5.1", - "fs-extra": "*", - "resolve": "*" + "should": ">= 0.5.1" }, "engines": { "node": ">=0.10.0" }, "repository": { From 4e8e6666d655d207aaa93e9cfc6be234e4872325 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 30 Oct 2017 14:15:49 -0400 Subject: [PATCH 28/79] travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 759e410..6feadcf 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,6 +12,7 @@ install: - npm install --only=dev - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 --yes - mv node_modules/JSUS/node_modules/resolve node_modules/ + - mv node_modules/fs-extra/node_modules/resolve node_modules/ script: From 0aa9b6907874201a94204a3a1abeefee477d6c93 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 30 Oct 2017 14:18:12 -0400 Subject: [PATCH 29/79] travis --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 6feadcf..362e0d0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ install: - npm install --only=dev - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 --yes - mv node_modules/JSUS/node_modules/resolve node_modules/ - - mv node_modules/fs-extra/node_modules/resolve node_modules/ + - mv node_modules/JSUS/node_modules/fs-extra node_modules/ script: From b6c09361f5f647d577c3c656e1d6cce120d5796f Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 30 Oct 2017 14:22:08 -0400 Subject: [PATCH 30/79] travis --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 362e0d0..e4450de 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ sudo: false language: node_js node_js: + - 6 + - 7 - 8 before_install: @@ -22,5 +24,5 @@ script: - npm test # Test Ultimatum game. - - cd node_modules/nodegame-test/games/ultimatum-game + - cd node_modules/nodegame/games/ultimatum-game - ./bin/run-standalone-test-v4.sh From cf85fdc3d012cf005ac149d97ebe293129591589 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 15 Nov 2017 18:38:54 -0500 Subject: [PATCH 31/79] ARRAY.each passes i --- lib/array.js | 5 +---- lib/obj.js | 4 ++-- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/lib/array.js b/lib/array.js index 029d1a2..45b6a67 100644 --- a/lib/array.js +++ b/lib/array.js @@ -135,8 +135,6 @@ * @param {Function} cb The callback for each element in the array * @param {object} context Optional. The context of execution of the * callback. Defaults ARRAY.each - * - * @return {boolean} TRUE, if execution was successful */ ARRAY.each = function(array, cb, context) { var i, len; @@ -152,9 +150,8 @@ context = context || this; len = array.length; for (i = 0 ; i < len; i++) { - cb.call(context, array[i]); + cb.call(context, array[i], i); } - return true; }; /** diff --git a/lib/obj.js b/lib/obj.js index aa09e31..30610a7 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -738,7 +738,7 @@ }; /** - * ## OBJ.subobj + * ## OBJ.subobj | subObj * * Creates a copy of an object containing only the properties * passed as second parameter @@ -756,7 +756,7 @@ * * @see OBJ.getNestedValue */ - OBJ.subobj = function(o, select) { + OBJ.subobj = OBJ.subObj = function(o, select) { var out, i, key; if (!o) return false; out = {}; From 53d9ddaea2f229dc6c037dbec2e59d61bdffd8bb Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 30 Nov 2017 22:21:10 -0500 Subject: [PATCH 32/79] fixed OBJ.keys --- lib/obj.js | 4 ++-- test/test.obj.js | 32 ++++++++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 30610a7..189b49f 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -333,8 +333,7 @@ /** * ## OBJ.keys * - * Scans an object an returns all the keys of the properties, - * into an array. + * Returns all the keys of an object until desired level of nestedness * * The second paramter controls the level of nested objects * to be evaluated. Defaults 0 (nested properties are skipped). @@ -358,6 +357,7 @@ if (curLevel < level) { if ('object' === typeof obj[key]) { result = result.concat(OBJ.objGetAllKeys(obj[key], + level, (curLevel+1))); } } diff --git a/test/test.obj.js b/test/test.obj.js index fcd71f1..3b9e712 100644 --- a/test/test.obj.js +++ b/test/test.obj.js @@ -518,17 +518,45 @@ describe('OBJ: ', function() { it('should returns all the first level keys with negative number', function() { - JSUS.keys(obj_complex, -1).should.be.eql(['a','b','c']); }); it('should returns all first and second level keys ("nested option")', function() { - JSUS.keys(obj_complex, 1) .should.be.eql([ 'a', 'b', 'a', 'b', 'c', 'c' ]); }); + + it('should returns all first and second level keys ("nested option")', + function() { + var obj; + obj = { + a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } + }; + JSUS.keys(obj, 2) + .should.be.eql([ 'a1', 'a2', 'a3', 'b3' ]); + }); + + it('should returns up to level 3 keys ("nested option")', + function() { + var obj; + obj = { + a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } + }; + JSUS.keys(obj, 3) + .should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4' ]); + }); + + it('should returns up to level 4 keys ("nested option")', + function() { + var obj; + obj = { + a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } + }; + JSUS.keys(obj, 4) + .should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4', 'b5' ]); + }); }); describe('#equals()', function() { From ee4daa0d0ecc4fb78f61a8666d6ea86d7f00363a Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 2 Dec 2017 20:16:22 -0500 Subject: [PATCH 33/79] new OBJ.keys needs testing --- lib/obj.js | 111 +++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 96 insertions(+), 15 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 189b49f..cafb604 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -330,21 +330,6 @@ return '?' + str.join('&'); }; - /** - * ## OBJ.keys - * - * Returns all the keys of an object until desired level of nestedness - * - * The second paramter controls the level of nested objects - * to be evaluated. Defaults 0 (nested properties are skipped). - * - * @param {object} obj The object from which extract the keys - * @param {number} level Optional. The level of recursion. Defaults 0 - * - * @return {array} The array containing the extracted keys - * - * @see Object.keys - */ OBJ.keys = OBJ.objGetAllKeys = function(obj, level, curLevel) { var result, key; if (!obj) return []; @@ -366,6 +351,102 @@ return result; }; + + /** + * ## OBJ.keys + * + * Returns all the keys of an object until desired level of nestedness + * + * The second paramter controls the level of nested objects + * to be evaluated. Defaults 0 (nested properties are skipped). + * + * @param {object} obj The object from which extract the keys + * @param {number} level Optional. How many nested levels to scan + * @param {object} options Optional. Configuration options: + * + * - type: 'all': all keys (default), + * 'level': keys of the specified level, + * 'leaf': keys that are leaves, i.e., keys that are at the + * the desired level or that do not point to an object + * - concat: true/false: If TRUE, keys are prefixed by parent keys + * - concatChar: a character to inter between parent and children keys; + * (default: '.') + * - distinct: if TRUE, only unique keys are returned (default: false) + * - curParent: the name of initial parent key (default: '') + * - array: an array to which the keys will be appended (default: []) + * + * TODO: cb? + * + * @return {array} The array containing the extracted keys + * + * @see Object.keys + */ + OBJ.keys = (function() { + return function(obj, level, options) { + var keys, type, allKeys, leafKeys, levelKeys, concatChar; + options = options || {}; + + type = options.type ? options.type.toLowerCase() : 'all'; + if (type === 'all') allKeys = true; + else if (type === 'leaf') leafKeys = true; + else if (type === 'level') levelKeys = true; + else throw new Error('keys: unknown type option: ' + type); + + if ('string' === typeof level) level = parseInt(level, 10); + + if (options.concat) { + if ('undefined' === typeof options.concatChar) concatChar = '.'; + else concatChar = options.concatChar; + } + + if (!options.concat && options.distinct) keys = {}; + + return _keys(obj, level || 0, 0, options.curParent || '', + options.concat, allKeys, leafKeys, levelKeys, + concatChar, options.array || [], keys); + } + + function _keys(obj, level, curLevel, curParent, + concatKeys, allKeys, leafKeys, levelKeys, + concatChar, res, uniqueKeys) { + + var key, isLevel, isObj; + isLevel = curLevel === level; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + isObj = 'object' === typeof obj[key]; + if (allKeys || + (leafKeys && (isLevel || !isObj)) || + (levelKeys && isLevel)) { + + if (concatKeys) { + res.push(curParent + key); + } + else { + if (uniqueKeys) { + if (!uniqueKeys[key]) { + res.push(key); + uniqueKeys[key] = true; + } + } + else { + res.push(key); + } + } + } + if (isObj && (curLevel < level)) { + _keys(obj[key], level, (curLevel+1), + concatKeys ? curParent + key + concatChar : key, + concatKeys, allKeys, leafKeys, levelKeys, + concatChar, res, uniqueKeys); + } + } + } + return res; + } + })(); + + /** * ## OBJ.implode * From 143e8aa93f05170d1a00e0db7c905f04ad680687 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 2 Dec 2017 20:18:24 -0500 Subject: [PATCH 34/79] removed OBJ.objGetAllKeys --- CHANGELOG | 2 ++ lib/obj.js | 22 ---------------------- 2 files changed, 2 insertions(+), 22 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 3e7cc7d..4b95370 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -5,6 +5,8 @@ - `#JSUS.get` alias of `JSUS.require` removed. `#JSUS.get` is now a method of the DOM library. - DOM library rewritten. Several methods removed and code optimized. Methods _added_: get, add|append, addAttributes. Methods _modified_: generateUniqueId, addCSS, addJS. Methods _removed_: getElement, addElement, addAttributes2Elem, getButton, addButton, getFieldset, addFieldset, getTextInput, addTextInput, getTextArea, addTextArea, getCanvas, addCanvas, getSlider, addSlider, getRadioButton, addRadioButton, getLabel, addLabel, getSelect, addSelect, getIFrame, addIFrame, addBreak, getDiv, addDiv. - `#DOM.shuffleElements` accepts a callback. +- '#OBJ.keys` improved, read inline docs for new options. +- `#OBJ.objGetAllKeys` removed. ## 0.18.0 - `#OBJ.mixin|mixout|mixcommon` return the mixed object. diff --git a/lib/obj.js b/lib/obj.js index cafb604..6ac207f 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -330,28 +330,6 @@ return '?' + str.join('&'); }; - OBJ.keys = OBJ.objGetAllKeys = function(obj, level, curLevel) { - var result, key; - if (!obj) return []; - level = 'number' === typeof level && level >= 0 ? level : 0; - curLevel = 'number' === typeof curLevel && curLevel >= 0 ? curLevel : 0; - result = []; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - result.push(key); - if (curLevel < level) { - if ('object' === typeof obj[key]) { - result = result.concat(OBJ.objGetAllKeys(obj[key], - level, - (curLevel+1))); - } - } - } - } - return result; - }; - - /** * ## OBJ.keys * From 35aa38ba8853181da7060cfaa622a69289b5e8f6 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 4 Dec 2017 11:22:17 -0500 Subject: [PATCH 35/79] tested OBJ.keys --- lib/obj.js | 56 +++++--- test/test.obj.js | 336 ++++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 354 insertions(+), 38 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 6ac207f..1305130 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -342,18 +342,17 @@ * @param {number} level Optional. How many nested levels to scan * @param {object} options Optional. Configuration options: * - * - type: 'all': all keys (default), + * - type: 'all': all keys (default), * 'level': keys of the specified level, * 'leaf': keys that are leaves, i.e., keys that are at the * the desired level or that do not point to an object * - concat: true/false: If TRUE, keys are prefixed by parent keys - * - concatChar: a character to inter between parent and children keys; + * - separator: a character to inter between parent and children keys; * (default: '.') * - distinct: if TRUE, only unique keys are returned (default: false) * - curParent: the name of initial parent key (default: '') * - array: an array to which the keys will be appended (default: []) - * - * TODO: cb? + * - cb: a callback to be applied to every key before adding to results * * @return {array} The array containing the extracted keys * @@ -361,7 +360,9 @@ */ OBJ.keys = (function() { return function(obj, level, options) { - var keys, type, allKeys, leafKeys, levelKeys, concatChar; + var keys, type, allKeys, leafKeys, levelKeys; + var separator, myLevel, curParent; + options = options || {}; type = options.type ? options.type.toLowerCase() : 'all'; @@ -370,23 +371,39 @@ else if (type === 'level') levelKeys = true; else throw new Error('keys: unknown type option: ' + type); - if ('string' === typeof level) level = parseInt(level, 10); + if (options.cb && 'function' !== typeof options.cb) { + throw new TypeError('JSUS.keys: options.cb must be function ' + + 'or undefined. Found: ' + options.cb); + } + + if ('undefined' === typeof level) myLevel = 0; + else if ('number' === typeof level) myLevel = level; + else if ('string' === typeof level) myLevel = parseInt(level, 10); + if ('number' !== typeof myLevel || isNaN(myLevel)) { + throw new Error('JSUS.keys: level must be number, undefined ' + + 'or a parsable string. Found: ' + level); + } + // No keys at level -1; + if (level < 0) return []; if (options.concat) { - if ('undefined' === typeof options.concatChar) concatChar = '.'; - else concatChar = options.concatChar; + if ('undefined' === typeof options.separator) separator = '.'; + else separator = options.separator; } - if (!options.concat && options.distinct) keys = {}; + if (options.curParent) curParent = options.curParent + separator; + else curParent = ''; + + if (!options.concat && options.distinct) keys = {}; - return _keys(obj, level || 0, 0, options.curParent || '', + return _keys(obj, myLevel, 0, curParent, options.concat, allKeys, leafKeys, levelKeys, - concatChar, options.array || [], keys); + separator, options.array || [], keys, options.cb); } function _keys(obj, level, curLevel, curParent, concatKeys, allKeys, leafKeys, levelKeys, - concatChar, res, uniqueKeys) { + separator, res, uniqueKeys, cb) { var key, isLevel, isObj; isLevel = curLevel === level; @@ -398,32 +415,35 @@ (levelKeys && isLevel)) { if (concatKeys) { - res.push(curParent + key); + if (cb) res.push(cb(curParent + key)); + else res.push(curParent + key); } else { if (uniqueKeys) { if (!uniqueKeys[key]) { - res.push(key); + if (cb) res.push(cb(key)); + else res.push(key); uniqueKeys[key] = true; } } else { - res.push(key); + if (cb) res.push(cb(key)); + else res.push(key); } } } if (isObj && (curLevel < level)) { _keys(obj[key], level, (curLevel+1), - concatKeys ? curParent + key + concatChar : key, + concatKeys ? curParent + key + separator : key, concatKeys, allKeys, leafKeys, levelKeys, - concatChar, res, uniqueKeys); + separator, res, uniqueKeys, cb); } } } return res; } })(); - + /** * ## OBJ.implode diff --git a/test/test.obj.js b/test/test.obj.js index 3b9e712..456a838 100644 --- a/test/test.obj.js +++ b/test/test.obj.js @@ -53,6 +53,12 @@ var obj_with_array_of_obj = { c: 3, }; + +var obj; +obj = { + a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } +}; + var array_simple = [1,2,3]; var array_complex = [1, array_simple, 3]; var array_with_null = [1,null,3]; @@ -512,53 +518,343 @@ describe('OBJ: ', function() { describe('#keys()', function() { - it('should returns all the first level keys', function() { + it('should return all the first level keys', function() { JSUS.keys(obj_complex).should.be.eql(['a','b','c']); }); - it('should returns all the first level keys with negative number', + it('should return all no keys with negative number', function() { - JSUS.keys(obj_complex, -1).should.be.eql(['a','b','c']); + JSUS.keys(obj_complex, -1).should.be.eql([]); }); - it('should returns all first and second level keys ("nested option")', + it('should return all the first level keys with 0', + function() { + JSUS.keys(obj_complex, 0).should.be.eql(['a','b','c']); + }); + + it('should return all first and second level keys ("level option")', function() { JSUS.keys(obj_complex, 1) .should.be.eql([ 'a', 'b', 'a', 'b', 'c', 'c' ]); }); - - it('should returns all first and second level keys ("nested option")', + it('should return all first and second level keys ("level option")', function() { - var obj; - obj = { - a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } - }; JSUS.keys(obj, 2) .should.be.eql([ 'a1', 'a2', 'a3', 'b3' ]); }); - it('should returns up to level 3 keys ("nested option")', + it('should return up to level 3 keys ("level option")', function() { - var obj; - obj = { - a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } - }; JSUS.keys(obj, 3) .should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4' ]); }); - it('should returns up to level 4 keys ("nested option")', + it('should return up to level 4 keys ("level option")', function() { - var obj; - obj = { - a1: { a2: { a3: "a", b3: { b4: { b5: "b" } } } } - }; JSUS.keys(obj, 4) .should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4', 'b5' ]); }); + + it('should return up to level 5 keys (equal to 4) ("level option")', + function() { + JSUS.keys(obj, 5) + .should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4', 'b5' ]); + }); + + // All Option. + + it('should return all the first level keys', function() { + JSUS.keys(obj_complex, undefined, { + type: 'all' + }).should.be.eql(['a','b','c']); + }); + + it('should return all no keys with negative number', + function() { + JSUS.keys(obj_complex, -1, { + type: 'all' + }).should.be.eql([]); + }); + + it('should return all the first level keys with 0', + function() { + JSUS.keys(obj_complex, 0, { + type: 'all' + }).should.be.eql(['a','b','c']); + }); + + it('should return all first and second level keys ("level option")', + function() { + JSUS.keys(obj_complex, 1, { + type: 'all' + }).should.be.eql([ 'a', 'b', 'a', 'b', 'c', 'c' ]); + }); + + it('should return all first and second level keys ("level option")', + function() { + JSUS.keys(obj, 2, { + type: 'all' + }).should.be.eql([ 'a1', 'a2', 'a3', 'b3' ]); + }); + + it('should return up to level 3 keys ("level option")', + function() { + JSUS.keys(obj, 3, { + type: 'all' + }).should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4' ]); + }); + + it('should return up to level 4 keys ("level option")', + function() { + JSUS.keys(obj, 4, { + type: 'all' + }).should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4', 'b5' ]); + }); + + it('should return up to level 5 keys (equal to 4) ("level option")', + function() { + JSUS.keys(obj, 5, { + type: 'all' + }).should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4', 'b5' ]); + }); + + // Level Option. + + it('should return level 0 keys only (type="level" option)', + function() { + JSUS.keys(obj, 0, { type: 'level' }) + .should.be.eql([ 'a1' ]); + }); + + it('should return level 1 keys only (type="level" option)', + function() { + JSUS.keys(obj, 1, { type: 'level' }) + .should.be.eql([ 'a2' ]); + }); + + it('should return level 2 keys only (type="level" option)', + function() { + JSUS.keys(obj, 2, { type: 'level' }) + .should.be.eql([ 'a3', 'b3' ]); + }); + + it('should return level 3 keys only (type="level" option)', + function() { + JSUS.keys(obj, 3, { type: 'level' }) + .should.be.eql([ 'b4' ]); + }); + + + it('should return level 4 keys only (type="level" option)', + function() { + JSUS.keys(obj, 4, { type: 'level' }) + .should.be.eql([ 'b5' ]); + }); + + it('should return level 5 keys only (no keys) (type="level" option)', + function() { + JSUS.keys(obj, 5, { type: 'level' }) + .should.be.eql([]); + }); + + // Leaf Option. + + it('should return level 4 leaf-keys (type="level" option)', + function() { + JSUS.keys(obj, 4, { type: 'leaf' }) + .should.be.eql([ 'a3', 'b5' ]); + }); + + it('should return level 3 leaf-keys (type="level" option)', + function() { + JSUS.keys(obj, 3, { type: 'leaf' }) + .should.be.eql([ 'a3', 'b4' ]); + }); + + it('should return level 2 keys only (type="leaf" option)', + function() { + JSUS.keys(obj, 2, { type: 'level' }) + .should.be.eql([ 'a3', 'b3' ]); + }); + + it('should return level 1 keys only (type="leaf" option)', + function() { + JSUS.keys(obj, 1, { type: 'level' }) + .should.be.eql([ 'a2' ]); + }); + + it('should return level 0 keys only (type="leaf" option)', + function() { + JSUS.keys(obj, 0, { type: 'level' }) + .should.be.eql([ 'a1' ]); + }); + + it('should return level -1 keys only (type="leaf" option)', + function() { + JSUS.keys(obj, -1, { type: 'level' }) + .should.be.eql([]); + }); + + // Concat Leaf Option. + + it('should return level 4 leaf-keys (concat option)', + function() { + JSUS.keys(obj, 4, { type: 'leaf', concat: true }) + .should.be.eql([ 'a1.a2.a3', 'a1.a2.b3.b4.b5' ]); + }); + + + // Concat Leaf Option + separator. + + it('should return level 4 leaf-keys (concat+separator option)', + function() { + JSUS.keys(obj, 4, { + type: 'leaf', + concat: true, + separator: '-' + }).should.be.eql([ 'a1-a2-a3', 'a1-a2-b3-b4-b5' ]); + }); + + // Concat All Option. + + it('should return all keys up to level 4 (concat option)', + function() { + JSUS.keys(obj, 4, { concat: true }) + .should.be.eql([ + 'a1', + 'a1.a2', + 'a1.a2.a3', + 'a1.a2.b3', + 'a1.a2.b3.b4', + 'a1.a2.b3.b4.b5' + ]); + }); + + + // Concat All Option + separator. + + it('should return all keys up to level 4 (concat+separator option)', + function() { + JSUS.keys(obj, 4, { + concat: true, + separator: '-' + }).should.be.eql([ + 'a1', + 'a1-a2', + 'a1-a2-a3', + 'a1-a2-b3', + 'a1-a2-b3-b4', + 'a1-a2-b3-b4-b5' + ]); + }); + + // Distinct Option. + + it('should return all keys up to level 4 (distinct option)', + function() { + var obj2; + obj2 = { + a1: { a2: { a3: "a", a2: { b4: { a3: "b" } } } } + }; + JSUS.keys(obj2, 4, { + distinct: true + }).should.be.eql([ + 'a1', 'a2', 'a3', 'b4' + ]); + }); + + + it('should return all keys up to level 4 (with duplicates)', + function() { + var obj2; + obj2 = { + a1: { a2: { a3: "a", a2: { b4: { a3: "b" } } } } + }; + JSUS.keys(obj2, 4).should.be.eql([ + 'a1', 'a2', 'a3', 'a2', 'b4', 'a3' + ]); + }); + + // Cb Option. + + it('should return all keys up to level 4 (distinct option)', + function() { + var obj2; + obj2 = { + a1: { a2: { a3: "a", a2: { b4: { a3: "b" } } } } + }; + JSUS.keys(obj2, 4, { + distinct: true, + cb: function(key) { return '_' + key; } + }).should.be.eql([ + '_a1', '_a2', '_a3', '_b4' + ]); + }); + + + it('should return all keys up to level 4 (with duplicates)', + function() { + var obj2; + obj2 = { + a1: { a2: { a3: "a", a2: { b4: { a3: "b" } } } } + }; + JSUS.keys(obj2, 4, { + cb: function(key) { return '_' + key; } + }).should.be.eql([ + '_a1', '_a2', '_a3', '_a2', '_b4', '_a3' + ]); + }); + + // Array Option. + + it('should return all keys up to level 4 (concat+sep+array option)', + function() { + var myarr = [ 1, 2 ]; + JSUS.keys(obj, 4, { + concat: true, + separator: '-', + array: myarr + }); + + myarr.should.be.eql([ + 1, 2, + 'a1', + 'a1-a2', + 'a1-a2-a3', + 'a1-a2-b3', + 'a1-a2-b3-b4', + 'a1-a2-b3-b4-b5' + ]); + }); + + // Array Option. + + it('should return all keys up to level 4 (concat+sep+array option)', + function() { + var myarr = [ 1, 2 ]; + JSUS.keys(obj, 4, { + concat: true, + separator: '-', + array: myarr, + curParent: 'parent' + }); + + myarr.should.be.eql([ + 1, 2, + 'parent-a1', + 'parent-a1-a2', + 'parent-a1-a2-a3', + 'parent-a1-a2-b3', + 'parent-a1-a2-b3-b4', + 'parent-a1-a2-b3-b4-b5' + ]); + }); }); + + + describe('#equals()', function() { it('should say that 1 and 1 are equal', function() { From 50ce00289b75f36628bad05a4d41bb5d96d7088c Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 4 Dec 2017 12:08:46 -0500 Subject: [PATCH 36/79] polymorphic JSUS.keys --- lib/obj.js | 12 +++++++++--- test/test.obj.js | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 1305130..73e3fe0 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -335,11 +335,12 @@ * * Returns all the keys of an object until desired level of nestedness * - * The second paramter controls the level of nested objects - * to be evaluated. Defaults 0 (nested properties are skipped). + * The second parameter can be omitted, and the level can be specified + * inside the options object passed as second parameter. * * @param {object} obj The object from which extract the keys - * @param {number} level Optional. How many nested levels to scan + * @param {number} level Optional. How many nested levels to scan. + * Default: 0, meaning 0 recursion, i.e., only first level keys. * @param {object} options Optional. Configuration options: * * - type: 'all': all keys (default), @@ -363,6 +364,11 @@ var keys, type, allKeys, leafKeys, levelKeys; var separator, myLevel, curParent; + if (arguments.length === 2 && 'object' === typeof level) { + options = level; + level = options.level; + } + options = options || {}; type = options.type ? options.type.toLowerCase() : 'all'; diff --git a/test/test.obj.js b/test/test.obj.js index 456a838..43ed219 100644 --- a/test/test.obj.js +++ b/test/test.obj.js @@ -850,6 +850,49 @@ describe('OBJ: ', function() { 'parent-a1-a2-b3-b4-b5' ]); }); + + // Shortcut syntax. + + + it('should return all keys up to level 4 (shortcut syntax)', + function() { + var myarr = [ 1, 2 ]; + JSUS.keys(obj, { + level: 4, + concat: true, + separator: '-', + array: myarr, + curParent: 'parent' + }); + + myarr.should.be.eql([ + 1, 2, + 'parent-a1', + 'parent-a1-a2', + 'parent-a1-a2-a3', + 'parent-a1-a2-b3', + 'parent-a1-a2-b3-b4', + 'parent-a1-a2-b3-b4-b5' + ]); + }); + + it('should return all keys up to level 4 (shortcut syntax)', + function() { + var myarr = [ 1, 2 ]; + JSUS.keys(obj, { + concat: true, + separator: '-', + array: myarr, + curParent: 'parent' + }); + + myarr.should.be.eql([ + 1, 2, + 'parent-a1' + ]); + }); + + }); From 09c32efa72798dc23b3c1bcdc36632dedea18a3c Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 4 Dec 2017 12:24:03 -0500 Subject: [PATCH 37/79] cb OBJ.keys --- lib/obj.js | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 73e3fe0..1a8d417 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -411,7 +411,7 @@ concatKeys, allKeys, leafKeys, levelKeys, separator, res, uniqueKeys, cb) { - var key, isLevel, isObj; + var key, isLevel, isObj, tmp; isLevel = curLevel === level; for (key in obj) { if (obj.hasOwnProperty(key)) { @@ -421,19 +421,21 @@ (levelKeys && isLevel)) { if (concatKeys) { - if (cb) res.push(cb(curParent + key)); - else res.push(curParent + key); + tmp = curParent + key; + if (cb) _doCb(tmp, res, cb); + else res.push(tmp); + } else { if (uniqueKeys) { if (!uniqueKeys[key]) { - if (cb) res.push(cb(key)); + if (cb) _doCb(key, res, cb); else res.push(key); uniqueKeys[key] = true; } } - else { - if (cb) res.push(cb(key)); + else { + if (cb) _doCb(key, res, cb); else res.push(key); } } @@ -448,6 +450,23 @@ } return res; } + + function _doCb(key, res, cb) { + var tmp; + tmp = cb(key); + // If string, substitute it. + if ('string' === typeof tmp || 'number' === typeof tmp) { + res.push(tmp); + } + // If array, expand it. + else if (J.isArray(tmp)) { + res = res.concat(tmp); + } + if ('undefined' === typeof tmp) { + res.push(key); + } + // Else, e.g. null, ignore it. + } })(); From 3964b86f4c2793da55536310feda61a2d0b09abe Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 4 Dec 2017 13:59:32 -0500 Subject: [PATCH 38/79] more OBJ.keys --- lib/obj.js | 75 ++++++++++++++++++++++++++++++++---------------- test/test.obj.js | 58 +++++++++++++++++++++++++++++++++---- 2 files changed, 103 insertions(+), 30 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 1a8d417..c94ea8c 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -351,9 +351,15 @@ * - separator: a character to inter between parent and children keys; * (default: '.') * - distinct: if TRUE, only unique keys are returned (default: false) - * - curParent: the name of initial parent key (default: '') + * - parent: the name of initial parent key (default: '') * - array: an array to which the keys will be appended (default: []) - * - cb: a callback to be applied to every key before adding to results + * - skip: an object containing keys to skip + * - cb: a callback to be applied to every key before adding to results. + * The return value of the callback is interpreted as follows: + * - string|number: inserted as it is + * - array: concatenated + * - undefined: the original key is inserted + * - null: nothing is inserted * * @return {array} The array containing the extracted keys * @@ -397,54 +403,59 @@ else separator = options.separator; } - if (options.curParent) curParent = options.curParent + separator; + if (options.parent) curParent = options.parent + separator; else curParent = ''; if (!options.concat && options.distinct) keys = {}; - return _keys(obj, myLevel, 0, curParent, - options.concat, allKeys, leafKeys, levelKeys, - separator, options.array || [], keys, options.cb); + return _keys(obj, myLevel, 0, curParent, options.concat, + allKeys, leafKeys, levelKeys, separator, + options.array || [], keys, options.skip, options.cb); } function _keys(obj, level, curLevel, curParent, concatKeys, allKeys, leafKeys, levelKeys, - separator, res, uniqueKeys, cb) { + separator, res, uniqueKeys, skipKeys, cb) { var key, isLevel, isObj, tmp; isLevel = curLevel === level; for (key in obj) { + console.log(skipKeys); if (obj.hasOwnProperty(key)) { + isObj = 'object' === typeof obj[key]; if (allKeys || (leafKeys && (isLevel || !isObj)) || (levelKeys && isLevel)) { - if (concatKeys) { - tmp = curParent + key; - if (cb) _doCb(tmp, res, cb); - else res.push(tmp); + if (!skipKeys || !skipKeys[key]) { + if (concatKeys) { + tmp = curParent + key; + if (cb) _doCb(tmp, res, cb); + else res.push(tmp); - } - else { - if (uniqueKeys) { - if (!uniqueKeys[key]) { + } + else { + if (uniqueKeys){ + if (!uniqueKeys[key]) { + if (cb) _doCb(key, res, cb); + else res.push(key); + uniqueKeys[key] = true; + } + } + else { if (cb) _doCb(key, res, cb); else res.push(key); - uniqueKeys[key] = true; } } - else { - if (cb) _doCb(key, res, cb); - else res.push(key); - } } + } if (isObj && (curLevel < level)) { _keys(obj[key], level, (curLevel+1), concatKeys ? curParent + key + separator : key, concatKeys, allKeys, leafKeys, levelKeys, - separator, res, uniqueKeys, cb); + separator, res, uniqueKeys, skipKeys, cb); } } } @@ -459,10 +470,26 @@ res.push(tmp); } // If array, expand it. - else if (J.isArray(tmp)) { - res = res.concat(tmp); + else if (JSUS.isArray(tmp) && tmp.length) { + if (tmp.length < 4) { + res.push(tmp[0]); + if (tmp.length > 1) { + res.push(tmp[1]); + if (tmp.length > 2) { + res.push(tmp[2]); + } + } + } + else { + (function() { + var i = -1, len = tmp.length; + for ( ; ++i < len ; ) { + res.push(tmp[i]); + } + })(tmp, res); + } } - if ('undefined' === typeof tmp) { + else if ('undefined' === typeof tmp) { res.push(key); } // Else, e.g. null, ignore it. diff --git a/test/test.obj.js b/test/test.obj.js index 43ed219..35b6c76 100644 --- a/test/test.obj.js +++ b/test/test.obj.js @@ -778,7 +778,7 @@ describe('OBJ: ', function() { // Cb Option. - it('should return all keys up to level 4 (distinct option)', + it('should return all keys up to level 4 (cb+distinct option)', function() { var obj2; obj2 = { @@ -793,7 +793,7 @@ describe('OBJ: ', function() { }); - it('should return all keys up to level 4 (with duplicates)', + it('should return all keys up to level 4 (cb+with duplicates)', function() { var obj2; obj2 = { @@ -806,6 +806,27 @@ describe('OBJ: ', function() { ]); }); + + it('should return all keys up to level 4 (cb advanced)', + function() { + var obj2; + obj2 = { + a1: { a2: { a3: "a", a2: { b4: { a3: "b" } } } } + }; + JSUS.keys(obj2, 4, { + cb: function(key) { + // Key as it is. + if (key === 'a3') return; + if (key === 'b4') return null; + if (key === 'a2') return [ 'ah', 'ah2' ]; + return '_' + key; + } + }).should.be.eql([ + '_a1', 'ah', 'ah2', 'a3', 'ah', 'ah2', 'a3' + ]); + }); + + // Array Option. it('should return all keys up to level 4 (concat+sep+array option)', @@ -837,7 +858,7 @@ describe('OBJ: ', function() { concat: true, separator: '-', array: myarr, - curParent: 'parent' + parent: 'parent' }); myarr.should.be.eql([ @@ -862,7 +883,7 @@ describe('OBJ: ', function() { concat: true, separator: '-', array: myarr, - curParent: 'parent' + parent: 'parent' }); myarr.should.be.eql([ @@ -883,7 +904,7 @@ describe('OBJ: ', function() { concat: true, separator: '-', array: myarr, - curParent: 'parent' + parent: 'parent' }); myarr.should.be.eql([ @@ -891,8 +912,33 @@ describe('OBJ: ', function() { 'parent-a1' ]); }); + + // Skip option. - + it('should return all keys up to level 4 (skip + shortcut)', + function() { + var myarr = [ 1, 2 ]; + JSUS.keys(obj, { + concat: true, + separator: '-', + array: myarr, + skip: { a1: true } + }); + + myarr.should.be.eql([ + 1, 2 + ]); + }); + + it('should return all keys up to level 4 (skip + shortcut)', + function() { + JSUS.keys(obj, { + level: 4, + skip: { a1: true, a2: true, a3: true } + }).should.be.eql([ + 'b3', 'b4', 'b5' + ]); + }); }); From 06d197541e77bf0799f1722e8c953d57beb45050 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 4 Dec 2017 16:06:19 -0500 Subject: [PATCH 39/79] mods --- lib/obj.js | 1 - 1 file changed, 1 deletion(-) diff --git a/lib/obj.js b/lib/obj.js index c94ea8c..fc67a39 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -420,7 +420,6 @@ var key, isLevel, isObj, tmp; isLevel = curLevel === level; for (key in obj) { - console.log(skipKeys); if (obj.hasOwnProperty(key)) { isObj = 'object' === typeof obj[key]; From 07fe760412eb9a691d4cd9f3ccfea226a4ae1d50 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 11 Jan 2018 10:01:39 -0500 Subject: [PATCH 40/79] modified JSUS.keys skip options to work with concatenated keys --- lib/obj.js | 29 +++++++------ test/test.obj.js | 104 ++++++++++++++++++++++++++++------------------- 2 files changed, 76 insertions(+), 57 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index fc67a39..498c66d 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -410,7 +410,8 @@ return _keys(obj, myLevel, 0, curParent, options.concat, allKeys, leafKeys, levelKeys, separator, - options.array || [], keys, options.skip, options.cb); + options.array || [], keys, options.skip || {}, + options.cb); } function _keys(obj, level, curLevel, curParent, @@ -427,28 +428,26 @@ (leafKeys && (isLevel || !isObj)) || (levelKeys && isLevel)) { - if (!skipKeys || !skipKeys[key]) { - if (concatKeys) { - tmp = curParent + key; + if (concatKeys) { + tmp = curParent + key; + if (!skipKeys[tmp]) { if (cb) _doCb(tmp, res, cb); else res.push(tmp); - } - else { - if (uniqueKeys){ - if (!uniqueKeys[key]) { - if (cb) _doCb(key, res, cb); - else res.push(key); - uniqueKeys[key] = true; - } - } - else { + } + else if (!skipKeys[key]) { + if (uniqueKeys){ + if (!uniqueKeys[key]) { if (cb) _doCb(key, res, cb); else res.push(key); + uniqueKeys[key] = true; } } + else { + if (cb) _doCb(key, res, cb); + else res.push(key); + } } - } if (isObj && (curLevel < level)) { _keys(obj[key], level, (curLevel+1), diff --git a/test/test.obj.js b/test/test.obj.js index 35b6c76..0d836f6 100644 --- a/test/test.obj.js +++ b/test/test.obj.js @@ -531,7 +531,7 @@ describe('OBJ: ', function() { function() { JSUS.keys(obj_complex, 0).should.be.eql(['a','b','c']); }); - + it('should return all first and second level keys ("level option")', function() { JSUS.keys(obj_complex, 1) @@ -543,13 +543,13 @@ describe('OBJ: ', function() { JSUS.keys(obj, 2) .should.be.eql([ 'a1', 'a2', 'a3', 'b3' ]); }); - + it('should return up to level 3 keys ("level option")', function() { JSUS.keys(obj, 3) .should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4' ]); }); - + it('should return up to level 4 keys ("level option")', function() { JSUS.keys(obj, 4) @@ -583,7 +583,7 @@ describe('OBJ: ', function() { type: 'all' }).should.be.eql(['a','b','c']); }); - + it('should return all first and second level keys ("level option")', function() { JSUS.keys(obj_complex, 1, { @@ -597,14 +597,14 @@ describe('OBJ: ', function() { type: 'all' }).should.be.eql([ 'a1', 'a2', 'a3', 'b3' ]); }); - + it('should return up to level 3 keys ("level option")', function() { JSUS.keys(obj, 3, { type: 'all' }).should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4' ]); }); - + it('should return up to level 4 keys ("level option")', function() { JSUS.keys(obj, 4, { @@ -618,40 +618,40 @@ describe('OBJ: ', function() { type: 'all' }).should.be.eql([ 'a1', 'a2', 'a3', 'b3', 'b4', 'b5' ]); }); - + // Level Option. - + it('should return level 0 keys only (type="level" option)', function() { JSUS.keys(obj, 0, { type: 'level' }) .should.be.eql([ 'a1' ]); }); - + it('should return level 1 keys only (type="level" option)', function() { JSUS.keys(obj, 1, { type: 'level' }) .should.be.eql([ 'a2' ]); }); - + it('should return level 2 keys only (type="level" option)', function() { JSUS.keys(obj, 2, { type: 'level' }) .should.be.eql([ 'a3', 'b3' ]); }); - + it('should return level 3 keys only (type="level" option)', function() { JSUS.keys(obj, 3, { type: 'level' }) .should.be.eql([ 'b4' ]); }); - + it('should return level 4 keys only (type="level" option)', function() { JSUS.keys(obj, 4, { type: 'level' }) .should.be.eql([ 'b5' ]); }); - + it('should return level 5 keys only (no keys) (type="level" option)', function() { JSUS.keys(obj, 5, { type: 'level' }) @@ -659,13 +659,13 @@ describe('OBJ: ', function() { }); // Leaf Option. - + it('should return level 4 leaf-keys (type="level" option)', function() { JSUS.keys(obj, 4, { type: 'leaf' }) .should.be.eql([ 'a3', 'b5' ]); }); - + it('should return level 3 leaf-keys (type="level" option)', function() { JSUS.keys(obj, 3, { type: 'leaf' }) @@ -677,19 +677,19 @@ describe('OBJ: ', function() { JSUS.keys(obj, 2, { type: 'level' }) .should.be.eql([ 'a3', 'b3' ]); }); - + it('should return level 1 keys only (type="leaf" option)', function() { JSUS.keys(obj, 1, { type: 'level' }) .should.be.eql([ 'a2' ]); }); - + it('should return level 0 keys only (type="leaf" option)', function() { JSUS.keys(obj, 0, { type: 'level' }) .should.be.eql([ 'a1' ]); }); - + it('should return level -1 keys only (type="leaf" option)', function() { JSUS.keys(obj, -1, { type: 'level' }) @@ -697,7 +697,7 @@ describe('OBJ: ', function() { }); // Concat Leaf Option. - + it('should return level 4 leaf-keys (concat option)', function() { JSUS.keys(obj, 4, { type: 'leaf', concat: true }) @@ -706,7 +706,7 @@ describe('OBJ: ', function() { // Concat Leaf Option + separator. - + it('should return level 4 leaf-keys (concat+separator option)', function() { JSUS.keys(obj, 4, { @@ -715,13 +715,13 @@ describe('OBJ: ', function() { separator: '-' }).should.be.eql([ 'a1-a2-a3', 'a1-a2-b3-b4-b5' ]); }); - + // Concat All Option. - + it('should return all keys up to level 4 (concat option)', function() { JSUS.keys(obj, 4, { concat: true }) - .should.be.eql([ + .should.be.eql([ 'a1', 'a1.a2', 'a1.a2.a3', @@ -733,13 +733,13 @@ describe('OBJ: ', function() { // Concat All Option + separator. - + it('should return all keys up to level 4 (concat+separator option)', function() { JSUS.keys(obj, 4, { concat: true, separator: '-' - }).should.be.eql([ + }).should.be.eql([ 'a1', 'a1-a2', 'a1-a2-a3', @@ -750,7 +750,7 @@ describe('OBJ: ', function() { }); // Distinct Option. - + it('should return all keys up to level 4 (distinct option)', function() { var obj2; @@ -763,8 +763,8 @@ describe('OBJ: ', function() { 'a1', 'a2', 'a3', 'b4' ]); }); - - + + it('should return all keys up to level 4 (with duplicates)', function() { var obj2; @@ -775,9 +775,9 @@ describe('OBJ: ', function() { 'a1', 'a2', 'a3', 'a2', 'b4', 'a3' ]); }); - + // Cb Option. - + it('should return all keys up to level 4 (cb+distinct option)', function() { var obj2; @@ -791,8 +791,8 @@ describe('OBJ: ', function() { '_a1', '_a2', '_a3', '_b4' ]); }); - - + + it('should return all keys up to level 4 (cb+with duplicates)', function() { var obj2; @@ -805,7 +805,7 @@ describe('OBJ: ', function() { '_a1', '_a2', '_a3', '_a2', '_b4', '_a3' ]); }); - + it('should return all keys up to level 4 (cb advanced)', function() { @@ -826,9 +826,9 @@ describe('OBJ: ', function() { ]); }); - + // Array Option. - + it('should return all keys up to level 4 (concat+sep+array option)', function() { var myarr = [ 1, 2 ]; @@ -848,9 +848,9 @@ describe('OBJ: ', function() { 'a1-a2-b3-b4-b5' ]); }); - + // Array Option. - + it('should return all keys up to level 4 (concat+sep+array option)', function() { var myarr = [ 1, 2 ]; @@ -874,7 +874,7 @@ describe('OBJ: ', function() { // Shortcut syntax. - + it('should return all keys up to level 4 (shortcut syntax)', function() { var myarr = [ 1, 2 ]; @@ -896,7 +896,7 @@ describe('OBJ: ', function() { 'parent-a1-a2-b3-b4-b5' ]); }); - + it('should return all keys up to level 4 (shortcut syntax)', function() { var myarr = [ 1, 2 ]; @@ -914,7 +914,7 @@ describe('OBJ: ', function() { }); // Skip option. - + it('should return all keys up to level 4 (skip + shortcut)', function() { var myarr = [ 1, 2 ]; @@ -929,7 +929,7 @@ describe('OBJ: ', function() { 1, 2 ]); }); - + it('should return all keys up to level 4 (skip + shortcut)', function() { JSUS.keys(obj, { @@ -939,11 +939,31 @@ describe('OBJ: ', function() { 'b3', 'b4', 'b5' ]); }); + + // Shadowing names. + + it('Shadow names at top level should return all keys: type LEAF', + function() { + var myobj = { a: 1, b: { a: 2, b: 3, c: 4 } }; + var res = JSUS.keys(myobj, { + concat: true, + separator: '.', + type: 'leaf', + level: 2 + }); + + res.should.be.eql([ + 'a', 'b.a', 'b.b', 'b.c' + ]); + }); + + + }); - + describe('#equals()', function() { it('should say that 1 and 1 are equal', function() { From f979b1fdc442383508273055d8a7ff159965770b Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Tue, 16 Jan 2018 09:22:08 -0500 Subject: [PATCH 41/79] minir --- lib/dom.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/lib/dom.js b/lib/dom.js index d5b60b4..00a3073 100644 --- a/lib/dom.js +++ b/lib/dom.js @@ -399,7 +399,7 @@ * @param {Node} parent The parent node * @param {array} order Optional. A pre-specified order. Defaults, random * @param {function} cb Optional. A callback to execute one each shuffled - * element (after re-positioning). This is always the last parameter, + * element (after re-positioning). This is always the last parameter, * so if order is omitted, it goes second. The callback takes as input: * - the element * - the new order @@ -438,7 +438,7 @@ throw new TypeError('DOM.shuffleElements: order must be ' + 'array. Found: ' + order); } - + // DOM4 compliant browsers. children = parent.children; @@ -1265,7 +1265,8 @@ } else if (!JSUS.isArray(titles)) { throw new TypeError(where + 'titles must be string, ' + - 'array of strings or undefined.'); + 'array of strings or undefined. Found: ' + + titles); } rotationId = 0; period = options.period || 1000; From c678f8dd6afda46d009fdeb1550d49f3ffcd76ea Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 21 Apr 2018 21:43:25 -0400 Subject: [PATCH 42/79] built --- build/jsus.js | 185 ++++++++++++++++++++++++++++++++++++++-------- build/jsus.min.js | 2 +- 2 files changed, 157 insertions(+), 30 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index a3b559f..dea0400 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -301,8 +301,6 @@ * @param {Function} cb The callback for each element in the array * @param {object} context Optional. The context of execution of the * callback. Defaults ARRAY.each - * - * @return {boolean} TRUE, if execution was successful */ ARRAY.each = function(array, cb, context) { var i, len; @@ -318,9 +316,8 @@ context = context || this; len = array.length; for (i = 0 ; i < len; i++) { - cb.call(context, array[i]); + cb.call(context, array[i], i); } - return true; }; /** @@ -1393,7 +1390,7 @@ * @param {Node} parent The parent node * @param {array} order Optional. A pre-specified order. Defaults, random * @param {function} cb Optional. A callback to execute one each shuffled - * element (after re-positioning). This is always the last parameter, + * element (after re-positioning). This is always the last parameter, * so if order is omitted, it goes second. The callback takes as input: * - the element * - the new order @@ -1432,7 +1429,7 @@ throw new TypeError('DOM.shuffleElements: order must be ' + 'array. Found: ' + order); } - + // DOM4 compliant browsers. children = parent.children; @@ -2259,7 +2256,8 @@ } else if (!JSUS.isArray(titles)) { throw new TypeError(where + 'titles must be string, ' + - 'array of strings or undefined.'); + 'array of strings or undefined. Found: ' + + titles); } rotationId = 0; period = options.period || 1000; @@ -4706,38 +4704,167 @@ /** * ## OBJ.keys * - * Scans an object an returns all the keys of the properties, - * into an array. + * Returns all the keys of an object until desired level of nestedness * - * The second paramter controls the level of nested objects - * to be evaluated. Defaults 0 (nested properties are skipped). + * The second parameter can be omitted, and the level can be specified + * inside the options object passed as second parameter. * * @param {object} obj The object from which extract the keys - * @param {number} level Optional. The level of recursion. Defaults 0 + * @param {number} level Optional. How many nested levels to scan. + * Default: 0, meaning 0 recursion, i.e., only first level keys. + * @param {object} options Optional. Configuration options: + * + * - type: 'all': all keys (default), + * 'level': keys of the specified level, + * 'leaf': keys that are leaves, i.e., keys that are at the + * the desired level or that do not point to an object + * - concat: true/false: If TRUE, keys are prefixed by parent keys + * - separator: a character to inter between parent and children keys; + * (default: '.') + * - distinct: if TRUE, only unique keys are returned (default: false) + * - parent: the name of initial parent key (default: '') + * - array: an array to which the keys will be appended (default: []) + * - skip: an object containing keys to skip + * - cb: a callback to be applied to every key before adding to results. + * The return value of the callback is interpreted as follows: + * - string|number: inserted as it is + * - array: concatenated + * - undefined: the original key is inserted + * - null: nothing is inserted * * @return {array} The array containing the extracted keys * * @see Object.keys */ - OBJ.keys = OBJ.objGetAllKeys = function(obj, level, curLevel) { - var result, key; - if (!obj) return []; - level = 'number' === typeof level && level >= 0 ? level : 0; - curLevel = 'number' === typeof curLevel && curLevel >= 0 ? curLevel : 0; - result = []; - for (key in obj) { - if (obj.hasOwnProperty(key)) { - result.push(key); - if (curLevel < level) { - if ('object' === typeof obj[key]) { - result = result.concat(OBJ.objGetAllKeys(obj[key], - (curLevel+1))); + OBJ.keys = (function() { + return function(obj, level, options) { + var keys, type, allKeys, leafKeys, levelKeys; + var separator, myLevel, curParent; + + if (arguments.length === 2 && 'object' === typeof level) { + options = level; + level = options.level; + } + + options = options || {}; + + type = options.type ? options.type.toLowerCase() : 'all'; + if (type === 'all') allKeys = true; + else if (type === 'leaf') leafKeys = true; + else if (type === 'level') levelKeys = true; + else throw new Error('keys: unknown type option: ' + type); + + if (options.cb && 'function' !== typeof options.cb) { + throw new TypeError('JSUS.keys: options.cb must be function ' + + 'or undefined. Found: ' + options.cb); + } + + if ('undefined' === typeof level) myLevel = 0; + else if ('number' === typeof level) myLevel = level; + else if ('string' === typeof level) myLevel = parseInt(level, 10); + if ('number' !== typeof myLevel || isNaN(myLevel)) { + throw new Error('JSUS.keys: level must be number, undefined ' + + 'or a parsable string. Found: ' + level); + } + // No keys at level -1; + if (level < 0) return []; + + if (options.concat) { + if ('undefined' === typeof options.separator) separator = '.'; + else separator = options.separator; + } + + if (options.parent) curParent = options.parent + separator; + else curParent = ''; + + if (!options.concat && options.distinct) keys = {}; + + return _keys(obj, myLevel, 0, curParent, options.concat, + allKeys, leafKeys, levelKeys, separator, + options.array || [], keys, options.skip || {}, + options.cb); + } + + function _keys(obj, level, curLevel, curParent, + concatKeys, allKeys, leafKeys, levelKeys, + separator, res, uniqueKeys, skipKeys, cb) { + + var key, isLevel, isObj, tmp; + isLevel = curLevel === level; + for (key in obj) { + if (obj.hasOwnProperty(key)) { + + isObj = 'object' === typeof obj[key]; + if (allKeys || + (leafKeys && (isLevel || !isObj)) || + (levelKeys && isLevel)) { + + if (concatKeys) { + tmp = curParent + key; + if (!skipKeys[tmp]) { + if (cb) _doCb(tmp, res, cb); + else res.push(tmp); + } + } + else if (!skipKeys[key]) { + if (uniqueKeys){ + if (!uniqueKeys[key]) { + if (cb) _doCb(key, res, cb); + else res.push(key); + uniqueKeys[key] = true; + } + } + else { + if (cb) _doCb(key, res, cb); + else res.push(key); + } + } + } + if (isObj && (curLevel < level)) { + _keys(obj[key], level, (curLevel+1), + concatKeys ? curParent + key + separator : key, + concatKeys, allKeys, leafKeys, levelKeys, + separator, res, uniqueKeys, skipKeys, cb); } } } + return res; } - return result; - }; + + function _doCb(key, res, cb) { + var tmp; + tmp = cb(key); + // If string, substitute it. + if ('string' === typeof tmp || 'number' === typeof tmp) { + res.push(tmp); + } + // If array, expand it. + else if (JSUS.isArray(tmp) && tmp.length) { + if (tmp.length < 4) { + res.push(tmp[0]); + if (tmp.length > 1) { + res.push(tmp[1]); + if (tmp.length > 2) { + res.push(tmp[2]); + } + } + } + else { + (function() { + var i = -1, len = tmp.length; + for ( ; ++i < len ; ) { + res.push(tmp[i]); + } + })(tmp, res); + } + } + else if ('undefined' === typeof tmp) { + res.push(key); + } + // Else, e.g. null, ignore it. + } + })(); + /** * ## OBJ.implode @@ -5111,7 +5238,7 @@ }; /** - * ## OBJ.subobj + * ## OBJ.subobj | subObj * * Creates a copy of an object containing only the properties * passed as second parameter @@ -5129,7 +5256,7 @@ * * @see OBJ.getNestedValue */ - OBJ.subobj = function(o, select) { + OBJ.subobj = OBJ.subObj = function(o, select) { var out, i, key; if (!o) return false; out = {}; diff --git a/build/jsus.min.js b/build/jsus.min.js index b4810d2..f38c930 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e,n){var r;n="undefined"==typeof n?!0:n;if(n&&"undefined"==typeof t.clone)return t.log("JSUS.require: JSUS.clone not found, but clone requested. Cannot continue."),!1;if("undefined"==typeof e)r=t._classes;else{r=t._classes[e];if("undefined"==typeof r)return t.log("JSUS.require: could not find component "+e),!1}return n?t.clone(r):r},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=t.objGetAllKeys=function(e,n,r){var i,s;if(!e)return[];n="number"==typeof n&&n>=0?n:0,r="number"==typeof r&&r>=0?r:0,i=[];for(s in e)e.hasOwnProperty(s)&&(i.push(s),r=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=function(){function t(e,r,i,s,o,u,a,f,l,c,h,p,d){var v,m,g,y;m=i===r;for(v in e)if(e.hasOwnProperty(v)){g="object"==typeof e[v];if(u||a&&(m||!g)||f&&m)o?(y=s+v,p[y]||(d?n(y,c,d):c.push(y))):p[v]||(h?h[v]||(d?n(v,c,d):c.push(v),h[v]=!0):d?n(v,c,d):c.push(v));g&&i1&&(n.push(i[1]),i.length>2&&n.push(i[2]))):function(){var e=-1,t=i.length;for(;++e=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o Date: Sat, 21 Apr 2018 21:46:31 -0400 Subject: [PATCH 43/79] minor --- LICENSE | 2 +- README.md | 56 ------------------------------------------------------- 2 files changed, 1 insertion(+), 57 deletions(-) diff --git a/LICENSE b/LICENSE index 26c0955..054092c 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ (The MIT License) -Copyright (c) 2015 Stefano Balietti +Copyright (c) 2018 Stefano Balietti Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the diff --git a/README.md b/README.md index f6ee070..1f4ea13 100644 --- a/README.md +++ b/README.md @@ -17,29 +17,10 @@ Collection of general purpose javascript functions. JSUS helps! 9. COMPATIBILITY 10. QUEUE - -All methods of all libraries are available directly as -JSUS.[methodName]. - -```javascript -JSUS.seq(1,10,2); // [ 1, 3, 5, 7, 9 ]; -``` - -To get a copy of one of the registered JSUS libraries do: - -```javascript -var ARRAY = JSUS.require('ARRAY'); -ARRAY.seq(1,10,2); // [ 1, 3, 5, 7, 9 ]; -``` - ## Browser In the browser, two objects are exported: `JSUS` and its shorthand `J`. -## Full Documentation. - -Full API description available [here](http://nodegame.github.io/JSUS/docs/jsus.js.html). - ## Build Create your customized build of JSUS.js using the make file in the bin directory @@ -49,43 +30,6 @@ node make.js build -a // Full build node make.js build -l obj,array -o jsus-oa.js // Only object and array libs. ``` - -## API Documentation - -Create html API documentation using the make file in the bin directory - -```javascript -node make.js doc -``` - -## Make help - - Usage: make.jsus.js [options] [command] - - Commands: - - build [options] [options] - Creates a custom build of JSUS.js - - doc - Build documentation files - - Options: - - -h, --help output usage information - -V, --version output the version number - - - Usage: build [options] [options] - - Options: - - -h, --help output usage information - -l, --lib choose libraries to include - -A, --analyse analyse build - -a, --all full build of JSUS - -o, --output - ## License [MIT](LICENSE) From 76d30e944c375237598a7cd913957f1cf06bae87 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sat, 21 Apr 2018 21:47:37 -0400 Subject: [PATCH 44/79] v1 --- docs/doc-filelist.js | 1 - docs/doc-script.js | 223 --- docs/doc-style.css | 369 ---- docs/jsus.js.html | 379 ---- docs/lib/array.js.html | 1538 --------------- docs/lib/compatibility.js.html | 130 -- docs/lib/dom.js.html | 3269 -------------------------------- docs/lib/eval.js.html | 164 -- docs/lib/fs.js.html | 556 ------ docs/lib/obj.js.html | 2433 ------------------------ docs/lib/parse.js.html | 1271 ------------- docs/lib/queue.js.html | 359 ---- docs/lib/random.js.html | 788 -------- docs/lib/time.js.html | 301 --- docs/readme | 2 + 15 files changed, 2 insertions(+), 11781 deletions(-) delete mode 100644 docs/doc-filelist.js delete mode 100644 docs/doc-script.js delete mode 100644 docs/doc-style.css delete mode 100644 docs/jsus.js.html delete mode 100644 docs/lib/array.js.html delete mode 100644 docs/lib/compatibility.js.html delete mode 100644 docs/lib/dom.js.html delete mode 100644 docs/lib/eval.js.html delete mode 100644 docs/lib/fs.js.html delete mode 100644 docs/lib/obj.js.html delete mode 100644 docs/lib/parse.js.html delete mode 100644 docs/lib/queue.js.html delete mode 100644 docs/lib/random.js.html delete mode 100644 docs/lib/time.js.html create mode 100644 docs/readme diff --git a/docs/doc-filelist.js b/docs/doc-filelist.js deleted file mode 100644 index 7d48c91..0000000 --- a/docs/doc-filelist.js +++ /dev/null @@ -1 +0,0 @@ -var tree={"files":["jsus.js"],"dirs":{"lib":{"files":["array.js","compatibility.js","dom.js","eval.js","fs.js","obj.js","parse.js","queue.js","random.js","time.js"]}}}; \ No newline at end of file diff --git a/docs/doc-script.js b/docs/doc-script.js deleted file mode 100644 index 7649f78..0000000 --- a/docs/doc-script.js +++ /dev/null @@ -1,223 +0,0 @@ -// # res/script.js -// -// This is the script file that gets copied into the output. It mainly manages the display -// of the folder tree. The idea of this script file is to be minimal and standalone. So -// that means no jQuery. - -// Use localStorage to store data about the tree's state: whether or not -// the tree is visible and which directories are expanded. Unless the state -var sidebarVisible = (window.localStorage && window.localStorage.docker_showSidebar) ? - window.localStorage.docker_showSidebar == 'yes' : - defaultSidebar; - -/** - * ## makeTree - * - * Consructs the folder tree view - * - * @param {object} treeData Folder structure as in [queueFile](../src/docker.js.html#docker.prototype.queuefile) - * @param {string} root Path from current file to root (ie `'../../'` etc.) - * @param {string} filename The current file name - */ -function makeTree(treeData, root, filename){ - var treeNode = document.getElementById('tree'); - var treeHandle = document.getElementById('sidebar-toggle'); - treeHandle.addEventListener('click', toggleTree, false); - - // Build the html and add it to the container. - treeNode.innerHTML = nodeHtml('', treeData, '', root); - - // Root folder (whole tree) should always be open - treeNode.childNodes[0].className += ' open'; - - // Attach click event handler - treeNode.addEventListener('click', nodeClicked, false); - - if(sidebarVisible) document.body.className += ' sidebar'; - - // Restore scroll position from localStorage if set. And attach scroll handler - if(window.localStorage && window.localStorage.docker_treeScroll) treeNode.scrollTop = window.localStorage.docker_treeScroll; - treeNode.onscroll = treeScrolled; - - // Only set a class to allow CSS transitions after the tree state has been painted - setTimeout(function(){ document.body.className += ' slidey'; }, 100); -} - -/** - * ## treeScrolled - * - * Called when the tree is scrolled. Stores the scroll position in localStorage - * so it can be restored on the next pageview. - */ -function treeScrolled(){ - var tree = document.getElementById('tree'); - if(window.localStorage) window.localStorage.docker_treeScroll = tree.scrollTop; -} - -/** - * ## nodeClicked - * - * Called when a directory is clicked. Toggles open state of the directory - * - * @param {Event} e The click event - */ -function nodeClicked(e){ - - // Find the target - var t = e.target; - - // If the click target is actually a file (rather than a directory), ignore it - if(t.tagName.toLowerCase() !== 'div' || t.className === 'children') return; - - // Recurse upwards until we find the actual directory node - while(t && t.className.substring(0,3) != 'dir') t = t.parentNode; - - // If we're at the root node, then do nothing (we don't allow collapsing of the whole tree) - if(!t || t.parentNode.id == 'tree') return; - - // Find the path and toggle the state, saving the state in the localStorage variable - var path = t.getAttribute('rel'); - if(t.className.indexOf('open') !== -1){ - t.className=t.className.replace(/\s*open/g,''); - if(window.localStorage) window.localStorage.removeItem('docker_openPath:' + path); - }else{ - t.className += ' open'; - if(window.localStorage) window.localStorage['docker_openPath:' + path] = 'yes'; - } -} - - -/** - * ## nodeHtml - * - * Constructs the markup for a directory in the tree - * - * @param {string} nodename The node name. - * @param {object} node Node object of same format as whole tree. - * @param {string} path The path form the base to this node - * @param {string} root Relative path from current page to root - */ -function nodeHtml(nodename, node, path, root){ - // Firstly, figure out whether or not the directory is expanded from localStorage - var isOpen = window.localStorage && window.localStorage['docker_openPath:' + path] == 'yes'; - var out = '
'; - out += '
' + nodename + '
'; - out += '
'; - - // Loop through all child directories first - if(node.dirs){ - var dirs = []; - for(var i in node.dirs){ - if(node.dirs.hasOwnProperty(i)) dirs.push({ name: i, html: nodeHtml(i, node.dirs[i], path + i + '/', root) }); - } - // Have to store them in an array first and then sort them alphabetically here - dirs.sort(function(a, b){ return (a.name > b.name) ? 1 : (a.name == b.name) ? 0 : -1; }); - - for(var k = 0; k < dirs.length; k += 1) out += dirs[k].html; - } - - // Now loop through all the child files alphabetically - if(node.files){ - node.files.sort(); - for(var j = 0; j < node.files.length; j += 1){ - out += '' + node.files[j] + ''; - } - } - - // Close things off - out += '
'; - - return out; -} - -/** - * ## toggleTree - * - * Toggles the visibility of the folder tree - */ -function toggleTree(){ - // Do the actual toggling by modifying the class on the body element. That way we can get some nice CSS transitions going. - if(sidebarVisible){ - document.body.className = document.body.className.replace(/\s*sidebar/g,''); - sidebarVisible = false; - }else{ - document.body.className += ' sidebar'; - sidebarVisible = true; - } - if(window.localStorage){ - if(sidebarVisible){ - window.localStorage.docker_showSidebar = 'yes'; - }else{ - window.localStorage.docker_showSidebar = 'no'; - } - } -} - -/** - * ## wireUpTabs - * - * Wires up events on the sidebar tabe - */ -function wireUpTabs(){ - var tabEl = document.getElementById('sidebar_switch'); - var children = tabEl.childNodes; - - // Each tab has a class corresponding of the id of its tab pane - for(var i = 0, l = children.length; i < l; i += 1){ - // Ignore text nodes - if(children[i].nodeType !== 1) continue; - children[i].addEventListener('click', function(c){ - return function(){ switchTab(c); }; - }(children[i].className)); - } -} - -/** - * ## switchTab - * - * Switches tabs in the sidebar - * - * @param {string} tab The ID of the tab to switch to - */ -function switchTab(tab){ - var tabEl = document.getElementById('sidebar_switch'); - var children = tabEl.childNodes; - - // Easiest way to go through tabs without any kind of selector is just to look at the tab bar - for(var i = 0, l = children.length; i < l; i += 1){ - // Ignore text nodes - if(children[i].nodeType !== 1) continue; - - // Figure out what tab pane this tab button corresponts to - var t = children[i].className.replace(/\s.*$/,''); - if(t === tab){ - // Show the tab pane, select the tab button - document.getElementById(t).style.display = 'block'; - if(children[i].className.indexOf('selected') === -1) children[i].className += ' selected'; - }else{ - // Hide the tab pane, deselect the tab button - document.getElementById(t).style.display = 'none'; - children[i].className = children[i].className.replace(/\sselected/,''); - } - } - - // Store the last open tab in localStorage - if(window.localStorage) window.localStorage.docker_sidebarTab = tab; -} - -/** - * ## window.onload - * - * When the document is ready, make the sidebar and all that jazz - */ -window.onload = function(){ - makeTree(tree, relativeDir, thisFile); - wireUpTabs(); - - // Switch to the last viewed sidebar tab if stored, otherwise default to folder tree - if(window.localStorage && window.localStorage.docker_sidebarTab){ - switchTab(window.localStorage.docker_sidebarTab); - }else{ - switchTab('tree'); - } -}; \ No newline at end of file diff --git a/docs/doc-style.css b/docs/doc-style.css deleted file mode 100644 index d205080..0000000 --- a/docs/doc-style.css +++ /dev/null @@ -1,369 +0,0 @@ -body { - font-family: "Palatino Linotype", "Book Antiqua", Palatino, FreeSerif, serif; - font-size: 15px; - line-height: 22px; - margin: 0; - padding: 0; } - -p, h1, h2, h3, h4, h5, h6 { - margin: 0 0 15px 0; } - -h1 { - margin-top: 40px; } - -#tree, #headings { - position: absolute; - top: 30px; - left: 0; - bottom: 0; - width: 290px; - padding: 10px 0; - overflow: auto; } - -#sidebar_wrapper { - position: fixed; - top: 0; - left: 0; - bottom: 0; - width: 0; - overflow: hidden; } - -#sidebar_switch { - position: absolute; - top: 0; - left: 0; - width: 290px; - height: 29px; - border-bottom: 1px solid; } - #sidebar_switch span { - display: block; - float: left; - width: 50%; - text-align: center; - line-height: 29px; - cursor: pointer; } - #sidebar_switch .selected { - font-weight: bold; } - -.slidey #sidebar_wrapper { - -webkit-transition: width 250ms linear; - -moz-transition: width 250ms linear; - -ms-transition: width 250ms linear; - -o-transition: width 250ms linear; - transition: width 250ms linear; } - -.sidebar #sidebar_wrapper { - width: 290px; } - -#tree .nodename { - text-indent: 12px; - background: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAg0lEQVQYlWNIS0tbAcSK////Z8CHGTIzM7+mp6d/ASouwqswKyvrO1DRfyg+CcRaxCgE4Z9A3AjEbIQUgjHQOQvwKgS6+ffChQt3AiUDcCqsra29d/v27R6ghCVWN2ZnZ/9YuXLlRqBAPBALYvVMR0fHmQcPHrQBOUZ4gwfqFj5CAQ4Al6wLIYDwo9QAAAAASUVORK5CYII=); - background-repeat: no-repeat; - background-position: left center; - cursor: pointer; } -#tree .open > .nodename { - background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAYAAACNMs+9AAAAlElEQVQYlWNIS0tbCsT/8eCN////Z2B49OhRfHZ29jdsioDiP27evJkNVggkONeuXbscm8Jly5atA8rzwRSCsG5DQ8MtZEU1NTUPgOLGUHm4QgaQFVlZWT9BijIzM39fuHChDCaHohBkBdCq9SCF8+bN2wHkC+FSCMLGkyZNOvb9+3dbNHEMhSDsDsRMxCjEiolWCADeUBHgU/IGQQAAAABJRU5ErkJggg==); - background-position: left 7px; } -#tree .dir, #tree .file { - position: relative; - min-height: 20px; - line-height: 20px; - padding-left: 12px; } - #tree .dir > .children, #tree .file > .children { - display: none; } - #tree .dir.open > .children, #tree .file.open > .children { - display: block; } -#tree .file { - padding-left: 24px; - display: block; - text-decoration: none; } -#tree > .dir { - padding-left: 0; } - -#headings .heading a { - text-decoration: none; - padding-left: 10px; - display: block; } -#headings .h1 { - padding-left: 0; - margin-top: 10px; - font-size: 1.3em; } -#headings .h2 { - padding-left: 10px; - margin-top: 8px; - font-size: 1.1em; } -#headings .h3 { - padding-left: 20px; - margin-top: 5px; - font-size: 1em; } -#headings .h4 { - padding-left: 30px; - margin-top: 3px; - font-size: 0.9em; } -#headings .h5 { - padding-left: 40px; - margin-top: 1px; - font-size: 0.8em; } -#headings .h6 { - padding-left: 50px; - font-size: 0.75em; } - -#sidebar-toggle { - position: fixed; - top: 0; - left: 0; - width: 5px; - bottom: 0; - z-index: 2; - cursor: pointer; } - #sidebar-toggle:hover { - width: 10px; } - -.slidey #sidebar-toggle, .slidey #container { - -webkit-transition: all 250ms linear; - -moz-transition: all 250ms linear; - -ms-transition: all 250ms linear; - -o-transition: all 250ms linear; - transition: all 250ms linear; } - -.sidebar #sidebar-toggle { - left: 290px; } - -#container { - position: fixed; - left: 5px; - right: 0; - top: 0; - bottom: 0; - overflow: auto; } - -.sidebar #container { - left: 295px; } - -.no-sidebar #sidebar_wrapper, .no-sidebar #sidebar-toggle { - display: none; } -.no-sidebar #container { - left: 0; } - -#page { - padding-top: 40px; } - -table td { - border: 0; - outline: 0; } - -.docs.markdown { - padding: 10px 50px; } - -td.docs { - max-width: 450px; - min-width: 450px; - min-height: 5px; - padding: 10px 25px 1px 50px; - overflow-x: hidden; - vertical-align: top; - text-align: left; } - -.docs pre { - margin: 15px 0 15px; - padding: 5px; - padding-left: 10px; - border: 1px solid; - font-size: 12px; - overflow: auto; } - .docs pre.code_stats { - font-size: 60%; } -.docs p tt, .docs p code, .docs li tt, .docs li code { - border: 1px solid; - font-size: 12px; - padding: 0 0.2em; } - -.dox { - border-top: 1px solid; - padding-top: 10px; - padding-bottom: 10px; } - .dox .details { - padding: 10px; - border: 1px solid; - margin-bottom: 10px; } - .dox .dox_tag_title { - font-weight: bold; } - .dox .dox_tag_detail { - margin-left: 10px; } - .dox .dox_tag_detail span { - margin-right: 5px; } - .dox .dox_type { - font-style: italic; } - .dox .dox_tag_name { - font-weight: bold; } - -.pilwrap { - position: relative; - padding-top: 1px; } - .pilwrap .pilcrow { - font: 12px Arial; - text-decoration: none; - color: #454545; - position: absolute; - top: 3px; - left: -20px; - padding: 1px 2px; - opacity: 0; - -webkit-transition: opacity 0.2s linear; - -moz-transition: opacity 0.2s linear; - -ms-transition: opacity 0.2s linear; - -o-transition: opacity 0.2s linear; - transition: opacity 0.2s linear; } - .pilwrap:hover .pilcrow { - opacity: 1; } - -td.code { - padding: 8px 15px 8px 25px; - width: 100%; - vertical-align: bottom; - border-left: 1px solid; } - -.background { - border-left: 1px solid; - position: absolute; - z-index: -1; - top: 0; - right: 0; - bottom: 0; - left: 525px; } - -pre, tt, code { - font-size: 12px; - line-height: 18px; - font-family: Menlo, Monaco, Consolas, "Lucida Console", monospace; - margin: 0; - padding: 0; } - -.line-num { - display: inline-block; - width: 50px; - text-align: right; - opacity: 0.3; - margin-left: -20px; - text-decoration: none; } - -/* All the stuff that can depend on colour scheme goes below here: */ -body { - background: white; - color: #2f2f24; } - -a { - color: #261a3b; } - a:visited { - color: #261a3b; } - -#sidebar_wrapper { - background: #f4f4f3; } - -#sidebar_switch { - background: #ededec; - border-bottom-color: #dededc; } - #sidebar_switch span { - color: #2d2d22; } - #sidebar_switch span:hover { - background: #f4f4f3; } - #sidebar_switch .selected { - background: #fafafa; - color: #252519; } - -#tree .file { - color: #252519; } - -#headings .heading a { - color: #252519; } - -#sidebar-toggle { - background: #e9e9e8; } - #sidebar-toggle:hover { - background: #dededc; } - -.docs.markdown { - background: white; } -.docs pre { - border-color: #dededc; } -.docs p tt, .docs p code, .docs li tt, .docs li code { - border-color: #dededc; - background: #f4f4f3; } - -.highlight { - background: #f4f4f3; - color: auto; } - -.dox { - border-top-color: #e6e6e4; } - .dox .details { - background: #f4f4f3; - border-color: #dededc; } - -.pilwrap .pilcrow { - color: #3a3a2f; } - -td.code, .background { - border-left-color: #dededc; } -body .highlight .hll { background-color: #ffffcc } -body .highlight { background: #f8f8f8; } -body .highlight .c { color: #408080; font-style: italic } /* Comment */ -body .highlight .err { border: 1px solid #FF0000 } /* Error */ -body .highlight .k { color: #008000; font-weight: bold } /* Keyword */ -body .highlight .o { color: #666666 } /* Operator */ -body .highlight .cm { color: #408080; font-style: italic } /* Comment.Multiline */ -body .highlight .cp { color: #BC7A00 } /* Comment.Preproc */ -body .highlight .c1 { color: #408080; font-style: italic } /* Comment.Single */ -body .highlight .cs { color: #408080; font-style: italic } /* Comment.Special */ -body .highlight .gd { color: #A00000 } /* Generic.Deleted */ -body .highlight .ge { font-style: italic } /* Generic.Emph */ -body .highlight .gr { color: #FF0000 } /* Generic.Error */ -body .highlight .gh { color: #000080; font-weight: bold } /* Generic.Heading */ -body .highlight .gi { color: #00A000 } /* Generic.Inserted */ -body .highlight .go { color: #808080 } /* Generic.Output */ -body .highlight .gp { color: #000080; font-weight: bold } /* Generic.Prompt */ -body .highlight .gs { font-weight: bold } /* Generic.Strong */ -body .highlight .gu { color: #800080; font-weight: bold } /* Generic.Subheading */ -body .highlight .gt { color: #0040D0 } /* Generic.Traceback */ -body .highlight .kc { color: #008000; font-weight: bold } /* Keyword.Constant */ -body .highlight .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */ -body .highlight .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */ -body .highlight .kp { color: #008000 } /* Keyword.Pseudo */ -body .highlight .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */ -body .highlight .kt { color: #B00040 } /* Keyword.Type */ -body .highlight .m { color: #666666 } /* Literal.Number */ -body .highlight .s { color: #BA2121 } /* Literal.String */ -body .highlight .na { color: #7D9029 } /* Name.Attribute */ -body .highlight .nb { color: #008000 } /* Name.Builtin */ -body .highlight .nc { color: #0000FF; font-weight: bold } /* Name.Class */ -body .highlight .no { color: #880000 } /* Name.Constant */ -body .highlight .nd { color: #AA22FF } /* Name.Decorator */ -body .highlight .ni { color: #999999; font-weight: bold } /* Name.Entity */ -body .highlight .ne { color: #D2413A; font-weight: bold } /* Name.Exception */ -body .highlight .nf { color: #0000FF } /* Name.Function */ -body .highlight .nl { color: #A0A000 } /* Name.Label */ -body .highlight .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */ -body .highlight .nt { color: #008000; font-weight: bold } /* Name.Tag */ -body .highlight .nv { color: #19177C } /* Name.Variable */ -body .highlight .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */ -body .highlight .w { color: #bbbbbb } /* Text.Whitespace */ -body .highlight .mf { color: #666666 } /* Literal.Number.Float */ -body .highlight .mh { color: #666666 } /* Literal.Number.Hex */ -body .highlight .mi { color: #666666 } /* Literal.Number.Integer */ -body .highlight .mo { color: #666666 } /* Literal.Number.Oct */ -body .highlight .sb { color: #BA2121 } /* Literal.String.Backtick */ -body .highlight .sc { color: #BA2121 } /* Literal.String.Char */ -body .highlight .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */ -body .highlight .s2 { color: #BA2121 } /* Literal.String.Double */ -body .highlight .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */ -body .highlight .sh { color: #BA2121 } /* Literal.String.Heredoc */ -body .highlight .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */ -body .highlight .sx { color: #008000 } /* Literal.String.Other */ -body .highlight .sr { color: #BB6688 } /* Literal.String.Regex */ -body .highlight .s1 { color: #BA2121 } /* Literal.String.Single */ -body .highlight .ss { color: #19177C } /* Literal.String.Symbol */ -body .highlight .bp { color: #008000 } /* Name.Builtin.Pseudo */ -body .highlight .vc { color: #19177C } /* Name.Variable.Class */ -body .highlight .vg { color: #19177C } /* Name.Variable.Global */ -body .highlight .vi { color: #19177C } /* Name.Variable.Instance */ -body .highlight .il { color: #666666 } /* Literal.Number.Integer.Long */ diff --git a/docs/jsus.js.html b/docs/jsus.js.html deleted file mode 100644 index 56800bd..0000000 --- a/docs/jsus.js.html +++ /dev/null @@ -1,379 +0,0 @@ - - - - jsus.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - JSUS: JavaScript UtilS. -

-
- - -

Copyright(c) 2015 Stefano Balietti -MIT Licensed

-
-

Collection of general purpose javascript functions. JSUS helps!

- - -
-

- - See README.md for extra help. -

-
- -
-
-
(function(exports) {
-
-    var JSUS = exports.JSUS = {};
-
-
-

- - JSUS._classes -

-
- - -

Reference to all the extensions

-
    JSUS._classes = {};
-
-
- -
-

Make sure that the console is available also in old browser, e.g. < IE8.

-
    if ('undefined' === typeof console) console = {};
-    if ('undefined' === typeof console.log) console.log = function() {};
-
-
-
-

- - JSUS.log -

-
- -
-

Reference to standard out, by default console.log

- -

Override to redirect the standard output of all JSUS functions.

-
-
-
Params
-
- txt - string - Text to output -
-
-
-
    JSUS.log = function(txt) {
-        console.log(txt);
-    };
-
-
-
-

- - JSUS.extend -

-
- -
-

Extends JSUS with additional methods and or properties

- -

The first parameter can be an object literal or a function. -A reference of the original extending object is stored in -JSUS._classes

- -

If a second parameter is passed, that will be the target of the -extension.

-
-
-
Params
-
- additional - object - Text to output -
-
- target - object - function - The object to extend -
-
Returns
-
- - object - function - target The extended object -
-
See
-
- JSUS.get - -
-
-
-
    JSUS.extend = function(additional, target) {
-        var name, prop;
-        if ('object' !== typeof additional &&
-            'function' !== typeof additional) {
-            return target;
-        }
-
-
- -
-

If we are extending JSUS, store a reference -of the additional object into the hidden -JSUS._classes object;

-
        if ('undefined' === typeof target) {
-            target = target || this;
-            if ('function' === typeof additional) {
-                name = additional.toString();
-                name = name.substr('function '.length);
-                name = name.substr(0, name.indexOf('('));
-            }
-
-
- -
-

Must be object.

-
            else {
-                name = additional.constructor ||
-                    additional.__proto__.constructor;
-            }
-            if (name) {
-                this._classes[name] = additional;
-            }
-        }
-
-        for (prop in additional) {
-            if (additional.hasOwnProperty(prop)) {
-                if (typeof target[prop] !== 'object') {
-                    target[prop] = additional[prop];
-                } else {
-                    JSUS.extend(additional[prop], target[prop]);
-                }
-            }
-        }
-
-
- -
-

Additional is a class (Function) -TODO: this is true also for {}

-
        if (additional.prototype) {
-            JSUS.extend(additional.prototype, target.prototype || target);
-        }
-
-        return target;
-    };
-
-
-
-

- - JSUS.require -

-
- -
-

Returns a copy of one / all the objects extending JSUS

- -

The first parameter is a string representation of the name of -the requested extending object. If no parameter is passed a copy -of all the extending objects is returned.

-
-
-
Params
-
- className - string - The name of the requested JSUS library -
-
Returns
-
- - function - boolean - The copy of the JSUS library, or - FALSE if the library does not exist -
-
-
-
    JSUS.require = JSUS.get = function(className) {
-        if ('undefined' === typeof JSUS.clone) {
-            JSUS.log('JSUS.clone not found. Cannot continue.');
-            return false;
-        }
-        if ('undefined' === typeof className) return JSUS.clone(JSUS._classes);
-        if ('undefined' === typeof JSUS._classes[className]) {
-            JSUS.log('Could not find class ' + className);
-            return false;
-        }
-        return JSUS.clone(JSUS._classes[className]);
-    };
-
-
-
-

- - JSUS.isNodeJS -

-
- -
-

Returns TRUE when executed inside Node.JS environment

-
-
-
Returns
-
- - boolean - TRUE when executed inside Node.JS environment -
-
-
-
    JSUS.isNodeJS = function() {
-        return 'undefined' !== typeof module &&
-            'undefined' !== typeof module.exports &&
-            'function' === typeof require;
-    };
-
-
-

- - Node.JS includes -

-
- - -

if node

-
    if (JSUS.isNodeJS()) {
-        require('./lib/compatibility');
-        require('./lib/obj');
-        require('./lib/array');
-        require('./lib/time');
-        require('./lib/eval');
-        require('./lib/dom');
-        require('./lib/random');
-        require('./lib/parse');
-        require('./lib/queue');
-        require('./lib/fs');
-    }
-
-
- -
-

end node

-
})(
-    'undefined' !== typeof module && 'undefined' !== typeof module.exports ?
-        module.exports: window
-);
-
-
-
- - diff --git a/docs/lib/array.js.html b/docs/lib/array.js.html deleted file mode 100644 index 559a5d1..0000000 --- a/docs/lib/array.js.html +++ /dev/null @@ -1,1538 +0,0 @@ - - - - array.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - ARRAY -

-
- - -

Copyright(c) 2016 Stefano Balietti -MIT Licensed

-
-

Collection of static functions to manipulate arrays

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    function ARRAY() {}
-
-
-
-

- - ARRAY.filter -

-
- -
-

Add the filter method to ARRAY objects in case the method is not -supported natively.

-
- -
-
    if (!Array.prototype.filter) {
-        Array.prototype.filter = function(fun /*, thisp */) {
-            if (this === void 0 || this === null) throw new TypeError();
-
-            var t = new Object(this);
-            var len = t.length >>> 0;
-            if (typeof fun !== "function") throw new TypeError();
-
-            var res = [];
-            var thisp = arguments[1];
-            for (var i = 0; i < len; i++) {
-                if (i in t) {
-                    var val = t[i]; // in case fun mutates this
-                    if (fun.call(thisp, val, i, t)) {
-                        res.push(val);
-                    }
-                }
-            }
-            return res;
-        };
-    }
-
-
-
-

- - ARRAY.isArray -

-
- -
-

Returns TRUE if a variable is an Array

- -

This method is exactly the same as Array.isArray, -but it works on a larger share of browsers.

-
-
-
Params
-
- o - object - The variable to check. -
-
See
-
- Array.isArray - -
-
-
-
    ARRAY.isArray = (function(f) {
-        if ('function' === typeof f) return f;
-        else return function(o) {
-            if (!o) return false;
-            return Object.prototype.toString.call(o) === '[object Array]';
-        };
-    })(Array.isArray);
-
-
-
-

- - ARRAY.seq -

-
- -
-

Returns an array of sequential numbers from start to end

- -

If start > end the series goes backward.

- -

The distance between two subsequent numbers can be controlled -by the increment parameter.

- -

When increment is not a divider of Abs(start - end), end will -be missing from the series.

- -

A callback function to apply to each element of the sequence -can be passed as fourth parameter.

- -

Returns FALSE, in case parameters are incorrectly specified

-
-
-
Params
-
- start - number - The first element of the sequence -
-
- end - number - The last element of the sequence -
-
- increment - number - Optional. The increment between two - subsequents element of the sequence -
-
- func - Function - Optional. A callback function that can modify - each number of the sequence before returning it -
-
Returns
-
- - array - The final sequence -
-
-
-
    ARRAY.seq = function(start, end, increment, func) {
-        var i, out;
-        if ('number' !== typeof start) return false;
-        if (start === Infinity) return false;
-        if ('number' !== typeof end) return false;
-        if (end === Infinity) return false;
-        if (start === end) return [start];
-
-        if (increment === 0) return false;
-        if (!JSUS.inArray(typeof increment, ['undefined', 'number'])) {
-            return false;
-        }
-
-        increment = increment || 1;
-        func = func || function(e) {return e;};
-
-        i = start;
-        out = [];
-
-        if (start < end) {
-            while (i <= end) {
-                out.push(func(i));
-                i = i + increment;
-            }
-        }
-        else {
-            while (i >= end) {
-                out.push(func(i));
-                i = i - increment;
-            }
-        }
-
-        return out;
-    };
-
-
-
-

- - ARRAY.each -

-
- -
-

Executes a callback on each element of the array

- -

If an error occurs returns FALSE.

-
-
-
Params
-
- array - array - The array to loop in -
-
- func - Function - The callback for each element in the array -
-
- context - object - Optional. The context of execution of the - callback. Defaults ARRAY.each -
-
Returns
-
- - boolean - TRUE, if execution was successful -
-
-
-
    ARRAY.each = function(array, func, context) {
-        if ('object' !== typeof array) return false;
-        if (!func) return false;
-
-        context = context || this;
-        var i, len = array.length;
-        for (i = 0 ; i < len; i++) {
-            func.call(context, array[i]);
-        }
-        return true;
-    };
-
-
-
-

- - ARRAY.map -

-
- -
-

Executes a callback to each element of the array and returns the result

- -

Any number of additional parameters can be passed after the -callback function.

-
-
-
Returns
-
- - array - The result of the mapping execution -
-
See
-
- ARRAY.each - -
-
-
-
    ARRAY.map = function() {
-        var i, len, args, out, o;
-        var array, func;
-
-        array = arguments[0];
-        func = arguments[1];
-
-        if (!ARRAY.isArray(array)) {
-            JSUS.log('ARRAY.map: first parameter must be array. Found: ' +
-                     array);
-            return;
-        }
-        if ('function' !== typeof func) {
-            JSUS.log('ARRAY.map: second parameter must be function. Found: ' +
-                     func);
-            return;
-        }
-
-        len = arguments.length;
-        if (len === 3) args = [null, arguments[2]];
-        else if (len === 4) args = [null, arguments[2], arguments[3]];
-        else {
-            len = len - 1;
-            args = new Array(len);
-            for (i = 1; i < (len); i++) {
-                args[i] = arguments[i+1];
-            }
-        }
-
-        out = [], len = array.length;
-        for (i = 0; i < len; i++) {
-            args[0] = array[i];
-            o = func.apply(this, args);
-            if ('undefined' !== typeof o) out.push(o);
-        }
-        return out;
-    };
-
-
-
-

- - ARRAY.removeElement -

-
- -
-

Removes an element from the the array, and returns it

- -

For objects, deep equality comparison is performed -through JSUS.equals.

- -

If no element is removed returns FALSE.

-
-
-
Params
-
- needle - mixed - The element to search in the array -
-
- haystack - array - The array to search in -
-
Returns
-
- - mixed - The element that was removed, FALSE if none was removed -
-
See
-
- JSUS.equals - -
-
-
-
    ARRAY.removeElement = function(needle, haystack) {
-        var func, i;
-        if ('undefined' === typeof needle || !haystack) return false;
-
-        if ('object' === typeof needle) {
-            func = JSUS.equals;
-        }
-        else {
-            func = function(a, b) {
-                return (a === b);
-            };
-        }
-
-        for (i = 0; i < haystack.length; i++) {
-            if (func(needle, haystack[i])){
-                return haystack.splice(i,1);
-            }
-        }
-        return false;
-    };
-
-
-
-

- - ARRAY.inArray -

-
- -
-

Returns TRUE if the element is contained in the array, -FALSE otherwise

- -

For objects, deep equality comparison is performed -through JSUS.equals.

-
-
-
Params
-
- needle - mixed - The element to search in the array -
-
- haystack - array - The array to search in -
-
Returns
-
- - boolean - TRUE, if the element is contained in the array -
-
See
-
- JSUS.equals - -
-
-
-
    ARRAY.inArray = function(needle, haystack) {
-        var func, i, len;
-        if (!haystack) return false;
-        func = JSUS.equals;
-        len = haystack.length;
-        for (i = 0; i < len; i++) {
-            if (func.call(this, needle, haystack[i])) {
-                return true;
-            }
-        }
-        return false;
-    };
-
-    ARRAY.in_array = function(needle, haystack) {
-        console.log('***ARRAY.in_array is deprecated. ' +
-                    'Use ARRAY.inArray instead.***');
-        return ARRAY.inArray(needle, haystack);
-    };
-
-
-
-

- - ARRAY.getNGroups -

-
- -
-

Returns an array of N array containing the same number of elements -If the length of the array and the desired number of elements per group -are not multiple, the last group could have less elements

- -

The original array is not modified.

- -

@see ARRAY.getGroupsSizeN - @see ARRAY.generateCombinations - @see ARRAY.matchN

-
-
-
Params
-
- array - array - The array to split in subgroups -
-
- N - number - The number of subgroups -
-
Returns
-
- - array - Array containing N groups -
-
-
-
    ARRAY.getNGroups = function(array, N) {
-        return ARRAY.getGroupsSizeN(array, Math.floor(array.length / N));
-    };
-
-
-
-

- - ARRAY.getGroupsSizeN -

-
- -
-

Returns an array of arrays containing N elements each

- -

The last group could have less elements

-
-
-
Params
-
- array - array - The array to split in subgroups -
-
- N - number - The number of elements in each subgroup -
-
Returns
-
- - array - Array containing groups of size N -
-
See
-
- ARRAY.getNGroups -
-
See
-
- ARRAY.generateCombinations -
-
See
-
- ARRAY.matchN - -
-
-
-
    ARRAY.getGroupsSizeN = function(array, N) {
-
-        var copy = array.slice(0);
-        var len = copy.length;
-        var originalLen = copy.length;
-        var result = [];
-
-
- -
-

Init values for the loop algorithm.

-
        var i, idx;
-        var group = [], count = 0;
-        for (i=0; i < originalLen; i++) {
-
-
- -
-

Get a random idx between 0 and array length.

-
            idx = Math.floor(Math.random()*len);
-
-
- -
-

Prepare the array container for the elements of a new group.

-
            if (count >= N) {
-                result.push(group);
-                count = 0;
-                group = [];
-            }
-
-
- -
-

Insert element in the group.

-
            group.push(copy[idx]);
-
-
- -
-

Update.

-
            copy.splice(idx,1);
-            len = copy.length;
-            count++;
-        }
-
-
- -
-

Add any remaining element.

-
        if (group.length > 0) {
-            result.push(group);
-        }
-
-        return result;
-    };
-
-
-
-

- - ARRAY._latinSquare -

-
- -
-

Generate a random Latin Square of size S

- -

If N is defined, it returns "Latin Rectangle" (SxN)

- -

A parameter controls for self-match, i.e. whether the symbol "i" -is found or not in in column "i".

-
-
-
API
-
- private - -
Params
-
- S - number - The number of rows -
-
- Optional. - number - N The number of columns. Defaults N = S -
-
- Optional. - boolean - If TRUE self-match is allowed. Defaults TRUE -
-
Returns
-
- - array - The resulting latin square (or rectangle) -
-
-
-
    ARRAY._latinSquare = function(S, N, self) {
-        self = ('undefined' === typeof self) ? true : self;
-
-
- -
-

Infinite loop.

-
        if (S === N && !self) return false;
-        var seq = [];
-        var latin = [];
-        for (var i=0; i< S; i++) {
-            seq[i] = i;
-        }
-
-        var idx = null;
-
-        var start = 0;
-        var limit = S;
-        var extracted = [];
-        if (!self) {
-            limit = S-1;
-        }
-
-        for (i=0; i < N; i++) {
-            do {
-                idx = JSUS.randomInt(start,limit);
-            }
-            while (JSUS.inArray(idx, extracted));
-            extracted.push(idx);
-
-            if (idx == 1) {
-                latin[i] = seq.slice(idx);
-                latin[i].push(0);
-            }
-            else {
-                latin[i] = seq.slice(idx).concat(seq.slice(0,(idx)));
-            }
-
-        }
-
-        return latin;
-    };
-
-
-
-

- - ARRAY.latinSquare -

-
- -
-

Generate a random Latin Square of size S

- -

If N is defined, it returns "Latin Rectangle" (SxN)

-
-
-
Params
-
- S - number - The number of rows -
-
- Optional. - number - N The number of columns. Defaults N = S -
-
Returns
-
- - array - The resulting latin square (or rectangle) -
-
-
-
    ARRAY.latinSquare = function(S, N) {
-        if (!N) N = S;
-        if (!S || S < 0 || (N < 0)) return false;
-        if (N > S) N = S;
-
-        return ARRAY._latinSquare(S, N, true);
-    };
-
-
-
-

- - ARRAY.latinSquareNoSelf -

-
- -
-

Generate a random Latin Square of size Sx(S-1), where -in each column "i", the symbol "i" is not found

- -

If N < S, it returns a "Latin Rectangle" (SxN)

-
-
-
Params
-
- S - number - The number of rows -
-
- Optional. - number - N The number of columns. Defaults N = S-1 -
-
Returns
-
- - array - The resulting latin square (or rectangle) -
-
-
-
    ARRAY.latinSquareNoSelf = function(S, N) {
-        if (!N) N = S-1;
-        if (!S || S < 0 || (N < 0)) return false;
-        if (N > S) N = S-1;
-
-        return ARRAY._latinSquare(S, N, false);
-    };
-
-
-
-

- - ARRAY.generateCombinations -

-
- -
-

Generates all distinct combinations of exactly r elements each

-
-
-
Params
-
- array - array - The array from which the combinations are extracted -
-
- r - number - The number of elements in each combination -
-
Returns
-
- - array - The total sets of combinations -
-
See
-
- ARRAY.getGroupSizeN -
-
See
-
- ARRAY.getNGroups -
-
See
- -
-
-
    ARRAY.generateCombinations = function combinations(arr, k) {
-        var i, subI, ret, sub, next;
-        ret = [];
-        for (i = 0; i < arr.length; i++) {
-            if (k === 1) {
-                ret.push( [ arr[i] ] );
-            }
-            else {
-                sub = combinations(arr.slice(i+1, arr.length), k-1);
-                for (subI = 0; subI < sub.length; subI++ ){
-                    next = sub[subI];
-                    next.unshift(arr[i]);
-                    ret.push( next );
-                }
-            }
-        }
-        return ret;
-    };
-
-
-
-

- - ARRAY.matchN -

-
- -
-

Match each element of the array with N random others

- -

If strict is equal to true, elements cannot be matched multiple times.

- -

Important: this method has a bug / feature. If the strict parameter -is set, the last elements could remain without match, because all the -other have been already used. Another recombination would be able -to match all the elements instead.

-
-
-
Params
-
- array - array - The array in which operate the matching -
-
- N - number - The number of matches per element -
-
- strict - boolean - Optional. If TRUE, matched elements cannot be - repeated. Defaults, FALSE -
-
Returns
-
- - array - The results of the matching -
-
See
-
- ARRAY.getGroupSizeN -
-
See
-
- ARRAY.getNGroups -
-
See
-
- ARRAY.generateCombinations - -
-
-
-
    ARRAY.matchN = function(array, N, strict) {
-        var result, i, copy, group, len, found;
-        if (!array) return;
-        if (!N) return array;
-
-        result = [];
-        len = array.length;
-        found = [];
-        for (i = 0 ; i < len ; i++) {
-
-
- -
-

Recreate the array.

-
            copy = array.slice(0);
-            copy.splice(i,1);
-            if (strict) {
-                copy = ARRAY.arrayDiff(copy,found);
-            }
-            group = ARRAY.getNRandom(copy,N);
-
-
- -
-

Add to the set of used elements.

-
            found = found.concat(group);
-
-
- -
-

Re-add the current element.

-
            group.splice(0,0,array[i]);
-            result.push(group);
-
-
- -
-

Update.

-
            group = [];
-        }
-        return result;
-    };
-
-
-
-

- - ARRAY.rep -

-
- -
-

Appends an array to itself a number of times and return a new array

- -

The original array is not modified.

-
-
-
Params
-
- array - array - the array to repeat -
-
- times - number - The number of times the array must be appended - to itself -
-
Returns
-
- - array - A copy of the original array appended to itself -
-
-
-
    ARRAY.rep = function(array, times) {
-        var i, result;
-        if (!array) return;
-        if (!times) return array.slice(0);
-        if (times < 1) {
-            JSUS.log('times must be greater or equal 1', 'ERR');
-            return;
-        }
-
-        i = 1;
-        result = array.slice(0);
-        for (; i < times; i++) {
-            result = result.concat(array);
-        }
-        return result;
-    };
-
-
-
-

- - ARRAY.stretch -

-
- -
-

Repeats each element of the array N times

- -

N can be specified as an integer or as an array. In the former case all -the elements are repeat the same number of times. In the latter, each -element can be repeated a custom number of times. If the length of the -times array differs from that of the array to stretch a recycle rule -is applied.

- -

The original array is not modified.

- -

E.g.:

- - -
 var foo = [1,2,3];
-
- ARRAY.stretch(foo, 2); // [1, 1, 2, 2, 3, 3]
-
- ARRAY.stretch(foo, [1,2,3]); // [1, 2, 2, 3, 3, 3];
-
- ARRAY.stretch(foo, [2,1]); // [1, 1, 2, 3, 3];
-
- - -
-
-
Params
-
- array - array - the array to strech -
-
- times - number - array - The number of times each element - must be repeated -
-
Returns
-
- - array - A stretched copy of the original array -
-
-
-
    ARRAY.stretch = function(array, times) {
-        var result, i, repeat, j;
-        if (!array) return;
-        if (!times) return array.slice(0);
-        if ('number' === typeof times) {
-            if (times < 1) {
-                JSUS.log('times must be greater or equal 1', 'ERR');
-                return;
-            }
-            times = ARRAY.rep([times], array.length);
-        }
-
-        result = [];
-        for (i = 0; i < array.length; i++) {
-            repeat = times[(i % times.length)];
-            for (j = 0; j < repeat ; j++) {
-                result.push(array[i]);
-            }
-        }
-        return result;
-    };
-
-
-
-

- - ARRAY.arrayIntersect -

-
- -
-

Computes the intersection between two arrays

- -

Arrays can contain both primitive types and objects.

-
-
-
Params
-
- a1 - array - The first array -
-
- a2 - array - The second array -
-
Returns
-
- - array - All the values of the first array that are found - also in the second one -
-
-
-
    ARRAY.arrayIntersect = function(a1, a2) {
-        return a1.filter( function(i) {
-            return JSUS.inArray(i, a2);
-        });
-    };
-
-
-
-

- - ARRAY.arrayDiff -

-
- -
-

Performs a diff between two arrays

- -

Arrays can contain both primitive types and objects.

-
-
-
Params
-
- a1 - array - The first array -
-
- a2 - array - The second array -
-
Returns
-
- - array - All the values of the first array that are not - found in the second one -
-
-
-
    ARRAY.arrayDiff = function(a1, a2) {
-        return a1.filter( function(i) {
-            return !(JSUS.inArray(i, a2));
-        });
-    };
-
-
-
-

- - ARRAY.shuffle -

-
- -
-

Shuffles the elements of the array using the Fischer algorithm

- -

The original array is not modified, and a copy is returned.

-
-
-
Params
-
- shuffle - array - The array to shuffle -
-
Returns
-
- - array - copy The shuffled array -
-
See
- -
-
-
    ARRAY.shuffle = function(array) {
-        var copy, len, j, tmp, i;
-        if (!array) return;
-        copy = Array.prototype.slice.call(array);
-        len = array.length-1; // ! -1
-        for (i = len; i > 0; i--) {
-            j = Math.floor(Math.random()*(i+1));
-            tmp = copy[j];
-            copy[j] = copy[i];
-            copy[i] = tmp;
-        }
-        return copy;
-    };
-
-
-
-

- - ARRAY.getNRandom -

-
- -
-

Select N random elements from the array and returns them

-
-
-
Params
-
- array - array - The array from which extracts random elements -
-
Returns
-
- - array - An new array with N elements randomly chosen -
-
-
-
    ARRAY.getNRandom = function(array, N) {
-        return ARRAY.shuffle(array).slice(0,N);
-    };
-
-
-
-

- - ARRAY.distinct -

-
- -
-

Removes all duplicates entries from an array and returns a copy of it

- -

Does not modify original array.

- -

Comparison is done with JSUS.equals.

-
-
-
Params
-
- array - array - The array from which eliminates duplicates -
-
Returns
-
- - array - A copy of the array without duplicates -
-
See
-
- JSUS.equals - -
-
-
-
    ARRAY.distinct = function(array) {
-        var out = [];
-        if (!array) return out;
-
-        ARRAY.each(array, function(e) {
-            if (!ARRAY.inArray(e, out)) {
-                out.push(e);
-            }
-        });
-        return out;
-    };
-
-
-
-

- - ARRAY.transpose -

-
- -
-

Transposes a given 2D array.

- -

The original array is not modified, and a new copy is -returned.

-
-
-
Params
-
- array - array - The array to transpose -
-
Returns
-
- - array - The Transposed Array -
-
-
-
    ARRAY.transpose = function(array) {
-        if (!array) return;
-
-
- -
-

Calculate width and height

-
        var w, h, i, j, t = [];
-        w = array.length || 0;
-        h = (ARRAY.isArray(array[0])) ? array[0].length : 0;
-        if (w === 0 || h === 0) return t;
-
-        for ( i = 0; i < h; i++) {
-            t[i] = [];
-            for ( j = 0; j < w; j++) {
-                t[i][j] = array[j][i];
-            }
-        }
-        return t;
-    };
-
-    JSUS.extend(ARRAY);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/compatibility.js.html b/docs/lib/compatibility.js.html deleted file mode 100644 index 14dc39b..0000000 --- a/docs/lib/compatibility.js.html +++ /dev/null @@ -1,130 +0,0 @@ - - - - compatibility.js - - - - - - - - - -
- - - - - - - - - - - -
-
-
-

- - COMPATIBILITY -

-
- -
-

Copyright(c) 2015 Stefano Balietti -MIT Licensed

- -

Tests browsers ECMAScript 5 compatibility

- -

For more information see http://kangax.github.com/es5-compat-table/

-
-
-
(function(JSUS) {
-    "use strict";
-
-    function COMPATIBILITY() {}
-
-
-
-

- - COMPATIBILITY.compatibility -

-
- -
-

Returns a report of the ECS5 features available

- -

Useful when an application routinely performs an operation -depending on a potentially unsupported ECS5 feature.

- -

Transforms multiple try-catch statements in a if-else

-
-
-
Returns
-
- - object - support The compatibility object -
-
-
-
    COMPATIBILITY.compatibility = function() {
-
-        var support = {};
-
-        try {
-            Object.defineProperty({}, "a", {enumerable: false, value: 1});
-            support.defineProperty = true;
-        }
-        catch(e) {
-            support.defineProperty = false;
-        }
-
-        try {
-            eval('({ get x(){ return 1 } }).x === 1');
-            support.setter = true;
-        }
-        catch(err) {
-            support.setter = false;
-        }
-
-        try {
-            var value;
-            eval('({ set x(v){ value = v; } }).x = 1');
-            support.getter = true;
-        }
-        catch(err) {
-            support.getter = false;
-        }
-
-        return support;
-    };
-
-
-    JSUS.extend(COMPATIBILITY);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/dom.js.html b/docs/lib/dom.js.html deleted file mode 100644 index 678fc8d..0000000 --- a/docs/lib/dom.js.html +++ /dev/null @@ -1,3269 +0,0 @@ - - - - dom.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - DOM -

-
- -
-

Copyright(c) 2016 Stefano Balietti -MIT Licensed

- -

Collection of static functions related to DOM manipulation

- -

Helper library to perform generic operation with DOM elements.

- -

The general syntax is the following: Every HTML element has associated -a get* and a add* method, whose syntax is very similar.

- -
    -
  • The get* method creates the element and returns it.
  • -
  • The add* method creates the element, append it as child to a root element, -and then returns it.
  • -
- -

The syntax of both method is the same, but the add* method -needs the root element as first parameter. E.g.

- -
    -
  • getButton(id, text, attributes);
  • -
  • addButton(root, id, text, attributes);
  • -
- -

The last parameter is generally an object containing a list of -of key-values pairs as additional attributes to set to the element.

- -

Only the methods which do not follow the above-mentioned syntax -will receive further explanation.

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    var onFocusChange, changeTitle;
-
-    function DOM() {}
-
-
-

- - GENERAL -

-
- -
-
-
-
-

- - DOM.write -

-
- -
-

Write a text, or append an HTML element or node, into a root element

-
-
-
Params
-
- root - Element - The HTML element where to write into -
-
- text - mixed - The text to write. Default, an ampty string -
-
Returns
-
- - TextNode - The text node inserted in the root element -
-
See
-
- DOM.writeln - -
-
-
-
    DOM.write = function(root, text) {
-        var content;
-        if ('undefined' === typeof text || text === null) text = "";
-        if (JSUS.isNode(text) || JSUS.isElement(text)) content = text;
-        else content = document.createTextNode(text);
-        root.appendChild(content);
-        return content;
-    };
-
-
-
-

- - DOM.writeln -

-
- -
-

Write a text and a break into a root element

- -

Default break element is
tag

-
-
-
Params
-
- root - Element - The HTML element where to write into -
-
- text - mixed - The text to write. Default, an ampty string -
-
- rc - string - the name of the tag to use as a break element -
-
Returns
-
- - TextNode - The text node inserted in the root element -
-
See
-
- DOM.write -
-
See
-
- DOM.addBreak - -
-
-
-
    DOM.writeln = function(root, text, rc) {
-        var content;
-        content = DOM.write(root, text);
-        this.addBreak(root, rc);
-        return content;
-    };
-
-
-
-

- - DOM.sprintf -

-
- -
-

Builds up a decorated HTML text element

- -

Performs string substitution from an args object where the first -character of the key bears the following semantic:

- -
    -
  • '@': variable substitution with escaping
  • -
  • '!': variable substitution without variable escaping
  • -
  • '%': wraps a portion of string into a span element to which is - possible to associate a css class or id. Alternatively, - it also possible to add in-line style. E.g.:
  • -
- - -
     sprintf('%sImportant!%s An error has occurred: %pre@err%pre', {
-             '%pre': {
-                     style: 'font-size: 12px; font-family: courier;'
-             },
-             '%s': {
-                     id: 'myId',
-                     'class': 'myClass',
-             },
-             '@err': 'file not found',
-     }, document.body);
-
- - - -

Special span elements are %strong and %em, which add -respectively a strong and em tag instead of the default -span tag. They cannot be styled.

-
-
-
Params
-
- string - string - A text to transform -
-
- args - object - Optional. An object containing string - transformations -
-
- root - Element - Optional. An HTML element to which append the - string. Defaults, a new span element -
-
Returns
-
- - Element - The root element. -
-
-
-
    DOM.sprintf = function(string, args, root) {
-
-        var text, span, idx_start, idx_finish, idx_replace, idxs;
-        var spans, key, i;
-
-        root = root || document.createElement('span');
-        spans = {};
-
-
- -
-

Create an args object, if none is provided. -Defaults %em and %strong are added.

-
        args = args || {};
-        args['%strong'] = '';
-        args['%em'] = '';
-
-
- -
-

Transform arguments before inserting them.

-
        for (key in args) {
-            if (args.hasOwnProperty(key)) {
-
-                switch(key.charAt(0)) {
-
-                case '%': // Span/Strong/Emph .
-
-                    idx_start = string.indexOf(key);
-
-
- -
-

Pattern not found. No error.

-
                    if (idx_start === -1) continue;
-
-                    idx_replace = idx_start + key.length;
-                    idx_finish = string.indexOf(key, idx_replace);
-
-                    if (idx_finish === -1) {
-                        JSUS.log('Error. Could not find closing key: ' + key);
-                        continue;
-                    }
-
-
- -
-

Can be strong, emph or a generic span.

-
                    spans[idx_start] = key;
-
-                    break;
-
-                case '@': // Replace and sanitize.
-                    string = string.replace(key, escape(args[key]));
-                    break;
-
-                case '!': // Replace and not sanitize.
-                    string = string.replace(key, args[key]);
-                    break;
-
-                default:
-                    JSUS.log('Identifier not in [!,@,%]: ' + key[0]);
-
-                }
-            }
-        }
-
-
- -
-

No span to create, return what we have.

-
        if (!JSUS.size(spans)) {
-            return root.appendChild(document.createTextNode(string));
-        }
-
-
- -
-

Re-assamble the string.

-
        idxs = JSUS.keys(spans).sort(function(a, b){ return a - b; });
-        idx_finish = 0;
-        for (i = 0; i < idxs.length; i++) {
-
-
- -
-

Add span.

-
            key = spans[idxs[i]];
-            idx_start = string.indexOf(key);
-
-
- -
-

Add fragments of string.

-
            if (idx_finish !== idx_start-1) {
-                root.appendChild(document.createTextNode(
-                    string.substring(idx_finish, idx_start)));
-            }
-
-            idx_replace = idx_start + key.length;
-            idx_finish = string.indexOf(key, idx_replace);
-
-            if (key === '%strong') {
-                span = document.createElement('strong');
-            }
-            else if (key === '%em') {
-                span = document.createElement('em');
-            }
-            else {
-                span = JSUS.getElement('span', null, args[key]);
-            }
-
-            text = string.substring(idx_replace, idx_finish);
-
-            span.appendChild(document.createTextNode(text));
-
-            root.appendChild(span);
-            idx_finish = idx_finish + key.length;
-        }
-
-
- -
-

Add the final part of the string.

-
        if (idx_finish !== string.length) {
-            root.appendChild(document.createTextNode(
-                string.substring(idx_finish)));
-        }
-
-        return root;
-    };
-
-
-
-

- - DOM.isNode -

-
- -
-

Returns TRUE if the object is a DOM node

-
-
-
Params
-
- The - mixed - variable to check -
-
Returns
-
- - boolean - TRUE, if the the object is a DOM node -
-
-
-
    DOM.isNode = function(o) {
-        if (!o || 'object' !== typeof o) return false;
-        return 'object' === typeof Node ? o instanceof Node :
-            'number' === typeof o.nodeType &&
-            'string' === typeof o.nodeName;
-    };
-
-
-
-

- - DOM.isElement -

-
- -
-

Returns TRUE if the object is a DOM element

- -

Notice: instanceof HTMLElement is not reliable in Safari, even if -the method is defined.

-
-
-
Params
-
- The - mixed - variable to check -
-
Returns
-
- - boolean - TRUE, if the the object is a DOM element -
-
-
-
    DOM.isElement = function(o) {
-        return o && 'object' === typeof o && o.nodeType === 1 &&
-            'string' === typeof o.nodeName;
-    };
-
-
-
-

- - DOM.shuffleElements -

-
- -
-

Shuffles the order of children of a parent Element

- -

All children must have the id attribute (live list elements cannot -be identified by position).

- -

Notice the difference between Elements and Nodes:

- -

http://stackoverflow.com/questions/7935689/ -what-is-the-difference-between-children-and-childnodes-in-javascript

-
-
-
Params
-
- parent - Node - The parent node -
-
- order - array - Optional. A pre-specified order. Defaults, random -
-
Returns
-
- - array - The order used to shuffle the nodes -
-
-
-
    DOM.shuffleElements = function(parent, order) {
-        var i, len, idOrder, children, child;
-        var id, forceId, missId;
-        if (!JSUS.isNode(parent)) {
-            throw new TypeError('DOM.shuffleElements: parent must be a node. ' +
-                               'Found: ' + parent);
-        }
-        if (!parent.children || !parent.children.length) {
-            JSUS.log('DOM.shuffleElements: parent has no children.', 'ERR');
-            return false;
-        }
-        if (order) {
-            if (!JSUS.isArray(order)) {
-                throw new TypeError('DOM.shuffleElements: order must array.' +
-                                   'Found: ' + order);
-            }
-            if (order.length !== parent.children.length) {
-                throw new Error('DOM.shuffleElements: order length must ' +
-                                'match the number of children nodes.');
-            }
-        }
-
-
- -
-

DOM4 compliant browsers.

-
        children = parent.children;
-
-
- -
-

https://developer.mozilla.org/en/DOM/Element.children -[IE lt 9] IE < 9

-
        if ('undefined' === typeof children) {
-            child = this.firstChild;
-            while (child) {
-                if (child.nodeType == 1) children.push(child);
-                child = child.nextSibling;
-            }
-        }
-
-        len = children.length;
-        idOrder = new Array(len);
-        if (!order) order = JSUS.sample(0, (len-1));
-        for (i = 0 ; i < len; i++) {
-            id = children[order[i]].id;
-            if ('string' !== typeof id || id === "") {
-                throw new Error('DOM.shuffleElements: no id found on ' +
-                                'child n. ' + order[i] + '.');
-            }
-            idOrder[i] = id;
-        }
-
-
- -
-

Two fors are necessary to follow the real sequence (Live List). -However, parent.children is a special object, so the sequence -could be unreliable.

-
        for (i = 0 ; i < len; i++) {
-            parent.appendChild(children[idOrder[i]]);
-        }
-        return idOrder;
-    };
-
-
-
-

- - DOM.shuffleNodes -

-
- -
-

It actually shuffles Elements.

-
-
-
-
-
    DOM.shuffleNodes = function(parent, order) {
-        console.log('***DOM.shuffleNodes is deprecated. ' +
-                    'Use Dom.shuffleElements instead.***');
-        return DOM.shuffleElements(parent, order);
-    };
-
-
-
-

- - DOM.getElement -

-
- -
-

Creates a generic HTML element with id and attributes as specified

-
-
-
Params
-
- elem - string - The name of the tag -
-
- id - string - Optional. The id of the tag -
-
- attributes - object - Optional. Object containing attributes for - the newly created element -
-
Returns
-
- - HTMLElement - The newly created HTML element -
-
See
-
- DOM.addAttributes2Elem - -
-
-
-
    DOM.getElement = function(elem, id, attributes) {
-        var e = document.createElement(elem);
-        if ('undefined' !== typeof id) {
-            e.id = id;
-        }
-        return this.addAttributes2Elem(e, attributes);
-    };
-
-
-
-

- - DOM.addElement -

-
- -
-

Creates and appends a generic HTML element with specified attributes

-
-
-
Params
-
- elem - string - The name of the tag -
-
- root - HTMLElement - The root element to which the new element will - be appended -
-
- id - string - Optional. The id of the tag -
-
- attributes - object - Optional. Object containing attributes for - the newly created element -
-
Returns
-
- - HTMLElement - The newly created HTML element -
-
See
-
- DOM.getElement -
-
See
-
- DOM.addAttributes2Elem - -
-
-
-
    DOM.addElement = function(elem, root, id, attributes) {
-        var el = this.getElement(elem, id, attributes);
-        return root.appendChild(el);
-    };
-
-
-
-

- - DOM.addAttributes2Elem -

-
- -
-

Adds attributes to an HTML element and returns it.

- -

Attributes are defined as key-values pairs. -Attributes 'label' is ignored, attribute 'className' ('class') and -'style' are special and are delegated to special methods.

-
-
-
Params
-
- e - HTMLElement - The element to decorate -
-
- a - object - Object containing attributes to add to the element -
-
Returns
-
- - HTMLElement - The decorated element -
-
See
-
- DOM.addLabel -
-
See
-
- DOM.addClass -
-
See
-
- DOM.style - -
-
-
-
    DOM.addAttributes2Elem = function(e, a) {
-        var key;
-        if (!e || !a) return e;
-        if ('object' != typeof a) return e;
-        for (key in a) {
-            if (a.hasOwnProperty(key)) {
-                if (key === 'id') {
-                    e.id = a[key];
-                }
-                else if (key === 'class' || key === 'className') {
-                    DOM.addClass(e, a[key]);
-                }
-                else if (key === 'style') {
-                    DOM.style(e, a[key]);
-                }
-                else if (key === 'label') {
-
-
- -
-

Handle the case.

-
                    JSUS.log('DOM.addAttributes2Elem: label attribute is not ' +
-                             'supported. Use DOM.addLabel instead.');
-                }
-                else {
-                    e.setAttribute(key, a[key]);
-                }
-
-
- -
-TODO: handle special cases - -
            }
-        }
-        return e;
-    };
-
-
-
-

- - DOM.populateSelect -

-
- -
-

Appends a list of options into a HTML select element. -The second parameter list is an object containing -a list of key-values pairs as text-value attributes for -the option.

-
-
-
Params
-
- select - HTMLElement - HTML select element -
-
- list - object - Options to add to the select element -
-
-
-
    DOM.populateSelect = function(select, list) {
-        var key, opt;
-        if (!select || !list) return;
-        for (key in list) {
-            if (list.hasOwnProperty(key)) {
-                opt = document.createElement('option');
-                opt.value = list[key];
-                opt.appendChild(document.createTextNode(key));
-                select.appendChild(opt);
-            }
-        }
-    };
-
-
-
-

- - DOM.removeChildrenFromNode -

-
- -
-

Removes all children from a node.

-
-
-
Params
-
- e - HTMLElement - HTML element. -
-
-
-
    DOM.removeChildrenFromNode = function(e) {
-        while (e.hasChildNodes()) {
-            e.removeChild(e.firstChild);
-        }
-    };
-
-
-
-

- - DOM.insertAfter -

-
- -
-

Insert a node element after another one.

- -

The first parameter is the node to add.

-
-
-
    DOM.insertAfter = function(node, referenceNode) {
-        referenceNode.insertBefore(node, referenceNode.nextSibling);
-    };
-
-
-
-

- - DOM.generateUniqueId -

-
- -
-

Generate a unique id for the page (frames included).

- -

TODO: now it always create big random strings, it does not actually -check if the string exists.

-
-
-
    DOM.generateUniqueId = function(prefix) {
-        var search = [window];
-        if (window.frames) {
-            search = search.concat(window.frames);
-        }
-
-        function scanDocuments(id) {
-            var found = true;
-            while (found) {
-                for (var i=0; i < search.length; i++) {
-                    found = search[i].document.getElementById(id);
-                    if (found) {
-                        id = '' + id + '_' + JSUS.randomInt(0, 1000);
-                        break;
-                    }
-                }
-            }
-            return id;
-        }
-
-
-        return scanDocuments(prefix + '_' + JSUS.randomInt(0, 10000000));
-
-
- -
-

return scanDocuments(prefix);

-
    };
-
-
-

- - GET/ADD -

-
- -
-
-
-
-

- - DOM.getButton -

-
- -
-
-
-
-
    DOM.getButton = function(id, text, attributes) {
-        var sb;
-        sb = document.createElement('button');
-        if ('undefined' !== typeof id) sb.id = id;
-        sb.appendChild(document.createTextNode(text || 'Send'));
-        return this.addAttributes2Elem(sb, attributes);
-    };
-
-
-
-

- - DOM.addButton -

-
- -
-
-
-
-
    DOM.addButton = function(root, id, text, attributes) {
-        var b = this.getButton(id, text, attributes);
-        return root.appendChild(b);
-    };
-
-
-
-

- - DOM.getFieldset -

-
- -
-
-
-
-
    DOM.getFieldset = function(id, legend, attributes) {
-        var f = this.getElement('fieldset', id, attributes);
-        var l = document.createElement('Legend');
-        l.appendChild(document.createTextNode(legend));
-        f.appendChild(l);
-        return f;
-    };
-
-
-
-

- - DOM.addFieldset -

-
- -
-
-
-
-
    DOM.addFieldset = function(root, id, legend, attributes) {
-        var f = this.getFieldset(id, legend, attributes);
-        return root.appendChild(f);
-    };
-
-
-
-

- - DOM.getTextInput -

-
- -
-
-
-
-
    DOM.getTextInput = function(id, attributes) {
-        var ti =  document.createElement('input');
-        if ('undefined' !== typeof id) ti.id = id;
-        ti.setAttribute('type', 'text');
-        return this.addAttributes2Elem(ti, attributes);
-    };
-
-
-
-

- - DOM.addTextInput -

-
- -
-
-
-
-
    DOM.addTextInput = function(root, id, attributes) {
-        var ti = this.getTextInput(id, attributes);
-        return root.appendChild(ti);
-    };
-
-
-
-

- - DOM.getTextArea -

-
- -
-
-
-
-
    DOM.getTextArea = function(id, attributes) {
-        var ta =  document.createElement('textarea');
-        if ('undefined' !== typeof id) ta.id = id;
-        return this.addAttributes2Elem(ta, attributes);
-    };
-
-
-
-

- - DOM.addTextArea -

-
- -
-
-
-
-
    DOM.addTextArea = function(root, id, attributes) {
-        var ta = this.getTextArea(id, attributes);
-        return root.appendChild(ta);
-    };
-
-
-
-

- - DOM.getCanvas -

-
- -
-
-
-
-
    DOM.getCanvas = function(id, attributes) {
-        var canvas = document.createElement('canvas');
-        var context = canvas.getContext('2d');
-
-        if (!context) {
-            alert('Canvas is not supported');
-            return false;
-        }
-
-        canvas.id = id;
-        return this.addAttributes2Elem(canvas, attributes);
-    };
-
-
-
-

- - DOM.addCanvas -

-
- -
-
-
-
-
    DOM.addCanvas = function(root, id, attributes) {
-        var c = this.getCanvas(id, attributes);
-        return root.appendChild(c);
-    };
-
-
-
-

- - DOM.getSlider -

-
- -
-
-
-
-
    DOM.getSlider = function(id, attributes) {
-        var slider = document.createElement('input');
-        slider.id = id;
-        slider.setAttribute('type', 'range');
-        return this.addAttributes2Elem(slider, attributes);
-    };
-
-
-
-

- - DOM.addSlider -

-
- -
-
-
-
-
    DOM.addSlider = function(root, id, attributes) {
-        var s = this.getSlider(id, attributes);
-        return root.appendChild(s);
-    };
-
-
-
-

- - DOM.getRadioButton -

-
- -
-
-
-
-
    DOM.getRadioButton = function(id, attributes) {
-        var radio = document.createElement('input');
-        radio.id = id;
-        radio.setAttribute('type', 'radio');
-        return this.addAttributes2Elem(radio, attributes);
-    };
-
-
-
-

- - DOM.addRadioButton -

-
- -
-
-
-
-
    DOM.addRadioButton = function(root, id, attributes) {
-        var rb = this.getRadioButton(id, attributes);
-        return root.appendChild(rb);
-    };
-
-
-
-

- - DOM.getLabel -

-
- -
-
-
-
-
    DOM.getLabel = function(forElem, id, labelText, attributes) {
-        if (!forElem) return false;
-        var label = document.createElement('label');
-        label.id = id;
-        label.appendChild(document.createTextNode(labelText));
-
-        if ('undefined' === typeof forElem.id) {
-            forElem.id = this.generateUniqueId();
-        }
-
-        label.setAttribute('for', forElem.id);
-        this.addAttributes2Elem(label, attributes);
-        return label;
-    };
-
-
-
-

- - DOM.addLabel -

-
- -
-
-
-
-
    DOM.addLabel = function(root, forElem, id, labelText, attributes) {
-        if (!root || !forElem || !labelText) return false;
-        var l = this.getLabel(forElem, id, labelText, attributes);
-        root.insertBefore(l, forElem);
-        return l;
-    };
-
-
-
-

- - DOM.getSelect -

-
- -
-
-
-
-
    DOM.getSelect = function(id, attributes) {
-        return this.getElement('select', id, attributes);
-    };
-
-
-
-

- - DOM.addSelect -

-
- -
-
-
-
-
    DOM.addSelect = function(root, id, attributes) {
-        return this.addElement('select', root, id, attributes);
-    };
-
-
-
-

- - DOM.getIFrame -

-
- -
-
-
-
-
    DOM.getIFrame = function(id, attributes) {
-        attributes = attributes || {};
-        if (!attributes.name) {
-            attributes.name = id; // For Firefox
-        }
-        return this.getElement('iframe', id, attributes);
-    };
-
-
-
-

- - DOM.addIFrame -

-
- -
-
-
-
-
    DOM.addIFrame = function(root, id, attributes) {
-        var ifr = this.getIFrame(id, attributes);
-        return root.appendChild(ifr);
-    };
-
-
-
-

- - DOM.addBreak -

-
- -
-
-
-
-
    DOM.addBreak = function(root, rc) {
-        var RC = rc || 'br';
-        var br = document.createElement(RC);
-        return root.appendChild(br);
-
-
- -
-

return this.insertAfter(br,root);

-
    };
-
-
-
-

- - DOM.getDiv -

-
- -
-
-
-
-
    DOM.getDiv = function(id, attributes) {
-        return this.getElement('div', id, attributes);
-    };
-
-
-
-

- - DOM.addDiv -

-
- -
-
-
-
-
    DOM.addDiv = function(root, id, attributes) {
-        return this.addElement('div', root, id, attributes);
-    };
-
-
-

- - CSS / JS -

-
- -
-
-
-
-

- - DOM.addCSS -

-
- -
-

If no root element is passed, it tries to add the CSS -link element to document.head, document.body, and -finally document. If it fails, returns FALSE.

-
-
-
    DOM.addCSS = function(root, css, id, attributes) {
-        root = root || document.head || document.body || document;
-        if (!root) return false;
-
-        attributes = attributes || {};
-
-        attributes = JSUS.merge(attributes, {rel : 'stylesheet',
-                                             type: 'text/css',
-                                             href: css
-                                            });
-
-        return this.addElement('link', root, id, attributes);
-    };
-
-
-
-

- - DOM.addJS -

-
- -
-
-
-
-
    DOM.addJS = function(root, js, id, attributes) {
-        root = root || document.head || document.body || document;
-        if (!root) return false;
-
-        attributes = attributes || {};
-
-        attributes = JSUS.merge(attributes, {charset : 'utf-8',
-                                             type: 'text/javascript',
-                                             src: js
-                                            });
-
-        return this.addElement('script', root, id, attributes);
-    };
-
-
-
-

- - DOM.highlight -

-
- -
-

Provides a simple way to highlight an HTML element -by adding a colored border around it.

- -

Three pre-defined modes are implemented:

- -
    -
  • OK: green
  • -
  • WARN: yellow
  • -
  • ERR: red (default)
  • -
- -

Alternatively, it is possible to specify a custom -color as HEX value. Examples:

- - -
highlight(myDiv, 'WARN'); // yellow border
-highlight(myDiv);          // red border
-highlight(myDiv, '#CCC'); // grey border
-
- - -
-
-
Params
-
- elem - HTMLElement - The element to highlight -
-
- code - string - The type of highlight -
-
See
-
- DOM.addBorder -
-
See
-
- DOM.style - -
-
-
-
    DOM.highlight = function(elem, code) {
-        var color;
-        if (!elem) return;
-
-
- -
-

default value is ERR

-
        switch (code) {
-        case 'OK':
-            color =  'green';
-            break;
-        case 'WARN':
-            color = 'yellow';
-            break;
-        case 'ERR':
-            color = 'red';
-            break;
-        default:
-            if (code.charAt(0) === '#') {
-                color = code;
-            }
-            else {
-                color = 'red';
-            }
-        }
-
-        return this.addBorder(elem, color);
-    };
-
-
-
-

- - DOM.addBorder -

-
- -
-

Adds a border around the specified element. Color, -width, and type can be specified.

-
-
-
    DOM.addBorder = function(elem, color, width, type) {
-        var properties;
-        if (!elem) return;
-
-        color = color || 'red';
-        width = width || '5px';
-        type = type || 'solid';
-
-        properties = { border: width + ' ' + type + ' ' + color };
-        return DOM.style(elem, properties);
-    };
-
-
-
-

- - DOM.style -

-
- -
-

Styles an element as an in-line css.

- -

Existing style properties are maintained, and new ones added.

-
-
-
Params
-
- elem - HTMLElement - The element to style -
-
- Objects - object - containing the properties to add. -
-
Returns
-
- - HTMLElement - The styled element -
-
-
-
    DOM.style = function(elem, properties) {
-        var i;
-        if (!elem || !properties) return;
-        if (!DOM.isElement(elem)) return;
-
-        for (i in properties) {
-            if (properties.hasOwnProperty(i)) {
-                elem.style[i] = properties[i];
-            }
-        }
-        return elem;
-    };
-
-
-
-

- - DOM.removeClass -

-
- -
-

Removes a specific class from the classNamex attribute of a given element

-
-
-
Params
-
- el - HTMLElement - An HTML element -
-
- c - string - The name of a CSS class already in the element -
-
Returns
-
- - HTMLElement - undefined - The HTML element with the removed - class, or undefined if the inputs are misspecified -
-
-
-
    DOM.removeClass = function(el, c) {
-        var regexpr, o;
-        if (!el || !c) return;
-        regexpr = new RegExp('(?:^|\\s)' + c + '(?!\\S)');
-        o = el.className = el.className.replace( regexpr, '' );
-        return el;
-    };
-
-
-
-

- - DOM.addClass -

-
- -
-

Adds one or more classes to the className attribute of the given element

- -

Takes care not to overwrite already existing classes.

-
-
-
Params
-
- el - HTMLElement - An HTML element -
-
- c - string - array - The name/s of CSS class/es -
-
Returns
-
- - HTMLElement - undefined - The HTML element with the additional - class, or undefined if the inputs are misspecified -
-
-
-
    DOM.addClass = function(el, c) {
-        if (!el) return;
-        if (c instanceof Array) c = c.join(' ');
-        else if ('string' !== typeof c) return;
-        if (!el.className || el.className === '') el.className = c;
-        else el.className += (' ' + c);
-        return el;
-    };
-
-
-
-

- - DOM.getElementsByClassName -

-
- -
-

Returns an array of elements with requested class name

-
-
-
Params
-
- document - object - The document object of a window or iframe -
-
- className - string - The requested className -
-
- - string - nodeName Optional. If set only elements with - the specified tag name will be searched -
-
Returns
-
- - array - Array of elements with the requested class name -
-
See
- -
See
- -
-
-
    DOM.getElementsByClassName = function(document, className, nodeName) {
-        var result, node, tag, seek, i, rightClass;
-        result = [];
-        tag = nodeName || '*';
-        if (document.evaluate) {
-            seek = '//' + tag +
-                '[contains(concat(" ", normalize-space(@class), " "), "' +
-                className + ' ")]';
-            seek = document.evaluate(seek, document, null, 0, null );
-            while ((node = seek.iterateNext())) {
-                result.push(node);
-            }
-        }
-        else {
-            rightClass = new RegExp( '(^| )'+ className +'( |$)' );
-            seek = document.getElementsByTagName(tag);
-            for (i = 0; i < seek.length; i++)
-                if (rightClass.test((node = seek[i]).className )) {
-                    result.push(seek[i]);
-                }
-        }
-        return result;
-    };
-
-
-

- - IFRAME -

-
- -
-
-
-
-

- - DOM.getIFrameDocument -

-
- -
-

Returns a reference to the document of an iframe object

-
-
-
Params
-
- iframe - HTMLIFrameElement - The iframe object -
-
Returns
-
- - HTMLDocument - null - The document of the iframe, or - null if not found. -
-
-
-
    DOM.getIFrameDocument = function(iframe) {
-        if (!iframe) return null;
-        return iframe.contentDocument ||
-            iframe.contentWindow ? iframe.contentWindow.document : null;
-    };
-
-
-
-

- - DOM.getIFrameAnyChild -

-
- -
-

Gets the first available child of an IFrame

- -

Tries head, body, lastChild and the HTML element

-
-
-
Params
-
- iframe - HTMLIFrameElement - The iframe object -
-
Returns
-
- - HTMLElement - undefined - The child, or undefined if none is found -
-
-
-
    DOM.getIFrameAnyChild = function(iframe) {
-        var contentDocument;
-        if (!iframe) return;
-        contentDocument = W.getIFrameDocument(iframe);
-        return contentDocument.head || contentDocument.body ||
-            contentDocument.lastChild ||
-            contentDocument.getElementsByTagName('html')[0];
-    };
-
-
-

- - RIGHT-CLICK -

-
- -
-
-
-
-

- - DOM.disableRightClick -

-
- -
-

Disables the popup of the context menu by right clicking with the mouse

-
-
-
Params
-
- Optional. - Document - A target document object. Defaults, document -
-
See
-
- DOM.enableRightClick - -
-
-
-
    DOM.disableRightClick = function(doc) {
-        doc = doc || document;
-        if (doc.layers) {
-            doc.captureEvents(Event.MOUSEDOWN);
-            doc.onmousedown = function clickNS4(e) {
-                if (doc.layers || doc.getElementById && !doc.all) {
-                    if (e.which == 2 || e.which == 3) {
-                        return false;
-                    }
-                }
-            };
-        }
-        else if (doc.all && !doc.getElementById) {
-            doc.onmousedown = function clickIE4() {
-                if (event.button == 2) {
-                    return false;
-                }
-            };
-        }
-        doc.oncontextmenu = new Function("return false");
-    };
-
-
-
-

- - DOM.enableRightClick -

-
- -
-

Enables the popup of the context menu by right clicking with the mouse

- -

It unregisters the event handlers created by DOM.disableRightClick

-
-
-
Params
-
- Optional. - Document - A target document object. Defaults, document -
-
See
-
- DOM.disableRightClick - -
-
-
-
    DOM.enableRightClick = function(doc) {
-        doc = doc || document;
-        if (doc.layers) {
-            doc.releaseEvents(Event.MOUSEDOWN);
-            doc.onmousedown = null;
-        }
-        else if (doc.all && !doc.getElementById) {
-            doc.onmousedown = null;
-        }
-        doc.oncontextmenu = null;
-    };
-
-
-
-

- - DOM.addEvent -

-
- -
-

Adds an event listener to an element (cross-browser)

-
-
-
Params
-
- element - Element - A target element -
-
- event - string - The name of the event to handle -
-
- func - function - The event listener -
-
- Optional. - boolean - If TRUE, the event will initiate a capture. - Available only in some browsers. Default, FALSE -
-
Returns
-
- - boolean - TRUE, on success. However, the return value is - browser dependent. -
-
See
- -
-
-
    DOM.addEvent = function(element, event, func, capture) {
-        capture = !!capture;
-        if (element.attachEvent) return element.attachEvent('on' + event, func);
-        else return element.addEventListener(event, func, capture);
-    };
-
-
-
-

- - DOM.removeEvent -

-
- -
-

Removes an event listener from an element (cross-browser)

-
-
-
Params
-
- element - Element - A target element -
-
- event - string - The name of the event to remove -
-
- func - function - The event listener -
-
- Optional. - boolean - If TRUE, the event was registered - as a capture. Available only in some browsers. Default, FALSE -
-
Returns
-
- - boolean - TRUE, on success. However, the return value is - browser dependent. -
-
See
-
- DOM.addEvent - -
-
-
-
    DOM.removeEvent = function(element, event, func, capture) {
-        capture = !!capture;
-        if (element.detachEvent) return element.detachEvent('on' + event, func);
-        else return element.removeEventListener(event, func, capture);
-    };
-
-
-
-

- - DOM.disableBackButton -

-
- -
-

Disables/re-enables backward navigation in history of browsed pages

- -

When disabling, it inserts twice the current url.

- -

It will still be possible to manually select the uri in the -history pane and nagivate to it.

-
-
-
Params
-
- disable - boolean - Optional. If TRUE disables back button, - if FALSE, re-enables it. Default: TRUE. -
-
Returns
-
- - boolean - The state of the back button (TRUE = disabled), - or NULL if the method is not supported by browser. -
-
-
-
    DOM.disableBackButton = (function(isDisabled) {
-        return function(disable) {
-            disable = 'undefined' === typeof disable ? true : disable;
-            if (disable && !isDisabled) {
-                if (!history.pushState || !history.go) {
-                    node.warn('DOM.disableBackButton: method not ' +
-                              'supported by browser.');
-                    return null;
-                }
-                history.pushState(null, null, location.href);
-                window.onpopstate = function(event) {
-                    history.go(1);
-                };
-            }
-            else if (isDisabled) {
-                window.onpopstate = null;
-            }
-            isDisabled = disable;
-            return disable;
-        };
-    })(false);
-
-
-
-

- - DOM.playSound -

-
- -
-

Plays a sound

-
-
-
Params
-
- sound - various - Audio tag or path to audio file to be played -
-
-
-
    DOM.playSound = 'undefined' === typeof Audio ?
-        function() {
-            console.log('JSUS.playSound: Audio tag not supported in your' +
-                    ' browser. Cannot play sound.');
-        } :
-        function(sound) {
-        var audio;
-        if ('string' === typeof sound) {
-            audio = new Audio(sound);
-        }
-        else if ('object' === typeof sound &&
-            'function' === typeof sound.play) {
-            audio = sound;
-        }
-        else {
-            throw new TypeError('JSUS.playSound: sound must be string' +
-               ' or audio element.');
-        }
-        audio.play();
-    };
-
-
-
-

- - DOM.onFocusIn -

-
- -
-

Registers a callback to be executed when the page acquires focus

-
-
-
Params
-
- cb - function - null - Callback executed if page acquires focus, - or NULL, to delete an existing callback. -
-
- ctx - object - function - Optional. Context of execution for cb -
-
See
-
- onFocusChange - -
-
-
-
    DOM.onFocusIn = function(cb, ctx) {
-        var origCb;
-        if ('function' !== typeof cb && null !== cb) {
-            throw new TypeError('JSUS.onFocusIn: cb must be function or null.');
-        }
-        if (ctx) {
-            if ('object' !== typeof ctx && 'function' !== typeof ctx) {
-                throw new TypeError('JSUS.onFocusIn: ctx must be object, ' +
-                                    'function or undefined.');
-            }
-            origCb = cb;
-            cb = function() { origCb.call(ctx); };
-        }
-
-        onFocusChange(cb);
-    };
-
-
-
-

- - DOM.onFocusOut -

-
- -
-

Registers a callback to be executed when the page loses focus

-
-
-
Params
-
- cb - function - Callback executed if page loses focus, - or NULL, to delete an existing callback. -
-
- ctx - object - function - Optional. Context of execution for cb -
-
See
-
- onFocusChange - -
-
-
-
    DOM.onFocusOut = function(cb, ctx) {
-        var origCb;
-        if ('function' !== typeof cb && null !== cb) {
-            throw new TypeError('JSUS.onFocusOut: cb must be ' +
-                                'function or null.');
-        }
-        if (ctx) {
-            if ('object' !== typeof ctx && 'function' !== typeof ctx) {
-                throw new TypeError('JSUS.onFocusIn: ctx must be object, ' +
-                                    'function or undefined.');
-            }
-            origCb = cb;
-            cb = function() { origCb.call(ctx); };
-        }
-        onFocusChange(undefined, cb);
-    };
-
-
-
-

- - DOM.blinkTitle -

-
- -
-

Changes the title of the page in regular intervals

- -

Calling the function without any arguments stops the blinking -If an array of strings is provided, that array will be cycled through. -If a signle string is provided, the title will alternate between '!!!' - and that string.

-
-
-
Params
-
- titles - mixed - New title to blink -
-
- options - object - Optional. Configuration object. - Accepted values and default in parenthesis: - - -
- stopOnFocus (false): Stop blinking if user switched to tab
-- stopOnClick (false): Stop blinking if user clicks on the
-    specified element
-- finalTitle (document.title): Title to set after blinking is done
-- repeatFor (undefined): Show each element in titles at most
-    N times -- might be stopped earlier by other events.
-- startOnBlur(false): Start blinking if user switches
-     away from tab
-- period (1000) How much time between two blinking texts in the title
-
- -
-
-
Returns
-
- - function - null - A function to clear the blinking of texts, - or NULL, if the interval was not created yet (e.g. with startOnBlur - option), or just destroyed. -
-
-
-
    DOM.blinkTitle = (function(id) {
-        var clearBlinkInterval, finalTitle, elem;
-        clearBlinkInterval = function(opts) {
-            clearInterval(id);
-            id = null;
-            if (elem) {
-                elem.removeEventListener('click', clearBlinkInterval);
-                elem = null;
-            }
-            if (finalTitle) {
-                document.title = finalTitle;
-                finalTitle = null;
-            }
-        };
-        return function(titles, options) {
-            var period, where, rotation;
-            var rotationId, nRepeats;
-
-            if (null !== id) clearBlinkInterval();
-            if ('undefined' === typeof titles) return null;
-
-            where = 'JSUS.blinkTitle: ';
-            options = options || {};
-
-
- -
-

Option finalTitle.

-
            if ('undefined' === typeof options.finalTitle) {
-                finalTitle = document.title;
-            }
-            else if ('string' === typeof options.finalTitle) {
-                finalTitle = options.finalTitle;
-            }
-            else {
-                throw new TypeError(where + 'options.finalTitle must be ' +
-                                    'string or undefined. Found: ' +
-                                    options.finalTitle);
-            }
-
-
- -
-

Option repeatFor.

-
            if ('undefined' !== typeof options.repeatFor) {
-                nRepeats = JSUS.isInt(options.repeatFor, 0);
-                if (false === nRepeats) {
-                    throw new TypeError(where + 'options.repeatFor must be ' +
-                                        'a positive integer. Found: ' +
-                                        options.repeatFor);
-                }
-            }
-
-
- -
-

Option stopOnFocus.

-
            if (options.stopOnFocus) {
-                JSUS.onFocusIn(function() {
-                    clearBlinkInterval();
-                    onFocusChange(null, null);
-                });
-            }
-
-
- -
-

Option stopOnClick.

-
            if ('undefined' !== typeof options.stopOnClick) {
-                if ('object' !== typeof options.stopOnClick ||
-                    !options.stopOnClick.addEventListener) {
-
-                    throw new TypeError(where + 'options.stopOnClick must be ' +
-                                        'an HTML element with method ' +
-                                        'addEventListener. Found: ' +
-                                        options.stopOnClick);
-                }
-                elem = options.stopOnClick;
-                elem.addEventListener('click', clearBlinkInterval);
-            }
-
-
- -
-

Option startOnBlur.

-
            if (options.startOnBlur) {
-                options.startOnBlur = null;
-                JSUS.onFocusOut(function() {
-                    JSUS.blinkTitle(titles, options);
-                });
-                return null;
-            }
-
-
- -
-

Prepare the rotation.

-
            if ('string' === typeof titles) {
-                titles = [titles, '!!!'];
-            }
-            else if (!JSUS.isArray(titles)) {
-                throw new TypeError(where + 'titles must be string, ' +
-                                    'array of strings or undefined.');
-            }
-            rotationId = 0;
-            period = options.period || 1000;
-
-
- -
-

Function to be executed every period.

-
            rotation = function() {
-                changeTitle(titles[rotationId]);
-                rotationId = (rotationId+1) % titles.length;
-
-
- -
-

Control the number of times it should be cycled through.

-
                if ('number' === typeof nRepeats) {
-                    if (rotationId === 0) {
-                        nRepeats--;
-                        if (nRepeats === 0) clearBlinkInterval();
-                    }
-                }
-            };
-
-
- -
-

Perform first rotation right now.

-
            rotation();
-            id = setInterval(rotation, period);
-
-
- -
-

Return clear function.

-
            return clearBlinkInterval;
-        };
-    })(null);
-
-
-
-

- - DOM.cookieSupport -

-
- -
-

Tests for cookie support

-
-
-
Returns
-
- - boolean - null - The type of support for cookies. Values: - -
    -
  • null: no cookies
  • -
  • false: only session cookies
  • -
  • true: session cookies and persistent cookies (although - the browser might clear them on exit)
  • -
- -Kudos: http://stackoverflow.com/questions/2167310/ - how-to-show-a-message-only-if-cookies-are-disabled-in-browser
-
-
-
-
    DOM.cookieSupport = function() {
-        var c, persist;
-        persist = true;
-        do {
-            c = 'gCStest=' + Math.floor(Math.random()*100000000);
-            document.cookie = persist ? c +
-                ';expires=Tue, 01-Jan-2030 00:00:00 GMT' : c;
-
-            if (document.cookie.indexOf(c) !== -1) {
-                document.cookie= c + ';expires=Sat, 01-Jan-2000 00:00:00 GMT';
-                return persist;
-            }
-        } while (!(persist = !persist));
-
-        return null;
-    };
-
-
-
-

- - DOM.viewportSize -

-
- -
-

Returns the current size of the viewport in pixels

- -

The viewport's size is the actual visible part of the browser's -window. This excludes, for example, the area occupied by the -JavaScript console.

-
-
-
Params
-
- dim - string - Optional. Controls the return value ('x', or 'y') -
-
Returns
-
- - object - number - An object containing x and y property, or - number specifying the value for x or y - -Kudos: http://stackoverflow.com/questions/3437786/ - get-the-size-of-the-screen-current-web-page-and-browser-window -
-
-
-
    DOM.viewportSize = function(dim) {
-        var w, d, e, g, x, y;
-        if (dim && dim !== 'x' && dim !== 'y') {
-            throw new TypeError('DOM.viewportSize: dim must be "x","y" or ' +
-                                'undefined. Found: ' + dim);
-        }
-        w = window;
-        d = document;
-        e = d.documentElement;
-        g = d.getElementsByTagName('body')[0];
-        x = w.innerWidth || e.clientWidth || g.clientWidth,
-        y = w.innerHeight|| e.clientHeight|| g.clientHeight;
-        return !dim ? {x: x, y: y} : dim === 'x' ? x : y;
-    };
-
-
-

- - Helper methods -

-
- -
-
-
-
-

- - onFocusChange -

-
- -
-

Helper function for DOM.onFocusIn and DOM.onFocusOut (cross-browser)

- -

Expects only one callback, either inCb, or outCb.

-
-
-
Params
-
- inCb - function - null - Optional. Executed if page acquires focus, - or NULL, to delete an existing callback. -
-
- outCb - function - null - Optional. Executed if page loses focus, - or NULL, to delete an existing callback. - -Kudos: http://stackoverflow.com/questions/1060008/ - is-there-a-way-to-detect-if-a-browser-window-is-not-currently-active -
-
See
- -
-
-
    onFocusChange = (function(document) {
-        var inFocusCb, outFocusCb, event, hidden, evtMap;
-
-        if (!document) {
-            return function() {
-                JSUS.log('onFocusChange: no document detected.');
-                return;
-            };
-        }
-
-        if ('hidden' in document) {
-            hidden = 'hidden';
-            event = 'visibilitychange';
-        }
-        else if ('mozHidden' in document) {
-            hidden = 'mozHidden';
-            event = 'mozvisibilitychange';
-        }
-        else if ('webkitHidden' in document) {
-            hidden = 'webkitHidden';
-            event = 'webkitvisibilitychange';
-        }
-        else if ('msHidden' in document) {
-            hidden = 'msHidden';
-            event = 'msvisibilitychange';
-        }
-
-        evtMap = {
-            focus: true, focusin: true, pageshow: true,
-            blur: false, focusout: false, pagehide: false
-        };
-
-        function onchange(evt) {
-            var isHidden;
-            evt = evt || window.event;
-
-
- -
-

If event is defined as one from event Map.

-
            if (evt.type in evtMap) isHidden = evtMap[evt.type];
-
-
- -
-

Or use the hidden property.

-
            else isHidden = this[hidden] ? true : false;
-
-
- -
-

Call the callback, if defined.

-
            if (!isHidden) { if (inFocusCb) inFocusCb(); }
-            else { if (outFocusCb) outFocusCb(); }
-        }
-
-        return function(inCb, outCb) {
-            var onchangeCb;
-
-            if ('undefined' !== typeof inCb) inFocusCb = inCb;
-            else outFocusCb = outCb;
-
-            onchangeCb = !inFocusCb && !outFocusCb ? null : onchange;
-
-
- -
-

Visibility standard detected.

-
            if (event) {
-                if (onchangeCb) document.addEventListener(event, onchange);
-                else document.removeEventListener(event, onchange);
-            }
-            else if ('onfocusin' in document) {
-                document.onfocusin = document.onfocusout = onchangeCb;
-            }
-
-
- -
-

All others.

-
            else {
-                window.onpageshow = window.onpagehide
-                    = window.onfocus = window.onblur = onchangeCb;
-            }
-        };
-    })('undefined' !== typeof document ? document : null);
-
-
-
-

- - changeTitle -

-
- -
-

Changes title of page

-
-
-
Params
-
- title - string - New title of the page -
-
-
-
    changeTitle = function(title) {
-        if ('string' === typeof title) {
-            document.title = title;
-        }
-        else {
-            throw new TypeError('JSUS.changeTitle: title must be string. ' +
-                                'Found: ' + title);
-        }
-    };
-
-    JSUS.extend(DOM);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/eval.js.html b/docs/lib/eval.js.html deleted file mode 100644 index a42f6ee..0000000 --- a/docs/lib/eval.js.html +++ /dev/null @@ -1,164 +0,0 @@ - - - - eval.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - EVAL -

-
- - -

Copyright(c) 2015 Stefano Balietti -MIT Licensed

-
-

Evaluation of strings as JavaScript commands

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    function EVAL() {}
-
-
-
-

- - EVAL.eval -

-
- -
-

Cross-browser eval function with context.

- -

If no context is passed a reference, this is used.

- -

In old IEs it will use window.execScript instead.

-
-
-
Params
-
- str - string - The command to executes -
-
- context - object - Optional. Execution context. Defaults, this -
-
Returns
-
- - mixed - The return value of the executed commands -
-
See
-
- eval -
-
See
-
- execScript -
-
See
-
- JSON.parse - -
-
-
-
    EVAL.eval = function(str, context) {
-        var func;
-        if (!str) return;
-        context = context || this;
-
-
- -
-

Eval must be called indirectly -i.e. eval.call is not possible

-
        func = function(str) {
-
-
- -
-

TODO: Filter str.

-
            str = '(' + str + ')';
-            if ('undefined' !== typeof window && window.execScript) {
-
-
- -
-

Notice: execScript doesn’t return anything.

-
                window.execScript('__my_eval__ = ' + str);
-                return __my_eval__;
-            }
-            else {
-                return eval(str);
-            }
-        };
-        return func.call(context, str);
-    };
-
-    JSUS.extend(EVAL);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/fs.js.html b/docs/lib/fs.js.html deleted file mode 100644 index dd6d9c6..0000000 --- a/docs/lib/fs.js.html +++ /dev/null @@ -1,556 +0,0 @@ - - - - fs.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - FS -

-
- - -

Copyright(c) 2016 Stefano Balietti -MIT Licensed

-
-

Collection of static functions related to file system operations

-
- -
-
(function(JSUS) {
-
-    "use strict";
-
-    if (!JSUS.isNodeJS()){
-        JSUS.log('Cannot load JSUS.FS outside of Node.JS.');
-        return false;
-    }
-
-    var resolve = require('resolve'),
-    path = require('path'),
-    fs = require('fs')
-
-
-    function FS() {}
-
-
-
-

- - FS.existsSync -

-
- -
-

Backward-compatible version of fs.existsSync

-
-
-
    FS.existsSync = ('undefined' === typeof fs.existsSync) ?
-        path.existsSync : fs.existsSync;
-
-
-
-

- - FS.resolveModuleDir -

-
- -
-

Resolves the root directory of a module

- -

Npm does not install a dependency if the same module -is available in a parent folder. This method returns -the full path of the root directory of the specified -module as installed by npm.

- -

Trailing slash is added.

-
-
-
Params
-
- module - string - The name of the module -
-
- basedir - string - Optional. The basedir from which to start - searching -
-
Returns
-
- - string - The path of the root directory of the module -
-
See
- -
-
-
    FS.resolveModuleDir = function(module, basedir) {
-        var str, stop;
-        if ('string' !== typeof module) {
-            throw new TypeError('FS.resolveModuleDir: module must be string.');
-        }
-        if (basedir && 'string' !== typeof basedir) {
-            throw new TypeError('FS.resolveModuleDir: basedir must be ' +
-                                'string or undefined.');
-        }
-
-
- -
-

Added this line because it might fail.

-
        if (module === 'JSUS') return path.resolve(__dirname, '..') + '/';
-        str = resolve.sync(module, {basedir: basedir || __dirname});
-        stop = str.indexOf(module) + module.length;
-        return str.substr(0, stop) + '/';
-    };
-
-
-
-

- - FS.deleteIfExists -

-
- -
-

Deletes a file or directory

- -

Returns false if the file does not exist.

-
-
-
Params
-
- file - string - The path to the file or directory -
-
- cb - function - Optional. A callback to execute after successful - file deletion -
-
Returns
-
- - boolean - TRUE, if file is found. An error can still occurs - during async removal -
-
See
-
- FS.cleanDir - -
-
-
-
    FS.deleteIfExists = function(file, cb) {
-        var stats;
-        if (!FS.existsSync(file)) {
-            return false;
-        }
-        stats = fs.lstatSync(file);
-        if (stats.isDirectory()) {
-            fs.rmdir(file, function(err) {
-                if (err) throw err;
-                if (cb) cb();
-            });
-        }
-        else {
-            fs.unlink(file, function(err) {
-                if (err) throw err;
-                if (cb) cb();
-            });
-        }
-        return true;
-    };
-
-
-
-

- - FS.cleanDir -

-
- -
-

Removes all files from a target directory

- -

It is possible to specify an extension as second parameter. -In such case, only file with that extension will be removed. -The '.' (dot) must be included as part of the extension.

-
-
-
Params
-
- dir - string - The directory to clean -
-
- ext - string - Optional. If set, only files with this extension - will be removed -
-
- cb - function - Optional. A callback function to call if - no error is raised -
-
Returns
-
- - boolean - TRUE, if the operation is successful -
-
See
-
- FS.deleteIfExists - -
-
-
-
    FS.cleanDir = function(dir, ext, cb) {
-        var filterFunc;
-        if (!dir) {
-            JSUS.log('You must specify a directory to clean.');
-            return false;
-        }
-        if (ext) {
-            filterFunc = function(file) {
-                return path.extname(file) ===  ext;
-            };
-        }
-        else {
-            filterFunc = function() {
-                return true;
-            };
-        }
-
-        if (dir[dir.length] !== '/') dir = dir + '/';
-
-        fs.readdir(dir, function(err, files) {
-            var asq, mycb;
-            if (err) {
-                JSUS.log(err);
-                return;
-            }
-
-
- -
-

Create async queue if a callback was specified.

-
            if (cb) asq = JSUS.getQueue();
-
-
- -
-

Create a nested callback for the async queue, if necessary.

-
            files.filter(filterFunc).forEach(function(file) {
-                if (cb) {
-                    asq.add(file);
-                    mycb = asq.getRemoveCb(file);
-                }
-                JSUS.deleteIfExists(dir + file, mycb);
-            });
-
-            if (cb) {
-                asq.onReady(cb);
-            }
-        });
-
-        return true;
-    };
-
-
-
-

- - FS.copyFromDir -

-
- -
-

Copies all files from a source directory to a destination -directory.

- -

It is possible to specify an extension as second parameter (e.g. '.js'). -In such case, only file with that extension will be copied.

- -

Warning! If an extension filter is not specified, and if subdirectories -are found, an error will occur.

-
-
-
Params
-
- dirIn - string - The source directory -
-
- dirOut - string - The destination directory -
-
- ext - string - Optional. If set, only files with this extension - will be copied -
-
- cb - function - Optional. A callback function to call if - no error is raised -
-
Returns
-
- - boolean - TRUE, if the operation is successful -
-
See
-
- FS.copyFile - -
-
-
-
    FS.copyFromDir = function(dirIn, dirOut, ext, cb) {
-        var i, dir, dirs, stats;
-        if (!dirIn) {
-            JSUS.log('You must specify a source directory.');
-            return false;
-        }
-        if (!dirOut) {
-            JSUS.log('You must specify a destination directory.');
-            return false;
-        }
-
-        dirOut = path.resolve(dirOut) + '/';
-        dirs = [dirIn, dirOut];
-
-        for (i = 0; i < 2; i++) {
-            dir = dirs[i];
-            if (!FS.existsSync(dir)) {
-                console.log(dir + ' does not exist.');
-                return false;
-            }
-
-            stats = fs.lstatSync(dir);
-            if (!stats.isDirectory()) {
-                console.log(dir + ' is not a directory.');
-                return false;
-            }
-        }
-
-        fs.readdir(dirIn, function(err, files) {
-            var asq, i, mycb;
-            if (err) {
-                JSUS.log(err);
-                throw new Error();
-            }
-
-
- -
-

Create async queue if a callback was specified.

-
            if (cb) asq = JSUS.getQueue();
-            for (i in files) {
-                if (ext && path.extname(files[i]) !== ext) {
-                    continue;
-                }
-
-
- -
-

Create a nested callback for the asq, if necessary.

-
                if (cb) {
-                    asq.add(i);
-                    mycb = asq.getRemoveCb(i);
-                }
-                FS.copyFile(dirIn + files[i], dirOut + files[i], mycb);
-            }
-
-            if (cb) {
-                asq.onReady(cb);
-            }
-        });
-
-        return true;
-    };
-
-
-
-

- - FS.copyFile -

-
- -
-

Copies a file into another path

-
-
-
Params
-
- srcFile - string - The source file -
-
- destFile - string - The destination file -
-
- cb - function - Optional. If set, the callback will be executed - upon success -
-
Returns
-
- - boolean - TRUE, if the operation is successful -
-
-
-
    FS.copyFile = function(srcFile, destFile, cb) {
-        var fdr, fdw;
-        fdr = fs.createReadStream(srcFile);
-        fdw = fs.createWriteStream(destFile);
-        fdr.on('end', function() {
-            if (cb) return cb(null);
-        });
-        return fdr.pipe(fdw);
-    };
-
-    JSUS.extend(FS);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/obj.js.html b/docs/lib/obj.js.html deleted file mode 100644 index 3e78c61..0000000 --- a/docs/lib/obj.js.html +++ /dev/null @@ -1,2433 +0,0 @@ - - - - obj.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - OBJ -

-
- - -

Copyright(c) 2017 Stefano Balietti -MIT Licensed

-
-

Collection of static functions to manipulate JavaScript objects

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    function OBJ() {}
-
-    var compatibility = null;
-
-    if ('undefined' !== typeof JSUS.compatibility) {
-        compatibility = JSUS.compatibility();
-    }
-
-
-
-

- - OBJ.createObj -

-
- -
-

Polyfill for Object.create (when missing)

-
-
-
    OBJ.createObj = (function() {
-
-
- -
-

From MDN Object.create (Polyfill)

-
        if (typeof Object.create !== 'function') {
-
-
- -
-

Production steps of ECMA-262, Edition 5, 15.2.3.5 -Reference: http://es5.github.io/#x15.2.3.5

-
            return (function() {
-
-
- -
-

To save on memory, use a shared constructor

-
                function Temp() {}
-
-
- -
-

make a safe reference to Object.prototype.hasOwnProperty

-
                var hasOwn = Object.prototype.hasOwnProperty;
-
-                return function(O) {
-
-
- -
-
    -
  1. If Type(O) is not Object or Null
  2. -
-
                    if (typeof O != 'object') {
-                        throw new TypeError('Object prototype may only ' +
-                                            'be an Object or null');
-                    }
-
-
- -
-
    -
  1. Let obj be the result of creating a new object as if -by the expression new Object() where Object is the -standard built-in constructor with that name
  2. -
  3. Set the [[Prototype]] internal property of obj to O.
  4. -
-
                    Temp.prototype = O;
-                    var obj = new Temp();
-                    Temp.prototype = null;
-
-
- -
-
    -
  1. If the argument Properties is present and not -undefined, add own properties to obj as if by calling -the standard built-in function Object.defineProperties -with arguments obj and Properties.
  2. -
-
                    if (arguments.length > 1) {
-
-
- -
-

Object.defineProperties does ToObject on -its first argument.

-
                        var Properties = new Object(arguments[1]);
-                        for (var prop in Properties) {
-                            if (hasOwn.call(Properties, prop)) {
-                                obj[prop] = Properties[prop];
-                            }
-                        }
-                    }
-
-
- -
-
    -
  1. Return obj
  2. -
-
                    return obj;
-                };
-            })();
-        }
-        return Object.create;
-    })();
-
-
-
-

- - OBJ.equals -

-
- -
-

Checks for deep equality between two objects, strings or primitive types

- -

All nested properties are checked, and if they differ in at least -one returns FALSE, otherwise TRUE.

- -

Takes care of comparing the following special cases:

- -
    -
  • undefined
  • -
  • null
  • -
  • NaN
  • -
  • Infinity
  • -
  • {}
  • -
  • falsy values
  • -
-
-
-
Params
-
- o1 - object - The first object -
-
- o2 - object - The second object -
-
Returns
-
- - boolean - TRUE if the objects are deeply equal -
-
-
-
    OBJ.equals = function(o1, o2) {
-        var type1, type2, primitives, p;
-        type1 = typeof o1;
-        type2 = typeof o2;
-
-        if (type1 !== type2) return false;
-
-        if ('undefined' === type1 || 'undefined' === type2) {
-            return (o1 === o2);
-        }
-        if (o1 === null || o2 === null) {
-            return (o1 === o2);
-        }
-        if (('number' === type1 && isNaN(o1)) &&
-            ('number' === type2 && isNaN(o2))) {
-            return (isNaN(o1) && isNaN(o2));
-        }
-
-
- -
-

Check whether arguments are not objects

-
        primitives = {number: '', string: '', boolean: ''};
-        if (type1 in primitives) {
-            return o1 === o2;
-        }
-
-        if ('function' === type1) {
-            return o1.toString() === o2.toString();
-        }
-
-        for (p in o1) {
-            if (o1.hasOwnProperty(p)) {
-
-                if ('undefined' === typeof o2[p] &&
-                    'undefined' !== typeof o1[p]) return false;
-
-                if (!o2[p] && o1[p]) return false;
-
-                if ('function' === typeof o1[p]) {
-                    if (o1[p].toString() !== o2[p].toString()) return false;
-                }
-                else
-                    if (!OBJ.equals(o1[p], o2[p])) return false;
-            }
-        }
-
-
- -
-

Check whether o2 has extra properties -TODO: improve, some properties have already been checked!

-
        for (p in o2) {
-            if (o2.hasOwnProperty(p)) {
-                if ('undefined' === typeof o1[p] &&
-                    'undefined' !== typeof o2[p]) return false;
-
-                if (!o1[p] && o2[p]) return false;
-            }
-        }
-
-        return true;
-    };
-
-
-
-

- - OBJ.isEmpty -

-
- -
-

Returns TRUE if an object has no own properties (supports other types)

- -

Map of input-type and return values:

- -
    -
  • undefined: TRUE
  • -
  • null: TRUE
  • -
  • string: TRUE if string === '' or if contains only spaces
  • -
  • number: FALSE if different from 0
  • -
  • function: FALSE
  • -
  • array: TRUE, if it contains zero elements
  • -
  • object: TRUE, if it does not contain own properties
  • -
- -

Notice: for object, it is much faster than Object.keys(o).length === 0, -because it does not pull out all keys. Own properties must be enumerable.

-
-
-
Params
-
- o - mixed - The object (or other type) to check -
-
Returns
-
- - boolean - TRUE, if the object is empty -
-
-
-
    OBJ.isEmpty = function(o) {
-        var key;
-        if (!o) return true;
-        if ('string' === typeof o) return o.trim() === '';
-        if ('number' === typeof o) return false;
-        if ('function' === typeof o) return false;
-        for (key in o) if (o.hasOwnProperty(key)) return false;
-        return true;
-    };
-
-
-
-

- - OBJ.size -

-
- -
-

Counts the number of own properties of an object.

- -

Prototype chain properties are excluded.

-
-
-
Params
-
- obj - object - The object to check -
-
Returns
-
- - number - The number of properties in the object -
-
-
-
    OBJ.size = OBJ.getListSize = function(obj) {
-        var n, key;
-        if (!obj) return 0;
-        if ('number' === typeof obj) return 0;
-        if ('string' === typeof obj) return 0;
-
-        n = 0;
-        for (key in obj) {
-            if (obj.hasOwnProperty(key)) {
-                n++;
-            }
-        }
-        return n;
-    };
-
-
-
-

- - OBJ._obj2Array -

-
- -
-

Explodes an object into an array of keys and values, -according to the specified parameters.

- -

A fixed level of recursion can be set.

-
-
-
API
-
- private - -
Params
-
- obj - object - The object to convert in array -
-
- keyed - boolean - TRUE, if also property names should be included. - Defaults, FALSE -
-
- level - number - Optional. The level of recursion. - Defaults, undefined -
-
Returns
-
- - array - The converted object -
-
-
-
    OBJ._obj2Array = function(obj, keyed, level, cur_level) {
-        var result, key;
-        if ('object' !== typeof obj) return [obj];
-
-        if (level) {
-            cur_level = ('undefined' !== typeof cur_level) ? cur_level : 1;
-            if (cur_level > level) return [obj];
-            cur_level = cur_level + 1;
-        }
-
-        result = [];
-        for (key in obj) {
-            if (obj.hasOwnProperty(key)) {
-                if (keyed) result.push(key);
-                if ('object' === typeof obj[key]) {
-                    result = result.concat(OBJ._obj2Array(obj[key], keyed,
-                                                          level, cur_level));
-                }
-                else {
-                    result.push(obj[key]);
-                }
-            }
-        }
-        return result;
-    };
-
-
-
-

- - OBJ.obj2Array -

-
- -
-

Converts an object into an array, keys are lost

- -

Recursively put the values of the properties of an object into -an array and returns it.

- -

The level of recursion can be set with the parameter level. -By default recursion has no limit, i.e. that the whole object -gets totally unfolded into an array.

-
-
-
Params
-
- obj - object - The object to convert in array -
-
- level - number - Optional. The level of recursion. Defaults, - undefined -
-
Returns
-
- - array - The converted object -
-
See
-
- OBJ._obj2Array -
-
See
-
- OBJ.obj2KeyedArray - -
-
-
-
    OBJ.obj2Array = function(obj, level) {
-        return OBJ._obj2Array(obj, false, level);
-    };
-
-
-
-

- - OBJ.obj2KeyedArray -

-
- -
-

Converts an object into array, keys are preserved

- -

Creates an array containing all keys and values of an object and -returns it.

-
-
-
Params
-
- obj - object - The object to convert in array -
-
- level - number - Optional. The level of recursion. Defaults, - undefined -
-
Returns
-
- - array - The converted object -
-
See
-
- OBJ.obj2Array - -
-
-
-
    OBJ.obj2KeyedArray = OBJ.obj2KeyArray = function(obj, level) {
-        return OBJ._obj2Array(obj, true, level);
-    };
-
-
-
-

- - OBJ.obj2QueryString -

-
- -
-

Creates a querystring with the key-value pairs of the given object.

-
-
-
Params
-
- obj - object - The object to convert -
-
Returns
-
- - string - The created querystring - -Kudos: -
-
See
- -
-
-
    OBJ.obj2QueryString = function(obj) {
-        var str;
-        var key;
-
-        if ('object' !== typeof obj) {
-            throw new TypeError(
-                    'JSUS.objectToQueryString: obj must be object.');
-        }
-
-        str = [];
-        for (key in obj) {
-            if (obj.hasOwnProperty(key)) {
-                str.push(encodeURIComponent(key) + '=' +
-                         encodeURIComponent(obj[key]));
-            }
-        }
-
-        return '?' + str.join('&');
-    };
-
-
-
-

- - OBJ.keys -

-
- -
-

Scans an object an returns all the keys of the properties, -into an array.

- -

The second paramter controls the level of nested objects -to be evaluated. Defaults 0 (nested properties are skipped).

-
-
-
Params
-
- obj - object - The object from which extract the keys -
-
- level - number - Optional. The level of recursion. Defaults 0 -
-
Returns
-
- - array - The array containing the extracted keys -
-
See
-
- Object.keys - -
-
-
-
    OBJ.keys = OBJ.objGetAllKeys = function(obj, level, curLevel) {
-        var result, key;
-        if (!obj) return [];
-        level = 'number' === typeof level && level >= 0 ? level : 0;
-        curLevel = 'number' === typeof curLevel && curLevel >= 0 ? curLevel : 0;
-        result = [];
-        for (key in obj) {
-            if (obj.hasOwnProperty(key)) {
-                result.push(key);
-                if (curLevel < level) {
-                    if ('object' === typeof obj[key]) {
-                        result = result.concat(OBJ.objGetAllKeys(obj[key],
-                                                                 (curLevel+1)));
-                    }
-                }
-            }
-        }
-        return result;
-    };
-
-
-
-

- - OBJ.implode -

-
- -
-

Separates each property into a new object and returns them into an array

- -

E.g.

- - -
var a = { b:2, c: {a:1}, e:5 };
-OBJ.implode(a); // [{b:2}, {c:{a:1}}, {e:5}]
-
- - -
-
-
Params
-
- obj - object - The object to implode -
-
Returns
-
- - array - The array containing all the imploded properties -
-
-
-
    OBJ.implode = OBJ.implodeObj = function(obj) {
-        var result, key, o;
-        if (!obj) return [];
-        result = [];
-        for (key in obj) {
-            if (obj.hasOwnProperty(key)) {
-                o = {};
-                o[key] = obj[key];
-                result.push(o);
-            }
-        }
-        return result;
-    };
-
-
-
-

- - OBJ.clone -

-
- -
-

Creates a perfect copy of the object passed as parameter

- -

Recursively scans all the properties of the object to clone. -Properties of the prototype chain are copied as well.

- -

Primitive types and special values are returned as they are.

-
-
-
Params
-
- obj - object - The object to clone -
-
Returns
-
- - object - The clone of the object -
-
-
-
    OBJ.clone = function(obj) {
-        var clone, i, value;
-        if (!obj) return obj;
-        if ('number' === typeof obj) return obj;
-        if ('string' === typeof obj) return obj;
-        if ('boolean' === typeof obj) return obj;
-
-
- -
-

NaN and +-Infinity are numbers, so no check is necessary.

-
        if ('function' === typeof obj) {
-            clone = function() {
-                var len, args;
-                len = arguments.length;
-                if (!len) return obj.call(clone);
-                else if (len === 1) return obj.call(clone, arguments[0]);
-                else if (len === 2) {
-                    return obj.call(clone, arguments[0], arguments[1]);
-                }
-                else {
-                    args = new Array(len);
-                    for (i = 0; i < len; i++) {
-                        args[i] = arguments[i];
-                    }
-                    return obj.apply(clone, args);
-                }
-            };
-        }
-        else {
-            clone = Object.prototype.toString.call(obj) === '[object Array]' ?
-                [] : {};
-        }
-        for (i in obj) {
-
-
- -
-

It is not NULL and it is an object. -Even if it is an array we need to use CLONE, -because slice() does not clone arrays of objects.

-
            if (obj[i] && 'object' === typeof obj[i]) {
-                value = OBJ.clone(obj[i]);
-            }
-            else {
-                value = obj[i];
-            }
-
-            if (obj.hasOwnProperty(i)) {
-                clone[i] = value;
-            }
-            else {
-
-
- -
-

We know if object.defineProperty is available.

-
                if (compatibility && compatibility.defineProperty) {
-                    Object.defineProperty(clone, i, {
-                        value: value,
-                        writable: true,
-                        configurable: true
-                    });
-                }
-                else {
-                    setProp(clone, i, value);
-                }
-            }
-        }
-        return clone;
-    };
-
-    function setProp(clone, i, value) {
-        try {
-            Object.defineProperty(clone, i, {
-                value: value,
-                writable: true,
-                configurable: true
-            });
-        }
-        catch(e) {
-            clone[i] = value;
-        }
-    }
-
-
-
-

- - OBJ.classClone -

-
- -
-

Creates a copy (keeping class) of the object passed as parameter

- -

Recursively scans all the properties of the object to clone. -The clone is an instance of the type of obj.

-
-
-
Params
-
- obj - object - The object to clone -
-
- depth - Number - how deep the copy should be -
-
Returns
-
- - object - The clone of the object -
-
-
-
    OBJ.classClone = function(obj, depth) {
-        var clone, i;
-        if (depth === 0) {
-            return obj;
-        }
-
-        if (obj && 'object' === typeof obj) {
-            clone = Object.prototype.toString.call(obj) === '[object Array]' ?
-                [] : JSUS.createObj(obj.constructor.prototype);
-
-            for (i in obj) {
-                if (obj.hasOwnProperty(i)) {
-                    if (obj[i] && 'object' === typeof obj[i]) {
-                        clone[i] = JSUS.classClone(obj[i], depth - 1);
-                    }
-                    else {
-                        clone[i] = obj[i];
-                    }
-                }
-            }
-            return clone;
-        }
-        else {
-            return JSUS.clone(obj);
-        }
-    };
-
-
-
-

- - OBJ.join -

-
- -
-

Performs a left join on the keys of two objects

- -

Creates a copy of obj1, and in case keys overlap -between obj1 and obj2, the values from obj2 are taken.

- -

Returns a new object, the original ones are not modified.

- -

E.g.

- - -
var a = { b:2, c:3, e:5 };
-var b = { a:10, b:2, c:100, d:4 };
-OBJ.join(a, b); // { b:2, c:100, e:5 }
-
- - -
-
-
Params
-
- obj1 - object - The object where the merge will take place -
-
- obj2 - object - The merging object -
-
Returns
-
- - object - The joined object -
-
See
-
- OBJ.merge - -
-
-
-
    OBJ.join = function(obj1, obj2) {
-        var clone, i;
-        clone = OBJ.clone(obj1);
-        if (!obj2) return clone;
-        for (i in clone) {
-            if (clone.hasOwnProperty(i)) {
-                if ('undefined' !== typeof obj2[i]) {
-                    if ('object' === typeof obj2[i]) {
-                        clone[i] = OBJ.join(clone[i], obj2[i]);
-                    } else {
-                        clone[i] = obj2[i];
-                    }
-                }
-            }
-        }
-        return clone;
-    };
-
-
-
-

- - OBJ.merge -

-
- -
-

Merges two objects in one

- -

In case keys overlap the values from obj2 are taken.

- -

Only own properties are copied.

- -

Returns a new object, the original ones are not modified.

- -

E.g.

- - -
var a = { a:1, b:2, c:3 };
-var b = { a:10, b:2, c:100, d:4 };
-OBJ.merge(a, b); // { a: 10, b: 2, c: 100, d: 4 }
-
- - -
-
-
Params
-
- obj1 - object - The object where the merge will take place -
-
- obj2 - object - The merging object -
-
Returns
-
- - object - The merged object -
-
See
-
- OBJ.join -
-
See
-
- OBJ.mergeOnKey - -
-
-
-
    OBJ.merge = function(obj1, obj2) {
-        var clone, i;
-
-
- -
-

Checking before starting the algorithm

-
        if (!obj1 && !obj2) return false;
-        if (!obj1) return OBJ.clone(obj2);
-        if (!obj2) return OBJ.clone(obj1);
-
-        clone = OBJ.clone(obj1);
-        for (i in obj2) {
-
-            if (obj2.hasOwnProperty(i)) {
-
-
- -
-

it is an object and it is not NULL

-
                if (obj2[i] && 'object' === typeof obj2[i]) {
-
-
- -
-

If we are merging an object into -a non-object, we need to cast the -type of obj1

-
                    if ('object' !== typeof clone[i]) {
-                        if (Object.prototype.toString.call(obj2[i]) ===
-                            '[object Array]') {
-
-                            clone[i] = [];
-                        }
-                        else {
-                            clone[i] = {};
-                        }
-                    }
-                    clone[i] = OBJ.merge(clone[i], obj2[i]);
-                }
-                else {
-                    clone[i] = obj2[i];
-                }
-            }
-        }
-        return clone;
-    };
-
-
-
-

- - OBJ.mixin -

-
- -
-

Adds all the properties of obj2 into obj1

- -

Original object is modified.

-
-
-
Params
-
- obj1 - object - The object to which the new properties will be added -
-
- obj2 - object - The mixin-in object -
-
Returns
-
- - object - obj1 -
-
-
-
    OBJ.mixin = function(obj1, obj2) {
-        var i;
-        if (!obj1 && !obj2) return;
-        if (!obj1) return obj2;
-        if (!obj2) return obj1;
-        for (i in obj2) {
-            obj1[i] = obj2[i];
-        }
-        return obj1;
-    };
-
-
-
-

- - OBJ.mixout -

-
- -
-

Copies only non-overlapping properties from obj2 to obj1

- -

Check only if a property is defined, not its value. -Original object is modified.

-
-
-
Params
-
- obj1 - object - The object to which the new properties will be added -
-
- obj2 - object - The mixin-in object -
-
Returns
-
- - object - obj1 -
-
-
-
    OBJ.mixout = function(obj1, obj2) {
-        var i;
-        if (!obj1 && !obj2) return;
-        if (!obj1) return obj2;
-        if (!obj2) return obj1;
-        for (i in obj2) {
-            if ('undefined' === typeof obj1[i]) obj1[i] = obj2[i];
-        }
-        return obj1;
-    };
-
-
-
-

- - OBJ.mixcommon -

-
- -
-

Copies only overlapping properties from obj2 to obj1

- -

Check only if a property is defined, not its value. -Original object is modified.

-
-
-
Params
-
- obj1 - object - The object to which the new properties will be added -
-
- obj2 - object - The mixin-in object -
-
Returns
-
- - object - obj1 -
-
-
-
    OBJ.mixcommon = function(obj1, obj2) {
-        var i;
-        if (!obj1 && !obj2) return;
-        if (!obj1) return obj2;
-        if (!obj2) return obj1;
-        for (i in obj2) {
-            if ('undefined' !== typeof obj1[i]) obj1[i] = obj2[i];
-        }
-        return obj1;
-    };
-
-
-
-

- - OBJ.mergeOnKey -

-
- -
-

Merges the properties of obj2 into a new property named 'key' in obj1.

- -

Returns a new object, the original ones are not modified.

- -

This method is useful when we want to merge into a larger -configuration (e.g. with properties min, max, value) object, another one -that contains just a subset of properties (e.g. value).

-
-
-
Params
-
- obj1 - object - The object where the merge will take place -
-
- obj2 - object - The merging object -
-
- key - string - The name of property under which the second object - will be merged -
-
Returns
-
- - object - The merged object -
-
See
-
- OBJ.merge - -
-
-
-
    OBJ.mergeOnKey = function(obj1, obj2, key) {
-        var clone, i;
-        clone = OBJ.clone(obj1);
-        if (!obj2 || !key) return clone;
-        for (i in obj2) {
-            if (obj2.hasOwnProperty(i)) {
-                if (!clone[i] || 'object' !== typeof clone[i]) {
-                    clone[i] = {};
-                }
-                clone[i][key] = obj2[i];
-            }
-        }
-        return clone;
-    };
-
-
-
-

- - OBJ.subobj -

-
- -
-

Creates a copy of an object containing only the properties -passed as second parameter

- -

The parameter select can be an array of strings, or the name -of a property.

- -

Use '.' (dot) to point to a nested property, however if a property -with a '.' in the name is found, it will be used first.

-
-
-
Params
-
- o - object - The object to dissect -
-
- select - string - array - The selection of properties to extract -
-
Returns
-
- - object - The subobject with the properties from the parent -
-
See
-
- OBJ.getNestedValue - -
-
-
-
    OBJ.subobj = function(o, select) {
-        var out, i, key;
-        if (!o) return false;
-        out = {};
-        if (!select) return out;
-        if (!(select instanceof Array)) select = [select];
-        for (i=0; i < select.length; i++) {
-            key = select[i];
-            if (o.hasOwnProperty(key)) {
-                out[key] = o[key];
-            }
-            else if (OBJ.hasOwnNestedProperty(key, o)) {
-                OBJ.setNestedValue(key, OBJ.getNestedValue(key, o), out);
-            }
-        }
-        return out;
-    };
-
-
-
-

- - OBJ.skim -

-
- -
-

Creates a copy of an object with some of the properties removed

- -

The parameter remove can be an array of strings, or the name -of a property.

- -

Use '.' (dot) to point to a nested property, however if a property -with a '.' in the name is found, it will be deleted first.

-
-
-
Params
-
- o - object - The object to dissect -
-
- remove - string - array - The selection of properties to remove -
-
Returns
-
- - object - The subobject with the properties from the parent -
-
See
-
- OBJ.getNestedValue - -
-
-
-
    OBJ.skim = function(o, remove) {
-        var out, i;
-        if (!o) return false;
-        out = OBJ.clone(o);
-        if (!remove) return out;
-        if (!(remove instanceof Array)) remove = [remove];
-        for (i = 0; i < remove.length; i++) {
-            if (out.hasOwnProperty(i)) {
-                delete out[i];
-            }
-            else {
-                OBJ.deleteNestedKey(remove[i], out);
-            }
-        }
-        return out;
-    };
-
-
-
-

- - OBJ.setNestedValue -

-
- -
-

Sets the value of a nested property of an object and returns it.

- -

If the object is not passed a new one is created. -If the nested property is not existing, a new one is created.

- -

Use '.' (dot) to point to a nested property.

- -

The original object is modified.

-
-
-
Params
-
- str - string - The path to the value -
-
- value - mixed - The value to set -
-
Returns
-
- - object - boolean - The modified object, or FALSE if error - occurrs -
-
See
-
- OBJ.getNestedValue -
-
See
-
- OBJ.deleteNestedKey - -
-
-
-
    OBJ.setNestedValue = function(str, value, obj) {
-        var keys, k;
-        if (!str) {
-            JSUS.log('Cannot set value of undefined property', 'ERR');
-            return false;
-        }
-        obj = ('object' === typeof obj) ? obj : {};
-        keys = str.split('.');
-        if (keys.length === 1) {
-            obj[str] = value;
-            return obj;
-        }
-        k = keys.shift();
-        obj[k] = OBJ.setNestedValue(keys.join('.'), value, obj[k]);
-        return obj;
-    };
-
-
-
-

- - OBJ.getNestedValue -

-
- -
-

Returns the value of a property of an object, as defined -by a path string.

- -

Use '.' (dot) to point to a nested property.

- -

Returns undefined if the nested property does not exist.

- -

E.g.

- - -
var o = { a:1, b:{a:2} };
-OBJ.getNestedValue('b.a', o); // 2
-
- - -
-
-
Params
-
- str - string - The path to the value -
-
- obj - object - The object from which extract the value -
-
Returns
-
- - mixed - The extracted value -
-
See
-
- OBJ.setNestedValue -
-
See
-
- OBJ.deleteNestedKey - -
-
-
-
    OBJ.getNestedValue = function(str, obj) {
-        var keys, k;
-        if (!obj) return;
-        keys = str.split('.');
-        if (keys.length === 1) {
-            return obj[str];
-        }
-        k = keys.shift();
-        return OBJ.getNestedValue(keys.join('.'), obj[k]);
-    };
-
-
-
-

- - OBJ.deleteNestedKey -

-
- -
-

Deletes a property from an object, as defined by a path string

- -

Use '.' (dot) to point to a nested property.

- -

The original object is modified.

- -

E.g.

- - -
var o = { a:1, b:{a:2} };
-OBJ.deleteNestedKey('b.a', o); // { a:1, b: {} }
-
- - -
-
-
Params
-
- str - string - The path string -
-
- obj - object - The object from which deleting a property -
-
- TRUE, - boolean - if the property was existing, and then deleted -
-
See
-
- OBJ.setNestedValue -
-
See
-
- OBJ.getNestedValue - -
-
-
-
    OBJ.deleteNestedKey = function(str, obj) {
-        var keys, k;
-        if (!obj) return;
-        keys = str.split('.');
-        if (keys.length === 1) {
-            delete obj[str];
-            return true;
-        }
-        k = keys.shift();
-        if ('undefined' === typeof obj[k]) {
-            return false;
-        }
-        return OBJ.deleteNestedKey(keys.join('.'), obj[k]);
-    };
-
-
-
-

- - OBJ.hasOwnNestedProperty -

-
- -
-

Returns TRUE if a (nested) property exists

- -

Use '.' to specify a nested property.

- -

E.g.

- - -
var o = { a:1, b:{a:2} };
-OBJ.hasOwnNestedProperty('b.a', o); // TRUE
-
- - -
-
-
Params
-
- str - string - The path of the (nested) property -
-
- obj - object - The object to test -
-
Returns
-
- - boolean - TRUE, if the (nested) property exists -
-
-
-
    OBJ.hasOwnNestedProperty = function(str, obj) {
-        var keys, k;
-        if (!obj) return false;
-        keys = str.split('.');
-        if (keys.length === 1) {
-            return obj.hasOwnProperty(str);
-        }
-        k = keys.shift();
-        return OBJ.hasOwnNestedProperty(keys.join('.'), obj[k]);
-    };
-
-
-
-

- - OBJ.split -

-
- -
-

Splits an object along a specified dimension

- -

All fragments are returned in an array (as copies).

- -

It creates as many new objects as the number of properties -contained in the specified dimension. E.g.

- - -
 var o = { a: 1,
-           b: {c: 2,
-               d: 3
-           },
-           e: 4
- };
-
- o = OBJ.split(o, 'b');
-
- // o becomes:
-
- [{ a: 1,
-    b: {c: 2},
-    e: 4
- },
- { a: 1,
-   b: {d: 3},
-   e: 4
- }];
-
- - -
-
-
Params
-
- o - object - The object to split -
-
- key - string - The name of the property to split -
-
- l - number - Optional. The recursion level. Default: 1. -
-
- positionAsKey - boolean - Optional. If TRUE, the position - of an element in the array to split will be used as key. -
-
Returns
-
- - array - A list of copies of the object with split values -
-
-
-
    OBJ.split = (function() {
-        var makeClone, splitValue;
-        var model, level, _key, posAsKeys;
-
-        makeClone = function(value, out, keys) {
-            var i, len, tmp, copy;
-            copy = JSUS.clone(model);
-
-            switch(keys.length) {
-            case 0:
-                copy[_key] = JSUS.clone(value);
-                break;
-            case 1:
-                copy[_key][keys[0]] = JSUS.clone(value);
-                break;
-            case 2:
-                copy[_key][keys[0]] = {};
-                copy[_key][keys[0]][keys[1]] = JSUS.clone(value);
-                break;
-            default:
-                i = -1, len = keys.length-1;
-                tmp = copy[_key];
-                for ( ; ++i < len ; ) {
-                    tmp[keys[i]] = {};
-                    tmp = tmp[keys[i]];
-                }
-                tmp[keys[keys.length-1]] = JSUS.clone(value);
-            }
-            out.push(copy);
-            return;
-        };
-
-        splitValue = function(value, out, curLevel, keysArray) {
-            var i, curPosAsKey;
-
-
- -
-

level == 0 means no limit.

-
            if (level && (curLevel >= level)) {
-                makeClone(value, out, keysArray);
-            }
-            else {
-
-                curPosAsKey = posAsKeys || !JSUS.isArray(value);
-
-                for (i in value) {
-                    if (value.hasOwnProperty(i)) {
-
-                        if ('object' === typeof value[i] &&
-                            (level && ((curLevel+1) <= level))) {
-
-                            splitValue(value[i], out, (curLevel+1),
-                                       curPosAsKey ?
-                                       keysArray.concat(i) : keysArray);
-                        }
-                        else {
-                            makeClone(value[i], out, curPosAsKey ?
-                                      keysArray.concat(i) : keysArray);
-                        }
-                    }
-                }
-            }
-        };
-
-        return function(o, key, l, positionAsKey) {
-            var out;
-            if ('object' !== typeof o) {
-                throw new TypeError('JSUS.split: o must be object. Found: ' +
-                                    o);
-            }
-            if ('string' !== typeof key || key.trim() === '') {
-                throw new TypeError('JSUS.split: key must a non-empty ' +
-                                    'string. Found: ' + key);
-            }
-            if (l && ('number' !== typeof l || l < 0)) {
-                throw new TypeError('JSUS.split: l must a non-negative ' +
-                                    'number or undefined. Found: ' + l);
-            }
-            model = JSUS.clone(o);
-            if ('object' !== typeof o[key]) return [model];
-
-
- -
-

Init.

-
            out = [];
-            _key = key;
-            model[key] = {};
-            level = 'undefined' === typeof l ? 1 : l;
-            posAsKeys = positionAsKey;
-
-
- -
-

Recursively compute split.

-
            splitValue(o[key], out, 0, []);
-
-
- -
-

Cleanup.

-
            _key = undefined;
-            model = undefined;
-            level = undefined;
-            posAsKeys = undefined;
-
-
- -
-

Return.

-
            return out;
-        };
-    })();
-
-
-
-

- - OBJ.melt -

-
- -
-

Creates a new object with the specified combination of -properties - values

- -

The values are assigned cyclically to the properties, so that -they do not need to have the same length. E.g.

- - -
 J.createObj(['a','b','c'], [1,2]); // { a: 1, b: 2, c: 1 }
-
- - -
-
-
Params
-
- keys - array - The names of the keys to add to the object -
-
- values - array - The values to associate to the keys -
-
Returns
-
- - object - A new object with keys and values melted together -
-
-
-
    OBJ.melt = function(keys, values) {
-        var o = {}, valen = values.length;
-        for (var i = 0; i < keys.length; i++) {
-            o[keys[i]] = values[i % valen];
-        }
-        return o;
-    };
-
-
-
-

- - OBJ.uniqueKey -

-
- -
-

Creates a random unique key name for a collection

- -

User can specify a tentative unique key name, and if already -existing an incremental index will be added as suffix to it.

- -

Notice: the method does not actually create the key -in the object, but it just returns the name.

-
-
-
Params
-
- obj - object - The collection for which a unique key will be created -
-
- prefixName - string - Optional. A tentative key name. Defaults, - a 15-digit random number -
-
- stop - number - Optional. The number of tries before giving up - searching for a unique key name. Defaults, 1000000. -
-
Returns
-
- - string - undefined - The unique key name, or undefined if it was - not found -
-
-
-
    OBJ.uniqueKey = function(obj, prefixName, stop) {
-        var name, duplicateCounter;
-        if (!obj) {
-            JSUS.log('Cannot find unique name in undefined object', 'ERR');
-            return;
-        }
-        duplicateCounter = 1;
-        prefixName = '' + (prefixName ||
-                           Math.floor(Math.random()*1000000000000000));
-        stop = stop || 1000000;
-        name = prefixName;
-        while (obj[name]) {
-            name = prefixName + duplicateCounter;
-            duplicateCounter++;
-            if (duplicateCounter > stop) {
-                return;
-            }
-        }
-        return name;
-    };
-
-
-
-

- - OBJ.augment -

-
- -
-

Pushes the values of the properties of an object into another one

- -

User can specifies the subset of keys from both objects -that will subject to augmentation. The values of the other keys -will not be changed

- -

Notice: the method modifies the first input paramteer

- -

E.g.

- - -
var a = { a:1, b:2, c:3 };
-var b = { a:10, b:2, c:100, d:4 };
-OBJ.augment(a, b); // { a: [1, 10], b: [2, 2], c: [3, 100]}
-
-OBJ.augment(a, b, ['b', 'c', 'd']);
-// { a: 1, b: [2, 2], c: [3, 100], d: [4]});
-
- - -
-
-
Params
-
- obj1 - object - The object whose properties will be augmented -
-
- obj2 - object - The augmenting object -
-
- key - array - Optional. Array of key names common to both objects - taken as the set of properties to augment -
-
-
-
    OBJ.augment = function(obj1, obj2, keys) {
-        var i, k;
-        keys = keys || OBJ.keys(obj1);
-
-        for (i = 0 ; i < keys.length; i++) {
-            k = keys[i];
-            if ('undefined' !== typeof obj1[k] &&
-                Object.prototype.toString.call(obj1[k]) !== '[object Array]') {
-                obj1[k] = [obj1[k]];
-            }
-            if ('undefined' !== obj2[k]) {
-                if (!obj1[k]) obj1[k] = [];
-                obj1[k].push(obj2[k]);
-            }
-        }
-    };
-
-
-
-

- - OBJ.pairwiseWalk -

-
- -
-

Executes a callback on all pairs of attributes with the same name

- -

The results of each callback are aggregated in a new object under the -same property name.

- -

Does not traverse nested objects, and properties of the prototype -are excluded.

- -

Returns a new object, the original ones are not modified.

- -

E.g.

- - -
var a = { b:2, c:3, d:5 };
-var b = { a:10, b:2, c:100, d:4 };
-var sum = function(a,b) {
-    if ('undefined' !== typeof a) {
-        return 'undefined' !== typeof b ? a + b : a;
-    }
-    return b;
-};
-OBJ.pairwiseWalk(a, b, sum); // { a:10, b:4, c:103, d:9 }
-
- - -
-
-
Params
-
- o1 - object - The first object -
-
- o2 - object - The second object -
-
Returns
-
- - object - The object aggregating the results -
-
-
-
    OBJ.pairwiseWalk = function(o1, o2, cb) {
-        var i, out;
-        if (!o1 && !o2) return;
-        if (!o1) return o2;
-        if (!o2) return o1;
-
-        out = {};
-        for (i in o1) {
-            if (o1.hasOwnProperty(i)) {
-                out[i] = o2.hasOwnProperty(i) ? cb(o1[i], o2[i]) : cb(o1[i]);
-            }
-        }
-
-        for (i in o2) {
-            if (o2.hasOwnProperty(i)) {
-                if ('undefined' === typeof out[i]) {
-                    out[i] = cb(undefined, o2[i]);
-                }
-            }
-        }
-        return out;
-    };
-
-
-
-

- - OBJ.getKeyByValue -

-
- -
-

Returns the key/s associated with a specific value

- -

Uses OBJ.equals so it can perform complicated comparisons of -the value of the keys.

- -

Properties of the prototype are not skipped.

-
-
-
Params
-
- obj - object - The object to search -
-
- value - mixed - The value to match -
-
- allKeys - boolean - Optional. If TRUE, all keys with the - specific value are returned. Default FALSE -
-
Returns
-
- - object - The object aggregating the results -
-
See
-
- OBJ.equals - -
-
-
-
    OBJ.getKeyByValue = function(obj, value, allKeys) {
-        var key, out;
-        if ('object' !== typeof obj) {
-            throw new TypeError('OBJ.getKeyByValue: obj must be object.');
-        }
-        if (allKeys) out = [];
-        for (key in obj) {
-            if (obj.hasOwnProperty(key) ) {
-                if (OBJ.equals(value, obj[key])) {
-                    if (!allKeys) return key;
-                    else out.push(key);
-                }
-            }
-        }
-        return out;
-    };
-
-    JSUS.extend(OBJ);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/parse.js.html b/docs/lib/parse.js.html deleted file mode 100644 index 2b6a949..0000000 --- a/docs/lib/parse.js.html +++ /dev/null @@ -1,1271 +0,0 @@ - - - - parse.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - PARSE -

-
- - -

Copyright(c) 2016 Stefano Balietti -MIT Licensed

-
-

Collection of static functions related to parsing strings

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    function PARSE() {}
-
-
-
-

- - PARSE.stringify_prefix -

-
- -
-

Prefix used by PARSE.stringify and PARSE.parse -to decode strings with special meaning

-
-
-
See
-
- PARSE.stringify -
-
See
-
- PARSE.parse - -
-
-
-
    PARSE.stringify_prefix = '!?_';
-
-    PARSE.marker_func = PARSE.stringify_prefix + 'function';
-    PARSE.marker_null = PARSE.stringify_prefix + 'null';
-    PARSE.marker_und = PARSE.stringify_prefix + 'undefined';
-    PARSE.marker_nan = PARSE.stringify_prefix + 'NaN';
-    PARSE.marker_inf = PARSE.stringify_prefix + 'Infinity';
-    PARSE.marker_minus_inf = PARSE.stringify_prefix + '-Infinity';
-
-
-
-

- - PARSE.getQueryString -

-
- -
-

Parses current querystring and returns the requested variable.

- -

If no variable name is specified, returns the full query string. -If requested variable is not found returns false.

-
-
-
Params
-
- name - string - Optional. If set, returns only the value - associated with this variable -
-
- referer - string - Optional. If set, searches this string -
-
Returns
-
- - string - boolean - The querystring, or a part of it, or FALSE - -Kudos: -
-
See
- -
-
-
    PARSE.getQueryString = function(name, referer) {
-        var regex, results;
-        if (referer && 'string' !== typeof referer) {
-            throw new TypeError('JSUS.getQueryString: referer must be string ' +
-                                'or undefined.');
-        }
-        referer = referer || window.location.search;
-        if ('undefined' === typeof name) return referer;
-        name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
-        regex = new RegExp("[\\?&]" + name + "=([^&#]*)");
-        results = regex.exec(referer);
-        return results === null ? false :
-            decodeURIComponent(results[1].replace(/\+/g, " "));
-    };
-
-
-
-

- - PARSE.tokenize -

-
- -
-

Splits a string in tokens that users can specified as input parameter. -Additional options can be specified with the modifiers parameter

- -
    -
  • limit: An integer that specifies the number of split items -after the split limit will not be included in the array
  • -
-
-
-
Params
-
- str - string - The string to split -
-
- separators - array - Array containing the separators words -
-
- modifiers - object - Optional. Configuration options - for the tokenizing -
-
Returns
-
- - array - Tokens in which the string was split -
-
-
-
    PARSE.tokenize = function(str, separators, modifiers) {
-        var pattern, regex;
-        if (!str) return;
-        if (!separators || !separators.length) return [str];
-        modifiers = modifiers || {};
-
-        pattern = '[';
-
-        JSUS.each(separators, function(s) {
-            if (s === ' ') s = '\\s';
-
-            pattern += s;
-        });
-
-        pattern += ']+';
-
-        regex = new RegExp(pattern);
-        return str.split(regex, modifiers.limit);
-    };
-
-
-
-

- - PARSE.stringify -

-
- -
-

Stringifies objects, functions, primitive, undefined or null values

- -

Makes uses JSON.stringify with a special reviver function, that -strinfifies also functions, undefined, and null values.

- -

A special prefix is prepended to avoid name collisions.

-
-
-
Params
-
- o - mixed - The value to stringify -
-
- spaces - number - Optional the number of indentation spaces. - Defaults, 0 -
-
Returns
-
- - string - The stringified result -
-
See
-
- JSON.stringify -
-
See
-
- PARSE.stringify_prefix - -
-
-
-
    PARSE.stringify = function(o, spaces) {
-        return JSON.stringify(o, function(key, value) {
-            var type = typeof value;
-            if ('function' === type) {
-                return PARSE.stringify_prefix + value.toString();
-            }
-
-            if ('undefined' === type) return PARSE.marker_und;
-            if (value === null) return PARSE.marker_null;
-            if ('number' === type && isNaN(value)) return PARSE.marker_nan;
-            if (value === Number.POSITIVE_INFINITY) return PARSE.marker_inf;
-            if (value === Number.NEGATIVE_INFINITY) {
-                return PARSE.marker_minus_inf;
-            }
-
-            return value;
-
-        }, spaces);
-    };
-
-
-
-

- - PARSE.stringifyAll -

-
- -
-

Copies all the properties of the prototype before stringifying

- -

Notice: The original object is modified!

-
-
-
Params
-
- o - mixed - The value to stringify -
-
- spaces - number - Optional the number of indentation spaces. - Defaults, 0 -
-
Returns
-
- - string - The stringified result -
-
See
-
- PARSE.stringify - -
-
-
-
    PARSE.stringifyAll = function(o, spaces) {
-        var i;
-        if ('object' === typeof o) {
-            for (i in o) {
-                if (!o.hasOwnProperty(i)) {
-                    if ('object' === typeof o[i]) {
-                        o[i] = PARSE.stringifyAll(o[i]);
-                    }
-                    else {
-                        o[i] = o[i];
-                    }
-                }
-            }
-        }
-        return PARSE.stringify(o);
-    };
-
-
-
-

- - PARSE.parse -

-
- -
-

Decodes strings in objects and other values

- -

Uses JSON.parse and then looks for special strings -encoded by PARSE.stringify

-
-
-
Params
-
- str - string - The string to decode -
-
Returns
-
- - mixed - The decoded value -
-
See
-
- JSON.parse -
-
See
-
- PARSE.stringify_prefix - -
-
-
-
    PARSE.parse = (function() {
-
-        var len_prefix = PARSE.stringify_prefix.length,
-            len_func = PARSE.marker_func.length,
-            len_null = PARSE.marker_null.length,
-            len_und = PARSE.marker_und.length,
-            len_nan = PARSE.marker_nan.length,
-            len_inf = PARSE.marker_inf.length,
-            len_minus_inf = PARSE.marker_minus_inf.length;
-
-        function walker(o) {
-            var i;
-            if ('object' !== typeof o) return reviver(o);
-            for (i in o) {
-                if (o.hasOwnProperty(i)) {
-                    if ('object' === typeof o[i]) walker(o[i]);
-                    else o[i] = reviver(o[i]);
-                }
-            }
-            return o;
-        }
-
-        function reviver(value) {
-            var type;
-            type = typeof value;
-
-            if (type === 'string') {
-                if (value.substring(0, len_prefix) !== PARSE.stringify_prefix) {
-                    return value;
-                }
-                else if (value.substring(0, len_func) === PARSE.marker_func) {
-                    return JSUS.eval(value.substring(len_prefix));
-                }
-                else if (value.substring(0, len_null) === PARSE.marker_null) {
-                    return null;
-                }
-                else if (value.substring(0, len_und) === PARSE.marker_und) {
-                    return undefined;
-                }
-
-                else if (value.substring(0, len_nan) === PARSE.marker_nan) {
-                    return NaN;
-                }
-                else if (value.substring(0, len_inf) === PARSE.marker_inf) {
-                    return Infinity;
-                }
-                else if (value.substring(0, len_minus_inf) ===
-                         PARSE.marker_minus_inf) {
-
-                    return -Infinity;
-                }
-
-            }
-            return value;
-        }
-
-        return function(str) {
-            return walker(JSON.parse(str));
-        };
-
-    })();
-
-
-
-

- - PARSE.isInt -

-
- -
-

Checks if a value is an integer number or a string containing one

- -

Non-numbers, Infinity, NaN, and floats will return FALSE

-
-
-
Params
-
- n - mixed - The value to check -
-
- lower - number - Optional. If set, n must be greater than lower -
-
- upper - number - Optional. If set, n must be smaller than upper -
-
Returns
-
- - boolean - number - The parsed integer, or FALSE if none was found -
-
See
-
- PARSE.isFloat -
-
See
-
- PARSE.isNumber - -
-
-
-
    PARSE.isInt = function(n, lower, upper) {
-        var regex, i;
-        regex = /^-?\d+$/;
-        if (!regex.test(n)) return false;
-        i = parseInt(n, 10);
-        if (i !== parseFloat(n)) return false;
-        return PARSE.isNumber(i, lower, upper);
-    };
-
-
-
-

- - PARSE.isFloat -

-
- -
-

Checks if a value is a float number or a string containing one

- -

Non-numbers, Infinity, NaN, and integers will return FALSE

-
-
-
Params
-
- n - mixed - The value to check -
-
- lower - number - Optional. If set, n must be greater than lower -
-
- upper - number - Optional. If set, n must be smaller than upper -
-
Returns
-
- - boolean - number - The parsed float, or FALSE if none was found -
-
See
-
- PARSE.isInt -
-
See
-
- PARSE.isNumber - -
-
-
-
    PARSE.isFloat = function(n, lower, upper) {
-        var regex;
-        regex = /^-?\d*(\.\d+)?$/;
-        if (!regex.test(n)) return false;
-        if (n.toString().indexOf('.') === -1) return false;
-        return PARSE.isNumber(n, lower, upper);
-    };
-
-
-
-

- - PARSE.isNumber -

-
- -
-

Checks if a value is a number (int or float) or a string containing one

- -

Non-numbers, Infinity, NaN will return FALSE

-
-
-
Params
-
- n - mixed - The value to check -
-
- lower - number - Optional. If set, n must be greater than lower -
-
- upper - number - Optional. If set, n must be smaller than upper -
-
Returns
-
- - boolean - number - The parsed number, or FALSE if none was found -
-
See
-
- PARSE.isInt -
-
See
-
- PARSE.isFloat - -
-
-
-
    PARSE.isNumber = function(n, lower, upper) {
-        if (isNaN(n) || !isFinite(n)) return false;
-        n = parseFloat(n);
-        if ('number' === typeof lower && n < lower) return false;
-        if ('number' === typeof upper && n > upper) return false;
-        return n;
-    };
-
-
-
-

- - PARSE.isEmail -

-
- -
-

Returns TRUE if the email's format is valid

-
-
-
Params
-
- The - string - email to check -
-
Returns
-
- - boolean - TRUE, if the email format is valid -
-
-
-
    PARSE.isEmail = function(email) {
-        var idx;
-        if ('string' !== typeof email) return false;
-        if (email.trim().length < 5) return false;
-        idx = email.indexOf('@');
-        if (idx === -1 || idx === 0 || idx === (email.length-1)) return false;
-        idx = email.lastIndexOf('.');
-        if (idx === -1 || idx === (email.length-1) || idx > (idx+1)) {
-            return false;
-        }
-        return true;
-    };
-
-
-
-

- - PARSE.range -

-
- -
-

Decodes semantic strings into an array of integers

- -

Let n, m and l be integers, then the tokens of the string are -interpreted in the following way:

- -
    -
  • *: Any integer
  • -
  • n: The integer n
  • -
  • begin: The smallest integer in available
  • -
  • end: The largest integer in available
  • -
  • <n, <=n, >n, >=n: Any integer (strictly) smaller/larger than n
  • -
  • n..m, [n,m]: Any integer between n and m (both inclusively)
  • -
  • n..l..m: Any i
  • -
  • [n,m): Any integer between n (inclusively) and m (exclusively)
  • -
  • (n,m]: Any integer between n (exclusively) and m (inclusively)
  • -
  • (n,m): Any integer between n and m (both exclusively)
  • -
  • %n: Divisible by n
  • -
  • %n = m: Divisible with rest m
  • -
  • !: Logical not
  • -
  • |, ||, ,: Logical or
  • -
  • &, &&: Logical and
  • -
- -

The elements of the resulting array are all elements of the available -array which satisfy the expression defined by expr.

- -

Examples:

- -

PARSE.range('2..5, >8 & !11', '[-2,12]'); // [2,3,4,5,9,10,12]

- -

PARSE.range('begin...end/2 | 3*end/4...3...end', '[0,40) & %2 = 1'); - // [1,3,5,7,9,11,13,15,17,19,29,35] (end == 39)

- -

PARSE.range('<=19, 22, %5', '>6 & !>27'); - // [7,8,9,10,11,12,13,14,15,16,17,18,19,20,22,25]

- -

PARSE.range('*','(3,8) & !%4, 22, (10,12]'); // [5,6,7,11,12,22]

- -

PARSE.range('<4', { - begin: 0, - end: 21, - prev: 0, - cur: 1, - next: function() { - var temp = this.prev; - this.prev = this.cur; - this.cur += temp; - return this.cur; - }, - isFinished: function() { - return this.cur + this.prev > this.end; - } - }); // [5, 8, 13, 21]

-
-
-
Params
-
- expr - string - number - The selection expression -
-
- available - mixed - Optional. If undefined expr is used. If: - - string: it is interpreted according to the same rules as expr; - - array: it is used as it is; - - object: provide functions next, isFinished and attributes begin, end -
-
Returns
-
- - array - The array containing the specified values -
-
See
-
- JSUS.eval - -
-
-
-
    PARSE.range = function(expr, available) {
-        var i,len, x;
-        var solution;
-        var begin, end, lowerBound, numbers;
-        var invalidChars, invalidBeforeOpeningBracket, invalidDot;
-
-        solution = [];
-        if ('undefined' === typeof expr) return solution;
-
-
- -
-

TODO: this could be improved, i.e. if it is a number, many -checks and regular expressions could be avoided.

-
        if ('number' === typeof expr) expr = '' + expr;
-        else if ('string' !== typeof expr) {
-            throw new TypeError('PARSE.range: expr must be string, number, ' +
-                                'undefined.');
-        }
-
-
- -
-

If no available numbers defined, assumes all possible are allowed.

-
        if ('undefined' === typeof available) {
-            available = expr;
-        }
-        else if (JSUS.isArray(available)) {
-            if (available.length === 0) return solution;
-            begin = Math.min.apply(null, available);
-            end = Math.max.apply(null, available);
-        }
-        else if ('object' === typeof available) {
-            if ('function' !== typeof available.next) {
-                throw new TypeError('PARSE.range: available.next must be ' +
-                                    'function.');
-            }
-            if ('function' !== typeof available.isFinished) {
-                throw new TypeError('PARSE.range: available.isFinished must ' +
-                                    'be function.');
-            }
-            if ('number' !== typeof available.begin) {
-                throw new TypeError('PARSE.range: available.begin must be ' +
-                                    'number.');
-            }
-            if ('number' !== typeof available.end) {
-                throw new TypeError('PARSE.range: available.end must be ' +
-                                    'number.');
-            }
-
-            begin = available.begin;
-            end = available.end;
-        }
-        else if ('string' === typeof available) {
-
-
- -
-

If the availble points are also only given implicitly, -compute set of available numbers by first guessing a bound.

-
            available = preprocessRange(available);
-
-            numbers = available.match(/([-+]?\d+)/g);
-            if (numbers === null) {
-                throw new Error(
-                    'PARSE.range: no numbers in available: ' + available);
-            }
-            lowerBound = Math.min.apply(null, numbers);
-
-            available = PARSE.range(available, {
-                begin: lowerBound,
-                end: Math.max.apply(null, numbers),
-                value: lowerBound,
-                next: function() {
-                    return this.value++;
-                },
-                isFinished: function() {
-                    return this.value > this.end;
-                }
-            });
-            begin = Math.min.apply(null, available);
-            end = Math.max.apply(null, available);
-        }
-        else {
-            throw new TypeError('PARSE.range: available must be string, ' +
-                                'array, object or undefined.');
-        }
-
-
- -
-

end -> maximal available value.

-
        expr = expr.replace(/end/g, parseInt(end, 10));
-
-
- -
-

begin -> minimal available value.

-
        expr = expr.replace(/begin/g, parseInt(begin, 10));
-
-
- -
-

Do all computations.

-
        expr = preprocessRange(expr);
-
-
- -
-

Round all floats

-
        expr = expr.replace(/([-+]?\d+\.\d+)/g, function(match, p1) {
-            return parseInt(p1, 10);
-        });
-
-
- -
-

Validate expression to only contain allowed symbols.

-
        invalidChars = /[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;
-        if (expr.match(invalidChars)) {
-            throw new Error('PARSE.range: invalid characters found: ' + expr);
-        }
-
-
- -
-

& -> && and | -> ||.

-
        expr = expr.replace(/([^& ]) *& *([^& ])/g, "$1&&$2");
-        expr = expr.replace(/([^| ]) *\| *([^| ])/g, "$1||$2");
-
-
- -
-

n -> (x == n).

-
        expr = expr.replace(/([-+]?\d+)/g, "(x==$1)");
-
-
- -
-

n has already been replaced by (x==n) so match for that from now on.

-
-
-
- -
-

%n -> !(x%n)

-
        expr = expr.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)");
-
-
- -
-

%n has already been replaced by !(x%n) so match for that from now on. -%n = m, %n == m -> (x%n == m).

-
        expr = expr.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,
-            "(x%$1==$2)");
-
-
- -
-

n, >=n -> (x < n), (x <= n), (x > n), (x >= n)

-
        expr = expr.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g, "(x$1$2)");
-
-
- -
-

n..l..m -> (x >= n && x <= m && !((x-n)%l)) for positive l.

-
        expr = expr.replace(
-            /\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,
-            "(x>=$1&&x<=$3&&!((x- $1)%$2))");
-
-
- -
-

n..l..m -> (x <= n && x >= m && !((x-n)%l)) for negative l.

-
        expr = expr.replace(
-            /\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,
-            "(x<=$1&&x>=$3&&!((x- $1)%$2))");
-
-
- -
-

n..m -> (x >= n && x <= m).

-
        expr = expr.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,
-                "(x>=$1&&x<=$2)");
-
-
- -
-

(n,m), ... ,[n,m] -> (x > n && x < m), ... , (x >= n && x <= m).

-
        expr = expr.replace(
-            /([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,
-                function (match, p1, p2, p3, p4) {
-                    return "(x>" + (p1 == '(' ? '': '=') + p2 + "&&x<" +
-                        (p4 == ')' ? '' : '=') + p3 + ')';
-            }
-        );
-
-
- -
-
    -
  • -> true.
  • -
-
        expr = expr.replace('*', 1);
-
-
- -
-

Remove spaces.

-
        expr = expr.replace(/\s/g, '');
-
-
- -
-

a, b -> (a) || (b)

-
        expr = expr.replace(/\)[,] *(!*)\(/g, ")||$1(");
-
-
- -
-

Validating the expression before eval"ing it.

-
        invalidChars = /[^ \d<>=!\|&,\(\)\-\+%x\.]/g;
-
-
- -
-

Only & | ! may be before an opening bracket.

-
        invalidBeforeOpeningBracket = /[^ &!|\(] *\(/g;
-
-
- -
-

Only dot in floats.

-
        invalidDot = /\.[^\d]|[^\d]\./;
-
-        if (expr.match(invalidChars)) {
-            throw new Error('PARSE.range: invalid characters found: ' + expr);
-        }
-        if (expr.match(invalidBeforeOpeningBracket)) {
-            throw new Error('PARSE.range: invalid character before opending ' +
-                            'bracket found: ' + expr);
-        }
-        if (expr.match(invalidDot)) {
-            throw new Error('PARSE.range: invalid dot found: ' + expr);
-        }
-
-        if (JSUS.isArray(available)) {
-            i = -1, len = available.length;
-            for ( ; ++i < len ; ) {
-                x = parseInt(available[i], 10);
-                if (JSUS.eval(expr.replace(/x/g, x))) {
-                    solution.push(x);
-                }
-            }
-        }
-        else {
-            while (!available.isFinished()) {
-                x = parseInt(available.next(), 10);
-                if (JSUS.eval(expr.replace(/x/g, x))) {
-                    solution.push(x);
-                }
-            }
-        }
-        return solution;
-    };
-
-    function preprocessRange(expr) {
-        var mult = function(match, p1, p2, p3) {
-            var n1 = parseInt(p1, 10);
-            var n3 = parseInt(p3, 10);
-            return p2 == '*' ? n1*n3 : n1/n3;
-        };
-        var add = function(match, p1, p2, p3) {
-            var n1 = parseInt(p1, 10);
-            var n3 = parseInt(p3, 10);
-            return p2 == '-' ? n1 - n3 : n1 + n3;
-        };
-        var mod = function(match, p1, p2, p3) {
-            var n1 = parseInt(p1, 10);
-            var n3 = parseInt(p3, 10);
-            return n1 % n3;
-        };
-
-        while (expr.match(/([-+]?\d+) *([*\/]) *([-+]?\d+)/g)) {
-            expr = expr.replace(/([-+]?\d+) *([*\/]) *([-+]?\d+)/, mult);
-        }
-
-        while (expr.match(/([-+]?\d+) *([-+]) *([-+]?\d+)/g)) {
-            expr = expr.replace(/([-+]?\d+) *([-+]) *([-+]?\d+)/, add);
-        }
-        while (expr.match(/([-+]?\d+) *% *([-+]?\d+)/g)) {
-            expr = expr.replace(/([-+]?\d+) *% *([-+]?\d+)/, mod);
-        }
-        return expr;
-    }
-
-
-
-

- - PARSE.funcName -

-
- -
-

Returns the name of the function

- -

Function.name is a non-standard JavaScript property, -although many browsers implement it. This is a cross-browser -implementation for it.

- -

In case of anonymous functions, an empty string is returned.

-
-
-
Params
-
- func - function - The function to check -
-
Returns
-
- - string - The name of the function - -Kudos to: -http://matt.scharley.me/2012/03/09/monkey-patch-name-ie.html -
-
-
-
    if ('undefined' !== typeof Function.prototype.name) {
-        PARSE.funcName = function(func) {
-            if ('function' !== typeof func) {
-                throw new TypeError('PARSE.funcName: func must be function.');
-            }
-            return func.name;
-        };
-    }
-    else {
-        PARSE.funcName = function(func) {
-            var funcNameRegex, res;
-            if ('function' !== typeof func) {
-                throw new TypeError('PARSE.funcName: func must be function.');
-            }
-            funcNameRegex = /function\s([^(]{1,})\(/;
-            res = (funcNameRegex).exec(func.toString());
-            return (res && res.length > 1) ? res[1].trim() : "";
-        };
-    }
-
-    JSUS.extend(PARSE);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/queue.js.html b/docs/lib/queue.js.html deleted file mode 100644 index 4999918..0000000 --- a/docs/lib/queue.js.html +++ /dev/null @@ -1,359 +0,0 @@ - - - - queue.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - QUEUE -

-
- - -

Copyright(c) 2015 Stefano Balietti -MIT Licensed

-
-

Handles a simple queue of operations

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    var QUEUE = {};
-
-    QUEUE.getQueue = function() {
-        return new Queue();
-    };
-
-
-
-

- - Queue constructor -

-
- -
-
-
-
-
    function Queue() {
-
-
-
-

- - Queue.queue -

-
- -
-

The list of functions waiting to be executed.

-
-
-
        this.queue = [];
-
-
-
-

- - Queue.inProgress -

-
- -
-

The list of operations ids currently in progress.

-
-
-
        this.inProgress = {};
-    }
-
-
-
-

- - Queue.isReady -

-
- -
-

Returns TRUE if no operation is in progress

-
-
-
Returns
-
- - boolean - TRUE, if no operation is in progress -
-
-
-
    Queue.prototype.isReady = function() {
-        return JSUS.isEmpty(this.inProgress);
-    };
-
-
-
-

- - Queue.onReady -

-
- -
-

Executes the specified callback once the queue has been cleared

- -

Multiple functions to execute can be added, and they are executed -sequentially once the queue is cleared.

- -

If the queue is already cleared, the function is executed immediately.

-
-
-
Params
-
- cb - function - The callback to execute -
-
-
-
    Queue.prototype.onReady = function(cb) {
-        if ('function' !== typeof cb) {
-            throw new TypeError('Queue.onReady: cb must be function. Found: ' +
-                               cb);
-        }
-        if (JSUS.isEmpty(this.inProgress)) cb();
-        else this.queue.push(cb);
-    };
-
-
-
-

- - Queue.add -

-
- -
-

Adds an item to the inProgress index

-
-
-
Params
-
- key - string - A tentative key name -
-
Returns
-
- - string - The unique key to be used to unregister the operation -
-
-
-
    Queue.prototype.add = function(key) {
-        if (key && 'string' !== typeof key) {
-            throw new Error('Queue.add: key must be string.');
-        }
-        key = JSUS.uniqueKey(this.inProgress, key);
-        if ('string' !== typeof key) {
-            throw new Error('Queue.add: an error occurred ' +
-                            'generating unique key.');
-        }
-        this.inProgress[key] = key;
-        return key;
-    };
-
-
-
-

- - Queue.remove -

-
- -
-

Remove a specified key from the inProgress index

-
-
-
Params
-
- key - string - The key to remove from the inProgress index. -
-
-
-
    Queue.prototype.remove = function(key) {
-        if ('string' !== typeof key) {
-            throw new Error('Queue.remove: key must be string.');
-        }
-        delete this.inProgress[key];
-        if (JSUS.isEmpty(this.inProgress)) {
-            this.executeAndClear();
-        }
-    };
-
-
-
-

- - Queue.getRemoveCb -

-
- -
-

Returns a callback to remove an item from the inProgress index

- -

This method is useful when the callbacks is defined inside loops, -so that a closure is created around the variable key.

-
-
-
Params
-
- key - string - The key to remove from the inProgress index. -
-
See
-
- Queue.remove - -
-
-
-
    Queue.prototype.getRemoveCb = function(key) {
-        var that;
-        if ('string' !== typeof key) {
-            throw new Error('Queue.getRemoveCb: key must be string.');
-        }
-        that = this;
-        return function() { that.remove(key); };
-    };
-
-
-
-

- - Queue.executeAndClear -

-
- -
-

Executes sequentially all callbacks, and removes them from the queue

-
-
-
    Queue.prototype.executeAndClear = function() {
-        var i, len;
-        i = -1;
-        len = this.queue.length;
-        for ( ; ++i < len ; ) {
-            this.queue[i]();
-        }
-    };
-
-    JSUS.extend(QUEUE);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/random.js.html b/docs/lib/random.js.html deleted file mode 100644 index a2c5f9b..0000000 --- a/docs/lib/random.js.html +++ /dev/null @@ -1,788 +0,0 @@ - - - - random.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - RANDOM -

-
- - -

Copyright(c) 2016 Stefano Balietti -MIT Licensed

-
-

Generates pseudo-random numbers

-
-
-
(function(JSUS) {
-
-    "use strict";
-
-    function RANDOM() {}
-
-
-
-

- - RANDOM.random -

-
- -
-

Generates a pseudo-random floating point number between -[a,b), a inclusive and b exclusive.

-
-
-
Params
-
- a - number - The lower limit -
-
- b - number - The upper limit -
-
Returns
-
- - number - A random floating point number in [a,b) -
-
-
-
    RANDOM.random = function(a, b) {
-        var c;
-        a = ('undefined' === typeof a) ? 0 : a;
-        b = ('undefined' === typeof b) ? 0 : b;
-        if (a === b) return a;
-
-        if (b < a) {
-            c = a;
-            a = b;
-            b = c;
-        }
-        return (Math.random() * (b - a)) + a;
-    };
-
-
-
-

- - RANDOM.randomInt -

-
- -
-

Generates a pseudo-random integer between (a,b] a exclusive, b inclusive

-
-
-
Params
-
- a - number - The lower limit -
-
- b - number - The upper limit -
-
Returns
-
- - number - A random integer in (a,b] -
-
See
-
- RANDOM.random - -
-
-
-
    RANDOM.randomInt = function(a, b) {
-        if (a === b) return a;
-        return Math.floor(RANDOM.random(a, b) + 1);
-    };
-
-
-
-

- - RANDOM.sample -

-
- -
-

Generates a randomly shuffled sequence of numbers in (a,b)

- -

Both a and b are inclued in the interval.

-
-
-
Params
-
- a - number - The lower limit -
-
- b - number - The upper limit -
-
Returns
-
- - array - The randomly shuffled sequence. -
-
See
-
- RANDOM.seq - -
-
-
-
    RANDOM.sample = function(a, b) {
-        var out;
-        out = JSUS.seq(a,b);
-        if (!out) return false;
-        return JSUS.shuffle(out);
-    };
-
-
-
-

- - RANDOM.getNormalGenerator -

-
- -
-

Returns a new generator of normally distributed pseudo random numbers

- -

The generator is independent from RANDOM.nextNormal

-
-
-
Returns
-
- - function - An independent generator -
-
See
-
- RANDOM.nextNormal - -
-
-
-
    RANDOM.getNormalGenerator = function() {
-
-        return (function() {
-
-            var oldMu, oldSigma;
-            var x2, multiplier, genReady;
-
-            return function normal(mu, sigma) {
-
-                var x1, u1, u2, v1, v2, s;
-
-                if ('number' !== typeof mu) {
-                    throw new TypeError('nextNormal: mu must be number.');
-                }
-                if ('number' !== typeof sigma) {
-                    throw new TypeError('nextNormal: sigma must be number.');
-                }
-
-                if (mu !== oldMu || sigma !== oldSigma) {
-                    genReady = false;
-                    oldMu = mu;
-                    oldSigma = sigma;
-                }
-
-                if (genReady) {
-                    genReady = false;
-                    return (sigma * x2) + mu;
-                }
-
-                u1 = Math.random();
-                u2 = Math.random();
-
-
- -
-

Normalize between -1 and +1.

-
                v1 = (2 * u1) - 1;
-                v2 = (2 * u2) - 1;
-
-                s = (v1 * v1) + (v2 * v2);
-
-
- -
-

Condition is true on average 1.27 times, -with variance equal to 0.587.

-
                if (s >= 1) {
-                    return normal(mu, sigma);
-                }
-
-                multiplier = Math.sqrt(-2 * Math.log(s) / s);
-
-                x1 = v1 * multiplier;
-                x2 = v2 * multiplier;
-
-                genReady = true;
-
-                return (sigma * x1) + mu;
-
-            };
-        })();
-    };
-
-
-
-

- - RANDOM.nextNormal -

-
- -
-

Generates random numbers with Normal Gaussian distribution.

- -

User must specify the expected mean, and standard deviation a input -parameters.

- -

Implements the Polar Method by Knuth, "The Art Of Computer -Programming", p. 117.

-
-
-
Params
-
- mu - number - The mean of the distribution -param {number} sigma The standard deviation of the distribution -
-
Returns
-
- - number - A random number following a Normal Gaussian distribution -
-
See
-
- RANDOM.getNormalGenerator - -
-
-
-
    RANDOM.nextNormal = RANDOM.getNormalGenerator();
-
-
-
-

- - RANDOM.nextLogNormal -

-
- -
-

Generates random numbers with LogNormal distribution.

- -

User must specify the expected mean, and standard deviation of the -underlying gaussian distribution as input parameters.

-
-
-
Params
-
- mu - number - The mean of the gaussian distribution -
-
- sigma - number - The standard deviation of the gaussian distribution -
-
Returns
-
- - number - A random number following a LogNormal distribution -
-
See
-
- RANDOM.nextNormal - -
-
-
-
    RANDOM.nextLogNormal = function(mu, sigma) {
-        if ('number' !== typeof mu) {
-            throw new TypeError('nextLogNormal: mu must be number.');
-        }
-        if ('number' !== typeof sigma) {
-            throw new TypeError('nextLogNormal: sigma must be number.');
-        }
-        return Math.exp(RANDOM.nextNormal(mu, sigma));
-    };
-
-
-
-

- - RANDOM.nextExponential -

-
- -
-

Generates random numbers with Exponential distribution.

- -

User must specify the lambda the rate parameter of the distribution. -The expected mean of the distribution is equal to Math.pow(lamba, -1).

-
-
-
Params
-
- lambda - number - The rate parameter -
-
Returns
-
- - number - A random number following an Exponential distribution -
-
-
-
    RANDOM.nextExponential = function(lambda) {
-        if ('number' !== typeof lambda) {
-            throw new TypeError('nextExponential: lambda must be number.');
-        }
-        if (lambda <= 0) {
-            throw new TypeError('nextExponential: ' +
-                                'lambda must be greater than 0.');
-        }
-        return - Math.log(1 - Math.random()) / lambda;
-    };
-
-
-
-

- - RANDOM.nextBinomial -

-
- -
-

Generates random numbers following the Binomial distribution.

- -

User must specify the probability of success and the number of trials.

-
-
-
Params
-
- p - number - The probability of success -
-
- trials - number - The number of trials -
-
Returns
-
- - number - The sum of successes in n trials -
-
-
-
    RANDOM.nextBinomial = function(p, trials) {
-        var counter, sum;
-
-        if ('number' !== typeof p) {
-            throw new TypeError('nextBinomial: p must be number.');
-        }
-        if ('number' !== typeof trials) {
-            throw new TypeError('nextBinomial: trials must be number.');
-        }
-        if (p < 0 || p > 1) {
-            throw new TypeError('nextBinomial: p must between 0 and 1.');
-        }
-        if (trials < 1) {
-            throw new TypeError('nextBinomial: trials must be greater than 0.');
-        }
-
-        counter = 0;
-        sum = 0;
-
-        while(counter < trials){
-            if (Math.random() < p) {
-                sum += 1;
-            }
-            counter++;
-        }
-
-        return sum;
-    };
-
-
-
-

- - RANDOM.nextGamma -

-
- -
-

Generates random numbers following the Gamma distribution.

- -

This function is experimental and untested. No documentation.

-
-
-
-
-
    RANDOM.nextGamma = function(alpha, k) {
-        var intK, kDiv, alphaDiv;
-        var u1, u2, u3;
-        var x, i, tmp;
-
-        if ('number' !== typeof alpha) {
-            throw new TypeError('nextGamma: alpha must be number.');
-        }
-        if ('number' !== typeof k) {
-            throw new TypeError('nextGamma: k must be number.');
-        }
-        if (alpha < 1) {
-            throw new TypeError('nextGamma: alpha must be greater than 1.');
-        }
-        if (k < 1) {
-            throw new TypeError('nextGamma: k must be greater than 1.');
-        }
-
-        u1 = Math.random();
-        u2 = Math.random();
-        u3 = Math.random();
-
-        intK = Math.floor(k) + 3;
-        kDiv = 1 / k;
-
-        alphaDiv = 1 / alpha;
-
-        x = 0;
-        for (i = 3 ; ++i < intK ; ) {
-            x += Math.log(Math.random());
-        }
-
-        x *= - alphaDiv;
-
-        tmp = Math.log(u3) *
-            (Math.pow(u1, kDiv) /
-             ((Math.pow(u1, kDiv) + Math.pow(u2, 1 / (1 - k)))));
-
-        tmp *=  - alphaDiv;
-
-        return x + tmp;
-    };
-
-
-
-

- - RANDOM.randomString -

-
- -
-

Creates a parametric random string

-
-
-
Params
-
- len - number - The length of string (must be > 0). Default, 6. -
-
- chars - string - A code specifying which sets of characters - to use. Available symbols (default 'a'): - - 'a': lower case letters - - 'A': upper case letters - - '1': digits - - '!': all remaining symbols (excluding spaces) - - '_': spaces (it can be followed by an integer > 0 - controlling the frequency of spaces, default = 1) -
-
- useChars - boolean - If TRUE, the characters of the chars - parameter are used as they are instead of interpreted as - special symbols. Default FALSE. -
-
Returns
-
- - string - result The random string - -Kudos to: http://stackoverflow.com/questions/10726909/ - random-alpha-numeric-string-in-javascript -
-
-
-
    RANDOM.randomString = function(len, chars, useChars) {
-        var mask, result, i, nSpaces;
-        if ('undefined' !== typeof len) {
-            if ('number' !== typeof len || len < 1) {
-                throw new Error('randomString: len must a number > 0 or ' +
-                                'undefined. Found: ' + len);
-
-            }
-        }
-        if ('undefined' !== typeof chars) {
-            if ('string' !== typeof chars || chars.trim() === '') {
-                throw new Error('randomString: chars must a non-empty string ' +
-                                'or undefined. Found: ' + chars);
-
-            }
-        }
-        else if (useChars) {
-            throw new Error('randomString: useChars is TRUE, but chars ' +
-                            'is undefined.');
-
-        }
-
-
- -
-

Defaults.

-
        len = len || 6;
-        chars = chars || 'a';
-
-
- -
-

Create/use mask from chars.

-
        mask = '';
-        if (!useChars) {
-            if (chars.indexOf('a') > -1) mask += 'abcdefghijklmnopqrstuvwxyz';
-            if (chars.indexOf('A') > -1) mask += 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
-            if (chars.indexOf('1') > -1) mask += '0123456789';
-            if (chars.indexOf('!') > -1) {
-                mask += '!~`@#$%^&*()_+-={}[]:";\'<>?,./|\\';
-            }
-
-
- -
-

Check how many spaces we should add.

-
            nSpaces = chars.indexOf('_');
-            if (nSpaces > -1) {
-                nSpaces = chars.charAt(nSpaces + 1);
-
-
- -
-

nSpaces is integer > 0 or 1.

-
                nSpaces = JSUS.isInt(nSpaces, 0) || 1;
-                if (nSpaces === 1) mask += ' ';
-                else if (nSpaces === 2) mask += '  ';
-                else if (nSpaces === 3) mask += '   ';
-                else {
-                    i = -1;
-                    for ( ; ++i < nSpaces ; ) {
-                        mask += ' ';
-                    }
-                }
-            }
-        }
-        else {
-            mask = chars;
-        }
-
-        i = -1, result = '';
-        for ( ; ++i < len ; ) {
-            result += mask[Math.floor(Math.random() * mask.length)];
-        }
-        return result;
-    };
-
-
-
-

- - RANDOM.randomEmail -

-
- -
-

Creates a random email address

-
-
-
Returns
-
- - string - result The random email -
-
-
-
    RANDOM.randomEmail = function() {
-        return RANDOM.randomString(RANDOM.randomInt(5,15), '!Aa0') + '@' +
-            RANDOM.randomString(RANDOM.randomInt(3,10))  + '.' +
-            RANDOM.randomString(RANDOM.randomInt(2,3));
-    };
-
-    JSUS.extend(RANDOM);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/lib/time.js.html b/docs/lib/time.js.html deleted file mode 100644 index 7182c3e..0000000 --- a/docs/lib/time.js.html +++ /dev/null @@ -1,301 +0,0 @@ - - - - time.js - - - - - - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
-
-

- - TIME -

-
- - -

Copyright(c) 2017 Stefano Balietti -MIT Licensed

-
-

Collection of static functions related to the generation, -manipulation, and formatting of time strings in JavaScript

-
-
-
(function (JSUS) {
-
-    "use strict";
-
-    function TIME() {}
-
-
- -
-

Polyfill for Date.toISOString (IE7, IE8, IE9) -Kudos: https://developer.mozilla.org/en-US/docs/Web/ -JavaScript/Reference/Global_Objects/Date/toISOString

-
    if (!Date.prototype.toISOString) {
-        (function() {
-
-            function pad(number) {
-                return (number < 10) ? '0' + number : number;
-            }
-
-            Date.prototype.toISOString = function() {
-                var ms = (this.getUTCMilliseconds() / 1000).toFixed(3);
-                return this.getUTCFullYear() +
-                    '-' + pad(this.getUTCMonth() + 1) +
-                    '-' + pad(this.getUTCDate()) +
-                    'T' + pad(this.getUTCHours()) +
-                    ':' + pad(this.getUTCMinutes()) +
-                    ':' + pad(this.getUTCSeconds()) +
-                    '.' + ms.slice(2, 5) + 'Z';
-            };
-
-        }());
-    }
-
-
-
-

- - TIME.getDate -

-
- -
-

Returns a string representation of the current date and time (ISO)

- -

String is formatted as follows:

- -

YYYY-MM-DDTHH:mm:ss.sssZ

-
-
-
Returns
-
- - string - Formatted time string YYYY-MM-DDTHH:mm:ss.sssZ -
-
-
-
    TIME.getDate = TIME.getFullDate = function() {
-        return new Date().toISOString();
-    };
-
-
-
-

- - TIME.getTime -

-
- -
-

Returns a string representation of the current time

- -

String is ormatted as follows:

- -

hh:mm:ss

-
-
-
Returns
-
- - string - Formatted time string hh:mm:ss -
-
See
-
- TIME.getTimeM - -
-
-
-
    TIME.getTime = function() {
-        var d;
-        d = new Date();
-        return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds();
-    };
-
-
-
-

- - TIME.getTimeM -

-
- -
-

Like TIME.getTime, but with millisecondsx

- -

String is ormatted as follows:

- -

hh:mm:ss:mls

-
-
-
Returns
-
- - string - Formatted time string hh:mm:ss:mls -
-
See
-
- TIME.getTime - -
-
-
-
    TIME.getTimeM = function() {
-        var d;
-        d = new Date();
-        return d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() +
-            ':' + d.getMilliseconds();
-    };
-
-
-
-

- - TIME.parseMilliseconds -

-
- -
-

Parses milliseconds into an array of days, hours, minutes and seconds

-
-
-
Params
-
- ms - number - Integer representing milliseconds -
-
Returns
-
- - array - Milleconds parsed in days, hours, minutes, and seconds -
-
-
-
    TIME.parseMilliseconds = function(ms) {
-        var result, x, seconds, minutes, hours, days;
-        if ('number' !== typeof ms) {
-            throw new TypeError('TIME.parseMilliseconds: ms must be number.');
-        }
-        result = [];
-        x = ms / 1000;
-        result[4] = x;
-        seconds = x % 60;
-        result[3] = Math.floor(seconds);
-        x = x / 60;
-        minutes = x % 60;
-        result[2] = Math.floor(minutes);
-        x = x / 60;
-        hours = x % 24;
-        result[1] = Math.floor(hours);
-        x = x / 24;
-        days = x;
-        result[1] = Math.floor(days);
-        return result;
-    };
-
-
-
-

- - TIME.now -

-
- -
-

Shortcut to Date.now (when existing), or its polyfill

-
-
-
Returns
-
- - number - The timestamp now -
-
-
-
    TIME.now = 'function' === typeof Date.now ?
-        Date.now : function() { return new Date().getTime(); }
-
-    JSUS.extend(TIME);
-
-})('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS);
-
-
-
- - diff --git a/docs/readme b/docs/readme new file mode 100644 index 0000000..0defebd --- /dev/null +++ b/docs/readme @@ -0,0 +1,2 @@ +Documentation temporarily removed, please refer to the project website: http://nodegame.org + From 02c4998a22c3c067e479a85ba5ea2a906402c2b8 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 7 Sep 2018 03:30:22 -0400 Subject: [PATCH 45/79] fs uses sep.path --- lib/fs.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/lib/fs.js b/lib/fs.js index b2a2d15..1883596 100644 --- a/lib/fs.js +++ b/lib/fs.js @@ -1,6 +1,6 @@ /** * # FS - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2018 Stefano Balietti * MIT Licensed * * Collection of static functions related to file system operations. @@ -21,8 +21,8 @@ } var resolve = require('resolve'), - path = require('path'), - fs = require('fs') + path = require('path'), + fs = require('fs'); function FS() {} @@ -67,10 +67,10 @@ 'string or undefined. Found: ' + basedir); } // Added this line because it might fail. - if (module === 'JSUS') return path.resolve(__dirname, '..') + '/'; + if (module === 'JSUS') return path.resolve(__dirname, '..') + path.sep; str = resolve.sync(module, {basedir: basedir || __dirname}); stop = str.indexOf(module) + module.length; - return str.substr(0, stop) + '/'; + return str.substr(0, stop) + path.sep; }; /** @@ -147,7 +147,7 @@ }; } - if (dir[dir.length] !== '/') dir = dir + '/'; + if (dir[dir.length] !== path.sep) dir = dir + path.sep; fs.readdir(dir, function(err, files) { var asq, mycb; @@ -209,7 +209,7 @@ return false; } - dirOut = path.resolve(dirOut) + '/'; + dirOut = path.resolve(dirOut) + path.sep; dirs = [dirIn, dirOut]; for (i = 0; i < 2; i++) { From da16b7fbfeb65b0ffdeba171dc31159088d76a8d Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 7 Sep 2018 03:33:45 -0400 Subject: [PATCH 46/79] travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index e4450de..f1f3ed9 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,6 +4,7 @@ node_js: - 6 - 7 - 8 + - 10 before_install: # Get installer script. From 2e63217c7964e27769c65033f345fad6f62d96ce Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 7 Sep 2018 03:35:41 -0400 Subject: [PATCH 47/79] travis --- .travis.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index f1f3ed9..fd39137 100644 --- a/.travis.yml +++ b/.travis.yml @@ -13,15 +13,16 @@ before_install: install: - npm install --only=dev - - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --branch v4 --yes + # --branch v4 + - node nodegame-installer.js @dev --install-dir node_modules --no-spinner --yes - mv node_modules/JSUS/node_modules/resolve node_modules/ - mv node_modules/JSUS/node_modules/fs-extra node_modules/ script: # Add JSUS tests here. - - ls -la node_modules/JSUS/node_modules/ - - ls -la node_modules/ + # - ls -la node_modules/JSUS/node_modules/ + # - ls -la node_modules/ - npm test # Test Ultimatum game. From 1654ad0fd37232daf03b5523ad284f88c1af059c Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 7 Sep 2018 03:47:15 -0400 Subject: [PATCH 48/79] travis --- CHANGELOG | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 4b95370..12e79c8 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # ChangeLog +## 1.0.1 +- `#FS` is using path.sep. + ## 1.0.0 - `J` is exported as a top level object in the browser's window. - `#JSUS.get` alias of `JSUS.require` removed. `#JSUS.get` is now a method of the DOM library. diff --git a/package.json b/package.json index e3886b7..2645fba 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "JSUS", "description": "JavaScript UtilS. Extended functional programming support. JSUS helps!", - "version": "1.0.0", + "version": "1.0.1", "keywords": [ "functional", "functional programming", "util", "general", "array", "eval", "time", "date", "object", "nodeGame"], "main": "jsus.js", "author": "Stefano Balietti ", From 21bd6aeed68f7d7e15859fc92dd844a55a58fcc0 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Wed, 26 Sep 2018 17:36:43 -0400 Subject: [PATCH 49/79] Equal comparison isInt isNumber isFloat --- lib/parse.js | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/lib/parse.js b/lib/parse.js index 309e972..13bb820 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -254,19 +254,21 @@ * @param {mixed} n The value to check * @param {number} lower Optional. If set, n must be greater than lower * @param {number} upper Optional. If set, n must be smaller than upper + * @param {boolean} leq Optional. If TRUE, n can also be equal to lower + * @param {boolean} ueq Optional. If TRUE, n can also be equal to upper * * @return {boolean|number} The parsed integer, or FALSE if none was found * * @see PARSE.isFloat * @see PARSE.isNumber */ - PARSE.isInt = function(n, lower, upper) { + PARSE.isInt = function(n, lower, upper, leq, ueq) { var regex, i; regex = /^-?\d+$/; if (!regex.test(n)) return false; i = parseInt(n, 10); if (i !== parseFloat(n)) return false; - return PARSE.isNumber(i, lower, upper); + return PARSE.isNumber(i, lower, upper, leq, ueq); }; /** @@ -279,18 +281,20 @@ * @param {mixed} n The value to check * @param {number} lower Optional. If set, n must be greater than lower * @param {number} upper Optional. If set, n must be smaller than upper + * @param {boolean} leq Optional. If TRUE, n can also be equal to lower + * @param {boolean} ueq Optional. If TRUE, n can also be equal to upper * * @return {boolean|number} The parsed float, or FALSE if none was found * * @see PARSE.isInt * @see PARSE.isNumber */ - PARSE.isFloat = function(n, lower, upper) { + PARSE.isFloat = function(n, lower, upper, leq, ueq) { var regex; regex = /^-?\d*(\.\d+)?$/; if (!regex.test(n)) return false; if (n.toString().indexOf('.') === -1) return false; - return PARSE.isNumber(n, lower, upper); + return PARSE.isNumber(n, lower, upper, leq, ueq); }; /** @@ -303,17 +307,23 @@ * @param {mixed} n The value to check * @param {number} lower Optional. If set, n must be greater than lower * @param {number} upper Optional. If set, n must be smaller than upper + * @param {boolean} leq Optional. If TRUE, n can also be equal to lower + * @param {boolean} ueq Optional. If TRUE, n can also be equal to upper * * @return {boolean|number} The parsed number, or FALSE if none was found * * @see PARSE.isInt * @see PARSE.isFloat */ - PARSE.isNumber = function(n, lower, upper) { + PARSE.isNumber = function(n, lower, upper, leq, ueq) { if (isNaN(n) || !isFinite(n)) return false; n = parseFloat(n); - if ('number' === typeof lower && n < lower) return false; - if ('number' === typeof upper && n > upper) return false; + if ('number' === typeof lower && (leq ? n <= lower : n < lower)) { + return false; + } + if ('number' === typeof upper && (ueq ? n >= upper : n > upper)) { + return false; + } return n; }; From 658ed53c4701d129c00247542e219e8a370fd851 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 18 Feb 2019 13:31:26 -0500 Subject: [PATCH 50/79] isMobileAgent --- lib/parse.js | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/lib/parse.js b/lib/parse.js index 13bb820..a8ed122 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -61,6 +61,35 @@ decodeURIComponent(results[1].replace(/\+/g, " ")); }; + /** + * ## PARSE.isMobileAgent + * + * Returns TRUE if a user agent is for a mobile device + * + * @param {string} agent Optional. The user agent to check. Default: + * navigator.userAgent + * + * @return {boolean} TRUE if a user agent is for a mobile device + */ + PARSE.isMobileAgent = function(agent) { + var rx; + if (!agent){ + if (!navigator) { + throw new Error('JSUS.isMobileAgent: agent undefined and ' + + 'no navigator. Are you in the browser?'); + } + agent = navigator.userAgent; + } + else if ('string' !== typeof agent) { + throw new TypeError('JSUS.isMobileAgent: agent must be undefined ' + + 'or string. Found: ' + agent); + } + rx = new RegExp('Android|webOS|iPhone|iPad|BlackBerry|' + + 'Windows Phone|Opera Mini|IEMobile|Mobile', 'i'); + + return rx.test(agent); + }; + /** * ## PARSE.tokenize * @@ -319,7 +348,7 @@ if (isNaN(n) || !isFinite(n)) return false; n = parseFloat(n); if ('number' === typeof lower && (leq ? n <= lower : n < lower)) { - return false; + return false; } if ('number' === typeof upper && (ueq ? n >= upper : n > upper)) { return false; From 54905afb93cc3c68eecf371adbb24d12128db628 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Mon, 18 Feb 2019 13:34:33 -0500 Subject: [PATCH 51/79] 1.1.0 --- CHANGELOG | 3 +++ package.json | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 12e79c8..f310e52 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # ChangeLog +## 1.1.0 +- `#PARSE.isMobileAgent` returns TRUE if a user agent is for a mobile device. + ## 1.0.1 - `#FS` is using path.sep. diff --git a/package.json b/package.json index 2645fba..cec7919 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,8 @@ { "name": "JSUS", "description": "JavaScript UtilS. Extended functional programming support. JSUS helps!", - "version": "1.0.1", - "keywords": [ "functional", "functional programming", "util", "general", "array", "eval", "time", "date", "object", "nodeGame"], + "version": "1.1.0", + "keywords": [ "functional", "functional programming", "util", "general", "array", "eval", "time", "date", "object", "dom", "parse", "nodeGame"], "main": "jsus.js", "author": "Stefano Balietti ", "license": "MIT", From 544258931b9d821b2438fb080764f4d11af985a2 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Tue, 19 Mar 2019 11:13:45 -0400 Subject: [PATCH 52/79] 1.1.1 --- CHANGELOG | 3 +++ lib/parse.js | 4 ++-- package.json | 25 ++++++++++++++++++++----- test/test.parse.js | 30 ++++++++++++++++++++++++++++++ 4 files changed, 55 insertions(+), 7 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index f310e52..accecf4 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # ChangeLog +## 1.1.1 +- `#PARSE.isNumber` fixed bug when ueq and leq params were set. + ## 1.1.0 - `#PARSE.isMobileAgent` returns TRUE if a user agent is for a mobile device. diff --git a/lib/parse.js b/lib/parse.js index a8ed122..950db4b 100644 --- a/lib/parse.js +++ b/lib/parse.js @@ -347,10 +347,10 @@ PARSE.isNumber = function(n, lower, upper, leq, ueq) { if (isNaN(n) || !isFinite(n)) return false; n = parseFloat(n); - if ('number' === typeof lower && (leq ? n <= lower : n < lower)) { + if ('number' === typeof lower && (leq ? n < lower : n <= lower)) { return false; } - if ('number' === typeof upper && (ueq ? n >= upper : n > upper)) { + if ('number' === typeof upper && (ueq ? n > upper : n >= upper)) { return false; } return n; diff --git a/package.json b/package.json index cec7919..0d5eb01 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,21 @@ { "name": "JSUS", "description": "JavaScript UtilS. Extended functional programming support. JSUS helps!", - "version": "1.1.0", - "keywords": [ "functional", "functional programming", "util", "general", "array", "eval", "time", "date", "object", "dom", "parse", "nodeGame"], + "version": "1.1.1", + "keywords": [ + "functional", + "functional programming", + "util", + "general", + "array", + "eval", + "time", + "date", + "object", + "dom", + "parse", + "nodeGame" + ], "main": "jsus.js", "author": "Stefano Balietti ", "license": "MIT", @@ -13,10 +26,12 @@ "fs-extra": "*" }, "devDependencies": { - "mocha": ">= 0.3.0", - "should": ">= 0.5.1" + "mocha": "^6.0.2", + "should": "^4.4.1" + }, + "engines": { + "node": ">=0.10.0" }, - "engines": { "node": ">=0.10.0" }, "repository": { "type": "git", "url": "https://github.com/nodeGame/JSUS.git" diff --git a/test/test.parse.js b/test/test.parse.js index 36bf184..88f4bf0 100644 --- a/test/test.parse.js +++ b/test/test.parse.js @@ -321,6 +321,36 @@ describe('PARSE: ', function() { it('should parse and return int number (interval bad)', function() { JSUS.isNumber('1.1', 4, 5).should.be.false; }); + }); + + describe('#isNumber() with ueq and leg', function() { + it('should parse and return the float number', function() { + JSUS.isNumber('1.1', 1).should.be.eql(1.1); + }); + it('should parse and return false (upper)', function() { + JSUS.isNumber('3', 0, 2).should.be.eql(false); + }); + it('should parse and return false (>upper leq)', function() { + JSUS.isNumber('3', 0, 2, true).should.be.eql(false); + }); + it('should parse and return 3 (==upper ueq), ', function() { + JSUS.isNumber('3', 0, 3, true, true).should.be.eql(3); + }); + it('should parse and return 3 (==upper ueq 2)', function() { + JSUS.isNumber('3', undefined, 3, undefined, true).should.be.eql(3); + }); }); }); From 89e80ef98287c89dc58ae97e1a5776ac42d05efc Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 29 Mar 2019 16:10:13 -0400 Subject: [PATCH 53/79] reverseObj --- build/jsus.js | 95 +- build/jsus.min.js | 2 +- build/nodegame-widgets.js | 17046 ++++++++++++++++++++++++++++++++ build/nodegame-widgets.min.js | 15 + lib/obj.js | 28 +- 5 files changed, 17163 insertions(+), 23 deletions(-) create mode 100644 build/nodegame-widgets.js create mode 100644 build/nodegame-widgets.min.js diff --git a/build/jsus.js b/build/jsus.js index dea0400..49ff18b 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -4086,7 +4086,7 @@ /** * # FS - * Copyright(c) 2016 Stefano Balietti + * Copyright(c) 2018 Stefano Balietti * MIT Licensed * * Collection of static functions related to file system operations. @@ -4107,8 +4107,8 @@ } var resolve = require('resolve'), - path = require('path'), - fs = require('fs') + path = require('path'), + fs = require('fs'); function FS() {} @@ -4153,10 +4153,10 @@ 'string or undefined. Found: ' + basedir); } // Added this line because it might fail. - if (module === 'JSUS') return path.resolve(__dirname, '..') + '/'; + if (module === 'JSUS') return path.resolve(__dirname, '..') + path.sep; str = resolve.sync(module, {basedir: basedir || __dirname}); stop = str.indexOf(module) + module.length; - return str.substr(0, stop) + '/'; + return str.substr(0, stop) + path.sep; }; /** @@ -4233,7 +4233,7 @@ }; } - if (dir[dir.length] !== '/') dir = dir + '/'; + if (dir[dir.length] !== path.sep) dir = dir + path.sep; fs.readdir(dir, function(err, files) { var asq, mycb; @@ -4295,7 +4295,7 @@ return false; } - dirOut = path.resolve(dirOut) + '/'; + dirOut = path.resolve(dirOut) + path.sep; dirs = [dirIn, dirOut]; for (i = 0; i < 2; i++) { @@ -4371,7 +4371,7 @@ /** * # OBJ - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2019 Stefano Balietti * MIT Licensed * * Collection of static functions to manipulate JavaScript objects @@ -5591,14 +5591,13 @@ /** * ## OBJ.melt * - * Creates a new object with the specified combination of - * properties - values + * Creates a new object with specific combination of properties - values * * The values are assigned cyclically to the properties, so that * they do not need to have the same length. E.g. * * ```javascript - * J.createObj(['a','b','c'], [1,2]); // { a: 1, b: 2, c: 1 } + * J.melt(['a','b','c'], [1,2]); // { a: 1, b: 2, c: 1 } * ``` * @param {array} keys The names of the keys to add to the object * @param {array} values The values to associate to the keys @@ -5791,6 +5790,27 @@ return out; }; + /** + * ## OBJ.reverseObj + * + * Returns a new object where they keys and values are switched + * + * @param {object} obj The object to reverse + * + * @return {object} The reversed object + */ + OBJ.reverseObj = function(o) { + var k, res; + res = {}; + if (!o) return res; + for (k in o) { + if (o.hasOwnProperty(k)) { + res[o[k]] = k; + } + } + return res; + }; + JSUS.extend(OBJ); })('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS); @@ -5858,6 +5878,35 @@ decodeURIComponent(results[1].replace(/\+/g, " ")); }; + /** + * ## PARSE.isMobileAgent + * + * Returns TRUE if a user agent is for a mobile device + * + * @param {string} agent Optional. The user agent to check. Default: + * navigator.userAgent + * + * @return {boolean} TRUE if a user agent is for a mobile device + */ + PARSE.isMobileAgent = function(agent) { + var rx; + if (!agent){ + if (!navigator) { + throw new Error('JSUS.isMobileAgent: agent undefined and ' + + 'no navigator. Are you in the browser?'); + } + agent = navigator.userAgent; + } + else if ('string' !== typeof agent) { + throw new TypeError('JSUS.isMobileAgent: agent must be undefined ' + + 'or string. Found: ' + agent); + } + rx = new RegExp('Android|webOS|iPhone|iPad|BlackBerry|' + + 'Windows Phone|Opera Mini|IEMobile|Mobile', 'i'); + + return rx.test(agent); + }; + /** * ## PARSE.tokenize * @@ -6051,19 +6100,21 @@ * @param {mixed} n The value to check * @param {number} lower Optional. If set, n must be greater than lower * @param {number} upper Optional. If set, n must be smaller than upper + * @param {boolean} leq Optional. If TRUE, n can also be equal to lower + * @param {boolean} ueq Optional. If TRUE, n can also be equal to upper * * @return {boolean|number} The parsed integer, or FALSE if none was found * * @see PARSE.isFloat * @see PARSE.isNumber */ - PARSE.isInt = function(n, lower, upper) { + PARSE.isInt = function(n, lower, upper, leq, ueq) { var regex, i; regex = /^-?\d+$/; if (!regex.test(n)) return false; i = parseInt(n, 10); if (i !== parseFloat(n)) return false; - return PARSE.isNumber(i, lower, upper); + return PARSE.isNumber(i, lower, upper, leq, ueq); }; /** @@ -6076,18 +6127,20 @@ * @param {mixed} n The value to check * @param {number} lower Optional. If set, n must be greater than lower * @param {number} upper Optional. If set, n must be smaller than upper + * @param {boolean} leq Optional. If TRUE, n can also be equal to lower + * @param {boolean} ueq Optional. If TRUE, n can also be equal to upper * * @return {boolean|number} The parsed float, or FALSE if none was found * * @see PARSE.isInt * @see PARSE.isNumber */ - PARSE.isFloat = function(n, lower, upper) { + PARSE.isFloat = function(n, lower, upper, leq, ueq) { var regex; regex = /^-?\d*(\.\d+)?$/; if (!regex.test(n)) return false; if (n.toString().indexOf('.') === -1) return false; - return PARSE.isNumber(n, lower, upper); + return PARSE.isNumber(n, lower, upper, leq, ueq); }; /** @@ -6100,17 +6153,23 @@ * @param {mixed} n The value to check * @param {number} lower Optional. If set, n must be greater than lower * @param {number} upper Optional. If set, n must be smaller than upper + * @param {boolean} leq Optional. If TRUE, n can also be equal to lower + * @param {boolean} ueq Optional. If TRUE, n can also be equal to upper * * @return {boolean|number} The parsed number, or FALSE if none was found * * @see PARSE.isInt * @see PARSE.isFloat */ - PARSE.isNumber = function(n, lower, upper) { + PARSE.isNumber = function(n, lower, upper, leq, ueq) { if (isNaN(n) || !isFinite(n)) return false; n = parseFloat(n); - if ('number' === typeof lower && n < lower) return false; - if ('number' === typeof upper && n > upper) return false; + if ('number' === typeof lower && (leq ? n < lower : n <= lower)) { + return false; + } + if ('number' === typeof upper && (ueq ? n > upper : n >= upper)) { + return false; + } return n; }; diff --git a/build/jsus.min.js b/build/jsus.min.js index f38c930..2182967 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e,n){var r;n="undefined"==typeof n?!0:n;if(n&&"undefined"==typeof t.clone)return t.log("JSUS.require: JSUS.clone not found, but clone requested. Cannot continue."),!1;if("undefined"==typeof e)r=t._classes;else{r=t._classes[e];if("undefined"==typeof r)return t.log("JSUS.require: could not find component "+e),!1}return n?t.clone(r):r},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=function(){function t(e,r,i,s,o,u,a,f,l,c,h,p,d){var v,m,g,y;m=i===r;for(v in e)if(e.hasOwnProperty(v)){g="object"==typeof e[v];if(u||a&&(m||!g)||f&&m)o?(y=s+v,p[y]||(d?n(y,c,d):c.push(y))):p[v]||(h?h[v]||(d?n(v,c,d):c.push(v),h[v]=!0):d?n(v,c,d):c.push(v));g&&i1&&(n.push(i[1]),i.length>2&&n.push(i[2]))):function(){var e=-1,t=i.length;for(;++e=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=function(){function t(e,r,i,s,o,u,a,f,l,c,h,p,d){var v,m,g,y;m=i===r;for(v in e)if(e.hasOwnProperty(v)){g="object"==typeof e[v];if(u||a&&(m||!g)||f&&m)o?(y=s+v,p[y]||(d?n(y,c,d):c.push(y))):p[v]||(h?h[v]||(d?n(v,c,d):c.push(v),h[v]=!0):d?n(v,c,d):c.push(v));g&&i1&&(n.push(i[1]),i.length>2&&n.push(i[2]))):function(){var e=-1,t=i.length;for(;++e=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in:e>=n)?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o + * MIT Licensed + * + * Prototype of a widget class + * + * Prototype methods will be injected in every new widget, if missing. + * + * Additional properties can be automatically, depending on configuration. + * + * @see Widgets.get + * @see Widgets.append + */ +(function(node) { + + "use strict"; + + var J = node.JSUS; + var NDDB = node.NDDB; + + node.Widget = Widget; + + /** + * ### Widget constructor + * + * Creates a new instance of widget + * + * Should create all widgets properties, but the `init` method + * initialize them. Some properties are added automatically + * by `Widgets.get` after the constructor has been called, + * but before `init`. + * + * @see Widgets.get + * @see Widget.init + */ + function Widget() {} + + /** + * ### Widget.init + * + * Inits the widget after constructor and default properties are added + * + * @param {object} options Configuration options + * + * @see Widgets.get + */ + Widget.prototype.init = function(options) {}; + + /** + * ### Widget.listeners + * + * Wraps calls event listeners registration + * + * Event listeners registered here are automatically removed + * when widget is destroyed (if still active) + * + * @see EventEmitter.setRecordChanges + * @see Widgets.destroy + */ + Widget.prototype.listeners = function() {}; + + /** + * ### Widget.append + * + * Creates HTML elements and appends them to the `panelDiv` element + * + * The method is called by `Widgets.append` which evaluates user-options + * and adds the default container elements of a widget: + * + * - panelDiv: the outer container + * - headingDiv: the title container + * - bodyDiv: the main container + * - footerDiv: the footer container + * + * To ensure correct destroyal of the widget, all HTML elements should + * be children of Widget.panelDiv + * + * @see Widgets.append + * @see Widgets.destroy + * @see Widget.panelDiv + * @see Widget.footerDiv + * @see Widget.headingDiv + */ + Widget.prototype.append = function() {}; + + /** + * ### Widget.getValues + * + * Returns the values currently stored by the widget + * + * @param {mixed} options Settings controlling the content of return value + * + * @return {mixed} The values of the widget + */ + Widget.prototype.getValues = function(options) {}; + + /** + * ### Widget.getValues + * + * Set the stored values directly + * + * The method should not set the values, if widget is disabled + * + * @param {mixed} values The values to store + */ + Widget.prototype.setValues = function(values) {}; + + /** + * ### Widget.reset + * + * Resets the widget + * + * Deletes current selection, any highlighting, and other data + * that the widget might have collected to far. + */ + Widget.prototype.reset = function(options) {}; + + /** + * ### Widget.highlight + * + * Hightlights the user interface of the widget in some way + * + * By default, it adds a red border around the bodyDiv. + * + * If widget was not appended, i.e., no `panelDiv` has been created, + * nothing happens. + * + * @param {mixed} options Settings controlling the type of highlighting + */ + Widget.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError(J.funcName(this.constructor) + '.highlight: ' + + 'border must be string or undefined. Found: ' + + border); + } + if (!this.isAppended() || this.isHighlighted()) return; + this.highlighted = true; + this.bodyDiv.style.border = border || '3px solid red'; + this.emit('highlighted', border); + }; + + /** + * ### Widget.highlight + * + * Hightlights the user interface of the widget in some way + * + * Should mark the state of widget as not `highlighted`. + * + * If widget was not appended, i.e., no `panelDiv` has been created, + * nothing happens. + * + * @see Widget.highlight + */ + Widget.prototype.unhighlight = function() { + if (!this.isHighlighted()) return; + this.highlighted = false; + this.bodyDiv.style.border = ''; + this.emit('unhighlighted'); + }; + + /** + * ### Widget.isHighlighted + * + * Returns TRUE if widget is currently highlighted + * + * @return {boolean} TRUE, if widget is currently highlighted + */ + Widget.prototype.isHighlighted = function() { + return !!this.highlighted; + }; + + /** + * ### Widget.isDocked + * + * Returns TRUE if widget is currently docked + * + * @return {boolean} TRUE, if widget is currently docked + */ + Widget.prototype.isDocked = function() { + return !!this.docked; + }; + + /** + * ### Widget.collapse + * + * Collapses the widget (hides the body and footer) + * + * Only, if it was previously appended to DOM + * + * @see Widget.uncollapse + * @see Widget.isCollapsed + */ + Widget.prototype.collapse = function() { + if (!this.panelDiv) return; + this.bodyDiv.style.display = 'none'; + this.collapsed = true; + if (this.collapseButton) { + this.collapseButton.src = '/images/maximize_small2.png'; + this.collapseButton.title = 'Restore'; + } + if (this.footer) this.footer.style.display = 'none'; + // Move into collapse target, if one is specified. + if (this.collapseTarget) this.collapseTarget.appendChild(this.panelDiv); + this.emit('collapsed'); + }; + + /** + * ### Widget.uncollapse + * + * Uncollapses the widget (shows the body and footer) + * + * Only if it was previously appended to DOM + * + * @see Widget.collapse + * @see Widget.isCollapsed + */ + Widget.prototype.uncollapse = function() { + if (!this.panelDiv) return; + if (this.collapseTarget) { + this.originalRoot.appendChild(this.panelDiv); + } + this.bodyDiv.style.display = ''; + this.collapsed = false; + if (this.collapseButton) { + this.collapseButton.src = '/images/maximize_small.png'; + this.collapseButton.title = 'Minimize'; + } + if (this.footer) this.footer.style.display = ''; + this.emit('uncollapsed'); + }; + + /** + * ### Widget.isCollapsed + * + * Returns TRUE if widget is currently collapsed + * + * @return {boolean} TRUE, if widget is currently collapsed + */ + Widget.prototype.isCollapsed = function() { + return !!this.collapsed; + }; + + /** + * ### Widget.enable + * + * Enables the widget + * + * An enabled widget allows the user to interact with it + */ + Widget.prototype.enable = function(options) { + if (!this.disabled) return; + this.disabled = false; + this.emit('enabled', options); + }; + + /** + * ### Widget.disable + * + * Disables the widget + * + * A disabled widget is still visible, but user cannot interact with it + */ + Widget.prototype.disable = function(options) { + if (this.disabled) return; + this.disabled = true; + this.emit('disabled', options); + }; + + /** + * ### Widget.isDisabled + * + * Returns TRUE if widget is disabled + * + * @return {boolean} TRUE if widget is disabled + * + * @see Widget.enable + * @see Widget.disable + * @see Widget.disabled + */ + Widget.prototype.isDisabled = function() { + return !!this.disabled; + }; + + /** + * ### Widget.hide + * + * Hides the widget, if it was previously appended to DOM + * + * Sets the 'display' property of `panelDiv` to 'none' + * + * @see Widget.show + * @see Widget.toggle + */ + Widget.prototype.hide = function() { + if (!this.panelDiv) return; + if (this.hidden) return; + this.panelDiv.style.display = 'none'; + this.hidden = true; + this.emit('hidden'); + }; + + /** + * ### Widget.show + * + * Shows the widget, if it was previously appended and hidden + * + * Sets the 'display' property of `panelDiv` to '' + * + * @param {string} display Optional. The value of the display + * property. Default: '' + * + * @see Widget.hide + * @see Widget.toggle + */ + Widget.prototype.show = function(display) { + if (this.panelDiv && this.panelDiv.style.display === 'none') { + this.panelDiv.style.display = display || ''; + this.hidden = false; + this.emit('shown'); + } + }; + + /** + * ### Widget.toggle + * + * Toggles the display of the widget, if it was previously appended + * + * @param {string} display Optional. The value of the display + * property in case the widget is currently hidden. Default: '' + * + * @see Widget.hide + */ + Widget.prototype.toggle = function(display) { + if (!this.panelDiv) return; + if (this.hidden) this.show(); + else this.hide(); + }; + + /** + * ### Widget.isHidden + * + * TRUE if widget is hidden or not yet appended + * + * @return {boolean} TRUE if widget is hidden, or if it was not + * appended to the DOM yet + */ + Widget.prototype.isHidden = function() { + return !!this.hidden; + }; + + /** + * ### Widget.destroy + * + * Performs cleanup operations + * + * `Widgets.get` wraps this method in an outer callback performing + * default cleanup operations. + * + * @see Widgets.get + */ + Widget.prototype.destroy = null; + + /** + * ### Widget.setTitle + * + * Creates/removes an heading div with a given title + * + * Adds/removes a div with class `panel-heading` to the `panelDiv`. + * + * @param {string|HTMLElement|false} Optional. The title for the heading, + * div an HTML element, or false to remove the header completely. + * @param {object} Optional. Options to be passed to `W.add` if a new + * heading div is created. Default: { className: 'panel-heading' } + * + * @see Widget.headingDiv + * @see GameWindow.add + */ + Widget.prototype.setTitle = function(title, options) { + var tmp; + if (!this.panelDiv) { + throw new Error('Widget.setTitle: panelDiv is missing.'); + } + + // Remove heading with false-ish argument. + if (!title) { + if (this.headingDiv) { + this.panelDiv.removeChild(this.headingDiv); + this.headingDiv = null; + } + } + else { + if (!this.headingDiv) { + // Add heading. + if (!options) { + options = { className: 'panel-heading' }; + } + else if ('object' !== typeof options) { + throw new TypeError('Widget.setTitle: options must ' + + 'be object or undefined. Found: ' + + options); + } + this.headingDiv = W.add('div', this.panelDiv, options); + // Move it to before the body (IE cannot have undefined). + tmp = (this.bodyDiv && this.bodyDiv.childNodes[0]) || null; + this.panelDiv.insertBefore(this.headingDiv, tmp); + } + + // Set title. + if (W.isElement(title)) { + // The given title is an HTML element. + this.headingDiv.innerHTML = ''; + this.headingDiv.appendChild(title); + } + else if ('string' === typeof title) { + this.headingDiv.innerHTML = title; + } + else { + throw new TypeError(J.funcName(this.constructor) + + '.setTitle: title must be string, ' + + 'HTML element or falsy. Found: ' + title); + } + if (this.collapsible) { + // Generates a button that hides the body of the panel. + (function(that) { + var link, img; + link = document.createElement('span'); + link.className = 'panel-collapse-link'; + img = document.createElement('img'); + img.src = '/images/minimize_small.png'; + link.appendChild(img); + link.onclick = function() { + if (that.isCollapsed()) that.uncollapse(); + else that.collapse(); + }; + that.headingDiv.appendChild(link); + })(this); + } + if (this.closable) { + (function(that) { + var link, img; + link = document.createElement('span'); + link.className = 'panel-collapse-link'; + // link.style['margin-right'] = '8px'; + img = document.createElement('img'); + img.src = '/images/close_small.png'; + link.appendChild(img); + link.onclick = function() { + that.destroy(); + }; + that.headingDiv.appendChild(link); + })(this); + } + } + }; + + /** + * ### Widget.setFooter + * + * Creates/removes a footer div with a given content + * + * Adds/removes a div with class `panel-footer` to the `panelDiv`. + * + * @param {string|HTMLElement|false} Optional. The title for the header, + * an HTML element, or false to remove the header completely. + * @param {object} Optional. Options to be passed to `W.add` if a new + * footer div is created. Default: { className: 'panel-footer' } + * + * @see Widget.footerDiv + * @see GameWindow.add + */ + Widget.prototype.setFooter = function(footer, options) { + if (!this.panelDiv) { + throw new Error('Widget.setFooter: panelDiv is missing.'); + } + + // Remove footer with false-ish argument. + if (!footer) { + if (this.footerDiv) { + this.panelDiv.removeChild(this.footerDiv); + delete this.footerDiv; + } + } + else { + if (!this.footerDiv) { + // Add footer. + if (!options) { + options = { className: 'panel-footer' }; + } + else if ('object' !== typeof options) { + throw new TypeError('Widget.setFooter: options must ' + + 'be object or undefined. Found: ' + + options); + } + this.footerDiv = W.add('div', this.panelDiv, options); + } + + // Set footer contents. + if (W.isElement(footer)) { + // The given footer is an HTML element. + this.footerDiv.innerHTML = ''; + this.footerDiv.appendChild(footer); + } + else if ('string' === typeof footer) { + this.footerDiv.innerHTML = footer; + } + else { + throw new TypeError(J.funcName(this.constructor) + + '.setFooter: footer must be string, ' + + 'HTML element or falsy. Found: ' + title); + } + } + }; + + /** + * ### Widget.setContext + * + * Changes the default context of the class 'panel-' + context + * + * Context are defined in Bootstrap framework. + * + * @param {string} context The type of the context + */ + Widget.prototype.setContext = function(context) { + if ('string' !== typeof context) { + throw new TypeError(J.funcName(this.constructor) + '.setContext: ' + + 'context must be string. Found: ' + context); + + } + W.removeClass(this.panelDiv, 'panel-[a-z]*'); + W.addClass(this.panelDiv, 'panel-' + context); + }; + + /** + * ### Widget.addFrame + * + * Adds a border and margins around the bodyDiv element + * + * @param {string} context The type of bootstrap context. + * Default: 'default' + * + * @see Widget.panelDiv + * @see Widget.bodyDiv + */ + Widget.prototype.addFrame = function(context) { + if ('undefined' === typeof context) { + context = 'default'; + } + else if ('string' !== typeof context || context.trim() === '') { + throw new TypeError(J.funcName(this.constructor) + + '.addFrame: context must be a non-empty ' + + 'string or undefined. Found: ' + context); + } + if (this.panelDiv) { + if (this.panelDiv.className.indexOf('panel-') === -1) { + W.addClass(this.panelDiv, 'panel-' + context); + } + } + if (this.bodyDiv) { + if (this.bodyDiv.className.indexOf('panel-body') === -1) { + W.addClass(this.bodyDiv, 'panel-body'); + } + } + }; + + /** + * ### Widget.removeFrame + * + * Removes the border and the margins around the bodyDiv element + * + * @see Widget.panelDiv + * @see Widget.bodyDiv + */ + Widget.prototype.removeFrame = function() { + if (this.panelDiv) W.removeClass(this.panelDiv, 'panel-[a-z]*'); + if (this.bodyDiv) W.removeClass(this.bodyDiv, 'panel-body'); + }; + + /** + * ### Widget.isAppended + * + * Returns TRUE if widget was appended to DOM (using Widget API) + * + * Checks if the panelDiv element has been created. + * + * @return {boolean} TRUE, if node.widgets.append was called + */ + Widget.prototype.isAppended = function() { + return !!this.panelDiv; + }; + + /** + * ### Widget.isDestroyed + * + * Returns TRUE if widget has been destroyed + * + * @return {boolean} TRUE, if the widget has been destroyed + */ + Widget.prototype.isDestroyed = function() { + return !!this.destroyed; + }; + + /** + * ### Widget.setSound + * + * Checks and assigns the value of a sound to play to user + * + * Throws an error if value is invalid + * + * @param {string} name The name of the sound to check + * @param {mixed} path Optional. The path to the audio file. If undefined + * the default value from Widget.sounds is used + * + * @see Widget.sounds + * @see Widget.getSound + * @see Widget.setSounds + * @see Widget.getSounds + */ + Widget.prototype.setSound = function(name, value) { + strSetter(this, name, value, 'sounds', 'Widget.setSound'); + }; + + /** + * ### Widget.setSounds + * + * Assigns multiple sounds at the same time + * + * @param {object} sounds Optional. Object containing sound paths + * + * @see Widget.sounds + * @see Widget.setSound + * @see Widget.getSound + * @see Widget.getSounds + */ + Widget.prototype.setSounds = function(sounds) { + strSetterMulti(this, sounds, 'sounds', 'setSound', + J.funcName(this.constructor) + '.setSounds'); + }; + + /** + * ### Widget.getSound + * + * Returns the requested sound path + * + * @param {string} name The name of the sound variable. + * @param {mixed} param Optional. Additional info to pass to the + * callback, if any + * + * @return {string} The requested sound + * + * @see Widget.sounds + * @see Widget.setSound + * @see Widget.getSound + * @see Widget.getSounds + */ + Widget.prototype.getSound = function(name, param) { + return strGetter(this, name, 'sounds', + J.funcName(this.constructor) + '.getSound', param); + }; + + /** + * ### Widget.getSounds + * + * Returns an object with selected sounds (paths) + * + * @param {object|array} keys Optional. An object whose keys, or an array + * whose values, are used of to select the properties to return. + * Default: all properties in the collection object. + * @param {object} param Optional. Object containing parameters to pass + * to the sounds functions (if any) + * + * @return {object} Selected sounds (paths) + * + * @see Widget.sounds + * @see Widget.setSound + * @see Widget.getSound + * @see Widget.setSounds + */ + Widget.prototype.getSounds = function(keys, param) { + return strGetterMulti(this, 'sounds', 'getSound', + J.funcName(this.constructor) + + '.getSounds', keys, param); + }; + + /** + * ### Widget.getAllSounds + * + * Returns an object with all current sounds + * + * @param {object} param Optional. Object containing parameters to pass + * to the sounds functions (if any) + * + * @return {object} All current sounds + * + * @see Widget.getSound + */ + Widget.prototype.getAllSounds = function(param) { + return strGetterMulti(this, 'sounds', 'getSound', + J.funcName(this.constructor) + '.getAllSounds', + undefined, param); + }; + + /** + * ### Widget.setText + * + * Checks and assigns the value of a text to display to user + * + * Throws an error if value is invalid + * + * @param {string} name The name of the property to check + * @param {mixed} value Optional. The value for the text. If undefined + * the default value from Widget.texts is used + * + * @see Widget.texts + * @see Widget.getText + * @see Widget.setTexts + * @see Widget.getTexts + */ + Widget.prototype.setText = function(name, value) { + strSetter(this, name, value, 'texts', + J.funcName(this.constructor) + '.setText'); + }; + + /** + * ### Widget.setTexts + * + * Assigns all texts + * + * @param {object} texts Optional. Object containing texts + * + * @see Widget.texts + * @see Widget.setText + * @see Widget.getText + * @see Widget.getTexts + */ + Widget.prototype.setTexts = function(texts) { + strSetterMulti(this, texts, 'texts', 'setText', + J.funcName(this.constructor) + '.setTexts'); + }; + + /** + * ### Widget.getText + * + * Returns the requested text + * + * @param {string} name The name of the text variable. + * @param {mixed} param Optional. Additional to pass to the callback, if any + * + * @return {string} The requested text + * + * @see Widget.texts + * @see Widget.setText + * @see Widget.setTexts + * @see Widget.getTexts + */ + Widget.prototype.getText = function(name, param) { + return strGetter(this, name, 'texts', + J.funcName(this.constructor) + '.getText', param); + }; + + /** + * ### Widget.getTexts + * + * Returns an object with selected texts + * + * @param {object|array} keys Optional. An object whose keys, or an array + * whose values, are used of to select the properties to return. + * Default: all properties in the collection object. + * @param {object} param Optional. Object containing parameters to pass + * to the sounds functions (if any) + * + * @return {object} Selected texts + * + * @see Widget.texts + * @see Widget.setText + * @see Widget.getText + * @see Widget.setTexts + * @see Widget.getAllTexts + */ + Widget.prototype.getTexts = function(keys, param) { + return strGetterMulti(this, 'texts', 'getText', + J.funcName(this.constructor) + + '.getTexts', keys, param); + }; + + /** + * ### Widget.getAllTexts + * + * Returns an object with all current texts + * + * @param {object|array} param Optional. Object containing parameters + * to pass to the texts functions (if any) + * + * @return {object} All current texts + * + * @see Widget.texts + * @see Widget.setText + * @see Widget.setTexts + * @see Widget.getText + */ + Widget.prototype.getAllTexts = function(param) { + return strGetterMulti(this, 'texts', 'getText', + J.funcName(this.constructor) + + '.getAllTexts', undefined, param); + }; + + // ## Event-Emitter methods borrowed from NDDB + + /** + * ### Widget.on + * + * Registers an event listener for the widget + * + * @see NDDB.off + */ + Widget.prototype.on = function() { + NDDB.prototype.on.apply(this, arguments); + }; + + /** + * ### Widget.off + * + * Removes and event listener for the widget + * + * @see NDDB.off + */ + Widget.prototype.off = function() { + NDDB.prototype.off.apply(this, arguments); + }; + + /** + * ### Widget.emit + * + * Emits an event within the widget + * + * @see NDDB.emit + */ + Widget.prototype.emit = function() { + NDDB.prototype.emit.apply(this, arguments); + }; + + /** + * ### Widget.throwErr + * + * Get the name of the actual widget and throws the error + * + * It does **not** perform type checking on itw own input parameters. + * + * @param {string} type Optional. The error type, e.g. 'TypeError'. + * Default, 'Error' + * @param {string} method Optional. The name of the method + * @param {string|object} err Optional. The error. Default, 'generic error' + * + * @see NDDB.throwErr + */ + Widget.prototype.throwErr = function(type, method, err) { + var errMsg; + errMsg = J.funcName(this.constructor) + '.' + method + ': '; + if ('object' === typeof err) errMsg += err.stack || err; + else if ('string' === typeof err) errMsg += err; + if (type === 'TypeError') throw new TypeError(errMsg); + throw new Error(errMsg); + }; + + // ## Helper methods. + + /** + * ### strGetter + * + * Returns the value a property from a collection in instance/constructor + * + * If the string is not found in the live instance, the default value + * from the same collection inside the contructor is returned instead. + * + * If the property is not found in the corresponding static + * collection in the constructor of the instance, an error is thrown. + * + * @param {object} that The main instance + * @param {string} name The name of the property inside the collection + * @param {string} collection The name of the collection inside the instance + * @param {string} method The name of the invoking method (for error string) + * @param {mixed} param Optional. If the value of the requested property + * is a function, this parameter is passed to it to get a return value. + * + * @return {string} res The value of requested property as found + * in the instance, or its default value as found in the constructor + */ + function strGetter(that, name, collection, method, param) { + var res; + if (!that.constructor[collection].hasOwnProperty(name)) { + throw new Error(method + ': name not found: ' + name); + } + res = 'undefined' !== typeof that[collection][name] ? + that[collection][name] : that.constructor[collection][name]; + if ('function' === typeof res) { + res = res(that, param); + if ('string' !== typeof res && res !== false) { + throw new TypeError(method + ': cb "' + name + '" did not ' + + 'return neither string or false. Found: ' + + res); + } + } + return res; + } + + /** + * ### strGetterMulti + * + * Same as strGetter, but returns multiple values at once + * + * @param {object} that The main instance + * @param {string} collection The name of the collection inside the instance + * @param {string} getMethod The name of the method to get each value + * @param {string} method The name of the invoking method (for error string) + * @param {object|array} keys Optional. An object whose keys, or an array + * whose values, are used of this object are to select the properties + * to return. Default: all properties in the collection object. + * @param {mixed} param Optional. If the value of the requested property + * is a function, this parameter is passed to it, when invoked to get + * a return value. Default: undefined + * + * @return {string} out The requested value. + * + * @see strGetter + */ + function strGetterMulti(that, collection, getMethod, method, keys, param) { + var out, k, len; + if (!keys) keys = that.constructor[collection]; + if ('undefined' === typeof param) { + param = {}; + } + out = {}; + if (J.isArray(keys)) { + k = -1, len = keys.length; + for ( ; ++k < len;) { + out[keys[k]] = that[getMethod](keys[k], param); + } + } + else { + for (k in keys) { + if (keys.hasOwnProperty(k)) { + out[k] = that[getMethod](k, param); + } + } + } + return out; + } + + /** + * ### strSetterMulti + * + * Same as strSetter, but sets multiple values at once + * + * @param {object} that The main instance + * @param {object} obj List of properties to set and their values + * @param {string} collection The name of the collection inside the instance + * @param {string} setMethod The name of the method to set each value + * @param {string} method The name of the invoking method (for error string) + * + * @see strSetter + */ + function strSetterMulti(that, obj, collection, setMethod, method) { + var i; + if ('object' !== typeof obj && 'undefined' !== typeof obj) { + throw new TypeError(method + ': ' + collection + + ' must be object or undefined. Found: ' + obj); + } + for (i in obj) { + if (obj.hasOwnProperty(i)) { + that[setMethod](i, obj[i]); + } + } + } + + /** + * ### strSetter + * + * Sets the value of a property in a collection if string, function or false + * + * @param {object} that The main instance + * @param {string} name The name of the property to set + * @param {string|function|false} value The value for the property + * @param {string} collection The name of the collection inside the instance + * @param {string} method The name of the invoking method (for error string) + * + * @see strSetter + */ + function strSetter(that, name, value, collection, method) { + if ('undefined' === typeof that.constructor[collection][name]) { + throw new TypeError(method + ': name not found: ' + name); + } + if ('string' === typeof value || + 'function' === typeof value || + false === value) { + + that[collection][name] = value; + } + else { + throw new TypeError(method + ': value for item "' + name + + '" must be string, function or false. ' + + 'Found: ' + value); + } + } + + + +})( + // Widgets works only in the browser environment. + ('undefined' !== typeof node) ? node : module.parent.exports.node +); + +/** + * # Widgets + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Helper class to interact with nodeGame widgets + * + * http://nodegame.org + */ +(function(window, node) { + + "use strict"; + + var NDDB = window.NDDB; + + // ## Widgets constructor + + function Widgets() { + var that; + + /** + * ### Widgets.widgets + * + * Container of currently registered widgets + * + * @see Widgets.register + */ + this.widgets = {}; + + /** + * ### Widgets.instances + * + * Container of appended widget instances + * + * @see Widgets.append + * @see Widgets.lastAppended + */ + this.instances = []; + + /** + * ### Widgets.lastAppended + * + * Reference to lastAppended widget + * + * @see Widgets.append + */ + this.lastAppended = null; + + /** + * ### Widgets.docked + * + * List of docked widgets + */ + this.docked = []; + + /** + * ### Widgets.dockedHidden + * + * List of hidden docked widgets (cause not enough space on page) + */ + this.dockedHidden = []; + + /** + * ### Widgets.boxSelector + * + * A box selector widget containing hidden docked widgets + */ + this.boxSelector = null; + + /** + * ### Widgets.collapseTarget + * + * Collapsed widgets are by default moved inside element + */ + this.collapseTarget = null; + + that = this; + node.registerSetup('widgets', function(conf) { + var name, root, collapseTarget; + if (!conf) return; + + // Add new widgets. + if (conf.widgets) { + for (name in conf.widgets) { + if (conf.widgets.hasOwnProperty(name)) { + that.register(name, conf.widgets[name]); + } + } + } + + // Destroy all existing widgets. + if (conf.destroyAll) that.destroyAll(); + + // Append existing widgets. + if (conf.append) { + for (name in conf.append) { + if (conf.append.hasOwnProperty(name)) { + // Determine root. + root = conf.append[name].root; + if ('function' === typeof root) { + root = root(); + } + else if ('string' === typeof root) { + root = W.getElementById(root); + } + if (!root) root = W.getScreen(); + + if (!root) { + node.warn('setup widgets: could not find a root ' + + 'for widget ' + name + '. Requested: ' + + conf.append[name].root); + } + else { + that.append(name, root, conf.append[name]); + } + } + } + } + + if (conf.collapseTarget) { + if ('function' === typeof conf.collapseTarget) { + collapseTarget = conf.collapseTarget(); + } + else if ('string' === typeof conf.collapseTarget) { + collapseTarget = W.getElementById(conf.collapseTarget); + } + else if (J.isElement(conf.collapseTarget)) { + collapseTarget = conf.collapseTarget; + } + if (!collapseTarget) { + node.warn('setup widgets: could not find collapse target.'); + } + else { + that.collapseTarget = collapseTarget; + } + } + + return conf; + }); + + // Garbage collection. + node.on('FRAME_LOADED', function() { + node.widgets.garbageCollection(); + }); + + node.info('node-widgets: loading'); + } + + // ## Widgets methods + + /** + * ### Widgets.register + * + * Registers a new widget in the collection + * + * A name and a prototype class must be provided. All properties + * that are present in `node.Widget`, but missing in the prototype + * are added. + * + * Registered widgets can be loaded with Widgets.get or Widgets.append. + * + * @param {string} name The id under which to register the widget + * @param {function} w The widget to add + * + * @return {object|boolean} The registered widget, + * or FALSE if an error occurs + */ + Widgets.prototype.register = function(name, w) { + if ('string' !== typeof name) { + throw new TypeError('Widgets.register: name must be string. ' + + 'Found: ' + name); + } + if ('function' !== typeof w) { + throw new TypeError('Widgets.register: w must be function.' + + 'Found: ' + w); + } + if ('undefined' === typeof w.sounds) w.sounds = {}; + if ('undefined' === typeof w.texts) w.texts = {}; + // Add default properties to widget prototype. + J.mixout(w.prototype, new node.Widget()); + this.widgets[name] = w; + return this.widgets[name]; + }; + + /** + * ### Widgets.get + * + * Retrieves, instantiates and returns the specified widget + * + * Performs the following checkings: + * + * - dependencies, as specified by widget prototype, must exist + * - id, if specified in options, must be string + * + * and throws an error if conditions are not met. + * + * Adds the following properties to the widget object: + * + * - title: as specified by the user or as found in the prototype + * - footer: as specified by the user or as found in the prototype + * - context: as specified by the user or as found in the prototype + * - className: as specified by the user or as found in the prototype + * - id: user-defined id + * - wid: random unique widget id + * - hooks: object containing event listeners + * - disabled: boolean flag indicating the widget state, set to FALSE + * - highlighted: boolean flag indicating whether the panelDiv is + * highlighted, set to FALSE + * - collapsible: boolean flag, TRUE if the widget can be collapsed + * and a button to hide body is added to the header + * - collapsed: boolan flag, TRUE if widget is collapsed (body hidden) + * - closable: boolean flag, TRUE if the widget can be closed (destroyed) + * + * Calls the `listeners` method of the widget. Any event listener + * registered here will be automatically removed when the widget + * is destroyed. !Important: it will erase previously recorded changes + * by the event listener. If `options.listeners` is equal to false, the + * listeners method is skipped. + * + * A `.destroy` method is added to the widget that perform the + * following operations: + * + * - removes the widget from DOM (if it was appended), + * - removes listeners defined during the creation, + * - and remove the widget from Widget.instances, + * - invoke the event 'destroyed'. + * + * + * Finally, a reference to the widget is added in `Widgets.instances`. + * + * @param {string} widgetName The name of the widget to load + * @param {object} options Optional. Configuration options, will be + * mixed out with attributes in the `defaults` property + * of the widget prototype. + * + * @return {object} widget The requested widget + * + * @see Widgets.append + * @see Widgets.instances + */ + Widgets.prototype.get = function(widgetName, options) { + var that; + var WidgetPrototype, widget, changes; + + if ('string' !== typeof widgetName) { + throw new TypeError('Widgets.get: widgetName must be string.' + + 'Found: ' + widgetName); + } + if (!options) { + options = {}; + } + else if ('object' !== typeof options) { + throw new TypeError('Widgets.get: ' + widgetName + ' options ' + + 'must be object or undefined. Found: ' + + options); + } + if (options.storeRef === false) { + if (options.docked === true) { + throw new TypeError('Widgets.get: ' + widgetName + + 'options.storeRef cannot be false ' + + 'if options.docked is true.'); + } + } + + that = this; + + WidgetPrototype = J.getNestedValue(widgetName, this.widgets); + + if (!WidgetPrototype) { + throw new Error('Widgets.get: ' + widgetName + ' not found'); + } + + node.info('creating widget ' + widgetName + + ' v.' + WidgetPrototype.version); + + if (!this.checkDependencies(WidgetPrototype)) { + throw new Error('Widgets.get: ' + widgetName + ' has unmet ' + + 'dependencies'); + } + + // Create widget. + widget = new WidgetPrototype(options); + + // Set ID. + if ('undefined' !== typeof options.id) { + if ('number' === typeof options.id) options.id = '' + options.id; + if ('string' === typeof options.id) { + widget.id = options.id; + } + else { + throw new TypeError('Widgets.get: options.id must be ' + + 'string, number or undefined. Found: ' + + options.id); + } + } + + // Set prototype values or options values. + widget.title = 'undefined' === typeof options.title ? + WidgetPrototype.title : options.title; + widget.panel = 'undefined' === typeof options.panel ? + WidgetPrototype.panel : options.panel; + widget.footer = 'undefined' === typeof options.footer ? + WidgetPrototype.footer : options.footer; + widget.className = WidgetPrototype.className; + if (J.isArray(options.className)) { + widget.className += ' ' + options.className.join(' '); + } + else if ('string' === typeof options.className) { + widget.className += ' ' + options.className; + } + else if ('undefined' !== typeof options.className) { + throw new TypeError('Widgets.get: className must be array, ' + + 'string, or undefined. Found: ' + + options.className); + } + widget.context = 'undefined' === typeof options.context ? + WidgetPrototype.context : options.context; + widget.sounds = 'undefined' === typeof options.sounds ? + WidgetPrototype.sounds : options.sounds; + widget.texts = 'undefined' === typeof options.texts ? + WidgetPrototype.texts : options.texts; + widget.collapsible = options.collapsible || false; + widget.closable = options.closable || false; + widget.collapseTarget = + options.collapseTarget || this.collapseTarget || null; + widget.hooks = { + hidden: [], + shown: [], + collapsed: [], + uncollapsed: [], + disabled: [], + enabled: [], + destroyed: [], + highlighted: [], + unhighlighted: [] + }; + + // Fixed properties. + + // Widget Name. + widget.widgetName = widgetName; + // Add random unique widget id. + widget.wid = '' + J.randomInt(0,10000000000000000000); + + // UI properties. + + widget.disabled = null; + widget.highlighted = null; + widget.collapsed = null; + widget.hidden = null; + widget.docked = null + + // Properties that will modify the UI of the widget once appended. + + if (options.disabled) widget._disabled = true; + if (options.highlighted) widget._highlighted = true; + if (options.collapsed) widget._collapsed = true; + if (options.hidden) widget._hidden = true; + if (options.docked) widget._docked = true; + + // Call init. + widget.init(options); + + // Call listeners. + if (options.listeners !== false) { + + // TODO: future versions should pass the right event listener + // to the listeners method. However, the problem is that it + // does not have `on.data` methods, those are aliases. + + // if ('undefined' === typeof options.listeners) { + // ee = node.getCurrentEventEmitter(); + // } + // else if ('string' === typeof options.listeners) { + // if (options.listeners !== 'game' && + // options.listeners !== 'stage' && + // options.listeners !== 'step') { + // + // throw new Error('Widget.get: widget ' + widgetName + + // ' has invalid value for option ' + + // 'listeners: ' + options.listeners); + // } + // ee = node.events[options.listeners]; + // } + // else { + // throw new Error('Widget.get: widget ' + widgetName + + // ' options.listeners must be false, string ' + + // 'or undefined. Found: ' + options.listeners); + // } + + // Start recording changes. + node.events.setRecordChanges(true); + + widget.listeners.call(widget); + + // Get registered listeners, clear changes, and stop recording. + changes = node.events.getChanges(true); + node.events.setRecordChanges(false); + } + + // If any listener was added or removed, the original situation will + // be restored when the widget is destroyed. + // The widget is also automatically removed from parent. + widget.destroy = function() { + var i, len, ee, eeName; + + (function() { + try { + // Remove the widget's div from its parent. + if (widget.panelDiv && widget.panelDiv.parentNode) { + widget.panelDiv.parentNode.removeChild(widget.panelDiv); + } + } + catch(e) { + node.warn(widgetName + '.destroy: error caught: ' + e); + } + })(); + + if (changes) { + for (eeName in changes) { + if (changes.hasOwnProperty(eeName)) { + ee = changes[eeName]; + i = -1, len = ee.added.length; + for ( ; ++i < len ; ) { + node.events.ee[eeName].off(ee.added[i].type, + ee.added[i].listener); + } + i = -1, len = changes[eeName].removed.length; + for ( ; ++i < len ; ) { + node.events.ee[eeName].on(ee.removed[i].type, + ee.removed[i].listener); + } + } + } + } + + // Remove widget from current instances, if found. + if (widget.storeRef !== false) { + i = -1, len = node.widgets.instances.length; + for ( ; ++i < len ; ) { + if (node.widgets.instances[i].wid === widget.wid) { + node.widgets.instances.splice(i,1); + break; + } + } + // Remove from lastAppended. + if (node.widgets.lastAppended && + node.widgets.lastAppended.wid === this.wid) { + + node.warn('node.widgets.lastAppended destroyed.'); + node.widgets.lastAppended = null; + } + } + + // Remove from docked or adjust frame height. + if (this.docked) closeDocked(widget.wid, false); + else if (node.window) node.window.adjustFrameHeight(undefined, 120); + + // In case the widget is stored somewhere else, set destroyed. + this.destroyed = true; + + this.emit('destroyed'); + }; + + // Store widget instance (e.g., used for destruction). + if (options.storeRef !== false) this.instances.push(widget); + else widget.storeRef = false; + + return widget; + }; + + /** + * ### Widgets.append + * + * Appends a widget to the specified root element + * + * If no root element is specified the widget is append to the global root. + * + * The first parameter can be string representing the name of the widget or + * a valid widget already loaded, for example through Widgets.get. + * In the latter case, dependencies are checked, and it returns FALSE if + * conditions are not met. + * + * @param {string|object} w The name of the widget to load or a loaded + * widget object + * @param {object} root Optional. The HTML element under which the widget + * will be appended. Default: the `document.body` element of the main + * frame (if one is defined), or `document.body` elment of the page + * @param {options} options Optional. Configuration options to be passed + * to the widget + * + * @return {object|boolean} The requested widget, or FALSE is an error + * occurs + * + * @see Widgets.get + */ + Widgets.prototype.append = function(w, root, options) { + var tmp, lastDocked, right; + var dockedMargin; + + if ('string' !== typeof w && 'object' !== typeof w) { + throw new TypeError('Widgets.append: w must be string or object. ' + + 'Found: ' + w); + } + if (root && !J.isElement(root)) { + throw new TypeError('Widgets.append: root must be HTMLElement ' + + 'or undefined. Found: ' + root); + } + if (options && 'object' !== typeof options) { + throw new TypeError('Widgets.append: options must be object or ' + + 'undefined. Found: ' + options); + } + + // Init default values. + options = options || {}; + + // If no root is defined, use the body element of the main frame, + // if none is found, use the document.body. + if (!root) { + root = W.getFrameDocument(); + if (root) root = root.body; + if (!root) root = document.body; + } + + if ('undefined' === typeof options.panel) { + if (root === W.getHeader()) options.panel = false; + } + + // Check if it is a object (new widget). + // If it is a string is the name of an existing widget. + // In this case a dependencies check is done. + if ('string' === typeof w) w = this.get(w, options); + + // Add panelDiv (with or without panel). + tmp = options.panel === false ? true : w.panel === false; + tmp = { + className: tmp ? [ 'ng_widget', 'no-panel', w.className ] : + [ 'ng_widget', 'panel', 'panel-default', w.className ] + }; + + // Dock it. + if (options.docked || w._docked) { + tmp.className.push('docked'); + this.docked.push(w); + w.docked = true; + } + + // Add div inside widget. + w.panelDiv = W.get('div', tmp); + + // Optionally add title (and div). + if (options.title !== false && w.title) { + tmp = options.panel === false ? + 'no-panel-heading' : 'panel-heading'; + w.setTitle(w.title, { className: tmp }); + } + + // Add body (with or without panel). + tmp = options.panel !== false ? 'panel-body' : 'no-panel-body'; + w.bodyDiv = W.append('div', w.panelDiv, { className: tmp }); + + // Optionally add footer. + if (w.footer) { + tmp = options.panel === false ? + 'no-panel-heading' : 'panel-heading'; + w.setFooter(w.footer); + } + + // Optionally set context. + if (w.context) w.setContext(w.context); + + // Adapt UI, if requested. + if (options.hidden || w._hidden) w.hide(); + if (options.collapsed || w._collapsed) w.collapse(); + if (options.disabled || w._disabled) w.disable(); + if (options.highlighted || w._highlighted) w.highlight(); + + // Append. + root.appendChild(w.panelDiv); + w.originalRoot = root; + w.append(); + + // Make sure the distance from the right side is correct. + if (w.docked) setRightStyle(w); + + // Store reference of last appended widget (.get method set storeRef). + if (w.storeRef !== false) this.lastAppended = w; + + return w; + }; + + Widgets.prototype.add = function(w, root, options) { + console.log('***Widgets.add is deprecated. Use ' + + 'Widgets.append instead.***'); + return this.append(w, root, options); + }; + + /** + * ### Widgets.isWidget + * + * Returns TRUE if the object is a widget-like + * + * @param {object} w The object to test + * @param {boolean} strict If TRUE, it checks if object is an + * instance of the Widget class. If FALSE, it just have to + * implement some of its methods (append and getValues). + * + * @return {boolean} TRUE, if the widget was found and destroyed. + * + * @see Widgets.get + * + * @api experimental + */ + Widgets.prototype.isWidget = function(w, strict) { + if (strict) return w instanceof node.Widget; + return ('object' === typeof w && + 'function' === typeof w.append && + 'function' === typeof w.getValues); + }; + + /** + * ### Widgets.destroyAll + * + * Removes all widgets that have been created through Widgets.get + * + * @see Widgets.instances + */ + Widgets.prototype.destroyAll = function() { + var i, len; + i = -1, len = this.instances.length; + // Nested widgets can be destroyed by previous calls to destroy, + // and each call to destroy modify the array of instances. + for ( ; ++i < len ; ) { + this.instances[0].destroy(); + } + this.lastAppended = null; + if (this.instances.length) { + node.warn('node.widgets.destroyAll: some widgets could ' + + 'not be destroyed.'); + } + }; + + /** + * ### Widgets.checkDependencies + * + * Checks if all the dependencies are already loaded + * + * Dependencies are searched for in the following objects: + * + * - window + * - node + * - this.widgets + * - node.window + * + * TODO: Check for version and other constraints. + * + * @param {object} w The widget to check + * @param {boolean} quiet Optional. If TRUE, no warning will be raised. + * Default: FALSE + * @return {boolean} TRUE, if all dependencies are met + */ + Widgets.prototype.checkDependencies = function(w, quiet) { + var parents, d, lib, found, i; + if (!w.dependencies) return true; + + parents = [window, node, this.widgets, node.window]; + + d = w.dependencies; + for (lib in d) { + if (d.hasOwnProperty(lib)) { + found = false; + for (i = 0; i < parents.length; i++) { + if (J.getNestedValue(lib, parents[i])) { + found = true; + break; + } + } + if (!found) { + if (!quiet) checkDepErrMsg(w, lib); + return false; + } + } + } + return true; + }; + + /** + * ### Widgets.garbageCollection + * + * Destroys previously appended widgets nowehere to be found on page + * + * @return {array} res List of destroyed widgets + */ + Widgets.prototype.garbageCollection = function() { + var w, i, fd, res; + res = []; + fd = W.getFrameDocument(); + w = node.widgets.instances; + for (i = 0; i < w.length; i++) { + // Check if widget is not on page any more. + if (w[i].isAppended() && + (fd && !fd.contains(w[i].panelDiv)) && + !document.body.contains(w[i].panelDiv)) { + + res.push(w[i]); + w[i].destroy(); + i--; + } + } + return res; + }; + + // ## Helper functions + + // ### checkDepErrMsg + // + // Prints out an error message for a dependency not met. + // + // @param {Widget} w The widget + // @param {string} d The dependency + function checkDepErrMsg(w, d) { + var name = w.name || w.id; + node.err(d + ' not found. ' + name + ' cannot be loaded'); + } + + // ### closeDocked + // + // Shifts docked widgets on page and remove a widget from the docked list + // + // @param {string} wid The widget id + // @param {boolean} remove TRUE, if widget should be removed from + // docked list. Default: FALSE. + // + // @return {boolean} TRUE if a widget with given wid was found + // + // @see BoxSelector + function closeDocked(wid, hide) { + var d, i, len, width, closed; + d = node.widgets.docked; + len = d.length; + for (i = 0; i < len; i++) { + if (width) { + d[i].panelDiv.style.right = + (getPxNum(d[i].panelDiv.style.right) - width) + 'px'; + } + else if (d[i].wid === wid) { + width = d[i].dockedOffsetWidth; + // Remove from docked list. + closed = node.widgets.docked.splice(i, 1)[0]; + if (hide) { + node.widgets.dockedHidden.push(closed); + closed.hide(); + + if (!node.widgets.boxSelector) { + node.widgets.boxSelector = + node.widgets.append('BoxSelector', document.body, { + className: 'docked-left', + getId: function(i) { return i.wid; }, + getDescr: function(i) { return i.title; }, + onclick: function(i, id) { + i.show(); + // First add back to docked list, + // then set right style. + node.widgets.docked.push(i); + setRightStyle(i); + this.removeItem(id); + if (this.items.length === 0) { + this.destroy(); + node.widgets.boxSelector = null; + } + }, + }); + + } + node.widgets.boxSelector.addItem(closed); + } + // Decrement len and i. + len--; + i--; + } + } + return !!width; + } + + // ### setRightStyle + // + // Sets the right property of the panelDiv of a docked widget + // + // May close docked widgets to make space to this one. + // + // @param {Widget} w The widget + function setRightStyle(w) { + var dockedMargin, safeMargin; + var lastDocked, right, ws, tmp; + + safeMargin = 200; + dockedMargin = 20; + + ws = node.widgets; + + right = 0; + // The widget w has been already added to the docked list. + if (ws.docked.length > 1) { + lastDocked = ws.docked[(ws.docked.length - 2)]; + right = getPxNum(lastDocked.panelDiv.style.right); + right += lastDocked.panelDiv.offsetWidth; + } + right += dockedMargin; + + w.panelDiv.style.right = (right + "px"); + + // Check if there is enough space on page? + tmp = 0; + right += w.panelDiv.offsetWidth + safeMargin; + while (ws.docked.length > 1 && + right > window.innerWidth && + tmp < (ws.docked.length - 1)) { + + // Make some space... + closeDocked(ws.docked[tmp].wid, true); + tmp++; + } + // Store final offsetWidth in widget, because we need it after + // it is destroyed. + w.dockedOffsetWidth = w.panelDiv.offsetWidth + dockedMargin; + } + + // ### getPxNum + // + // Returns the numeric value of string containg 'px' at the end, e.g. 20px. + // + // @param {string} The value of a css property containing 'px' at the end + // + // @return {number} The numeric value of the css property + function getPxNum(str) { + return parseInt(str.substring(0, str.length - 2), 10); + } + + // Expose Widgets to the global object. + node.widgets = new Widgets(); + +})( + // Widgets works only in the browser environment. + ('undefined' !== typeof window) ? window : module.parent.exports.window, + ('undefined' !== typeof window) ? window.node : module.parent.exports.node +); + +/** + * # BackButton + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a button that if pressed goes to the previous step + * + * // TODO: check the changes to node.game.getProperty + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('BackButton', BackButton); + + // ## Meta-data + + BackButton.version = '0.1.0'; + BackButton.description = 'Creates a button that if ' + + 'pressed goes to the previous step.'; + + BackButton.title = false; + BackButton.className = 'backbutton'; + BackButton.texts.back = 'Back'; + + // ## Dependencies + + BackButton.dependencies = { + JSUS: {} + }; + + /** + * ## BackButton constructor + * + * Creates a new instance of BackButton + * + * @param {object} options Optional. Configuration options. + * If a `button` option is specified, it sets it as the clickable + * button. All other options are passed to the init method. + * + * @see BackButton.init + */ + function BackButton(options) { + var that; + that = this; + + /** + * ### BackButton.button + * + * The HTML element. + */ + if ('object' === typeof options.button) { + this.button = options.button; + } + else if ('undefined' === typeof options.button) { + this.button = document.createElement('input'); + this.button.type = 'button'; + } + else { + throw new TypeError('BackButton constructor: options.button must ' + + 'be object or undefined. Found: ' + + options.button); + } + + /** + * ### BackButton.acrossStages + * + * If TRUE, the Back button allows to go back within the same stage only + * + * Default: FALSE + */ + this.acrossStages = null; + + /** + * ### BackButton.acrossRounds + * + * If TRUE, the Back button allows to go back within the same stage only + * + * Default: TRUE + */ + this.acrossRounds = null; + + this.button.onclick = function() { + var res; + res = getPreviousStep(that); + if (!res) return; + res = node.game.gotoStep(res); + if (res) that.disable(); + }; + } + + // ## BackButton methods + + /** + * ### BackButton.init + * + * Initializes the instance + * + * Available options are: + * + * - id: id of the HTML button, or false to have none. Default: + * BackButton.className + * - className: the className of the button (string, array), or false + * to have none. Default bootstrap classes: 'btn btn-lg btn-primary' + * - text: the text on the button. Default: BackButton.text + * - acrossStages: if TRUE, allows going back to previous stages. + * Default: FALSE + * - acrossRounds: if TRUE, allows going back to previous rounds in + * the same stage. Default: TRUE + * + * @param {object} options Optional. Configuration options + */ + BackButton.prototype.init = function(options) { + var tmp; + options = options || {}; + + //Button + if ('undefined' === typeof options.id) { + tmp = BackButton.className; + } + else if ('string' === typeof options.id) { + tmp = options.id; + } + else if (false === options.id) { + tmp = ''; + } + else { + throw new TypeError('BackButton.init: options.id must ' + + 'be string, false, or undefined. Found: ' + + options.id); + } + this.button.id = tmp; + + if ('undefined' === typeof options.className) { + tmp = 'btn btn-lg btn-secondary'; + } + else if (options.className === false) { + tmp = ''; + } + else if ('string' === typeof options.className) { + tmp = options.className; + } + else if (J.isArray(options.className)) { + tmp = options.className.join(' '); + } + else { + throw new TypeError('BackButton.init: options.className must ' + + 'be string, array, or undefined. Found: ' + + options.className); + } + this.button.className = tmp; + + // Button text. + this.button.value = 'string' === typeof options.text ? + options.text : this.getText('back'); + + this.acrossStages = 'undefined' === typeof options.acrossStages ? + false : !!options.acrossStages; + this.acrossRounds = 'undefined' === typeof options.acrossRounds ? + true : !!options.acrossRounds; + }; + + BackButton.prototype.append = function() { + this.bodyDiv.appendChild(this.button); + }; + + BackButton.prototype.listeners = function() { + var that = this; + + // Locks the back button in case of a timeout. + node.on('PLAYING', function() { + var prop, step; + step = getPreviousStep(that); + // It might be enabled already, but we do it again. + if (step) that.enable(); + // Check options. + prop = node.game.getProperty('backbutton'); + if (!step || prop === false || + (prop && prop.enableOnPlaying === false)) { + + // It might be disabled already, but we do it again. + that.disable(); + } + if ('string' === typeof prop) that.button.value = prop; + else if (prop && prop.text) that.button.value = prop.text; + }); + }; + + /** + * ### BackButton.disable + * + * Disables the back button + */ + BackButton.prototype.disable = function() { + this.button.disabled = 'disabled'; + }; + + /** + * ### BackButton.enable + * + * Enables the back button + */ + BackButton.prototype.enable = function() { + this.button.disabled = false; + }; + + // ## Helper functions. + + /** + * ### getPreviousStage + * + * Returns the previous step accordingly with widget's settings + * + * @param {BackButton} that The current instance + * + * @return {GameStage|Boolean} The previous step or FALSE if none is found + */ + function getPreviousStep(that) { + var curStage, prevStage; + curStage = node.game.getCurrentGameStage(); + if (curStage.stage === 0) return; + prevStage = node.game.getPreviousStep(); + if (prevStage.stage === 0) return; + if ((curStage.stage > prevStage.stage) && !that.acrossStages) { + return false; + } + if ((curStage.round > prevStage.round) && !that.acrossRounds) { + return false; + } + return prevStage; + } + +})(node); + +/** + * # BoxSelector + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a simple box that opens a menu of items to choose from + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + var NDDB = node.NDDB; + + node.widgets.register('BoxSelector', BoxSelector); + + // ## Meta-data + + BoxSelector.version = '1.0.0'; + BoxSelector.description = 'Creates a simple box that opens a menu ' + + 'of items to choose from.'; + + BoxSelector.panel = false; + BoxSelector.title = false; + BoxSelector.className = 'boxselector'; + + // ## Dependencies + + BoxSelector.dependencies = { + JSUS: {} + }; + + /** + * ## BoxSelector constructor + * + * `BoxSelector` is a simple configurable chat + * + * @see BoxSelector.init + */ + function BoxSelector() { + + /** + * ### BoxSelector.button + * + * The button that if pressed shows the items + * + * @see BoxSelector.ul + */ + this.button = null; + + /** + * ### BoxSelector.buttonText + * + * The text on the button + * + * @see BoxSelector.button + */ + this.buttonText = ''; + + /** + * ### BoxSelector.items + * + * List of items to choose from + */ + this.items = []; + + /** + * ### BoxSelector.onclick + * + * A callback to call when an item from the list is clicked + * + * Callback is executed with the BoxSelector instance as context. + * + * Optional. If not specified, items won't be clickable. + * + * @see BoxSelector.items + */ + this.onclick = null; + + /** + * ### BoxSelector.getDescr + * + * A callback that renders an element into a text + */ + this.getDescr = null; + + /** + * ### BoxSelector.getId + * + * A callback that returns the id of an item + * + * Default: returns item.id. + */ + this.getId = function(item) { return item.id; }; + + /** + * ### BoxSelector.ul + * + * The HTML UL element displaying the list of items + * + * @see BoxSelector.items + */ + this.ul = null; + } + + // ## BoxSelector methods + + /** + * ### BoxSelector.init + * + * Initializes the widget + * + * @param {object} options Configuration options. + */ + BoxSelector.prototype.init = function(options) { + if (options.onclick) { + if ('function' !== typeof options.onclick) { + throw new Error('BoxSelector.init: options.getId must be ' + + 'function or undefined. Found: ' + + options.getId); + } + this.onclick = options.onclick; + } + + if ('function' !== typeof options.getDescr) { + throw new Error('BoxSelector.init: options.getDescr must be ' + + 'function. Found: ' + options.getDescr); + } + this.getDescr = options.getDescr; + + if (options.getId && 'function' !== typeof options.getId) { + throw new Error('BoxSelector.init: options.getId must be ' + + 'function or undefined. Found: ' + options.getId); + } + this.getId = options.getId; + + + }; + + + BoxSelector.prototype.append = function() { + var that, ul, btn, btnGroup, toggled; + + btnGroup = W.add('div', this.bodyDiv); + btnGroup.role = 'group'; + btnGroup['aria-label'] = 'Select Items'; + btnGroup.className = 'btn-group dropup'; + + // Here we create the Button holding the treatment. + btn = this.button = W.add('button', btnGroup); + btn.className = 'btn btn-default btn dropdown-toggle'; + btn['data-toggle'] = 'dropdown'; + btn['aria-haspopup'] = 'true'; + btn['aria-expanded'] = 'false'; + btn.innerHTML = this.buttonText + ' '; + + W.add('span', btn, { className: 'caret' }); + + // Here the create the UL of treatments. + // It will be populated later. + ul = this.ul = W.add('ul', btnGroup); + ul.className = 'dropdown-menu'; + ul.style.display = 'none'; + + // Variable toggled controls if the dropdown menu + // is displayed (we are not using bootstrap js files) + // and we redo the job manually here. + toggled = false; + btn.onclick = function() { + if (toggled) { + ul.style.display = 'none'; + toggled = false; + } + else { + ul.style.display = 'block'; + toggled = true; + } + }; + + if (this.onclick) { + that = this; + ul.onclick = function(eventData) { + var id, i, len; + id = eventData.target; + // When '' is hidden by bootstrap class. + ul.style.display = ''; + toggled = false; + id = id.parentNode.id; + // Clicked on description? + if (!id) id = eventData.target.parentNode.parentNode.id; + // Nothing relevant clicked (e.g., header). + if (!id) return; + len = that.items.length; + // Call the onclick. + for ( i = 0 ; i < len ; i++) { + if (that.getId(that.items[i]) === id) { + that.onclick.call(that, that.items[i], id); + break; + } + } + }; + } + }; + + /** + * ### BoxSelector.addItem + * + * Adds an item to the list and renders it + * + * @param {mixed} item The item to add + */ + BoxSelector.prototype.addItem = function(item) { + var ul, li, a, tmp; + ul = this.ul; + li = document.createElement('li'); + // Text. + tmp = this.getDescr(item); + if (!tmp || 'string' !== typeof tmp) { + throw new Error('BoxSelector.addItem: getDescr did not return a ' + + 'string. Found: ' + tmp + '. Item: ' + item); + } + if (this.onclick) { + a = document.createElement('a'); + a.href = '#'; + a.innerHTML = tmp; + li.appendChild(a); + } + else { + li.innerHTML = tmp; + } + // Id. + tmp = this.getId(item); + if (!tmp || 'string' !== typeof tmp) { + throw new Error('BoxSelector.addItem: getId did not return a ' + + 'string. Found: ' + tmp + '. Item: ' + item); + } + li.id = tmp; + li.className = 'dropdown-header'; + ul.appendChild(li); + this.items.push(item); + }; + + /** + * ### BoxSelector.removeItem + * + * Removes an item with given id from the list and the dom + * + * @param {mixed} item The item to add + * + * @return {mixed|boolean} The removed item or false if not found + */ + BoxSelector.prototype.removeItem = function(id) { + var i, len, elem; + len = this.items.length; + for ( i = 0 ; i < len ; i++) { + if (this.getId(this.items[i]) === id) { + elem = W.gid(id); + this.ul.removeChild(elem); + return this.items.splice(i, 1); + } + } + return false; + }; + + BoxSelector.prototype.getValues = function() { + return this.items; + }; + + // ## Helper functions. + + +})(node); + +/** + * # Chat + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a simple configurable chat + * + * // TODO: add is...typing + * // TODO: add bootstrap badge to count msg when collapsed + * // TODO: check on data if message comes back + * // TODO: highlight better incoming msg. Play sound? + * // TODO: removeParticipant and addParticipant methods. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + var NDDB = node.NDDB; + + node.widgets.register('Chat', Chat); + + // ## Texts. + + Chat.texts = { + outgoing: function(w, data) { + return data.msg; + // return '' + data.msg + ''; + }, + incoming: function(w, data) { + var str; + str = ''; + if (w.recipientsIds.length > 1) { + str += '' + + (w.senderToNameMap[data.id] || data.id) + ': '; + } + str += data.msg + ''; + return str; + }, + quit: function(w, data) { + return (w.senderToNameMap[data.id] || data.id) + ' left the chat'; + }, + noMoreParticipants: function(w, data) { + return 'No active participant left. Chat disabled.'; + }, + // For both collapse and uncollapse. + collapse: function(w, data) { + return (w.senderToNameMap[data.id] || data.id) + ' ' + + (data.collapsed ? 'mini' : 'maxi') + 'mized the chat'; + }, + textareaPlaceholder: function(w) { + return w.useSubmitButton ? 'Type something' : + 'Type something and press enter to send'; + }, + submitButton: 'Send' + }; + + // ## Meta-data + + Chat.version = '1.0.0'; + Chat.description = 'Offers a uni-/bi-directional communication interface ' + + 'between players, or between players and the server.'; + + Chat.title = 'Chat'; + Chat.className = 'chat'; + + Chat.panel = false; + + // ## Dependencies + + Chat.dependencies = { + JSUS: {} + }; + + /** + * ## Chat constructor + * + * `Chat` is a simple configurable chat + * + * @see Chat.init + */ + function Chat() { + + /** + * ### Chat.chatEvent + * + * The suffix used to fire chat events + * + * Default: 'CHAT' + */ + this.chatEvent = null; + + /** + * ### Chat.stats + * + * Some basic statistics about message counts + */ + this.stats = { + received: 0, + sent: 0, + unread: 0 + }; + + /** + * ### Chat.submitButton + * + * Button to send a text to server + * + * @see Chat.useSubmitButton + */ + this.submitButton = null; + + /** + * ### Chat.useSubmitButton + * + * If TRUE, a button is added to send messages else ENTER sends msgs + * + * By default, this is TRUE on mobile devices. + * + * @see Chat.submitButton + * @see Chat.receiverOnly + */ + this.useSubmitButton = null; + + /** + * ### Chat.receiverOnly + * + * If TRUE, users cannot send messages (no textarea and submit button) + * + * @see Chat.textarea + */ + this.receiverOnly = false; + + /** + * ### Chat.storeMsgs + * + * If TRUE, a copy of sent and received messages is stored in db + * + * @see Chat.db + */ + this.storeMsgs = false; + + /** + * ### Chat.db + * + * An NDDB database for storing incoming and outgoing messages + * + * @see Chat.storeMsgs + */ + this.db = null; + + /** + * ### Chat.chatDiv + * + * The DIV wherein to display the chat + */ + this.chatDiv = null; + + /** + * ### Chat.textarea + * + * The textarea wherein to write and read + */ + this.textarea = null; + + /** + * ### Chat.initialMsg + * + * An object with an initial msg and the id of sender (if not self) + * + * Example: + * + * ``` + * { + * id: '1234', // Optional, add only this is an 'incoming' msg. + * msg: 'the text' + * } + */ + this.initialMsg = null; + + /** + * ### Chat.recipientsIds + * + * Array of ids of current recipients of messages + */ + this.recipientsIds = null; + + /** + * ### Chat.recipientsIdsQuitted + * + * Array of ids of recipients that have previously quitted the chat + */ + this.recipientsIdsQuitted = null; + + /** + * ### Chat.senderToNameMap + * + * Map sender id (msg.from) to display name + * + * Note: The 'from' field of a message can be different + * from the 'to' field of its reply (e.g., for MONITOR) + */ + this.senderToNameMap = null; + + /** + * ### Chat.recipientToNameMap + * + * Map recipient id (msg.to) to display name + */ + this.recipientToNameMap = null; + + /** + * ### Chat.senderToRecipientMap + * + * Map sender id (msg.from) to recipient id (msg.to) + */ + this.senderToRecipientMap = null; + + /** + * ### Chat.recipientToSenderMap + * + * Map recipient id (msg.to) to sender id (msg.from) + */ + this.recipientToSenderMap = null; + } + + // ## Chat methods + + /** + * ### Chat.init + * + * Initializes the widget + * + * @param {object} options Optional. Configuration options. + * + * The options object can have the following attributes: + * - `receiverOnly`: If TRUE, no message can be sent + * - `chatEvent`: The event to fire when sending/receiving a message + * - `useSubmitButton`: If TRUE, a submit button is added, otherwise + * messages are sent by pressing ENTER. Default: TRUE on mobile + * - `storeMsgs`: If TRUE, a copy of every message is stored in + * a local db + * - `participants`: An array containing the ids of participants, + * cannot be empty + * - `initialMsg`: Initial message to be displayed as soon as the chat + * is opened. + * - `uncollapseOnMsg`: If TRUE, a minimized chat will automatically + * open when receiving a msg. Default: FALSE. + * - `printStartTime`: If TRUE, the initial time of the chat is + * printed at the beginning of the chat. Default: FALSE. + * - `printNames`: If TRUE, the names of the participants of the chat + * is printed at the beginning of the chat. Default: FALSE. + */ + Chat.prototype.init = function(options) { + var tmp, i, rec, sender, that; + + that = this; + + // Chat id. + tmp = options.chatEvent; + if (tmp) { + if ('string' !== typeof tmp) { + throw new TypeError('Chat.init: chatEvent must be a non-' + + 'empty string or undefined. Found: ' + tmp); + } + this.chatEvent = options.chatEvent; + } + else { + this.chatEvent = 'CHAT'; + } + + // Store. + this.storeMsgs = !!options.storeMsgs; + if (this.storeMsgs) { + if (!this.db) this.db = new NDDB(); + } + + // Button or send on Enter?. + this.useSubmitButton = 'undefined' === typeof options.useSubmitButton ? + J.isMobileAgent() : !!options.useSubmitButton; + + // Participants. + tmp = options.participants; + if (!J.isArray(tmp) || !tmp.length) { + throw new TypeError('Chat.init: participants must be ' + + 'a non-empty array. Found: ' + tmp); + } + + // Build maps. + this.recipientsIds = new Array(tmp.length); + this.recipientsIdsQuitted = []; + this.recipientToSenderMap = {}; + this.recipientToNameMap = {}; + this.senderToNameMap = {}; + this.senderToRecipientMap = {}; + + for (i = 0; i < tmp.length; i++) { + // Everything i the same if string. + if ('string' === typeof tmp[i]) { + this.recipientsIds[i] = tmp[i]; + this.recipientToNameMap[tmp[i]] = tmp[i]; + this.recipientToSenderMap[tmp[i]] = tmp[i]; + this.senderToRecipientMap[tmp[i]] = tmp[i]; + this.senderToNameMap[tmp[i]] = tmp[i]; + } + // Sender may be different from receiver if object. + else if ('object' === typeof tmp[i]) { + rec = tmp[i].recipient; + sender = tmp[i].sender; + this.recipientsIds[i] = rec; + this.recipientToSenderMap[rec] = sender || rec; + this.recipientToNameMap[rec] = tmp[i].name || rec; + this.senderToRecipientMap[sender] = rec; + this.senderToNameMap[sender] = this.recipientToNameMap[rec]; + } + else { + throw new TypeError('Chat.init: participants array must ' + + 'contain string or object. Found: ' + + tmp[i]); + } + } + + // Other. + this.uncollapseOnMsg = options.uncollapseOnMsg || false; + + this.printStartTime = options.printStartTime || false; + this.printNames = options.printNames || false; + + if (options.initialMsg) { + if ('object' !== typeof options.initialMsg) { + throw new TypeError('Chat.init: initialMsg must be ' + + 'object or undefined. Found: ' + + options.initialMsg); + } + this.initialMsg = options.initialMsg; + } + + this.on('uncollapsed', function() { + // Make sure that we do not have the title highlighted any more. + that.setTitle(that.title); + if (that.recipientsIds.length) { + node.say(that.chatEvent + '_COLLAPSE', + that.recipientsIds, false); + } + }); + + this.on('collapsed', function() { + if (that.recipientsIds.length) { + node.say(that.chatEvent + '_COLLAPSE', + that.recipientsIds, true); + } + }); + + this.on('destroyed', function() { + if (that.recipientsIds.length) { + node.say(that.chatEvent + '_QUIT', that.recipientsIds); + } + }); + }; + + Chat.prototype.append = function() { + var that, inputGroup, initialText; + + this.chatDiv = W.get('div', { className: 'chat_chat' }); + this.bodyDiv.appendChild(this.chatDiv); + + if (!this.receiverOnly) { + that = this; + + // Input group. + inputGroup = document.createElement('div'); + inputGroup.className = 'chat_inputgroup'; + + this.textarea = W.get('textarea', { + className: 'chat_textarea form-control', + placeholder: this.getText('textareaPlaceholder') + }); + inputGroup.appendChild(this.textarea); + + if (this.useSubmitButton) { + this.submitButton = W.get('button', { + className: 'btn-sm btn-info form-control chat_submit', + innerHTML: this.getText('submitButton') + }); + this.submitButton.onclick = function() { + sendMsg(that); + }; + inputGroup.appendChild(this.submitButton); + } + else { + this.textarea.onkeydown = function(e) { + e = e || window.event; + if ((e.keyCode || e.which) === 13) sendMsg(that); + }; + } + + this.bodyDiv.appendChild(inputGroup); + } + + if (this.printStartTime) { + W.add('div', this.chatDiv, { + innerHTML: Date(J.getDate()), + className: 'chat_event' + }); + initialText = true; + } + + if (this.printNames) { + W.add('div', this.chatDiv, { + className: 'chat_event', + innerHTML: 'Participants: ' + + J.keys(this.senderToNameMap).join(', ') + }); + initialText = true; + } + + if (initialText) { + W.add('div', this.chatDiv, { + className: 'chat_event', + innerHTML: ' ' + }); + } + + if (this.initialMsg) { + this.writeMsg(this.initialMsg.id ? 'incoming' : 'outgoing', + this.initialMsg); + } + }; + + /** + * ### Chat.readTextarea + * + * Reads the value of the textarea, trims it, and removes it from textarea + * + * @return {string} The current value in the textarea + */ + Chat.prototype.readTextarea = function() { + var txt; + txt = this.textarea.value; + this.textarea.value = ''; + return txt.trim(); + }; + + /** + * ### Chat.writeMsg + * + * Writes (and formats) a message (or an event) in the message area + * + * Chat is scrolled up so that the message is last always on focus. + * + * @param {string} code A value indicating the the type of msg. Available: + * 'incoming', 'outgoing', and anything else. + * @param {string} data The content of the message + * + * @return {string} The current value in the textarea + * + * @see Chat.chatDiv + */ + Chat.prototype.writeMsg = function(code, data) { + var c; + c = (code === 'incoming' || code === 'outgoing') ? code : 'event'; + W.add('div', this.chatDiv, { + innerHTML: this.getText(code, data), + className: 'chat_msg chat_msg_' + c + }); + this.chatDiv.scrollTop = this.chatDiv.scrollHeight; + }; + + Chat.prototype.listeners = function() { + var that = this; + + node.on.data(this.chatEvent, function(msg) { + if (!that.handleMsg(msg)) return; + that.stats.received++; + // Store message if so requested. + if (that.storeMsgs) { + that.db.insert({ + from: msg.from, + text: msg.data, + time: node.timer.getTimeSince('step'), + timestamp: J.now() + }); + } + that.writeMsg('incoming', { msg: msg.data, id: msg.from }); + }); + + node.on.data(this.chatEvent + '_QUIT', function(msg) { + var i, len, rec; + if (!that.handleMsg(msg)) return; + that.writeMsg('quit', { id: msg.from }); + len = that.recipientsIds.length; + for ( i = 0 ; i < len ; i++) { + if (that.recipientsIds[i] === + that.senderToRecipientMap[msg.from]) { + + rec = that.recipientsIds.splice(i, 1); + that.recipientsIdsQuitted.push(rec); + + if (that.recipientsIds.length === 0) { + that.writeMsg('noMoreParticipants'); + that.disable(); + } + break; + } + } + node.warn('Chat: participant quitted not found: ' + msg.from); + }); + + node.on.data(this.chatEvent + '_COLLAPSE', function(msg) { + if (!that.handleMsg(msg)) return; + that.writeMsg('collapse', { id: msg.from, collapsed: msg.data}); + }); + }; + + /** + * ### Chat.handleMsg + * + * Checks a (incoming) message and takes some actions + * + * If chat is minimized, it maximizes it if option `uncollapseOnMsg` + * it TRUE; otherwise, it increments the stats for unread messages. + * + * @param {string} msg The content of the message + * + * @return {boolean} TRUE if the message is valid + * + * @see Chat.chatDiv + */ + Chat.prototype.handleMsg = function(msg) { + var from, args; + from = msg.from; + if (from === node.player.id || from === node.player.sid) { + node.warn('Chat: your own message came back: ' + msg.id); + return false; + } + if (this.isCollapsed()) { + if (this.uncollapseOnMsg) { + this.uncollapse(); + this.stats.unread = 0; + } + else { + this.setTitle('' + this.title + ''); + this.stats.unread++; + } + } + return true; + }; + + Chat.prototype.disable = function() { + if (this.submitButton) this.submitButton.disabled = true; + this.textarea.disabled = true; + this.disabled = true; + }; + + Chat.prototype.enable = function() { + if (this.submitButton) this.submitButton.disabled = false; + this.textarea.disabled = false; + this.disabled = false; + }; + + Chat.prototype.getValues = function() { + var out; + out = { + participants: this.participants, + totSent: this.stats.sent, + totReceived: this.stats.received, + totUnread: this.stats.unread, + initialMsg: this.initialMsg + }; + if (this.db) out.msgs = db.fetch(); + return out; + }; + + // ## Helper functions. + + // ### sendMsg + // Reads the textarea and delivers the msg to the server. + function sendMsg(that) { + var msg, to, ids; + + // No msg sent. + if (that.isDisabled()) return; + + msg = that.readTextarea(); + + // Move cursor at the beginning. + if (msg === '') { + node.warn('Chat: message has no text, not sent.'); + return; + } + // Simplify things, if there is only one recipient. + ids = that.recipientsIds; + if (ids.length === 0) { + node.warn('Chat: empty recipient list, message not sent.'); + return; + } + to = ids.length === 1 ? ids[0] : ids; + that.writeMsg('outgoing', { msg: msg }); // to not used now. + node.say(that.chatEvent, to, msg); + // Make sure the cursor goes back to top. + setTimeout(function() { that.textarea.value = ''; }); + } + +})(node); + +/** + * # ChernoffFaces + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Displays multidimensional data in the shape of a Chernoff Face. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + var Table = W.Table; + + node.widgets.register('ChernoffFaces', ChernoffFaces); + + // ## Meta-data + + ChernoffFaces.version = '0.6.2'; + ChernoffFaces.description = + 'Display parametric data in the form of a Chernoff Face.'; + + ChernoffFaces.title = 'ChernoffFaces'; + ChernoffFaces.className = 'chernofffaces'; + + // ## Dependencies + ChernoffFaces.dependencies = { + JSUS: {}, + Table: {}, + Canvas: {}, + SliderControls: {} + }; + + ChernoffFaces.FaceVector = FaceVector; + ChernoffFaces.FacePainter = FacePainter; + ChernoffFaces.width = 100; + ChernoffFaces.height = 100; + ChernoffFaces.onChange = 'CF_CHANGE'; + + /** + * ## ChernoffFaces constructor + * + * Creates a new instance of ChernoffFaces + * + * @see Canvas constructor + */ + function ChernoffFaces(options) { + var that = this; + + // ## Public Properties + + // ### ChernoffFaces.options + // Configuration options + this.options = null; + + // ### ChernoffFaces.table + // The table containing everything + this.table = null; + + // ### ChernoffFaces.sc + // The slider controls of the interface + // Can be set manually via options.controls. + // @see SliderControls + this.sc = null; + + // ### ChernoffFaces.fp + // The object generating the Chernoff faces + // @see FacePainter + this.fp = null; + + // ### ChernoffFaces.canvas + // The HTMLElement canvas where the faces are created + this.canvas = null; + + // ### ChernoffFaces.changes + // History all the changes (if options.trackChanges is TRUE). + // Each time the `draw` method is called, the input parameters + // and a time measurement will be added here. + this.changes = []; + + // ### ChernoffFaces.onChange + // Name of the event to emit to update the canvas (falsy disabled) + this.onChange = null; + + // ### ChernoffFaces.onChangeCb + // Updates the canvas when the onChange event is emitted + // + // @param {object} f Optional. The list of features to change. + // Can be a complete set or subset of all the features. If + // not specified, it will try to get the features from the + // the controls object, and if not found, a random vector + // will be created. + // @param {boolean} updateControls Optional. If TRUE, controls + // are updated with the new values. Default, FALSE. + // + // @see ChernoffFaces.draw + this.onChangeCb = function(f, updateControls) { + if ('undefined' === typeof updateControls) updateControls = false; + if (!f) { + if (that.sc) f = that.sc.getValues(); + else f = FaceVector.random(); + } + that.draw(f, updateControls); + }; + + /** + * ### ChoiceTable.timeFrom + * + * Name of the event to measure time from for each change + * + * Default event is a new step is loaded (user can interact with + * the screen). Set it to FALSE, to have absolute time. + * + * @see node.timer.getTimeSince + */ + this.timeFrom = 'step'; + + // ### ChernoffFaces.features + // The object containing all the features to draw Chernoff faces + this.features = null; + } + + /** + * ### ChernoffFaces.init + * + * Inits the widget + * + * Stores the reference to options, most of the operations are done + * by the `append` method. + * + * @param {object} options Configuration options. Accepted options: + * + * - canvas {object} containing all options for canvas + * + * - width {number} width of the canvas (read only if canvas is not set) + * + * - height {number} height of the canvas (read only if canvas is not set) + * + * - features {FaceVector} vector of face-features. Default: random + * + * - onChange {string|boolean} The name of the event that will trigger + * redrawing the canvas, or null/false to disable event listener + * + * - controls {object|false} the controls (usually a set of sliders) + * offering the user the ability to manipulate the canvas. If equal + * to false no controls will be created. Default: SlidersControls. + * Any custom implementation must provide the following methods: + * + * - getValues: returns the current features vector + * - refresh: redraws the current feature vector + * - init: accepts a configuration object containing a + * features and onChange as specified above. + * + */ + ChernoffFaces.prototype.init = function(options) { + + this.options = options; + + // Face Painter. + if (options.features) { + this.features = new FaceVector(options.features); + } + else if (!this.features) { + this.features = FaceVector.random(); + } + + // Draw features, if facepainter was already created. + if (this.fp) this.fp.draw(this.features); + + // onChange event. + if (options.onChange === false || options.onChange === null) { + if (this.onChange) { + node.off(this.onChange, this.onChangeCb); + this.onChange = null; + } + } + else { + this.onChange = 'undefined' === typeof options.onChange ? + ChernoffFaces.onChange : options.onChange; + node.on(this.onChange, this.onChangeCb); + } + }; + + /** + * ## ChernoffFaces.getCanvas + * + * Returns the reference to current wrapper Canvas object + * + * To get to the HTML Canvas element use `canvas.canvas`. + * + * @return {Canvas} Canvas object + * + * @see Canvas + */ + ChernoffFaces.prototype.getCanvas = function() { + return this.canvas; + }; + + /** + * ## ChernoffFaces.buildHTML + * + * Builds HTML objects, but does not append them + * + * Creates the table, canvas, draw the current image, and + * eventually adds the controls. + * + * If the table was already built, it returns immediately. + */ + ChernoffFaces.prototype.buildHTML = function() { + var controlsOptions, f; + var tblOptions, options; + + if (this.table) return; + + options = this.options; + + // Table. + tblOptions = {}; + if (this.id) tblOptions.id = this.id; + + if ('string' === typeof options.className) { + tblOptions.className = options.className; + } + else if (options.className !== false) { + tblOptions.className = 'cf_table'; + } + + this.table = new Table(tblOptions); + + // Canvas. + if (!this.canvas) this.buildCanvas(); + + // Controls. + if ('undefined' === typeof options.controls || options.controls) { + // Sc options. + f = J.mergeOnKey(FaceVector.defaults, this.features, 'value'); + controlsOptions = { + id: 'cf_controls', + features: f, + onChange: this.onChange, + submit: 'Send' + }; + // Create them. + if ('object' === typeof options.controls) { + this.sc = options.controls; + } + else { + this.sc = node.widgets.get('SliderControls', controlsOptions); + } + } + + // Table. + if (this.sc) { + this.table.addRow( + [{ + content: this.sc, + id: this.id + '_td_controls' + },{ + content: this.canvas, + id: this.id + '_td_cf' + }] + ); + } + else { + this.table.add({ + content: this.canvas, + id: this.id + '_td_cf' + }); + } + + // Create and append table. + this.table.parse(); + }; + + /** + * ## ChernoffFaces.buildCanvas + * + * Builds the canvas object and face painter + * + * All the necessary to draw faces + * + * If the canvas was already built, it simply returns it. + * + * @return {canvas} + */ + ChernoffFaces.prototype.buildCanvas = function() { + var options; + if (!this.canvas) { + options = this.options; + + if (!options.canvas) { + options.canvas = {}; + if ('undefined' !== typeof options.height) { + options.canvas.height = options.height; + } + if ('undefined' !== typeof options.width) { + options.canvas.width = options.width; + } + } + this.canvas = W.getCanvas('ChernoffFaces_canvas', options.canvas); + + // Face Painter. + this.fp = new FacePainter(this.canvas); + this.fp.draw(this.features); + } + }; + + /** + * ## ChernoffFaces.append + * + * Appends the widget + * + * Creates table, canvas, face painter (fp) and controls (sc), according + * to current options. + * + * @see ChernoffFaces.buildHTML + * @see ChernoffFaces.fp + * @see ChernoffFaces.sc + * @see ChernoffFaces.table + * @see Table + * @see Canvas + * @see SliderControls + * @see FacePainter + * @see FaceVector + */ + ChernoffFaces.prototype.append = function() { + if (!this.table) this.buildHTML(); + this.bodyDiv.appendChild(this.table.table); + }; + + /** + * ### ChernoffFaces.draw + * + * Draw a face on canvas and optionally updates the controls + * + * Stores the current value of the drawn image under `.features`. + * + * @param {object} features The features to draw (If not a complete + * set of features, they will be merged with current values) + * @param {boolean} updateControls Optional. If equal to false, + * controls are not updated. Default: true + * + * @see ChernoffFaces.sc + * @see ChernoffFaces.features + */ + ChernoffFaces.prototype.draw = function(features, updateControls) { + var time; + if ('object' !== typeof features) { + throw new TypeError('ChernoffFaces.draw: features must be object.'); + } + if (this.options.trackChanges) { + // Relative time. + if ('string' === typeof this.timeFrom) { + time = node.timer.getTimeSince(this.timeFrom); + } + // Absolute time. + else { + time = Date.now ? Date.now() : new Date().getTime(); + } + this.changes.push({ + time: time, + change: features + }); + } + + // Create a new FaceVector, if features is not one, mixing-in + // new features and old ones. + this.features = (features instanceof FaceVector) ? features : + new FaceVector(features, this.features); + + this.fp.redraw(this.features); + if (this.sc && (updateControls !== false)) { + // Without merging wrong values are passed as attributes. + this.sc.init({ + features: J.mergeOnKey(FaceVector.defaults, features, 'value') + }); + this.sc.refresh(); + } + }; + + ChernoffFaces.prototype.getValues = function(options) { + if (options && options.changes) { + return { + changes: this.changes, + cf: this.features + }; + } + else { + return this.fp.face; + } + }; + + /** + * ### ChernoffFaces.randomize + * + * Draws a random image and updates controls accordingly (if found) + * + * @see ChernoffFaces.sc + */ + ChernoffFaces.prototype.randomize = function() { + var fv; + fv = FaceVector.random(); + this.fp.redraw(fv); + // If controls are visible, updates them. + if (this.sc) { + this.sc.init({ + features: J.mergeOnValue(FaceVector.defaults, fv), + onChange: this.onChange + }); + this.sc.refresh(); + } + return true; + }; + + + /** + * # FacePainter + * + * Draws faces on a Canvas + * + * @param {HTMLCanvas} canvas The canvas + * @param {object} settings Optional. Settings (not used). + */ + function FacePainter(canvas, settings) { + + /** + * ### FacePainter.canvas + * + * The wrapper element for the HTML canvas + * + * @see Canvas + */ + this.canvas = new W.Canvas(canvas); + + /** + * ### FacePainter.scaleX + * + * Scales images along the X-axis of this proportion + */ + this.scaleX = canvas.width / ChernoffFaces.width; + + /** + * ### FacePainter.scaleX + * + * Scales images along the X-axis of this proportion + */ + this.scaleY = canvas.height / ChernoffFaces.heigth; + + /** + * ### FacePainter.face + * + * The last drawn face + */ + this.face = null; + } + + // ## Methods + + /** + * ### FacePainter.draw + * + * Draws a face into the canvas and stores it as reference + * + * @param {object} face Multidimensional vector of features + * @param {number} x Optional. The x-coordinate to center the image. + * Default: the center of the canvas + * @param {number} y Optional. The y-coordinate to center the image. + * Default: the center of the canvas + * + * @see Canvas + * @see Canvas.centerX + * @see Canvas.centerY + */ + FacePainter.prototype.draw = function(face, x, y) { + if (!face) return; + this.face = face; + + this.fit2Canvas(face); + this.canvas.scale(face.scaleX, face.scaleY); + + //console.log('Face Scale ' + face.scaleY + ' ' + face.scaleX ); + + x = x || this.canvas.centerX; + y = y || this.canvas.centerY; + + this.drawHead(face, x, y); + + this.drawEyes(face, x, y); + + this.drawPupils(face, x, y); + + this.drawEyebrow(face, x, y); + + this.drawNose(face, x, y); + + this.drawMouth(face, x, y); + }; + + FacePainter.prototype.redraw = function(face, x, y) { + this.canvas.clear(); + this.draw(face, x, y); + }; + + FacePainter.prototype.scale = function(x, y) { + this.canvas.scale(this.scaleX, this.scaleY); + }; + + // TODO: Improve. It eats a bit of the margins + FacePainter.prototype.fit2Canvas = function(face) { + var ratio; + if (!this.canvas) { + console.log('No canvas found'); + return; + } + + if (this.canvas.width > this.canvas.height) { + ratio = this.canvas.width / face.head_radius * face.head_scale_x; + } + else { + ratio = this.canvas.height / face.head_radius * face.head_scale_y; + } + + face.scaleX = ratio / 2; + face.scaleY = ratio / 2; + }; + + FacePainter.prototype.drawHead = function(face, x, y) { + + var radius = face.head_radius; + + this.canvas.drawOval({ + x: x, + y: y, + radius: radius, + scale_x: face.head_scale_x, + scale_y: face.head_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + }; + + FacePainter.prototype.drawEyes = function(face, x, y) { + + var height = FacePainter.computeFaceOffset(face, face.eye_height, y); + var spacing = face.eye_spacing; + + var radius = face.eye_radius; + //console.log(face); + this.canvas.drawOval({ + x: x - spacing, + y: height, + radius: radius, + scale_x: face.eye_scale_x, + scale_y: face.eye_scale_y, + color: face.color, + lineWidth: face.lineWidth + + }); + //console.log(face); + this.canvas.drawOval({ + x: x + spacing, + y: height, + radius: radius, + scale_x: face.eye_scale_x, + scale_y: face.eye_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + }; + + FacePainter.prototype.drawPupils = function(face, x, y) { + + var radius = face.pupil_radius; + var spacing = face.eye_spacing; + var height = FacePainter.computeFaceOffset(face, face.eye_height, y); + + this.canvas.drawOval({ + x: x - spacing, + y: height, + radius: radius, + scale_x: face.pupil_scale_x, + scale_y: face.pupil_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + + this.canvas.drawOval({ + x: x + spacing, + y: height, + radius: radius, + scale_x: face.pupil_scale_x, + scale_y: face.pupil_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + + }; + + FacePainter.prototype.drawEyebrow = function(face, x, y) { + + var height = FacePainter.computeEyebrowOffset(face,y); + var spacing = face.eyebrow_spacing; + var length = face.eyebrow_length; + var angle = face.eyebrow_angle; + + this.canvas.drawLine({ + x: x - spacing, + y: height, + length: length, + angle: angle, + color: face.color, + lineWidth: face.lineWidth + + + }); + + this.canvas.drawLine({ + x: x + spacing, + y: height, + length: 0-length, + angle: -angle, + color: face.color, + lineWidth: face.lineWidth + }); + + }; + + FacePainter.prototype.drawNose = function(face, x, y) { + + var height = FacePainter.computeFaceOffset(face, face.nose_height, y); + var nastril_r_x = x + face.nose_width / 2; + var nastril_r_y = height + face.nose_length; + var nastril_l_x = nastril_r_x - face.nose_width; + var nastril_l_y = nastril_r_y; + + this.canvas.ctx.lineWidth = face.lineWidth; + this.canvas.ctx.strokeStyle = face.color; + + this.canvas.ctx.save(); + this.canvas.ctx.beginPath(); + this.canvas.ctx.moveTo(x,height); + this.canvas.ctx.lineTo(nastril_r_x,nastril_r_y); + this.canvas.ctx.lineTo(nastril_l_x,nastril_l_y); + //this.canvas.ctx.closePath(); + this.canvas.ctx.stroke(); + this.canvas.ctx.restore(); + + }; + + FacePainter.prototype.drawMouth = function(face, x, y) { + + var height = FacePainter.computeFaceOffset(face, face.mouth_height, y); + var startX = x - face.mouth_width / 2; + var endX = x + face.mouth_width / 2; + + var top_y = height - face.mouth_top_y; + var bottom_y = height + face.mouth_bottom_y; + + // Upper Lip + this.canvas.ctx.moveTo(startX,height); + this.canvas.ctx.quadraticCurveTo(x, top_y, endX, height); + this.canvas.ctx.stroke(); + + //Lower Lip + this.canvas.ctx.moveTo(startX,height); + this.canvas.ctx.quadraticCurveTo(x, bottom_y, endX, height); + this.canvas.ctx.stroke(); + + }; + + + //TODO Scaling ? + FacePainter.computeFaceOffset = function(face, offset, y) { + y = y || 0; + //var pos = y - face.head_radius * face.scaleY + + // face.head_radius * face.scaleY * 2 * offset; + var pos = y - face.head_radius + face.head_radius * 2 * offset; + //console.log('POS: ' + pos); + return pos; + }; + + FacePainter.computeEyebrowOffset = function(face, y) { + y = y || 0; + var eyemindistance = 2; + return FacePainter.computeFaceOffset(face, face.eye_height, y) - + eyemindistance - face.eyebrow_eyedistance; + }; + + + /** + * FaceVector.defaults + * + * Numerical description of all the components of a standard Chernoff Face + */ + FaceVector.defaults = { + // Head + head_radius: { + // id can be specified otherwise is taken head_radius + min: 10, + max: 100, + step: 0.01, + value: 30, + label: 'Face radius' + }, + head_scale_x: { + min: 0.2, + max: 2, + step: 0.01, + value: 0.5, + label: 'Scale head horizontally' + }, + head_scale_y: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale head vertically' + }, + // Eye + eye_height: { + min: 0.1, + max: 0.9, + step: 0.01, + value: 0.4, + label: 'Eye height' + }, + eye_radius: { + min: 2, + max: 30, + step: 0.01, + value: 5, + label: 'Eye radius' + }, + eye_spacing: { + min: 0, + max: 50, + step: 0.01, + value: 10, + label: 'Eye spacing' + }, + eye_scale_x: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale eyes horizontally' + }, + eye_scale_y: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale eyes vertically' + }, + // Pupil + pupil_radius: { + min: 1, + max: 9, + step: 0.01, + value: 1, //this.eye_radius; + label: 'Pupil radius' + }, + pupil_scale_x: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale pupils horizontally' + }, + pupil_scale_y: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale pupils vertically' + }, + // Eyebrow + eyebrow_length: { + min: 1, + max: 30, + step: 0.01, + value: 10, + label: 'Eyebrow length' + }, + eyebrow_eyedistance: { + min: 0.3, + max: 10, + step: 0.01, + value: 3, // From the top of the eye + label: 'Eyebrow from eye' + }, + eyebrow_angle: { + min: -2, + max: 2, + step: 0.01, + value: -0.5, + label: 'Eyebrow angle' + }, + eyebrow_spacing: { + min: 0, + max: 20, + step: 0.01, + value: 5, + label: 'Eyebrow spacing' + }, + // Nose + nose_height: { + min: 0.4, + max: 1, + step: 0.01, + value: 0.4, + label: 'Nose height' + }, + nose_length: { + min: 0.2, + max: 30, + step: 0.01, + value: 15, + label: 'Nose length' + }, + nose_width: { + min: 0, + max: 30, + step: 0.01, + value: 10, + label: 'Nose width' + }, + // Mouth + mouth_height: { + min: 0.2, + max: 2, + step: 0.01, + value: 0.75, + label: 'Mouth height' + }, + mouth_width: { + min: 2, + max: 100, + step: 0.01, + value: 20, + label: 'Mouth width' + }, + mouth_top_y: { + min: -10, + max: 30, + step: 0.01, + value: -2, + label: 'Upper lip' + }, + mouth_bottom_y: { + min: -10, + max: 30, + step: 0.01, + value: 20, + label: 'Lower lip' + }, + + scaleX: { + min: 0, + max: 20, + step: 0.01, + value: 0.2, + label: 'Scale X' + }, + + scaleY: { + min: 0, + max: 20, + step: 0.01, + value: 0.2, + label: 'Scale Y' + }, + + color: { + min: 0, + max: 20, + step: 0.01, + value: 0.2, + label: 'color' + }, + + lineWidth: { + min: 0, + max: 20, + step: 0.01, + value: 0.2, + label: 'lineWidth' + } + + }; + + // Compute range for each feature. + (function(defaults) { + var key; + for (key in defaults) { + if (defaults.hasOwnProperty(key)) { + defaults[key].range = defaults[key].max - defaults[key].min; + } + } + })(FaceVector.defaults); + + // Constructs a random face vector. + FaceVector.random = function() { + console.log('*** FaceVector.random is deprecated. ' + + 'Use new FaceVector() instead.'); + return new FaceVector(); + }; + + function FaceVector(faceVector, defaults) { + var key; + // Make random vector. + if ('undefined' === typeof faceVector) { + for (key in FaceVector.defaults) { + if (FaceVector.defaults.hasOwnProperty(key)) { + if (key === 'color') { + this.color = 'red'; + } + else if (key === 'lineWidth') { + this.lineWidth = 1; + } + else if (key === 'scaleX') { + this.scaleX = 1; + } + else if (key === 'scaleY') { + this.scaleY = 1; + } + else { + this[key] = FaceVector.defaults[key].min + + Math.random() * FaceVector.defaults[key].range; + } + } + } + } + // Mixin values. + else if ('object' === typeof faceVector) { + + this.scaleX = faceVector.scaleX || 1; + this.scaleY = faceVector.scaleY || 1; + + this.color = faceVector.color || 'green'; + this.lineWidth = faceVector.lineWidth || 1; + + defaults = defaults || FaceVector.defaults; + + // Merge on key. + for (key in defaults) { + if (defaults.hasOwnProperty(key)){ + if (faceVector.hasOwnProperty(key)) { + this[key] = faceVector[key]; + } + else { + this[key] = defaults ? defaults[key] : + FaceVector.defaults[key].value; + } + } + } + } + else { + throw new TypeError('FaceVector constructor: faceVector must be ' + + 'object or undefined.'); + } + } + +// //Constructs a random face vector. +// FaceVector.prototype.shuffle = function() { +// for (var key in this) { +// if (this.hasOwnProperty(key)) { +// if (FaceVector.defaults.hasOwnProperty(key)) { +// if (key !== 'color') { +// this[key] = FaceVector.defaults[key].min + +// Math.random() * FaceVector.defaults[key].max; +// } +// } +// } +// } +// }; + +// //Computes the Euclidean distance between two FaceVectors. +// FaceVector.prototype.distance = function(face) { +// return FaceVector.distance(this, face); +// }; +// +// +// FaceVector.distance = function(face1, face2) { +// var sum = 0.0; +// var diff; +// +// for (var key in face1) { +// if (face1.hasOwnProperty(key)) { +// diff = face1[key] - face2[key]; +// sum = sum + diff * diff; +// } +// } +// +// return Math.sqrt(sum); +// }; +// +// FaceVector.prototype.toString = function() { +// var out = 'Face: '; +// for (var key in this) { +// if (this.hasOwnProperty(key)) { +// out += key + ' ' + this[key]; +// } +// } +// return out; +// }; + +})(node); + +/** + * # ChernoffFacesSimple + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Displays multidimensional data in the shape of a Chernoff Face. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + var Table = W.Table; + + node.widgets.register('ChernoffFacesSimple', ChernoffFaces); + + // ## Defaults + + ChernoffFaces.defaults = {}; + ChernoffFaces.defaults.id = 'ChernoffFaces'; + ChernoffFaces.defaults.canvas = {}; + ChernoffFaces.defaults.canvas.width = 100; + ChernoffFaces.defaults.canvas.heigth = 100; + + // ## Meta-data + + ChernoffFaces.version = '0.4'; + ChernoffFaces.description = + 'Display parametric data in the form of a Chernoff Face.'; + + // ## Dependencies + ChernoffFaces.dependencies = { + JSUS: {}, + Table: {}, + Canvas: {}, + 'Controls.Slider': {} + }; + + ChernoffFaces.FaceVector = FaceVector; + ChernoffFaces.FacePainter = FacePainter; + + function ChernoffFaces (options) { + this.options = options; + this.id = options.id; + this.table = new Table({id: 'cf_table'}); + this.root = options.root || document.createElement('div'); + this.root.id = this.id; + + this.sc = node.widgets.get('Controls.Slider'); // Slider Controls + this.fp = null; // Face Painter + this.canvas = null; + this.dims = null; // width and height of the canvas + + this.change = 'CF_CHANGE'; + var that = this; + this.changeFunc = function() { + that.draw(that.sc.getAllValues()); + }; + + this.features = null; + this.controls = null; + } + + ChernoffFaces.prototype.init = function(options) { + this.id = options.id || this.id; + var PREF = this.id + '_'; + + this.features = options.features || this.features || + FaceVector.random(); + + this.controls = ('undefined' !== typeof options.controls) ? + options.controls : true; + + var idCanvas = (options.idCanvas) ? options.idCanvas : PREF + 'canvas'; + + this.dims = { + width: options.width ? + options.width : ChernoffFaces.defaults.canvas.width, + height: options.height ? + options.height : ChernoffFaces.defaults.canvas.heigth + }; + + this.canvas = W.getCanvas(idCanvas, this.dims); + this.fp = new FacePainter(this.canvas); + this.fp.draw(new FaceVector(this.features)); + + var sc_options = { + id: 'cf_controls', + features: + J.mergeOnKey(FaceVector.defaults, this.features, 'value'), + change: this.change, + fieldset: {id: this.id + '_controls_fieldest', + legend: this.controls.legend || 'Controls' + }, + submit: 'Send' + }; + + this.sc = node.widgets.get('Controls.Slider', sc_options); + + // Controls are always there, but may not be visible + if (this.controls) { + this.table.add(this.sc); + } + + // Dealing with the onchange event + if ('undefined' === typeof options.change) { + node.on(this.change, this.changeFunc); + } else { + if (options.change) { + node.on(options.change, this.changeFunc); + } + else { + node.removeListener(this.change, this.changeFunc); + } + this.change = options.change; + } + + + this.table.add(this.canvas); + this.table.parse(); + this.root.appendChild(this.table.table); + }; + + ChernoffFaces.prototype.getRoot = function() { + return this.root; + }; + + ChernoffFaces.prototype.getCanvas = function() { + return this.canvas; + }; + + ChernoffFaces.prototype.append = function(root) { + root.appendChild(this.root); + this.table.parse(); + return this.root; + }; + + ChernoffFaces.prototype.listeners = function() {}; + + ChernoffFaces.prototype.draw = function(features) { + if (!features) return; + var fv = new FaceVector(features); + this.fp.redraw(fv); + // Without merging wrong values are passed as attributes + this.sc.init({ + features: J.mergeOnKey(FaceVector.defaults, features, 'value') + }); + this.sc.refresh(); + }; + + ChernoffFaces.prototype.getAllValues = function() { + //if (this.sc) return this.sc.getAllValues(); + return this.fp.face; + }; + + ChernoffFaces.prototype.randomize = function() { + var fv = FaceVector.random(); + this.fp.redraw(fv); + + var sc_options = { + features: J.mergeOnKey(FaceVector.defaults, fv, 'value'), + change: this.change + }; + this.sc.init(sc_options); + this.sc.refresh(); + + return true; + }; + + // FacePainter + // The class that actually draws the faces on the Canvas + function FacePainter(canvas, settings) { + this.canvas = new W.Canvas(canvas); + this.scaleX = canvas.width / ChernoffFaces.defaults.canvas.width; + this.scaleY = canvas.height / ChernoffFaces.defaults.canvas.heigth; + } + + // Draws a Chernoff face. + FacePainter.prototype.draw = function(face, x, y) { + if (!face) return; + this.face = face; + this.fit2Canvas(face); + this.canvas.scale(face.scaleX, face.scaleY); + + //console.log('Face Scale ' + face.scaleY + ' ' + face.scaleX ); + + x = x || this.canvas.centerX; + y = y || this.canvas.centerY; + + this.drawHead(face, x, y); + + this.drawEyes(face, x, y); + + this.drawPupils(face, x, y); + + this.drawEyebrow(face, x, y); + + this.drawNose(face, x, y); + + this.drawMouth(face, x, y); + + }; + + FacePainter.prototype.redraw = function(face, x, y) { + this.canvas.clear(); + this.draw(face,x,y); + }; + + FacePainter.prototype.scale = function(x, y) { + this.canvas.scale(this.scaleX, this.scaleY); + }; + + // TODO: Improve. It eats a bit of the margins + FacePainter.prototype.fit2Canvas = function(face) { + var ratio; + if (!this.canvas) { + console.log('No canvas found'); + return; + } + + if (this.canvas.width > this.canvas.height) { + ratio = this.canvas.width / face.head_radius * face.head_scale_x; + } + else { + ratio = this.canvas.height / face.head_radius * face.head_scale_y; + } + + face.scaleX = ratio / 2; + face.scaleY = ratio / 2; + }; + + FacePainter.prototype.drawHead = function(face, x, y) { + + var radius = face.head_radius; + + this.canvas.drawOval({ + x: x, + y: y, + radius: radius, + scale_x: face.head_scale_x, + scale_y: face.head_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + }; + + FacePainter.prototype.drawEyes = function(face, x, y) { + + var height = FacePainter.computeFaceOffset(face, face.eye_height, y); + var spacing = face.eye_spacing; + + var radius = face.eye_radius; + //console.log(face); + this.canvas.drawOval({ + x: x - spacing, + y: height, + radius: radius, + scale_x: face.eye_scale_x, + scale_y: face.eye_scale_y, + color: face.color, + lineWidth: face.lineWidth + + }); + //console.log(face); + this.canvas.drawOval({ + x: x + spacing, + y: height, + radius: radius, + scale_x: face.eye_scale_x, + scale_y: face.eye_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + }; + + FacePainter.prototype.drawPupils = function(face, x, y) { + + var radius = face.pupil_radius; + var spacing = face.eye_spacing; + var height = FacePainter.computeFaceOffset(face, face.eye_height, y); + + this.canvas.drawOval({ + x: x - spacing, + y: height, + radius: radius, + scale_x: face.pupil_scale_x, + scale_y: face.pupil_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + + this.canvas.drawOval({ + x: x + spacing, + y: height, + radius: radius, + scale_x: face.pupil_scale_x, + scale_y: face.pupil_scale_y, + color: face.color, + lineWidth: face.lineWidth + }); + + }; + + FacePainter.prototype.drawEyebrow = function(face, x, y) { + + var height = FacePainter.computeEyebrowOffset(face,y); + var spacing = face.eyebrow_spacing; + var length = face.eyebrow_length; + var angle = face.eyebrow_angle; + + this.canvas.drawLine({ + x: x - spacing, + y: height, + length: length, + angle: angle, + color: face.color, + lineWidth: face.lineWidth + + + }); + + this.canvas.drawLine({ + x: x + spacing, + y: height, + length: 0-length, + angle: -angle, + color: face.color, + lineWidth: face.lineWidth + }); + + }; + + FacePainter.prototype.drawNose = function(face, x, y) { + + var height = FacePainter.computeFaceOffset(face, face.nose_height, y); + var nastril_r_x = x + face.nose_width / 2; + var nastril_r_y = height + face.nose_length; + var nastril_l_x = nastril_r_x - face.nose_width; + var nastril_l_y = nastril_r_y; + + this.canvas.ctx.lineWidth = face.lineWidth; + this.canvas.ctx.strokeStyle = face.color; + + this.canvas.ctx.save(); + this.canvas.ctx.beginPath(); + this.canvas.ctx.moveTo(x,height); + this.canvas.ctx.lineTo(nastril_r_x,nastril_r_y); + this.canvas.ctx.lineTo(nastril_l_x,nastril_l_y); + //this.canvas.ctx.closePath(); + this.canvas.ctx.stroke(); + this.canvas.ctx.restore(); + + }; + + FacePainter.prototype.drawMouth = function(face, x, y) { + + var height = FacePainter.computeFaceOffset(face, face.mouth_height, y); + var startX = x - face.mouth_width / 2; + var endX = x + face.mouth_width / 2; + + var top_y = height - face.mouth_top_y; + var bottom_y = height + face.mouth_bottom_y; + + // Upper Lip + this.canvas.ctx.moveTo(startX,height); + this.canvas.ctx.quadraticCurveTo(x, top_y, endX, height); + this.canvas.ctx.stroke(); + + //Lower Lip + this.canvas.ctx.moveTo(startX,height); + this.canvas.ctx.quadraticCurveTo(x, bottom_y, endX, height); + this.canvas.ctx.stroke(); + + }; + + + //TODO Scaling ? + FacePainter.computeFaceOffset = function(face, offset, y) { + y = y || 0; + //var pos = y - face.head_radius * face.scaleY + + // face.head_radius * face.scaleY * 2 * offset; + var pos = y - face.head_radius + face.head_radius * 2 * offset; + //console.log('POS: ' + pos); + return pos; + }; + + FacePainter.computeEyebrowOffset = function(face, y) { + y = y || 0; + var eyemindistance = 2; + return FacePainter.computeFaceOffset(face, face.eye_height, y) - + eyemindistance - face.eyebrow_eyedistance; + }; + + + /*! + * + * A description of a Chernoff Face. + * + * This class packages the 11-dimensional vector of numbers from 0 through + * 1 that completely describe a Chernoff face. + * + */ + + + FaceVector.defaults = { + // Head + head_radius: { + // id can be specified otherwise is taken head_radius + min: 10, + max: 100, + step: 0.01, + value: 30, + label: 'Face radius' + }, + head_scale_x: { + min: 0.2, + max: 2, + step: 0.01, + value: 0.5, + label: 'Scale head horizontally' + }, + head_scale_y: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale head vertically' + }, + // Eye + eye_height: { + min: 0.1, + max: 0.9, + step: 0.01, + value: 0.4, + label: 'Eye height' + }, + eye_radius: { + min: 2, + max: 30, + step: 0.01, + value: 5, + label: 'Eye radius' + }, + eye_spacing: { + min: 0, + max: 50, + step: 0.01, + value: 10, + label: 'Eye spacing' + }, + eye_scale_x: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale eyes horizontally' + }, + eye_scale_y: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale eyes vertically' + }, + // Pupil + pupil_radius: { + min: 1, + max: 9, + step: 0.01, + value: 1, //this.eye_radius; + label: 'Pupil radius' + }, + pupil_scale_x: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale pupils horizontally' + }, + pupil_scale_y: { + min: 0.2, + max: 2, + step: 0.01, + value: 1, + label: 'Scale pupils vertically' + }, + // Eyebrow + eyebrow_length: { + min: 1, + max: 30, + step: 0.01, + value: 10, + label: 'Eyebrow length' + }, + eyebrow_eyedistance: { + min: 0.3, + max: 10, + step: 0.01, + value: 3, // From the top of the eye + label: 'Eyebrow from eye' + }, + eyebrow_angle: { + min: -2, + max: 2, + step: 0.01, + value: -0.5, + label: 'Eyebrow angle' + }, + eyebrow_spacing: { + min: 0, + max: 20, + step: 0.01, + value: 5, + label: 'Eyebrow spacing' + }, + // Nose + nose_height: { + min: 0.4, + max: 1, + step: 0.01, + value: 0.4, + label: 'Nose height' + }, + nose_length: { + min: 0.2, + max: 30, + step: 0.01, + value: 15, + label: 'Nose length' + }, + nose_width: { + min: 0, + max: 30, + step: 0.01, + value: 10, + label: 'Nose width' + }, + // Mouth + mouth_height: { + min: 0.2, + max: 2, + step: 0.01, + value: 0.75, + label: 'Mouth height' + }, + mouth_width: { + min: 2, + max: 100, + step: 0.01, + value: 20, + label: 'Mouth width' + }, + mouth_top_y: { + min: -10, + max: 30, + step: 0.01, + value: -2, + label: 'Upper lip' + }, + mouth_bottom_y: { + min: -10, + max: 30, + step: 0.01, + value: 20, + label: 'Lower lip' + } + }; + + //Constructs a random face vector. + FaceVector.random = function() { + var out = {}; + for (var key in FaceVector.defaults) { + if (FaceVector.defaults.hasOwnProperty(key)) { + if (!J.inArray(key, + ['color', 'lineWidth', 'scaleX', 'scaleY'])) { + + out[key] = FaceVector.defaults[key].min + + Math.random() * FaceVector.defaults[key].max; + } + } + } + + out.scaleX = 1; + out.scaleY = 1; + + out.color = 'green'; + out.lineWidth = 1; + + return new FaceVector(out); + }; + + function FaceVector(faceVector) { + faceVector = faceVector || {}; + + this.scaleX = faceVector.scaleX || 1; + this.scaleY = faceVector.scaleY || 1; + + + this.color = faceVector.color || 'green'; + this.lineWidth = faceVector.lineWidth || 1; + + // Merge on key + for (var key in FaceVector.defaults) { + if (FaceVector.defaults.hasOwnProperty(key)){ + if (faceVector.hasOwnProperty(key)){ + this[key] = faceVector[key]; + } + else { + this[key] = FaceVector.defaults[key].value; + } + } + } + + } + + //Constructs a random face vector. + FaceVector.prototype.shuffle = function() { + for (var key in this) { + if (this.hasOwnProperty(key)) { + if (FaceVector.defaults.hasOwnProperty(key)) { + if (key !== 'color') { + this[key] = FaceVector.defaults[key].min + + Math.random() * FaceVector.defaults[key].max; + } + } + } + } + }; + + //Computes the Euclidean distance between two FaceVectors. + FaceVector.prototype.distance = function(face) { + return FaceVector.distance(this,face); + }; + + + FaceVector.distance = function(face1, face2) { + var sum = 0.0; + var diff; + + for (var key in face1) { + if (face1.hasOwnProperty(key)) { + diff = face1[key] - face2[key]; + sum = sum + diff * diff; + } + } + + return Math.sqrt(sum); + }; + + FaceVector.prototype.toString = function() { + var out = 'Face: '; + for (var key in this) { + if (this.hasOwnProperty(key)) { + out += key + ' ' + this[key]; + } + } + return out; + }; + +})(node); + +/** + * # ChoiceManager + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates and manages a set of selectable choices forms (e.g., ChoiceTable). + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('ChoiceManager', ChoiceManager); + + // ## Meta-data + + ChoiceManager.version = '1.2.0'; + ChoiceManager.description = 'Groups together and manages a set of ' + + 'selectable choices forms (e.g. ChoiceTable).'; + + ChoiceManager.title = false; + ChoiceManager.className = 'choicemanager'; + + // ## Dependencies + + ChoiceManager.dependencies = { + JSUS: {} + }; + + /** + * ## ChoiceManager constructor + * + * Creates a new instance of ChoiceManager + */ + function ChoiceManager() { + var that; + that = this; + + /** + * ### ChoiceManager.dl + * + * The clickable list containing all the forms + */ + this.dl = null; + + /** + * ### ChoiceManager.mainText + * + * The main text introducing the choices + * + * @see ChoiceManager.spanMainText + */ + this.mainText = null; + + /** + * ### ChoiceManager.spanMainText + * + * The span containing the main text + */ + this.spanMainText = null; + + /** + * ### ChoiceManager.forms + * + * The array available forms + * + * @see ChoiceManager.formsById + */ + this.forms = null; + + /** + * ### ChoiceManager.forms + * + * A map form id to form + * + * Note: if a form does not have an id, it will not be added here. + * + * @see ChoiceManager.forms + */ + this.formsById = null; + + /** + * ### ChoiceManager.order + * + * The order of the forms as displayed (if shuffled) + */ + this.order = null; + + /** + * ### ChoiceManager.shuffleForms + * + * TRUE, if forms have been shuffled + */ + this.shuffleForms = null; + + /** + * ### ChoiceManager.group + * + * The name of the group where the list belongs, if any + */ + this.group = null; + + /** + * ### ChoiceManager.groupOrder + * + * The order of the list within the group + */ + this.groupOrder = null; + + /** + * ### ChoiceManager.formsOptions + * + * An object containing options to be added to every form + * + * Options are added only if forms are specified as object literals, + * and can be overriden by each individual form. + */ + this.formsOptions = { + title: false, + frame: false, + storeRef: false + }; + + /** + * ### ChoiceManager.freeText + * + * If truthy, a textarea for free-text comment will be added + * + * If 'string', the text will be added inside the the textarea + */ + this.freeText = null; + + /** + * ### ChoiceManager.textarea + * + * Textarea for free-text comment + */ + this.textarea = null; + } + + // ## ChoiceManager methods + + /** + * ### ChoiceManager.init + * + * Initializes the instance + * + * Available options are: + * + * - className: the className of the list (string, array), or false + * to have none. + * - group: the name of the group (number or string), if any + * - groupOrder: the order of the list in the group, if any + * - mainText: a text to be displayed above the list + * - shuffleForms: if TRUE, forms are shuffled before being added + * to the list + * - freeText: if TRUE, a textarea will be added under the list, + * if 'string', the text will be added inside the the textarea + * - forms: the forms to displayed, formatted as explained in + * `ChoiceManager.setForms` + * - formsOptions: a set of default options to add to every form + * + * @param {object} options Configuration options + * + * @see ChoiceManager.setForms + */ + ChoiceManager.prototype.init = function(options) { + var tmp, that; + that = this; + + // Option shuffleForms, default false. + if ('undefined' === typeof options.shuffleForms) tmp = false; + else tmp = !!options.shuffleForms; + this.shuffleForms = tmp; + + + // Set the group, if any. + if ('string' === typeof options.group || + 'number' === typeof options.group) { + + this.group = options.group; + } + else if ('undefined' !== typeof options.group) { + throw new TypeError('ChoiceManager.init: options.group must ' + + 'be string, number or undefined. Found: ' + + options.group); + } + + // Set the groupOrder, if any. + if ('number' === typeof options.groupOrder) { + + this.groupOrder = options.groupOrder; + } + else if ('undefined' !== typeof options.group) { + throw new TypeError('ChoiceManager.init: options.groupOrder must ' + + 'be number or undefined. Found: ' + + options.groupOrder); + } + + // Set the mainText, if any. + if ('string' === typeof options.mainText) { + this.mainText = options.mainText; + } + else if ('undefined' !== typeof options.mainText) { + throw new TypeError('ChoiceManager.init: options.mainText must ' + + 'be string or undefined. Found: ' + + options.mainText); + } + + // formsOptions. + if ('undefined' !== typeof options.formsOptions) { + if ('object' !== typeof options.formsOptions) { + throw new TypeError('ChoiceManager.init: options.formsOptions' + + ' must be object or undefined. Found: ' + + options.formsOptions); + } + if (options.formsOptions.hasOwnProperty('name')) { + throw new Error('ChoiceManager.init: options.formsOptions ' + + 'cannot contain property name. Found: ' + + options.formsOptions); + } + this.formsOptions = J.mixin(this.formsOptions, + options.formsOptions); + } + + this.freeText = 'string' === typeof options.freeText ? + options.freeText : !!options.freeText; + + + // After all configuration options are evaluated, add forms. + + if ('undefined' !== typeof options.forms) this.setForms(options.forms); + }; + + /** + * ### ChoiceManager.setForms + * + * Sets the available forms + * + * Each form element can be: + * + * - an instantiated widget + * - a "widget-like" element (`append` and `getValues` methods must exist) + * - an object with the `name` of the widget and optional settings, e.g.: + * + * ``` + * { + * name: 'ChoiceTable', + * mainText: 'Did you commit the crime?', + * choices: [ 'Yes', 'No' ], + * } + * ``` + * + * @param {array|function} forms The array of forms or a function + * returning an array of forms + * + * @see ChoiceManager.order + * @see ChoiceManager.isWidget + * @see ChoiceManager.shuffleForms + * @see ChoiceManager.buildForms + * @see ChoiceManager.buildTableAndForms + */ + ChoiceManager.prototype.setForms = function(forms) { + var form, formsById, i, len, parsedForms; + if ('function' === typeof forms) { + parsedForms = forms.call(node.game); + if (!J.isArray(parsedForms)) { + throw new TypeError('ChoiceManager.setForms: forms is a ' + + 'callback, but did not returned an ' + + 'array. Found: ' + parsedForms); + } + } + else if (J.isArray(forms)) { + parsedForms = forms; + } + else { + throw new TypeError('ChoiceManager.setForms: forms must be array ' + + 'or function. Found: ' + forms); + } + + len = parsedForms.length; + if (!len) { + throw new Error('ChoiceManager.setForms: forms is an empty array.'); + } + + // Manual clone forms. + formsById = {}; + forms = new Array(len); + i = -1; + for ( ; ++i < len ; ) { + form = parsedForms[i]; + if (!node.widgets.isWidget(form)) { + if ('string' === typeof form.name) { + // Add defaults. + J.mixout(form, this.formsOptions); + form = node.widgets.get(form.name, form); + } + if (!node.widgets.isWidget(form)) { + throw new Error('ChoiceManager.setForms: one of the ' + + 'forms is not a widget-like element: ' + + form); + } + } + forms[i] = form; + if (form.id) { + if (formsById[form.id]) { + throw new Error('ChoiceManager.setForms: duplicated ' + + 'form id: ' + form.id); + } + formsById[form.id] = forms[i]; + } + } + // Assigned verified forms. + this.forms = forms; + this.formsById = formsById; + + // Save the order in which the choices will be added. + this.order = J.seq(0, len-1); + if (this.shuffleForms) this.order = J.shuffle(this.order); + }; + + /** + * ### ChoiceManager.buildDl + * + * Builds the list of all forms + * + * Must be called after forms have been set already. + * + * @see ChoiceManager.setForms + * @see ChoiceManager.order + */ + ChoiceManager.prototype.buildDl = function() { + var i, len, dl, dt; + var form; + + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + dt = document.createElement('dt'); + dt.className = 'question'; + form = this.forms[this.order[i]]; + node.widgets.append(form, dt); + this.dl.appendChild(dt); + } + }; + + ChoiceManager.prototype.append = function() { + var tmp; + // Id must be unique. + if (W.getElementById(this.id)) { + throw new Error('ChoiceManager.append: id is not ' + + 'unique: ' + this.id); + } + + // MainText. + if (this.mainText) { + this.spanMainText = document.createElement('span'); + this.spanMainText.className = ChoiceManager.className + '-maintext'; + this.spanMainText.innerHTML = this.mainText; + // Append mainText. + this.bodyDiv.appendChild(this.spanMainText); + } + + // Dl. + this.dl = document.createElement('dl'); + this.buildDl(); + // Append Dl. + this.bodyDiv.appendChild(this.dl); + + // Creates a free-text textarea, possibly with placeholder text. + if (this.freeText) { + this.textarea = document.createElement('textarea'); + this.textarea.id = this.id + '_text'; + if ('string' === typeof this.freeText) { + this.textarea.placeholder = this.freeText; + } + tmp = this.className ? this.className + '-freetext' : 'freetext'; + this.textarea.className = tmp; + // Append textarea. + this.bodyDiv.appendChild(this.textarea); + } + }; + + /** + * ### ChoiceManager.listeners + * + * Implements Widget.listeners + * + * Adds two listeners two disable/enable the widget on events: + * INPUT_DISABLE, INPUT_ENABLE + * + * @see Widget.listeners + */ + ChoiceManager.prototype.listeners = function() { + var that = this; + node.on('INPUT_DISABLE', function() { + that.disable(); + }); + node.on('INPUT_ENABLE', function() { + that.enable(); + }); + }; + + /** + * ### ChoiceManager.disable + * + * Disables all forms + */ + ChoiceManager.prototype.disable = function() { + var i, len; + if (this.disabled) return; + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + this.forms[i].disable(); + } + this.disabled = true; + this.emit('disabled'); + }; + + /** + * ### ChoiceManager.enable + * + * Enables all forms + */ + ChoiceManager.prototype.enable = function() { + var i, len; + if (!this.disabled) return; + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + this.forms[i].enable(); + } + this.disabled = false; + this.emit('enabled') + }; + + /** + * ### ChoiceManager.verifyChoice + * + * Compares the current choice/s with the correct one/s + * + * @param {boolean} markAttempt Optional. If TRUE, the value of + * current choice is added to the attempts array. Default + * + * @return {boolean|null} TRUE if current choice is correct, + * FALSE if it is not correct, or NULL if no correct choice + * was set + * + * @see ChoiceManager.attempts + * @see ChoiceManager.setCorrectChoice + */ + ChoiceManager.prototype.verifyChoice = function(markAttempt) { + var i, len, obj, form; + obj = { + id: this.id, + order: this.order, + forms: {} + }; + // Mark attempt by default. + markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + form = this.forms[i]; + obj.forms[form.id] = form.verifyChoice(markAttempt); + if (!obj.form[form.id]) obj.fail = true; + } + return obj; + }; + + /** + * ### ChoiceManager.setCurrentChoice + * + * Marks a choice as current in each form + * + * If the item allows it, multiple choices can be set as current. + * + * @param {number|string} The choice to mark as current + */ + ChoiceManager.prototype.setCurrentChoice = function(choice) { + var i, len; + i = -1, len = this.forms[i].length; + for ( ; ++i < len ; ) { + this.forms[i].setCurrentChoice(choice); + } + }; + + /** + * ### ChoiceManager.unsetCurrentChoice + * + * Deletes the value for currentChoice in each form + * + * If `ChoiceManager.selectMultiple` is set the + * + * @param {number|string} Optional. The choice to delete + * when multiple selections are allowed + */ + ChoiceManager.prototype.unsetCurrentChoice = function(choice) { + var i, len; + i = -1, len = this.forms[i].length; + for ( ; ++i < len ; ) { + this.forms[i].unsetCurrentChoice(choice); + } + }; + + /** + * ### ChoiceManager.highlight + * + * Highlights the choice table + * + * @param {string} The style for the dl's border. + * Default '1px solid red' + * + * @see ChoiceManager.highlighted + */ + ChoiceManager.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError('ChoiceManager.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (!this.dl || this.highlighted === true) return; + this.dl.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted'); + }; + + /** + * ### ChoiceManager.unhighlight + * + * Removes highlight from the choice dl + * + * @see ChoiceManager.highlighted + */ + ChoiceManager.prototype.unhighlight = function() { + if (!this.dl || this.highlighted !== true) return; + this.dl.style.border = ''; + this.highlighted = false; + this.emit('unhighlighted'); + }; + + /** + * ### ChoiceManager.getValues + * + * Returns the values for current selection and other paradata + * + * Paradata that is not set or recorded will be omitted + * + * @param {object} opts Optional. Configures the return value. + * Available optionts: + * + * - markAttempt: If TRUE, getting the value counts as an attempt + * to find the correct answer. Default: TRUE. + * - highlight: If TRUE, forms that do not have a correct value + * will be highlighted. Default: FALSE. + * + * @return {object} Object containing the choice and paradata + * + * @see ChoiceManager.verifyChoice + */ + ChoiceManager.prototype.getValues = function(opts) { + var obj, i, len, form; + obj = { + id: this.id, + order: this.order, + forms: {}, + missValues: [] + }; + opts = opts || {}; + if (opts.markAttempt) obj.isCorrect = true; + opts = opts || {}; + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + form = this.forms[i]; + obj.forms[form.id] = form.getValues(opts); + if (obj.forms[form.id].requiredChoice && + (obj.forms[form.id].choice === null || + (form.selectMultiple && !obj.forms[form.id].choice.length))) { + + obj.missValues.push(form.id); + } + if (opts.markAttempt && obj.forms[form.id].isCorrect === false) { + obj.isCorrect = false; + } + } + if (this.textarea) obj.freetext = this.textarea.value; + return obj; + }; + + /** + * ### ChoiceManager.setValues + * + * Sets values for forms in manager as specified by the options + * + * @param {object} options Optional. Options specifying how to set + * the values. If no parameter is specified, random values will + * be set. + */ + ChoiceManager.prototype.setValues = function(opts) { + var i, len; + if (!this.forms || !this.forms.length) { + throw new Error('ChoiceManager.setValues: no forms found.'); + } + opts = opts || {}; + i = -1, len = this.forms.length; + for ( ; ++i < len ; ) { + this.forms[i].setValues(opts); + } + + // Make a random comment. + if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); + }; + + // ## Helper methods. + +})(node); + +/** + * # ChoiceTable + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a configurable table where each cell is a selectable choice + * + * // TODO: register time for each current choice if selectMultiple is on? + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('ChoiceTable', ChoiceTable); + + // ## Meta-data + + ChoiceTable.version = '1.5.1'; + ChoiceTable.description = 'Creates a configurable table where ' + + 'each cell is a selectable choice.'; + + ChoiceTable.title = 'Make your choice'; + ChoiceTable.className = 'choicetable'; + + ChoiceTable.texts.autoHint = function(w) { + var res; + if (!w.requiredChoice && !w.selectMultiple) return false; + if (!w.selectMultiple) return '*' + res = '(pick '; + res += !w.requiredChoice ? 'up to ' + w.selectMultiple : + 'between ' + w.requiredChoice + ' and ' + w.selectMultiple; + return res + ')'; + }; + + ChoiceTable.separator = '::'; + + // ## Dependencies + + ChoiceTable.dependencies = { + JSUS: {} + }; + + /** + * ## ChoiceTable constructor + * + * Creates a new instance of ChoiceTable + * + * @param {object} options Optional. Configuration options. + * If a `table` option is specified, it sets it as the clickable + * table. All other options are passed to the init method. + */ + function ChoiceTable(options) { + var that; + that = this; + + /** + * ### ChoiceTable.table + * + * The HTML element triggering the listener function when clicked + */ + this.table = null; + + /** + * ### ChoiceTable.choicesSetSize + * + * How many choices can be on the same row/column + */ + this.choicesSetSize = null; + + /** + * ### ChoiceTable.tr + * + * Reference to TR elements of the table + * + * Note: if the orientation is vertical there will be multiple TR + * otherwise just one. + * + * @see createTR + */ + this.trs = []; + + /** + * ### ChoiceTable.listener + * + * The listener function + */ + this.listener = function(e) { + var name, value, td; + var i, len, removed; + + e = e || window.event; + td = e.target || e.srcElement; + + // Not a clickable choice. + if ('undefined' === typeof that.choicesIds[td.id]) return; + + + // Relative time. + if ('string' === typeof that.timeFrom) { + that.timeCurrentChoice = node.timer.getTimeSince(that.timeFrom); + } + // Absolute time. + else { + that.timeCurrentChoice = Date.now ? + Date.now() : new Date().getTime(); + } + + // Id of elements are in the form of name_value or name_item_value. + value = td.id.split(that.separator); + + // Separator not found, not a clickable cell. + if (value.length === 1) return; + + name = value[0]; + value = value[1]; + + // One more click. + that.numberOfClicks++; + + // Click on an already selected choice. + if (that.isChoiceCurrent(value)) { + that.unsetCurrentChoice(value); + J.removeClass(td, 'selected'); + + if (that.selectMultiple) { + // Remove selected TD (need to keep this clean for reset). + i = -1, len = that.selected.length; + for ( ; ++i < len ; ) { + if (that.selected[i].id === td.id) { + that.selected.splice(i, 1); + break; + } + } + } + else { + that.selected = null; + } + removed = true; + } + // Click on a new choice. + else { + + // Have we exhausted available choices? + if ('number' === typeof that.selectMultiple && + that.selected.length === that.selectMultiple) return; + + that.setCurrentChoice(value); + J.addClass(td, 'selected'); + + if (that.selectMultiple) { + that.selected.push(td); + } + else { + // If only 1 selection allowed, remove old selection. + if (that.selected) J.removeClass(that.selected, 'selected'); + that.selected = td; + } + } + + // Remove any warning/errors on click. + if (that.isHighlighted()) that.unhighlight(); + + // Call onclick, if any. + if (that.onclick) that.onclick.call(that, value, td, removed); + }; + + /** + * ### ChoiceTable.mainText + * + * The main text introducing the choices + * + * @see ChoiceTable.spanMainText + */ + this.mainText = null; + + /** + * ### ChoiceTable.hint + * + * An additional text with information about how to select items + * + * If not specified, it may be auto-filled, e.g. '(pick 2)'. + * + * @see Feedback.texts.autoHint + */ + this.hint = null; + + /** + * ### ChoiceTable.spanMainText + * + * The span containing the main text + */ + this.spanMainText = null; + + /** + * ### ChoiceTable.choices + * + * The array available choices + */ + this.choices = null; + + /** + * ### ChoiceTable.choicesValues + * + * Map of choices' values to indexes in the choices array + */ + this.choicesValues = {}; + + /** + * ### ChoiceTable.choicesIds + * + * Map of choices' cells ids to choices + * + * Used to determine what are the clickable choices. + */ + this.choicesIds = {}; + + /** + * ### ChoiceTable.choicesCells + * + * The cells of the table associated with each choice + */ + this.choicesCells = null; + + /** + * ### ChoiceTable.left + * + * A non-clickable first cell of the row/column + * + * It will be placed to the left of the choices if orientation + * is horizontal, or above the choices if orientation is vertical + * + * @see ChoiceTable.orientation + */ + this.left = null; + + /** + * ### ChoiceTable.leftCell + * + * The rendered left cell + * + * @see ChoiceTable.renderSpecial + */ + this.leftCell = null; + + /** + * ### ChoiceTable.right + * + * A non-clickable last cell of the row/column + * + * It will be placed to the right of the choices if orientation + * is horizontal, or below the choices if orientation is vertical + * + * @see ChoiceTable.orientation + */ + this.right = null; + + /** + * ### ChoiceTable.rightCell + * + * The rendered right cell + * + * @see ChoiceTable.renderSpecial + */ + this.rightCell = null; + + /** + * ### ChoiceTable.timeCurrentChoice + * + * Time when the last choice was made + */ + this.timeCurrentChoice = null; + + /** + * ### ChoiceTable.timeFrom + * + * Time is measured from timestamp as saved by node.timer + * + * Default event is a new step is loaded (user can interact with + * the screen). Set it to FALSE, to have absolute time. + * + * @see node.timer.getTimeSince + */ + this.timeFrom = 'step'; + + /** + * ### ChoiceTable.order + * + * The current order of display of choices + * + * May differ from `originalOrder` if shuffled. + * + * @see ChoiceTable.originalOrder + */ + this.order = null; + + /** + * ### ChoiceTable.originalOrder + * + * The initial order of display of choices + * + * TODO: Do we need this? originalOrder is always 0,1,2,3... + * ChoiceManager does not have it. + * + * @see ChoiceTable.order + */ + this.originalOrder = null; + + /** + * ### ChoiceTable.correctChoice + * + * The array of correct choice/s + * + * The field is an array or number|string depending + * on the value of ChoiceTable.selectMultiple + * + * @see ChoiceTable.selectMultiple + */ + this.correctChoice = null; + + /** + * ### ChoiceTable.requiredChoice + * + * The number of required choices. Default 0 + */ + this.requiredChoice = null; + + /** + * ### ChoiceTable.attempts + * + * List of currentChoices at the moment of verifying correct answers + */ + this.attempts = []; + + /** + * ### ChoiceTable.numberOfClicks + * + * Total number of clicks on different choices + */ + this.numberOfClicks = 0; + + /** + * ### ChoiceTable.selected + * + * Currently selected TD elements + * + * @see ChoiceTable.currentChoice + */ + this.selected = null; + + /** + * ### ChoiceTable.currentChoice + * + * Choice/s associated with currently selected cell/s + * + * The field is an array or number|string depending + * on the value of ChoiceTable.selectMultiple + * + * @see ChoiceTable.selectMultiple + * + * @see ChoiceTable.selected + */ + this.currentChoice = null; + + /** + * ### ChoiceTable.selectMultiple + * + * If TRUE, it allows to select multiple cells + */ + this.selectMultiple = null; + + /** + * ### ChoiceTable.shuffleChoices + * + * If TRUE, choices are randomly assigned to cells + * + * @see ChoiceTable.order + */ + this.shuffleChoices = null; + + /** + * ### ChoiceTable.renderer + * + * A callback that renders the content of each cell + * + * The callback must accept three parameters: + * + * - a td HTML element, + * - a choice + * - the index of the choice element within the choices array + * + * and optionally return the _value_ for the choice (otherwise + * the order in the choices array is used as value). + */ + this.renderer = null; + + /** + * ### ChoiceTable.orientation + * + * Orientation of display of choices: vertical ('V') or horizontal ('H') + * + * Default orientation is horizontal. + */ + this.orientation = 'H'; + + /** + * ### ChoiceTable.group + * + * The name of the group where the table belongs, if any + */ + this.group = null; + + /** + * ### ChoiceTable.groupOrder + * + * The order of the choice table within the group + */ + this.groupOrder = null; + + /** + * ### ChoiceTable.freeText + * + * If truthy, a textarea for free-text comment will be added + * + * If 'string', the text will be added inside the textarea + */ + this.freeText = null; + + /** + * ### ChoiceTable.textarea + * + * Textarea for free-text comment + */ + this.textarea = null; + + /** + * ### ChoiceTable.separator + * + * Symbol used to separate tokens in the id attribute of every cell + * + * Default ChoiceTable.separator + * + * @see ChoiceTable.renderChoice + */ + this.separator = ChoiceTable.separator; + } + + // ## ChoiceTable methods + + /** + * ### ChoiceTable.init + * + * Initializes the instance + * + * Available options are: + * + * - left: the content of the left (or top) cell + * - right: the content of the right (or bottom) cell + * - className: the className of the table (string, array), or false + * to have none. + * - orientation: orientation of the table: vertical (v) or horizontal (h) + * - group: the name of the group (number or string), if any + * - groupOrder: the order of the table in the group, if any + * - listener: a function executed at every click. Context is + * `this` instance + * - onclick: a function executed after the listener function. Context is + * `this` instance + * - mainText: a text to be displayed above the table + * - hint: a text with extra info to be displayed after mainText + * - choices: the array of available choices. See + * `ChoiceTable.renderChoice` for info about the format + * - correctChoice: the array|number|string of correct choices. See + * `ChoiceTable.setCorrectChoice` for info about the format + * - selectMultiple: if TRUE multiple cells can be selected + * - shuffleChoices: if TRUE, choices are shuffled before being added + * to the table + * - renderer: a function that will render the choices. See + * ChoiceTable.renderer for info about the format + * - freeText: if TRUE, a textarea will be added under the table, + * if 'string', the text will be added inside the textarea + * - timeFrom: The timestamp as recorded by `node.timer.setTimestamp` + * or FALSE, to measure absolute time for current choice + * + * @param {object} options Configuration options + */ + ChoiceTable.prototype.init = function(options) { + var tmp, that; + that = this; + + if (!this.id) { + throw new TypeError('ChoiceTable.init: options.id is missing'); + } + + // Option orientation, default 'H'. + if ('undefined' === typeof options.orientation) { + tmp = 'H'; + } + else if ('string' !== typeof options.orientation) { + throw new TypeError('ChoiceTable.init: options.orientation must ' + + 'be string, or undefined. Found: ' + + options.orientation); + } + else { + tmp = options.orientation.toLowerCase().trim(); + if (tmp === 'horizontal' || tmp === 'h') { + tmp = 'H'; + } + else if (tmp === 'vertical' || tmp === 'v') { + tmp = 'V'; + } + else { + throw new Error('ChoiceTable.init: options.orientation is ' + + 'invalid: ' + tmp); + } + } + this.orientation = tmp; + + // Option shuffleChoices, default false. + if ('undefined' === typeof options.shuffleChoices) tmp = false; + else tmp = !!options.shuffleChoices; + this.shuffleChoices = tmp; + + // Option selectMultiple, default false. + tmp = options.selectMultiple; + if ('undefined' === typeof tmp) { + tmp = false; + } + else if ('boolean' !== typeof tmp) { + tmp = J.isInt(tmp, 1); + if (!tmp) { + throw new Error('ChoiceTable.init: selectMultiple must be ' + + 'undefined or an integer > 1. Found: ' + tmp); + } + } + this.selectMultiple = tmp; + // Make an array for currentChoice and selected. + if (tmp) { + this.selected = []; + this.currentChoice = []; + } + + // Option requiredChoice, if any. + if ('number' === typeof options.requiredChoice) { + if (!J.isInt(options.requiredChoice, 0)) { + throw new Error('ChoiceTable.init: if number, requiredChoice ' + + 'must a positive integer. Found: ' + + options.requiredChoice); + } + if ('number' === typeof this.selectMultiple && + options.requiredChoice > this.selectMultiple) { + + throw new Error('ChoiceTable.init: requiredChoice cannot be ' + + 'larger than selectMultiple. Found: ' + + options.requiredChoice + ' > ' + + this.selectMultiple); + } + this.requiredChoice = options.requiredChoice; + } + else if ('boolean' === typeof options.requiredChoice) { + this.requiredChoice = options.requiredChoice ? 1 : null; + } + else if ('undefined' !== typeof options.requiredChoice) { + throw new TypeError('ChoiceTable.init: options.requiredChoice ' + + 'be number, boolean or undefined. Found: ' + + options.requiredChoice); + } + + // Set the group, if any. + if ('string' === typeof options.group || + 'number' === typeof options.group) { + + this.group = options.group; + } + else if ('undefined' !== typeof options.group) { + throw new TypeError('ChoiceTable.init: options.group must ' + + 'be string, number or undefined. Found: ' + + options.group); + } + + // Set the groupOrder, if any. + if ('number' === typeof options.groupOrder) { + this.groupOrder = options.groupOrder; + } + else if ('undefined' !== typeof options.groupOrder) { + throw new TypeError('ChoiceTable.init: options.groupOrder must ' + + 'be number or undefined. Found: ' + + options.groupOrder); + } + + // Set the main onclick listener, if any. + if ('function' === typeof options.listener) { + this.listener = function(e) { + options.listener.call(this, e); + }; + } + else if ('undefined' !== typeof options.listener) { + throw new TypeError('ChoiceTable.init: options.listener must ' + + 'be function or undefined. Found: ' + + options.listener); + } + + // Set an additional onclick onclick, if any. + if ('function' === typeof options.onclick) { + this.onclick = options.onclick; + } + else if ('undefined' !== typeof options.onclick) { + throw new TypeError('ChoiceTable.init: options.onclick must ' + + 'be function or undefined. Found: ' + + options.onclick); + } + + // Set the mainText, if any. + if ('string' === typeof options.mainText) { + this.mainText = options.mainText; + } + else if ('undefined' !== typeof options.mainText) { + throw new TypeError('ChoiceTable.init: options.mainText must ' + + 'be string or undefined. Found: ' + + options.mainText); + } + + // Set the hint, if any. + if ('string' === typeof options.hint || false === options.hint) { + this.hint = options.hint; + } + else if ('undefined' !== typeof options.hint) { + throw new TypeError('ChoiceTable.init: options.hint must ' + + 'be a string, false, or undefined. Found: ' + + options.hint); + } + else { + // Returns undefined if there are no constraints. + this.hint = this.getText('autoHint'); + } + + // Set the timeFrom, if any. + if (options.timeFrom === false || + 'string' === typeof options.timeFrom) { + + this.timeFrom = options.timeFrom; + } + else if ('undefined' !== typeof options.timeFrom) { + throw new TypeError('ChoiceTable.init: options.timeFrom must ' + + 'be string, false, or undefined. Found: ' + + options.timeFrom); + } + + // Set the separator, if any. + if ('string' === typeof options.separator) { + this.separator = options.separator; + } + else if ('undefined' !== typeof options.separator) { + throw new TypeError('ChoiceTable.init: options.separator must ' + + 'be string, or undefined. Found: ' + + options.separator); + } + + // Conflict might be generated by id or seperator, + // as specified by user. + if (this.id.indexOf(options.separator) !== -1) { + throw new Error('ChoiceTable.init: options.separator ' + + 'cannot be a sequence of characters ' + + 'included in the table id. Found: ' + + options.separator); + } + + if ('string' === typeof options.left || + 'number' === typeof options.left) { + + this.left = '' + options.left; + } + else if(J.isNode(options.left) || + J.isElement(options.left)) { + + this.left = options.left; + } + else if ('undefined' !== typeof options.left) { + throw new TypeError('ChoiceTable.init: options.left must ' + + 'be string, number, an HTML Element or ' + + 'undefined. Found: ' + options.left); + } + + if ('string' === typeof options.right || + 'number' === typeof options.right) { + + this.right = '' + options.right; + } + else if(J.isNode(options.right) || + J.isElement(options.right)) { + + this.right = options.right; + } + else if ('undefined' !== typeof options.right) { + throw new TypeError('ChoiceTable.init: options.right must ' + + 'be string, number, an HTML Element or ' + + 'undefined. Found: ' + options.right); + } + + + // Set the className, if not use default. + if ('undefined' === typeof options.className) { + this.className = ChoiceTable.className; + } + else if (options.className === false || + 'string' === typeof options.className || + J.isArray(options.className)) { + + this.className = options.className; + } + else { + throw new TypeError('ChoiceTable.init: options.' + + 'className must be string, array, ' + + 'or undefined. Found: ' + options.className); + } + + // Set the renderer, if any. + if ('function' === typeof options.renderer) { + this.renderer = options.renderer; + } + else if ('undefined' !== typeof options.renderer) { + throw new TypeError('ChoiceTable.init: options.renderer must ' + + 'be function or undefined. Found: ' + + options.renderer); + } + + // After all configuration options are evaluated, add choices. + + // Set table. + if ('object' === typeof options.table) { + this.table = options.table; + } + else if ('undefined' !== typeof options.table && + false !== options.table) { + + throw new TypeError('ChoiceTable.init: options.table ' + + 'must be object, false or undefined. ' + + 'Found: ' + options.table); + } + + this.table = options.table; + + this.freeText = 'string' === typeof options.freeText ? + options.freeText : !!options.freeText; + + // Add the choices. + if ('undefined' !== typeof options.choices) { + this.setChoices(options.choices); + } + + // Add the correct choices. + if ('undefined' !== typeof options.correctChoice) { + if (this.requiredChoice) { + throw new Error('ChoiceTable.init: cannot specify both ' + + 'options requiredChoice and correctChoice'); + } + this.setCorrectChoice(options.correctChoice); + } + + // Add the correct choices. + if ('undefined' !== typeof options.choicesSetSize) { + if (!J.isInt(options.choicesSetSize, 0)) { + throw new Error('ChoiceTable.init: choicesSetSize must be ' + + 'undefined or an integer > 0. Found: ' + + options.choicesSetSize); + } + + this.choicesSetSize = options.choicesSetSize; + } + }; + + /** + * ### ChoiceTable.setChoices + * + * Sets the available choices and optionally builds the table + * + * If a table is defined, it will automatically append the choices + * as TD cells. Otherwise, the choices will be built but not appended. + * + * @param {array} choices The array of choices + * + * @see ChoiceTable.table + * @see ChoiceTable.shuffleChoices + * @see ChoiceTable.order + * @see ChoiceTable.buildChoices + * @see ChoiceTable.buildTableAndChoices + */ + ChoiceTable.prototype.setChoices = function(choices) { + var len; + if (!J.isArray(choices)) { + throw new TypeError('ChoiceTable.setChoices: choices ' + + 'must be array.'); + } + if (!choices.length) { + throw new Error('ChoiceTable.setChoices: choices is empty array.'); + } + this.choices = choices; + len = choices.length; + + // Save the order in which the choices will be added. + this.order = J.seq(0, len-1); + if (this.shuffleChoices) { + this.originalOrder = this.order; + this.order = J.shuffle(this.order); + } + + // Build the table and choices at once (faster). + if (this.table) this.buildTableAndChoices(); + // Or just build choices. + else this.buildChoices(); + }; + + + /** + * ### ChoiceTable.buildChoices + * + * Render every choice and stores cell in `choicesCells` array + * + * Left and right cells are also rendered, if specified. + * + * Follows a shuffled order, if set + * + * @see ChoiceTable.order + * @see ChoiceTable.renderChoice + * @see ChoiceTable.renderSpecial + */ + ChoiceTable.prototype.buildChoices = function() { + var i, len; + i = -1, len = this.choices.length; + // Pre-allocate the choicesCells array. + this.choicesCells = new Array(len); + for ( ; ++i < len ; ) { + this.renderChoice(this.choices[this.order[i]], i); + } + if (this.left) this.renderSpecial('left', this.left); + if (this.right) this.renderSpecial('right', this.right); + }; + + /** + * ### ChoiceTable.buildTable + * + * Builds the table of clickable choices and enables it + * + * Must be called after choices have been set already. + * + * @see ChoiceTable.setChoices + * @see ChoiceTable.order + * @see ChoiceTable.renderChoice + * @see ChoiceTable.orientation + * @see ChoiceTable.choicesSetSize + */ + ChoiceTable.prototype.buildTable = (function() { + + function makeSet(i, len, H, doSets) { + var tr, counter; + counter = 0; + // Start adding tr/s and tds based on the orientation. + if (H) { + tr = createTR(this, 'main'); + // Add horizontal choices title. + if (this.leftCell) tr.appendChild(this.leftCell); + } + // Main loop. + for ( ; ++i < len ; ) { + if (!H) { + tr = createTR(this, 'left'); + // Add vertical choices title. + if (i === 0 && this.leftCell) { + tr.appendChild(this.leftCell); + tr = createTR(this, i); + } + } + // Clickable cell. + tr.appendChild(this.choicesCells[i]); + // Stop if we reached set size (still need to add the right). + if (doSets && ++counter >= this.choicesSetSize) break; + } + if (this.rightCell) { + if (!H) tr = createTR(this, 'right'); + tr.appendChild(this.rightCell); + } + + // Start a new set, if necessary. + if (i !== len) makeSet.call(this, i, len, H, doSets); + } + + return function() { + var i, len, H, doSets; + + if (!this.choicesCells) { + throw new Error('ChoiceTable.buildTable: choices not set, ' + + 'cannot build table. Id: ' + this.id); + } + + H = this.orientation === 'H'; + len = this.choicesCells.length; + doSets = 'number' === typeof this.choicesSetSize; + + // Recursively makes sets + makeSet.call(this, -1, len, H, doSets); + + // Enable onclick listener. + this.enable(); + }; + })(); + + /** + * ### ChoiceTable.buildTableAndChoices + * + * Builds the table of clickable choices + * + * @see ChoiceTable.choices + * @see ChoiceTable.order + * @see ChoiceTable.renderChoice + * @see ChoiceTable.orientation + */ + ChoiceTable.prototype.buildTableAndChoices = function() { + var i, len, tr, td, H; + + len = this.choices.length; + // Pre-allocate the choicesCells array. + this.choicesCells = new Array(len); + + // Start adding tr/s and tds based on the orientation. + i = -1, H = this.orientation === 'H'; + + if (H) { + tr = createTR(this, 'main'); + // Add horizontal choices left. + if (this.left) { + td = this.renderSpecial('left', this.left); + tr.appendChild(td); + } + } + // Main loop. + for ( ; ++i < len ; ) { + if (!H) { + tr = createTR(this, 'left'); + // Add vertical choices left. + if (i === 0 && this.left) { + td = this.renderSpecial('left', this.left); + tr.appendChild(td); + tr = createTR(this, i); + } + } + // Clickable cell. + td = this.renderChoice(this.choices[this.order[i]], i); + tr.appendChild(td); + } + if (this.right) { + if (!H) tr = createTR(this, 'right'); + td = this.renderSpecial('right', this.right); + tr.appendChild(td); + } + + // Enable onclick listener. + this.enable(); + }; + + /** + * ### ChoiceTable.renderSpecial + * + * Renders a non-choice element into a cell of the table (e.g. left/right) + * + * @param {string} type The type of special cell ('left' or 'right'). + * @param {mixed} special The special element. It must be string or number, + * or array where the first element is the 'value' (incorporated in the + * `id` field) and the second the text to display as choice. + * + * @return {HTMLElement} td The newly created cell of the table + * + * @see ChoiceTable.left + * @see ChoiceTable.right + */ + ChoiceTable.prototype.renderSpecial = function(type, special) { + var td, className; + td = document.createElement('td'); + if ('string' === typeof special) td.innerHTML = special; + // HTML element (checked before). + else td.appendChild(special); + if (type === 'left') { + className = this.className ? this.className + '-left' : 'left'; + this.leftCell = td; + } + else if (type === 'right') { + className = this.className ? this.className + '-right' : 'right'; + this.rightCell = td; + } + else { + throw new Error('ChoiceTable.renderSpecial: unknown type: ' + type); + } + td.className = className; + td.id = this.id + this.separator + 'special-cell-' + type; + return td; + }; + + /** + * ### ChoiceTable.renderChoice + * + * Transforms a choice element into a cell of the table + * + * A reference to the cell is saved in `choicesCells`. + * + * @param {mixed} choice The choice element. It must be string or number, + * or array where the first element is the 'value' (incorporated in the + * `id` field) and the second the text to display as choice. If a + * renderer function is defined there are no restriction on the + * format of choice + * @param {number} idx The position of the choice within the choice array + * + * @return {HTMLElement} td The newly created cell of the table + * + * @see ChoiceTable.renderer + * @see ChoiceTable.separator + * @see ChoiceTable.choicesCells + */ + ChoiceTable.prototype.renderChoice = function(choice, idx) { + var td, value; + td = document.createElement('td'); + + // Use custom renderer. + if (this.renderer) { + value = this.renderer(td, choice, idx); + if ('undefined' === typeof value) value = idx; + } + // Or use standard format. + else { + if (J.isArray(choice)) { + value = choice[0]; + choice = choice[1]; + } + else { + value = this.shuffleChoices ? this.order[idx] : idx; + } + + if ('string' === typeof choice || 'number' === typeof choice) { + td.innerHTML = choice; + } + else if (J.isElement(choice) || J.isNode(choice)) { + td.appendChild(choice); + } + else { + throw new Error('ChoiceTable.renderChoice: invalid choice: ' + + choice); + } + } + + // Map a value to the index. + if ('undefined' !== typeof this.choicesValues[value]) { + throw new Error('ChoiceTable.renderChoice: value already ' + + 'in use: ' + value); + } + + // Add the id if not added already by the renderer function. + if (!td.id || td.id === '') { + td.id = this.id + this.separator + value; + } + + // All fine, updates global variables. + this.choicesValues[value] = idx; + this.choicesCells[idx] = td; + this.choicesIds[td.id] = td; + + return td; + }; + + /** + * ### ChoiceTable.setCorrectChoice + * + * Set the correct choice/s + * + * Correct choice/s are always stored as 'strings', or not number + * because then they are compared against the valued saved in + * the `id` field of the cell + * + * @param {number|string|array} If `selectMultiple` is set, param must + * be an array, otherwise a string or a number. Each correct choice + * must have been already defined as choice (value) + * + * @see ChoiceTable.setChoices + * @see checkCorrectChoiceParam + */ + ChoiceTable.prototype.setCorrectChoice = function(choice) { + var i, len; + if (!this.selectMultiple) { + choice = checkCorrectChoiceParam(this, choice); + } + else { + if (J.isArray(choice) && choice.length) { + i = -1, len = choice.length; + for ( ; ++i < len ; ) { + choice[i] = checkCorrectChoiceParam(this, choice[i]); + } + } + else { + throw new TypeError('ChoiceTable.setCorrectChoice: choice ' + + 'must be non-empty array. Found: ' + + choice); + } + } + this.correctChoice = choice; + }; + + /** + * ### ChoiceTable.append + * + * Implements Widget.append + * + * Checks that id is unique. + * + * Appends (all optional): + * + * - mainText: a question or statement introducing the choices + * - table: the table containing the choices + * - freeText: a textarea for comments + * + * @see Widget.append + */ + ChoiceTable.prototype.append = function() { + var tmp; + // Id must be unique. + if (W.getElementById(this.id)) { + throw new Error('ChoiceTable.append: id is not ' + + 'unique: ' + this.id); + } + + // MainText. + if (this.mainText) { + this.spanMainText = W.append('span', this.bodyDiv, { + className: 'choicetable-maintext', + innerHTML: this.mainText + }); + } + // Hint. + if (this.hint) { + W.append('span', this.spanMainText || this.bodyDiv, { + className: 'choicetable-hint', + innerHTML: this.hint + }); + } + + // Create/set table. + if (this.table !== false) { + // Create table, if it was not passed as object before. + if ('undefined' === typeof this.table) { + this.table = document.createElement('table'); + this.buildTable(); + } + // Set table id. + this.table.id = this.id; + if (this.className) J.addClass(this.table, this.className); + else this.table.className = ''; + // Append table. + this.bodyDiv.appendChild(this.table); + } + + // Creates a free-text textarea, possibly with placeholder text. + if (this.freeText) { + this.textarea = document.createElement('textarea'); + this.textarea.id = this.id + '_text'; + if ('string' === typeof this.freeText) { + this.textarea.placeholder = this.freeText; + } + tmp = this.className ? this.className + '-freetext' : 'freetext'; + this.textarea.className = tmp; + // Append textarea. + this.bodyDiv.appendChild(this.textarea); + } + }; + + /** + * ### ChoiceTable.listeners + * + * Implements Widget.listeners + * + * Adds two listeners two disable/enable the widget on events: + * INPUT_DISABLE, INPUT_ENABLE + * + * @see Widget.listeners + */ + ChoiceTable.prototype.listeners = function() { + var that = this; + node.on('INPUT_DISABLE', function() { + that.disable(); + }); + node.on('INPUT_ENABLE', function() { + that.enable(); + }); + }; + + /** + * ### ChoiceTable.disable + * + * Disables clicking on the table and removes CSS 'clicklable' class + */ + ChoiceTable.prototype.disable = function() { + if (this.disabled === true) return; + this.disabled = true; + if (this.table) { + J.removeClass(this.table, 'clickable'); + this.table.removeEventListener('click', this.listener); + } + this.emit('disabled'); + }; + + /** + * ### ChoiceTable.enable + * + * Enables clicking on the table and adds CSS 'clicklable' class + * + * @return {function} cb The event listener function + */ + ChoiceTable.prototype.enable = function() { + if (this.disabled === false) return; + if (!this.table) { + throw new Error('ChoiceTable.enable: table not defined'); + } + this.disabled = false; + J.addClass(this.table, 'clickable'); + this.table.addEventListener('click', this.listener); + this.emit('enabled'); + }; + + /** + * ### ChoiceTable.verifyChoice + * + * Compares the current choice/s with the correct one/s + * + * Depending on current settings, there are two modes of verifying + * choices: + * + * - requiredChoice: there must be at least N choices selected + * - correctChoice: the choices are compared against correct ones. + * + * @param {boolean} markAttempt Optional. If TRUE, the value of + * current choice is added to the attempts array. Default: TRUE + * + * @return {boolean|null} TRUE if current choice is correct, + * FALSE if it is not correct, or NULL if no correct choice + * was set + * + * @see ChoiceTable.attempts + * @see ChoiceTable.setCorrectChoice + */ + ChoiceTable.prototype.verifyChoice = function(markAttempt) { + var i, len, j, lenJ, c, clone, found; + var correctChoice; + + // Check the number of choices. + if (this.requiredChoice !== null) { + if (!this.selectMultiple) return this.currentChoice !== null; + else return this.currentChoice.length >= this.requiredChoice; + } + + // If no correct choice is set return null. + if (!this.correctChoice) return null; + // Mark attempt by default. + markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; + if (markAttempt) this.attempts.push(this.currentChoice); + if (!this.selectMultiple) { + return this.currentChoice === this.correctChoice; + } + else { + // Make it an array (can be a string). + correctChoice = J.isArray(this.correctChoice) ? + this.correctChoice : [this.correctChoice]; + + len = correctChoice.length; + lenJ = this.currentChoice.length; + // Quick check. + if (len !== lenJ) return false; + // Check every item. + i = -1; + clone = this.currentChoice.slice(0); + for ( ; ++i < len ; ) { + found = false; + c = correctChoices[i]; + j = -1; + for ( ; ++j < lenJ ; ) { + if (clone[j] === c) { + found = true; + break; + } + } + if (!found) return false; + } + return true; + } + }; + + /** + * ### ChoiceTable.setCurrentChoice + * + * Marks a choice as current + * + * If `ChoiceTable.selectMultiple` is set multiple choices can be current. + * + * @param {number|string} The choice to mark as current + * + * @see ChoiceTable.currentChoice + * @see ChoiceTable.selectMultiple + */ + ChoiceTable.prototype.setCurrentChoice = function(choice) { + if (!this.selectMultiple) this.currentChoice = choice; + else this.currentChoice.push(choice); + }; + + /** + * ### ChoiceTable.unsetCurrentChoice + * + * Deletes the value of currentChoice + * + * If `ChoiceTable.selectMultiple` is activated, then it is + * possible to select the choice to unset. + * + * @param {number|string} Optional. The choice to delete from + * currentChoice when multiple selections are allowed + * + * @see ChoiceTable.currentChoice + * @see ChoiceTable.selectMultiple + */ + ChoiceTable.prototype.unsetCurrentChoice = function(choice) { + var i, len; + if (!this.selectMultiple || 'undefined' === typeof choice) { + this.currentChoice = null; + } + else { + if ('string' !== typeof choice && 'number' !== typeof choice) { + throw new TypeError('ChoiceTable.unsetCurrentChoice: choice ' + + 'must be string, number or ' + + 'undefined. Found: ' + choice); + } + i = -1, len = this.currentChoice.length; + for ( ; ++i < len ; ) { + if (this.currentChoice[i] === choice) { + this.currentChoice.splice(i,1); + break; + } + } + } + }; + + /** + * ### ChoiceTable.isChoiceCurrent + * + * Returns TRUE if a choice is currently selected + * + * @param {number|string} The choice to check + * + * @return {boolean} TRUE, if the choice is currently selected + */ + ChoiceTable.prototype.isChoiceCurrent = function(choice) { + var i, len; + if ('number' === typeof choice) { + choice = '' + choice; + } + else if ('string' !== typeof choice) { + throw new TypeError('ChoiceTable.isChoiceCurrent: choice ' + + 'must be string or number. Found: ' + choice); + } + if (!this.selectMultiple) { + return this.currentChoice === choice; + } + else { + i = -1, len = this.currentChoice.length; + for ( ; ++i < len ; ) { + if (this.currentChoice[i] === choice) { + return true; + } + } + } + return false; + }; + + /** + * ### ChoiceTable.highlight + * + * Highlights the choice table + * + * @param {string} The style for the table's border. + * Default '3px solid red' + * + * @see ChoiceTable.highlighted + */ + ChoiceTable.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError('ChoiceTable.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (!this.table || this.highlighted) return; + this.table.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted', border); + }; + + /** + * ### ChoiceTable.unhighlight + * + * Removes highlight from the choice table + * + * @see ChoiceTable.highlighted + */ + ChoiceTable.prototype.unhighlight = function() { + if (!this.table || this.highlighted !== true) return; + this.table.style.border = ''; + this.highlighted = false; + this.emit('unhighlighted'); + }; + + /** + * ### ChoiceTable.getValues + * + * Returns the values for current selection and other paradata + * + * Paradata that is not set or recorded will be omitted + * + * @param {object} opts Optional. Configures the return value. + * Available optionts: + * + * - markAttempt: If TRUE, getting the value counts as an attempt + * to find the correct answer. Default: TRUE. + * - highlight: If TRUE, if current value is not the correct + * value, widget will be highlighted. Default: FALSE. + * - reset: If TRUTHY and a correct choice is selected (or not + * specified), then it resets the state of the widgets before + * returning it. Default: FALSE. + * + * @return {object} Object containing the choice and paradata + * + * @see ChoiceTable.verifyChoice + * @see ChoiceTable.reset + */ + ChoiceTable.prototype.getValues = function(opts) { + var obj, resetOpts; + opts = opts || {}; + obj = { + id: this.id, + choice: opts.reset ? + this.currentChoice: J.clone(this.currentChoice), + time: this.timeCurrentChoice, + nClicks: this.numberOfClicks + }; + if (opts.processChoice) { + obj.choice = opts.processChoice.call(this, obj.choice); + } + if (this.shuffleChoices) { + obj.originalOrder = this.originalOrder; + obj.order = this.order; + } + if (this.group === 0 || this.group) { + obj.group = this.group; + } + if (this.groupOrder === 0 || this.groupOrder) { + obj.groupOrder = this.groupOrder; + } + if (null !== this.correctChoice || null !== this.requiredChoice) { + obj.isCorrect = this.verifyChoice(opts.markAttempt); + obj.attempts = this.attempts; + if (!obj.isCorrect && opts.highlight) this.highlight(); + } + if (this.textarea) obj.freetext = this.textarea.value; + if (obj.isCorrect !== false && opts.reset) { + resetOpts = 'object' !== typeof opts.reset ? {} : opts.reset; + this.reset(resetOpts); + } + return obj; + }; + + /** + * ### ChoiceTable.setValues + * + * Sets values in the choice table as specified by the options + * + * @param {object} options Optional. Options specifying how to set + * the values. If no parameter is specified, random values will + * be set. + * + * @experimental + */ + ChoiceTable.prototype.setValues = function(options) { + var choice, correctChoice, cell; + var i, len, j, lenJ; + + if (!this.choices || !this.choices.length) { + throw new Error('ChoiceTable.setValues: no choices found.'); + } + options = options || {}; + + // TODO: allow it to set it visually or just in the background. + // Use options.visual. + + // TODO: allow it to set random or fixed values, or correct values + + if (!this.choicesCells || !this.choicesCells.length) { + throw new Error('Choicetable.setValues: table was not ' + + 'built yet.'); + } + + if (options.correct) { + // Value this.correctChoice can be string or array. + if (!this.correctChoice || !this.correctChoice.length) { + throw new Error('Choicetable.setValues: "correct" is set, ' + + 'but no correct choice is found.'); + } + // Make it an array (can be a string). + correctChoice = J.isArray(this.correctChoice) ? + this.correctChoice : [this.correctChoice]; + + i = -1, len = correctChoice.length; + for ( ; ++i < len ; ) { + choice = parseInt(correctChoice[i], 10); + if (this.shuffleChoices) { + j = -1, lenJ = this.order.length; + for ( ; ++j < lenJ ; ) { + if (this.order[j] === choice) { + choice = j; + break; + } + } + } + + this.choicesCells[choice].click(); + } + return; + } + + // How many random choices? + if (!this.selectMultiple) len = 1; + else len = J.randomInt(0, this.choicesCells.length); + + i = -1; + for ( ; ++i < len ; ) { + choice = J.randomInt(0, this.choicesCells.length)-1; + // Do not click it again if it is already selected. + if (!this.isChoiceCurrent(choice)) { + this.choicesCells[choice].click(); + } + } + + // Make a random comment. + if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); + }; + + /** + * ### ChoiceTable.reset + * + * Resets current selection and collected paradata + * + * @param {object} options Optional. Available options: + * - shuffleChoices: If TRUE, choices are shuffled. Default: FALSE + */ + ChoiceTable.prototype.reset = function(options) { + var i, len; + + options = options || {}; + + this.attempts = []; + this.numberOfClicks = 0; + this.timeCurrentChoice = null; + + if (this.selectMultiple) { + i = -1, len = this.selected.length; + for ( ; ++i < len ; ) { + J.removeClass(this.selected[i], 'selected'); + } + this.selected = []; + this.currentChoice = []; + + } + else { + if (this.selected) { + J.removeClass(this.selected, 'selected'); + this.selected = null; + this.currentChoice = null; + } + } + + if (this.textArea) this.textArea.value = ''; + if (this.isHighlighted()) this.unhighlight(); + + if (options.shuffleChoices) this.shuffle(); + }; + + /** + * ### ChoiceTable.shuffle + * + * Shuffles the order of the choices + */ + ChoiceTable.prototype.shuffle = function() { + var order, H; + var i, len, cell, choice; + var choicesValues, choicesCells; + var parentTR; + + H = this.orientation === 'H'; + order = J.shuffle(this.order); + i = -1, len = order.length; + choicesValues = {}; + choicesCells = new Array(len); + + for ( ; ++i < len ; ) { + choice = order[i]; + cell = this.choicesCells[this.choicesValues[choice]]; + choicesCells[i] = cell; + choicesValues[choice] = i; + if (H) { + this.trs[0].appendChild(cell); + } + else { + parentTR = cell.parentElement || cell.parentNode; + this.table.appendChild(parentTR); + } + } + if (this.rightCell) { + if (H) { + this.trs[0].appendChild(this.rightCell); + } + else { + parentTR = this.rightCell.parentElement || + this.rightCell.parentNode; + this.table.appendChild(parentTR); + } + } + + this.order = order; + this.choicesCells = choicesCells; + this.choicesValues = choicesValues; + }; + + // ## Helper methods. + + /** + * ### checkCorrectChoiceParam + * + * Checks the input parameters of method ChoiceTable.setCorrectChoice + * + * The function transforms numbers into string, because then the checking + * is done with strings (they are serialized in the id property of tds). + * + * If `ChoiceTable.selectMultiple` is set, the function checks each + * value of the array separately. + * + * @param {ChoiceTable} that This instance + * @param {string|number} An already existing value of a choice + * + * @return {string} The checked choice + */ + function checkCorrectChoiceParam(that, choice) { + if ('number' === typeof choice) choice = '' + choice; + if ('string' !== typeof choice) { + throw new TypeError('ChoiceTable.setCorrectChoice: each choice ' + + 'must be number or string. Found: ' + choice); + } + if ('undefined' === typeof that.choicesValues[choice]) { + + throw new TypeError('ChoiceTable.setCorrectChoice: choice ' + + 'not found: ' + choice); + } + return choice; + } + + /** + * ### createTR + * + * Creates and append a new TR element + * + * Adds the the `id` attribute formatted as: + * 'tr' + separator + widget_id + * + * @param {ChoiceTable} that This instance + * + * @return {HTMLElement} Thew newly created TR element + * + * @see ChoiceTable.tr + */ + function createTR(that, trid) { + var tr; + tr = document.createElement('tr'); + tr.id = 'tr' + that.separator + that.id; + that.table.appendChild(tr); + // Store reference. + that.trs.push(tr); + return tr; + } + +})(node); + +/** + * # ChoiceTableGroup + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a table that groups together several choice tables widgets + * + * @see ChoiceTable + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('ChoiceTableGroup', ChoiceTableGroup); + + // ## Meta-data + + ChoiceTableGroup.version = '1.5.0'; + ChoiceTableGroup.description = 'Groups together and manages sets of ' + + 'ChoiceTable widgets.'; + + ChoiceTableGroup.title = 'Make your choice'; + ChoiceTableGroup.className = 'choicetable'; + + ChoiceTableGroup.separator = '::'; + + ChoiceTableGroup.texts.autoHint = function(w) { + if (w.requiredChoice) return '*'; + else return false; + }; + + // ## Dependencies + + ChoiceTableGroup.dependencies = { + JSUS: {} + }; + + /** + * ## ChoiceTableGroup constructor + * + * Creates a new instance of ChoiceTableGroup + * + * @param {object} options Optional. Configuration options. + * If a `table` option is specified, it sets it as the clickable + * table. All other options are passed to the init method. + */ + function ChoiceTableGroup(options) { + var that; + that = this; + + /** + * ### ChoiceTableGroup.dl + * + * The clickable table containing all the cells + */ + this.table = null; + + /** + * ### ChoiceTableGroup.trs + * + * Collection of all trs created + * + * Useful when shuffling items/choices + * + * @see ChoiceTableGroup.shuffle + */ + this.trs = []; + + /** + * ## ChoiceTableGroup.listener + * + * The listener function + * + * @see GameChoice.enable + * @see GameChoice.disable + */ + this.listener = function(e) { + var name, value, item, td, oldSelected; + var time; + + // Relative time. + if ('string' === typeof that.timeFrom) { + time = node.timer.getTimeSince(that.timeFrom); + } + // Absolute time. + else { + time = Date.now ? Date.now() : new Date().getTime(); + } + + e = e || window.event; + td = e.target || e.srcElement; + + // Not a clickable choice. + if (!td.id || td.id === '') return; + + // Not a clickable choice. + if (!that.choicesById[td.id]) return; + + // Id of elements are in the form of name_value or name_item_value. + value = td.id.split(that.separator); + + // Separator not found, not a clickable cell. + if (value.length === 1) return; + + name = value[0]; + value = value[1]; + + item = that.itemsById[name]; + + // Not a clickable cell. + if (!item) return; + + item.timeCurrentChoice = time; + + // One more click. + item.numberOfClicks++; + + // If only 1 selection allowed, remove selection from oldSelected. + if (!item.selectMultiple) { + oldSelected = item.selected; + if (oldSelected) J.removeClass(oldSelected, 'selected'); + + if (item.isChoiceCurrent(value)) { + item.unsetCurrentChoice(value); + } + else { + item.currentChoice = value; + J.addClass(td, 'selected'); + item.selected = td; + } + } + + // Remove any warning/error from form on click. + if (that.isHighlighted()) that.unhighlight(); + }; + + /** + * ### ChoiceTableGroup.mainText + * + * The main text introducing the choices + * + * @see ChoiceTableGroup.spanMainText + */ + this.mainText = null; + + /** + * ### ChoiceTableGroup.spanMainText + * + * The span containing the main text + */ + this.spanMainText = null; + + /** + * ### ChoiceTableGroup.hint + * + * An additional text with information about how to select items + * + * If not specified, it may be auto-filled, e.g. '(pick 2)'. + * + * @see Feedback.texts.autoHint + */ + this.hint = null; + + /** + * ### ChoiceTableGroup.items + * + * The array of available items + */ + this.items = null; + + /** + * ### ChoiceTableGroup.itemsById + * + * Map of items ids to items + */ + this.itemsById = {}; + + /** + * ### ChoiceTableGroup.itemsMap + * + * Maps items ids to the position in the items array + */ + this.itemsMap = {}; + + /** + * ### ChoiceTableGroup.choices + * + * Array of default choices (if passed as global parameter) + */ + this.choices = null; + + /** + * ### ChoiceTableGroup.choicesById + * + * Map of items choices ids to corresponding cell + * + * Useful to detect clickable cells. + */ + this.choicesById = {}; + + /** + * ### ChoiceTableGroup.itemsSettings + * + * The array of settings for each item + */ + this.itemsSettings = null; + + /** + * ### ChoiceTableGroup.order + * + * The current order of display of choices + * + * May differ from `originalOrder` if shuffled. + * + * @see ChoiceTableGroup.originalOrder + */ + this.order = null; + + /** + * ### ChoiceTableGroup.originalOrder + * + * The initial order of display of choices + * + * @see ChoiceTable.order + */ + this.originalOrder = null; + + /** + * ### ChoiceTableGroup.shuffleItems + * + * If TRUE, items are inserted in random order + * + * @see ChoiceTableGroup.order + */ + this.shuffleItems = null; + + /** + * ### ChoiceTableGroup.requiredChoice + * + * The number of required choices. + */ + this.requiredChoice = null; + + /** + * ### ChoiceTableGroup.orientation + * + * Orientation of display of items: vertical ('V') or horizontal ('H') + * + * Default orientation is horizontal. + */ + this.orientation = 'H'; + + /** + * ### ChoiceTableGroup.group + * + * The name of the group where the table belongs, if any + */ + this.group = null; + + /** + * ### ChoiceTableGroup.groupOrder + * + * The order of the choice table within the group + */ + this.groupOrder = null; + + /** + * ### ChoiceTableGroup.freeText + * + * If truthy, a textarea for free-text comment will be added + * + * If 'string', the text will be added inside the the textarea + */ + this.freeText = null; + + /** + * ### ChoiceTableGroup.textarea + * + * Textarea for free-text comment + */ + this.textarea = null; + + // Options passed to each individual item. + + /** + * ### ChoiceTableGroup.timeFrom + * + * Time is measured from timestamp as saved by node.timer + * + * Default event is a new step is loaded (user can interact with + * the screen). Set it to FALSE, to have absolute time. + * + * This option is passed to each individual item. + * + * @see mixinSettings + * + * @see node.timer.getTimeSince + */ + this.timeFrom = 'step'; + + /** + * ### ChoiceTableGroup.selectMultiple + * + * If TRUE, it allows to select multiple cells + * + * This option is passed to each individual item. + * + * @see mixinSettings + */ + this.selectMultiple = null; + + /** + * ### ChoiceTableGroup.renderer + * + * A callback that renders the content of each cell + * + * The callback must accept three parameters: + * + * - a td HTML element, + * - a choice + * - the index of the choice element within the choices array + * + * and optionally return the _value_ for the choice (otherwise + * the order in the choices array is used as value). + * + * This option is passed to each individual item. + * + * @see mixinSettings + */ + this.renderer = null; + + /** + * ### ChoiceTableGroup.separator + * + * Symbol used to separate tokens in the id attribute of every cell + * + * Default ChoiceTableGroup.separator + * + * This option is passed to each individual item. + * + * @see mixinSettings + */ + this.separator = ChoiceTableGroup.separator; + + /** + * ### ChoiceTableGroup.shuffleChoices + * + * If TRUE, choices in items are shuffled + * + * This option is passed to each individual item. + * + * @see mixinSettings + */ + this.shuffleChoices = null; + } + + // ## ChoiceTableGroup methods + + /** + * ### ChoiceTableGroup.init + * + * Initializes the instance + * + * Available options are: + * + * - className: the className of the table (string, array), or false + * to have none. + * - orientation: orientation of the table: vertical (v) or horizontal (h) + * - group: the name of the group (number or string), if any + * - groupOrder: the order of the table in the group, if any + * - onclick: a custom onclick listener function. Context is + * `this` instance + * - mainText: a text to be displayed above the table + * - shuffleItems: if TRUE, items are shuffled before being added + * to the table + * - freeText: if TRUE, a textarea will be added under the table, + * if 'string', the text will be added inside the the textarea + * - timeFrom: The timestamp as recorded by `node.timer.setTimestamp` + * or FALSE, to measure absolute time for current choice + * + * @param {object} options Configuration options + */ + ChoiceTableGroup.prototype.init = function(options) { + var tmp, that; + that = this; + + // TODO: many options checking are replicated. Skip them all? + // Have a method in ChoiceTable? + + if (!this.id) { + throw new TypeError('ChoiceTableGroup.init: options.id ' + + 'is missing.'); + } + + // Option orientation, default 'H'. + if ('undefined' === typeof options.orientation) { + tmp = 'H'; + } + else if ('string' !== typeof options.orientation) { + throw new TypeError('ChoiceTableGroup.init: options.orientation ' + + 'must be string, or undefined. Found: ' + + options.orientation); + } + else { + tmp = options.orientation.toLowerCase().trim(); + if (tmp === 'horizontal' || tmp === 'h') { + tmp = 'H'; + } + else if (tmp === 'vertical' || tmp === 'v') { + tmp = 'V'; + } + else { + throw new Error('ChoiceTableGroup.init: options.orientation ' + + 'is invalid: ' + tmp); + } + } + this.orientation = tmp; + + // Option shuffleItems, default false. + if ('undefined' === typeof options.shuffleItems) tmp = false; + else tmp = !!options.shuffleItems; + this.shuffleItems = tmp; + + // Option requiredChoice, if any. + if ('number' === typeof options.requiredChoice) { + this.requiredChoice = options.requiredChoice; + } + else if ('boolean' === typeof options.requiredChoice) { + this.requiredChoice = options.requiredChoice ? 1 : 0; + } + else if ('undefined' !== typeof options.requiredChoice) { + throw new TypeError('ChoiceTableGroup.init: ' + + 'options.requiredChoice ' + + 'be number or boolean or undefined. Found: ' + + options.requiredChoice); + } + + // Set the group, if any. + if ('string' === typeof options.group || + 'number' === typeof options.group) { + + this.group = options.group; + } + else if ('undefined' !== typeof options.group) { + throw new TypeError('ChoiceTableGroup.init: options.group must ' + + 'be string, number or undefined. Found: ' + + options.group); + } + + // Set the groupOrder, if any. + if ('number' === typeof options.groupOrder) { + + this.groupOrder = options.groupOrder; + } + else if ('undefined' !== typeof options.group) { + throw new TypeError('ChoiceTableGroup.init: options.groupOrder ' + + 'must be number or undefined. Found: ' + + options.groupOrder); + } + + // Set the onclick listener, if any. + if ('function' === typeof options.onclick) { + this.listener = function(e) { + options.onclick.call(this, e); + }; + } + else if ('undefined' !== typeof options.onclick) { + throw new TypeError('ChoiceTableGroup.init: options.onclick must ' + + 'be function or undefined. Found: ' + + options.onclick); + } + + // Set the mainText, if any. + if ('string' === typeof options.mainText) { + this.mainText = options.mainText; + } + else if ('undefined' !== typeof options.mainText) { + throw new TypeError('ChoiceTableGroup.init: options.mainText ' + + 'must be string or undefined. Found: ' + + options.mainText); + } + + // Set the hint, if any. + if ('string' === typeof options.hint || false === options.hint) { + this.hint = options.hint; + } + else if ('undefined' !== typeof options.hint) { + throw new TypeError('ChoiceTableGroup.init: options.hint must ' + + 'be a string, false, or undefined. Found: ' + + options.hint); + } + else { + // Returns undefined if there are no constraints. + this.hint = this.getText('autoHint'); + } + + // Set the timeFrom, if any. + if (options.timeFrom === false || + 'string' === typeof options.timeFrom) { + + this.timeFrom = options.timeFrom; + } + else if ('undefined' !== typeof options.timeFrom) { + throw new TypeError('ChoiceTableGroup.init: options.timeFrom ' + + 'must be string, false, or undefined. Found: ' + + options.timeFrom); + } + + // Option shuffleChoices, default false. + if ('undefined' !== typeof options.shuffleChoices) { + this.shuffleChoices = !!options.shuffleChoices; + } + + // Set the renderer, if any. + if ('function' === typeof options.renderer) { + this.renderer = options.renderer; + } + else if ('undefined' !== typeof options.renderer) { + throw new TypeError('ChoiceTableGroup.init: options.renderer ' + + 'must be function or undefined. Found: ' + + options.renderer); + } + + // Set default choices, if any. + if ('undefined' !== typeof options.choices) { + this.choices = options.choices; + } + + // Set the className, if not use default. + if ('undefined' === typeof options.className) { + this.className = ChoiceTableGroup.className; + } + else if (options.className === false || + 'string' === typeof options.className || + J.isArray(options.className)) { + + this.className = options.className; + } + else { + throw new TypeError('ChoiceTableGroup.init: options.' + + 'className must be string, array, ' + + 'or undefined. Found: ' + options.className); + } + + // After all configuration options are evaluated, add items. + + if ('object' === typeof options.table) { + this.table = options.table; + } + else if ('undefined' !== typeof options.table && + false !== options.table) { + + throw new TypeError('ChoiceTableGroup.init: options.table ' + + 'must be object, false or undefined. ' + + 'Found: ' + options.table); + } + + this.table = options.table; + + this.freeText = 'string' === typeof options.freeText ? + options.freeText : !!options.freeText; + + // Add the items. + if ('undefined' !== typeof options.items) this.setItems(options.items); + + }; + + /** + * ### ChoiceTableGroup.setItems + * + * Sets the available items and optionally builds the table + * + * @param {array} items The array of items + * + * @see ChoiceTableGroup.table + * @see ChoiceTableGroup.order + * @see ChoiceTableGroup.shuffleItems + * @see ChoiceTableGroup.buildTable + */ + ChoiceTableGroup.prototype.setItems = function(items) { + var len; + if (!J.isArray(items)) { + throw new TypeError('ChoiceTableGroup.setItems: ' + + 'items must be array. Found: ' + items); + } + if (!items.length) { + throw new Error('ChoiceTableGroup.setItems: ' + + 'items is an empty array.'); + } + + len = items.length; + this.itemsSettings = items; + this.items = new Array(len); + + // Save the order in which the items will be added. + this.order = J.seq(0, len-1); + if (this.shuffleItems) this.order = J.shuffle(this.order); + this.originalOrder = this.order; + + // Build the table and items at once (faster). + if (this.table) this.buildTable(); + }; + + /** + * ### ChoiceTableGroup.buildTable + * + * Builds the table of clickable items and enables it + * + * Must be called after items have been set already. + * + * @see ChoiceTableGroup.setChoiceTables + * @see ChoiceTableGroup.order + */ + ChoiceTableGroup.prototype.buildTable = function() { + var i, len, tr, H, ct; + var j, lenJ, lenJOld, hasRight, cell; + + H = this.orientation === 'H'; + i = -1, len = this.itemsSettings.length; + if (H) { + for ( ; ++i < len ; ) { + // Get item. + ct = getChoiceTable(this, i); + + // Add new TR. + tr = createTR(this, ct.id); + + // Append choices for item. + tr.appendChild(ct.leftCell); + j = -1, lenJ = ct.choicesCells.length; + // Make sure all items have same number of choices. + if (i === 0) { + lenJOld = lenJ; + } + else if (lenJ !== lenJOld) { + throw new Error('ChoiceTableGroup.buildTable: item ' + + 'do not have same number of choices: ' + + ct.id); + } + // TODO: might optimize. There are two loops (+1 inside ct). + for ( ; ++j < lenJ ; ) { + cell = ct.choicesCells[j]; + tr.appendChild(cell); + this.choicesById[cell.id] = cell; + } + if (ct.rightCell) tr.appendChild(ct.rightCell); + } + } + else { + + // Add new TR. + tr = createTR(this, 'header'); + + // Build all items first. + for ( ; ++i < len ; ) { + + // Get item, append choices for item. + ct = getChoiceTable(this, i); + + // Make sure all items have same number of choices. + lenJ = ct.choicesCells.length; + if (i === 0) { + lenJOld = lenJ; + } + else if (lenJ !== lenJOld) { + throw new Error('ChoiceTableGroup.buildTable: item ' + + 'do not have same number of choices: ' + + ct.id); + } + + if ('undefined' === typeof hasRight) { + hasRight = !!ct.rightCell; + } + else if ((!ct.rightCell && hasRight) || + (ct.rightCell && !hasRight)) { + + throw new Error('ChoiceTableGroup.buildTable: either all ' + + 'items or no item must have the right ' + + 'cell: ' + ct.id); + + } + // Add left. + tr.appendChild(ct.leftCell); + } + + if (hasRight) lenJ++; + + j = -1; + for ( ; ++j < lenJ ; ) { + // Add new TR. + tr = createTR(this, 'row' + (j+1)); + + i = -1; + // TODO: might optimize. There are two loops (+1 inside ct). + for ( ; ++i < len ; ) { + if (hasRight && j === (lenJ-1)) { + tr.appendChild(this.items[i].rightCell); + } + else { + cell = this.items[i].choicesCells[j]; + tr.appendChild(cell); + this.choicesById[cell.id] = cell; + } + } + } + + } + + // Enable onclick listener. + this.enable(true); + }; + + /** + * ### ChoiceTableGroup.append + * + * Implements Widget.append + * + * Checks that id is unique. + * + * Appends (all optional): + * + * - mainText: a question or statement introducing the choices + * - table: the table containing the choices + * - freeText: a textarea for comments + * + * @see Widget.append + */ + ChoiceTableGroup.prototype.append = function() { + // Id must be unique. + if (W.getElementById(this.id)) { + throw new Error('ChoiceTableGroup.append: id ' + + 'is not unique: ' + this.id); + } + + // MainText. + if (this.mainText) { + this.spanMainText = W.append('span', this.bodyDiv, { + className: 'custominput-maintext', + innerHTML: this.mainText + }); + } + // Hint. + if (this.hint) { + W.append('span', this.spanMainText || this.bodyDiv, { + className: 'choicetable-hint', + innerHTML: this.hint + }); + } + + // Create/set table, if requested. + if (this.table !== false) { + if ('undefined' === typeof this.table) { + this.table = document.createElement('table'); + if (this.items) this.buildTable(); + } + // Set table id. + this.table.id = this.id; + if (this.className) J.addClass(this.table, this.className); + else this.table.className = ''; + // Append table. + this.bodyDiv.appendChild(this.table); + } + + // Creates a free-text textarea, possibly with placeholder text. + if (this.freeText) { + this.textarea = document.createElement('textarea'); + this.textarea.id = this.id + '_text'; + this.textarea.className = ChoiceTableGroup.className + '-freetext'; + if ('string' === typeof this.freeText) { + this.textarea.placeholder = this.freeText; + } + // Append textarea. + this.bodyDiv.appendChild(this.textarea); + } + }; + + /** + * ### ChoiceTableGroup.listeners + * + * Implements Widget.listeners + * + * Adds two listeners two disable/enable the widget on events: + * INPUT_DISABLE, INPUT_ENABLE + * + * Notice! Nested choice tables listeners are not executed. + * + * @see Widget.listeners + * @see mixinSettings + */ + ChoiceTableGroup.prototype.listeners = function() { + var that = this; + node.on('INPUT_DISABLE', function() { + that.disable(); + }); + node.on('INPUT_ENABLE', function() { + that.enable(); + }); + }; + + /** + * ### ChoiceTableGroup.disable + * + * Disables clicking on the table and removes CSS 'clicklable' class + */ + ChoiceTableGroup.prototype.disable = function() { + if (this.disabled === true || !this.table) return; + this.disabled = true; + J.removeClass(this.table, 'clickable'); + this.table.removeEventListener('click', this.listener); + this.emit('disabled'); + }; + + /** + * ### ChoiceTableGroup.enable + * + * Enables clicking on the table and adds CSS 'clicklable' class + * + * @return {function} cb The event listener function + */ + ChoiceTableGroup.prototype.enable = function(force) { + if (!this.table || (!force && !this.disabled)) return; + this.disabled = false; + J.addClass(this.table, 'clickable'); + this.table.addEventListener('click', this.listener); + this.emit('enabled'); + }; + + /** + * ### ChoiceTableGroup.verifyChoice + * + * Compares the current choice/s with the correct one/s + * + * @param {boolean} markAttempt Optional. If TRUE, the value of + * current choice is added to the attempts array. Default + * + * @return {boolean|null} TRUE if current choice is correct, + * FALSE if it is not correct, or NULL if no correct choice + * was set + * + * @see ChoiceTableGroup.attempts + * @see ChoiceTableGroup.setCorrectChoice + */ + ChoiceTableGroup.prototype.verifyChoice = function(markAttempt) { + var i, len, out; + out = {}; + // Mark attempt by default. + markAttempt = 'undefined' === typeof markAttempt ? true : markAttempt; + i = -1, len = this.items.length; + for ( ; ++i < len ; ) { + out[this.items[i].id] = this.items[i].verifyChoice(markAttempt); + } + return out; + }; + + /** + * ### ChoiceTable.setCurrentChoice + * + * Marks a choice as current in each item + * + * If the item allows it, multiple choices can be set as current. + * + * @param {number|string} The choice to mark as current + * + * @see ChoiceTable.currentChoice + * @see ChoiceTable.selectMultiple + */ + ChoiceTableGroup.prototype.setCurrentChoice = function(choice) { + var i, len; + i = -1, len = this.items[i].length; + for ( ; ++i < len ; ) { + this.items[i].setCurrentChoice(choice); + } + }; + + /** + * ### ChoiceTableGroup.unsetCurrentChoice + * + * Deletes the value for currentChoice from every item + * + * If `ChoiceTableGroup.selectMultiple` is set the + * + * @param {number|string} Optional. The choice to delete from currentChoice + * when multiple selections are allowed + * + * @see ChoiceTableGroup.currentChoice + * @see ChoiceTableGroup.selectMultiple + */ + ChoiceTableGroup.prototype.unsetCurrentChoice = function(choice) { + var i, len; + i = -1, len = this.items.length; + for ( ; ++i < len ; ) { + this.items[i].unsetCurrentChoice(choice); + } + }; + + /** + * ### ChoiceTableGroup.highlight + * + * Highlights the choice table + * + * @param {string} The style for the table's border. + * Default '1px solid red' + * + * @see ChoiceTableGroup.highlighted + */ + ChoiceTableGroup.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError('ChoiceTableGroup.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (!this.table || this.highlighted === true) return; + this.table.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted', border); + }; + + /** + * ### ChoiceTableGroup.unhighlight + * + * Removes highlight from the choice table + * + * @see ChoiceTableGroup.highlighted + */ + ChoiceTableGroup.prototype.unhighlight = function() { + if (!this.table || this.highlighted !== true) return; + this.table.style.border = ''; + this.highlighted = false; + this.emit('unhighlighted'); + }; + + /** + * ### ChoiceTableGroup.getValues + * + * Returns the values for current selection and other paradata + * + * Paradata that is not set or recorded will be omitted + * + * @param {object} opts Optional. Configures the return value. + * Available optionts: + * + * - markAttempt: If TRUE, getting the value counts as an attempt + * to find the correct answer. Default: TRUE. + * - highlight: If TRUE, if current value is not the correct + * value, widget will be highlighted. Default: FALSE. + * - reset: If TRUTHY and no item raises an error, + * then it resets the state of all items before + * returning it. Default: FALSE. + * + * @return {object} Object containing the choice and paradata + * + * @see ChoiceTableGroup.verifyChoice + * @see ChoiceTableGroup.reset + */ + ChoiceTableGroup.prototype.getValues = function(opts) { + var obj, i, len, tbl, toHighlight, toReset; + obj = { + id: this.id, + order: this.order, + items: {}, + isCorrect: true + }; + opts = opts || {}; + // Make sure reset is done only at the end. + toReset = opts.reset; + opts.reset = false; + i = -1, len = this.items.length; + for ( ; ++i < len ; ) { + tbl = this.items[i]; + obj.items[tbl.id] = tbl.getValues(opts); + if (obj.items[tbl.id].choice === null) { + obj.missValues = true; + if (tbl.requiredChoice) { + toHighlight = true; + obj.isCorrect = false; + } + } + if (obj.items[tbl.id].isCorrect === false && opts.highlight) { + toHighlight = true; + } + } + + if (toHighlight) this.highlight(); + else if (toReset) this.reset(toReset); + if (this.textarea) obj.freetext = this.textarea.value; + return obj; + }; + + /** + * ### ChoiceTableGroup.setValues + * + * Sets values in the choice table group as specified by the options + * + * @param {object} options Optional. Options specifying how to set + * the values. If no parameter is specified, random values will + * be set. + * + * @see ChoiceTable.setValues + * + * @experimental + */ + ChoiceTableGroup.prototype.setValues = function(opts) { + var i, len; + if (!this.items || !this.items.length) { + throw new Error('ChoiceTableGroup.setValues: no items found.'); + } + opts = opts || {}; + i = -1, len = this.items.length; + for ( ; ++i < len ; ) { + this.items[i].setValues(opts); + } + + // Make a random comment. + if (this.textarea) this.textarea.value = J.randomString(100, '!Aa0'); + }; + + /** + * ### ChoiceTableGroup.reset + * + * Resets all the ChoiceTable items and textarea + * + * @param {object} options Optional. Options specifying how to set + * to reset each item + * + * @see ChoiceTable.reset + * @see ChoiceTableGroup.shuffle + */ + ChoiceTableGroup.prototype.reset = function(opts) { + var i, len; + opts = opts || {}; + i = -1, len = this.items.length; + for ( ; ++i < len ; ) { + this.items[i].reset(opts); + } + // Delete textarea, if found. + if (this.textarea) this.textarea.value = ''; + if (opts.shuffleItems) this.shuffle(); + if (this.isHighlighted()) this.unhighlight(); + }; + + /** + * ### ChoiceTableGroup.shuffle + * + * Shuffles the order of the displayed items + * + * Assigns the new order of items to `this.order`. + * + * @param {object} options Optional. Not used for now. + * + * TODO: shuffle choices in each item. (Note: can't use + * item.shuffle, because the cells are taken out, so + * there is no table and no tr in there) + * + * JSUS.shuffleElements + */ + ChoiceTableGroup.prototype.shuffle = function(opts) { + var order, i, len, j, lenJ, that, cb, newOrder; + if (!this.items) return; + len = this.items.length; + if (!len) return; + that = this; + newOrder = new Array(len); + // Updates the groupOrder property of each item, + // and saves the order of items correctly. + cb = function(el, newPos, oldPos) { + var i; + i = el.id.split(that.separator); + i = that.orientation === 'H' ? i[2] : i[0]; + i = that.itemsMap[i]; + that.items[i].groupOrder = (newPos+1); + newOrder[newPos] = i; + }; + order = J.shuffle(this.order); + if (this.orientation === 'H') { + J.shuffleElements(this.table, order, cb); + } + else { + // Here we maintain the columns manually. Each TR contains TD + // belonging to different items, we make sure the order is the + // same for all TR. + len = this.trs.length; + for ( i = -1 ; ++i < len ; ) { + J.shuffleElements(this.trs[i], order, cb); + // Call cb only on first iteration. + cb = undefined; + } + } + this.order = newOrder; + }; + + + + // ## Helper methods. + + /** + * ### mixinSettings + * + * Mix-ins global settings with local settings for specific choice tables + * + * @param {ChoiceTableGroup} that This instance + * @param {object|string} s The current settings for the item + * (choice table), or just its id, to mixin all settings. + * @param {number} i The ordinal position of the table in the group + * + * @return {object} s The mixed-in settings + */ + function mixinSettings(that, s, i) { + if ('string' === typeof s) { + s = { id: s }; + } + else if ('object' !== typeof s) { + throw new TypeError('ChoiceTableGroup.buildTable: item must be ' + + 'string or object. Found: ' + s); + } + s.group = that.id; + s.groupOrder = i+1; + s.orientation = that.orientation; + s.title = false; + s.listeners = false; + s.separator = that.separator; + + if ('undefined' === typeof s.choices && that.choices) { + s.choices = that.choices; + } + + if (!s.renderer && that.renderer) s.renderer = that.renderer; + + if ('undefined' === typeof s.requiredChoice && that.requiredChoice) { + s.requiredChoice = that.requiredChoice; + } + + if ('undefined' === typeof s.selectMultiple && + null !== that.selectMultiple) { + + s.selectMultiple = that.selectMultiple; + } + + if ('undefined' === typeof s.shuffleChoices && that.shuffleChoices) { + s.shuffleChoices = that.shuffleChoices; + } + + if ('undefined' === typeof s.timeFrom) s.timeFrom = that.timeFrom; + + if ('undefined' === typeof s.left) s.left = s.id; + + // No reference is stored in node.widgets. + s.storeRef = false; + + return s; + } + + /** + * ### getChoiceTable + * + * Creates a instance i-th of choice table with relative settings + * + * Stores a reference of each table in `itemsById` + * + * @param {ChoiceTableGroup} that This instance + * @param {number} i The ordinal position of the table in the group + * + * @return {object} ct The requested choice table + * + * @see ChoiceTableGroup.itemsSettings + * @see ChoiceTableGroup.itemsById + * @see mixinSettings + */ + function getChoiceTable(that, i) { + var ct, s, idx; + idx = that.order[i]; + s = mixinSettings(that, that.itemsSettings[idx], i); + ct = node.widgets.get('ChoiceTable', s); + if (that.itemsById[ct.id]) { + throw new Error('ChoiceTableGroup.buildTable: an item ' + + 'with the same id already exists: ' + ct.id); + } + if (!ct.leftCell) { + throw new Error('ChoiceTableGroup.buildTable: item ' + + 'is missing a left cell: ' + s.id); + } + that.itemsById[ct.id] = ct; + that.items[idx] = ct; + that.itemsMap[ct.id] = idx; + return ct; + } + + /** + * ### createTR + * + * Creates and append a new TR element + * + * If required by current configuration, the `id` attribute is + * added to the TR in the form of: 'tr' + separator + widget_id + * + * @param {ChoiceTable} that This instance + * + * @return {HTMLElement} Thew newly created TR element + */ + function createTR(that, trid) { + var tr, sep; + tr = document.createElement('tr'); + that.table.appendChild(tr); + // Set id. + sep = that.separator; + tr.id = that.id + sep + 'tr' + sep + trid; + // Store reference. + that.trs.push(tr); + return tr; + } + +})(node); + +/** + * # Controls + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Creates and manipulates a set of forms + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + // TODO: handle different events, beside onchange + + node.widgets.register('Controls', Controls); + + // ## Meta-data + + Controls.version = '0.5.1'; + Controls.description = 'Wraps a collection of user-inputs controls.'; + + Controls.title = 'Controls'; + Controls.className = 'controls'; + + /** + * ## Controls constructor + * + * `Control` wraps a collection of user-input controls + * + * @param {object} options Optional. Configuration options + * which is stored and forwarded to Controls.init. + * + * The options object can have the following attributes: + * - Any option that can be passed to `W.List` constructor. + * - `change`: Event to fire when contents change. + * - `features`: Collection of collection attributes for individual + * controls. + * - `submit`: Description of the submit button. + * If submit.id is defined, the button will get that id and + * the text on the button will be the text in submit.name. + * If submit is a string, it will be the text on the button. + * - `attributes`: Attributes of the submit button. + * + * @see Controls.init + */ + function Controls(options) { + this.options = options; + + /** + * ### Controls.listRoot + * + * The list which holds the controls + */ + this.listRoot = null; + + /** + * ### Controls.submit + * + * The submit button + */ + this.submit = null; + + /** + * ### Controls.changeEvent + * + * The event to be fired when the list changes + */ + this.changeEvent = 'Controls_change'; + + /** + * ### Controls.hasChanged + * + * Flag to indicate whether the list has changed + */ + this.hasChanged = false; + } + + Controls.prototype.add = function(root, id, attributes) { + // TODO: replace W.addTextInput + //return W.addTextInput(root, id, attributes); + }; + + Controls.prototype.getItem = function(id, attributes) { + // TODO: replace W.addTextInput + //return W.getTextInput(id, attributes); + }; + + // ## Controls methods + + /** + * ### Controls.init + * + * Initializes the widget + * + * @param {object} options Optional. Configuration options. + * + * The options object can have the following attributes: + * - Any option that can be passed to `W.List` constructor. + * - `change`: Event to fire when contents change. + * - `features`: Collection of collection attributes for individual + * controls. + * + * @see nodegame-window/List + */ + Controls.prototype.init = function(options) { + this.hasChanged = false; // TODO: should this be inherited? + if ('undefined' !== typeof options.change) { + if (!options.change) { + this.changeEvent = false; + } + else { + this.changeEvent = options.change; + } + } + this.list = new W.List(options); + this.listRoot = this.list.getRoot(); + + if (options.features) { + this.features = options.features; + this.populate(); + } + }; + + /** + * ### Controls.append + * + * Appends the widget to `this.bodyDiv` + * + * @see Controls.init + */ + Controls.prototype.append = function() { + var that = this; + var idButton = 'submit_Controls'; + + + this.list.parse(); + this.bodyDiv.appendChild(this.listRoot); + + if (this.options.submit) { + if (this.options.submit.id) { + idButton = this.options.submit.id; + this.option.submit = this.option.submit.name; + } + this.submit = W.add('button', this.bodyDiv, + J.merge(this.options.attributes, { + id: idButton, + innerHTML: this.options.submit + })); + + this.submit.onclick = function() { + if (that.options.change) { + node.emit(that.options.change); + } + }; + } + }; + + Controls.prototype.parse = function() { + return this.list.parse(); + }; + + /** + * ### Controls.populate + * + * Adds features to the list. + * + * @see Controls.init + */ + Controls.prototype.populate = function() { + var key, id, attributes, container, elem; + var that = this; + + for (key in this.features) { + if (this.features.hasOwnProperty(key)) { + // Prepare the attributes vector. + attributes = this.features[key]; + id = key; + if (attributes.id) { + id = attributes.id; + delete attributes.id; + } + + container = document.createElement('div'); + // Add a different element according + // to the subclass instantiated. + elem = this.add(container, id, attributes); + + // Fire the onChange event, if one defined + if (this.changeEvent) { + elem.onchange = function() { + node.emit(that.changeEvent); + }; + } + + if (attributes.label) { + W.add('label', container, { + 'for': elem.id, + innerHTML: attributes.label + }); + } + + // Element added to the list. + this.list.addDT(container); + } + } + }; + + Controls.prototype.listeners = function() { + var that = this; + // TODO: should this be inherited? + node.on(this.changeEvent, function() { + that.hasChanged = true; + }); + + }; + + Controls.prototype.refresh = function() { + var key, el; + for (key in this.features) { + if (this.features.hasOwnProperty(key)) { + el = W.getElementById(key); + if (el) { + // node.log('KEY: ' + key, 'DEBUG'); + // node.log('VALUE: ' + el.value, 'DEBUG'); + el.value = this.features[key].value; + // TODO: set all the other attributes + // TODO: remove/add elements + } + + } + } + + return true; + }; + + Controls.prototype.getValues = function() { + var out, el, key; + out = {}; + for (key in this.features) { + if (this.features.hasOwnProperty(key)) { + el = W.getElementById(key); + if (el) out[key] = Number(el.value); + } + } + return out; + }; + + Controls.prototype.highlight = function(code) { + return W.highlight(this.listRoot, code); + }; + + // ## Sub-classes + + /** + * ### Slider + */ + + + SliderControls.prototype.__proto__ = Controls.prototype; + SliderControls.prototype.constructor = SliderControls; + + SliderControls.version = '0.2.2'; + SliderControls.description = 'Collection of Sliders.'; + + SliderControls.title = 'Slider Controls'; + SliderControls.className = 'slidercontrols'; + + SliderControls.dependencies = { + Controls: {} + }; + + // Need to be after the prototype is inherited. + node.widgets.register('SliderControls', SliderControls); + + function SliderControls(options) { + Controls.call(this, options); + } + + SliderControls.prototype.add = function(root, id, attributes) { + attributes = attributes || {}; + attributes.id = id; + attributes.type = 'range'; + return W.add('input', root, attributes); + }; + + SliderControls.prototype.getItem = function(id, attributes) { + attributes = attributes || {}; + attributes.id = id; + return W.get('input', attributes); + }; + + /** + * ### jQuerySlider + */ + + + jQuerySliderControls.prototype.__proto__ = Controls.prototype; + jQuerySliderControls.prototype.constructor = jQuerySliderControls; + + jQuerySliderControls.version = '0.14'; + jQuerySliderControls.description = 'Collection of jQuery Sliders.'; + + jQuerySliderControls.title = 'jQuery Slider Controls'; + jQuerySliderControls.className = 'jqueryslidercontrols'; + + jQuerySliderControls.dependencies = { + jQuery: {}, + Controls: {} + }; + + node.widgets.register('jQuerySliderControls', jQuerySliderControls); + + function jQuerySliderControls(options) { + Controls.call(this, options); + } + + jQuerySliderControls.prototype.add = function(root, id, attributes) { + var slider = jQuery('
', { + id: id + }).slider(); + + var s = slider.appendTo(root); + return s[0]; + }; + + jQuerySliderControls.prototype.getItem = function(id, attributes) { + var slider = jQuery('
', { + id: id + }).slider(); + + return slider; + }; + + /** + * ### RadioControls + */ + + RadioControls.prototype.__proto__ = Controls.prototype; + RadioControls.prototype.constructor = RadioControls; + + RadioControls.version = '0.1.2'; + RadioControls.description = 'Collection of Radio Controls.'; + + RadioControls.title = 'Radio Controls'; + RadioControls.className = 'radiocontrols'; + + RadioControls.dependencies = { + Controls: {} + }; + + node.widgets.register('RadioControls', RadioControls); + + function RadioControls(options) { + Controls.call(this,options); + this.groupName = ('undefined' !== typeof options.name) ? options.name : + W.generateUniqueId(); + this.radioElem = null; + } + + // overriding populate also. There is an error with the Label + RadioControls.prototype.populate = function() { + var key, id, attributes, elem, that; + that = this; + + if (!this.radioElem) { + this.radioElem = document.createElement('radio'); + this.radioElem.group = this.name || "radioGroup"; + this.radioElem.group = this.className || "radioGroup"; + this.bodyDiv.appendChild(this.radioElem); + } + + for (key in this.features) { + if (this.features.hasOwnProperty(key)) { + // Prepare the attributes vector. + attributes = this.features[key]; + id = key; + if (attributes.id) { + id = attributes.id; + delete attributes.id; + } + + // Add a different element according + // to the subclass instantiated. + elem = this.add(this.radioElem, id, attributes); + + // Fire the onChange event, if one defined + if (this.changeEvent) { + elem.onchange = function() { + node.emit(that.changeEvent); + }; + } + + // Element added to the list. + this.list.addDT(elem); + } + } + }; + + RadioControls.prototype.add = function(root, id, attributes) { + var elem; + if ('undefined' === typeof attributes.name) { + attributes.name = this.groupName; + } + attributes.id = id; + attributes.type = 'radio'; + elem = W.add('input', root, attributes); + // Adding the text for the radio button + elem.appendChild(document.createTextNode(attributes.label)); + return elem; + }; + + RadioControls.prototype.getItem = function(id, attributes) { + attributes = attributes || {}; + // add the group name if not specified + // TODO: is this a javascript bug? + if ('undefined' === typeof attributes.name) { + attributes.name = this.groupName; + } + attributes.id = id; + attributes.type = 'radio'; + return W.get('input', attributes); + }; + + // Override getAllValues for Radio Controls + RadioControls.prototype.getValues = function() { + var key, el; + for (key in this.features) { + if (this.features.hasOwnProperty(key)) { + el = W.getElementById(key); + if (el.checked) return el.value; + } + } + return false; + }; + +})(node); + +/** + * # CustomInput + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a configurable input form with validation + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('CustomInput', CustomInput); + + // ## Meta-data + + CustomInput.version = '0.7.0'; + CustomInput.description = 'Creates a configurable input form'; + + CustomInput.title = false; + CustomInput.panel = false; + CustomInput.className = 'custominput'; + + CustomInput.types = { + text: true, + number: true, + 'float': true, + 'int': true, + date: true, + list: true, + us_city_state_zip: true + }; + + var sepNames = { + ',': 'comma', + ' ': 'space', + '.': 'dot' + }; + + var usStates = { + Alabama: 'AL', + Alaska: 'AK', + Arizona: 'AZ', + Arkansas: 'AR', + California: 'CA', + Colorado: 'CO', + Connecticut: 'CT', + Delaware: 'DE', + Florida: 'FL', + Georgia: 'GA', + Hawaii: 'HI', + Idaho: 'ID', + Illinois: 'IL', + Indiana: 'IN', + Iowa: 'IA', + Kansas: 'KS', + Kentucky: 'KY', + Louisiana: 'LA', + Maine: 'ME', + Maryland: 'MD', + Massachusetts: 'MA', + Michigan: 'MI', + Minnesota: 'MN', + Mississippi: 'MS', + Missouri: 'MO', + Montana: 'MT', + Nebraska: 'NE', + Nevada: 'NV', + 'New Hampshire': 'NH', + 'New Jersey': 'NJ', + 'New Mexico': 'NM', + 'New York': 'NY', + 'North Carolina': 'NC', + 'North Dakota': 'ND', + Ohio: 'OH', + Oklahoma: 'OK', + Oregon: 'OR', + Pennsylvania: 'PA', + 'Rhode Island': 'RI', + 'South Carolina': 'SC', + 'South Dakota': 'SD', + Tennessee: 'TN', + Texas: 'TX', + Utah: 'UT', + Vermont: 'VT', + Virginia: 'VA', + Washington: 'WA', + 'West Virginia': 'WV', + Wisconsin: 'WI', + Wyoming: 'WY', + }; + + var usTerr = { + 'American Samoa': 'AS', + 'District of Columbia': 'DC', + 'Federated States of Micronesia': 'FM', + Guam: 'GU', + 'Marshall Islands': 'MH', + 'Northern Mariana Islands': 'MP', + Palau: 'PW', + 'Puerto Rico': 'PR', + 'Virgin Islands': 'VI' + }; + + // To be filled if requested. + var usTerrByAbbr; + var usStatesByAbbr; + var usStatesTerr; + var usStatesTerrByAbbr; + + CustomInput.texts = { + listErr: function(w, param) { + if (param === 'min') { + return 'Too few items. Min: ' + w.params.minItems; + } + if (param === 'max') { + return 'Too many items. Max: ' + w.params.maxItems; + } + return 'Check that there are no empty items; do not end with ' + + 'the separator'; + }, + autoHint: function(w) { + var res, sep; + if (w.type === 'list') { + sep = sepNames[w.params.listSep] || w.params.listSep; + res = '(if more than one, separate with ' + sep + ')'; + } + return w.requiredChoice ? (res + '*') : (res || false); + }, + numericErr: function(w) { + var str, p, inc; + p = w.params; + // Weird, but valid, case. + if (p.exactly) return 'Must enter ' + p.lower; + // Others. + inc = '(inclusive)'; + str = 'Must be a'; + if (w.type === 'float') str += 'floating point'; + else if (w.type === 'int') str += 'n integer'; + str += ' number '; + if (p.between) { + str += 'between ' + p.lower; + if (p.leq) str += inc; + str += ' and ' + p.upper; + if (p.ueq) str += inc; + } + else if ('undefined' !== typeof p.lower) { + str += 'greater than '; + if (p.leq) str += 'or equal to '; + str += p.lower; + } + else { + str += 'less than '; + if (p.leq) str += 'or equal to '; + str += p.upper; + } + return str; + }, + textErr: function(w, len) { + var str, p; + p = w.params; + str = 'Must be '; + if (p.exactly) { + str += 'exactly ' + (p.lower + 1); + } + else if (p.between) { + str += 'between ' + p.lower + ' and ' + p.upper; + } + else if ('undefined' !== typeof p.lower) { + str += ' more than ' + (p.lower -1); + } + else if ('undefined' !== typeof p.upper) { + str += ' less than ' + (p.upper + 1); + } + str += ' characters long'; + if (p.between) str += ' (extremes included)'; + str += '. Current length: ' + len; + return str; + }, + dateErr: function(w, invalid) { + return invalid ? 'Date is invalid' : 'Must follow format ' + + w.params.format; + }, + emptyErr: function(w) { + return 'Cannot be empty' + } + }; + + // ## Dependencies + + CustomInput.dependencies = { + JSUS: {} + }; + + /** + * ## CustomInput constructor + * + * Creates a new instance of CustomInput + */ + function CustomInput() { + + /** + * ### CustomInput.input + * + * The HTML input element + */ + this.input = null; + + /** + * ### CustomInput.placeholder + * + * The placeholder text for the input form + * + * Some types preset it automatically + */ + this.placeholder = null; + + /** + * ### CustomInput.inputWidth + * + * The width of the input form as string (css attribute) + * + * Some types preset it automatically + */ + this.inputWidth = null; + + /** + * ### CustomInput.type + * + * The type of input + */ + this.type = null; + + /** + * ### CustomInput.preprocess + * + * The function that preprocess the input before validation + * + * The function receives the input form and must modify it directly + */ + this.preprocess = null; + + /** + * ### CustomInput.validation + * + * The validation function for the input + * + * The function returns an object like: + * + * ```javascript + * { + * value: 'validvalue', + * err: 'This error occurred' // If invalid. + * } + * ``` + */ + this.validation = null; + + /** + * ### CustomInput.validationSpeed + * + * How often (in milliseconds) the validation function is called + * + * Default: 500 + */ + this.validationSpeed = 500; + + /** + * ### CustomInput.postprocess + * + * The function that postprocess the input after validation + * + * The function returns the postprocessed valued + */ + this.postprocess = null; + + /** + * ### CustomInput.params + * + * Object containing extra validation params + * + * This object is populated by the init function + */ + this.params = {}; + + /** + * ### CustomInput.errorBox + * + * An HTML element displayed when a validation error occurs + */ + this.errorBox = null; + + /** + * ### CustomInput.mainText + * + * A text preceeding the custom input + */ + this.mainText = null; + + /** + * ### CustomInput.hint + * + * An additional text with information about the input + * + * If not specified, it may be auto-filled, e.g. '*'. + * + * @see CustomInput.texts.autoHint + */ + this.hint = null; + + /** + * ### CustomInput.requiredChoice + * + * If TRUE, the input form cannot be left empty + * + * Default: TRUE + */ + this.requiredChoice = null; + + /** + * ### CustomInput.timeBegin + * + * When the first character was inserted + */ + this.timeBegin = null; + + /** + * ### CustomInput.timeEnd + * + * When the last character was inserted + */ + this.timeEnd = null; + } + + // ## CustomInput methods + + /** + * ### CustomInput.init + * + * Initializes the instance + * + * @param {object} opts Configuration options + */ + CustomInput.prototype.init = function(opts) { + var tmp, that, e, isText; + that = this; + e = 'CustomInput.init: '; + + // TODO: this becomes false later on. Why??? + this.requiredChoice = !!opts.requiredChoice; + + if (opts.type) { + if (!CustomInput.types[opts.type]) { + throw new Error(e + 'type not supported: ' + opts.type); + } + this.type = opts.type; + } + else { + this.type = 'text'; + } + + if (opts.validation) { + if ('function' !== typeof opts.validation) { + throw new TypeError(e + 'validation must be function ' + + 'or undefined. Found: ' + + opts.validation); + } + tmp = opts.validation; + } + else { + // Add default validations based on type. + + if (this.type === 'number' || this.type === 'float' || + this.type === 'int' || this.type === 'text') { + + isText = this.type === 'text'; + + // Greater than. + if ('undefined' !== typeof opts.min) { + tmp = J.isNumber(opts.min); + if (false === tmp) { + throw new TypeError(e + 'min must be number or ' + + 'undefined. Found: ' + opts.min); + } + this.params.lower = opts.min; + this.params.leq = true; + } + // Less than. + if ('undefined' !== typeof opts.max) { + tmp = J.isNumber(opts.max); + if (false === tmp) { + throw new TypeError(e + 'max must be number or ' + + 'undefined. Found: ' + opts.max); + } + this.params.upper = opts.max; + this.params.ueq = true; + } + + if (opts.strictlyGreater) this.params.leq = false; + if (opts.strictlyLess) this.params.ueq = false; + + // Checks on both min and max. + if ('undefined' !== typeof this.params.lower && + 'undefined' !== typeof this.params.upper) { + + if (this.params.lower > this.params.upper) { + throw new TypeError(e + 'min cannot be greater ' + + 'than max. Found: ' + + opts.min + '> ' + opts.max); + } + // Exact length. + if (this.params.lower === this.params.upper) { + if (!this.params.leq || !this.params.ueq) { + + throw new TypeError(e + 'min cannot be equal to ' + + 'max when strictlyGreater or ' + + 'strictlyLess are set. ' + + 'Found: ' + opts.min); + } + if (this.type === 'int' || this.type === 'text') { + if (J.isFloat(this.params.lower)) { + + + throw new TypeError(e + 'min cannot be a ' + + 'floating point number ' + + 'and equal to ' + + 'max, when type ' + + 'is not "float". Found: ' + + opts.min); + } + } + // Store this to create better error strings. + this.params.exactly = true; + } + else { + // Store this to create better error strings. + this.params.between = true; + } + } + + // Checks for text only. + if (isText) { + if ('undefined' !== typeof this.params.lower) { + if (this.params.lower < 0) { + throw new TypeError(e + 'min cannot be negative ' + + 'when type is "text". Found: ' + + this.params.lower); + } + if (!this.params.leq) this.params.lower++; + } + if ('undefined' !== typeof this.params.upper) { + if (this.params.upper < 0) { + throw new TypeError(e + 'max cannot be negative ' + + 'when type is "text". Found: ' + + this.params.upper); + } + if (!this.params.ueq) this.params.upper--; + } + + tmp = function(value) { + var len, p, out, err; + p = that.params; + len = value.length; + out = { value: value }; + if (p.exactly) { + err = len !== p.lower; + } + else { + if (('undefined' !== typeof p.lower && + len < p.lower) || + ('undefined' !== typeof p.upper && + len > p.upper)) { + + err = true; + } + } + if (err) out.err = that.getText('textErr', len); + return out; + }; + } + else { + tmp = (function() { + var cb; + if (that.type === 'float') cb = J.isFloat; + else if (that.type === 'int') cb = J.isInt; + else cb = J.isNumber; + return function(value) { + var res, p; + p = that.params; + res = cb(value, p.lower, p.upper, p.leq, p.ueq); + if (res !== false) return { value: res }; + return { + value: value, + err: that.getText('numericErr') + }; + }; + })(); + } + + // Preset inputWidth. + if (this.params.upper) { + if (this.params.upper < 10) this.inputWidth = '100px'; + else if (this.params.upper < 20) this.inputWidth = '200px'; + } + + } + else if (this.type === 'date') { + if ('undefined' !== typeof opts.format) { + // TODO: use regex. + if (opts.format !== 'mm-dd-yy' && + opts.format !== 'dd-mm-yy' && + opts.format !== 'mm-dd-yyyy' && + opts.format !== 'dd-mm-yyyy' && + opts.format !== 'mm.dd.yy' && + opts.format !== 'dd.mm.yy' && + opts.format !== 'mm.dd.yyyy' && + opts.format !== 'dd.mm.yyyy' && + opts.format !== 'mm/dd/yy' && + opts.format !== 'dd/mm/yy' && + opts.format !== 'mm/dd/yyyy' && + opts.format !== 'dd/mm/yyyy') { + + throw new Error(e + 'date format is invalid. Found: ' + + opts.format); + } + this.params.format = opts.format; + } + else { + this.params.format = 'mm/dd/yyyy'; + } + + this.params.sep = this.params.format.charAt(2); + tmp = this.params.format.split(this.params.sep); + this.params.yearDigits = tmp[2].length; + this.params.dayPos = tmp[0].charAt(0) === 'd' ? 0 : 1; + this.params.monthPos = this.params.dayPos ? 0 : 1; + this.params.dateLen = tmp[2].length + 6; + + + // Preset inputWidth. + if (this.params.yearDigits === 2) this.inputWidth = '100px'; + else this.inputWidth = '150px'; + + // Preset placeholder. + this.placeholder = this.params.format; + + tmp = function(value) { + var p, tokens, tmp, err, res, dayNum, l1, l2; + p = that.params; + + // Is the format valid. + + tokens = value.split(p.sep); + if (tokens.length !== 3) { + return { err: that.getText('dateErr') }; + } + + // Year. + if (tokens[2].length !== p.yearDigits) { + return { err: that.getText('dateErr') }; + } + + // Now we check if the date is valid. + + res = {}; + if (p.yearDigits === 2) { + l1 = -1; + l2 = 100; + } + else { + l1 = -1 + l2 = 10000; + } + tmp = J.isInt(tokens[2], l1, l2); + if (tmp !== false) res.year = tmp; + else err = true; + + + // Month. + tmp = J.isInt(tokens[p.monthPos], 1, 12, 1, 1); + if (!tmp) err = true; + else res.month = tmp; + // 31 or 30 days? + if (tmp === 1 || tmp === 3 || tmp === 5 || tmp === 7 || + tmp === 8 || tmp === 10 || tmp === 12) { + + dayNum = 31; + } + else if (tmp !== 2) { + dayNum = 30; + } + else { + // Is it leap year? + dayNum = (res.year % 4 === 0 && res.year % 100 !== 0) || + res.year % 400 === 0 ? 29 : 28; + } + res.month = tmp; + // Day. + tmp = J.isInt(tokens[p.dayPos], 1, dayNum, 1, 1); + if (!tmp) err = true; + else res.day = tmp; + + // + if (err) res.err = that.getText('dateErr', true); + return res; + }; + } + // List, subtypes. + + else if (this.type === 'list' || + this.type === 'us_city_state_zip') { + + if (opts.listSeparator) { + if ('string' !== typeof opts.listSeparator) { + throw new TypeError(e + 'listSeparator must be ' + + 'string or undefined. Found: ' + + opts.listSeperator); + } + this.params.listSep = opts.listSeparator; + } + else { + this.params.listSep = ','; + } + + if (this.type === 'us_city_state_zip') { + // Create validation abbr. + if (!usStatesTerrByAbbr) { + usStatesTerr = J.mixin(usStates, usTerr); + usStatesTerrByAbbr = J.reverseObj(usStatesTerr); + } + this.params.minItems = this.params.maxItems = 3; + this.params.itemValidation = function(item, idx) { + if (idx === 2 && !usStatesTerrByAbbr[item]) { + return { err: that.getText('usStateErr') }; + } + }; + } + else { + if ('undefined' !== typeof opts.minItems) { + tmp = J.isInt(opts.minItems, 0); + if (tmp === false) { + throw new TypeError(e + 'minItems must be ' + + 'a positive integer. Found: ' + + opts.minItems); + } + this.params.minItems = tmp; + } + if ('undefined' !== typeof opts.maxItems) { + tmp = J.isInt(opts.maxItems, 0); + if (tmp === false) { + throw new TypeError(e + 'maxItems must be ' + + 'a positive integer. Found: ' + + opts.maxItems); + } + if (this.params.minItems && + this.params.minItems > tmp) { + + throw new TypeError(e + 'maxItems must be larger ' + + 'than minItems. Found: ' + + tmp + ' < ' + + this.params.minItems); + } + this.params.maxItems = tmp; + } + } + + tmp = function(value) { + var i, len, v, iVal, err; + value = value.split(that.params.listSep); + len = value.length; + if (that.params.minItems && len < that.params.minItems) { + return { err: that.getText('listErr', 'min') }; + } + if (that.params.maxItems && len > that.params.maxItems) { + return { err: that.getText('listErr', 'max') }; + } + if (!len) return value; + iVal = that.params.itemValidation; + i = 0; + v = value[0].trim(); + if (!v) return { err: that.getText('listErr') }; + if (iVal) { + err = iVal(v, 1); + if (err) return err; + } + value[i++] = v; + if (len > 1) { + v = value[1].trim(); + if (!v) return { err: that.getText('listErr') }; + if (iVal) { + err = iVal(v, (i+1)); + if (err) return err; + } + value[i++] = v; + } + if (len > 2) { + v = value[2].trim(); + if (!v) return { err: that.getText('listErr') }; + if (iVal) { + err = iVal(v, (i+1)); + if (err) return err; + } + value[i++] = v; + } + if (len > 3) { + for ( ; i < len ; ) { + v = value[i].trim(); + if (!v) return { err: that.getText('listErr') }; + if (iVal) { + err = iVal(v, (i+1)); + if (err) return err; + } + value[i++] = v; + } + } + return { value: value }; + } + } + + // US_Town,State, Zip Code + + // TODO: add other types, e.g.int and email. + } + + // Variable tmp contains a validation function, either from + // defaults, or from user option. + + this.validation = function(value) { + var res; + res = { value: value }; + if (value.trim() === '') { + if (that.requiredChoice) res.err = that.getText('emptyErr'); + } + else if (tmp) { + res = tmp(value); + } + return res; + }; + + + + // Preprocess + + if (opts.preprocess) { + if ('function' !== typeof opts.preprocess) { + throw new TypeError(e + 'preprocess must be function or ' + + 'undefined. Found: ' + opts.preprocess); + } + this.preprocess = opts.preprocess; + } + else if (opts.preprocess !== false) { + + if (this.type === 'date') { + this.preprocess = function(input) { + var sep, len; + len = input.value.length; + sep = that.params.sep; + if (len === 2) { + if (input.selectionStart === 2) { + if (input.value.charAt(1) !== sep) { + input.value += sep; + } + } + } + else if (len === 5) { + if (input.selectionStart === 5) { + if (input.value.charAt(4) !== sep && + (input.value.split(sep).length - 1) === 1) { + + input.value += sep; + } + } + } + else if (len > this.params.dateLen) { + input.value = + input.value.substring(0, this.params.dateLen); + } + }; + } + else if (this.type === 'list') { + // Add a space after separator, if separator is not space. + if (this.params.listSep.trim() !== '') { + this.preprocess = function(input) { + var sep, len; + len = input.value.length; + sep = that.params.listSep; + if (len > 1 && + len === input.selectionStart && + input.value.charAt(len-1) === sep && + input.value.charAt(len-2) !== sep) { + + input.value += ' '; + } + }; + } + } + } + + // Postprocess. + + if (opts.postprocess) { + if ('function' !== typeof opts.postprocess) { + throw new TypeError(e + 'postprocess must be function or ' + + 'undefined. Found: ' + opts.postprocess); + } + this.postprocess = opts.postprocess; + } + else { + if (this.type === 'date') { + this.postprocess = function(value, valid) { + if (!valid || !value) return value; + return { + value: value, + day: value.substring(0,2), + month: value.substring(3,5), + year: value.subtring(6, value.length) + }; + }; + } + } + + // Validation Speed + if ('undefined' !== typeof opts.validationSpeed) { + tmp = J.isInt(opts.valiadtionSpeed, 0, undefined, true); + if (tmp === false) { + throw new TypeError(e + 'validationSpeed must a non-negative ' + + 'number or undefined. Found: ' + + opts.validationSpeed); + } + this.validationSpeed = tmp; + } + + // MainText, Hint, and other visuals. + + if (opts.mainText) { + if ('string' !== typeof opts.mainText) { + throw new TypeError(e + 'mainText must be string or ' + + 'undefined. Found: ' + opts.mainText); + } + this.mainText = opts.mainText; + } + if ('undefined' !== typeof opts.hint) { + if (false !== opts.hint && 'string' !== typeof opts.hint) { + throw new TypeError(e + 'hint must be a string, false, or ' + + 'undefined. Found: ' + opts.hint); + } + this.hint = opts.hint; + } + else { + this.hint = this.getText('autoHint'); + } + if (opts.placeholder) { + if ('string' !== typeof opts.placeholder) { + throw new TypeError(e + 'placeholder must be string or ' + + 'undefined. Found: ' + opts.placeholder); + } + this.placeholder = opts.placeholder; + } + if (opts.width) { + if ('string' !== typeof opts.width) { + throw new TypeError(e + 'width must be string or ' + + 'undefined. Found: ' + opts.width); + } + this.inputWidth = opts.width; + } + }; + + + /** + * ### CustomInput.append + * + * Implements Widget.append + * + * @see Widget.append + */ + CustomInput.prototype.append = function() { + var that, timeout; + that = this; + + // MainText. + if (this.mainText) { + this.spanMainText = W.append('span', this.bodyDiv, { + className: 'custominput-maintext', + innerHTML: this.mainText + }); + } + // Hint. + if (this.hint) { + W.append('span', this.spanMainText || this.bodyDiv, { + className: 'choicetable-hint', + innerHTML: this.hint + }); + } + + this.input = W.append('input', this.bodyDiv); + if (this.placeholder) this.input.placeholder = this.placeholder; + if (this.inputWidth) this.input.style.width = this.inputWidth; + + this.errorBox = W.append('div', this.bodyDiv, { className: 'errbox' }); + + this.input.oninput = function() { + if (!that.timeBegin) { + that.timeEnd = that.timeBegin = node.timer.getTimeSince('step'); + } + else { + that.timeEnd = node.timer.getTimeSince('step'); + } + if (timeout) clearTimeout(timeout); + if (that.isHighlighted()) that.unhighlight(); + if (that.preprocess) that.preprocess(that.input); + timeout = setTimeout(function() { + var res; + if (that.validation) { + res = that.validation(that.input.value); + if (res.err) that.setError(res.err); + } + }, that.validationSpeed); + }; + this.input.onclick = function() { + if (that.isHighlighted()) that.unhighlight(); + }; + }; + + /** + * ### CustomInput.setError + * + * Set the error msg inside the errorBox and call highlight + * + * @param {string} The error msg (can contain HTML) + * + * @see CustomInput.highlight + * @see CustomInput.errorBox + */ + CustomInput.prototype.setError = function(err) { + this.errorBox.innerHTML = err; + this.highlight(); + }; + + /** + * ### CustomInput.highlight + * + * Highlights the choice table + * + * @param {string} The style for the table's border. + * Default '3px solid red' + * + * @see CustomInput.highlighted + */ + CustomInput.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError('CustomInput.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (!this.input || this.highlighted) return; + this.input.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted', border); + }; + + /** + * ### CustomInput.unhighlight + * + * Removes highlight from the choice table + * + * @see CustomInput.highlighted + */ + CustomInput.prototype.unhighlight = function() { + if (!this.input || this.highlighted !== true) return; + this.input.style.border = ''; + this.highlighted = false; + this.errorBox.innerHTML = ''; + this.emit('unhighlighted'); + }; + + /** + * ### CustomInput.reset + * + * Resets the widget + */ + CustomInput.prototype.reset = function() { + if (this.input) this.input.value = ''; + if (this.isHighlighted()) this.unhighlight(); + this.timeBegin = this.timeEnd = null; + }; + + /** + * ### CustomInput.getValues + * + * Returns the value currently in the input + * + * The postprocess function is called if specified + * + * @param {object} opts Optional. Configures the return value. + * + * @return {mixed} The value in the input + * + * @see CustomInput.verifyChoice + * @see CustomInput.reset + */ + CustomInput.prototype.getValues = function(opts) { + var res, valid; + opts = opts || {}; + res = this.input.value; + res = this.validation ? this.validation(res) : { value: res }; + res.isCorrect = valid = !res.err; + res.timeBegin = this.timeBegin; + res.timeEnd = this.timeEnd; + if (this.postprocess) res.value = this.postprocess(res.value, valid); + if (!valid) { + this.setError(res.err); + res.isCorrect = false; + } + else if (opts.reset) { + this.reset(); + } + res.id = this.id; + return res; + }; + +})(node); + +/** + * # D3 + * Copyright(c) 2015 Stefano Balietti + * MIT Licensed + * + * Integrates nodeGame with the D3 library to plot a real-time chart + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('D3', D3); + node.widgets.register('D3ts', D3ts); + + D3.prototype.__proto__ = node.Widget.prototype; + D3.prototype.constructor = D3; + + // ## Defaults + + D3.defaults = {}; + D3.defaults.id = 'D3'; + D3.defaults.fieldset = { + legend: 'D3 plot' + }; + + + // ## Meta-data + + D3.version = '0.1'; + D3.description = 'Real time plots for nodeGame with d3.js'; + + // ## Dependencies + + D3.dependencies = { + d3: {}, + JSUS: {} + }; + + function D3 (options) { + this.id = options.id || D3.id; + this.event = options.event || 'D3'; + this.svg = null; + + var that = this; + node.on(this.event, function(value) { + that.tick.call(that, value); + }); + } + + D3.prototype.append = function(root) { + this.root = root; + this.svg = d3.select(root).append("svg"); + return root; + }; + + D3.prototype.tick = function() {}; + + // # D3ts + + + // ## Meta-data + + D3ts.id = 'D3ts'; + D3ts.version = '0.1'; + D3ts.description = 'Time series plot for nodeGame with d3.js'; + + // ## Dependencies + D3ts.dependencies = { + D3: {}, + JSUS: {} + }; + + D3ts.prototype.__proto__ = D3.prototype; + D3ts.prototype.constructor = D3ts; + + D3ts.defaults = {}; + + D3ts.defaults.width = 400; + D3ts.defaults.height = 200; + + D3ts.defaults.margin = { + top: 10, + right: 10, + bottom: 20, + left: 40 + }; + + D3ts.defaults.domain = { + x: [0, 10], + y: [0, 1] + }; + + D3ts.defaults.range = { + x: [0, D3ts.defaults.width], + y: [D3ts.defaults.height, 0] + }; + + function D3ts(options) { + var o, x, y; + D3.call(this, options); + + this.options = o = J.merge(D3ts.defaults, options); + this.n = o.n; + this.data = [0]; + + this.margin = o.margin; + + this.width = o.width - this.margin.left - this.margin.right; + this.height = o.height - this.margin.top - this.margin.bottom; + + // Identity function. + this.x = x = d3.scale.linear() + .domain(o.domain.x) + .range(o.range.x); + + this.y = y = d3.scale.linear() + .domain(o.domain.y) + .range(o.range.y); + + // line generator + this.line = d3.svg.line() + .x(function(d, i) { return x(i); }) + .y(function(d, i) { return y(d); }); + } + + D3ts.prototype.init = function(options) { + //D3.init.call(this, options); + + console.log('init!'); + var x = this.x, + y = this.y, + height = this.height, + width = this.width, + margin = this.margin; + + + // Create the SVG and place it in the middle + this.svg.attr("width", width + margin.left + margin.right) + .attr("height", height + margin.top + margin.bottom) + .append("g") + .attr("transform", "translate(" + margin.left + "," + margin.top + + ")"); + + + // Line does not go out the axis + this.svg.append("defs").append("clipPath") + .attr("id", "clip") + .append("rect") + .attr("width", width) + .attr("height", height); + + // X axis + this.svg.append("g") + .attr("class", "x axis") + .attr("transform", "translate(0," + height + ")") + .call(d3.svg.axis().scale(x).orient("bottom")); + + // Y axis + this.svg.append("g") + .attr("class", "y axis") + .call(d3.svg.axis().scale(y).orient("left")); + + this.path = this.svg.append("g") + .attr("clip-path", "url(#clip)") + .append("path") + .data([this.data]) + .attr("class", "line") + .attr("d", this.line); + }; + + D3ts.prototype.tick = function(value) { + this.alreadyInit = this.alreadyInit || false; + if (!this.alreadyInit) { + this.init(); + this.alreadyInit = true; + } + + var x = this.x; + + console.log('tick!'); + + // push a new data point onto the back + this.data.push(value); + + // redraw the line, and slide it to the left + this.path + .attr("d", this.line) + .attr("transform", null); + + // pop the old data point off the front + if (this.data.length > this.n) { + + this.path + .transition() + .duration(500) + .ease("linear") + .attr("transform", "translate(" + x(-1) + ")"); + + this.data.shift(); + + } + }; + +})(node); + +/** + * # DebugInfo + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Display information about the state of a player + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + var Table = W.Table; + + node.widgets.register('DebugInfo', DebugInfo); + + // ## Meta-data + + DebugInfo.version = '0.6.2'; + DebugInfo.description = 'Display basic info a client\'s status.'; + + DebugInfo.title = 'Debug Info'; + DebugInfo.className = 'debuginfo'; + + // ## Dependencies + + DebugInfo.dependencies = { + Table: {} + }; + + /** + * ## DebugInfo constructor + * + * `DebugInfo` displays information about the state of a player + */ + function DebugInfo() { + + /** + * ### DebugInfo.table + * + * The `Table` which holds the information + * + * @See nodegame-window/Table + */ + this.table = null; + + /** + * ### DebugInfo.interval + * + * The interval checking node properties + */ + this.interval = null; + + /** + * ### DebugInfo.intervalTime + * + * The frequency of update of the interval. Default: 1000 + */ + this.intervalTime = 1000; + } + + // ## DebugInfo methods + + /** + * ### DebugInfo.init + * + * Appends widget to `this.bodyDiv` and calls `this.updateAll` + * + * @see DebugInfo.updateAll + */ + DebugInfo.prototype.init = function(options) { + var that; + if ('number' === typeof options.intervalTime) { + this.intervalTime = options.intervalTime; + } + + that = this; + this.on('destroyed', function() { + clearInterval(that.interval); + that.interval = null; + node.silly('DebugInfo destroyed.'); + }); + }; + + /** + * ### DebugInfo.append + * + * Appends widget to `this.bodyDiv` and calls `this.updateAll` + * + * @see DebugInfo.updateAll + */ + DebugInfo.prototype.append = function() { + var that; + + this.table = new Table(); + this.bodyDiv.appendChild(this.table.table); + + this.updateAll(); + that = this; + this.interval = setInterval(function() { + that.updateAll(); + }, this.intervalTime); + }; + + /** + * ### DebugInfo.updateAll + * + * Updates information in `this.table` + */ + DebugInfo.prototype.updateAll = function() { + var stage, stageNo, stageId, playerId; + var stageLevel, stateLevel, winLevel; + var errMsg, connected, treatment; + var tmp, miss; + + if (!this.bodyDiv) { + node.err('DebugInfo.updateAll: bodyDiv not found.'); + return; + } + + miss = '-'; + + stageId = miss; + stageNo = miss; + + stage = node.game.getCurrentGameStage(); + if (stage) { + tmp = node.game.plot.getStep(stage); + stageId = tmp ? tmp.id : '-'; + stageNo = stage.toString(); + } + + stageLevel = J.getKeyByValue(node.constants.stageLevels, + node.game.getStageLevel()); + + stateLevel = J.getKeyByValue(node.constants.stateLevels, + node.game.getStateLevel()); + + winLevel = J.getKeyByValue(node.constants.windowLevels, + W.getStateLevel()); + + + playerId = node.player ? node.player.id : miss; + + errMsg = node.errorManager.lastErr || miss; + + treatment = node.game.settings && node.game.settings.treatmentName ? + node.game.settings.treatmentName : miss; + + connected = node.socket.connected ? 'yes' : 'no'; + + this.table.clear(true); + this.table.addRow(['Treatment: ', treatment]); + this.table.addRow(['Connected: ', connected]); + this.table.addRow(['Player Id: ', playerId]); + this.table.addRow(['Stage No: ', stageNo]); + this.table.addRow(['Stage Id: ', stageId]); + this.table.addRow(['Stage Lvl: ', stageLevel]); + this.table.addRow(['State Lvl: ', stateLevel]); + this.table.addRow(['Players : ', node.game.pl.size()]); + this.table.addRow(['Win Lvl: ', winLevel]); + this.table.addRow(['Win Loads: ', W.areLoading]); + this.table.addRow(['Last Err: ', errMsg]); + + this.table.parse(); + + }; + +})(node); + +/** + * # DebugWall + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a wall where all incoming and outgoing messages are printed + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('DebugWall', DebugWall); + + // ## Meta-data + + DebugWall.version = '1.0.0'; + DebugWall.description = 'Intercepts incoming and outgoing messages, and ' + + 'logs and prints them numbered and timestamped. Warning! Modifies ' + + 'core functions, therefore its usage in production is ' + + 'not recommended.'; + + DebugWall.title = 'Debug Wall'; + DebugWall.className = 'debugwall'; + + // ## Dependencies + + DebugWall.dependencies = { + JSUS: {} + }; + + /** + * ## DebugWall constructor + * + * Creates a new DebugWall oject + */ + function DebugWall() { + + /** + * ### DebugWall.buttonsDiv + * + * Div contains controls for the display info inside the wall. + */ + this.buttonsDiv = null; + + /** + * ### DebugWall.hidden + * + * Keep tracks of what is hidden in the wall + */ + this.hiddenTypes = {}; + + /** + * ### DebugWall.counterIn + * + * Counts number of incoming message printed on wall + */ + this.counterIn = 0; + + /** + * ### DebugWall.counterOut + * + * Counts number of outgoing message printed on wall + */ + this.counterOut = 0; + + /** + * ### DebugWall.counterLog + * + * Counts number of log entries printed on wall + */ + this.counterLog = 0; + + /** + * ### DebugWall.wall + * + * The table element in which to write + */ + this.wall = null; + + /** + * ### DebugWall.wallDiv + * + * The div element containing the wall (for scrolling) + */ + this.wallDiv = null; + + /** + * ### DebugWall.origMsgInCb + * + * The original function that receives incoming msgs + */ + this.origMsgInCb = null; + + /** + * ### DebugWall.origMsgOutCb + * + * The original function that sends msgs + */ + this.origMsgOutCb = null; + + /** + * ### DebugWall.origLogCb + * + * The original log callback + */ + this.origLogCb = null; + } + + // ## DebugWall methods + + /** + * ### DebugWall.init + * + * Initializes the instance + * + * @param {object} options Optional. Configuration options + * + * - msgIn: If FALSE, incoming messages are ignored. + * - msgOut: If FALSE, outgoing messages are ignored. + * - log: If FALSE, log messages are ignored. + * - hiddenTypes: An object containing what is currently hidden + * in the wall. + */ + DebugWall.prototype.init = function(options) { + var that; + that = this; + if (options.msgIn !== false) { + this.origMsgInCb = node.socket.onMessage; + node.socket.onMessage = function(msg) { + that.write('in', that.makeTextIn(msg)); + that.origMsgInCb.call(node.socket, msg); + }; + } + if (options.msgOut !== false) { + this.origMsgOutCb = node.socket.send; + node.socket.send = function(msg) { + that.write('out', that.makeTextOut(msg)); + that.origMsgOutCb.call(node.socket, msg); + }; + } + if (options.log !== false) { + this.origLogCb = node.log; + node.log = function(txt, level, prefix) { + that.write(level || 'info', + that.makeTextLog(txt, level, prefix)); + that.origLogCb.call(node, txt, level, prefix); + }; + } + + if (options.hiddenTypes) { + if ('object' !== typeof hiddenTypes) { + throw new TypeError('DebugWall.init: hiddenTypes must be ' + + 'object. Found: ' + hiddenTypes); + } + this.hiddenTypes = hiddenTypes; + } + + this.on('destroyed', function() { + if (that.origLogCb) node.log = that.origLogCb; + if (that.origMsgOutCb) node.socket.send = that.origMsgOutCb; + if (that.origMsgInCb) node.socket.onMessage = that.origMsgInCb; + }); + + }; + + DebugWall.prototype.append = function() { + var displayIn, displayOut, displayLog, that; + var btnGroup, cb, div; + this.buttonsDiv = W.add('div', this.bodyDiv, { + className: 'wallbuttonsdiv' + }); + + btnGroup = document.createElement('div'); + btnGroup.role = 'group'; + btnGroup['aria-label'] = 'Toggle visibility'; + btnGroup.className = 'btn-group'; + + displayIn = W.add('button', btnGroup, { + innerHTML: 'Incoming', + className: 'btn btn-secondary' + }); + displayOut = W.add('button', btnGroup, { + innerHTML: 'Outgoing', + className: 'btn btn-secondary' + }); + displayLog = W.add('button', btnGroup, { + innerHTML: 'Log', + className: 'btn btn-secondary' + }); + + this.buttonsDiv.appendChild(btnGroup); + + that = this; + + cb = function(type) { + var items, i, vis, className; + className = 'wall_' + type; + items = that.wall.getElementsByClassName(className); + vis = items[0].style.display === '' ? 'none' : ''; + for (i = 0; i < items.length; i++) { + items[i].style.display = vis; + } + that.hiddenTypes[type] = !!vis; + }; + + displayIn.onclick = function() { cb('in'); }; + displayOut.onclick = function() { cb('out'); }; + displayLog.onclick = function() { cb('log'); }; + + this.wallDiv = W.add('div', this.bodyDiv, { className: 'walldiv' }); + this.wall = W.add('table', this.wallDiv); + }; + + /** + * ### DebugWall.write + * + * Writes argument as first entry of this.wall if document is fully loaded + * + * @param {string} type 'in', 'out', or 'log' (different levels) + * @param {string} text The text to write + */ + DebugWall.prototype.shouldHide = function(type, text) { + return this.hiddenTypes[type]; + }; + /** + * ### DebugWall.write + * + * Writes argument as first entry of this.wall if document is fully loaded + * + * @param {string} type 'in', 'out', or 'log' (different levels) + * @param {string} text The text to write + */ + DebugWall.prototype.write = function(type, text) { + var spanContainer, spanDots, spanExtra, counter, className; + var limit; + var TR, TDtext; + if (this.isAppended()) { + + counter = type === 'in' ? ++this.counterIn : + (type === 'out' ? ++this.counterOut : ++this.counterLog); + + limit = 200; + className = 'wall_' + type; + TR = W.add('tr', this.wall, { className: className }); + if (type !== 'in' && type !== 'out') TR.className += ' wall_log'; + + if (this.shouldHide(type, text)) TR.style.display = 'none'; + + W.add('td', TR, { innerHTML: counter }); + W.add('td', TR, { innerHTML: type }); + W.add('td', TR, { innerHTML: J.getTimeM()}); + TDtext = W.add('td', TR); + + if (text.length > limit) { + spanContainer = W.add('span', TDtext, { + className: className + '_click' , + innerHTML: text.substr(0, limit) + }); + spanExtra = W.add('span', spanContainer, { + className: className + '_extra', + innerHTML: text.substr(limit, text.length), + id: 'wall_' + type + '_' + counter, + style: { display: 'none' } + + }); + spanDots = W.add('span', spanContainer, { + className: className + '_dots', + innerHTML: ' ...', + id: 'wall_' + type + '_' + counter + }); + + spanContainer.onclick = function() { + if (spanDots.style.display === 'none') { + spanDots.style.display = ''; + spanExtra.style.display = 'none'; + } + else { + spanDots.style.display = 'none'; + spanExtra.style.display = ''; + } + }; + } + else { + spanContainer = W.add('span', TDtext, { + innerHTML: text + }); + } + this.wallDiv.scrollTop = this.wallDiv.scrollHeight; + } + else { + node.warn('Wall not appended, cannot write.'); + } + }; + + DebugWall.prototype.makeTextIn = function(msg) { + var text, d; + d = new Date(msg.created); + text = d.getHours() + ':' + d.getMinutes() + ':' + d.getSeconds() + + ':' + d.getMilliseconds(); + text += ' | ' + msg.to + ' | ' + msg.target + + ' | ' + msg.action + ' | ' + msg.text + ' | ' + msg.data; + return text; + }; + + + DebugWall.prototype.makeTextOut = function(msg) { + var text; + text = msg.from + ' | ' + msg.target + ' | ' + msg.action + ' | ' + + msg.text + ' | ' + msg.data; + return text; + }; + + DebugWall.prototype.makeTextLog = function(text, level, prefix) { + return text; + }; + +})(node); + +/** + * # DisconnectBox + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Shows a disconnect button + * + * // TODO: add light on/off for connected/disconnected status + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('DisconnectBox', DisconnectBox); + + // ## Meta-data + + DisconnectBox.version = '0.2.3'; + DisconnectBox.description = + 'Visually display current, previous and next stage of the game.'; + + DisconnectBox.title = false; + DisconnectBox.panel = false; + DisconnectBox.className = 'disconnectbox'; + + DisconnectBox.texts = { + leave: 'Leave Experiment', + left: 'You Left' + }; + + // ## Dependencies + + DisconnectBox.dependencies = {}; + + /** + * ## DisconnectBox constructor + * + * `DisconnectBox` displays current, previous and next stage of the game + */ + function DisconnectBox() { + // ### DisconnectBox.disconnectButton + // The button for disconnection + this.disconnectButton = null; + // ### DisconnectBox.ee + // The event emitter with whom the events are registered + this.ee = null; + } + + // ## DisconnectBox methods + + /** + * ### DisconnectBox.append + * + * Appends widget to `this.bodyDiv` and writes the stage + * + * @see DisconnectBox.writeStage + */ + DisconnectBox.prototype.append = function() { + var that = this; + this.disconnectButton = W.add('button', this.bodyDiv, { + innerHTML: this.getText('leave'), + className: 'btn btn-lg' + }); + + this.disconnectButton.onclick = function() { + that.disconnectButton.disabled = true; + node.socket.disconnect(); + that.disconnectButton.innerHTML = that.getText('left'); + }; + }; + + DisconnectBox.prototype.listeners = function() { + var that = this; + + this.ee = node.getCurrentEventEmitter(); + this.ee.on('SOCKET_DISCONNECT', function DBdiscon() { + // console.log('DB got socket_diconnect'); + }); + + this.ee.on('SOCKET_CONNECT', function DBcon() { + // console.log('DB got socket_connect'); + if (that.disconnectButton.disabled) { + that.disconnectButton.disabled = false; + that.disconnectButton.innerHTML = that.getText('leave'); + } + }); + + this.on('destroyed', function() { + that.ee.off('SOCKET_DISCONNECT', 'DBdiscon'); + that.ee.off('SOCKET_CONNECT', 'DBcon'); + }); + }; + + +})(node); + +/** + * # DoneButton + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates a button that if pressed emits node.done() + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('DoneButton', DoneButton); + + // ## Meta-data + + DoneButton.version = '1.0.0'; + DoneButton.description = 'Creates a button that if ' + + 'pressed emits node.done().'; + + DoneButton.title = false; + DoneButton.className = 'donebutton'; + DoneButton.texts.done = 'Done'; + + // ## Dependencies + + DoneButton.dependencies = { + JSUS: {} + }; + + /** + * ## DoneButton constructor + * + * Creates a new instance of DoneButton + * + * @param {object} options Optional. Configuration options. + * If a `button` option is specified, it sets it as the clickable + * button. All other options are passed to the init method. + * + * @see DoneButton.init + */ + function DoneButton(options) { + var that; + that = this; + + /** + * ### DoneButton.button + * + * The HTML element triggering node.done() when pressed + */ + if ('object' === typeof options.button) { + this.button = options.button; + } + else if ('undefined' === typeof options.button) { + this.button = document.createElement('input'); + this.button.type = 'button'; + } + else { + throw new TypeError('DoneButton constructor: options.button must ' + + 'be object or undefined. Found: ' + + options.button); + } + + this.button.onclick = function() { + var res; + res = node.done(); + if (res) that.disable(); + }; + } + + // ## DoneButton methods + + /** + * ### DoneButton.init + * + * Initializes the instance + * + * Available options are: + * + * - id: id of the HTML button, or false to have none. Default: + * DoneButton.className + * - className: the className of the button (string, array), or false + * to have none. Default bootstrap classes: 'btn btn-lg btn-primary' + * - text: the text on the button. Default: DoneButton.text + * + * @param {object} options Optional. Configuration options + */ + DoneButton.prototype.init = function(options) { + var tmp; + options = options || {}; + + //Button + if ('undefined' === typeof options.id) { + tmp = DoneButton.className; + } + else if ('string' === typeof options.id) { + tmp = options.id; + } + else if (false === options.id) { + tmp = ''; + } + else { + throw new TypeError('DoneButton.init: options.id must ' + + 'be string, false, or undefined. Found: ' + + options.id); + } + this.button.id = tmp; + + // Button className. + if ('undefined' === typeof options.className) { + tmp = 'btn btn-lg btn-primary'; + } + else if (options.className === false) { + tmp = ''; + } + else if ('string' === typeof options.className) { + tmp = options.className; + } + else if (J.isArray(options.className)) { + tmp = options.className.join(' '); + } + else { + throw new TypeError('DoneButton.init: options.className must ' + + 'be string, array, or undefined. Found: ' + + options.className); + } + this.button.className = tmp; + + // Button text. + this.button.value = 'string' === typeof options.text ? + options.text : this.getText('done'); + }; + + DoneButton.prototype.append = function() { + this.bodyDiv.appendChild(this.button); + }; + + DoneButton.prototype.listeners = function() { + var that = this; + + // This is normally executed after the PLAYING listener of + // GameWindow where lockUnlockedInputs takes place. + // In case of a timeup, the donebutton will be locked and + // then unlocked by GameWindow, but otherwise it must be + // done here. + node.on('PLAYING', function() { + var prop, step; + step = node.game.getCurrentGameStage(); + prop = node.game.plot.getProperty(step, 'donebutton'); + if (prop === false || (prop && prop.enableOnPlaying === false)) { + // It might be disabled already, but we do it again. + that.disable(); + } + else { + // It might be enabled already, but we do it again. + that.enable(); + } + if ('string' === typeof prop) that.button.value = prop; + else if (prop && prop.text) that.button.value = prop.text; + }); + }; + + /** + * ### DoneButton.disable + * + * Disables the done button + */ + DoneButton.prototype.disable = function() { + this.button.disabled = 'disabled'; + }; + + /** + * ### DoneButton.enable + * + * Enables the done button + */ + DoneButton.prototype.enable = function() { + this.button.disabled = false; + }; + +})(node); + +/** + * # EmailForm + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Displays a form to input email + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('EmailForm', EmailForm); + + // ## Meta-data + + EmailForm.version = '0.10.0'; + EmailForm.description = 'Displays a configurable email form.'; + + EmailForm.title = 'Email'; + EmailForm.className = 'emailform'; + + EmailForm.texts.label = 'Enter your email:'; + EmailForm.texts.errString = 'Not a valid email address, ' + + 'please correct it and submit it again.'; + + // ## Dependencies + + EmailForm.dependencies = { JSUS: {} }; + + /** + * ## EmailForm constructor + * + * `EmailForm` sends a feedback message to the server + * + * @param {object} options configuration option + */ + function EmailForm(options) { + + /** + * ### EmailForm.onsubmit + * + * Options passed to `getValues` when the submit button is pressed + * + * @see Feedback.getValues + */ + if (!options.onsubmit) { + this.onsubmit = { emailOnly: true, say: true, updateUI: true }; + } + else if ('object' === typeof options.onsubmit) { + this.onsubmit = options.onsubmit; + } + else { + throw new TypeError('EmailForm constructor: options.onsubmit ' + + 'must be string or object. Found: ' + + options.onsubmit); + } + + /** + * ### EmailForm._email + * + * Internal storage of the value of the email + * + * This value is used when the form has not been created yet + * + * @see EmailForm.createForm + */ + this._email = options.email || null; + + /** + * ### EmailForm.attempts + * + * Invalid emails tried + */ + this.attempts = []; + + /** + * ### EmailForm.timeInput + * + * Time when the email was inserted (first character, last attempt) + */ + this.timeInput = null; + + /** + * ### EmailForm.formElement + * + * The email's HTML form + */ + this.formElement = null; + + /** + * ### EmailForm.inputElement + * + * The email's HTML input form + */ + this.inputElement = null; + + /** + * ### EmailForm.buttonElement + * + * The email's HTML submit button + */ + this.buttonElement = null; + } + + // ## EmailForm methods + + EmailForm.prototype.createForm = function() { + var that; + var formElement, labelElement, inputElement, buttonElement; + + that = this; + + formElement = document.createElement('form'); + formElement.className = 'emailform-form'; + + labelElement = document.createElement('label'); + labelElement.innerHTML = this.getText('label'); + + inputElement = document.createElement('input'); + inputElement.setAttribute('type', 'text'); + inputElement.setAttribute('placeholder', 'Email'); + inputElement.className = 'emailform-input form-control'; + + buttonElement = document.createElement('input'); + buttonElement.setAttribute('type', 'submit'); + buttonElement.setAttribute('value', 'Submit email'); + buttonElement.className = 'btn btn-lg btn-primary ' + + 'emailform-submit'; + + formElement.appendChild(labelElement); + formElement.appendChild(inputElement); + formElement.appendChild(buttonElement); + + // Add listeners on input form. + J.addEvent(formElement, 'submit', function(event) { + event.preventDefault(); + that.getValues(that.onsubmit); + }, true); + J.addEvent(formElement, 'input', function(event) { + if (!that.timeInput) that.timeInput = J.now(); + if (that.isHighlighted()) that.unhighlight(); + }, true); + + + // Store references. + this.formElement = formElement; + this.inputElement = inputElement; + this.buttonElement = buttonElement; + + // If a value was previously set, insert it in the form. + if (this._email) this.formElement.value = this._email; + this._email = null; + + return formElement; + }; + + /** + * ### EmailForm.verifyInput + * + * Verify current email, updates interface, and optionally marks attempt + * + * @param {boolean} markAttempt Optional. If TRUE, the current email + * is added to the attempts array. Default: TRUE + * @param {boolean} updateUI Optional. If TRUE, the interface is updated. + * Default: FALSE + * + * @return {boolean} TRUE, if the email is valid + * + * @see EmailForm.getValues + * @see getEmail + */ + EmailForm.prototype.verifyInput = function(markAttempt, updateUI) { + var email, res; + email = getEmail.call(this); + res = J.isEmail(email); + if (res && updateUI) { + if (this.inputElement) this.inputElement.disabled = true; + if (this.buttonElement) { + this.buttonElement.disabled = true; + this.buttonElement.value = 'Sent!'; + } + } + else { + if (updateUI && this.buttonElement) { + this.buttonElement.value = this.getText('errString'); + } + if ('undefined' === typeof markAttempt || markAttempt) { + this.attempts.push(email); + } + } + return res; + }; + + /** + * ### EmailForm.append + * + * Appends widget to this.bodyDiv + */ + EmailForm.prototype.append = function() { + this.createForm(); + this.bodyDiv.appendChild(this.formElement); + }; + + /** + * ### EmailForm.setValues + * + * Set the value of the email input form + */ + EmailForm.prototype.setValues = function(options) { + var email; + options = options || {}; + if (!options.email) email = J.randomEmail(); + else email = options.email; + + if (!this.inputElement) this._email = email; + else this.inputElement.value = email; + + this.timeInput = J.now(); + }; + + /** + * ### EmailForm.getValues + * + * Returns the email and paradata + * + * @param {object} opts Optional. Configures the return value. + * Available optionts: + * + * - emailOnly: If TRUE, returns just the email (default: FALSE), + * - verify: If TRUE, check if the email is valid (default: TRUE), + * - reset: If TRUTHY and the email is valid, then it resets + * the email value before returning (default: FALSE), + * - markAttempt: If TRUE, getting the value counts as an attempt + * (default: TRUE), + * - updateUI: If TRUE, the UI (form, input, button) is updated. + * Default: FALSE. + * - highlight: If TRUE, if email is not the valid, widget is + * is highlighted. Default: (updateUI || FALSE). + * - say: If TRUE, and the email is valid, then it sends + * a data msg. Default: FALSE. + * - sayAnyway: If TRUE, it sends a data msg regardless of the + * validity of the email. Default: FALSE. + * + * @return {string|object} The email, and optional paradata + * + * @see EmailForm.sendValues + * @see EmailForm.verifyInput + * @see getEmail + */ + EmailForm.prototype.getValues = function(opts) { + var email, res; + opts = opts || {}; + + email = getEmail.call(this); + + if (opts.verify !== false) { + res = this.verifyInput(opts.markAttempt, opts.updateUI); + } + + // Only value. + if (!opts.emailOnly) { + email = { + time: this.timeInput, + email: email, + attempts: this.attempts, + valid: res + }; + } + + if (res === false) { + if (opts.updateUI || opts.highlight) this.highlight(); + this.timeInput = null; + } + + // Send the message. + if ((opts.say && res) || opts.sayAnyway) { + this.sendValues({ values: email }); + } + + if (opts.reset) this.reset(); + + return email; + }; + + /** + * ### EmailForm.sendValues + * + * Sends a DATA message with label 'email' with current email and paradata + * + * @param {object} opts Optional. Options to pass to the `getValues` + * method. Additional options: + * + * - values: actual values to send, instead of the return + * value of `getValues` + * - to: recipient of the message. Default: 'SERVER' + * + * @return {string|object} The email, and optional paradata + * + * @see EmailForm.getValues + */ + EmailForm.prototype.sendValues = function(opts) { + var values; + opts = opts || { emailOnly: true }; + values = opts.values || this.getValues(opts); + node.say('email', opts.to || 'SERVER', values); + return values; + }; + + /** + * ### EmailForm.highlight + * + * Highlights the email form + * + * @param {string} The style for the form border. Default: '1px solid red' + * + * @see EmailForm.highlighted + */ + EmailForm.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError('EmailForm.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (!this.inputElement || this.highlighted === true) return; + this.inputElement.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted', border); + }; + + /** + * ### EmailForm.unhighlight + * + * Removes highlight from the form + * + * @see EmailForm.highlighted + */ + EmailForm.prototype.unhighlight = function() { + if (!this.inputElement || this.highlighted !== true) return; + this.inputElement.style.border = ''; + this.highlighted = false; + this.emit('unhighlighted'); + }; + + /** + * ### EmailForm.reset + * + * Resets email and collected paradata + */ + EmailForm.prototype.reset = function() { + this.attempts = []; + this.timeInput = null; + this._email = null; + + if (this.inputElement) this.inputElement.value = ''; + if (this.isHighlighted()) this.unhighlight(); + }; + + // ## Helper methods. + + /** + * ### getEmail + * + * Returns the value of the email in form or in `_email` + * + * Must be invoked with right context + * + * @return {string|null} The value of the email, if any + */ + function getEmail() { + return this.inputElement ? this.inputElement.value : this._email; + } + +})(node); + +/** + * # EndScreen + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Creates an interface to display final earnings, exit code, etc. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + // Register the widget in the widgets collection. + node.widgets.register('EndScreen', EndScreen); + + // ## Add Meta-data + + EndScreen.version = '0.6.0'; + EndScreen.description = 'Game end screen. With end game message, ' + + 'email form, and exit code.'; + + EndScreen.title = 'End Screen'; + EndScreen.className = 'endscreen'; + + EndScreen.texts.headerMessage = 'Thank you for participating!'; + EndScreen.texts.message = 'You have now completed this task ' + + 'and your data has been saved. ' + + 'Please go back to the Amazon Mechanical Turk ' + + 'web site and submit the HIT.'; + EndScreen.texts.contactQuestion = 'Would you like to be contacted again' + + 'for future experiments? If so, leave' + + 'your email here and press submit: '; + EndScreen.texts.totalWin = 'Your total win:'; + EndScreen.texts.exitCode = 'Your exit code:'; + EndScreen.texts.errTotalWin = 'Error: invalid total win.'; + EndScreen.texts.errExitCode = 'Error: invalid exit code.'; + EndScreen.texts.copyButton = 'Copy'; + EndScreen.texts.exitCopyMsg = 'Exit code copied to clipboard.'; + EndScreen.texts.exitCopyError = 'Failed to copy exit code. Please copy it' + + ' manually.'; + + // ## Dependencies + + // Checked when the widget is created. + EndScreen.dependencies = { + JSUS: {}, + Feedback: {}, + EmailForm: {} + }; + + /** + * ## EndScreen constructor + * + * Creates a new instance of EndScreen + * + * @param {object} options Configuration options + * + * @see EndScreen.init + */ + function EndScreen(options) { + + /** + * ### EndScreen.showEmailForm + * + * If true, the email form is shown + * + * Default: true + */ + if (options.email === false) { + options.showEmailForm = false; + } + else if ('undefined' === typeof options.showEmailForm) { + this.showEmailForm = true; + } + else if ('boolean' === typeof options.showEmailForm) { + this.showEmailForm = options.showEmailForm; + } + else { + throw new TypeError('EndScreen constructor: ' + + 'options.showEmailForm ' + + 'must be boolean or undefined. ' + + 'Found: ' + options.showEmailForm); + } + + /** + * ### EndScreen.showFeedbackForm + * + * If true, the feedback form is shown + * + * Default: true + */ + if (options.feedback === false) { + options.showFeedbackForm = false; + } + else if ('undefined' === typeof options.showFeedbackForm) { + this.showFeedbackForm = true; + } + else if ('boolean' === typeof options.showFeedbackForm) { + this.showFeedbackForm = options.showFeedbackForm; + } + else { + throw new TypeError('EndScreen constructor: ' + + 'options.showFeedbackForm ' + + 'must be boolean or undefined. ' + + 'Found: ' + options.showFeedbackForm); + } + + /** + * ### EndScreen.showTotalWin + * + * If true, the total win is shown + * + * Default: true + */ + if (options.totalWin === false) { + options.showTotalWin = false; + } + else if ('undefined' === typeof options.showTotalWin) { + this.showTotalWin = true; + } + else if ('boolean' === typeof options.showTotalWin) { + this.showTotalWin = options.showTotalWin; + } + else { + throw new TypeError('EndScreen constructor: ' + + 'options.showTotalWin ' + + 'must be boolean or undefined. ' + + 'Found: ' + options.showTotalWin); + } + + /** + * ### EndScreen.showExitCode + * + * If true, the exit code is shown + * + * Default: true + */ + if (options.exitCode === false) { + options.showExitCode !== false + } + else if ('undefined' === typeof options.showExitCode) { + this.showExitCode = true; + } + else if ('boolean' === typeof options.showExitCode) { + this.showExitCode = options.showExitCode; + } + else { + throw new TypeError('EndScreen constructor: ' + + 'options.showExitCode ' + + 'must be boolean or undefined. ' + + 'Found: ' + options.showExitCode); + } + + /** + * ### EndScreen.totalWinCurrency + * + * The currency displayed after totalWin + * + * Default: 'USD' + */ + if ('undefined' === typeof options.totalWinCurrency) { + this.totalWinCurrency = 'USD'; + } + else if ('string' === typeof options.totalWinCurrency && + options.totalWinCurrency.trim() !== '') { + + this.totalWinCurrency = options.totalWinCurrency; + } + else { + throw new TypeError('EndScreen constructor: ' + + 'options.totalWinCurrency must be undefined ' + + 'or a non-empty string. Found: ' + + options.totalWinCurrency); + } + + /** + * ### EndScreen.totalWinCb + * + * If defined, the return value is displayed inside the totalWin box + * + * Accepts two parameters: a data object (as sent from server), and + * the reference to the EndScreen. + */ + if (options.totalWinCb) { + if ('function' === typeof options.totalWinCb) { + this.totalWinCb = options.totalWinCb; + } + else { + throw new TypeError('EndScreen constructor: ' + + 'options.totalWinCb ' + + 'must be function or undefined. ' + + 'Found: ' + options.totalWinCb); + } + } + + /** + * ### EndScreen.emailForm + * + * EmailForm widget element + * + * @see EmailForm + */ + this.emailForm = null; + + /** + * ### EndScreen.feedback + * + * Feedback widget element + * + * @see Feedback + */ + this.feedback = null; + + /** + * ### EndScreen.endScreenElement + * + * Endscreen HTML element + * + * Default: an HTML element, + * null initially, element added on append() + */ + this.endScreenHTML = null; + } + + EndScreen.prototype.init = function(options) { + if (this.showEmailForm && !this.emailForm) { + this.emailForm = node.widgets.get('EmailForm', J.mixin({ + label: this.getText('contactQuestion'), + onsubmit: { say: true, emailOnly: true, updateUI: true }, + storeRef: false + }, options.email)); + } + + if (this.showFeedbackForm) { + this.feedback = node.widgets.get('Feedback', J.mixin( + { storeRef: false }, + options.feedback)); + } + }; + + // Implements the Widget.append method. + EndScreen.prototype.append = function() { + this.endScreenHTML = this.makeEndScreen(); + this.bodyDiv.appendChild(this.endScreenHTML); + }; + + /** + * ### EndScreen.makeEndScreen + * + * Builds up the end screen (HTML + nested widgets) + */ + EndScreen.prototype.makeEndScreen = function() { + var endScreenElement; + var headerElement, messageElement; + var totalWinElement, totalWinParaElement, totalWinInputElement; + var exitCodeElement, exitCodeParaElement, exitCodeInputElement; + var exitCodeBtn, exitCodeGroup; + var that = this; + + endScreenElement = document.createElement('div'); + endScreenElement.className = 'endscreen'; + + headerElement = document.createElement('h1'); + headerElement.innerHTML = this.getText('headerMessage'); + endScreenElement.appendChild(headerElement); + + messageElement = document.createElement('p'); + messageElement.innerHTML = this.getText('message'); + endScreenElement.appendChild(messageElement); + + if (this.showTotalWin) { + totalWinElement = document.createElement('div'); + + totalWinParaElement = document.createElement('p'); + totalWinParaElement.innerHTML = '' + + this.getText('totalWin') + + ''; + + totalWinInputElement = document.createElement('input'); + totalWinInputElement.className = 'endscreen-total form-control'; + totalWinInputElement.setAttribute('disabled', 'true'); + + totalWinParaElement.appendChild(totalWinInputElement); + totalWinElement.appendChild(totalWinParaElement); + + endScreenElement.appendChild(totalWinElement); + this.totalWinInputElement = totalWinInputElement; + } + + if (this.showExitCode) { + exitCodeElement = document.createElement('div'); + exitCodeElement.className = 'input-group'; + + exitCodeParaElement = document.createElement('span'); + exitCodeParaElement.innerHTML = '' + + this.getText('exitCode') + ''; + + exitCodeInputElement = document.createElement('input'); + exitCodeInputElement.id = 'exit_code'; + exitCodeInputElement.className = 'endscreen-exit-code ' + + 'form-control'; + exitCodeInputElement.setAttribute('disabled', 'true'); + + exitCodeGroup = document.createElement('span'); + exitCodeGroup.className = 'input-group-btn'; + + exitCodeBtn = document.createElement('button'); + exitCodeBtn.className = 'btn btn-default endscreen-copy-btn'; + exitCodeBtn.innerHTML = this.getText('copyButton'); + exitCodeBtn.type = 'button'; + exitCodeBtn.onclick = function() { + that.copy(exitCodeInputElement.value); + }; + + exitCodeGroup.appendChild(exitCodeBtn); + endScreenElement.appendChild(exitCodeParaElement); + exitCodeElement.appendChild(exitCodeGroup); + exitCodeElement.appendChild(exitCodeInputElement); + + endScreenElement.appendChild(exitCodeElement); + this.exitCodeInputElement = exitCodeInputElement; + } + + if (this.showEmailForm) { + node.widgets.append(this.emailForm, endScreenElement, { + title: false, + panel: false + }); + } + + if (this.showFeedbackForm) { + node.widgets.append(this.feedback, endScreenElement, { + title: false, + panel: false + }); + } + + return endScreenElement; + }; + + // Implements the Widget.listeners method. + EndScreen.prototype.listeners = function() { + var that; + that = this; + node.on.data('WIN', function(message) { + that.updateDisplay(message.data); + }); + }; + + EndScreen.prototype.copy = function(text) { + var inp = document.createElement('input'); + try { + document.body.appendChild(inp); + inp.value = text; + inp.select(); + document.execCommand('copy', false); + inp.remove(); + alert(this.getText('exitCopyMsg')); + } catch (err) { + alert(this.getText('exitCopyError')); + } + }; + + /** + * ### EndScreen.updateDisplay + * + * Updates the display + * + * @param {object} data An object containing the info to update. Format: + * - total: The total won. + * - exit: An exit code. + */ + EndScreen.prototype.updateDisplay = function(data) { + var preWin, totalWin, totalRaw, exitCode; + var totalHTML, exitCodeHTML, ex, err; + + if (this.totalWinCb) { + totalWin = this.totalWinCb(data, this); + } + else { + if ('undefined' === typeof data.total && + 'undefined' === typeof data.totalRaw) { + + throw new Error('EndScreen.updateDisplay: data.total and ' + + 'data.totalRaw cannot be both undefined.'); + } + + if ('undefined' !== typeof data.total) { + totalWin = J.isNumber(data.total, 0); + if (totalWin === false) { + node.err('EndScreen.updateDisplay: invalid data.total: ' + + data.total); + totalWin = this.getText('errTotalWin'); + err = true; + } + } + + if (data.partials) { + if (!J.isArray(data.partials)) { + node.err('EndScreen error, invalid partials win: ' + + data.partials); + } + else { + preWin = data.partials.join(' + '); + } + } + + if ('undefined' !== typeof data.totalRaw) { + if (preWin) preWin += ' = '; + else preWin = ''; + preWin += data.totalRaw; + + // Get Exchange Rate. + ex = 'undefined' !== typeof data.exchangeRate ? + data.exchangeRate : node.game.settings.EXCHANGE_RATE; + + // If we have an exchange rate, check if we have a totalRaw. + if ('undefined' !== typeof ex) preWin += '*' + ex; + + // Need to compute total manually. + if ('undefined' === typeof totalWin) { + totalRaw = J.isNumber(data.totalRaw, 0); + totalWin = parseFloat(ex*data.totalRaw).toFixed(2); + totalWin = J.isNumber(totalWin, 0); + if (totalWin === false) { + node.err('EndScreen.updateDisplay: invalid : ' + + 'totalWin calculation from totalRaw.'); + totalWin = this.getText('errTotalWin'); + err = true; + } + } + if (!err) totalWin = preWin + ' = ' + totalWin; + } + + if (!err) totalWin += ' ' + this.totalWinCurrency; + } + + exitCode = data.exit; + if ('string' !== typeof exitCode) { + node.err('EndScreen error, invalid exit code: ' + exitCode); + exitCode = this.getText('errExitCode'); + } + + totalHTML = this.totalWinInputElement; + exitCodeHTML = this.exitCodeInputElement; + + if (totalHTML && this.showTotalWin) { + totalHTML.value = totalWin; + } + + if (exitCodeHTML && this.showExitCode) { + exitCodeHTML.value = exitCode; + } + }; + +})(node); + +/** + * # Feedback + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Sends a feedback message to the server + * + * www.nodegame.org + * + * TODO: rename css class feedback-char-count + * TODO: words and chars count without contraints, just show. + * TODO: shows all constraints in gray before the textarea. + */ +(function(node) { + + "use strict"; + + node.widgets.register('Feedback', Feedback); + + // ## Meta-data + + Feedback.version = '1.4.0'; + Feedback.description = 'Displays a configurable feedback form'; + + Feedback.title = 'Feedback'; + Feedback.className = 'feedback'; + + Feedback.texts = { + autoHint: function(w) { + var res, res2; + if (w.minChars && w.maxChars) { + res = 'beetween ' + w.minChars + ' and ' + w.maxChars + + ' characters'; + } + else if (w.minChars) { + res = 'at least ' + w.minChars + ' character'; + if (w.minChars > 1) res += 's'; + } + else if (w.maxChars) { + res = 'at most ' + w.maxChars + ' character'; + if (w.maxChars > 1) res += 's'; + } + if (w.minWords && w.maxWords) { + res2 = 'beetween ' + w.minWords + ' and ' + w.maxWords + + ' words'; + } + else if (w.minWords) { + res2 = 'at least ' + w.minWords + ' word'; + if (w.minWords > 1) res += 's'; + } + else if (w.maxWords) { + res2 = 'at most ' + w.maxWords + ' word'; + if (w.maxWords > 1) res += 's'; + } + if (res) { + res = '(' + res;; + if (res2) res += ', and ' + res2; + return res + ')'; + } + else if (res2) { + return '(' + res2 + ')'; + } + return false; + }, + submit: 'Submit feedback', + label: 'Any feedback? Let us know here:', + sent: 'Sent!', + counter: function(w, param) { + var res; + res = param.chars ? ' character' : ' word'; + if (param.len !== 1) res += 's'; + if (param.needed) res += ' needed'; + else if (param.over) res += ' over'; + else res += ' remaining'; + return res; + } + }; + + // Colors for missing, excess or ok. + var colNeeded, colOver, colRemain; + colNeeded = '#a32020'; // #f2dede'; + colOver = '#a32020'; // #f2dede'; + colRemain = '#78b360'; // '#dff0d8'; + + // ## Dependencies + + Feedback.dependencies = { + JSUS: {} + }; + + /** + * ## Feedback constructor + * + * `Feedback` sends a feedback message to the server + * + * @param {object} options Optional. Configuration options + */ + function Feedback(options) { + var tmp; + + if ('undefined' !== typeof options.maxLength) { + console.log('***Feedback constructor: maxLength is deprecated, ' + + 'use maxChars instead***'); + options.maxChars = options.maxLength; + } + if ('undefined' !== typeof options.minLength) { + console.log('***Feedback constructor: minLength is deprecated, ' + + 'use minChars instead***'); + options.minChars = options.minLength; + } + + /** + * ### Feedback.mainText + * + * The main text introducing the choices + * + * @see Feedback.spanMainText + */ + this.mainText = null; + + /** + * ### Feedback.hint + * + * An additional text with information about how to select items + * + * If not specified, it may be auto-filled, e.g. '(pick 2)'. + * + * @see Feedback.texts.autoHint + */ + this.hint = null; + + /** + * ### Feedback.spanMainText + * + * The span containing the main text + */ + this.spanMainText = null; + + /** + * ### Feedback.maxChars + * + * The maximum character length for feedback to be submitted + * + * Default: 800 + */ + if ('undefined' === typeof options.maxChars) { + this.maxChars = 800; + } + else { + tmp = J.isInt(options.maxChars, 0); + if (tmp !== false) { + this.maxChars = options.maxChars; + } + else { + throw new TypeError('Feedback constructor: maxChars ' + + 'must be an integer >= 0 or undefined. ' + + 'Found: ' + options.maxChars); + } + } + + /** + * ### Feedback.minChars + * + * The minimum character length for feedback to be submitted + * + * If minChars = 0, then there is no minimum length checked. + * + * Default: 1 + */ + if ('undefined' === typeof options.minChars) { + this.minChars = 1; + } + else { + tmp = J.isInt(options.minChars, 0, undefined, true); + if (tmp !== false) { + this.minChars = options.minChars; + } + else { + throw new TypeError('Feedback constructor: minChars ' + + 'must be an integer >= 0 or undefined. ' + + 'Found: ' + options.minChars); + } + } + + /** + * ### Feedback.maxWords + * + * The maximum number of words for feedback to be submitted + * + * Set to 0 for no checks. + * + * Default: 0 + */ + if ('undefined' === typeof options.maxWords) { + this.maxWords = 0; + } + else { + tmp = J.isInt(options.maxWords, 0, undefined, true); + if (tmp !== false) { + this.maxWords = options.maxWords; + } + else { + throw new TypeError('Feedback constructor: maxWords ' + + 'must be an integer >= 0 or undefined. ' + + 'Found: ' + options.maxWords); + } + } + + /** + * ### Feedback.minWords + * + * The minimum number of words for feedback to be submitted + * + * If minChars = 0, then there is no minimum checked. + * + * Default: 0 + */ + if ('undefined' === typeof options.minWords) { + this.minWords = 0; + } + else { + tmp = J.isInt(options.minWords, 0, undefined, true); + if (tmp !== false) { + this.minWords = options.minWords; + + // Checking if words and characters limit are compatible. + if (this.maxChars) { + tmp = (this.maxChars+1)/2; + if (this.minWords > tmp) { + + throw new TypeError('Feedback constructor: minWords ' + + 'cannot be larger than ' + + '(maxChars+1)/2. Found: ' + + this.minWords + ' > ' + tmp); + } + } + } + else { + throw new TypeError('Feedback constructor: minWords ' + + 'must be an integer >= 0 or undefined. ' + + 'Found: ' + options.minWords); + } + } + + /** + * ### Feedback.rows + * + * The number of initial rows of the texarea + * + * Default: 3 + */ + if ('undefined' === typeof options.rows) { + this.rows = 3; + } + else if (J.isInt(options.rows, 0) !== false) { + this.rows = options.rows; + } + else { + throw new TypeError('Feedback constructor: rows ' + + 'must be an integer > 0 or undefined. ' + + 'Found: ' + options.rows); + } + + /** + * ### Feedback.maxAttemptLength + * + * The maximum character length for an attempt to submit feedback + * + * Attempts are stored in the attempts array. This allows to store + * longer texts than accepts feedbacks + * + * Default: Max(2000, maxChars) + */ + if ('undefined' === typeof options.maxAttemptLength) { + this.maxAttemptLength = 2000; + } + else if (J.isNumber(options.maxAttemptLength, 0) !== false) { + this.maxAttemptLength = Math.max(this.maxChars, + options.maxAttemptLength); + } + else { + throw new TypeError('Feedback constructor: ' + + 'options.maxAttemptLength must be a number ' + + '>= 0 or undefined. Found: ' + + options.maxAttemptLength); + } + + /** + * ### Feedback.showSubmit + * + * If TRUE, the submit button is shown + * + * Default: true + * + * @see Feedback.submitButton + */ + this.showSubmit = 'undefined' === typeof options.showSubmit ? + true : !!options.showSubmit; + + /** + * ### Feedback.onsubmit + * + * Options passed to `getValues` when the submit button is pressed + * + * @see Feedback.getValues + */ + if (!options.onsubmit) { + this.onsubmit = { feedbackOnly: true, say: true, updateUI: true }; + } + else if ('object' === typeof options.onsubmit) { + this.onsubmit = options.onsubmit; + } + else { + throw new TypeError('Feedback constructor: onsubmit ' + + 'must be string or object. Found: ' + + options.onsubmit); + } + + /** + * ### Feedback._feedback + * + * Internal storage of the value of the feedback + * + * This value is used when the form has not been created yet + */ + this._feedback = options.feedback || null; + + /** + * ### Feedback.attempts + * + * Invalid feedbacks tried + */ + this.attempts = []; + + /** + * ### Feedback.timeInputBegin + * + * Time when feedback was inserted (first character, last attempt) + */ + this.timeInputBegin = null; + + /** + * ### Feedback.feedbackForm + * + * The HTML form element containing the textarea + */ + this.feedbackForm = null; + + /** + * ### Feedback.textareaElement + * + * The HTML textarea element containing the feedback + */ + this.textareaElement = null; + + /** + * ### Feedback.charCounter + * + * The HTML span element containing the characters count + */ + this.charCounter = null; + + /** + * ### Feedback.wordCounter + * + * The HTML span element containing the words count + */ + this.wordCounter = null; + + /** + * ### Feedback.submitButton + * + * The HTML submit button + */ + this.submitButton = null; + + } + + // ## Feedback methods + + // TODO: move all initialization here from constructor. + Feedback.prototype.init = function(options) { + // Set the mainText, if any. + if ('string' === typeof options.mainText) { + this.mainText = options.mainText; + } + else if ('undefined' === typeof options.mainText) { + this.mainText = this.getText('label'); + } + else { + throw new TypeError('Feedback.init: options.mainText must ' + + 'be string or undefined. Found: ' + + options.mainText); + } + + // Set the hint, if any. + if ('string' === typeof options.hint || false === options.hint) { + this.hint = options.hint; + } + else if ('undefined' !== typeof options.hint) { + throw new TypeError('Feedback.init: options.hint must ' + + 'be a string, false, or undefined. Found: ' + + options.hint); + } + else { + // Returns undefined if there are no constraints. + this.hint = this.getText('autoHint'); + } + }; + + /** + * ### Feedback.verifyFeedback + * + * Verify feedback and optionally marks attempt and updates interface + * + * @param {boolean} markAttempt Optional. If TRUE, the current feedback + * is added to the attempts array (if too long, may be truncateed). + * Default: TRUE + * @param {boolean} updateUI Optional. If TRUE, the interface is updated. + * Default: FALSE + * + * @return {boolean} TRUE, if the feedback is valid + * + * @see Feedback.getValues + * @see getFeedback + */ + Feedback.prototype.verifyFeedback = function(markAttempt, updateUI) { + var feedback, length, res; + var submitButton, charCounter, wordCounter, tmp; + var updateCharCount, updateCharColor, updateWordCount, updateWordColor; + + feedback = getFeedback.call(this); + length = feedback ? feedback.length : 0; + + submitButton = this.submitButton; + charCounter = this.charCounter; + wordCounter = this.wordCounter; + + res = true; + + if (length < this.minChars) { + res = false; + tmp = this.minChars - length; + updateCharCount = tmp + this.getText('counter', { + chars: true, + needed: true, + len: tmp + }); + updateCharColor = colNeeded; + } + else if (this.maxChars && length > this.maxChars) { + res = false; + tmp = length - this.maxChars; + updateCharCount = tmp + this.getText('counter', { + chars: true, + over: true, + len: tmp + }); + updateCharColor = colOver; + } + else { + tmp = this.maxChars - length; + updateCharCount = tmp + this.getText('counter', { + chars: true, + len: tmp + }); + updateCharColor = colRemain; + } + + if (wordCounter) { + // kudos: https://css-tricks.com/build-word-counter-app/ + // word count using \w metacharacter - + // replacing this with .* to match anything between word + // boundaries since it was not taking 'a' as a word. + // this is a masterstroke - to count words with any number + // of hyphens as one word + // [-?(\w+)?]+ looks for hyphen and a word (we make + // both optional with ?). + at the end makes it a repeated pattern + // \b is word boundary metacharacter + tmp = feedback ? feedback.match(/\b[-?(\w+)?]+\b/gi) : 0; + length = tmp ? tmp.length : 0; + if (length < this.minWords) { + res = false; + tmp = tmp = this.minWords - length; + updateWordCount = tmp + this.getText('counter', { + needed: true, + len: tmp + }); + updateWordColor = colNeeded; + } + else if (this.maxWords && length > this.maxWords) { + res = false; + tmp = length - this.maxWords; + updateWordCount = tmp + this.getText('counter', { + over: true, + len: tmp + }); + updateWordColor = colOver; + } + else { + tmp = this.maxWords - length; + updateWordCount = tmp + this.getText('counter', { + len: tmp + }); + updateWordColor = colRemain; + } + } + + if (updateUI) { + if (submitButton) submitButton.disabled = !res; + if (charCounter) { + charCounter.style.backgroundColor = updateCharColor; + charCounter.innerHTML = updateCharCount; + } + if (wordCounter) { + wordCounter.style.backgroundColor = updateWordColor; + wordCounter.innerHTML = updateWordCount; + } + } + + if (!res && ('undefined' === typeof markAttempt || markAttempt)) { + if (length > this.maxAttemptLength) { + feedback = feedback.substr(0, this.maxAttemptLength); + } + this.attempts.push(feedback); + } + return res; + }; + + /** + * ### Feedback.append + * + * Appends widget to this.bodyDiv + */ + Feedback.prototype.append = function() { + var that, label; + that = this; + + // this.feedbackForm = W.get('div', { className: 'feedback' }); + + this.feedbackForm = W.append('form', this.bodyDiv, { + className: 'feedback-form' + }); + + // MainText. + if (this.mainText) { + this.spanMainText = W.append('span', this.feedbackForm, { + className: 'feedback-maintext', + innerHTML: this.mainText + }); + } + // Hint. + if (this.hint) { + W.append('span', this.spanMainText || this.feedbackForm, { + className: 'feedback-hint', + innerHTML: this.hint + }); + } + + this.textareaElement = W.append('textarea', this.feedbackForm, { + className: 'feedback-textarea form-control', + type: 'text', + rows: this.rows + }); + + if (this.showSubmit) { + this.submit = W.append('input', this.feedbackForm, { + className: 'btn btn-lg btn-primary', + type: 'submit', + value: this.getText('submit') + }); + + // Add listeners. + J.addEvent(this.feedbackForm, 'submit', function(event) { + event.preventDefault(); + that.getValues(that.onsubmit); + }); + } + + this.showCounters(); + + J.addEvent(this.feedbackForm, 'input', function(event) { + if (that.isHighlighted()) that.unhighlight(); + that.verifyFeedback(false, true); + }); + J.addEvent(this.feedbackForm, 'click', function(event) { + if (that.isHighlighted()) that.unhighlight(); + }); + + // Check it once at the beginning to initialize counter. + this.verifyFeedback(false, true); + }; + + /** + * ### Feedback.setValues + * + * Set the value of the feedback + */ + Feedback.prototype.setValues = function(options) { + var feedback; + options = options || {}; + if (!options.feedback) { + feedback = J.randomString(J.randomInt(0, this.maxChars), + 'aA_1'); + } + else { + feedback = options.feedback; + } + + if (!this.textareaElement) this._feedback = feedback; + else this.textareaElement.value = feedback; + + this.timeInputBegin = J.now(); + }; + + /** + * ### Feedback.getValues + * + * Returns the feedback and paradata + * + * @param {object} opts Optional. Configures the return value. + * Available optionts: + * + * - feedbackOnly:If TRUE, returns just the feedback (default: FALSE), + * - keepBreaks: If TRUE, returns a value where all line breaks are + * substituted with HTML
tags (default: FALSE) + * - verify: If TRUE, check if the feedback is valid (default: TRUE), + * - reset: If TRUTHY and the feedback is valid, then it resets + * the feedback value before returning (default: FALSE), + * - markAttempt: If TRUE, getting the value counts as an attempt + * (default: TRUE), + * - updateUI: If TRUE, the UI (form, input, button) is updated. + * Default: FALSE. + * - highlight: If TRUE, if feedback is not the valid, widget is + * is highlighted. Default: (updateUI || FALSE). + * - say: If TRUE, and the feedback is valid, then it sends + * a data msg. Default: FALSE. + * - sayAnyway: If TRUE, it sends a data msg regardless of the + * validity of the feedback. Default: FALSE. + * + * @return {string|object} The feedback, and optional paradata + * + * @see Feedback.sendValues + * @see Feedback.verifyFeedback + * @see getFeedback + */ + Feedback.prototype.getValues = function(opts) { + var feedback, feedbackBr, res; + + opts = opts || {}; + + feedback = getFeedback.call(this); + + if (opts.keepBreaks) feedback = feedback.replace(/\n\r?/g, '
'); + + if (opts.verify !== false) res = this.verifyFeedback(opts.markAttempt, + opts.updateUI); + + if (res === false && + (opts.updateUI || opts.highlight)) this.highlight(); + + // Only value. + if (!opts.feedbackOnly) { + feedback = { + timeBegin: this.timeInputBegin, + feedback: feedback, + attempts: this.attempts, + valid: res, + isCorrect: res + }; + } + + // Send the message. + if ((opts.say && res) || opts.sayAnyway) { + this.sendValues({ values: feedback }); + if (opts.updateUI) { + this.submitButton.setAttribute('value', this.getText('sent')); + this.submitButton.disabled = true; + this.textareaElement.disabled = true; + } + } + + if (opts.reset) this.reset(); + + return feedback; + }; + + /** + * ### Feedback.sendValues + * + * Sends a DATA message with label 'feedback' with feedback and paradata + * + * @param {object} opts Optional. Options to pass to the `getValues` + * method. Additional options: + * + * - values: actual values to send, instead of the return + * value of `getValues` + * - to: recipient of the message. Default: 'SERVER' + * + * @return {string|object} The feedback, and optional paradata + * + * @see Feedback.getValues + */ + Feedback.prototype.sendValues = function(opts) { + var values; + opts = opts || { feedbackOnly: true }; + values = opts.values || this.getValues(opts); + node.say('feedback', opts.to || 'SERVER', values); + return values; + }; + + /** + * ### Feedback.highlight + * + * Highlights the feedback form + * + * @param {string} The style for the form border. Default: '1px solid red' + * + * @see Feedback.highlighted + */ + Feedback.prototype.highlight = function(border) { + if (border && 'string' !== typeof border) { + throw new TypeError('Feedback.highlight: border must be ' + + 'string or undefined. Found: ' + border); + } + if (!this.isAppended() || this.highlighted === true) return; + this.textareaElement.style.border = border || '3px solid red'; + this.highlighted = true; + this.emit('highlighted', border); + }; + + /** + * ### Feedback.unhighlight + * + * Removes highlight from the form + * + * @see Feedback.highlighted + */ + Feedback.prototype.unhighlight = function() { + if (!this.isAppended() || this.highlighted !== true) return; + this.textareaElement.style.border = ''; + this.highlighted = false; + this.emit('unhighlighted'); + }; + + /** + * ### Feedback.reset + * + * Resets feedback and collected paradata + */ + Feedback.prototype.reset = function() { + this.attempts = []; + this.timeInputBegin = null; + this._feedback = null; + + if (this.textareaElement) this.textareaElement.value = ''; + if (this.isHighlighted()) this.unhighlight(); + }; + + /** + * ### Feedback.disable + * + * Disables texarea and submit button (if present) + */ + Feedback.prototype.disable = function() { + // TODO: This gets off when WaitScreen locks all inputs. + // if (this.disabled === true) return; + if (!this.textareaElement || this.textareaElement.disabled) return; + this.disabled = true; + if (this.submitElement) this.submitElement.disabled = true; + this.textareaElement.disabled = true; + this.emit('disabled'); + }; + + /** + * ### Feedback.enable + * + * Enables texarea and submit button (if present) + * + */ + Feedback.prototype.enable = function() { + // TODO: This gets off when WaitScreen locks all inputs. + // if (this.disabled === false || !this.textareaElement) return; + if (!this.textareaElement || !this.textareaElement.disabled) return; + this.disabled = false; + if (this.submitElement) this.submitElement.disabled = false; + this.textareaElement.disabled = false; + this.emit('enabled'); + }; + + /** + * ### Feedback.showCounters + * + * Shows the character counter + * + * If not existing before, it creates it. + * + * @see Feedback.charCounter + */ + Feedback.prototype.showCounters = function() { + if (!this.charCounter) { + if (this.minChars || this.maxChars) { + this.charCounter = W.append('span', this.feedbackForm, { + className: 'feedback-char-count badge', + innerHTML: this.maxChars + }); + } + } + else { + this.charCounter.style.display = ''; + } + if (!this.wordCounter) { + if (this.minWords || this.maxWords) { + this.wordCounter = W.append('span', this.feedbackForm, { + className: 'feedback-char-count badge', + innerHTML: this.maxWords + }); + if (this.charCounter) { + this.wordCounter.style['margin-left'] = '10px'; + } + } + } + else { + this.wordCounter.style.display = ''; + } + }; + + /** + * ### Feedback.hideCounters + * + * Hides the character counter + */ + Feedback.prototype.hideCounters = function() { + if (this.charCounter) this.charCounter.style.display = 'none'; + if (this.wordCounter) this.wordCounter.style.display = 'none'; + }; + + // ## Helper functions. + + /** + * ### getFeedback + * + * Returns the value of the feedback textarea or in `_feedback` + * + * Must be invoked with right context + * + * @return {string|null} The value of the feedback, if any + */ + function getFeedback() { + var out; + out = this.textareaElement ? + this.textareaElement.value : this._feedback; + return out ? out.trim() : out; + } + +})(node); + +/** + * # LanguageSelector + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Manages and displays information about languages available and selected + * + * @TODO: bubble event in case of buttons (now there are many listeners). + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('LanguageSelector', LanguageSelector); + + // ## Meta-data + + LanguageSelector.version = '0.6.2'; + LanguageSelector.description = 'Display information about the current ' + + 'language and allows to change language.'; + LanguageSelector.title = 'Language'; + LanguageSelector.className = 'languageselector'; + + LanguageSelector.texts.loading = 'Loading language information...'; + + // ## Dependencies + + LanguageSelector.dependencies = { + JSUS: {} + }; + + /** + * ## LanguageSelector constructor + * + * Manages the setting and display of the language used + * + * @param {object} options Optional. Configuration options + * + * @see Player.lang + */ + function LanguageSelector(options) { + var that = this; + + this.options = options; + + /** + * ### LanguageSelector.availableLanguages + * + * Object containing an object per availble language. + * + * The language object contains at least the following properties: + * + * - `name`: Name of the language in English. + * - `nativeName`: Native name of the language + * - `shortName`: An abbreviation for the language, also determines the + * path to the context files for this language. + * + * The key for each language object is its `shortName`. + * + * @see Player.lang + */ + this.availableLanguages = { + en: { + name: 'English', + nativeName: 'English', + shortName: 'en' + } + }; + + /** + * ### LanguageSelector.currentLanguageIndex + * + * A reference to the currently used language + * + * @see LanguageSelector.availableLanguages + */ + this.currentLanguage = null; + + /** + * ### LanguageSelector.buttonListLength + * + * Specifies maximum number of radio buttons used in selection tool + */ + this.buttonListLength = null; + + /** + * ### LanguageSelector.displayForm + * + * The form in which the widget displays the language information + */ + this.displayForm = null; + + /** + * ### LanguageSelector.optionsLabel + * + * Array containing the labels for the language selection optionsDisplay + */ + this.optionsLabel = {}; + + /** + * ### LanguageSelector.optionsDisplay + * + * Array containing the optionsDisplay for the language selection + */ + this.optionsDisplay = {}; + + /** + * ### LanguageSelector.loadingDiv + * + * Div displaying information on whether the languages have been loaded + */ + this.loadingDiv = null; + + /** + * ### LanguageSelector.languagesLoaded + * + * Flag indicating whether languages have been loaded from server + */ + this.languagesLoaded = false; + + /** + * ## LanguageSelector.usingButtons + * + * Flag indicating if the interface should have buttons + * + * Default: TRUE. + */ + this.usingButtons = true; + + /** + * ## LanguageSelector.updatePlayer + * + * Specifies when updating the player + * + * Available options: + * + * - false: alias for 'never', + * - 'never': never notifies, + * - 'onselect': each time a selection is made, + * - 'ondone': when current step is done. + * + * Default: 'ondone' + */ + this.updatePlayer = 'ondone'; + + /** + * ## LanguageSelector.setUriPrefix + * + * If TRUE, the Window URI prefix is updated when the player is updated + * + * Default: TRUE. + * + * @see GameWindow.setUriPrefix + */ + this.setUriPrefix = true; + + /** + * ## LanguageSelector.notifyServer + * + * If TRUE, a message is sent to the server when the player is updated + * + * Default: TRUE. + */ + this.notifyServer = true; + + /** + * ### LanguageSelector.onLangCallback + * + * Function to be called when languages have been loaded + * + * Initializes form displaying the information as well + * as the optionsDisplay and their labels. + * Initializes language to English. + * Forwards to `LanguageSelector.onLangCallbackExtension` at the very + * end. + * + * @param {object} msg GameMsg + * + * @see LanguageSelector.setLanguage + */ + this.onLangCallback = function(msg) { + var language; + + // Clear display. + while (that.displayForm.firstChild) { + that.displayForm.removeChild(that.displayForm.firstChild); + } + + // Initialize widget. + that.availableLanguages = msg.data; + if (that.usingButtons) { + + // Creates labeled buttons. + for (language in msg.data) { + if (msg.data.hasOwnProperty(language)) { + that.optionsLabel[language] = W.get('label', { + id: language + 'Label', + 'for': language + 'RadioButton' + }); + + that.optionsDisplay[language] = W.get('input', { + id: language + 'RadioButton', + type: 'radio', + name: 'languageButton', + value: msg.data[language].name + }); + + that.optionsDisplay[language].onclick = + makeSetLanguageOnClick(language); + + that.optionsLabel[language].appendChild( + that.optionsDisplay[language]); + that.optionsLabel[language].appendChild( + document.createTextNode( + msg.data[language].nativeName)); + W.add('br', that.displayForm); + that.optionsLabel[language].className = + 'unselectedButtonLabel'; + that.displayForm.appendChild( + that.optionsLabel[language]); + } + } + } + else { + + that.displaySelection = W.get('select', 'selectLanguage'); + for (language in msg.data) { + that.optionsLabel[language] = + document.createTextNode(msg.data[language].nativeName); + that.optionsDisplay[language] = W.get('option', { + id: language + 'Option', + value: language + }); + that.optionsDisplay[language].appendChild( + that.optionsLabel[language]); + that.displaySelection.appendChild( + that.optionsDisplay[language]); + + } + that.displayForm.appendChild(that.displaySelection); + that.displayForm.onchange = function() { + that.setLanguage(that.displaySelection.value, + that.updatePlayer === 'onselect'); + }; + } + + that.loadingDiv.style.display = 'none'; + that.languagesLoaded = true; + + // Initialize with current value inside player object, + // or default to English. Does not update the player object yet. + that.setLanguage(node.player.lang.shortName || 'en', false); + + // Extension point. + if (that.onLangCallbackExtension) { + that.onLangCallbackExtension(msg); + that.onLangCallbackExtension = null; + } + + function makeSetLanguageOnClick(langStr) { + return function() { + that.setLanguage(langStr, that.updatePlayer === 'onselect'); + }; + } + }; + + /** + * ### LanguageSelector.onLangCallbackExtension + * + * Extension point to `LanguageSelector.onLangCallback` + * + * @see LanguageSelector.onLangCallback + */ + this.onLangCallbackExtension = null; + } + + // ## LanguageSelector methods + + /** + * ### LanguageSelector.init + * + * Initializes the widget + * + * @param {object} options Optional. Configuration options + * + * @see LanguageSelector.onLangCallback + */ + LanguageSelector.prototype.init = function(options) { + J.mixout(options, this.options); + this.options = options; + + if ('undefined' !== typeof this.options.usingButtons) { + this.usingButtons = !!this.options.usingButtons; + } + + if ('undefined' !== typeof this.options.notifyServer) { + if (false === this.options.notifyServer) { + this.options.notifyServer = 'never'; + } + else if ('string' === typeof this.options.notifyServer) { + if ('never' === this.options.notifyServer || + 'onselect' === this.options.notifyServer || + 'ondone' === this.options.notifyServer) { + + this.notifyServer = this.options.notifyServer; + } + else { + throw new Error('LanguageSelector.init: invalid value ' + + 'for notifyServer: "' + + this.options.notifyServer + '". Valid ' + + 'values: "never","onselect", "ondone".'); + } + } + else { + throw new Error('LanguageSelector.init: options.notifyServer ' + + 'must be ' + + this.options.notifyServer); + } + } + + if ('undefined' !== typeof this.options.setUriPrefix) { + this.setUriPrefix = !!this.options.setUriPrefix; + } + + // Register listener. + // TODO: should it be moved into the listeners method? + // TODO: calling init twice will add it twice. + node.on.lang(this.onLangCallback); + + // Display initialization. + this.displayForm = W.get('form', 'radioButtonForm'); + this.loadingDiv = W.add('div', this.displayForm); + this.loadingDiv.innerHTML = this.getText('loading'); + + this.loadLanguages(); + }; + + LanguageSelector.prototype.append = function() { + this.bodyDiv.appendChild(this.displayForm); + }; + + /** + * ### LanguageSelector.setLanguage + * + * Sets language within the widget and globally and updates the display + * + * @param {string} langName shortName of language to be set + * @param {boolean} updatePlayer If FALSE, the language is set only + * inside the widget, and no changes are made to the player object. + * Default: TRUE + * + * @see NodeGameClient.setLanguage + */ + LanguageSelector.prototype.setLanguage = function(langName, updatePlayer) { + + if (this.usingButtons) { + + // Uncheck current language button and change className of label. + if (this.currentLanguage !== null && + this.currentLanguage !== this.availableLanguages[langName] ) { + + this.optionsDisplay[this.currentLanguage].checked = + 'unchecked'; + this.optionsLabel[this.currentLanguage].className = + 'unselectedButtonLabel'; + } + } + + // Set current language index. + this.currentLanguage = langName; + + if (this.usingButtons) { + // Check language button and change className of label. + this.optionsDisplay[this.currentLanguage].checked = 'checked'; + this.optionsLabel[this.currentLanguage].className = + 'selectedButtonLabel'; + } + else { + this.displaySelection.value = this.currentLanguage; + } + + // Update node.player. + if (updatePlayer !== false) { + node.setLanguage(this.availableLanguages[this.currentLanguage], + this.setUriPrefix, this.notifyServer); + } + }; + + /** + * ### LanguageSelector.updateAvailableLanguages + * + * Updates available languages asynchronously + * + * @param {object} options Optional. Configuration options + */ + LanguageSelector.prototype.updateAvalaibleLanguages = function(options) { + if (options && options.callback) { + this.onLangCallbackExtension = options.callback; + } + node.socket.send(node.msg.create({ + target: "LANG", + to: "SERVER", + action: "get" + })); + }; + + /** + * ### LanguageSelector.loadLanguages + * + * Loads languages once from server + * + * @param {object} options Optional. Configuration options + * + * @see LanguageSelector.updateAvalaibleLanguages + */ + LanguageSelector.prototype.loadLanguages = function(options) { + if (!this.languagesLoaded) this.updateAvalaibleLanguages(options); + else if (options && options.callback) options.callback(); + }; + + /** + * ### LanguageSelector.listeners + * + * Implements Widget.listeners + */ + LanguageSelector.prototype.listeners = function() { + var that; + that = this; + node.events.step.on('REALLY_DONE', function() { + if (that.updatePlayer === 'ondone') { + node.setLanguage(that.availableLanguages[that.currentLanguage], + that.setUriPrefix, that.notifyServer); + } + }); + }; + +})(node); + +/** + * # MoneyTalks + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Displays a box for formatting earnings ("money") in currency + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('MoneyTalks', MoneyTalks); + + // ## Meta-data + + MoneyTalks.version = '0.4.0'; + MoneyTalks.description = 'Displays the earnings of a player.'; + + MoneyTalks.title = 'Earnings'; + MoneyTalks.className = 'moneytalks'; + + // ## Dependencies + + MoneyTalks.dependencies = { + JSUS: {} + }; + + /** + * ## MoneyTalks constructor + * + * `MoneyTalks` displays the earnings ("money") of the player so far + * + * @param {object} options Optional. Configuration options + * which is forwarded to MoneyTalks.init. + * + * @see MoneyTalks.init + */ + function MoneyTalks(options) { + + /** + * ### MoneyTalks.spanCurrency + * + * The SPAN which holds information on the currency + */ + this.spanCurrency = null; + + /** + * ### MoneyTalks.spanMoney + * + * The SPAN which holds information about the money earned so far + */ + this.spanMoney = null; + + /** + * ### MoneyTalks.currency + * + * String describing the currency + */ + this.currency = 'ECU'; + + /** + * ### MoneyTalks.money + * + * Currently earned money + */ + this.money = 0; + + /** + * ### MoneyTalks.precicison + * + * Precision of floating point number to display + */ + this.precision = 2; + + /** + * ### MoneyTalks.showCurrency + * + * If TRUE, the currency is displayed after the money + */ + this.showCurrency = true; + + /** + * ### MoneyTalks.currencyClassname + * + * Class name to be attached to the currency span + */ + this.classnameCurrency = 'moneytalkscurrency'; + + /** + * ### MoneyTalks.currencyClassname + * + * Class name to be attached to the money span + */ + this.classnameMoney = 'moneytalksmoney'; + } + + // ## MoneyTalks methods + + /** + * ### MoneyTalks.init + * + * Initializes the widget + * + * @param {object} options Optional. Configuration options. + * + * The options object can have the following attributes: + * + * - `currency`: The name of currency. + * - `money`: Initial amount of money earned. + * - `precision`: How mamy floating point digits to use. + * - `currencyClassName`: Class name to be set for this.spanCurrency. + * - `moneyClassName`: Class name to be set for this.spanMoney. + * - `showCurrency`: Flag whether the name of currency is to be displayed. + */ + MoneyTalks.prototype.init = function(options) { + if ('string' === typeof options.currency) { + this.currency = options.currency; + } + if ('undefined' !== typeof options.showCurrency) { + this.showCurrency = !!options.showCurrency; + } + if ('number' === typeof options.money) { + this.money = options.money; + } + if ('number' === typeof options.precision) { + this.precision = options.precision; + } + if ('string' === typeof options.MoneyClassName) { + this.classnameMoney = options.MoneyClassName; + } + if ('string' === typeof options.currencyClassName) { + this.classnameCurrency = options.currencyClassName; + } + }; + + MoneyTalks.prototype.append = function() { + if (!this.spanMoney) { + this.spanMoney = document.createElement('span'); + } + if (!this.spanCurrency) { + this.spanCurrency = document.createElement('span'); + } + if (!this.showCurrency) this.spanCurrency.style.display = 'none'; + + this.spanMoney.className = this.classnameMoney; + this.spanCurrency.className = this.classnameCurrency; + + this.spanCurrency.innerHTML = this.currency; + this.spanMoney.innerHTML = this.money; + + this.bodyDiv.appendChild(this.spanMoney); + this.bodyDiv.appendChild(this.spanCurrency); + }; + + MoneyTalks.prototype.listeners = function() { + var that = this; + node.on('MONEYTALKS', function(amount, clear) { + that.update(amount, clear); + }); + }; + + /** + * ### MoneyTalks.update + * + * Updates the display and the count of available "money" + * + * @param {string|number} amount Amount to add to current value of money + * @param {boolean} clear Optional. If TRUE, money will be set to 0 + * before adding the new amount + * + * @see MoneyTalks.money + * @see MonetyTalks.spanMoney + */ + MoneyTalks.prototype.update = function(amount, clear) { + var parsedAmount; + parsedAmount = J.isNumber(amount); + if (parsedAmount === false) { + node.err('MoneyTalks.update: invalid amount: ' + amount); + return; + } + if (clear) this.money = 0; + this.money += parsedAmount; + this.spanMoney.innerHTML = this.money.toFixed(this.precision); + }; + + /** + * ### MoneyTalks.getValues + * + * Returns the current value of "money" + * + * @see MoneyTalks.money + */ + MoneyTalks.prototype.getValues = function() { + return this.money; + }; + +})(node); + +/** + * # MoodGauge + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Displays an interface to query users about mood, emotions and well-being + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('MoodGauge', MoodGauge); + + // ## Meta-data + + MoodGauge.version = '0.3.0'; + MoodGauge.description = 'Displays an interface to measure mood ' + + 'and emotions.'; + + MoodGauge.title = 'Mood Gauge'; + MoodGauge.className = 'moodgauge'; + + MoodGauge.texts.mainText = 'Thinking about yourself and how you normally' + + ' feel, to what extent do you generally feel: '; + + // ## Dependencies + MoodGauge.dependencies = { + JSUS: {} + }; + + /** + * ## MoodGauge constructor + * + * Creates a new instance of MoodGauge + * + * @param {object} options Optional. Configuration options + * which is forwarded to MoodGauge.init. + * + * @see MoodGauge.init + */ + function MoodGauge(options) { + + /** + * ### MoodGauge.methods + * + * List of available methods + * + * Maps names to functions. + * + * Each function is called with `this` instance as context, + * and accepts the `options` parameters passed to constructor. + * Each method must return widget-like gauge object + * implementing functions: append, enable, disable, getValues + * + * or an error will be thrown + */ + this.methods = {}; + + /** + * ## MoodGauge.method + * + * The method used to measure mood + * + * Available methods: 'I-PANAS-SF' + * + * Default method is: 'I-PANAS-SF' + * + * References: + * + * 'I-PANAS-SF', Thompson E.R. (2007) "Development + * and Validation of an Internationally Reliable Short-Form of + * the Positive and Negative Affect Schedule (PANAS)" + */ + this.method = 'I-PANAS-SF'; + + /** + * ## SVOGauge.gauge + * + * The object measuring mood + * + * @see SVOGauge.method + */ + this.gauge = null; + + this.addMethod('I-PANAS-SF', I_PANAS_SF); + } + + // ## MoodGauge methods. + + /** + * ### MoodGauge.init + * + * Initializes the widget + * + * @param {object} options Optional. Configuration options. + */ + MoodGauge.prototype.init = function(options) { + var gauge; + if ('undefined' !== typeof options.method) { + if ('string' !== typeof options.method) { + throw new TypeError('MoodGauge.init: options.method must be ' + + 'string or undefined: ' + options.method); + } + if (!this.methods[options.method]) { + throw new Error('MoodGauge.init: options.method is not a ' + + 'valid method: ' + options.method); + } + this.method = options.method; + } + // Call method. + gauge = this.methods[this.method].call(this, options); + // Check properties. + checkGauge(this.method, gauge); + // Approved. + this.gauge = gauge; + + this.on('enabled', function() { + gauge.enable(); + }); + + this.on('disabled', function() { + gauge.disable(); + }); + + this.on('highlighted', function() { + gauge.highlight(); + }); + + this.on('unhighlighted', function() { + gauge.unhighlight(); + }); + }; + + MoodGauge.prototype.append = function() { + node.widgets.append(this.gauge, this.bodyDiv, { panel: false }); + }; + + /** + * ## MoodGauge.addMethod + * + * Adds a new method to measure mood + * + * @param {string} name The name of the method + * @param {function} cb The callback implementing it + */ + MoodGauge.prototype.addMethod = function(name, cb) { + if ('string' !== typeof name) { + throw new Error('MoodGauge.addMethod: name must be string: ' + + name); + } + if ('function' !== typeof cb) { + throw new Error('MoodGauge.addMethod: cb must be function: ' + + cb); + } + if (this.methods[name]) { + throw new Error('MoodGauge.addMethod: name already existing: ' + + name); + } + this.methods[name] = cb; + }; + + MoodGauge.prototype.getValues = function(opts) { + return this.gauge.getValues(opts); + }; + + MoodGauge.prototype.setValues = function(opts) { + return this.gauge.setValues(opts); + }; + + // ## Helper functions. + + /** + * ### checkGauge + * + * Checks if a gauge is properly constructed, throws an error otherwise + * + * @param {string} method The name of the method creating it + * @param {object} gauge The object to check + * + * @see ModdGauge.init + */ + function checkGauge(method, gauge) { + if (!gauge) { + throw new Error('MoodGauge.init: method ' + method + + 'did not create element gauge.'); + } + if ('function' !== typeof gauge.getValues) { + throw new Error('MoodGauge.init: method ' + method + + ': gauge missing function getValues.'); + } + if ('function' !== typeof gauge.enable) { + throw new Error('MoodGauge.init: method ' + method + + ': gauge missing function enable.'); + } + if ('function' !== typeof gauge.disable) { + throw new Error('MoodGauge.init: method ' + method + + ': gauge missing function disable.'); + } + if ('function' !== typeof gauge.append) { + throw new Error('MoodGauge.init: method ' + method + + ': gauge missing function append.'); + } + } + + // ## Available methods. + + // ### I_PANAS_SF + function I_PANAS_SF(options) { + var items, emotions, choices, left, right; + var gauge, i, len; + + choices = options.choices || + [ '1', '2', '3', '4', '5' ]; + + emotions = options.emotions || [ + 'Upset', + 'Hostile', + 'Alert', + 'Ashamed', + 'Inspired', + 'Nervous', + 'Determined', + 'Attentive', + 'Afraid', + 'Active' + ]; + + left = options.left || 'never'; + + right = options.right || 'always'; + + len = emotions.length; + + items = new Array(len); + + i = -1; + for ( ; ++i < len ; ) { + items[i] = { + id: emotions[i], + left: '' + emotions[i] + ': never', + right: right, + choices: choices + }; + } + + gauge = node.widgets.get('ChoiceTableGroup', { + id: 'ipnassf', + items: items, + mainText: this.getText('mainText'), + title: false, + requiredChoice: true, + storeRef: false + }); + + return gauge; + } + +})(node); + +/** + * # Requirements + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Checks a list of requirements and displays the results + * + * TODO: see if we need to reset the state between two + * consecutive calls to checkRequirements (results array). + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('Requirements', Requirements); + + // ## Meta-data + + Requirements.version = '0.7.1'; + Requirements.description = 'Checks a set of requirements and display the ' + + 'results'; + + Requirements.title = 'Requirements'; + Requirements.className = 'requirements'; + + Requirements.texts.errStr = 'One or more function is taking too long. ' + + 'This is likely to be due to a compatibility' + + ' issue with your browser or to bad network' + + ' connectivity.'; + Requirements.texts.testPassed = 'All tests passed.'; + + // ## Dependencies + + Requirements.dependencies = { + JSUS: {}, + List: {} + }; + + /** + * ## Requirements constructor + * + * Instantiates a new Requirements object + * + * @param {object} options + */ + function Requirements(options) { + + /** + * ### Requirements.callbacks + * + * Array of all test callbacks + */ + this.requirements = []; + + /** + * ### Requirements.stillChecking + * + * Number of tests still pending + */ + this.stillChecking = 0; + + /** + * ### Requirements.withTimeout + * + * If TRUE, a maximum timeout to the execution of ALL tests is set + */ + this.withTimeout = options.withTimeout || true; + + /** + * ### Requirements.timeoutTime + * + * The time in milliseconds for the timeout to expire + */ + this.timeoutTime = options.timeoutTime || 10000; + + /** + * ### Requirements.timeoutId + * + * The id of the timeout, if created + */ + this.timeoutId = null; + + /** + * ### Requirements.summary + * + * Span summarizing the status of the tests + */ + this.summary = null; + + /** + * ### Requirements.summaryUpdate + * + * Span counting how many tests have been completed + */ + this.summaryUpdate = null; + + /** + * ### Requirements.summaryResults + * + * Span displaying the results of the tests + */ + this.summaryResults = null; + + /** + * ### Requirements.dots + * + * Looping dots to give the user the feeling of code execution + */ + this.dots = null; + + /** + * ### Requirements.hasFailed + * + * TRUE if at least one test has failed + */ + this.hasFailed = false; + + /** + * ### Requirements.results + * + * The outcomes of all tests + */ + this.results = []; + + /** + * ### Requirements.completed + * + * Maps all tests that have been completed already to avoid duplication + */ + this.completed = {}; + + /** + * ### Requirements.sayResult + * + * If true, the final result of the tests will be sent to the server + */ + this.sayResults = options.sayResults || false; + + /** + * ### Requirements.sayResultLabel + * + * The label of the SAY message that will be sent to the server + */ + this.sayResultsLabel = options.sayResultLabel || 'requirements'; + + /** + * ### Requirements.addToResults + * + * Callback to add properties to result object sent to server + */ + this.addToResults = options.addToResults || null; + + /** + * ### Requirements.onComplete + * + * Callback to be executed at the end of all tests + */ + this.onComplete = null; + + /** + * ### Requirements.onSuccess + * + * Callback to be executed at the end of all tests + */ + this.onSuccess = null; + + /** + * ### Requirements.onFailure + * + * Callback to be executed at the end of all tests + */ + this.onFailure = null; + + /** + * ### Requirements.callbacksExecuted + * + * TRUE, the callbacks have been executed + */ + this.callbacksExecuted = false; + + /** + * ### Requirements.list + * + * `List` to render the results + * + * @see nodegame-server/List + */ + // TODO: simplify render syntax. + this.list = new W.List({ + render: { + pipeline: renderResult, + returnAt: 'first' + } + }); + + function renderResult(o) { + var imgPath, img, span, text; + imgPath = '/images/' + (o.content.success ? + 'success-icon.png' : 'delete-icon.png'); + img = document.createElement('img'); + img.src = imgPath; + + // Might be the full exception object. + if ('object' === typeof o.content.text) { + o.content.text = extractErrorMsg(o.content.text); + } + + text = document.createTextNode(o.content.text); + span = document.createElement('span'); + span.className = 'requirement'; + span.appendChild(img); + + span.appendChild(text); + return span; + } + } + + // ## Requirements methods + + /** + * ### Requirements.init + * + * Setups the requirements widget + * + * Available options: + * + * - requirements: array of callback functions or objects formatted as + * { cb: function [, params: object] [, name: string] }; + * - onComplete: function executed with either failure or success + * - onFailure: function executed when at least one test fails + * - onSuccess: function executed when all tests succeed + * - maxWaitTime: max waiting time to execute all tests (in milliseconds) + * + * @param {object} conf Configuration object. + */ + Requirements.prototype.init = function(conf) { + if ('object' !== typeof conf) { + throw new TypeError('Requirements.init: conf must be object. ' + + 'Found: ' + conf); + } + if (conf.requirements) { + if (!J.isArray(conf.requirements)) { + throw new TypeError('Requirements.init: conf.requirements ' + + 'must be array or undefined. Found: ' + + conf.requirements); + } + this.requirements = conf.requirements; + } + if ('undefined' !== typeof conf.onComplete) { + if (null !== conf.onComplete && + 'function' !== typeof conf.onComplete) { + + throw new TypeError('Requirements.init: conf.onComplete must ' + + 'be function, null or undefined. Found: ' + + conf.onComplete); + } + this.onComplete = conf.onComplete; + } + if ('undefined' !== typeof conf.onSuccess) { + if (null !== conf.onSuccess && + 'function' !== typeof conf.onSuccess) { + + throw new TypeError('Requirements.init: conf.onSuccess must ' + + 'be function, null or undefined. Found: ' + + conf.onSuccess); + } + this.onSuccess = conf.onSuccess; + } + if ('undefined' !== typeof conf.onFailure) { + if (null !== conf.onFailure && + 'function' !== typeof conf.onFailure) { + + throw new TypeError('Requirements.init: conf.onFailure must ' + + 'be function, null or undefined. Found: ' + + conf.onFailure); + } + this.onFailure = conf.onFailure; + } + if (conf.maxExecTime) { + if (null !== conf.maxExecTime && + 'number' !== typeof conf.maxExecTime) { + + throw new TypeError('Requirements.init: conf.onMaxExecTime ' + + 'must be number, null or undefined. ' + + 'Found: ' + conf.maxExecTime); + } + this.withTimeout = !!conf.maxExecTime; + this.timeoutTime = conf.maxExecTime; + } + }; + + /** + * ### Requirements.addRequirements + * + * Adds any number of requirements to the requirements array + * + * Callbacks can be asynchronous or synchronous. + * + * An asynchronous callback must call the `results` function + * passed as input parameter to communicate the outcome of the test. + * + * A synchronous callback must return the value immediately. + * + * In both cases the return is an array, where every item is an + * error message. Empty array means test passed. + * + * @see this.requirements + */ + Requirements.prototype.addRequirements = function() { + var i, len; + i = -1, len = arguments.length; + for ( ; ++i < len ; ) { + if ('function' !== typeof arguments[i] && + 'object' !== typeof arguments[i] ) { + + throw new TypeError('Requirements.addRequirements: ' + + 'requirements must be function or ' + + 'object. Found: ' + arguments[i]); + } + this.requirements.push(arguments[i]); + } + }; + + /** + * ### Requirements.checkRequirements + * + * Asynchronously or synchronously checks all registered callbacks + * + * Can add a timeout for the max execution time of the callbacks, if the + * corresponding option is set. + * + * Results are displayed conditionally + * + * @param {boolean} display If TRUE, results are displayed + * + * @return {array} The array containing the errors + * + * @see this.withTimeout + * @see this.requirements + */ + Requirements.prototype.checkRequirements = function(display) { + var i, len; + var errors, cbName, errMsg; + if (!this.requirements.length) { + throw new Error('Requirements.checkRequirements: no requirements ' + + 'to check.'); + } + + this.updateStillChecking(this.requirements.length, true); + + errors = []; + i = -1, len = this.requirements.length; + for ( ; ++i < len ; ) { + // Get Test Name. + if (this.requirements[i] && this.requirements[i].name) { + cbName = this.requirements[i].name; + } + else { + cbName = i + 1; + } + try { + resultCb(this, cbName, i); + } + catch(e) { + this.hasFailed = true; + errMsg = extractErrorMsg(e); + this.updateStillChecking(-1); + errors.push('An error occurred in requirement n.' + + cbName + ': ' + errMsg); + } + } + + if (this.withTimeout) this.addTimeout(); + + if ('undefined' === typeof display ? true : false) { + this.displayResults(errors); + } + + if (this.isCheckingFinished()) this.checkingFinished(); + + return errors; + }; + + /** + * ### Requirements.addTimeout + * + * Starts a timeout for the max execution time of the requirements + * + * Upon time out results are checked, and eventually displayed. + * + * @see this.stillCheckings + * @see this.withTimeout + * @see this.requirements + */ + Requirements.prototype.addTimeout = function() { + var that = this; + + this.timeoutId = setTimeout(function() { + if (that.stillChecking > 0) { + that.displayResults([this.getText('errStr')]); + } + that.timeoutId = null; + that.hasFailed = true; + that.checkingFinished(); + }, this.timeoutTime); + }; + + /** + * ### Requirements.clearTimeout + * + * Clears the timeout for the max execution time of the requirements + * + * @see this.timeoutId + * @see this.stillCheckings + * @see this.requirements + */ + Requirements.prototype.clearTimeout = function() { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + }; + + /** + * ### Requirements.updateStillChecking + * + * Updates the number of requirements still running on the display + * + * @param {number} update The number of requirements still running, or an + * increment as compared to the current value + * @param {boolean} absolute TRUE, if `update` is to be interpreted as an + * absolute value + * + * @see this.summaryUpdate + * @see this.stillCheckings + * @see this.requirements + */ + Requirements.prototype.updateStillChecking = function(update, absolute) { + var total, remaining; + + this.stillChecking = absolute ? update : this.stillChecking + update; + + total = this.requirements.length; + remaining = total - this.stillChecking; + this.summaryUpdate.innerHTML = ' (' + remaining + ' / ' + total + ')'; + }; + + /** + * ### Requirements.isCheckingFinished + * + * Returns TRUE if all requirements have returned + * + * @see this.stillCheckings + * @see this.requirements + */ + Requirements.prototype.isCheckingFinished = function() { + return this.stillChecking <= 0; + }; + + /** + * ### Requirements.checkingFinished + * + * Clears up timer and dots, and executes final callbacks accordingly + * + * First, executes the `onComplete` callback in any case. Then if no + * errors have been raised executes the `onSuccess` callback, otherwise + * the `onFailure` callback. + * + * @param {boolean} force If TRUE, the function is executed again, + * regardless of whether it was already executed. Default: FALSE + * + * @see this.onComplete + * @see this.onSuccess + * @see this.onFailure + * @see this.stillCheckings + * @see this.requirements + */ + Requirements.prototype.checkingFinished = function(force) { + var results; + + // Sometimes, if all requirements are almost synchronous, it + // can happen that this function is called twice (from resultCb + // and at the end of all requirements checkings. + if (this.callbacksExecuted && !force) return; + this.callbacksExecuted = true; + + if (this.timeoutId) clearTimeout(this.timeoutId); + + this.dots.stop(); + + if (this.sayResults) { + results = { + success: !this.hasFailed, + results: this.results + }; + + if (this.addToResults) { + J.mixin(results, this.addToResults()); + } + node.say(this.sayResultsLabel, 'SERVER', results); + } + + if (this.onComplete) this.onComplete(); + + if (this.hasFailed) { + if (this.onFailure) this.onFailure(); + } + else if (this.onSuccess) { + this.onSuccess(); + } + }; + + /** + * ### Requirements.displayResults + * + * Displays the results of the requirements on the screen + * + * Creates a new item in the list of results for every error found + * in the results array. + * + * If no error was raised, the results array should be empty. + * + * @param {array} results The array containing the return values of all + * the requirements + * + * @see this.onComplete + * @see this.onSuccess + * @see this.onFailure + * @see this.stillCheckings + * @see this.requirements + */ + Requirements.prototype.displayResults = function(results) { + var i, len; + + if (!this.list) { + throw new Error('Requirements.displayResults: list not found. ' + + 'Have you called .append() first?'); + } + + if (!J.isArray(results)) { + throw new TypeError('Requirements.displayResults: results must ' + + 'be array. Found: ' + results); + } + + // No errors. + if (!this.hasFailed && this.stillChecking <= 0) { + // All tests passed. + this.list.addDT({ + success: true, + text: this.getText('testPassed') + }); + } + else { + // Add the errors. + i = -1, len = results.length; + for ( ; ++i < len ; ) { + this.list.addDT({ + success: false, + text: results[i] + }); + } + } + // Parse deletes previously existing nodes in the list. + this.list.parse(); + }; + + Requirements.prototype.append = function() { + + this.summary = document.createElement('span'); + this.summary.appendChild( + document.createTextNode('Evaluating requirements')); + + this.summaryUpdate = document.createElement('span'); + this.summary.appendChild(this.summaryUpdate); + + this.dots = W.getLoadingDots(); + this.summary.appendChild(this.dots.span); + + this.summaryResults = document.createElement('div'); + this.summary.appendChild(document.createElement('br')); + this.summary.appendChild(this.summaryResults); + + + this.bodyDiv.appendChild(this.summary); + this.bodyDiv.appendChild(this.list.getRoot()); + }; + + Requirements.prototype.listeners = function() { + var that; + that = this; + node.registerSetup('requirements', function(conf) { + if (!conf) return; + if ('object' !== typeof conf) { + node.warn('requirements widget: invalid setup object: ' + conf); + return; + } + // Configure all requirements. + that.init(conf); + // Start a checking immediately if requested. + if (conf.doChecking !== false) that.checkRequirements(); + + return conf; + }); + + this.on('destroyed', function() { + node.deregisterSetup('requirements'); + }); + }; + + // ## Helper methods. + + function resultCb(that, name, i) { + var req, update, res; + + update = function(success, errors, data) { + if (that.completed[name]) { + throw new Error('Requirements.checkRequirements: test ' + + 'already completed: ' + name); + } + that.completed[name] = true; + that.updateStillChecking(-1); + if (!success) that.hasFailed = true; + + if ('string' === typeof errors) errors = [ errors ]; + + if (errors) { + if (!J.isArray(errors)) { + throw new Error('Requirements.checkRequirements: ' + + 'errors must be array or undefined. ' + + 'Found: ' + errors); + } + that.displayResults(errors); + } + + that.results.push({ + name: name, + success: success, + errors: errors, + data: data + }); + + if (that.isCheckingFinished()) that.checkingFinished(); + }; + + req = that.requirements[i]; + if ('function' === typeof req) { + res = req(update); + } + else if ('object' === typeof req) { + res = req.cb(update, req.params || {}); + } + else { + throw new TypeError('Requirements.checkRequirements: invalid ' + + 'requirement: ' + name + '.'); + } + // Synchronous checking. + if (res) update(res.success, res.errors, res.data); + } + + function extractErrorMsg(e) { + var errMsg; + if (e.msg) { + errMsg = e.msg; + } + else if (e.message) { + errMsg = e.message; + } + else if (e.description) { + errMsg = errMsg.description; + } + else { + errMsg = e.toString(); + } + return errMsg; + } + +})(node); + +/** + * # SVOGauge + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Displays an interface to measure users' social value orientation (S.V.O.) + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('SVOGauge', SVOGauge); + + // ## Meta-data + + SVOGauge.version = '0.6.0'; + SVOGauge.description = 'Displays an interface to measure social ' + + 'value orientation (S.V.O.).'; + + SVOGauge.title = 'SVO Gauge'; + SVOGauge.className = 'svogauge'; + + SVOGauge.texts.mainText = 'Select your preferred option among those' + + ' available below:'; + SVOGauge.texts.left = 'You:
Other:'; + + // ## Dependencies + + SVOGauge.dependencies = { + JSUS: {} + }; + + /** + * ## SVOGauge constructor + * + * Creates a new instance of SVOGauge + * + * @param {object} options Optional. Configuration options + * which is forwarded to SVOGauge.init. + * + * @see SVOGauge.init + */ + function SVOGauge(options) { + + /** + * ### SVOGauge.methods + * + * List of available methods + * + * Maps names to functions. + * + * Each function is called with `this` instance as context, + * and accepts the `options` parameters passed to constructor. + * Each method must return widget-like gauge object + * implementing functions: append, enable, disable, getValues + * + * or an error will be thrown + */ + this.methods = {}; + + /** + * ## SVOGauge.method + * + * The method used to measure svo + * + * Available methods: 'Slider' + * + * Default method is: 'Slider' + * + * References: + * + * 'Slider', Murphy R.O., Ackermann K.A. and Handgraaf M.J.J. (2011). + * "Measuring social value orientation" + */ + this.method = 'Slider'; + + /** + * ## SVOGauge.gauge + * + * The object measuring svo + * + * @see SVOGauge.method + */ + this.gauge = null; + + this.addMethod('Slider', SVO_Slider); + } + + // ## SVOGauge methods. + + /** + * ### SVOGauge.init + * + * Initializes the widget + * + * @param {object} options Optional. Configuration options. + */ + SVOGauge.prototype.init = function(options) { + var gauge; + if ('undefined' !== typeof options.method) { + if ('string' !== typeof options.method) { + throw new TypeError('SVOGauge.init: options.method must be ' + + 'string or undefined: ' + options.method); + } + if (!this.methods[options.method]) { + throw new Error('SVOGauge.init: options.method is not a ' + + 'valid method: ' + options.method); + } + this.method = options.method; + } + // Call method. + gauge = this.methods[this.method].call(this, options); + // Check properties. + checkGauge(this.method, gauge); + // Approved. + this.gauge = gauge; + + this.on('enabled', function() { + gauge.enable(); + }); + + this.on('disabled', function() { + gauge.disable(); + }); + + this.on('highlighted', function() { + gauge.highlight(); + }); + + this.on('unhighlighted', function() { + gauge.unhighlight(); + }); + }; + + SVOGauge.prototype.append = function() { + node.widgets.append(this.gauge, this.bodyDiv); + }; + + /** + * ## SVOGauge.addMethod + * + * Adds a new method to measure mood + * + * @param {string} name The name of the method + * @param {function} cb The callback implementing it + */ + SVOGauge.prototype.addMethod = function(name, cb) { + if ('string' !== typeof name) { + throw new Error('SVOGauge.addMethod: name must be string: ' + + name); + } + if ('function' !== typeof cb) { + throw new Error('SVOGauge.addMethod: cb must be function: ' + + cb); + } + if (this.methods[name]) { + throw new Error('SVOGauge.addMethod: name already existing: ' + + name); + } + this.methods[name] = cb; + }; + + SVOGauge.prototype.getValues = function(opts) { + opts = opts || {}; + // Transform choice in numerical values. + if ('undefined' === typeof opts.processChoice) { + opts.processChoice = function(choice) { + return choice === null ? null : this.choices[choice]; + }; + } + return this.gauge.getValues(opts); + }; + + SVOGauge.prototype.setValues = function(opts) { + return this.gauge.setValues(opts); + }; + + // ## Helper functions. + + /** + * ### checkGauge + * + * Checks if a gauge is properly constructed, throws an error otherwise + * + * @param {string} method The name of the method creating it + * @param {object} gauge The object to check + * + * @see ModdGauge.init + */ + function checkGauge(method, gauge) { + if (!gauge) { + throw new Error('SVOGauge.init: method ' + method + + 'did not create element gauge.'); + } + if ('function' !== typeof gauge.getValues) { + throw new Error('SVOGauge.init: method ' + method + + ': gauge missing function getValues.'); + } + if ('function' !== typeof gauge.enable) { + throw new Error('SVOGauge.init: method ' + method + + ': gauge missing function enable.'); + } + if ('function' !== typeof gauge.disable) { + throw new Error('SVOGauge.init: method ' + method + + ': gauge missing function disable.'); + } + if ('function' !== typeof gauge.append) { + throw new Error('SVOGauge.init: method ' + method + + ': gauge missing function append.'); + } + } + + // ## Available methods. + + // ### SVO_Slider + function SVO_Slider(options) { + var items, sliders; + var gauge, i, len; + var renderer; + + sliders = options.sliders || [ + [ + [85, 85], + [85, 76], + [85, 68], + [85, 59], + [85, 50], + [85, 41], + [85, 33], + [85, 24], + [85, 15] + ], + [ + [85, 15], + [87, 19], + [89, 24], + [91, 28], + [93, 33], + [94, 37], + [96, 41], + [98, 46], + [100, 50] + ], + [ + [50, 100], + [54, 98], + [59, 96], + [63, 94], + [68, 93], + [72, 91], + [76, 89], + [81, 87], + [85, 85] + ], + [ + [50, 100], + [54, 89], + [59, 79], + [63, 68], + [68, 58], + [72, 47], + [76, 36], + [81, 26], + [85, 15] + ], + [ + [100, 50], + [94, 56], + [88, 63], + [81, 69], + [75, 75], + [69, 81], + [63, 88], + [56, 94], + [50, 100] + ], + [ + [100, 50], + [98, 54], + [96, 59], + [94, 63], + [93, 68], + [91, 72], + [89, 76], + [87, 81], + [85, 85] + ] + ]; + + this.sliders = sliders; + + + renderer = options.renderer || function(td, choice, idx) { + td.innerHTML = choice[0] + '
' + choice[1]; + }; + + len = sliders.length; + items = new Array(len); + + i = -1; + for ( ; ++i < len ; ) { + items[i] = { + id: (i+1), + left: this.getText('left'), + choices: sliders[i] + }; + } + + gauge = node.widgets.get('ChoiceTableGroup', { + id: 'svo_slider', + items: items, + mainText: this.getText('mainText'), + title: false, + renderer: renderer, + requiredChoice: true, + storeRef: false + }); + + return gauge; + } + +})(node); + +/** + * # VisualRound + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Display information about rounds and/or stage in the game + * + * Accepts different visualization options (e.g. countdown, etc.). + * See `VisualRound` constructor for a list of all available options. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('VisualRound', VisualRound); + + // ## Meta-data + + VisualRound.version = '0.8.0'; + VisualRound.description = 'Display number of current round and/or stage.' + + 'Can also display countdown and total number of rounds and/or stages.'; + + VisualRound.title = false; + VisualRound.className = 'visualround'; + + VisualRound.texts.round = 'Round'; + VisualRound.texts.stage = 'Stage'; + VisualRound.texts.roundLeft = 'Round Left'; + VisualRound.texts.stageLeft = 'Stage left'; + + // ## Dependencies + + VisualRound.dependencies = { + GamePlot: {}, + JSUS: {} + }; + + /** + * ## VisualRound constructor + * + * Displays information on the current and total rounds and stages + */ + function VisualRound() { + + /** + * ### VisualRound.options + * + * Current configuration + */ + this.options = null; + + /** + * ### VisualRound.displayMode + * + * Object which determines what information is displayed + * + * Set through `VisualRound.setDisplayMode` using a string to describe + * the displayMode. + * + * @see VisualRound.setDisplayMode + */ + this.displayMode = null; + + /** + * ### VisualRound.stager + * + * Reference to a `GameStager` object providing stage and round info + * + * @see GameStager + */ + this.stager = null; + + /** + * ### VisualRound.gamePlot + * + * `GamePlot` object to provide stage and round information + * + * @see GamePlot + */ + this.gamePlot = null; + + /** + * ### VisualRound.curStage + * + * Number of the current stage + */ + this.curStage = null; + + /** + * ### VisualRound.totStage + * + * Total number of stages. Might be null if in `flexibleMode` + */ + this.totStage = null; + + /** + * ### VisualRound.curRound + * + * Number of the current round + */ + this.curRound = null; + + /** + * ### VisualRound.totRound + * + * Total number of rounds. Might be null if in `flexibleMode` + */ + this.totRound = null; + + /** + * ### VisualRound.stageOffset + * + * Stage displayed is the actual stage minus stageOffset + */ + this.stageOffset = null; + + /** + * ### VisualRound.totStageOffset + * + * Total number of stages displayed minus totStageOffset + * + * If not set, and it is set equal to stageOffset + */ + this.totStageOffset = null; + + /** + * ### VisualRound.oldStageId + * + * Stage id of the previous stage + * + * Needed in `flexibleMode` to count rounds. + */ + this.oldStageId = null; + + /** + * ### VisualRound.separator + * + * Stages and rounds are separated with this string, if needed + * + * E.g., Stage 3/5 + */ + this.separator = ' / '; + + /** + * ### VisualRound.layout + * + * Display layout + * + * @see VisualRound.setLayout + */ + this.layout = null; + + } + + // ## VisualRound methods + + /** + * ### VisualRound.init + * + * Initializes the instance + * + * If called on running instance, options are mixed-in into current + * settings. See `VisualRound` constructor for which options are allowed. + * + * @param {object} options Optional. Configuration options. + * The options it can take are: + * + * - `stageOffset`: + * Stage displayed is the actual stage minus stageOffset + * - `flexibleMode`: + * Set `true`, if number of rounds and/or stages can change dynamically + * - `curStage`: + * When (re)starting in `flexibleMode`, sets the current stage + * - `curRound`: + * When (re)starting in `flexibleMode`, sets the current round + * - `totStage`: + * When (re)starting in `flexibleMode`, sets the total number of stages + * - `totRound`: + * When (re)starting in `flexibleMode`, sets the total number of + * rounds + * - `oldStageId`: + * When (re)starting in `flexibleMode`, sets the id of the current + * stage + * - `displayMode`: + * Array of strings which determines the display style of the widget + * - `displayModeNames`: alias of displayMode, deprecated + * + * + * @see VisualRound.setDisplayMode + * @see GameStager + * @see GamePlot + */ + VisualRound.prototype.init = function(options) { + options = options || {}; + + J.mixout(options, this.options); + this.options = options; + + this.stageOffset = this.options.stageOffset || 0; + this.totStageOffset = + 'undefined' === typeof this.options.totStageOffset ? + this.stageOffset : this.options.totStageOffset; + + if (this.options.flexibleMode) { + this.curStage = this.options.curStage || 1; + this.curStage -= this.options.stageOffset || 0; + this.curRound = this.options.curRound || 1; + this.totStage = this.options.totStage; + this.totRound = this.options.totRound; + this.oldStageId = this.options.oldStageId; + } + + // Save references to gamePlot and stager for convenience. + if (!this.gamePlot) this.gamePlot = node.game.plot; + if (!this.stager) this.stager = this.gamePlot.stager; + + this.updateInformation(); + + if (!this.options.displayMode && this.options.displayModeNames) { + console.log('***VisualTimer.init: options.displayModeNames is ' + + 'deprecated. Use options.displayMode instead.***'); + this.options.displayMode = this.options.displayModeNames; + } + + if (!this.options.displayMode) { + this.setDisplayMode([ + 'COUNT_UP_ROUNDS_TO_TOTAL_IFNOT1', + 'COUNT_UP_STAGES_TO_TOTAL' + ]); + } + else { + this.setDisplayMode(this.options.displayMode); + } + + if ('undefined' !== typeof options.separator) { + this.separator = options.separator; + } + + if ('undefined' !== typeof options.layout) { + this.layout = options.layout; + } + + this.updateDisplay(); + }; + + VisualRound.prototype.append = function() { + this.activate(this.displayMode); + this.updateDisplay(); + }; + + /** + * ### VisualRound.updateDisplay + * + * Updates the values displayed by forwarding the call to displayMode obj + * + * @see VisualRound.displayMode + */ + VisualRound.prototype.updateDisplay = function() { + if (this.displayMode) this.displayMode.updateDisplay(); + }; + + /** + * ### VisualRound.setDisplayMode + * + * Sets the `VisualRound.displayMode` value + * + * Multiple displayModes are allowed, and will be merged together into a + * `CompoundDisplayMode` object. The old `displayMode` is deactivated and + * the new one is activated. + * + * The following strings are valid display names: + * + * - `COUNT_UP_STAGES`: Display only current stage number. + * - `COUNT_UP_ROUNDS`: Display only current round number. + * - `COUNT_UP_STAGES_TO_TOTAL`: Display current and total stage number. + * - `COUNT_UP_ROUNDS_TO_TOTAL`: Display current and total round number. + * - `COUNT_DOWN_STAGES`: Display number of stages left to play. + * - `COUNT_DOWN_ROUNDS`: Display number of rounds left in this stage. + * + * @param {array|string} displayMode Array of strings representing the names + * + * @see VisualRound.displayMode + * @see CompoundDisplayMode + * @see VisualRound.init + */ + VisualRound.prototype.setDisplayMode = function(displayMode) { + var i, len, displayModes; + + if ('string' === typeof displayMode) { + displayMode = [ displayMode ]; + } + else if (!J.isArray(displayMode)) { + throw new TypeError('VisualRound.setDisplayMode: ' + + 'displayMode must be array or string. ' + + 'Found: ' + displayMode); + } + len = displayMode.length; + if (len === 0) { + throw new Error('VisualRound.setDisplayMode: displayMode is empty'); + } + + if (this.displayMode) { + // Nothing to do if mode is already active. + if (displayMode.join('&') === this.displayMode.name) return; + this.deactivate(this.displayMode); + } + + // Build `CompoundDisplayMode`. + displayModes = []; + i = -1; + for (; ++i < len; ) { + switch (displayMode[i]) { + case 'COUNT_UP_STAGES_TO_TOTAL': + displayModes.push(new CountUpStages(this, { toTotal: true })); + break; + case 'COUNT_UP_STAGES': + displayModes.push(new CountUpStages(this)); + break; + case 'COUNT_DOWN_STAGES': + displayModes.push(new CountDownStages(this)); + break; + case 'COUNT_UP_ROUNDS_TO_TOTAL': + displayModes.push(new CountUpRounds(this, { toTotal: true })); + break; + case 'COUNT_UP_ROUNDS': + displayModes.push(new CountUpRounds(this)); + break; + case 'COUNT_UP_ROUNDS_TO_TOTAL_IFNOT1': + displayModes.push(new CountUpRounds(this, { + toTotal: true, + ifNotOne: true + })); + break; + case 'COUNT_UP_ROUNDS_IFNOT1': + displayModes.push(new CountUpRounds(this, { ifNotOne: true })); + break; + case 'COUNT_DOWN_ROUNDS': + displayModes.push(new CountDownRounds(this)); + break; + default: + throw new Error('VisualRound.setDisplayMode: unknown mode: ' + + displayMode[i]); + } + } + this.displayMode = new CompoundDisplayMode(this, displayModes); + this.activate(this.displayMode); + }; + + /** + * ### VisualRound.getDisplayMode + * + * Returns name of the current displayMode + * + * @return {string} Name of the current displayMode + */ + VisualRound.prototype.getDisplayModeName = function() { + return this.displayMode.name; + }; + + /** + * ### VisualRound.activate + * + * Appends the displayDiv of the given displayMode to `this.bodyDiv` + * + * Calls `displayMode.activate`, if one is defined. + * + * @param {object} displayMode DisplayMode to activate + * + * @see VisualRound.deactivate + */ + VisualRound.prototype.activate = function(displayMode) { + if (this.bodyDiv) this.bodyDiv.appendChild(displayMode.displayDiv); + if (displayMode.activate) displayMode.activate(); + }; + + /** + * ### VisualRound.deactivate + * + * Removes the displayDiv of the given displayMode from `this.bodyDiv` + * + * Calls `displayMode.deactivate` if it is defined. + * + * @param {object} displayMode DisplayMode to deactivate + * + * @see VisualRound.activate + */ + VisualRound.prototype.deactivate = function(displayMode) { + this.bodyDiv.removeChild(displayMode.displayDiv); + if (displayMode.deactivate) displayMode.deactivate(); + }; + + VisualRound.prototype.listeners = function() { + var that; + that = this; + node.on('STEP_CALLBACK_EXECUTED', function() { + that.updateInformation(); + }); + // TODO: Game over and init? + }; + + /** + * ### VisualRound.updateInformation + * + * Updates information about rounds and stages and updates the display + * + * Updates `curRound`, `curStage`, `totRound`, `totStage`, `oldStageId` and + * calls `VisualRound.updateDisplay`. + * + * @see VisualRound.updateDisplay + */ + VisualRound.prototype.updateInformation = function() { + var stage, len; + + stage = node.player.stage; + + // Game not started. + if (stage.stage === 0) { + this.curStage = 0; + this.totStage = 0; + this.totRound = 0; + } + // Flexible mode. + else if (this.options.flexibleMode) { + if (stage.id === this.oldStageId) { + this.curRound += 1; + } + else if (stage.id) { + this.curRound = 1; + this.curStage += 1; + } + this.oldStageId = stage.id; + } + // Normal mode. + else { + this.curStage = stage.stage; + // Stage can be indexed by id or number in the sequence. + if ('string' === typeof this.curStage) { + this.curStage = + this.gamePlot.normalizeGameStage(stage).stage; + } + this.curRound = stage.round; + this.totRound = this.stager.sequence[this.curStage -1].num || 1; + this.curStage -= this.stageOffset; + len = this.stager.sequence.length; + this.totStage = len - this.totStageOffset; + if (this.stager.sequence[(len-1)].type === 'gameover') { + this.totStage--; + } + } + // Update display. + this.updateDisplay(); + }; + + /** + * ### VisualRound.setLayout + * + * Arranges the relative position of the various elements of VisualRound + * + * @param {string} layout. Admitted values: + * - 'vertical' (alias: 'multimode_vertical') + * - 'horizontal' + * - 'multimode_horizontal' + * - 'all_horizontal' + */ + VisualRound.prototype.setLayout = function(layout) { + if ('string' !== typeof layout || layout.trim() === '') { + throw new TypeError('VisualRound.setLayout: layout must be ' + + 'a non-empty string. Found: ' + layout); + } + this.layout = layout; + if (this.displayMode) this.displayMode.setLayout(layout); + }; + + // ## Display Modes. + + /** + * # CountUpStages + * + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Display mode for `VisualRound` which with current/total number of stages + */ + + /** + * ## CountUpStages constructor + * + * DisplayMode which displays the current number of stages + * + * Can be constructed to furthermore display the total number of stages. + * + * @param {VisualRound} visualRound The `VisualRound` object to which the + * displayMode belongs + * @param {object} options Optional. Configuration options. + * If `options.toTotal == true`, then the total number of stages is + * displayed + * + * @see VisualRound + */ + function CountUpStages(visualRound, options) { + + generalConstructor(this, visualRound, 'COUNT_UP_STAGES', options); + + /** + * ### CountUpStages.curStageNumber + * + * The span in which the current stage number is displayed + */ + this.curStageNumber = null; + + /** + * ### CountUpStages.totStageNumber + * + * The span in which the total stage number is displayed + */ + this.totStageNumber = null; + + /** + * ### CountUpStages.displayDiv + * + * The span in which the text ` of ` is displayed + */ + this.textDiv = null; + + // Inits it! + this.init(); + } + + // ## CountUpStages methods + + /** + * ### CountUpStages.init + * + * Initializes the instance + * + * @see CountUpStages.updateDisplay + */ + CountUpStages.prototype.init = function() { + generalInit(this, 'stagediv', this.visualRound.getText('stage')); + + this.curStageNumber = W.append('span', this.contentDiv, { + className: 'number' + }); + if (this.options.toTotal) { + this.textDiv = W.append('span', this.contentDiv, { + className: 'text', + innerHTML: this.visualRound.separator + }); + this.totStageNumber = W.append('span', this.contentDiv, { + className: 'number' + }); + } + this.updateDisplay(); + }; + + /** + * ### CountUpStages.updateDisplay + * + * Updates the content of `curStageNumber` and `totStageNumber` + * + * Values are updated according to the state of `visualRound`. + * + * @see VisualRound.updateDisplay + */ + CountUpStages.prototype.updateDisplay = function() { + this.curStageNumber.innerHTML = this.visualRound.curStage; + if (this.options.toTotal) { + this.totStageNumber.innerHTML = this.visualRound.totStage || '?'; + } + }; + + /** + * # CountDownStages + * + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Defines a displayMode for the `VisualRound` which displays the remaining + * number of stages + */ + + /** + * ## CountDownStages constructor + * + * Display mode which displays the remaining number of stages + * + * @param {VisualRound} visualRound The `VisualRound` object to which the + * displayMode belongs. + * @param {object} options Optional. Configuration options + * + * @see VisualRound + */ + function CountDownStages(visualRound, options) { + + generalConstructor(this, visualRound, 'COUNT_DOWN_STAGES', options); + + /** + * ### CountDownStages.stagesLeft + * + * The DIV in which the number stages left is displayed + */ + this.stagesLeft = null; + + this.init(); + } + + // ## CountDownStages methods + + /** + * ### CountDownStages.init + * + * Initializes the instance + * + * @see CountDownStages.updateDisplay + */ + CountDownStages.prototype.init = function() { + generalInit(this, 'stagediv', this.visualRound.getText('stageLeft')); + this.stagesLeft = W.add('div', this.contentDiv, { + className: 'number' + }); + this.updateDisplay(); + }; + + /** + * ### CountDownStages.updateDisplay + * + * Updates the content of `stagesLeft` according to `visualRound` + * + * @see VisualRound.updateDisplay + */ + CountDownStages.prototype.updateDisplay = function() { + var v; + v = this.visualRound; + if (v.totStage === v.curStage) { + this.stagesLeft.innerHTML = 0; + } + else { + this.stagesLeft.innerHTML = (v.totStage - v.curStage) || '?'; + } + }; + + /** + * # CountUpRounds + * + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Defines a displayMode for the `VisualRound` which displays the current + * and possibly the total number of rounds + */ + + /** + * ## CountUpRounds constructor + * + * Display mode which displays the current number of rounds + * + * Can be constructed to furthermore display the total number of stages. + * + * @param {VisualRound} visualRound The `VisualRound` object to which the + * displayMode belongs + * @param {object} options Optional. Configuration options. If + * `options.toTotal == true`, then the total number of rounds is displayed + * + * @see VisualRound + */ + function CountUpRounds(visualRound, options) { + + generalConstructor(this, visualRound, 'COUNT_UP_ROUNDS', options); + + /** + * ### CountUpRounds.curRoundNumber + * + * The span in which the current round number is displayed + */ + this.curRoundNumber = null; + + /** + * ### CountUpRounds.totRoundNumber + * + * The element in which the total round number is displayed + */ + this.totRoundNumber = null; + + this.init(); + } + + // ## CountUpRounds methods + + /** + * ### CountUpRounds.init + * + * Initializes the instance + * + * @param {object} options Optional. Configuration options. If + * `options.toTotal == true`, then the total number of rounds is displayed + * + * @see CountUpRounds.updateDisplay + */ + CountUpRounds.prototype.init = function() { + + generalInit(this, 'rounddiv', this.visualRound.getText('round')); + + this.curRoundNumber = W.add('span', this.contentDiv, { + className: 'number' + }); + if (this.options.toTotal) { + this.textDiv = W.add('span', this.contentDiv, { + className: 'text', + innerHTML: this.visualRound.separator + }); + + this.totRoundNumber = W.add('span', this.contentDiv, { + className: 'number' + }); + } + this.updateDisplay(); + }; + + /** + * ### CountUpRounds.updateDisplay + * + * Updates the content of `curRoundNumber` and `totRoundNumber` + * + * Values are updated according to the state of `visualRound`. + * + * @see VisualRound.updateDisplay + */ + CountUpRounds.prototype.updateDisplay = function() { + if (this.options.ifNotOne && this.visualRound.totRound === 1) { + this.displayDiv.style.display = 'none'; + } + else { + this.curRoundNumber.innerHTML = this.visualRound.curRound; + if (this.options.toTotal) { + this.totRoundNumber.innerHTML = + this.visualRound.totRound || '?'; + } + this.displayDiv.style.display = ''; + } + }; + + + /** + * # CountDownRounds + * + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Defines a displayMode for the `VisualRound` which displays the remaining + * number of rounds + */ + + /** + * ## CountDownRounds constructor + * + * Display mode which displays the remaining number of rounds + * + * @param {VisualRound} visualRound The `VisualRound` object to which the + * displayMode belongs + * @param {object} options Optional. Configuration options + * + * @see VisualRound + */ + function CountDownRounds(visualRound, options) { + + generalConstructor(this, visualRound, 'COUNT_DOWN_ROUNDS', options); + + /** + * ### CountDownRounds.roundsLeft + * + * The DIV in which the number rounds left is displayed + */ + this.roundsLeft = null; + + this.init(); + } + + // ## CountDownRounds methods + + /** + * ### CountDownRounds.init + * + * Initializes the instance + * + * @see CountDownRounds.updateDisplay + */ + CountDownRounds.prototype.init = function() { + generalInit(this, 'rounddiv', this.visualRound.getText('roundLeft')); + + this.roundsLeft = W.add('div', this.displayDiv); + this.roundsLeft.className = 'number'; + + this.updateDisplay(); + }; + + /** + * ### CountDownRounds.updateDisplay + * + * Updates the content of `roundsLeft` according to `visualRound` + * + * @see VisualRound.updateDisplay + */ + CountDownRounds.prototype.updateDisplay = function() { + var v; + v = this.visualRound; + if (v.totRound === v.curRound) { + this.roundsLeft.innerHTML = 0; + } + else { + this.roundsLeft.innerHTML = (v.totRound - v.curRound) || '?'; + } + }; + + /** + * # CompoundDisplayMode + * + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Defines a displayMode for the `VisualRound` which displays the + * information according to multiple displayModes + */ + + /** + * ## CompoundDisplayMode constructor + * + * Display mode which combines multiple other display displayModes + * + * @param {VisualRound} visualRound The `VisualRound` object to which the + * displayMode belongs + * @param {array} displayModes Array of displayModes to be used in + * combination + * @param {object} options Optional. Configuration options + * + * @see VisualRound + */ + function CompoundDisplayMode(visualRound, displayModes, options) { + + /** + * ### CompoundDisplayMode.visualRound + * + * The `VisualRound` object to which the displayMode belongs + * + * @see VisualRound + */ + this.visualRound = visualRound; + + /** + * ### CompoundDisplayMode.displayModes + * + * The array of displayModes to be used in combination + */ + this.displayModes = displayModes; + + /** + * ### CompoundDisplayMode.name + * + * The name of the displayMode + */ + this.name = displayModes.join('&'); + + /** + * ### CompoundDisplayMode.options + * + * Current options + */ + this.options = options || {}; + + /** + * ### CompoundDisplayMode.displayDiv + * + * The DIV in which the information is displayed + */ + this.displayDiv = null; + + this.init(options); + } + + // ## CompoundDisplayMode methods + + /** + * ### CompoundDisplayMode.init + * + * Initializes the instance + * + * @param {object} options Optional. Configuration options + * + * @see CompoundDisplayMode.updateDisplay + */ + CompoundDisplayMode.prototype.init = function(options) { + var i, len; + this.displayDiv = W.get('div'); + i = -1, len = this.displayModes.length; + for (; ++i < len; ) { + this.displayDiv.appendChild(this.displayModes[i].displayDiv); + } + this.updateDisplay(); + }; + + /** + * ### CompoundDisplayMode.updateDisplay + * + * Calls `updateDisplay` for all displayModes in the combination + * + * @see VisualRound.updateDisplay + */ + CompoundDisplayMode.prototype.updateDisplay = function() { + var i, len; + i = -1, len = this.displayModes.length; + for (; ++i < len; ) { + this.displayModes[i].updateDisplay(); + } + }; + + CompoundDisplayMode.prototype.activate = function() { + var i, len, d, layout; + layout = this.visualRound.layout; + i = -1, len = this.displayModes.length; + for (; ++i < len; ) { + d = this.displayModes[i]; + if (d.activate) this.displayModes[i].activate(); + if (layout) setLayout(d, layout, i === (len-1)); + } + }; + + CompoundDisplayMode.prototype.deactivate = function() { + var i, len, d; + i = -1, len = this.displayModes.length; + for (; ++i < len; ) { + d = this.displayModes[i]; + if (d.deactivate) d.deactivate(); + } + }; + + CompoundDisplayMode.prototype.setLayout = function(layout) { + var i, len, d; + i = -1, len = this.displayModes.length; + for (; ++i < len; ) { + d = this.displayModes[i]; + setLayout(d, layout, i === (len-1)); + } + }; + + // ## Helper Methods. + + + function setLayout(d, layout, lastDisplay) { + if (layout === 'vertical' || layout === 'multimode_vertical' || + layout === 'all_vertical') { + + d.displayDiv.style.float = 'none'; + d.titleDiv.style.float = 'none'; + d.titleDiv.style['margin-right'] = '0px'; + d.contentDiv.style.float = 'none'; + return true; + } + if (layout === 'horizontal') { + d.displayDiv.style.float = 'none'; + d.titleDiv.style.float = 'left'; + d.titleDiv.style['margin-right'] = '6px'; + d.contentDiv.style.float = 'right'; + return true; + } + if (layout === 'multimode_horizontal') { + d.displayDiv.style.float = 'left'; + d.titleDiv.style.float = 'none'; + d.titleDiv.style['margin-right'] = '0px'; + d.contentDiv.style.float = 'none'; + if (!lastDisplay) { + d.displayDiv.style['margin-right'] = '10px'; + } + return true; + } + if (layout === 'all_horizontal') { + d.displayDiv.style.float = 'left'; + d.titleDiv.style.float = 'left'; + d.titleDiv.style['margin-right'] = '6px'; + d.contentDiv.style.float = 'right'; + if (!lastDisplay) { + d.displayDiv.style['margin-right'] = '10px'; + } + return true; + } + return false; + } + + + /** + * ### generalConstructor + * + * Sets up the basic attributes of visualization mode for VisualRound + * + * @param {object} that The visualization mode instance + * @param {VisualRound} visualRound The VisualRound instance + * @param {string} name The name of the visualization mode + * @param {object} options Additional options, e.g. 'toTotal' + */ + function generalConstructor(that, visualRound, name, options) { + + /** + * #### visualRound + * + * The `VisualRound` object to which the displayMode belongs + * + * @see VisualRound + */ + that.visualRound = visualRound; + + /** + * #### name + * + * The name of the displayMode + */ + that.name = name; + if (options.toTotal) that.name += '_TO_TOTAL'; + + /** + * #### options + * + * The options for this instance + */ + that.options = options || {}; + + /** + * #### displayDiv + * + * The DIV in which the information is displayed + */ + that.displayDiv = null; + + /** + * #### displayDiv + * + * The DIV in which the title is displayed + */ + that.titleDiv = null; + + /** + * #### contentDiv + * + * The DIV containing the actual information + */ + that.contentDiv = null; + + /** + * #### textDiv + * + * The span in which the text ` of ` is displayed + */ + that.textDiv = null; + + } + + /** + * ### generalInit + * + * Adds three divs: a container with a nested title and content div + * + * Adds references to the instance: displayDiv, titleDiv, contentDiv. + * + * @param {object} The instance to which the references are added. + * @param {string} The name of the container div + */ + function generalInit(that, containerName, title) { + that.displayDiv = W.get('div', { className: containerName }); + that.titleDiv = W.add('div', that.displayDiv, { + className: 'title', + innerHTML: title + }); + that.contentDiv = W.add('div', that.displayDiv, { + className: 'content' + }); + } + +})(node); + +/** + * # VisualStage + * Copyright(c) 2017 Stefano Balietti + * MIT Licensed + * + * Shows current, previous and next stage. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + var Table = W.Table; + + node.widgets.register('VisualStage', VisualStage); + + // ## Meta-data + + VisualStage.version = '0.2.3'; + VisualStage.description = + 'Visually display current, previous and next stage of the game.'; + + VisualStage.title = 'Stage'; + VisualStage.className = 'visualstage'; + + // ## Dependencies + + VisualStage.dependencies = { + JSUS: {}, + Table: {} + }; + + /** + * ## VisualStage constructor + * + * `VisualStage` displays current, previous and next stage of the game + */ + function VisualStage() { + this.table = new Table(); + } + + // ## VisualStage methods + + /** + * ### VisualStage.append + * + * Appends widget to `this.bodyDiv` and writes the stage + * + * @see VisualStage.writeStage + */ + VisualStage.prototype.append = function() { + this.bodyDiv.appendChild(this.table.table); + this.writeStage(); + }; + + VisualStage.prototype.listeners = function() { + var that = this; + + node.on('STEP_CALLBACK_EXECUTED', function() { + that.writeStage(); + }); + // Game over and init? + }; + + /** + * ### VisualStage.writeStage + * + * Writes the current, previous and next stage into `this.table` + */ + VisualStage.prototype.writeStage = function() { + var miss, stage, pr, nx, tmp; + var curStep, nextStep, prevStep; + var t; + + miss = '-'; + stage = 'Uninitialized'; + pr = miss; + nx = miss; + + curStep = node.game.getCurrentGameStage(); + + if (curStep) { + tmp = node.game.plot.getStep(curStep); + stage = tmp ? tmp.id : miss; + + prevStep = node.game.plot.previous(curStep); + if (prevStep) { + tmp = node.game.plot.getStep(prevStep); + pr = tmp ? tmp.id : miss; + } + + nextStep = node.game.plot.next(curStep); + if (nextStep) { + tmp = node.game.plot.getStep(nextStep); + nx = tmp ? tmp.id : miss; + } + } + + this.table.clear(true); + + this.table.addRow(['Previous: ', pr]); + this.table.addRow(['Current: ', stage]); + this.table.addRow(['Next: ', nx]); + + t = this.table.selexec('y', '=', 0); + t.addClass('strong'); + t.selexec('x', '=', 2).addClass('underline'); + this.table.parse(); + }; + +})(node); + +/** + * # VisualTimer + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Display a configurable timer for the game + * + * Timer can trigger events, only for countdown smaller than 1h. + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('VisualTimer', VisualTimer); + + // ## Meta-data + + VisualTimer.version = '0.9.2'; + VisualTimer.description = 'Display a configurable timer for the game. ' + + 'Can trigger events. Only for countdown smaller than 1h.'; + + VisualTimer.title = 'Time Left'; + VisualTimer.className = 'visualtimer'; + + // ## Dependencies + + VisualTimer.dependencies = { + GameTimer: {}, + JSUS: {} + }; + + /** + * ## VisualTimer constructor + * + * `VisualTimer` displays and manages a `GameTimer` + * + * @param {object} options Optional. Configuration options + * The options it can take are: + * + * - any options that can be passed to a `GameTimer` + * - `waitBoxOptions`: an option object to be passed to `TimerBox` + * - `mainBoxOptions`: an option object to be passed to `TimerBox` + * + * @see TimerBox + * @see GameTimer + */ + function VisualTimer() { + + /** + * ### VisualTimer.gameTimer + * + * The timer which counts down the game time + * + * @see node.timer.createTimer + */ + this.gameTimer = null; + + /** + * ### VisualTimer.mainBox + * + * The `TimerBox` which displays the main timer + * + * @see TimerBox + */ + this.mainBox = null; + + /** + * ### VisualTimer.waitBox + * + * The `TimerBox` which displays the wait timer + * + * @see TimerBox + */ + this.waitBox = null; + + /** + * ### VisualTimer.activeBox + * + * The `TimerBox` in which to display the time + * + * This variable is always a reference to either `waitBox` or + * `mainBox`. + * + * @see TimerBox + */ + this.activeBox = null; + + /** + * ### VisualTimer.isInitialized + * + * Indicates whether the instance has been initializded already + */ + this.isInitialized = false; + + /** + * ### VisualTimer.options + * + * Currently stored options + */ + this.options = {}; + + /** + * ### VisualTimer.internalTimer + * + * TRUE, if the timer is created internally + * + * Internal timers are destroyed when widget is destroyed or cleared + * + * @see VisualTimer.gameTimer + * @see VisualTimer.clear + */ + this.internalTimer = null; + } + + // ## VisualTimer methods + + /** + * ### VisualTimer.init + * + * Initializes the instance. When called again, adds options to current ones + * + * The options it can take are: + * + * - any options that can be passed to a `GameTimer` + * - waitBoxOptions: an option object to be passed to `TimerBox` + * - mainBoxOptions: an option object to be passed to `TimerBox` + * + * @param {object} options Optional. Configuration options + * + * @see TimerBox + * @see GameTimer + */ + VisualTimer.prototype.init = function(options) { + var t, gameTimerOptions; + + // We keep the check for object, because this widget is often + // called by users and the restart methods does not guarantee + // an object. + options = options || {}; + if ('object' !== typeof options) { + throw new TypeError('VisualTimer.init: options must be ' + + 'object or undefined. Found: ' + options); + } + + // Important! Do not modify directly options, because it might + // modify a step-property. Will manual clone later. + gameTimerOptions = {}; + + // If gameTimer is not already set, check options, then + // try to use node.game.timer, if defined, otherwise crete a new timer. + if ('undefined' !== typeof options.gameTimer) { + + if (this.gameTimer) { + throw new Error('GameTimer.init: options.gameTimer cannot ' + + 'be set if a gameTimer is already existing: ' + + this.name); + } + if ('object' !== typeof options.gameTimer) { + throw new TypeError('VisualTimer.init: options.' + + 'gameTimer must be object or ' + + 'undefined. Found: ' + options.gameTimer); + } + this.gameTimer = options.gameTimer; + } + else { + if (!this.isInitialized) { + this.internalTimer = true; + this.gameTimer = node.timer.createTimer({ + name: options.name || 'VisualTimer' + }); + } + } + + if (options.hooks) { + if (!this.internalTimer) { + throw new Error('VisualTimer.init: cannot add hooks on ' + + 'external gameTimer.'); + } + if (!J.isArray(options.hooks)) { + gameTimerOptions.hooks = [ options.hooks ]; + } + } + else { + gameTimerOptions.hooks = []; + } + + // Only push this hook once. + if (!this.isInitialized) { + gameTimerOptions.hooks.push({ + name: 'VisualTimer_' + this.wid, + hook: this.updateDisplay, + ctx: this + }); + } + + // Important! Manual clone must be done after hooks and gameTimer. + + // Parse milliseconds option. + if ('undefined' !== typeof options.milliseconds) { + gameTimerOptions.milliseconds = + node.timer.parseInput('milliseconds', options.milliseconds); + } + + // Parse update option. + if ('undefined' !== typeof options.update) { + gameTimerOptions.update = + node.timer.parseInput('update', options.update); + } + else { + gameTimerOptions.update = 1000; + } + + // Parse timeup option. + if ('undefined' !== typeof options.timeup) { + gameTimerOptions.timeup = options.timeup; + } + + // Init the gameTimer, regardless of the source (internal vs external). + this.gameTimer.init(gameTimerOptions); + + t = this.gameTimer; + +// TODO: not using session for now. +// node.session.register('visualtimer', { +// set: function(p) { +// // TODO +// }, +// get: function() { +// return { +// startPaused: t.startPaused, +// status: t.status, +// timeLeft: t.timeLeft, +// timePassed: t.timePassed, +// update: t.update, +// updateRemaining: t.updateRemaining, +// updateStart: t. updateStart +// }; +// } +// }); + + this.options = gameTimerOptions; + + if ('undefined' === typeof this.options.stopOnDone) { + this.options.stopOnDone = true; + } + if ('undefined' === typeof this.options.startOnPlaying) { + this.options.startOnPlaying = true; + } + + if (!this.options.mainBoxOptions) { + this.options.mainBoxOptions = {}; + } + if (!this.options.waitBoxOptions) { + this.options.waitBoxOptions = {}; + } + + J.mixout(this.options.mainBoxOptions, + {classNameBody: options.className, hideTitle: true}); + J.mixout(this.options.waitBoxOptions, + {title: 'Max. wait timer', + classNameTitle: 'waitTimerTitle', + classNameBody: 'waitTimerBody', hideBox: true}); + + if (!this.mainBox) { + this.mainBox = new TimerBox(this.options.mainBoxOptions); + } + else { + this.mainBox.init(this.options.mainBoxOptions); + } + if (!this.waitBox) { + this.waitBox = new TimerBox(this.options.waitBoxOptions); + } + else { + this.waitBox.init(this.options.waitBoxOptions); + } + + this.activeBox = this.options.activeBox || this.mainBox; + + this.isInitialized = true; + }; + + VisualTimer.prototype.append = function() { + this.bodyDiv.appendChild(this.mainBox.boxDiv); + this.bodyDiv.appendChild(this.waitBox.boxDiv); + + this.activeBox = this.mainBox; + this.updateDisplay(); + }; + + /** + * ### VisualTimer.clear + * + * Reverts state of `VisualTimer` to right after creation + * + * @param {object} options Configuration object + * + * @return {object} oldOptions The Old options + * + * @see node.timer.destroyTimer + * @see VisualTimer.init + */ + VisualTimer.prototype.clear = function(options) { + var oldOptions; + options = options || {}; + oldOptions = this.options; + + if (this.internalTimer) { + node.timer.destroyTimer(this.gameTimer); + this.internalTimer = null; + } + else { + this.gameTimer.removeHook(this.updateHookName); + } + + this.gameTimer = null; + this.activeBox = null; + this.isInitialized = false; + this.init(options); + + return oldOptions; + }; + + /** + * ### VisualTimer.updateDisplay + * + * Changes `activeBox` to display current time of `gameTimer` + * + * @see TimerBox.bodyDiv + */ + VisualTimer.prototype.updateDisplay = function() { + var time, minutes, seconds; + if (!this.gameTimer.milliseconds || this.gameTimer.milliseconds === 0) { + this.activeBox.bodyDiv.innerHTML = '00:00'; + return; + } + time = this.gameTimer.milliseconds - this.gameTimer.timePassed; + time = J.parseMilliseconds(time); + minutes = (time[2] < 10) ? '' + '0' + time[2] : time[2]; + seconds = (time[3] < 10) ? '' + '0' + time[3] : time[3]; + this.activeBox.bodyDiv.innerHTML = minutes + ':' + seconds; + }; + + /** + * ### VisualTimer.start + * + * Starts the timer + * + * @see VisualTimer.updateDisplay + * @see GameTimer.start + */ + VisualTimer.prototype.start = function() { + this.updateDisplay(); + this.gameTimer.start(); + }; + + /** + * ### VisualTimer.restart + * + * Restarts the timer with new options + * + * @param {object} options Configuration object + * + * @see VisualTimer.init + * @see VisualTimer.start + * @see VisualTimer.stop + */ + VisualTimer.prototype.restart = function(options) { + this.stop(); + this.init(options); + this.start(); + }; + + /** + * ### VisualTimer.stop + * + * Stops the timer display and stores the time left in `activeBox.timeLeft` + * + * @param {object} options Configuration object + * + * @see GameTimer.isStopped + * @see GameTimer.stop + */ + VisualTimer.prototype.stop = function(options) { + if (!this.gameTimer.isStopped()) { + this.activeBox.timeLeft = this.gameTimer.timeLeft; + this.gameTimer.stop(); + } + }; + /** + * ### VisualTimer.switchActiveBoxTo + * + * Switches the display of the `gameTimer` into the `TimerBox` `box` + * + * Stores `gameTimer.timeLeft` into `activeBox` and then switches + * `activeBox` to reference `box`. + * + * @param {TimerBox} box TimerBox in which to display `gameTimer` time + */ + VisualTimer.prototype.switchActiveBoxTo = function(box) { + this.activeBox.timeLeft = this.gameTimer.timeLeft || 0; + this.activeBox = box; + this.updateDisplay(); + }; + + /** + * ### VisualTimer.startWaiting + * + * Stops the timer and changes the appearance to a max. wait timer + * + * If options and/or options.milliseconds are undefined, the wait timer + * will start with the current time left on the `gameTimer`. The mainBox + * will be striked out, the waitBox set active and unhidden. All other + * options are forwarded directly to `VisualTimer.restart`. + * + * @param {object} options Configuration object + * + * @see VisualTimer.restart + */ + VisualTimer.prototype.startWaiting = function(options) { + if ('undefined' === typeof options) options = {}; + + if ('undefined' === typeof options.milliseconds) { + options.milliseconds = this.gameTimer.timeLeft; + } + if ('undefined' === typeof options.mainBoxOptions) { + options.mainBoxOptions = {}; + } + if ('undefined' === typeof options.waitBoxOptions) { + options.waitBoxOptions = {}; + } + options.mainBoxOptions.classNameBody = 'strike'; + options.mainBoxOptions.timeLeft = this.gameTimer.timeLeft || 0; + options.activeBox = this.waitBox; + options.waitBoxOptions.hideBox = false; + this.restart(options); + }; + + /** + * ### VisualTimer.startTiming + * + * Starts the timer and changes appearance to a regular countdown + * + * The mainBox will be unstriked and set active, the waitBox will be + * hidden. All other options are forwarded directly to + * `VisualTimer.restart`. + * + * @param {object} options Configuration object + * + * @see VisualTimer.restart + */ + VisualTimer.prototype.startTiming = function(options) { + if ('undefined' === typeof options) { + options = {}; + } + if ('undefined' === typeof options.mainBoxOptions) { + options.mainBoxOptions = {}; + } + if ('undefined' === typeof options.waitBoxOptions) { + options.waitBoxOptions = {}; + } + options.activeBox = this.mainBox; + options.waitBoxOptions.timeLeft = this.gameTimer.timeLeft || 0; + options.waitBoxOptions.hideBox = true; + options.mainBoxOptions.classNameBody = ''; + this.restart(options); + }; + + /** + * ### VisualTimer.resume + * + * Resumes the `gameTimer` + * + * @see GameTimer.resume + */ + VisualTimer.prototype.resume = function() { + this.gameTimer.resume(); + }; + + /** + * ### VisualTimer.setToZero + * + * Stops `gameTimer` and sets `activeBox` to display `00:00` + * + * @see GameTimer.resume + */ + VisualTimer.prototype.setToZero = function() { + this.stop(); + this.activeBox.bodyDiv.innerHTML = '00:00'; + this.activeBox.setClassNameBody('strike'); + }; + + /** + * ### VisualTimer.isTimeup + * + * Returns TRUE if the timer expired + * + * This method is added for backward compatibility. + * + * @see GameTimer.isTimeup + */ + VisualTimer.prototype.isTimeup = function() { + return this.gameTimer.isTimeup(); + }; + + /** + * ### VisualTimer.doTimeUp + * + * Stops the timer and calls the timeup + * + * @see GameTimer.doTimeup + */ + VisualTimer.prototype.doTimeUp = function() { + this.gameTimer.doTimeUp(); + }; + + VisualTimer.prototype.listeners = function() { + var that = this; + + // Add listeners only on internal timer. + if (!this.internalTimer) return; + + node.on('PLAYING', function() { + var options; + if (that.options.startOnPlaying) { + options = that.gameTimer.getStepOptions(); + if (options) { + // Visual update is here (1000 usually). + options.update = that.update; + // Make sure timeup is not used (game.timer does it). + options.timeup = undefined; + // Options other than `update`, `timeup`, + // `milliseconds`, `hooks`, `gameTimer` are ignored. + that.startTiming(options); + } + else { + // Set to zero if it was not started already. + if (!that.gameTimer.isRunning()) that.setToZero(); + } + } + }); + + node.on('REALLY_DONE', function() { + if (that.options.stopOnDone) { + if (!that.gameTimer.isStopped()) { + // This was creating problems, so we just stop it. + // It could be an option, though. + // that.startWaiting(); + that.stop(); + } + } + }); + + // Handle destroy. + this.on('destroyed', function() { + if (that.internalTimer) { + node.timer.destroyTimer(that.gameTimer); + that.internalTimer = null; + } + else { + that.gameTimer.removeHook('VisualTimer_' + that.wid); + } + that.bodyDiv.removeChild(that.mainBox.boxDiv); + that.bodyDiv.removeChild(that.waitBox.boxDiv); + }); + }; + + /** + * # TimerBox + * + * Copyright(c) 2015 Stefano Balietti + * MIT Licensed + * + * Represents a box wherin to display a `VisualTimer` + */ + + /** + * ## TimerBox constructor + * + * `TimerBox` represents a box wherein to display the timer + * + * @param {object} options Optional. Configuration options + * The options it can take are: + * + * - `hideTitle` + * - `hideBody` + * - `hideBox` + * - `title` + * - `classNameTitle` + * - `classNameBody` + * - `timeLeft` + */ + function TimerBox(options) { + /** + * ### TimerBox.boxDiv + * + * The Div which will contain the title and body Divs + */ + this.boxDiv = null; + + /** + * ### TimerBox.titleDiv + * + * The Div which will contain the title + */ + this.titleDiv = null; + /** + * ### TimerBox.bodyDiv + * + * The Div which will contain the numbers + */ + this.bodyDiv = null; + + /** + * ### TimerBox.timeLeft + * + * Used to store the last value before focus is taken away + */ + this.timeLeft = null; + + this.boxDiv = W.get('div'); + this.titleDiv = W.add('div', this.boxDiv); + this.bodyDiv = W.add('div', this.boxDiv); + + this.init(options); + } + + TimerBox.prototype.init = function(options) { + if (options) { + if (options.hideTitle) { + this.hideTitle(); + } + else { + this.unhideTitle(); + } + if (options.hideBody) { + this.hideBody(); + } + else { + this.unhideBody(); + } + if (options.hideBox) { + this.hideBox(); + } + else { + this.unhideBox(); + } + } + + this.setTitle(options.title || ''); + this.setClassNameTitle(options.classNameTitle || ''); + this.setClassNameBody(options.classNameBody || ''); + + if (options.timeLeft) { + this.timeLeft = options.timeLeft; + } + }; + + // ## TimerBox methods + + /** + * ### TimerBox.hideBox + * + * Hides entire `TimerBox` + */ + TimerBox.prototype.hideBox = function() { + this.boxDiv.style.display = 'none'; + }; + + /** + * ### TimerBox.unhideBox + * + * Hides entire `TimerBox` + */ + TimerBox.prototype.unhideBox = function() { + this.boxDiv.style.display = ''; + }; + + /** + * ### TimerBox.hideTitle + * + * Hides title of `TimerBox` + */ + TimerBox.prototype.hideTitle = function() { + this.titleDiv.style.display = 'none'; + }; + + /** + * ### TimerBox.unhideTitle + * + * Unhides title of `TimerBox` + */ + TimerBox.prototype.unhideTitle = function() { + this.titleDiv.style.display = ''; + }; + + /** + * ### TimerBox.hideBody + * + * Hides body of `TimerBox` + */ + TimerBox.prototype.hideBody = function() { + this.bodyDiv.style.display = 'none'; + }; + + /** + * ### TimerBox.unhideBody + * + * Unhides Body of `TimerBox` + */ + TimerBox.prototype.unhideBody = function() { + this.bodyDiv.style.display = ''; + }; + + /** + * ### TimerBox.setTitle + * + * Sets title of `TimerBox` + */ + TimerBox.prototype.setTitle = function(title) { + this.titleDiv.innerHTML = title; + }; + + /** + * ### TimerBox.setClassNameTitle + * + * Sets class name of title of `TimerBox` + */ + TimerBox.prototype.setClassNameTitle = function(className) { + this.titleDiv.className = className; + }; + + /** + * ### TimerBox.setClassNameBody + * + * Sets class name of body of `TimerBox` + */ + TimerBox.prototype.setClassNameBody = function(className) { + this.bodyDiv.className = className; + }; + +})(node); + +/** + * # WaitingRoom + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Displays the number of connected/required players to start a game + * + * www.nodegame.org + */ +(function(node) { + + "use strict"; + + node.widgets.register('WaitingRoom', WaitingRoom); + // ## Meta-data + + WaitingRoom.version = '1.3.0'; + WaitingRoom.description = 'Displays a waiting room for clients.'; + + WaitingRoom.title = 'Waiting Room'; + WaitingRoom.className = 'waitingroom'; + + // ## Dependencies + + WaitingRoom.dependencies = { + JSUS: {}, + VisualTimer: {} + }; + + // ## Prototype Properties. + + /** ### WaitingRoom.sounds + * + * Default sounds to play on particular events + */ + WaitingRoom.sounds = { + + // #### dispatch + dispatch: '/sounds/doorbell.ogg' + }; + + /** ### WaitingRoom.texts + * + * Default texts to display + */ + WaitingRoom.texts = { + + // #### blinkTitle + blinkTitle: 'GAME STARTS!', + + // #### waitingForConf + waitingForConf: 'Waiting to receive data', + + // #### executionMode + executionMode: function(w) { + var startDate; + if (w.executionMode === 'WAIT_FOR_N_PLAYERS') { + return 'Waiting for All Players to Connect: '; + } + if (w.executionMode === 'WAIT_FOR_DISPATCH') { + return 'Task will start soon. Please be patient.'; + } + // TIMEOUT. + return 'Task will start at:
' + w.startDate; + }, + + // #### disconnect + disconnect: 'You have been ' + + 'disconnected. Please try again later.' + + '

', + + // #### waitedTooLong + waitedTooLong: 'Waiting for too long. Please look ' + + 'for a HIT called Trouble Ticket and file' + + ' a new trouble ticket reporting your experience.', + + // #### notEnoughPlayers + notEnoughPlayers: '

' + + 'Thank you for your patience.
' + + 'Unfortunately, there are not enough participants in ' + + 'your group to start the experiment.
', + + // #### roomClosed + roomClosed: ' The ' + + 'waiting room is CLOSED. You have been ' + + 'disconnected. Please try again later.

', + + // #### tooManyPlayers + tooManyPlayers: function(widget, data) { + var str; + str = 'There are more players in this waiting room ' + + 'than playslots in the game. '; + if (widget.poolSize === 1) { + str += 'Each player will play individually.'; + } + else { + str += 'Only ' + data.nGames + ' players will be selected ' + + 'to play the game.'; + } + return str; + }, + + // #### notSelectedClosed + notSelectedClosed: '

' + + 'Unfortunately, you were ' + + 'not selected to join the game this time. ' + + 'Thank you for your participation.



', + + // #### notSelectedOpen + notSelectedOpen: '

' + + 'Unfortunately, you were ' + + 'not selected to join the game this time, ' + + 'but you may join the next one.' + + 'Ok, I got it.



' + + 'Thank you for your participation.

', + + // #### exitCode + exitCode: function(widget, data) { + return '
You have been disconnected. ' + + ('undefined' !== typeof data.exit ? + ('Please report this exit code: ' + data.exit) : '') + + '
'; + }, + + // #### playBot + playBot: function(widget) { + if (widget.poolSize === widget.groupSize && + widget.groupSize === 1) { + + return 'Play'; + } + if (widget.groupSize === 2) return 'Play With Bot'; + return 'Play With Bots'; + }, + + // #### connectingBots + connectingBots: function(widget) { + console.log(widget.poolSize, widget.groupSize); + if (widget.poolSize === widget.groupSize && + widget.groupSize === 1) { + + return 'Starting, Please Wait...'; + } + if (widget.groupSize === 2) return 'Connecting Bot, Please Wait...'; + return 'Connecting Bot/s, Please Wait...'; + }, + + // #### selectTreatment + // Trailing space makes it nicer. + selectTreatment: 'Select Treatment ', + + // #### gameTreatments + gameTreatments: 'Game:', + + // #### defaultTreatments + defaultTreatments: 'Defaults:' + + + }; + + /** + * ## WaitingRoom constructor + * + * Instantiates a new WaitingRoom object + * + * @param {object} options + */ + function WaitingRoom(options) { + + /** + * ### WaitingRoom.connected + * + * Number of players connected + */ + this.connected = 0; + + /** + * ### WaitingRoom.poolSize + * + * Number of players connected before groups are made + */ + this.poolSize = 0; + + /** + * ### WaitingRoom.nGames + * + * Total number of games to be dispatched + * + * Server will close the waiting room afterwards. + * + * Undefined means no limit. + */ + this.nGames = undefined; + + /** + * ### WaitingRoom.groupSize + * + * The size of the group + */ + this.groupSize = 0; + + /** + * ### WaitingRoom.waitTime + * + * The time in milliseconds for the timeout to expire + */ + this.waitTime = null; + + /** + * ### WaitingRoom.executionMode + * + * The execution mode. + */ + this.executionMode = null; + + /** + * ### WaitingRoom.startDate + * + * The exact date and time when the game starts + */ + this.startDate = null; + + /** + * ### WaitingRoom.timeoutId + * + * The id of the timeout, if created + */ + this.timeoutId = null; + + /** + * ### WaitingRoom.execModeDiv + * + * Div containing the span for displaying the number of players + * + * @see WaitingRoom.playerCount + */ + this.execModeDiv = null; + + /** + * ### WaitingRoom.playerCount + * + * Span displaying the number of connected players + */ + this.playerCount = null; + + /** + * ### WaitingRoom.startDateDiv + * + * Div containing the start date + */ + this.startDateDiv = null; + + /** + * ### WaitingRoom.msgDiv + * + * Div containing optional messages to display + */ + this.msgDiv = null; + + /** + * ### WaitingRoom.timerDiv + * + * Div containing the timer + * + * @see WaitingRoom.timer + */ + this.timerDiv = null; + + /** + * ### WaitingRoom.timer + * + * VisualTimer instance for max wait time. + * + * @see VisualTimer + */ + this.timer = null; + + /** + * ### WaitingRoom.dots + * + * Looping dots to give the user the feeling of code execution + */ + this.dots = null; + + /** + * ### WaitingRoom.onTimeout + * + * Callback to be executed if the timer expires + */ + this.onTimeout = null; + + /** + * ### WaitingRoom.disconnectIfNotSelected + * + * Flag that indicates whether to disconnect an not selected player + */ + this.disconnectIfNotSelected = null; + + /** + * ### WaitingRoom.playWithBotOption + * + * If TRUE, it displays a button to begin the game with bots + * + * This option is set by the server, local modifications will + * not have an effect if server does not allow it + * + * @see WaitingRoom.playBotBtn + */ + this.playWithBotOption = null; + + /** + * ### WaitingRoom.playBotBtn + * + * Reference to the button to play with bots + * + * Will be created if requested by options. + * + * @see WaitingRoom.playWithBotOption + */ + this.playBotBtn = null; + + /** + * ### WaitingRoom.selectTreatmentOption + * + * If TRUE, it displays a selector to choose the treatment of the game + * + * This option is set by the server, local modifications will + * not have an effect if server does not allow it + */ + this.selectTreatmentOption = null; + + /** + * ### WaitingRoom.treatmentBtn + * + * Holds the name of selected treatment + * + * Only used if `selectTreatmentOption` is enabled + * + * @see WaitingRoom.selectTreatmentOption + */ + this.selectedTreatment = null; + + } + + // ## WaitingRoom methods + + /** + * ### WaitingRoom.init + * + * Setups the requirements widget + * + * TODO: Update this doc (list of options). + * + * Available options: + * + * - onComplete: function executed with either failure or success + * - onTimeout: function executed when timer runs out + * - onSuccess: function executed when all tests succeed + * - waitTime: max waiting time to execute all tests (in milliseconds) + * - startDate: max waiting time to execute all tests (in milliseconds) + * - playWithBotOption: displays button to dispatch players with bots + * - selectTreatmentOption: displays treatment selector + * + * @param {object} conf Configuration object. + */ + WaitingRoom.prototype.init = function(conf) { + var that = this; + + if ('object' !== typeof conf) { + throw new TypeError('WaitingRoom.init: conf must be object. ' + + 'Found: ' + conf); + } + + // It receives the TEXTS AND SOUNDS only first. + if (!conf.executionMode) return; + + // TODO: check types and conditions? + this.executionMode = conf.executionMode; + + if (conf.onTimeout) { + if ('function' !== typeof conf.onTimeout) { + throw new TypeError('WaitingRoom.init: conf.onTimeout must ' + + 'be function, null or undefined. Found: ' + + conf.onTimeout); + } + this.onTimeout = conf.onTimeout; + } + + if (conf.waitTime) { + if (null !== conf.waitTime && + 'number' !== typeof conf.waitTime) { + + throw new TypeError('WaitingRoom.init: conf.waitTime ' + + 'must be number, null or undefined. ' + + 'Found: ' + conf.waitTime); + } + this.waitTime = conf.waitTime; + } + + if (conf.startDate) { + this.startDate = new Date(conf.startDate).toString(); + } + + if (conf.poolSize) { + if (conf.poolSize && 'number' !== typeof conf.poolSize) { + throw new TypeError('WaitingRoom.init: conf.poolSize ' + + 'must be number or undefined. Found: ' + + conf.poolSize); + } + this.poolSize = conf.poolSize; + } + + if (conf.groupSize) { + if (conf.groupSize && 'number' !== typeof conf.groupSize) { + throw new TypeError('WaitingRoom.init: conf.groupSize ' + + 'must be number or undefined. Found: ' + + conf.groupSize); + } + this.groupSize = conf.groupSize; + } + if (conf.nGames) { + if (conf.nGames && 'number' !== typeof conf.nGames) { + throw new TypeError('WaitingRoom.init: conf.nGames ' + + 'must be number or undefined. Found: ' + + conf.nGames); + } + this.nGames = conf.nGames; + } + + if (conf.connected) { + if (conf.connected && 'number' !== typeof conf.connected) { + throw new TypeError('WaitingRoom.init: conf.connected ' + + 'must be number or undefined. Found: ' + + conf.connected); + } + this.connected = conf.connected; + } + + if (conf.disconnectIfNotSelected) { + if ('boolean' !== typeof conf.disconnectIfNotSelected) { + throw new TypeError('WaitingRoom.init: ' + + 'conf.disconnectIfNotSelected must be boolean or ' + + 'undefined. Found: ' + conf.disconnectIfNotSelected); + } + this.disconnectIfNotSelected = conf.disconnectIfNotSelected; + } + else { + this.disconnectIfNotSelected = false; + } + + + if (conf.playWithBotOption) this.playWithBotOption = true; + else this.playWithBotOption = false; + if (conf.selectTreatmentOption) this.selectTreatmentOption = true; + else this.selectTreatmentOption = false; + + + // Display Exec Mode. + this.displayExecMode(); + + // Button for bots and treatments. + + if (this.playWithBotOption && !document.getElementById('bot_btn')) { + // Closure to create button group. + (function(w) { + var btnGroup = document.createElement('div'); + btnGroup.role = 'group'; + btnGroup['aria-label'] = 'Play Buttons'; + btnGroup.className = 'btn-group'; + + var playBotBtn = document.createElement('input'); + playBotBtn.className = 'btn btn-primary btn-lg'; + playBotBtn.value = w.getText('playBot'); + playBotBtn.id = 'bot_btn'; + playBotBtn.type = 'button'; + playBotBtn.onclick = function() { + w.playBotBtn.value = w.getText('connectingBots'); + w.playBotBtn.disabled = true; + node.say('PLAYWITHBOT', 'SERVER', w.selectedTreatment); + setTimeout(function() { + w.playBotBtn.value = w.getText('playBot'); + w.playBotBtn.disabled = false; + }, 5000); + }; + + btnGroup.appendChild(playBotBtn); + + // Store reference in widget. + w.playBotBtn = playBotBtn; + + if (w.selectTreatmentOption) { + + var btnGroupTreatments = document.createElement('div'); + btnGroupTreatments.role = 'group'; + btnGroupTreatments['aria-label'] = 'Select Treatment'; + btnGroupTreatments.className = 'btn-group'; + + var btnTreatment = document.createElement('button'); + btnTreatment.className = 'btn btn-default btn-lg ' + + 'dropdown-toggle'; + btnTreatment['data-toggle'] = 'dropdown'; + btnTreatment['aria-haspopup'] = 'true'; + btnTreatment['aria-expanded'] = 'false'; + btnTreatment.innerHTML = w.getText('selectTreatment'); + + var span = document.createElement('span'); + span.className = 'caret'; + + btnTreatment.appendChild(span); + + var ul = document.createElement('ul'); + ul.className = 'dropdown-menu'; + ul.style['text-align'] = 'left'; + + var li, a, t, liT1, liT2; + if (conf.availableTreatments) { + li = document.createElement('li'); + li.innerHTML = w.getText('gameTreatments'); + li.className = 'dropdown-header'; + ul.appendChild(li); + for (t in conf.availableTreatments) { + if (conf.availableTreatments.hasOwnProperty(t)) { + li = document.createElement('li'); + li.id = t; + a = document.createElement('a'); + a.href = '#'; + a.innerHTML = '' + t + ': ' + + conf.availableTreatments[t]; + li.appendChild(a); + if (t === 'treatment_rotate') liT1 = li; + else if (t === 'treatment_random') liT2 = li; + else ul.appendChild(li); + } + } + li = document.createElement('li'); + li.role = 'separator'; + li.className = 'divider'; + ul.appendChild(li); + li = document.createElement('li'); + li.innerHTML = w.getText('defaultTreatments'); + li.className = 'dropdown-header'; + ul.appendChild(li); + ul.appendChild(liT1); + ul.appendChild(liT2); + } + + btnGroupTreatments.appendChild(btnTreatment); + btnGroupTreatments.appendChild(ul); + + btnGroup.appendChild(btnGroupTreatments); + + // We are not using bootstrap js files + // and we redo the job manually here. + btnTreatment.onclick = function() { + // When '' is hidden by bootstrap class. + if (ul.style.display === '') { + ul.style.display = 'block'; + } + else { + ul.style.display = ''; + } + }; + + ul.onclick = function(eventData) { + var t; + t = eventData.target; + // When '' is hidden by bootstrap class. + ul.style.display = ''; + t = t.parentNode.id; + // Clicked on description? + if (!t) t = eventData.target.parentNode.parentNode.id; + // Nothing relevant clicked (e.g., header). + if (!t) return; + btnTreatment.innerHTML = t + ' '; + btnTreatment.appendChild(span); + w.selectedTreatment = t; + }; + + // Store Reference in widget. + w.treatmentBtn = btnTreatment; + } + // Append button group. + w.bodyDiv.appendChild(document.createElement('br')); + w.bodyDiv.appendChild(btnGroup); + + })(this); + } + + // Handle destroy. + this.on('destroyed', function() { + if (that.dots) that.dots.stop(); + node.deregisterSetup('waitroom'); + }); + }; + + /** + * ### WaitingRoom.startTimer + * + * Starts a timeout for the max waiting time + */ + WaitingRoom.prototype.startTimer = function() { + var that = this; + if (this.timer) return; + if (!this.waitTime) return; + if (!this.timerDiv) { + this.timerDiv = document.createElement('div'); + this.timerDiv.id = 'timer-div'; + } + this.timerDiv.appendChild(document.createTextNode( + 'Maximum Waiting Time: ' + )); + this.timer = node.widgets.append('VisualTimer', this.timerDiv, { + milliseconds: this.waitTime, + timeup: function() { + that.bodyDiv.innerHTML = that.getText('waitedTooLong'); + }, + update: 1000 + }); + // Style up: delete title and border; + this.timer.setTitle(); + this.timer.panelDiv.className = 'ng_widget visualtimer'; + // Append to bodyDiv. + this.bodyDiv.appendChild(this.timerDiv); + this.timer.start(); + }; + + /** + * ### WaitingRoom.clearTimeout + * + * Clears the timeout for the max execution time of the requirements + * + * @see this.timeoutId + * @see this.stillCheckings + * @see this.requirements + */ + WaitingRoom.prototype.clearTimeout = function() { + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + }; + + /** + * ### WaitingRoom.updateState + * + * Displays the state of the waiting room on screen + * + * @see WaitingRoom.updateState + */ + WaitingRoom.prototype.updateState = function(update) { + if (!update) return; + if ('number' === typeof update.connected) { + this.connected = update.connected; + } + if ('number' === typeof update.poolSize) { + this.poolSize = update.poolSize; + } + if ('number' === typeof update.groupSize) { + this.groupSize = update.groupSize; + } + }; + + /** + * ### WaitingRoom.updateDisplay + * + * Displays the state of the waiting room on screen (player count) + * + * @see WaitingRoom.updateState + */ + WaitingRoom.prototype.updateDisplay = function() { + var numberOfGameSlots, numberOfGames; + if (this.connected > this.poolSize) { + numberOfGames = Math.floor(this.connected / this.groupSize); + if ('undefined' !== typeof this.nGames) { + numberOfGames = numberOfGames > this.nGames ? + this.nGames : numberOfGames; + } + numberOfGameSlots = numberOfGames * this.groupSize; + + this.playerCount.innerHTML = '' + + this.connected + '' + ' / ' + this.poolSize; + this.playerCountTooHigh.style.display = ''; + + // Update text. + this.playerCountTooHigh.innerHTML = + this.getText('tooManyPlayers', { nGames: numberOfGameSlots }); + } + else { + this.playerCount.innerHTML = this.connected + ' / ' + this.poolSize; + this.playerCountTooHigh.style.display = 'none'; + } + }; + + /** + * ### WaitingRoom.displayExecMode + * + * Builds the basic layout of the execution mode + * + * @see WaitingRoom.executionMode + */ + WaitingRoom.prototype.displayExecMode = function() { + this.bodyDiv.innerHTML = ''; + + this.execModeDiv = document.createElement('div'); + this.execModeDiv.id = 'exec-mode-div'; + + this.execModeDiv.innerHTML = this.getText('executionMode'); + + // TODO: add only on some modes? Depending on settings? + this.playerCount = document.createElement('p'); + this.playerCount.id = 'player-count'; + this.execModeDiv.appendChild(this.playerCount); + + this.playerCountTooHigh = document.createElement('div'); + this.playerCountTooHigh.style.display = 'none'; + this.execModeDiv.appendChild(this.playerCountTooHigh); + + this.startDateDiv = document.createElement('div'); + this.startDateDiv.style.display= 'none'; + this.execModeDiv.appendChild(this.startDateDiv); + + this.dots = W.getLoadingDots(); + this.execModeDiv.appendChild(this.dots.span); + + this.bodyDiv.appendChild(this.execModeDiv); + + this.msgDiv = document.createElement('div'); + this.bodyDiv.appendChild(this.msgDiv); + + + // if (this.startDate) this.setStartDate(this.startDate); + if (this.waitTime) this.startTimer(); + + }; + + WaitingRoom.prototype.append = function() { + // Configuration will arrive soon. + this.bodyDiv.innerHTML = this.getText('waitingForConf'); + }; + + WaitingRoom.prototype.listeners = function() { + var that; + that = this; + + node.registerSetup('waitroom', function(conf) { + if (!conf) return; + if ('object' !== typeof conf) { + node.warn('waiting room widget: invalid setup object: ' + conf); + return; + } + + // It receives 2 conf messages. + if (!conf.executionMode) { + // Sounds. + that.setSounds(conf.sounds); + // Texts. + that.setTexts(conf.texts); + } + else { + // Configure all requirements. + that.init(conf); + } + + return conf; + }); + + // NodeGame Listeners. + node.on.data('PLAYERSCONNECTED', function(msg) { + if (!msg.data) return; + that.connected = msg.data; + that.updateDisplay(); + }); + + node.on.data('DISPATCH', function(msg) { + var data, reportExitCode; + msg = msg || {}; + data = msg.data || {}; + + if (that.dots) that.dots.stop(); + + // Alert player he/she is about to play. + if (data.action === 'allPlayersConnected') { + that.alertPlayer(); + } + // Not selected/no game/etc. + else { + reportExitCode = that.getText('exitCode', data); + + if (data.action === 'notEnoughPlayers') { + that.bodyDiv.innerHTML = that.getText(data.action); + if (that.onTimeout) that.onTimeout(msg.data); + that.disconnect(that.bodyDiv.innerHTML + reportExitCode); + } + else if (data.action === 'notSelected') { + + if (false === data.shouldDispatchMoreGames || + that.disconnectIfNotSelected) { + + that.bodyDiv.innerHTML = + that.getText('notSelectedClosed'); + + that.disconnect(that.bodyDiv.innerHTML + + reportExitCode); + } + else { + that.msgDiv.innerHTML = that.getText('notSelectedOpen'); + } + } + else if (data.action === 'disconnect') { + that.disconnect(that.bodyDiv.innerHTML + reportExitCode); + } + } + }); + + node.on.data('TIME', function(msg) { + msg = msg || {}; + node.info('waiting room: TIME IS UP!'); + that.stopTimer(); + }); + + // Start waiting time timer. + node.on.data('WAITTIME', function(msg) { + + // Avoid running multiple timers. + // if (timeCheck) clearInterval(timeCheck); + + that.updateState(msg.data); + that.updateDisplay(); + + }); + + node.on('SOCKET_DISCONNECT', function() { + + // Terminate countdown. + that.stopTimer(); + + // Write about disconnection in page. + that.bodyDiv.innerHTML = that.getText('disconnect'); + + // Enough to not display it in case of page refresh. + // setTimeout(function() { + // alert('Disconnection from server detected!'); + // }, 200); + }); + + node.on.data('ROOM_CLOSED', function() { + that.disconnect(that.getText('roomClosed')); + }); + }; + + WaitingRoom.prototype.stopTimer = function() { + if (this.timer) { + node.info('waiting room: STOPPING TIMER'); + this.timer.destroy(); + } + }; + + /** + * ### WaitingRoom.disconnect + * + * Disconnects the playr, stops the timer, and displays a msg + * + * @param {string|function} msg. Optional. A disconnect message. If set, + * replaces the current value for future calls. + * + * @see WaitingRoom.setText + */ + WaitingRoom.prototype.disconnect = function(msg) { + if (msg) this.setText('disconnect', msg); + node.socket.disconnect(); + this.stopTimer(); + }; + + WaitingRoom.prototype.alertPlayer = function() { + var clearBlink, onFrame; + var blink, sound; + + blink = this.getText('blinkTitle'); + + sound = this.getSound('dispatch'); + + // Play sound, if requested. + if (sound) J.playSound(sound); + + // If blinkTitle is falsy, don't blink the title. + if (!blink) return; + + // If document.hasFocus() returns TRUE, then just one repeat is enough. + if (document.hasFocus && document.hasFocus()) { + J.blinkTitle(blink, { repeatFor: 1 }); + } + // Otherwise we repeat blinking until an event that shows that the + // user is active on the page happens, e.g. focus and click. However, + // the iframe is not created yet, and even later. if the user clicks it + // it won't be detected in the main window, so we need to handle it. + else { + clearBlink = J.blinkTitle(blink, { + stopOnFocus: true, + stopOnClick: window + }); + onFrame = function() { + var frame; + clearBlink(); + frame = W.getFrame(); + if (frame) { + frame.removeEventListener('mouseover', onFrame, false); + } + }; + node.events.ng.once('FRAME_GENERATED', function(frame) { + frame.addEventListener('mouseover', onFrame, false); + }); + } + }; + +})(node); diff --git a/build/nodegame-widgets.min.js b/build/nodegame-widgets.min.js new file mode 100644 index 0000000..9cb63b4 --- /dev/null +++ b/build/nodegame-widgets.min.js @@ -0,0 +1,15 @@ +/** + * # Widget + * Copyright(c) 2019 Stefano Balietti + * MIT Licensed + * + * Prototype of a widget class + * + * Prototype methods will be injected in every new widget, if missing. + * + * Additional properties can be automatically, depending on configuration. + * + * @see Widgets.get + * @see Widgets.append + */ +(function(e){"use strict";function r(){}function i(e,t,n,r,i){var s;if(!e.constructor[n].hasOwnProperty(t))throw new Error(r+": name not found: "+t);s="undefined"!=typeof e[n][t]?e[n][t]:e.constructor[n][t];if("function"==typeof s){s=s(e,i);if("string"!=typeof s&&s!==!1)throw new TypeError(r+': cb "'+t+'" did not '+"return neither string or false. Found: "+s)}return s}function s(e,n,r,i,s,o){var u,a,f;s||(s=e.constructor[n]),"undefined"==typeof o&&(o={}),u={};if(t.isArray(s)){a=-1,f=s.length;for(;++a1&&(o=f.docked[f.docked.length-2],a=u(o.panelDiv.style.right),a+=o.panelDiv.offsetWidth),a+=r,n.panelDiv.style.right=a+"px",l=0,a+=n.panelDiv.offsetWidth+i;while(f.docked.length>1&&a>e.innerWidth&&lr.stage&&!t.acrossStages?!1:n.round>r.round&&!t.acrossRounds?!1:r}e.widgets.register("BackButton",t),t.version="0.1.0",t.description="Creates a button that if pressed goes to the previous step.",t.title=!1,t.className="backbutton",t.texts.back="Back",t.dependencies={JSUS:{}},t.prototype.init=function(e){var n;e=e||{};if("undefined"==typeof e.id)n=t.className;else if("string"==typeof e.id)n=e.id;else{if(!1!==e.id)throw new TypeError("BackButton.init: options.id must be string, false, or undefined. Found: "+e.id);n=""}this.button.id=n;if("undefined"==typeof e.className)n="btn btn-lg btn-secondary";else if(e.className===!1)n="";else if("string"==typeof e.className)n=e.className;else{if(!J.isArray(e.className))throw new TypeError("BackButton.init: options.className must be string, array, or undefined. Found: "+e.className);n=e.className.join(" ")}this.button.className=n,this.button.value="string"==typeof e.text?e.text:this.getText("back"),this.acrossStages="undefined"==typeof e.acrossStages?!1:!!e.acrossStages,this.acrossRounds="undefined"==typeof e.acrossRounds?!0:!!e.acrossRounds},t.prototype.append=function(){this.bodyDiv.appendChild(this.button)},t.prototype.listeners=function(){var t=this;e.on("PLAYING",function(){var r,i;i=n(t),i&&t.enable(),r=e.game.getProperty("backbutton"),(!i||r===!1||r&&r.enableOnPlaying===!1)&&t.disable(),"string"==typeof r?t.button.value=r:r&&r.text&&(t.button.value=r.text)})},t.prototype.disable=function(){this.button.disabled="disabled"},t.prototype.enable=function(){this.button.disabled=!1}}(node),function(e){"use strict";function n(){this.button=null,this.buttonText="",this.items=[],this.onclick=null,this.getDescr=null,this.getId=function(e){return e.id},this.ul=null}var t=e.NDDB;e.widgets.register("BoxSelector",n),n.version="1.0.0",n.description="Creates a simple box that opens a menu of items to choose from.",n.panel=!1,n.title=!1,n.className="boxselector",n.dependencies={JSUS:{}},n.prototype.init=function(e){if(e.onclick){if("function"!=typeof e.onclick)throw new Error("BoxSelector.init: options.getId must be function or undefined. Found: "+e.getId);this.onclick=e.onclick}if("function"!=typeof e.getDescr)throw new Error("BoxSelector.init: options.getDescr must be function. Found: "+e.getDescr);this.getDescr=e.getDescr;if(e.getId&&"function"!=typeof e.getId)throw new Error("BoxSelector.init: options.getId must be function or undefined. Found: "+e.getId);this.getId=e.getId},n.prototype.append=function(){var e,t,n,r,i;r=W.add("div",this.bodyDiv),r.role="group",r["aria-label"]="Select Items",r.className="btn-group dropup",n=this.button=W.add("button",r),n.className="btn btn-default btn dropdown-toggle",n["data-toggle"]="dropdown",n["aria-haspopup"]="true",n["aria-expanded"]="false",n.innerHTML=this.buttonText+" ",W.add("span",n,{className:"caret"}),t=this.ul=W.add("ul",r),t.className="dropdown-menu",t.style.display="none",i=!1,n.onclick=function(){i?(t.style.display="none",i=!1):(t.style.display="block",i=!0)},this.onclick&&(e=this,t.onclick=function(n){var r,s,o;r=n.target,t.style.display="",i=!1,r=r.parentNode.id,r||(r=n.target.parentNode.parentNode.id);if(!r)return;o=e.items.length;for(s=0;s1&&(n+=''+(e.senderToNameMap[t.id]||t.id)+": "),n+=t.msg+"",n},quit:function(e,t){return(e.senderToNameMap[t.id]||t.id)+" left the chat"},noMoreParticipants:function(e,t){return"No active participant left. Chat disabled."},collapse:function(e,t){return(e.senderToNameMap[t.id]||t.id)+" "+(t.collapsed?"mini":"maxi")+"mized the chat"},textareaPlaceholder:function(e){return e.useSubmitButton?"Type something":"Type something and press enter to send"},submitButton:"Send"},n.version="1.0.0",n.description="Offers a uni-/bi-directional communication interface between players, or between players and the server.",n.title="Chat",n.className="chat",n.panel=!1,n.dependencies={JSUS:{}},n.prototype.init=function(n){var r,i,s,o,u;u=this,r=n.chatEvent;if(r){if("string"!=typeof r)throw new TypeError("Chat.init: chatEvent must be a non-empty string or undefined. Found: "+r);this.chatEvent=n.chatEvent}else this.chatEvent="CHAT";this.storeMsgs=!!n.storeMsgs,this.storeMsgs&&(this.db||(this.db=new t)),this.useSubmitButton="undefined"==typeof n.useSubmitButton?J.isMobileAgent():!!n.useSubmitButton,r=n.participants;if(!J.isArray(r)||!r.length)throw new TypeError("Chat.init: participants must be a non-empty array. Found: "+r);this.recipientsIds=new Array(r.length),this.recipientsIdsQuitted=[],this.recipientToSenderMap={},this.recipientToNameMap={},this.senderToNameMap={},this.senderToRecipientMap={};for(i=0;i"+this.title+""),this.stats.unread++)),!0)},n.prototype.disable=function(){this.submitButton&&(this.submitButton.disabled=!0),this.textarea.disabled=!0,this.disabled=!0},n.prototype.enable=function(){this.submitButton&&(this.submitButton.disabled=!1),this.textarea.disabled=!1,this.disabled=!1},n.prototype.getValues=function(){var e;return e={participants:this.participants,totSent:this.stats.sent,totReceived:this.stats.received,totUnread:this.stats.unread,initialMsg:this.initialMsg},this.db&&(e.msgs=db.fetch()),e}}(node),function(e){"use strict";function n(e){var t=this;this.options=null,this.table=null,this.sc=null,this.fp=null,this.canvas=null,this.changes=[],this.onChange=null,this.onChangeCb=function(e,n){"undefined"==typeof n&&(n=!1),e||(t.sc?e=t.sc.getValues():e=i.random()),t.draw(e,n)},this.timeFrom="step",this.features=null}function r(e,t){this.canvas=new W.Canvas(e),this.scaleX=e.width/n.width,this.scaleY=e.height/n.heigth,this.face=null}function i(e,t){var n;if("undefined"==typeof e)for(n in i.defaults)i.defaults.hasOwnProperty(n)&&(n==="color"?this.color="red":n==="lineWidth"?this.lineWidth=1:n==="scaleX"?this.scaleX=1:n==="scaleY"?this.scaleY=1:this[n]=i.defaults[n].min+Math.random()*i.defaults[n].range);else{if("object"!=typeof e)throw new TypeError("FaceVector constructor: faceVector must be object or undefined.");this.scaleX=e.scaleX||1,this.scaleY=e.scaleY||1,this.color=e.color||"green",this.lineWidth=e.lineWidth||1,t=t||i.defaults;for(n in t)t.hasOwnProperty(n)&&(e.hasOwnProperty(n)?this[n]=e[n]:this[n]=t?t[n]:i.defaults[n].value)}}var t=W.Table;e.widgets.register("ChernoffFaces",n),n.version="0.6.2",n.description="Display parametric data in the form of a Chernoff Face.",n.title="ChernoffFaces",n.className="chernofffaces",n.dependencies={JSUS:{},Table:{},Canvas:{},SliderControls:{}},n.FaceVector=i,n.FacePainter=r,n.width=100,n.height=100,n.onChange="CF_CHANGE",n.prototype.init=function(t){this.options=t,t.features?this.features=new i(t.features):this.features||(this.features=i.random()),this.fp&&this.fp.draw(this.features),t.onChange===!1||t.onChange===null?this.onChange&&(e.off(this.onChange,this.onChangeCb),this.onChange=null):(this.onChange="undefined"==typeof t.onChange?n.onChange:t.onChange,e.on(this.onChange,this.onChangeCb))},n.prototype.getCanvas=function(){return this.canvas},n.prototype.buildHTML=function(){var n,r,s,o;if(this.table)return;o=this.options,s={},this.id&&(s.id=this.id),"string"==typeof o.className?s.className=o.className:o.className!==!1&&(s.className="cf_table"),this.table=new t(s),this.canvas||this.buildCanvas();if("undefined"==typeof o.controls||o.controls)r=J.mergeOnKey(i.defaults,this.features,"value"),n={id:"cf_controls",features:r,onChange:this.onChange,submit:"Send"},"object"==typeof o.controls?this.sc=o.controls:this.sc=e.widgets.get("SliderControls",n);this.sc?this.table.addRow([{content:this.sc,id:this.id+"_td_controls"},{content:this.canvas,id:this.id+"_td_cf"}]):this.table.add({content:this.canvas,id:this.id+"_td_cf"}),this.table.parse()},n.prototype.buildCanvas=function(){var e;this.canvas||(e=this.options,e.canvas||(e.canvas={},"undefined"!=typeof e.height&&(e.canvas.height=e.height),"undefined"!=typeof e.width&&(e.canvas.width=e.width)),this.canvas=W.getCanvas("ChernoffFaces_canvas",e.canvas),this.fp=new r(this.canvas),this.fp.draw(this.features))},n.prototype.append=function(){this.table||this.buildHTML(),this.bodyDiv.appendChild(this.table.table)},n.prototype.draw=function(t,n){var r;if("object"!=typeof t)throw new TypeError("ChernoffFaces.draw: features must be object.");this.options.trackChanges&&("string"==typeof this.timeFrom?r=e.timer.getTimeSince(this.timeFrom):r=Date.now?Date.now():(new Date).getTime(),this.changes.push({time:r,change:t})),this.features=t instanceof i?t:new i(t,this.features),this.fp.redraw(this.features),this.sc&&n!==!1&&(this.sc.init({features:J.mergeOnKey(i.defaults,t,"value")}),this.sc.refresh())},n.prototype.getValues=function(e){return e&&e.changes?{changes:this.changes,cf:this.features}:this.fp.face},n.prototype.randomize=function(){var e;return e=i.random(),this.fp.redraw(e),this.sc&&(this.sc.init({features:J.mergeOnValue(i.defaults,e),onChange:this.onChange}),this.sc.refresh()),!0},r.prototype.draw=function(e,t,n){if(!e)return;this.face=e,this.fit2Canvas(e),this.canvas.scale(e.scaleX,e.scaleY),t=t||this.canvas.centerX,n=n||this.canvas.centerY,this.drawHead(e,t,n),this.drawEyes(e,t,n),this.drawPupils(e,t,n),this.drawEyebrow(e,t,n),this.drawNose(e,t,n),this.drawMouth(e,t,n)},r.prototype.redraw=function(e,t,n){this.canvas.clear(),this.draw(e,t,n)},r.prototype.scale=function(e,t){this.canvas.scale(this.scaleX,this.scaleY)},r.prototype.fit2Canvas=function(e){var t;if(!this.canvas){console.log("No canvas found");return}this.canvas.width>this.canvas.height?t=this.canvas.width/e.head_radius*e.head_scale_x:t=this.canvas.height/e.head_radius*e.head_scale_y,e.scaleX=t/2,e.scaleY=t/2},r.prototype.drawHead=function(e,t,n){var r=e.head_radius;this.canvas.drawOval({x:t,y:n,radius:r,scale_x:e.head_scale_x,scale_y:e.head_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyes=function(e,t,n){var i=r.computeFaceOffset(e,e.eye_height,n),s=e.eye_spacing,o=e.eye_radius;this.canvas.drawOval({x:t-s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawPupils=function(e,t,n){var i=e.pupil_radius,s=e.eye_spacing,o=r.computeFaceOffset(e,e.eye_height,n);this.canvas.drawOval({x:t-s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyebrow=function(e,t,n){var i=r.computeEyebrowOffset(e,n),s=e.eyebrow_spacing,o=e.eyebrow_length,u=e.eyebrow_angle;this.canvas.drawLine({x:t-s,y:i,length:o,angle:u,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawLine({x:t+s,y:i,length:0-o,angle:-u,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawNose=function(e,t,n){var i=r.computeFaceOffset(e,e.nose_height,n),s=t+e.nose_width/2,o=i+e.nose_length,u=s-e.nose_width,a=o;this.canvas.ctx.lineWidth=e.lineWidth,this.canvas.ctx.strokeStyle=e.color,this.canvas.ctx.save(),this.canvas.ctx.beginPath(),this.canvas.ctx.moveTo(t,i),this.canvas.ctx.lineTo(s,o),this.canvas.ctx.lineTo(u,a),this.canvas.ctx.stroke(),this.canvas.ctx.restore()},r.prototype.drawMouth=function(e,t,n){var i=r.computeFaceOffset(e,e.mouth_height,n),s=t-e.mouth_width/2,o=t+e.mouth_width/2,u=i-e.mouth_top_y,a=i+e.mouth_bottom_y;this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,u,o,i),this.canvas.ctx.stroke(),this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,a,o,i),this.canvas.ctx.stroke()},r.computeFaceOffset=function(e,t,n){n=n||0;var r=n-e.head_radius+e.head_radius*2*t;return r},r.computeEyebrowOffset=function(e,t){t=t||0;var n=2;return r.computeFaceOffset(e,e.eye_height,t)-n-e.eyebrow_eyedistance},i.defaults={head_radius:{min:10,max:100,step:.01,value:30,label:"Face radius"},head_scale_x:{min:.2,max:2,step:.01,value:.5,label:"Scale head horizontally"},head_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale head vertically"},eye_height:{min:.1,max:.9,step:.01,value:.4,label:"Eye height"},eye_radius:{min:2,max:30,step:.01,value:5,label:"Eye radius"},eye_spacing:{min:0,max:50,step:.01,value:10,label:"Eye spacing"},eye_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale eyes horizontally"},eye_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale eyes vertically"},pupil_radius:{min:1,max:9,step:.01,value:1,label:"Pupil radius"},pupil_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale pupils horizontally"},pupil_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale pupils vertically"},eyebrow_length:{min:1,max:30,step:.01,value:10,label:"Eyebrow length"},eyebrow_eyedistance:{min:.3,max:10,step:.01,value:3,label:"Eyebrow from eye"},eyebrow_angle:{min:-2,max:2,step:.01,value:-0.5,label:"Eyebrow angle"},eyebrow_spacing:{min:0,max:20,step:.01,value:5,label:"Eyebrow spacing"},nose_height:{min:.4,max:1,step:.01,value:.4,label:"Nose height"},nose_length:{min:.2,max:30,step:.01,value:15,label:"Nose length"},nose_width:{min:0,max:30,step:.01,value:10,label:"Nose width"},mouth_height:{min:.2,max:2,step:.01,value:.75,label:"Mouth height"},mouth_width:{min:2,max:100,step:.01,value:20,label:"Mouth width"},mouth_top_y:{min:-10,max:30,step:.01,value:-2,label:"Upper lip"},mouth_bottom_y:{min:-10,max:30,step:.01,value:20,label:"Lower lip"},scaleX:{min:0,max:20,step:.01,value:.2,label:"Scale X"},scaleY:{min:0,max:20,step:.01,value:.2,label:"Scale Y"},color:{min:0,max:20,step:.01,value:.2,label:"color"},lineWidth:{min:0,max:20,step:.01,value:.2,label:"lineWidth"}},function(e){var t;for(t in e)e.hasOwnProperty(t)&&(e[t].range=e[t].max-e[t].min)}(i.defaults),i.random=function(){return console.log("*** FaceVector.random is deprecated. Use new FaceVector() instead."),new i}}(node),function(e){"use strict";function n(n){this.options=n,this.id=n.id,this.table=new t({id:"cf_table"}),this.root=n.root||document.createElement("div"),this.root.id=this.id,this.sc=e.widgets.get("Controls.Slider"),this.fp=null,this.canvas=null,this.dims=null,this.change="CF_CHANGE";var r=this;this.changeFunc=function(){r.draw(r.sc.getAllValues())},this.features=null,this.controls=null}function r(e,t){this.canvas=new W.Canvas(e),this.scaleX=e.width/n.defaults.canvas.width,this.scaleY=e.height/n.defaults.canvas.heigth}function i(e){e=e||{},this.scaleX=e.scaleX||1,this.scaleY=e.scaleY||1,this.color=e.color||"green",this.lineWidth=e.lineWidth||1;for(var t in i.defaults)i.defaults.hasOwnProperty(t)&&(e.hasOwnProperty(t)?this[t]=e[t]:this[t]=i.defaults[t].value)}var t=W.Table;e.widgets.register("ChernoffFacesSimple",n),n.defaults={},n.defaults.id="ChernoffFaces",n.defaults.canvas={},n.defaults.canvas.width=100,n.defaults.canvas.heigth=100,n.version="0.4",n.description="Display parametric data in the form of a Chernoff Face.",n.dependencies={JSUS:{},Table:{},Canvas:{},"Controls.Slider":{}},n.FaceVector=i,n.FacePainter=r,n.prototype.init=function(t){this.id=t.id||this.id;var s=this.id+"_";this.features=t.features||this.features||i.random(),this.controls="undefined"!=typeof t.controls?t.controls:!0;var o=t.idCanvas?t.idCanvas:s+"canvas";this.dims={width:t.width?t.width:n.defaults.canvas.width,height:t.height?t.height:n.defaults.canvas.heigth},this.canvas=W.getCanvas(o,this.dims),this.fp=new r(this.canvas),this.fp.draw(new i(this.features));var u={id:"cf_controls",features:J.mergeOnKey(i.defaults,this.features,"value"),change:this.change,fieldset:{id:this.id+"_controls_fieldest",legend:this.controls.legend||"Controls"},submit:"Send"};this.sc=e.widgets.get("Controls.Slider",u),this.controls&&this.table.add(this.sc),"undefined"==typeof t.change?e.on(this.change,this.changeFunc):(t.change?e.on(t.change,this.changeFunc):e.removeListener(this.change,this.changeFunc),this.change=t.change),this.table.add(this.canvas),this.table.parse(),this.root.appendChild(this.table.table)},n.prototype.getRoot=function(){return this.root},n.prototype.getCanvas=function(){return this.canvas},n.prototype.append=function(e){return e.appendChild(this.root),this.table.parse(),this.root},n.prototype.listeners=function(){},n.prototype.draw=function(e){if(!e)return;var t=new i(e);this.fp.redraw(t),this.sc.init({features:J.mergeOnKey(i.defaults,e,"value")}),this.sc.refresh()},n.prototype.getAllValues=function(){return this.fp.face},n.prototype.randomize=function(){var e=i.random();this.fp.redraw(e);var t={features:J.mergeOnKey(i.defaults,e,"value"),change:this.change};return this.sc.init(t),this.sc.refresh(),!0},r.prototype.draw=function(e,t,n){if(!e)return;this.face=e,this.fit2Canvas(e),this.canvas.scale(e.scaleX,e.scaleY),t=t||this.canvas.centerX,n=n||this.canvas.centerY,this.drawHead(e,t,n),this.drawEyes(e,t,n),this.drawPupils(e,t,n),this.drawEyebrow(e,t,n),this.drawNose(e,t,n),this.drawMouth(e,t,n)},r.prototype.redraw=function(e,t,n){this.canvas.clear(),this.draw(e,t,n)},r.prototype.scale=function(e,t){this.canvas.scale(this.scaleX,this.scaleY)},r.prototype.fit2Canvas=function(e){var t;if(!this.canvas){console.log("No canvas found");return}this.canvas.width>this.canvas.height?t=this.canvas.width/e.head_radius*e.head_scale_x:t=this.canvas.height/e.head_radius*e.head_scale_y,e.scaleX=t/2,e.scaleY=t/2},r.prototype.drawHead=function(e,t,n){var r=e.head_radius;this.canvas.drawOval({x:t,y:n,radius:r,scale_x:e.head_scale_x,scale_y:e.head_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyes=function(e,t,n){var i=r.computeFaceOffset(e,e.eye_height,n),s=e.eye_spacing,o=e.eye_radius;this.canvas.drawOval({x:t-s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:i,radius:o,scale_x:e.eye_scale_x,scale_y:e.eye_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawPupils=function(e,t,n){var i=e.pupil_radius,s=e.eye_spacing,o=r.computeFaceOffset(e,e.eye_height,n);this.canvas.drawOval({x:t-s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawOval({x:t+s,y:o,radius:i,scale_x:e.pupil_scale_x,scale_y:e.pupil_scale_y,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawEyebrow=function(e,t,n){var i=r.computeEyebrowOffset(e,n),s=e.eyebrow_spacing,o=e.eyebrow_length,u=e.eyebrow_angle;this.canvas.drawLine({x:t-s,y:i,length:o,angle:u,color:e.color,lineWidth:e.lineWidth}),this.canvas.drawLine({x:t+s,y:i,length:0-o,angle:-u,color:e.color,lineWidth:e.lineWidth})},r.prototype.drawNose=function(e,t,n){var i=r.computeFaceOffset(e,e.nose_height,n),s=t+e.nose_width/2,o=i+e.nose_length,u=s-e.nose_width,a=o;this.canvas.ctx.lineWidth=e.lineWidth,this.canvas.ctx.strokeStyle=e.color,this.canvas.ctx.save(),this.canvas.ctx.beginPath(),this.canvas.ctx.moveTo(t,i),this.canvas.ctx.lineTo(s,o),this.canvas.ctx.lineTo(u,a),this.canvas.ctx.stroke(),this.canvas.ctx.restore()},r.prototype.drawMouth=function(e,t,n){var i=r.computeFaceOffset(e,e.mouth_height,n),s=t-e.mouth_width/2,o=t+e.mouth_width/2,u=i-e.mouth_top_y,a=i+e.mouth_bottom_y;this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,u,o,i),this.canvas.ctx.stroke(),this.canvas.ctx.moveTo(s,i),this.canvas.ctx.quadraticCurveTo(t,a,o,i),this.canvas.ctx.stroke()},r.computeFaceOffset=function(e,t,n){n=n||0;var r=n-e.head_radius+e.head_radius*2*t;return r},r.computeEyebrowOffset=function(e,t){t=t||0;var n=2;return r.computeFaceOffset(e,e.eye_height,t)-n-e.eyebrow_eyedistance},i.defaults={head_radius:{min:10,max:100,step:.01,value:30,label:"Face radius"},head_scale_x:{min:.2,max:2,step:.01,value:.5,label:"Scale head horizontally"},head_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale head vertically"},eye_height:{min:.1,max:.9,step:.01,value:.4,label:"Eye height"},eye_radius:{min:2,max:30,step:.01,value:5,label:"Eye radius"},eye_spacing:{min:0,max:50,step:.01,value:10,label:"Eye spacing"},eye_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale eyes horizontally"},eye_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale eyes vertically"},pupil_radius:{min:1,max:9,step:.01,value:1,label:"Pupil radius"},pupil_scale_x:{min:.2,max:2,step:.01,value:1,label:"Scale pupils horizontally"},pupil_scale_y:{min:.2,max:2,step:.01,value:1,label:"Scale pupils vertically"},eyebrow_length:{min:1,max:30,step:.01,value:10,label:"Eyebrow length"},eyebrow_eyedistance:{min:.3,max:10,step:.01,value:3,label:"Eyebrow from eye"},eyebrow_angle:{min:-2,max:2,step:.01,value:-0.5,label:"Eyebrow angle"},eyebrow_spacing:{min:0,max:20,step:.01,value:5,label:"Eyebrow spacing"},nose_height:{min:.4,max:1,step:.01,value:.4,label:"Nose height"},nose_length:{min:.2,max:30,step:.01,value:15,label:"Nose length"},nose_width:{min:0,max:30,step:.01,value:10,label:"Nose width"},mouth_height:{min:.2,max:2,step:.01,value:.75,label:"Mouth height"},mouth_width:{min:2,max:100,step:.01,value:20,label:"Mouth width"},mouth_top_y:{min:-10,max:30,step:.01,value:-2,label:"Upper lip"},mouth_bottom_y:{min:-10,max:30,step:.01,value:20,label:"Lower lip"}},i.random=function(){var e={};for(var t in i.defaults)i.defaults.hasOwnProperty(t)&&(J.inArray(t,["color","lineWidth","scaleX","scaleY"])||(e[t]=i.defaults[t].min+Math.random()*i.defaults[t].max));return e.scaleX=1,e.scaleY=1,e.color="green",e.lineWidth=1,new i(e)},i.prototype.shuffle=function(){for(var e in this)this.hasOwnProperty(e)&&i.defaults.hasOwnProperty(e)&&e!=="color"&&(this[e]=i.defaults[e].min+Math.random()*i.defaults[e].max)},i.prototype.distance=function(e){return i.distance(this,e)},i.distance=function(e,t){var n=0,r;for(var i in e)e.hasOwnProperty(i)&&(r=e[i]-t[i],n+=r*r);return Math.sqrt(n)},i.prototype.toString=function(){var e="Face: ";for(var t in this)this.hasOwnProperty(t)&&(e+=t+" "+this[t]);return e}}(node),function(e){"use strict";function t(){var e;e=this,this.dl=null,this.mainText=null,this.spanMainText=null,this.forms=null,this.formsById=null,this.order=null,this.shuffleForms=null,this.group=null,this.groupOrder=null,this.formsOptions={title:!1,frame:!1,storeRef:!1},this.freeText=null,this.textarea=null}e.widgets.register("ChoiceManager",t),t.version="1.2.0",t.description="Groups together and manages a set of selectable choices forms (e.g. ChoiceTable).",t.title=!1,t.className="choicemanager",t.dependencies={JSUS:{}},t.prototype.init=function(e){var t,n;n=this,"undefined"==typeof e.shuffleForms?t=!1:t=!!e.shuffleForms,this.shuffleForms=t;if("string"==typeof e.group||"number"==typeof e.group)this.group=e.group;else if("undefined"!=typeof e.group)throw new TypeError("ChoiceManager.init: options.group must be string, number or undefined. Found: "+e.group);if("number"==typeof e.groupOrder)this.groupOrder=e.groupOrder;else if("undefined"!=typeof e.group)throw new TypeError("ChoiceManager.init: options.groupOrder must be number or undefined. Found: "+e.groupOrder);if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("ChoiceManager.init: options.mainText must be string or undefined. Found: "+e.mainText);if("undefined"!=typeof e.formsOptions){if("object"!=typeof e.formsOptions)throw new TypeError("ChoiceManager.init: options.formsOptions must be object or undefined. Found: "+e.formsOptions);if(e.formsOptions.hasOwnProperty("name"))throw new Error("ChoiceManager.init: options.formsOptions cannot contain property name. Found: "+e.formsOptions);this.formsOptions=J.mixin(this.formsOptions,e.formsOptions)}this.freeText="string"==typeof e.freeText?e.freeText:!!e.freeText,"undefined"!=typeof e.forms&&this.setForms(e.forms)},t.prototype.setForms=function(t){var n,r,i,s,o;if("function"==typeof t){o=t.call(e.game);if(!J.isArray(o))throw new TypeError("ChoiceManager.setForms: forms is a callback, but did not returned an array. Found: "+o)}else{if(!J.isArray(t))throw new TypeError("ChoiceManager.setForms: forms must be array or function. Found: "+t);o=t}s=o.length;if(!s)throw new Error("ChoiceManager.setForms: forms is an empty array.");r={},t=new Array(s),i=-1;for(;++i 1. Found: "+n)}this.selectMultiple=n,n&&(this.selected=[],this.currentChoice=[]);if("number"==typeof e.requiredChoice){if(!J.isInt(e.requiredChoice,0))throw new Error("ChoiceTable.init: if number, requiredChoice must a positive integer. Found: "+e.requiredChoice);if("number"==typeof this.selectMultiple&&e.requiredChoice>this.selectMultiple)throw new Error("ChoiceTable.init: requiredChoice cannot be larger than selectMultiple. Found: "+e.requiredChoice+" > "+this.selectMultiple);this.requiredChoice=e.requiredChoice}else if("boolean"==typeof e.requiredChoice)this.requiredChoice=e.requiredChoice?1:null;else if("undefined"!=typeof e.requiredChoice)throw new TypeError("ChoiceTable.init: options.requiredChoice be number, boolean or undefined. Found: "+e.requiredChoice);if("string"==typeof e.group||"number"==typeof e.group)this.group=e.group;else if("undefined"!=typeof e.group)throw new TypeError("ChoiceTable.init: options.group must be string, number or undefined. Found: "+e.group);if("number"==typeof e.groupOrder)this.groupOrder=e.groupOrder;else if("undefined"!=typeof e.groupOrder)throw new TypeError("ChoiceTable.init: options.groupOrder must be number or undefined. Found: "+e.groupOrder);if("function"==typeof e.listener)this.listener=function(t){e.listener.call(this,t)};else if("undefined"!=typeof e.listener)throw new TypeError("ChoiceTable.init: options.listener must be function or undefined. Found: "+e.listener);if("function"==typeof e.onclick)this.onclick=e.onclick;else if("undefined"!=typeof e.onclick)throw new TypeError("ChoiceTable.init: options.onclick must be function or undefined. Found: "+e.onclick);if("string"==typeof e.mainText)this.mainText=e.mainText;else if("undefined"!=typeof e.mainText)throw new TypeError("ChoiceTable.init: options.mainText must be string or undefined. Found: "+e.mainText);if("string"==typeof e.hint||!1===e.hint)this.hint=e.hint;else{if("undefined"!=typeof e.hint)throw new TypeError("ChoiceTable.init: options.hint must be a string, false, or undefined. Found: "+e.hint);this.hint=this.getText("autoHint")}if(e.timeFrom===!1||"string"==typeof e.timeFrom)this.timeFrom=e.timeFrom;else if("undefined"!=typeof e.timeFrom)throw new TypeError("ChoiceTable.init: options.timeFrom must be string, false, or undefined. Found: "+e.timeFrom);if("string"==typeof e.separator)this.separator=e.separator;else if("undefined"!=typeof e.separator)throw new TypeError("ChoiceTable.init: options.separator must be string, or undefined. Found: "+e.separator);if(this.id.indexOf(e.separator)!==-1)throw new Error("ChoiceTable.init: options.separator cannot be a sequence of characters included in the table id. Found: "+e.separator);if("string"==typeof e.left||"number"==typeof e.left)this.left=""+e.left;else if(J.isNode(e.left)||J.isElement(e.left))this.left=e.left;else if("undefined"!=typeof e.left)throw new TypeError("ChoiceTable.init: options.left must be string, number, an HTML Element or undefined. Found: "+e.left);if("string"==typeof e.right||"number"==typeof e.right)this.right=""+e.right;else if(J.isNode(e.right)||J.isElement(e.right))this.right=e.right;else if("undefined"!=typeof e.right)throw new TypeError("ChoiceTable.init: options.right must be string, number, an HTML Element or undefined. Found: "+e.right);if("undefined"==typeof e.className)this.className=t.className;else{if(e.className!==!1&&"string"!=typeof e.className&&!J.isArray(e.className))throw new TypeError("ChoiceTable.init: options.className must be string, array, or undefined. Found: "+e.className);this.className=e.className}if("function"==typeof e.renderer)this.renderer=e.renderer;else if("undefined"!=typeof e.renderer)throw new TypeError("ChoiceTable.init: options.renderer must be function or undefined. Found: "+e.renderer);if("object"==typeof e.table)this.table=e.table;else if("undefined"!=typeof e.table&&!1!==e.table)throw new TypeError("ChoiceTable.init: options.table must be object, false or undefined. Found: "+e.table);this.table=e.table,this.freeText="string"==typeof e.freeText?e.freeText:!!e.freeText,"undefined"!=typeof e.choices&&this.setChoices(e.choices);if("undefined"!=typeof e.correctChoice){if(this.requiredChoice)throw new Error("ChoiceTable.init: cannot specify both options requiredChoice and correctChoice");this.setCorrectChoice(e.correctChoice)}if("undefined"!=typeof e.choicesSetSize){if(!J.isInt(e.choicesSetSize,0))throw new Error("ChoiceTable.init: choicesSetSize must be undefined or an integer > 0. Found: "+e.choicesSetSize);this.choicesSetSize=e.choicesSetSize}},t.prototype.setChoices=function(e){var t;if(!J.isArray(e))throw new TypeError("ChoiceTable.setChoices: choices must be array.");if(!e.length)throw new Error("ChoiceTable.setChoices: choices is empty array.");this.choices=e,t=e.length,this.order=J.seq(0,t-1),this.shuffleChoices&&(this.originalOrder=this.order,this.order=J.shuffle(this.order)),this.table?this.buildTableAndChoices():this.buildChoices()},t.prototype.buildChoices=function(){var e,t;e=-1,t=this.choices.length,this.choicesCells=new Array(t);for(;++e=this.choicesSetSize)break}this.rightCell&&(i||(o=r(this,"right")),o.appendChild(this.rightCell)),t!==n&&e.call(this,t,n,i,s)}return function(){var t,n,r,i;if(!this.choicesCells)throw new Error("ChoiceTable.buildTable: choices not set, cannot build table. Id: "+this.id);r=this.orientation==="H",n=this.choicesCells.length,i="number"==typeof this.choicesSetSize,e.call(this,-1,n,r,i),this.enable()}}(),t.prototype.buildTableAndChoices=function(){var e,t,n,i,s;t=this.choices.length,this.choicesCells=new Array(t),e=-1,s=this.orientation==="H",s&&(n=r(this,"main"),this.left&&(i=this.renderSpecial("left",this.left),n.appendChild(i)));for(;++e=this.requiredChoice:this.currentChoice!==null;if(!this.correctChoice)return null;e="undefined"==typeof e?!0:e,e&&this.attempts.push(this.currentChoice);if(!this.selectMultiple)return this.currentChoice===this.correctChoice;a=J.isArray(this.correctChoice)?this.correctChoice:[this.correctChoice],n=a.length,i=this.currentChoice.length;if(n!==i)return!1;t=-1,o=this.currentChoice.slice(0);for(;++t",{id:t}).slider(),i=r.appendTo(e);return i[0]},r.prototype.getItem=function(e,t){var n=jQuery("
",{id:e}).slider();return n},i.prototype.__proto__=t.prototype,i.prototype.constructor=i,i.version="0.1.2",i.description="Collection of Radio Controls.",i.title="Radio Controls",i.className="radiocontrols",i.dependencies={Controls:{}},e.widgets.register("RadioControls",i),i.prototype.populate=function(){var t,n,r,i,s;s=this,this.radioElem||(this.radioElem=document.createElement("radio"),this.radioElem.group=this.name||"radioGroup",this.radioElem.group=this.className||"radioGroup",this.bodyDiv.appendChild(this.radioElem));for(t in this.features)this.features.hasOwnProperty(t)&&(r=this.features[t],n=t,r.id&&(n=r.id,delete r.id),i=this.add(this.radioElem,n,r),this.changeEvent&&(i.onchange=function(){e.emit(s.changeEvent)}),this.list.addDT(i))},i.prototype.add=function(e,t,n){var r;return"undefined"==typeof n.name&&(n.name=this.groupName),n.id=t,n.type="radio",r=W.add("input",e,n),r.appendChild(document.createTextNode(n.label)),r},i.prototype.getItem=function(e,t){return t=t||{},"undefined"==typeof t.name&&(t.name=this.groupName),t.id=e,t.type="radio",W.get("input",t)},i.prototype.getValues=function(){var e,t;for(e in this.features)if(this.features.hasOwnProperty(e)){t=W.getElementById(e);if(t.checked)return t.value}return!1}}(node),function(e){"use strict";function a(){this.input=null,this.placeholder=null,this.inputWidth=null,this.type=null,this.preprocess=null,this.validation=null,this.validationSpeed=500,this.postprocess=null,this.params={},this.errorBox=null,this.mainText=null,this.hint=null,this.requiredChoice=null,this.timeBegin=null,this.timeEnd=null}e.widgets.register("CustomInput",a),a.version="0.7.0",a.description="Creates a configurable input form",a.title=!1,a.panel=!1,a.className="custominput",a.types={text:!0,number:!0,"float":!0,"int":!0,date:!0,list:!0,us_city_state_zip:!0};var t={",":"comma"," ":"space",".":"dot"},n={Alabama:"AL",Alaska:"AK",Arizona:"AZ",Arkansas:"AR",California:"CA",Colorado:"CO",Connecticut:"CT",Delaware:"DE",Florida:"FL",Georgia:"GA",Hawaii:"HI",Idaho:"ID",Illinois:"IL",Indiana:"IN",Iowa:"IA",Kansas:"KS",Kentucky:"KY",Louisiana:"LA",Maine:"ME",Maryland:"MD",Massachusetts:"MA",Michigan:"MI",Minnesota:"MN",Mississippi:"MS",Missouri:"MO",Montana:"MT",Nebraska:"NE",Nevada:"NV","New Hampshire":"NH","New Jersey":"NJ","New Mexico":"NM","New York":"NY","North Carolina":"NC","North Dakota":"ND",Ohio:"OH",Oklahoma:"OK",Oregon:"OR",Pennsylvania:"PA","Rhode Island":"RI","South Carolina":"SC","South Dakota":"SD",Tennessee:"TN",Texas:"TX",Utah:"UT",Vermont:"VT",Virginia:"VA",Washington:"WA","West Virginia":"WV",Wisconsin:"WI",Wyoming:"WY"},r={"American Samoa":"AS","District of Columbia":"DC","Federated States of Micronesia":"FM",Guam:"GU","Marshall Islands":"MH","Northern Mariana Islands":"MP",Palau:"PW","Puerto Rico":"PR","Virgin Islands":"VI"},i,s,o,u;a.texts={listErr:function(e,t){return t==="min"?"Too few items. Min: "+e.params.minItems:t==="max"?"Too many items. Max: "+e.params.maxItems:"Check that there are no empty items; do not end with the separator"},autoHint:function(e){var n,r;return e.type==="list"&&(r=t[e.params.listSep]||e.params.listSep,n="(if more than one, separate with "+r+")"),e.requiredChoice?n+"*":n||!1},numericErr:function(e){var t,n,r;return n=e.params,n.exactly?"Must enter "+n.lower:(r="(inclusive)",t="Must be a",e.type==="float"?t+="floating point":e.type==="int"&&(t+="n integer"),t+=" number ",n.between?(t+="between "+n.lower,n.leq&&(t+=r),t+=" and "+n.upper,n.ueq&&(t+=r)):"undefined"!=typeof n.lower?(t+="greater than ",n.leq&&(t+="or equal to "),t+=n.lower):(t+="less than ",n.leq&&(t+="or equal to "),t+=n.upper),t)},textErr:function(e,t){var n,r;return r=e.params,n="Must be ",r.exactly?n+="exactly "+(r.lower+1):r.between?n+="between "+r.lower+" and "+r.upper:"undefined"!=typeof r.lower?n+=" more than "+(r.lower-1):"undefined"!=typeof r.upper&&(n+=" less than "+(r.upper+1)),n+=" characters long",r.between&&(n+=" (extremes included)"),n+=". Current length: "+t,n},dateErr:function(e,t){return t?"Date is invalid":"Must follow format "+e.params.format},emptyErr:function(e){return"Cannot be empty"}},a.dependencies={JSUS:{}},a.prototype.init=function(e){var t,i,s,f;i=this,s="CustomInput.init: ",this.requiredChoice=!!e.requiredChoice;if(e.type){if(!a.types[e.type])throw new Error(s+"type not supported: "+e.type);this.type=e.type}else this.type="text";if(e.validation){if("function"!=typeof e.validation)throw new TypeError(s+"validation must be function "+"or undefined. Found: "+e.validation);t=e.validation}else if(this.type==="number"||this.type==="float"||this.type==="int"||this.type==="text"){f=this.type==="text";if("undefined"!=typeof e.min){t=J.isNumber(e.min);if(!1===t)throw new TypeError(s+"min must be number or "+"undefined. Found: "+e.min);this.params.lower=e.min,this.params.leq=!0}if("undefined"!=typeof e.max){t=J.isNumber(e.max);if(!1===t)throw new TypeError(s+"max must be number or "+"undefined. Found: "+e.max);this.params.upper=e.max,this.params.ueq=!0}e.strictlyGreater&&(this.params.leq=!1),e.strictlyLess&&(this.params.ueq=!1);if("undefined"!=typeof this.params.lower&&"undefined"!=typeof this.params.upper){if(this.params.lower>this.params.upper)throw new TypeError(s+"min cannot be greater "+"than max. Found: "+e.min+"> "+e.max);if(this.params.lower===this.params.upper){if(!this.params.leq||!this.params.ueq)throw new TypeError(s+"min cannot be equal to "+"max when strictlyGreater or "+"strictlyLess are set. "+"Found: "+e.min);if(this.type==="int"||this.type==="text")if(J.isFloat(this.params.lower))throw new TypeError(s+"min cannot be a "+"floating point number "+"and equal to "+"max, when type "+'is not "float". Found: '+e.min);this.params.exactly=!0}else this.params.between=!0}if(f){if("undefined"!=typeof this.params.lower){if(this.params.lower<0)throw new TypeError(s+"min cannot be negative "+'when type is "text". Found: '+this.params.lower);this.params.leq||this.params.lower++}if("undefined"!=typeof this.params.upper){if(this.params.upper<0)throw new TypeError(s+"max cannot be negative "+'when type is "text". Found: '+this.params.upper);this.params.ueq||this.params.upper--}t=function(e){var t,n,r,s;n=i.params,t=e.length,r={value:e};if(n.exactly)s=t!==n.lower;else if("undefined"!=typeof n.lower&&tn.upper)s=!0;return s&&(r.err=i.getText("textErr",t)),r}}else t=function(){var e;return i.type==="float"?e=J.isFloat:i.type==="int"?e=J.isInt:e=J.isNumber,function(t){var n,r;return r=i.params,n=e(t,r.lower,r.upper,r.leq,r.ueq),n!==!1?{value:n}:{value:t,err:i.getText("numericErr")}}}();this.params.upper&&(this.params.upper<10?this.inputWidth="100px":this.params.upper<20&&(this.inputWidth="200px"))}else if(this.type==="date"){if("undefined"!=typeof e.format){if(e.format!=="mm-dd-yy"&&e.format!=="dd-mm-yy"&&e.format!=="mm-dd-yyyy"&&e.format!=="dd-mm-yyyy"&&e.format!=="mm.dd.yy"&&e.format!=="dd.mm.yy"&&e.format!=="mm.dd.yyyy"&&e.format!=="dd.mm.yyyy"&&e.format!=="mm/dd/yy"&&e.format!=="dd/mm/yy"&&e.format!=="mm/dd/yyyy"&&e.format!=="dd/mm/yyyy")throw new Error(s+"date format is invalid. Found: "+e.format);this.params.format=e.format}else this.params.format="mm/dd/yyyy";this.params.sep=this.params.format.charAt(2),t=this.params.format.split(this.params.sep),this.params.yearDigits=t[2].length,this.params.dayPos=t[0].charAt(0)==="d"?0:1,this.params.monthPos=this.params.dayPos?0:1,this.params.dateLen=t[2].length+6,this.params.yearDigits===2?this.inputWidth="100px":this.inputWidth="150px",this.placeholder=this.params.format,t=function(e){var t,n,r,s,o,u,a,f;return t=i.params,n=e.split(t.sep),n.length!==3?{err:i.getText("dateErr")}:n[2].length!==t.yearDigits?{err:i.getText("dateErr")}:(o={},t.yearDigits===2?(a=-1,f=100):(a=-1,f=1e4),r=J.isInt(n[2],a,f),r!==!1?o.year=r:s=!0,r=J.isInt(n[t.monthPos],1,12,1,1),r?o.month=r:s=!0,r===1||r===3||r===5||r===7||r===8||r===10||r===12?u=31:r!==2?u=30:u=o.year%4===0&&o.year%100!==0||o.year%400===0?29:28,o.month=r,r=J.isInt(n[t.dayPos],1,u,1,1),r?o.day=r:s=!0,s&&(o.err=i.getText("dateErr",!0)),o)}}else if(this.type==="list"||this.type==="us_city_state_zip"){if(e.listSeparator){if("string"!=typeof e.listSeparator)throw new TypeError(s+"listSeparator must be "+"string or undefined. Found: "+e.listSeperator);this.params.listSep=e.listSeparator}else this.params.listSep=",";if(this.type==="us_city_state_zip")u||(o=J.mixin(n,r),u=J.reverseObj(o)),this.params.minItems=this.params.maxItems=3,this.params.itemValidation=function(e,t){if(t===2&&!u[e])return{err:i.getText("usStateErr")}};else{if("undefined"!=typeof e.minItems){t=J.isInt(e.minItems,0);if(t===!1)throw new TypeError(s+"minItems must be "+"a positive integer. Found: "+e.minItems);this.params.minItems=t}if("undefined"!=typeof e.maxItems){t=J.isInt(e.maxItems,0);if(t===!1)throw new TypeError(s+"maxItems must be "+"a positive integer. Found: "+e.maxItems);if(this.params.minItems&&this.params.minItems>t)throw new TypeError(s+"maxItems must be larger "+"than minItems. Found: "+t+" < "+this.params.minItems);this.params.maxItems=t}}t=function(e){var t,n,r,s,o;e=e.split(i.params.listSep),n=e.length;if(i.params.minItems&&ni.params.maxItems)return{err:i.getText("listErr","max")};if(!n)return e;s=i.params.itemValidation,t=0,r=e[0].trim();if(!r)return{err:i.getText("listErr")};if(s){o=s(r,1);if(o)return o}e[t++]=r;if(n>1){r=e[1].trim();if(!r)return{err:i.getText("listErr")};if(s){o=s(r,t+1);if(o)return o}e[t++]=r}if(n>2){r=e[2].trim();if(!r)return{err:i.getText("listErr")};if(s){o=s(r,t+1);if(o)return o}e[t++]=r}if(n>3)for(;tthis.params.dateLen&&(e.value=e.value.substring(0,this.params.dateLen))}:this.type==="list"&&this.params.listSep.trim()!==""&&(this.preprocess=function(e){var t,n;n=e.value.length,t=i.params.listSep,n>1&&n===e.selectionStart&&e.value.charAt(n-1)===t&&e.value.charAt(n-2)!==t&&(e.value+=" ")}));if(e.postprocess){if("function"!=typeof e.postprocess)throw new TypeError(s+"postprocess must be function or "+"undefined. Found: "+e.postprocess);this.postprocess=e.postprocess}else this.type==="date"&&(this.postprocess=function(e,t){return!t||!e?e:{value:e,day:e.substring(0,2),month:e.substring(3,5),year:e.subtring(6,e.length)}});if("undefined"!=typeof e.validationSpeed){t=J.isInt(e.valiadtionSpeed,0,undefined,!0);if(t===!1)throw new TypeError(s+"validationSpeed must a non-negative "+"number or undefined. Found: "+e.validationSpeed);this.validationSpeed=t}if(e.mainText){if("string"!=typeof e.mainText)throw new TypeError(s+"mainText must be string or "+"undefined. Found: "+e.mainText);this.mainText=e.mainText}if("undefined"!=typeof e.hint){if(!1!==e.hint&&"string"!=typeof e.hint)throw new TypeError(s+"hint must be a string, false, or "+"undefined. Found: "+e.hint);this.hint=e.hint}else this.hint=this.getText("autoHint");if(e.placeholder){if("string"!=typeof e.placeholder)throw new TypeError(s+"placeholder must be string or "+"undefined. Found: "+e.placeholder);this.placeholder=e.placeholder}if(e.width){if("string"!=typeof e.width)throw new TypeError(s+"width must be string or "+"undefined. Found: "+e.width);this.inputWidth=e.width}},a.prototype.append=function(){var t,n;t=this,this.mainText&&(this.spanMainText=W.append("span",this.bodyDiv,{className:"custominput-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.spanMainText||this.bodyDiv,{className:"choicetable-hint",innerHTML:this.hint}),this.input=W.append("input",this.bodyDiv),this.placeholder&&(this.input.placeholder=this.placeholder),this.inputWidth&&(this.input.style.width=this.inputWidth),this.errorBox=W.append("div",this.bodyDiv,{className:"errbox"}),this.input.oninput=function(){t.timeBegin?t.timeEnd=e.timer.getTimeSince("step"):t.timeEnd=t.timeBegin=e.timer.getTimeSince("step"),n&&clearTimeout(n),t.isHighlighted()&&t.unhighlight(),t.preprocess&&t.preprocess(t.input),n=setTimeout(function(){var e;t.validation&&(e=t.validation(t.input.value),e.err&&t.setError(e.err))},t.validationSpeed)},this.input.onclick=function(){t.isHighlighted()&&t.unhighlight()}},a.prototype.setError=function(e){this.errorBox.innerHTML=e,this.highlight()},a.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("CustomInput.highlight: border must be string or undefined. Found: "+e);if(!this.input||this.highlighted)return;this.input.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},a.prototype.unhighlight=function(){if(!this.input||this.highlighted!==!0)return;this.input.style.border="",this.highlighted=!1,this.errorBox.innerHTML="",this.emit("unhighlighted")},a.prototype.reset=function(){this.input&&(this.input.value=""),this.isHighlighted()&&this.unhighlight(),this.timeBegin=this.timeEnd=null},a.prototype.getValues=function(e){var t,n;return e=e||{},t=this.input.value,t=this.validation?this.validation(t):{value:t},t.isCorrect=n=!t.err,t.timeBegin=this.timeBegin,t.timeEnd=this.timeEnd,this.postprocess&&(t.value=this.postprocess(t.value,n)),n?e.reset&&this.reset():(this.setError(t.err),t.isCorrect=!1),t.id=this.id,t}}(node),function(e){"use strict";function t(n){this.id=n.id||t.id,this.event=n.event||"D3",this.svg=null;var r=this;e.on(this.event,function(e){r.tick.call(r,e)})}function n(e){var r,i,s;t.call(this,e),this.options=r=J.merge(n.defaults,e),this.n=r.n,this.data=[0],this.margin=r.margin,this.width=r.width-this.margin.left-this.margin.right,this.height=r.height-this.margin.top-this.margin.bottom,this.x=i=d3.scale.linear().domain(r.domain.x).range(r.range.x),this.y=s=d3.scale.linear().domain(r.domain.y).range(r.range.y),this.line=d3.svg.line().x(function(e,t){return i(t)}).y(function(e,t){return s(e)})}e.widgets.register("D3",t),e.widgets.register("D3ts",n),t.prototype.__proto__=e.Widget.prototype,t.prototype.constructor=t,t.defaults={},t.defaults.id="D3",t.defaults.fieldset={legend:"D3 plot"},t.version="0.1",t.description="Real time plots for nodeGame with d3.js",t.dependencies={d3:{},JSUS:{}},t.prototype.append=function(e){return this.root=e,this.svg=d3.select(e).append("svg"),e},t.prototype.tick=function(){},n.id="D3ts",n.version="0.1",n.description="Time series plot for nodeGame with d3.js",n.dependencies={D3:{},JSUS:{}},n.prototype.__proto__=t.prototype,n.prototype.constructor=n,n.defaults={},n.defaults.width=400,n.defaults.height=200,n.defaults.margin={top:10,right:10,bottom:20,left:40},n.defaults.domain={x:[0,10],y:[0,1]},n.defaults.range={x:[0,n.defaults.width],y:[n.defaults.height,0]},n.prototype.init=function(e){console.log("init!");var t=this.x,n=this.y,r=this.height,i=this.width,s=this.margin;this.svg.attr("width",i+s.left+s.right).attr("height",r+s.top+s.bottom).append("g").attr("transform","translate("+s.left+","+s.top+")"),this.svg.append("defs").append("clipPath").attr("id","clip").append("rect").attr("width",i).attr("height",r),this.svg.append("g").attr("class","x axis").attr("transform","translate(0,"+r+")").call(d3.svg.axis().scale(t).orient("bottom")),this.svg.append("g").attr("class","y axis").call(d3.svg.axis().scale(n).orient("left")),this.path=this.svg.append("g").attr("clip-path","url(#clip)").append("path").data([this.data]).attr("class","line").attr("d",this.line)},n.prototype.tick=function(e){this.alreadyInit=this.alreadyInit||!1,this.alreadyInit||(this.init(),this.alreadyInit=!0);var t=this.x;console.log("tick!"),this.data.push(e),this.path.attr("d",this.line).attr("transform",null),this.data.length>this.n&&(this.path.transition().duration(500).ease("linear").attr("transform","translate("+t(-1)+")"),this.data.shift())}}(node),function(e){"use strict";function n(){this.table=null,this.interval=null,this.intervalTime=1e3}var t=W.Table;e.widgets.register("DebugInfo",n),n.version="0.6.2",n.description="Display basic info a client's status.",n.title="Debug Info",n.className="debuginfo",n.dependencies={Table:{}},n.prototype.init=function(t){var n;"number"==typeof t.intervalTime&&(this.intervalTime=t.intervalTime),n=this,this.on("destroyed",function(){clearInterval(n.interval),n.interval=null,e.silly("DebugInfo destroyed.")})},n.prototype.append=function(){var e;this.table=new t,this.bodyDiv.appendChild(this.table.table),this.updateAll(),e=this,this.interval=setInterval(function(){e.updateAll()},this.intervalTime)},n.prototype.updateAll=function(){var t,n,r,i,s,o,u,a,f,l,c,h;if(!this.bodyDiv){e.err("DebugInfo.updateAll: bodyDiv not found.");return}h="-",r=h,n=h,t=e.game.getCurrentGameStage(),t&&(c=e.game.plot.getStep(t),r=c?c.id:"-",n=t.toString()),s=J.getKeyByValue(e.constants.stageLevels,e.game.getStageLevel()),o=J.getKeyByValue(e.constants.stateLevels,e.game.getStateLevel()),u=J.getKeyByValue(e.constants.windowLevels,W.getStateLevel()),i=e.player?e.player.id:h,a=e.errorManager.lastErr||h,l=e.game.settings&&e.game.settings.treatmentName?e.game.settings.treatmentName:h,f=e.socket.connected?"yes":"no",this.table.clear(!0),this.table.addRow(["Treatment: ",l]),this.table.addRow(["Connected: ",f]),this.table.addRow(["Player Id: ",i]),this.table.addRow(["Stage No: ",n]),this.table.addRow(["Stage Id: ",r]),this.table.addRow(["Stage Lvl: ",s]),this.table.addRow(["State Lvl: ",o]),this.table.addRow(["Players : ",e.game.pl.size()]),this.table.addRow(["Win Lvl: ",u]),this.table.addRow(["Win Loads: ",W.areLoading]),this.table.addRow(["Last Err: ",a]),this.table.parse()}}(node),function(e){"use strict";function t(){this.buttonsDiv=null,this.hiddenTypes={},this.counterIn=0,this.counterOut=0,this.counterLog=0,this.wall=null,this.wallDiv=null,this.origMsgInCb=null,this.origMsgOutCb=null,this.origLogCb=null}e.widgets.register("DebugWall",t),t.version="1.0.0",t.description="Intercepts incoming and outgoing messages, and logs and prints them numbered and timestamped. Warning! Modifies core functions, therefore its usage in production is not recommended.",t.title="Debug Wall",t.className="debugwall",t.dependencies={JSUS:{}},t.prototype.init=function(t){var n;n=this,t.msgIn!==!1&&(this.origMsgInCb=e.socket.onMessage,e.socket.onMessage=function(t){n.write("in",n.makeTextIn(t)),n.origMsgInCb.call(e.socket,t)}),t.msgOut!==!1&&(this.origMsgOutCb=e.socket.send,e.socket.send=function(t){n.write("out",n.makeTextOut(t)),n.origMsgOutCb.call(e.socket,t)}),t.log!==!1&&(this.origLogCb=e.log,e.log=function(t,r,i){n.write(r||"info",n.makeTextLog(t,r,i)),n.origLogCb.call(e,t,r,i)});if(t.hiddenTypes){if("object"!=typeof hiddenTypes)throw new TypeError("DebugWall.init: hiddenTypes must be object. Found: "+hiddenTypes);this.hiddenTypes=hiddenTypes}this.on("destroyed",function(){n.origLogCb&&(e.log=n.origLogCb),n.origMsgOutCb&&(e.socket.send=n.origMsgOutCb),n.origMsgInCb&&(e.socket.onMessage=n.origMsgInCb)})},t.prototype.append=function(){var e,t,n,r,i,s,o;this.buttonsDiv=W.add("div",this.bodyDiv,{className:"wallbuttonsdiv"}),i=document.createElement("div"),i.role="group",i["aria-label"]="Toggle visibility",i.className="btn-group",e=W.add("button",i,{innerHTML:"Incoming",className:"btn btn-secondary"}),t=W.add("button",i,{innerHTML:"Outgoing",className:"btn btn-secondary"}),n=W.add("button",i,{innerHTML:"Log",className:"btn btn-secondary"}),this.buttonsDiv.appendChild(i),r=this,s=function(e){var t,n,i,s;s="wall_"+e,t=r.wall.getElementsByClassName(s),i=t[0].style.display===""?"none":"";for(n=0;na?(r=W.add("span",l,{className:u+"_click",innerHTML:n.substr(0,a)}),s=W.add("span",r,{className:u+"_extra",innerHTML:n.substr(a,n.length),id:"wall_"+t+"_"+o,style:{display:"none"}}),i=W.add("span",r,{className:u+"_dots",innerHTML:" ...",id:"wall_"+t+"_"+o}),r.onclick=function(){i.style.display==="none"?(i.style.display="",s.style.display="none"):(i.style.display="none",s.style.display="")}):r=W.add("span",l,{innerHTML:n}),this.wallDiv.scrollTop=this.wallDiv.scrollHeight):e.warn("Wall not appended, cannot write.")},t.prototype.makeTextIn=function(e){var t,n;return n=new Date(e.created),t=n.getHours()+":"+n.getMinutes()+":"+n.getSeconds()+":"+n.getMilliseconds(),t+=" | "+e.to+" | "+e.target+" | "+e.action+" | "+e.text+" | "+e.data,t},t.prototype.makeTextOut=function(e){var t;return t=e.from+" | "+e.target+" | "+e.action+" | "+e.text+" | "+e.data,t},t.prototype.makeTextLog=function(e,t,n){return e}}(node),function(e){"use strict";function t(){this.disconnectButton=null,this.ee=null}e.widgets.register("DisconnectBox",t),t.version="0.2.3",t.description="Visually display current, previous and next stage of the game.",t.title=!1,t.panel=!1,t.className="disconnectbox",t.texts={leave:"Leave Experiment",left:"You Left"},t.dependencies={},t.prototype.append=function(){var t=this;this.disconnectButton=W.add("button",this.bodyDiv,{innerHTML:this.getText("leave"),className:"btn btn-lg"}),this.disconnectButton.onclick=function(){t.disconnectButton.disabled=!0,e.socket.disconnect(),t.disconnectButton.innerHTML=t.getText("left")}},t.prototype.listeners=function(){var t=this;this.ee=e.getCurrentEventEmitter(),this.ee.on("SOCKET_DISCONNECT",function(){}),this.ee.on("SOCKET_CONNECT",function(){t.disconnectButton.disabled&&(t.disconnectButton.disabled=!1,t.disconnectButton.innerHTML=t.getText("leave"))}),this.on("destroyed",function(){t.ee.off("SOCKET_DISCONNECT","DBdiscon"),t.ee.off("SOCKET_CONNECT","DBcon")})}}(node),function(e){"use strict";function t(t){var n;n=this;if("object"==typeof t.button)this.button=t.button;else{if("undefined"!=typeof t.button)throw new TypeError("DoneButton constructor: options.button must be object or undefined. Found: "+t.button);this.button=document.createElement("input"),this.button.type="button"}this.button.onclick=function(){var t;t=e.done(),t&&n.disable()}}e.widgets.register("DoneButton",t),t.version="1.0.0",t.description="Creates a button that if pressed emits node.done().",t.title=!1,t.className="donebutton",t.texts.done="Done",t.dependencies={JSUS:{}},t.prototype.init=function(e){var n;e=e||{};if("undefined"==typeof e.id)n=t.className;else if("string"==typeof e.id)n=e.id;else{if(!1!==e.id)throw new TypeError("DoneButton.init: options.id must be string, false, or undefined. Found: "+e.id);n=""}this.button.id=n;if("undefined"==typeof e.className)n="btn btn-lg btn-primary";else if(e.className===!1)n="";else if("string"==typeof e.className)n=e.className;else{if(!J.isArray(e.className))throw new TypeError("DoneButton.init: options.className must be string, array, or undefined. Found: "+e.className);n=e.className.join(" ")}this.button.className=n,this.button.value="string"==typeof e.text?e.text:this.getText("done")},t.prototype.append=function(){this.bodyDiv.appendChild(this.button)},t.prototype.listeners=function(){var t=this;e.on("PLAYING",function(){var n,r;r=e.game.getCurrentGameStage(),n=e.game.plot.getProperty(r,"donebutton"),n===!1||n&&n.enableOnPlaying===!1?t.disable():t.enable(),"string"==typeof n?t.button.value=n:n&&n.text&&(t.button.value=n.text)})},t.prototype.disable=function(){this.button.disabled="disabled"},t.prototype.enable=function(){this.button.disabled=!1}}(node),function(e){"use strict";function t(e){if(!e.onsubmit)this.onsubmit={emailOnly:!0,say:!0,updateUI:!0};else{if("object"!=typeof e.onsubmit)throw new TypeError("EmailForm constructor: options.onsubmit must be string or object. Found: "+e.onsubmit);this.onsubmit=e.onsubmit}this._email=e.email||null,this.attempts=[],this.timeInput=null,this.formElement=null,this.inputElement=null,this.buttonElement=null}function n(){return this.inputElement?this.inputElement.value:this._email}e.widgets.register("EmailForm",t),t.version="0.10.0",t.description="Displays a configurable email form.",t.title="Email",t.className="emailform",t.texts.label="Enter your email:",t.texts.errString="Not a valid email address, please correct it and submit it again.",t.dependencies={JSUS:{}},t.prototype.createForm=function(){var e,t,n,r,i;return e=this,t=document.createElement("form"),t.className="emailform-form",n=document.createElement("label"),n.innerHTML=this.getText("label"),r=document.createElement("input"),r.setAttribute("type","text"),r.setAttribute("placeholder","Email"),r.className="emailform-input form-control",i=document.createElement("input"),i.setAttribute("type","submit"),i.setAttribute("value","Submit email"),i.className="btn btn-lg btn-primary emailform-submit",t.appendChild(n),t.appendChild(r),t.appendChild(i),J.addEvent(t,"submit",function(t){t.preventDefault(),e.getValues(e.onsubmit)},!0),J.addEvent(t,"input",function(t){e.timeInput||(e.timeInput=J.now()),e.isHighlighted()&&e.unhighlight()},!0),this.formElement=t,this.inputElement=r,this.buttonElement=i,this._email&&(this.formElement.value=this._email),this._email=null,t},t.prototype.verifyInput=function(e,t){var r,i;return r=n.call(this),i=J.isEmail(r),i&&t?(this.inputElement&&(this.inputElement.disabled=!0),this.buttonElement&&(this.buttonElement.disabled=!0,this.buttonElement.value="Sent!")):(t&&this.buttonElement&&(this.buttonElement.value=this.getText("errString")),("undefined"==typeof e||e)&&this.attempts.push(r)),i},t.prototype.append=function(){this.createForm(),this.bodyDiv.appendChild(this.formElement)},t.prototype.setValues=function(e){var t;e=e||{},e.email?t=e.email:t=J.randomEmail(),this.inputElement?this.inputElement.value=t:this._email=t,this.timeInput=J.now()},t.prototype.getValues=function(e){var t,r;return e=e||{},t=n.call(this),e.verify!==!1&&(r=this.verifyInput(e.markAttempt,e.updateUI)),e.emailOnly||(t={time:this.timeInput,email:t,attempts:this.attempts,valid:r}),r===!1&&((e.updateUI||e.highlight)&&this.highlight(),this.timeInput=null),(e.say&&r||e.sayAnyway)&&this.sendValues({values:t}),e.reset&&this.reset(),t},t.prototype.sendValues=function(t){var n;return t=t||{emailOnly:!0},n=t.values||this.getValues(t),e.say("email",t.to||"SERVER",n),n},t.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("EmailForm.highlight: border must be string or undefined. Found: "+e);if(!this.inputElement||this.highlighted===!0)return;this.inputElement.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},t.prototype.unhighlight=function(){if(!this.inputElement||this.highlighted!==!0)return;this.inputElement.style.border="",this.highlighted=!1,this.emit("unhighlighted")},t.prototype.reset=function(){this.attempts=[],this.timeInput=null,this._email=null,this.inputElement&&(this.inputElement.value=""),this.isHighlighted()&&this.unhighlight()}}(node),function(e){"use strict";function t(e){if(e.email===!1)e.showEmailForm=!1;else if("undefined"==typeof e.showEmailForm)this.showEmailForm=!0;else{if("boolean"!=typeof e.showEmailForm)throw new TypeError("EndScreen constructor: options.showEmailForm must be boolean or undefined. Found: "+e.showEmailForm);this.showEmailForm=e.showEmailForm}if(e.feedback===!1)e.showFeedbackForm=!1;else if("undefined"==typeof e.showFeedbackForm)this.showFeedbackForm=!0;else{if("boolean"!=typeof e.showFeedbackForm)throw new TypeError("EndScreen constructor: options.showFeedbackForm must be boolean or undefined. Found: "+e.showFeedbackForm);this.showFeedbackForm=e.showFeedbackForm}if(e.totalWin===!1)e.showTotalWin=!1;else if("undefined"==typeof e.showTotalWin)this.showTotalWin=!0;else{if("boolean"!=typeof e.showTotalWin)throw new TypeError("EndScreen constructor: options.showTotalWin must be boolean or undefined. Found: "+e.showTotalWin);this.showTotalWin=e.showTotalWin}if(e.exitCode===!1)e.showExitCode!==!1;else if("undefined"==typeof e.showExitCode)this.showExitCode=!0;else{if("boolean"!=typeof e.showExitCode)throw new TypeError("EndScreen constructor: options.showExitCode must be boolean or undefined. Found: "+e.showExitCode);this.showExitCode=e.showExitCode}if("undefined"==typeof e.totalWinCurrency)this.totalWinCurrency="USD";else{if("string"!=typeof e.totalWinCurrency||e.totalWinCurrency.trim()==="")throw new TypeError("EndScreen constructor: options.totalWinCurrency must be undefined or a non-empty string. Found: "+e.totalWinCurrency);this.totalWinCurrency=e.totalWinCurrency}if(e.totalWinCb){if("function"!=typeof e.totalWinCb)throw new TypeError("EndScreen constructor: options.totalWinCb must be function or undefined. Found: "+e.totalWinCb);this.totalWinCb=e.totalWinCb}this.emailForm=null,this.feedback=null,this.endScreenHTML=null}e.widgets.register("EndScreen",t),t.version="0.6.0",t.description="Game end screen. With end game message, email form, and exit code.",t.title="End Screen",t.className="endscreen",t.texts.headerMessage="Thank you for participating!",t.texts.message="You have now completed this task and your data has been saved. Please go back to the Amazon Mechanical Turk web site and submit the HIT.",t.texts.contactQuestion="Would you like to be contacted againfor future experiments? If so, leaveyour email here and press submit: ",t.texts.totalWin="Your total win:",t.texts.exitCode="Your exit code:",t.texts.errTotalWin="Error: invalid total win.",t.texts.errExitCode="Error: invalid exit code.",t.texts.copyButton="Copy",t.texts.exitCopyMsg="Exit code copied to clipboard.",t.texts.exitCopyError="Failed to copy exit code. Please copy it manually.",t.dependencies={JSUS:{},Feedback:{},EmailForm:{}},t.prototype.init=function(t){this.showEmailForm&&!this.emailForm&&(this.emailForm=e.widgets.get("EmailForm",J.mixin({label:this.getText("contactQuestion"),onsubmit:{say:!0,emailOnly:!0,updateUI:!0},storeRef:!1},t.email))),this.showFeedbackForm&&(this.feedback=e.widgets.get("Feedback",J.mixin({storeRef:!1},t.feedback)))},t.prototype.append=function(){this.endScreenHTML=this.makeEndScreen(),this.bodyDiv.appendChild(this.endScreenHTML)},t.prototype.makeEndScreen=function(){var t,n,r,i,s,o,u,a,f,l,c,h=this;return t=document.createElement("div"),t.className="endscreen",n=document.createElement("h1"),n.innerHTML=this.getText("headerMessage"),t.appendChild(n),r=document.createElement("p"),r.innerHTML=this.getText("message"),t.appendChild(r),this.showTotalWin&&(i=document.createElement("div"),s=document.createElement("p"),s.innerHTML=""+this.getText("totalWin")+"",o=document.createElement("input"),o.className="endscreen-total form-control",o.setAttribute("disabled","true"),s.appendChild(o),i.appendChild(s),t.appendChild(i),this.totalWinInputElement=o),this.showExitCode&&(u=document.createElement("div"),u.className="input-group",a=document.createElement("span"),a.innerHTML=""+this.getText("exitCode")+"",f=document.createElement("input"),f.id="exit_code",f.className="endscreen-exit-code form-control",f.setAttribute("disabled","true"),c=document.createElement("span"),c.className="input-group-btn",l=document.createElement("button"),l.className="btn btn-default endscreen-copy-btn",l.innerHTML=this.getText("copyButton"),l.type="button",l.onclick=function(){h.copy(f.value)},c.appendChild(l),t.appendChild(a),u.appendChild(c),u.appendChild(f),t.appendChild(u),this.exitCodeInputElement=f),this.showEmailForm&&e.widgets.append(this.emailForm,t,{title:!1,panel:!1}),this.showFeedbackForm&&e.widgets.append(this.feedback,t,{title:!1,panel:!1}),t},t.prototype.listeners=function(){var t;t=this,e.on.data("WIN",function(e){t.updateDisplay(e.data)})},t.prototype.copy=function(e){var t=document.createElement("input");try{document.body.appendChild(t),t.value=e,t.select(),document.execCommand("copy",!1),t.remove(),alert(this.getText("exitCopyMsg"))}catch(n){alert(this.getText("exitCopyError"))}},t.prototype.updateDisplay=function(t){var n,r,i,s,o,u,a,f;if(this.totalWinCb)r=this.totalWinCb(t,this);else{if("undefined"==typeof t.total&&"undefined"==typeof t.totalRaw)throw new Error("EndScreen.updateDisplay: data.total and data.totalRaw cannot be both undefined.");"undefined"!=typeof t.total&&(r=J.isNumber(t.total,0),r===!1&&(e.err("EndScreen.updateDisplay: invalid data.total: "+t.total),r=this.getText("errTotalWin"),f=!0)),t.partials&&(J.isArray(t.partials)?n=t.partials.join(" + "):e.err("EndScreen error, invalid partials win: "+t.partials)),"undefined"!=typeof t.totalRaw&&(n?n+=" = ":n="",n+=t.totalRaw,a="undefined"!=typeof t.exchangeRate?t.exchangeRate:e.game.settings.EXCHANGE_RATE,"undefined"!=typeof a&&(n+="*"+a),"undefined"==typeof r&&(i=J.isNumber(t.totalRaw,0),r=parseFloat(a*t.totalRaw).toFixed(2),r=J.isNumber(r,0),r===!1&&(e.err("EndScreen.updateDisplay: invalid : totalWin calculation from totalRaw."),r=this.getText("errTotalWin"),f=!0)),f||(r=n+" = "+r)),f||(r+=" "+this.totalWinCurrency)}s=t.exit,"string"!=typeof s&&(e.err("EndScreen error, invalid exit code: "+s),s=this.getText("errExitCode")),o=this.totalWinInputElement,u=this.exitCodeInputElement,o&&this.showTotalWin&&(o.value=r),u&&this.showExitCode&&(u.value=s)}}(node),function(e){"use strict";function i(e){var t;"undefined"!=typeof e.maxLength&&(console.log("***Feedback constructor: maxLength is deprecated, use maxChars instead***"),e.maxChars=e.maxLength),"undefined"!=typeof e.minLength&&(console.log("***Feedback constructor: minLength is deprecated, use minChars instead***"),e.minChars=e.minLength),this.mainText=null,this.hint=null,this.spanMainText=null;if("undefined"==typeof e.maxChars)this.maxChars=800;else{t=J.isInt(e.maxChars,0);if(t===!1)throw new TypeError("Feedback constructor: maxChars must be an integer >= 0 or undefined. Found: "+e.maxChars);this.maxChars=e.maxChars}if("undefined"==typeof e.minChars)this.minChars=1;else{t=J.isInt(e.minChars,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: minChars must be an integer >= 0 or undefined. Found: "+e.minChars);this.minChars=e.minChars}if("undefined"==typeof e.maxWords)this.maxWords=0;else{t=J.isInt(e.maxWords,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: maxWords must be an integer >= 0 or undefined. Found: "+e.maxWords);this.maxWords=e.maxWords}if("undefined"==typeof e.minWords)this.minWords=0;else{t=J.isInt(e.minWords,0,undefined,!0);if(t===!1)throw new TypeError("Feedback constructor: minWords must be an integer >= 0 or undefined. Found: "+e.minWords);this.minWords=e.minWords;if(this.maxChars){t=(this.maxChars+1)/2;if(this.minWords>t)throw new TypeError("Feedback constructor: minWords cannot be larger than (maxChars+1)/2. Found: "+this.minWords+" > "+t)}}if("undefined"==typeof e.rows)this.rows=3;else{if(J.isInt(e.rows,0)===!1)throw new TypeError("Feedback constructor: rows must be an integer > 0 or undefined. Found: "+e.rows);this.rows=e.rows}if("undefined"==typeof e.maxAttemptLength)this.maxAttemptLength=2e3;else{if(J.isNumber(e.maxAttemptLength,0)===!1)throw new TypeError("Feedback constructor: options.maxAttemptLength must be a number >= 0 or undefined. Found: "+e.maxAttemptLength);this.maxAttemptLength=Math.max(this.maxChars,e.maxAttemptLength)}this.showSubmit="undefined"==typeof e.showSubmit?!0:!!e.showSubmit;if(!e.onsubmit)this.onsubmit={feedbackOnly:!0,say:!0,updateUI:!0};else{if("object"!=typeof e.onsubmit)throw new TypeError("Feedback constructor: onsubmit must be string or object. Found: "+e.onsubmit);this.onsubmit=e.onsubmit}this._feedback=e.feedback||null,this.attempts=[],this.timeInputBegin=null,this.feedbackForm=null,this.textareaElement=null,this.charCounter=null,this.wordCounter=null,this.submitButton=null}function s(){var e;return e=this.textareaElement?this.textareaElement.value:this._feedback,e?e.trim():e}e.widgets.register("Feedback",i),i.version="1.4.0",i.description="Displays a configurable feedback form",i.title="Feedback",i.className="feedback",i.texts={autoHint:function(e){var t,n;return e.minChars&&e.maxChars?t="beetween "+e.minChars+" and "+e.maxChars+" characters":e.minChars?(t="at least "+e.minChars+" character",e.minChars>1&&(t+="s")):e.maxChars&&(t="at most "+e.maxChars+" character",e.maxChars>1&&(t+="s")),e.minWords&&e.maxWords?n="beetween "+e.minWords+" and "+e.maxWords+" words":e.minWords?(n="at least "+e.minWords+" word",e.minWords>1&&(t+="s")):e.maxWords&&(n="at most "+e.maxWords+" word",e.maxWords>1&&(t+="s")),t?(t="("+t,n&&(t+=", and "+n),t+")"):n?"("+n+")":!1},submit:"Submit feedback",label:"Any feedback? Let us know here:",sent:"Sent!",counter:function(e,t){var n;return n=t.chars?" character":" word",t.len!==1&&(n+="s"),t.needed?n+=" needed":t.over?n+=" over":n+=" remaining",n}};var t,n,r;t="#a32020",n="#a32020",r="#78b360",i.dependencies={JSUS:{}},i.prototype.init=function(e){if("string"==typeof e.mainText)this.mainText=e.mainText;else{if("undefined"!=typeof e.mainText)throw new TypeError("Feedback.init: options.mainText must be string or undefined. Found: "+e.mainText);this.mainText=this.getText("label")}if("string"==typeof e.hint||!1===e.hint)this.hint=e.hint;else{if("undefined"!=typeof e.hint)throw new TypeError("Feedback.init: options.hint must be a string, false, or undefined. Found: "+e.hint);this.hint=this.getText("autoHint")}},i.prototype.verifyFeedback=function(e,i){var o,u,a,f,l,c,h,p,d,v,m;return o=s.call(this),u=o?o.length:0,f=this.submitButton,l=this.charCounter,c=this.wordCounter,a=!0,uthis.maxChars?(a=!1,h=u-this.maxChars,p=h+this.getText("counter",{chars:!0,over:!0,len:h}),d=n):(h=this.maxChars-u,p=h+this.getText("counter",{chars:!0,len:h}),d=r),c&&(h=o?o.match(/\b[-?(\w+)?]+\b/gi):0,u=h?h.length:0,uthis.maxWords?(a=!1,h=u-this.maxWords,v=h+this.getText("counter",{over:!0,len:h}),m=n):(h=this.maxWords-u,v=h+this.getText("counter",{len:h}),m=r)),i&&(f&&(f.disabled=!a),l&&(l.style.backgroundColor=d,l.innerHTML=p),c&&(c.style.backgroundColor=m,c.innerHTML=v)),!a&&("undefined"==typeof e||e)&&(u>this.maxAttemptLength&&(o=o.substr(0,this.maxAttemptLength)),this.attempts.push(o)),a},i.prototype.append=function(){var e,t;e=this,this.feedbackForm=W.append("form",this.bodyDiv,{className:"feedback-form"}),this.mainText&&(this.spanMainText=W.append("span",this.feedbackForm,{className:"feedback-maintext",innerHTML:this.mainText})),this.hint&&W.append("span",this.spanMainText||this.feedbackForm,{className:"feedback-hint",innerHTML:this.hint}),this.textareaElement=W.append("textarea",this.feedbackForm,{className:"feedback-textarea form-control",type:"text",rows:this.rows}),this.showSubmit&&(this.submit=W.append("input",this.feedbackForm,{className:"btn btn-lg btn-primary",type:"submit",value:this.getText("submit")}),J.addEvent(this.feedbackForm,"submit",function(t){t.preventDefault(),e.getValues(e.onsubmit)})),this.showCounters(),J.addEvent(this.feedbackForm,"input",function(t){e.isHighlighted()&&e.unhighlight(),e.verifyFeedback(!1,!0)}),J.addEvent(this.feedbackForm,"click",function(t){e.isHighlighted()&&e.unhighlight()}),this.verifyFeedback(!1,!0)},i.prototype.setValues=function(e){var t;e=e||{},e.feedback?t=e.feedback:t=J.randomString(J.randomInt(0,this.maxChars),"aA_1"),this.textareaElement?this.textareaElement.value=t:this._feedback=t,this.timeInputBegin=J.now()},i.prototype.getValues=function(e){var t,n,r;e=e||{},t=s.call(this),e.keepBreaks&&(t=t.replace(/\n\r?/g,"
")),e.verify!==!1&&(r=this.verifyFeedback(e.markAttempt,e.updateUI)),r===!1&&(e.updateUI||e.highlight)&&this.highlight(),e.feedbackOnly||(t={timeBegin:this.timeInputBegin,feedback:t,attempts:this.attempts,valid:r,isCorrect:r});if(e.say&&r||e.sayAnyway)this.sendValues({values:t}),e.updateUI&&(this.submitButton.setAttribute("value",this.getText("sent")),this.submitButton.disabled=!0,this.textareaElement.disabled=!0);return e.reset&&this.reset(),t},i.prototype.sendValues=function(t){var n;return t=t||{feedbackOnly:!0},n=t.values||this.getValues(t),e.say("feedback",t.to||"SERVER",n),n},i.prototype.highlight=function(e){if(e&&"string"!=typeof e)throw new TypeError("Feedback.highlight: border must be string or undefined. Found: "+e);if(!this.isAppended()||this.highlighted===!0)return;this.textareaElement.style.border=e||"3px solid red",this.highlighted=!0,this.emit("highlighted",e)},i.prototype.unhighlight=function(){if(!this.isAppended()||this.highlighted!==!0)return;this.textareaElement.style.border="",this.highlighted=!1,this.emit("unhighlighted")},i.prototype.reset=function(){this.attempts=[],this.timeInputBegin=null,this._feedback=null,this.textareaElement&&(this.textareaElement.value=""),this.isHighlighted()&&this.unhighlight()},i.prototype.disable=function(){if(!this.textareaElement||this.textareaElement.disabled)return;this.disabled=!0,this.submitElement&&(this.submitElement.disabled=!0),this.textareaElement.disabled=!0,this.emit("disabled")},i.prototype.enable=function(){if(!this.textareaElement||!this.textareaElement.disabled)return;this.disabled=!1,this.submitElement&&(this.submitElement.disabled=!1),this.textareaElement.disabled=!1,this.emit("enabled")},i.prototype.showCounters=function(){if(!this.charCounter){if(this.minChars||this.maxChars)this.charCounter=W.append("span",this.feedbackForm,{className:"feedback-char-count badge",innerHTML:this.maxChars})}else this.charCounter.style.display="";if(!this.wordCounter){if(this.minWords||this.maxWords)this.wordCounter=W.append("span",this.feedbackForm,{className:"feedback-char-count badge",innerHTML:this.maxWords}),this.charCounter&&(this.wordCounter.style["margin-left"]="10px")}else this.wordCounter.style.display=""},i.prototype.hideCounters=function(){this.charCounter&&(this.charCounter.style.display="none"),this.wordCounter&&(this.wordCounter.style.display="none")}}(node),function(e){"use strict";function t(t){var n=this;this.options=t,this.availableLanguages={en:{name:"English",nativeName:"English",shortName:"en"}},this.currentLanguage=null,this.buttonListLength=null,this.displayForm=null,this.optionsLabel={},this.optionsDisplay={},this.loadingDiv=null,this.languagesLoaded=!1,this.usingButtons=!0,this.updatePlayer="ondone",this.setUriPrefix=!0,this.notifyServer=!0,this.onLangCallback=function(t){function i(e){return function(){n.setLanguage(e,n.updatePlayer==="onselect")}}var r;while(n.displayForm.firstChild)n.displayForm.removeChild(n.displayForm.firstChild);n.availableLanguages=t.data;if(n.usingButtons)for(r in t.data)t.data.hasOwnProperty(r)&&(n.optionsLabel[r]=W.get("label",{id:r+"Label","for":r+"RadioButton"}),n.optionsDisplay[r]=W.get("input",{id:r+"RadioButton",type:"radio",name:"languageButton",value:t.data[r].name}),n.optionsDisplay[r].onclick=i(r),n.optionsLabel[r].appendChild(n.optionsDisplay[r]),n.optionsLabel[r].appendChild(document.createTextNode(t.data[r].nativeName)),W.add("br",n.displayForm),n.optionsLabel[r].className="unselectedButtonLabel",n.displayForm.appendChild(n.optionsLabel[r]));else{n.displaySelection=W.get("select","selectLanguage");for(r in t.data)n.optionsLabel[r]=document.createTextNode(t.data[r].nativeName),n.optionsDisplay[r]=W.get("option",{id:r+"Option",value:r}),n.optionsDisplay[r].appendChild(n.optionsLabel[r]),n.displaySelection.appendChild(n.optionsDisplay[r]);n.displayForm.appendChild(n.displaySelection),n.displayForm.onchange=function(){n.setLanguage(n.displaySelection.value,n.updatePlayer==="onselect")}}n.loadingDiv.style.display="none",n.languagesLoaded=!0,n.setLanguage(e.player.lang.shortName||"en",!1),n.onLangCallbackExtension&&(n.onLangCallbackExtension(t),n.onLangCallbackExtension=null)},this.onLangCallbackExtension=null}e.widgets.register("LanguageSelector",t),t.version="0.6.2",t.description="Display information about the current language and allows to change language.",t.title="Language",t.className="languageselector",t.texts.loading="Loading language information...",t.dependencies={JSUS:{}},t.prototype.init=function(t){J.mixout(t,this.options),this.options=t,"undefined"!=typeof this.options.usingButtons&&(this.usingButtons=!!this.options.usingButtons);if("undefined"!=typeof this.options.notifyServer)if(!1===this.options.notifyServer)this.options.notifyServer="never";else{if("string"!=typeof this.options.notifyServer)throw new Error("LanguageSelector.init: options.notifyServer must be "+this.options.notifyServer);if("never"!==this.options.notifyServer&&"onselect"!==this.options.notifyServer&&"ondone"!==this.options.notifyServer)throw new Error('LanguageSelector.init: invalid value for notifyServer: "'+this.options.notifyServer+'". Valid '+'values: "never","onselect", "ondone".');this.notifyServer=this.options.notifyServer}"undefined"!=typeof this.options.setUriPrefix&&(this.setUriPrefix=!!this.options.setUriPrefix),e.on.lang(this.onLangCallback),this.displayForm=W.get("form","radioButtonForm"),this.loadingDiv=W.add("div",this.displayForm),this.loadingDiv.innerHTML=this.getText("loading"),this.loadLanguages()},t.prototype.append=function(){this.bodyDiv.appendChild(this.displayForm)},t.prototype.setLanguage=function(t,n){this.usingButtons&&this.currentLanguage!==null&&this.currentLanguage!==this.availableLanguages[t]&&(this.optionsDisplay[this.currentLanguage].checked="unchecked",this.optionsLabel[this.currentLanguage].className="unselectedButtonLabel"),this.currentLanguage=t,this.usingButtons?(this.optionsDisplay[this.currentLanguage].checked="checked",this.optionsLabel[this.currentLanguage].className="selectedButtonLabel"):this.displaySelection.value=this.currentLanguage,n!==!1&&e.setLanguage(this.availableLanguages[this.currentLanguage],this.setUriPrefix,this.notifyServer)},t.prototype.updateAvalaibleLanguages=function(t){t&&t.callback&&(this.onLangCallbackExtension=t.callback),e.socket.send(e.msg.create({target:"LANG",to:"SERVER",action:"get"}))},t.prototype.loadLanguages=function(e){this.languagesLoaded?e&&e.callback&&e.callback():this.updateAvalaibleLanguages(e)},t.prototype.listeners=function(){var t;t=this,e.events.step.on("REALLY_DONE",function(){t.updatePlayer==="ondone"&&e.setLanguage(t.availableLanguages[t.currentLanguage],t.setUriPrefix,t.notifyServer)})}}(node),function(e){"use strict";function t(e){this.spanCurrency=null,this.spanMoney=null,this.currency="ECU",this.money=0,this.precision=2,this.showCurrency=!0,this.classnameCurrency="moneytalkscurrency",this.classnameMoney="moneytalksmoney"}e.widgets.register("MoneyTalks",t),t.version="0.4.0",t.description="Displays the earnings of a player.",t.title="Earnings",t.className="moneytalks",t.dependencies={JSUS:{}},t.prototype.init=function(e){"string"==typeof e.currency&&(this.currency=e.currency),"undefined"!=typeof e.showCurrency&&(this.showCurrency=!!e.showCurrency),"number"==typeof e.money&&(this.money=e.money),"number"==typeof e.precision&&(this.precision=e.precision),"string"==typeof e.MoneyClassName&&(this.classnameMoney=e.MoneyClassName),"string"==typeof e.currencyClassName&&(this.classnameCurrency=e.currencyClassName)},t.prototype.append=function(){this.spanMoney||(this.spanMoney=document.createElement("span")),this.spanCurrency||(this.spanCurrency=document.createElement("span")),this.showCurrency||(this.spanCurrency.style.display="none"),this.spanMoney.className=this.classnameMoney,this.spanCurrency.className=this.classnameCurrency,this.spanCurrency.innerHTML=this.currency,this.spanMoney.innerHTML=this.money,this.bodyDiv.appendChild(this.spanMoney),this.bodyDiv.appendChild(this.spanCurrency)},t.prototype.listeners=function(){var t=this;e.on("MONEYTALKS",function(e,n){t.update(e,n)})},t.prototype.update=function(t,n){var r;r=J.isNumber(t);if(r===!1){e.err("MoneyTalks.update: invalid amount: "+t);return}n&&(this.money=0),this.money+=r,this.spanMoney.innerHTML=this.money.toFixed(this.precision)},t.prototype.getValues=function(){return this.money}}(node),function(e){"use strict";function t(e){this.methods={},this.method="I-PANAS-SF",this.gauge=null,this.addMethod("I-PANAS-SF",r)}function n(e,t){if(!t)throw new Error("MoodGauge.init: method "+e+"did not create element gauge.");if("function"!=typeof t.getValues)throw new Error("MoodGauge.init: method "+e+": gauge missing function getValues.");if("function"!=typeof t.enable)throw new Error("MoodGauge.init: method "+e+": gauge missing function enable.");if("function"!=typeof t.disable)throw new Error("MoodGauge.init: method "+e+": gauge missing function disable.");if("function"!=typeof t.append)throw new Error("MoodGauge.init: method "+e+": gauge missing function append.")}function r(t){var n,r,i,s,o,u,a,f;i=t.choices||["1","2","3","4","5"],r=t.emotions||["Upset","Hostile","Alert","Ashamed","Inspired","Nervous","Determined","Attentive","Afraid","Active"],s=t.left||"never",o=t.right||"always",f=r.length,n=new Array(f),a=-1;for(;++a'+r[a]+": never",right:o,choices:i};return u=e.widgets.get("ChoiceTableGroup",{id:"ipnassf",items:n,mainText:this.getText("mainText"),title:!1,requiredChoice:!0,storeRef:!1}),u}e.widgets.register("MoodGauge",t),t.version="0.3.0",t.description="Displays an interface to measure mood and emotions.",t.title="Mood Gauge",t.className="moodgauge",t.texts.mainText="Thinking about yourself and how you normally feel, to what extent do you generally feel: ",t.dependencies={JSUS:{}},t.prototype.init=function(e){var t;if("undefined"!=typeof e.method){if("string"!=typeof e.method)throw new TypeError("MoodGauge.init: options.method must be string or undefined: "+e.method);if(!this.methods[e.method])throw new Error("MoodGauge.init: options.method is not a valid method: "+e.method);this.method=e.method}t=this.methods[this.method].call(this,e),n(this.method,t),this.gauge=t,this.on("enabled",function(){t.enable()}),this.on("disabled",function(){t.disable()}),this.on("highlighted",function(){t.highlight()}),this.on("unhighlighted",function(){t.unhighlight()})},t.prototype.append=function(){e.widgets.append(this.gauge,this.bodyDiv,{panel:!1})},t.prototype.addMethod=function(e,t){if("string"!=typeof e)throw new Error("MoodGauge.addMethod: name must be string: "+e);if("function"!=typeof t)throw new Error("MoodGauge.addMethod: cb must be function: "+t);if(this.methods[e])throw new Error("MoodGauge.addMethod: name already existing: "+e);this.methods[e]=t},t.prototype.getValues=function(e){return this.gauge.getValues(e)},t.prototype.setValues=function(e){return this.gauge.setValues(e)}}(node),function(e){"use strict";function t(e){function t(e){var t,n,i,s;return t="/images/"+(e.content.success?"success-icon.png":"delete-icon.png"),n=document.createElement("img"),n.src=t,"object"==typeof e.content.text&&(e.content.text=r(e.content.text)),s=document.createTextNode(e.content.text),i=document.createElement("span"),i.className="requirement",i.appendChild(n),i.appendChild(s),i}this.requirements=[],this.stillChecking=0,this.withTimeout=e.withTimeout||!0,this.timeoutTime=e.timeoutTime||1e4,this.timeoutId=null,this.summary=null,this.summaryUpdate=null,this.summaryResults=null,this.dots=null,this.hasFailed=!1,this.results=[],this.completed={},this.sayResults=e.sayResults||!1,this.sayResultsLabel=e.sayResultLabel||"requirements",this.addToResults=e.addToResults||null,this.onComplete=null,this.onSuccess=null,this.onFailure=null,this.callbacksExecuted=!1,this.list=new W.List({render:{pipeline:t,returnAt:"first"}})}function n(e,t,n){var r,i,s;i=function(n,r,i){if(e.completed[t])throw new Error("Requirements.checkRequirements: test already completed: "+t);e.completed[t]=!0,e.updateStillChecking(-1),n||(e.hasFailed=!0),"string"==typeof r&&(r=[r]);if(r){if(!J.isArray(r))throw new Error("Requirements.checkRequirements: errors must be array or undefined. Found: "+r);e.displayResults(r)}e.results.push({name:t,success:n,errors:r,data:i}),e.isCheckingFinished()&&e.checkingFinished()},r=e.requirements[n];if("function"==typeof r)s=r(i);else{if("object"!=typeof r)throw new TypeError("Requirements.checkRequirements: invalid requirement: "+t+".");s=r.cb(i,r.params||{})}s&&i(s.success,s.errors,s.data)}function r(e){var t;return e.msg?t=e.msg:e.message?t=e.message:e.description?t=t.description:t=e.toString(),t}e.widgets.register("Requirements",t),t.version="0.7.1",t.description="Checks a set of requirements and display the results",t.title="Requirements",t.className="requirements",t.texts.errStr="One or more function is taking too long. This is likely to be due to a compatibility issue with your browser or to bad network connectivity.",t.texts.testPassed="All tests passed.",t.dependencies={JSUS:{},List:{}},t.prototype.init=function(e){if("object"!=typeof e)throw new TypeError("Requirements.init: conf must be object. Found: "+e);if(e.requirements){if(!J.isArray(e.requirements))throw new TypeError("Requirements.init: conf.requirements must be array or undefined. Found: "+e.requirements);this.requirements=e.requirements}if("undefined"!=typeof e.onComplete){if(null!==e.onComplete&&"function"!=typeof e.onComplete)throw new TypeError("Requirements.init: conf.onComplete must be function, null or undefined. Found: "+e.onComplete);this.onComplete=e.onComplete}if("undefined"!=typeof e.onSuccess){if(null!==e.onSuccess&&"function"!=typeof e.onSuccess)throw new TypeError("Requirements.init: conf.onSuccess must be function, null or undefined. Found: "+e.onSuccess);this.onSuccess=e.onSuccess}if("undefined"!=typeof e.onFailure){if(null!==e.onFailure&&"function"!=typeof e.onFailure)throw new TypeError("Requirements.init: conf.onFailure must be function, null or undefined. Found: "+e.onFailure);this.onFailure=e.onFailure}if(e.maxExecTime){if(null!==e.maxExecTime&&"number"!=typeof e.maxExecTime)throw new TypeError("Requirements.init: conf.onMaxExecTime must be number, null or undefined. Found: "+e.maxExecTime);this.withTimeout=!!e.maxExecTime,this.timeoutTime=e.maxExecTime}},t.prototype.addRequirements=function(){var e,t;e=-1,t=arguments.length;for(;++e0&&e.displayResults([this.getText("errStr")]),e.timeoutId=null,e.hasFailed=!0,e.checkingFinished()},this.timeoutTime)},t.prototype.clearTimeout=function(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)},t.prototype.updateStillChecking=function(e,t){var n,r;this.stillChecking=t?e:this.stillChecking+e,n=this.requirements.length,r=n-this.stillChecking,this.summaryUpdate.innerHTML=" ("+r+" / "+n+")"},t.prototype.isCheckingFinished=function(){return this.stillChecking<=0},t.prototype.checkingFinished=function(t){var n;if(this.callbacksExecuted&&!t)return;this.callbacksExecuted=!0,this.timeoutId&&clearTimeout(this.timeoutId),this.dots.stop(),this.sayResults&&(n={success:!this.hasFailed,results:this.results},this.addToResults&&J.mixin(n,this.addToResults()),e.say(this.sayResultsLabel,"SERVER",n)),this.onComplete&&this.onComplete(),this.hasFailed?this.onFailure&&this.onFailure():this.onSuccess&&this.onSuccess()},t.prototype.displayResults=function(e){var t,n;if(!this.list)throw new Error("Requirements.displayResults: list not found. Have you called .append() first?");if(!J.isArray(e))throw new TypeError("Requirements.displayResults: results must be array. Found: "+e);if(!this.hasFailed&&this.stillChecking<=0)this.list.addDT({success:!0,text:this.getText("testPassed")});else{t=-1,n=e.length;for(;++t"+t[1]},o=r.length,n=new Array(o),s=-1;for(;++s"+e.startDate},disconnect:'You have been disconnected. Please try again later.

',waitedTooLong:"Waiting for too long. Please look for a HIT called Trouble Ticket and file a new trouble ticket reporting your experience.",notEnoughPlayers:'

Thank you for your patience.
Unfortunately, there are not enough participants in your group to start the experiment.
',roomClosed:' The waiting room is CLOSED. You have been disconnected. Please try again later.

',tooManyPlayers:function(e,t){var n;return n="There are more players in this waiting room than playslots in the game. ",e.poolSize===1?n+="Each player will play individually.":n+="Only "+t.nGames+" players will be selected "+"to play the game.",n},notSelectedClosed:'

Unfortunately, you were not selected to join the game this time. Thank you for your participation.



',notSelectedOpen:'

Unfortunately, you were not selected to join the game this time, but you may join the next one.Ok, I got it.



Thank you for your participation.

',exitCode:function(e,t){return"
You have been disconnected. "+("undefined"!=typeof t.exit?"Please report this exit code: "+t.exit:"")+"
"},playBot:function(e){return e.poolSize===e.groupSize&&e.groupSize===1?"Play":e.groupSize===2?"Play With Bot":"Play With Bots"},connectingBots:function(e){return console.log(e.poolSize,e.groupSize),e.poolSize===e.groupSize&&e.groupSize===1?"Starting, Please Wait...":e.groupSize===2?"Connecting Bot, Please Wait...":"Connecting Bot/s, Please Wait..."},selectTreatment:"Select Treatment ",gameTreatments:"Game:",defaultTreatments:"Defaults:"},t.prototype.init=function(t){var n=this;if("object"!=typeof t)throw new TypeError("WaitingRoom.init: conf must be object. Found: "+t);if(!t.executionMode)return;this.executionMode=t.executionMode;if(t.onTimeout){if("function"!=typeof t.onTimeout)throw new TypeError("WaitingRoom.init: conf.onTimeout must be function, null or undefined. Found: "+t.onTimeout);this.onTimeout=t.onTimeout}if(t.waitTime){if(null!==t.waitTime&&"number"!=typeof t.waitTime)throw new TypeError("WaitingRoom.init: conf.waitTime must be number, null or undefined. Found: "+t.waitTime);this.waitTime=t.waitTime}t.startDate&&(this.startDate=(new Date(t.startDate)).toString());if(t.poolSize){if(t.poolSize&&"number"!=typeof t.poolSize)throw new TypeError("WaitingRoom.init: conf.poolSize must be number or undefined. Found: "+t.poolSize);this.poolSize=t.poolSize}if(t.groupSize){if(t.groupSize&&"number"!=typeof t.groupSize)throw new TypeError("WaitingRoom.init: conf.groupSize must be number or undefined. Found: "+t.groupSize);this.groupSize=t.groupSize}if(t.nGames){if(t.nGames&&"number"!=typeof t.nGames)throw new TypeError("WaitingRoom.init: conf.nGames must be number or undefined. Found: "+t.nGames);this.nGames=t.nGames}if(t.connected){if(t.connected&&"number"!=typeof t.connected)throw new TypeError("WaitingRoom.init: conf.connected must be number or undefined. Found: "+t.connected);this.connected=t.connected}if(t.disconnectIfNotSelected){if("boolean"!=typeof t.disconnectIfNotSelected)throw new TypeError("WaitingRoom.init: conf.disconnectIfNotSelected must be boolean or undefined. Found: "+t.disconnectIfNotSelected);this.disconnectIfNotSelected=t.disconnectIfNotSelected}else this.disconnectIfNotSelected=!1;t.playWithBotOption?this.playWithBotOption=!0:this.playWithBotOption=!1,t.selectTreatmentOption?this.selectTreatmentOption=!0:this.selectTreatmentOption=!1,this.displayExecMode(),this.playWithBotOption&&!document.getElementById("bot_btn")&&function(n){var r=document.createElement("div");r.role="group",r["aria-label"]="Play Buttons",r.className="btn-group";var i=document.createElement("input");i.className="btn btn-primary btn-lg",i.value=n.getText("playBot"),i.id="bot_btn",i.type="button",i.onclick=function(){n.playBotBtn.value=n.getText("connectingBots"),n.playBotBtn.disabled=!0,e.say("PLAYWITHBOT","SERVER",n.selectedTreatment),setTimeout(function(){n.playBotBtn.value=n.getText("playBot"),n.playBotBtn.disabled=!1},5e3)},r.appendChild(i),n.playBotBtn=i;if(n.selectTreatmentOption){var s=document.createElement("div");s.role="group",s["aria-label"]="Select Treatment",s.className="btn-group";var o=document.createElement("button");o.className="btn btn-default btn-lg dropdown-toggle",o["data-toggle"]="dropdown",o["aria-haspopup"]="true",o["aria-expanded"]="false",o.innerHTML=n.getText("selectTreatment");var u=document.createElement("span");u.className="caret",o.appendChild(u);var a=document.createElement("ul");a.className="dropdown-menu",a.style["text-align"]="left";var f,l,c,h,p;if(t.availableTreatments){f=document.createElement("li"),f.innerHTML=n.getText("gameTreatments"),f.className="dropdown-header",a.appendChild(f);for(c in t.availableTreatments)t.availableTreatments.hasOwnProperty(c)&&(f=document.createElement("li"),f.id=c,l=document.createElement("a"),l.href="#",l.innerHTML=""+c+": "+t.availableTreatments[c],f.appendChild(l),c==="treatment_rotate"?h=f:c==="treatment_random"?p=f:a.appendChild(f));f=document.createElement("li"),f.role="separator",f.className="divider",a.appendChild(f),f=document.createElement("li"),f.innerHTML=n.getText("defaultTreatments"),f.className="dropdown-header",a.appendChild(f),a.appendChild(h),a.appendChild(p)}s.appendChild(o),s.appendChild(a),r.appendChild(s),o.onclick=function(){a.style.display===""?a.style.display="block":a.style.display=""},a.onclick=function(e){var t;t=e.target,a.style.display="",t=t.parentNode.id,t||(t=e.target.parentNode.parentNode.id);if(!t)return;o.innerHTML=t+" ",o.appendChild(u),n.selectedTreatment=t},n.treatmentBtn=o}n.bodyDiv.appendChild(document.createElement("br")),n.bodyDiv.appendChild(r)}(this),this.on("destroyed",function(){n.dots&&n.dots.stop(),e.deregisterSetup("waitroom")})},t.prototype.startTimer=function(){var t=this;if(this.timer)return;if(!this.waitTime)return;this.timerDiv||(this.timerDiv=document.createElement("div"),this.timerDiv.id="timer-div"),this.timerDiv.appendChild(document.createTextNode("Maximum Waiting Time: ")),this.timer=e.widgets.append("VisualTimer",this.timerDiv,{milliseconds:this.waitTime,timeup:function(){t.bodyDiv.innerHTML=t.getText("waitedTooLong")},update:1e3}),this.timer.setTitle(),this.timer.panelDiv.className="ng_widget visualtimer",this.bodyDiv.appendChild(this.timerDiv),this.timer.start()},t.prototype.clearTimeout=function(){this.timeoutId&&(clearTimeout(this.timeoutId),this.timeoutId=null)},t.prototype.updateState=function(e){if(!e)return;"number"==typeof e.connected&&(this.connected=e.connected),"number"==typeof e.poolSize&&(this.poolSize=e.poolSize),"number"==typeof e.groupSize&&(this.groupSize=e.groupSize)},t.prototype.updateDisplay=function(){var e,t;this.connected>this.poolSize?(t=Math.floor(this.connected/this.groupSize),"undefined"!=typeof this.nGames&&(t=t>this.nGames?this.nGames:t),e=t*this.groupSize,this.playerCount.innerHTML=''+this.connected+""+" / "+this.poolSize,this.playerCountTooHigh.style.display="",this.playerCountTooHigh.innerHTML=this.getText("tooManyPlayers",{nGames:e})):(this.playerCount.innerHTML=this.connected+" / "+this.poolSize,this.playerCountTooHigh.style.display="none")},t.prototype.displayExecMode=function(){this.bodyDiv.innerHTML="",this.execModeDiv=document.createElement("div"),this.execModeDiv.id="exec-mode-div",this.execModeDiv.innerHTML=this.getText("executionMode"),this.playerCount=document.createElement("p"),this.playerCount.id="player-count",this.execModeDiv.appendChild(this.playerCount),this.playerCountTooHigh=document.createElement("div"),this.playerCountTooHigh.style.display="none",this.execModeDiv.appendChild(this.playerCountTooHigh),this.startDateDiv=document.createElement("div"),this.startDateDiv.style.display="none",this.execModeDiv.appendChild(this.startDateDiv),this.dots=W.getLoadingDots(),this.execModeDiv.appendChild(this.dots.span),this.bodyDiv.appendChild(this.execModeDiv),this.msgDiv=document.createElement("div"),this.bodyDiv.appendChild(this.msgDiv),this.waitTime&&this.startTimer()},t.prototype.append=function(){this.bodyDiv.innerHTML=this.getText("waitingForConf")},t.prototype.listeners=function(){var t;t=this,e.registerSetup("waitroom",function(n){if(!n)return;if("object"!=typeof n){e.warn("waiting room widget: invalid setup object: "+n);return}return n.executionMode?t.init(n):(t.setSounds(n.sounds),t.setTexts(n.texts)),n}),e.on.data("PLAYERSCONNECTED",function(e){if(!e.data)return;t.connected=e.data,t.updateDisplay()}),e.on.data("DISPATCH",function(e){var n,r;e=e||{},n=e.data||{},t.dots&&t.dots.stop(),n.action==="allPlayersConnected"?t.alertPlayer():(r=t.getText("exitCode",n),n.action==="notEnoughPlayers"?(t.bodyDiv.innerHTML=t.getText(n.action),t.onTimeout&&t.onTimeout(e.data),t.disconnect(t.bodyDiv.innerHTML+r)):n.action==="notSelected"?!1===n.shouldDispatchMoreGames||t.disconnectIfNotSelected?(t.bodyDiv.innerHTML=t.getText("notSelectedClosed"),t.disconnect(t.bodyDiv.innerHTML+r)):t.msgDiv.innerHTML=t.getText("notSelectedOpen"):n.action==="disconnect"&&t.disconnect(t.bodyDiv.innerHTML+r))}),e.on.data("TIME",function(n){n=n||{},e.info("waiting room: TIME IS UP!"),t.stopTimer()}),e.on.data("WAITTIME",function(e){t.updateState(e.data),t.updateDisplay()}),e.on("SOCKET_DISCONNECT",function(){t.stopTimer(),t.bodyDiv.innerHTML=t.getText("disconnect")}),e.on.data("ROOM_CLOSED",function(){t.disconnect(t.getText("roomClosed"))})},t.prototype.stopTimer=function(){this.timer&&(e.info("waiting room: STOPPING TIMER"),this.timer.destroy())},t.prototype.disconnect=function(t){t&&this.setText("disconnect",t),e.socket.disconnect(),this.stopTimer()},t.prototype.alertPlayer=function(){var t,n,r,i;r=this.getText("blinkTitle"),i=this.getSound("dispatch"),i&&J.playSound(i);if(!r)return;document.hasFocus&&document.hasFocus()?J.blinkTitle(r,{repeatFor:1}):(t=J.blinkTitle(r,{stopOnFocus:!0,stopOnClick:window}),n=function(){var e;t(),e=W.getFrame(),e&&e.removeEventListener("mouseover",n,!1)},e.events.ng.once("FRAME_GENERATED",function(e){e.addEventListener("mouseover",n,!1)}))}}(node) \ No newline at end of file diff --git a/lib/obj.js b/lib/obj.js index 498c66d..6fd4368 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -1,6 +1,6 @@ /** * # OBJ - * Copyright(c) 2017 Stefano Balietti + * Copyright(c) 2019 Stefano Balietti * MIT Licensed * * Collection of static functions to manipulate JavaScript objects @@ -1220,14 +1220,13 @@ /** * ## OBJ.melt * - * Creates a new object with the specified combination of - * properties - values + * Creates a new object with specific combination of properties - values * * The values are assigned cyclically to the properties, so that * they do not need to have the same length. E.g. * * ```javascript - * J.createObj(['a','b','c'], [1,2]); // { a: 1, b: 2, c: 1 } + * J.melt(['a','b','c'], [1,2]); // { a: 1, b: 2, c: 1 } * ``` * @param {array} keys The names of the keys to add to the object * @param {array} values The values to associate to the keys @@ -1420,6 +1419,27 @@ return out; }; + /** + * ## OBJ.reverseObj + * + * Returns a new object where they keys and values are switched + * + * @param {object} obj The object to reverse + * + * @return {object} The reversed object + */ + OBJ.reverseObj = function(o) { + var k, res; + res = {}; + if (!o) return res; + for (k in o) { + if (o.hasOwnProperty(k)) { + res[o[k]] = k; + } + } + return res; + }; + JSUS.extend(OBJ); })('undefined' !== typeof JSUS ? JSUS : module.parent.exports.JSUS); From cecda38d9b55732c1a841014428a95b52ad12010 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Fri, 29 Mar 2019 16:11:21 -0400 Subject: [PATCH 54/79] 1.2.0 --- CHANGELOG | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index accecf4..8652bd3 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # ChangeLog +## 1.2.0 +- `#OBJ.reverseObj` switch all keys with values. + ## 1.1.1 - `#PARSE.isNumber` fixed bug when ueq and leq params were set. diff --git a/package.json b/package.json index 0d5eb01..ac66c83 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "JSUS", "description": "JavaScript UtilS. Extended functional programming support. JSUS helps!", - "version": "1.1.1", + "version": "1.2.0", "keywords": [ "functional", "functional programming", From d14b49221ea171d6250a2b5068e386469252a6e6 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 18 Apr 2019 16:36:42 -0400 Subject: [PATCH 55/79] obj.reverseObj cb --- lib/obj.js | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/lib/obj.js b/lib/obj.js index 6fd4368..5a1a404 100644 --- a/lib/obj.js +++ b/lib/obj.js @@ -1425,16 +1425,29 @@ * Returns a new object where they keys and values are switched * * @param {object} obj The object to reverse + * @param {function} cb Optional. A callback processing a key-value pair. + * Takes as inputs current key and value and must return an array with + * updated key and value: [ newKey, newValue ]. * * @return {object} The reversed object */ - OBJ.reverseObj = function(o) { + OBJ.reverseObj = function(o, cb) { var k, res; + if (cb && 'function' !== typeof cb) { + throw new TypeError('OBJ.reverseObj: cb must be function or ' + + 'undefined. Found: ' + cb); + } res = {}; if (!o) return res; for (k in o) { if (o.hasOwnProperty(k)) { - res[o[k]] = k; + if (cb) { + k = cb(k, o[k]); + res[k[1]] = res[k[0]]; + } + else { + res[o[k]] = k; + } } } return res; From f97f795b999ed358ec6b780db01ca244860de05e Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Thu, 18 Apr 2019 16:36:58 -0400 Subject: [PATCH 56/79] 1.3.0 --- CHANGELOG | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 8652bd3..114438b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,8 @@ # ChangeLog +## 1.3.0 +- `#OBJ.reverseObj` accepts a callback to edit keys and values. + ## 1.2.0 - `#OBJ.reverseObj` switch all keys with values. diff --git a/package.json b/package.json index ac66c83..b3ec1d8 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "JSUS", "description": "JavaScript UtilS. Extended functional programming support. JSUS helps!", - "version": "1.2.0", + "version": "1.3.0", "keywords": [ "functional", "functional programming", From 40ab6857be556bfb782065b90df6c3f0c2b404f1 Mon Sep 17 00:00:00 2001 From: Stefano Balietti Date: Sun, 5 May 2019 16:46:07 -0400 Subject: [PATCH 57/79] randomDate and randomKey --- build/jsus.js | 78 ++- build/jsus.min.js | 2 +- build/nodegame-widgets.js | 1095 +++++++++++++++++++++++++++------ build/nodegame-widgets.min.js | 2 +- lib/obj.js | 22 +- lib/random.js | 39 +- 6 files changed, 1033 insertions(+), 205 deletions(-) diff --git a/build/jsus.js b/build/jsus.js index 49ff18b..bc74d4b 100644 --- a/build/jsus.js +++ b/build/jsus.js @@ -5653,6 +5653,25 @@ return name; }; + /** + * ## OBJ.randomKey + * + * Returns a random key from an existing object + * + * @param {object} obj The object from which the key will be extracted + * + * @return {string} The random key + */ + OBJ.randomKey = function(obj) { + var keys; + if ('object' !== typeof obj) { + throw new TypeError('OBJ.randomKey: obj must be object. ' + + 'Found: ' + obj); + } + keys = Object.keys(obj); + return keys[ keys.length * Math.random() << 0]; + }; + /** * ## OBJ.augment * @@ -5776,7 +5795,8 @@ OBJ.getKeyByValue = function(obj, value, allKeys) { var key, out; if ('object' !== typeof obj) { - throw new TypeError('OBJ.getKeyByValue: obj must be object.'); + throw new TypeError('OBJ.getKeyByValue: obj must be object. ' + + 'Found: ' + obj); } if (allKeys) out = []; for (key in obj) { @@ -5796,16 +5816,29 @@ * Returns a new object where they keys and values are switched * * @param {object} obj The object to reverse + * @param {function} cb Optional. A callback processing a key-value pair. + * Takes as inputs current key and value and must return an array with + * updated key and value: [ newKey, newValue ]. * - * @return {object} The reversed object + * @return {object} The reversed object */ - OBJ.reverseObj = function(o) { + OBJ.reverseObj = function(o, cb) { var k, res; + if (cb && 'function' !== typeof cb) { + throw new TypeError('OBJ.reverseObj: cb must be function or ' + + 'undefined. Found: ' + cb); + } res = {}; if (!o) return res; for (k in o) { if (o.hasOwnProperty(k)) { - res[o[k]] = k; + if (cb) { + k = cb(k, o[k]); + res[k[1]] = res[k[0]]; + } + else { + res[o[k]] = k; + } } } return res; @@ -6740,6 +6773,43 @@ return Math.floor(RANDOM.random(a, b) + 1); }; + /** + * ## RANDOM.randomDate + * + * Generates a pseudo-random date between + * + * @param {Date} startDate The lower date + * @param {Date} endDate Optional. The upper date. Default: today. + * + * @return {number} A random date in the chosen interval + * + * @see RANDOM.randomDate + */ + RANDOM.randomDate = (function() { + function isValidDate(date) { + return date && + Object.prototype.toString.call(date) === "[object Date]" && + !isNaN(date); + } + return function(startDate, endDate) { + if (!isValidDate(startDate)) { + throw new TypeError('randomDate: startDate must be a valid ' + + 'date. Found: ' + startDate); + } + if (endDate) { + if (!isValidDate(endDate)) { + throw new TypeError('randomDate: endDate must be a valid ' + + 'date or undefined. Found: ' + endDate); + } + } + else { + endDate = new Date(); + } + return new Date(startDate.getTime() + Math.random() * + (endDate.getTime() - startDate.getTime())); + }; + })(); + /** * ## RANDOM.sample * diff --git a/build/jsus.min.js b/build/jsus.min.js index 2182967..859874a 100644 --- a/build/jsus.min.js +++ b/build/jsus.min.js @@ -8,4 +8,4 @@ * See README.md for extra help. * --- */ -(function(e){var t=e.JSUS={};t._classes={},"undefined"==typeof console&&(console={}),"undefined"==typeof console.log&&(console.log=function(){}),t.log=function(e){console.log(e)},t.extend=function(e,n){var r,i;if("object"!=typeof e&&"function"!=typeof e)return n;"undefined"==typeof n&&(n=n||this,"function"==typeof e?(r=e.toString(),r=r.substr("function ".length),r=r.substr(0,r.indexOf("("))):r=e.constructor||e.__proto__.constructor,r&&(this._classes[r]=e));for(i in e)e.hasOwnProperty(i)&&(typeof n[i]!="object"?n[i]=e[i]:t.extend(e[i],n[i]));return e.prototype&&t.extend(e.prototype,n.prototype||n),n},t.require=function(e,n){var r;n="undefined"==typeof n?!0:n;if(n&&"undefined"==typeof t.clone)return t.log("JSUS.require: JSUS.clone not found, but clone requested. Cannot continue."),!1;if("undefined"==typeof e)r=t._classes;else{r=t._classes[e];if("undefined"==typeof r)return t.log("JSUS.require: could not find component "+e),!1}return n?t.clone(r):r},t.isNodeJS=function(){return"undefined"!=typeof module&&"undefined"!=typeof module.exports&&"function"==typeof require},t.isNodeJS()?(require("./lib/compatibility"),require("./lib/obj"),require("./lib/array"),require("./lib/time"),require("./lib/eval"),require("./lib/dom"),require("./lib/random"),require("./lib/parse"),require("./lib/queue"),require("./lib/fs")):e.J=e.JSUS})("undefined"!=typeof module&&"undefined"!=typeof module.exports?module.exports:window),function(e){"use strict";function t(){}Array.prototype.filter||(Array.prototype.filter=function(e){if(this===void 0||this===null)throw new TypeError;var t=new Object(this),n=t.length>>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=function(){function t(e,r,i,s,o,u,a,f,l,c,h,p,d){var v,m,g,y;m=i===r;for(v in e)if(e.hasOwnProperty(v)){g="object"==typeof e[v];if(u||a&&(m||!g)||f&&m)o?(y=s+v,p[y]||(d?n(y,c,d):c.push(y))):p[v]||(h?h[v]||(d?n(v,c,d):c.push(v),h[v]=!0):d?n(v,c,d):c.push(v));g&&i1&&(n.push(i[1]),i.length>2&&n.push(i[2]))):function(){var e=-1,t=i.length;for(;++e=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in:e>=n)?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o>>0;if(typeof e!="function")throw new TypeError;var r=[],i=arguments[1];for(var s=0;s=n)o.push(i(s)),s-=r;return o},t.each=function(e,t,n){var r,i;if("object"!=typeof e)throw new TypeError("ARRAY.each: array must be object. Found: "+e);if("function"!=typeof t)throw new TypeError("ARRAY.each: cb must be function. Found: "+t);n=n||this,i=e.length;for(r=0;r=t&&(s.push(a),f=0,a=[]),a.push(n[u]),n.splice(u,1),r=n.length,f++;return a.length>0&&s.push(a),s},t._latinSquare=function(t,n,r){r="undefined"==typeof r?!0:r;if(t===n&&!r)return!1;var i=[],s=[];for(var o=0;oe&&(n=e),t._latinSquare(e,n,!0))},t.latinSquareNoSelf=function(e,n){return n||(n=e-1),!e||e<0||n<0?!1:(n>e&&(n=e-1),t._latinSquare(e,n,!1))},t.generateCombinations=function n(e,t){var r,i,s,o,u;s=[];for(r=0;r0;s--)r=Math.floor(Math.random()*(s+1)),i=t[r],t[r]=t[s],t[s]=i;return t},t.getNRandom=function(e,n){return t.shuffle(e).slice(0,n)},t.distinct=function(e){var n=[];return e?(t.each(e,function(e){t.inArray(e,n)||n.push(e)}),n):n},t.transpose=function(e){if(!e)return;var n,r,i,s,o=[];n=e.length||0,r=t.isArray(e[0])?e[0].length:0;if(n===0||r===0)return o;for(i=0;it)throw new Error("DOM.generateUniqueId: could not find unique id within "+t+" trials.")}return s}}(),r.removeClass=function(e,t){var n,i;if(!r.isElement(e))throw new TypeError("DOM.removeClass: elem must be HTMLElement. Found: "+e);if(t){if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.removeClass: className must be HTMLElement. Found: "+t);n=new RegExp("(?:^|\\s)"+t+"(?!\\S)"),i=e.className=e.className.replace(n,"")}return e},r.addClass=function(e,t){if(!r.isElement(e))throw new TypeError("DOM.addClass: elem must be HTMLElement. Found: "+e);if(t){t instanceof Array&&(t=t.join(" "));if("string"!=typeof t||t.trim()==="")throw new TypeError("DOM.addClass: className must be HTMLElement. Found: "+t);e.className?e.className+=" "+t:e.className=t}return e},r.getElementsByClassName=function(e,t,n){var r,i,s,o,u,a;r=[],s=n||"*";if(e.evaluate){o="//"+s+'[contains(concat(" ", normalize-space(@class), " "), "'+t+' ")]',o=e.evaluate(o,e,null,0,null);while(i=o.iterateNext())r.push(i)}else{a=new RegExp("(^| )"+t+"( |$)"),o=e.getElementsByTagName(s);for(u=0;u1){var i=new Object(arguments[1]);for(var s in i)t.call(i,s)&&(r[s]=i[s])}return r}}():Object.create}(),t.equals=function(e,n){var r,i,s,o;r=typeof e,i=typeof n;if(r!==i)return!1;if("undefined"===r||"undefined"===i)return e===n;if(e===null||n===null)return e===n;if("number"===r&&isNaN(e)&&"number"===i&&isNaN(n))return isNaN(e)&&isNaN(n);s={number:"",string:"","boolean":""};if(r in s)return e===n;if("function"===r)return e.toString()===n.toString();for(o in e)if(e.hasOwnProperty(o)){if("undefined"==typeof n[o]&&"undefined"!=typeof e[o])return!1;if(!n[o]&&e[o])return!1;if("function"==typeof e[o]){if(e[o].toString()!==n[o].toString())return!1}else if(!t.equals(e[o],n[o]))return!1}for(o in n)if(n.hasOwnProperty(o)){if("undefined"==typeof e[o]&&"undefined"!=typeof n[o])return!1;if(!e[o]&&n[o])return!1}return!0},t.isEmpty=function(e){var t;if(!e)return!0;if("string"==typeof e)return e.trim()==="";if("number"==typeof e)return!1;if("function"==typeof e)return!1;for(t in e)if(e.hasOwnProperty(t))return!1;return!0},t.size=t.getListSize=function(e){var t,n;if(!e)return 0;if("number"==typeof e)return 0;if("string"==typeof e)return 0;t=0;for(n in e)e.hasOwnProperty(n)&&t++;return t},t._obj2Array=function(e,n,r,i){var s,o;if("object"!=typeof e)return[e];if(r){i="undefined"!=typeof i?i:1;if(i>r)return[e];i+=1}s=[];for(o in e)e.hasOwnProperty(o)&&(n&&s.push(o),"object"==typeof e[o]?s=s.concat(t._obj2Array(e[o],n,r,i)):s.push(e[o]));return s},t.obj2Array=function(e,n){return t._obj2Array(e,!1,n)},t.obj2KeyedArray=t.obj2KeyArray=function(e,n){return t._obj2Array(e,!0,n)},t.obj2QueryString=function(e){var t,n;if("object"!=typeof e)throw new TypeError("JSUS.objectToQueryString: obj must be object.");t=[];for(n in e)e.hasOwnProperty(n)&&t.push(encodeURIComponent(n)+"="+encodeURIComponent(e[n]));return"?"+t.join("&")},t.keys=function(){function t(e,r,i,s,o,u,a,f,l,c,h,p,d){var v,m,g,y;m=i===r;for(v in e)if(e.hasOwnProperty(v)){g="object"==typeof e[v];if(u||a&&(m||!g)||f&&m)o?(y=s+v,p[y]||(d?n(y,c,d):c.push(y))):p[v]||(h?h[v]||(d?n(v,c,d):c.push(v),h[v]=!0):d?n(v,c,d):c.push(v));g&&i1&&(n.push(i[1]),i.length>2&&n.push(i[2]))):function(){var e=-1,t=i.length;for(;++e=i)t(r,s,a);else{l=o||!e.isArray(r);for(f in r)r.hasOwnProperty(f)&&("object"==typeof r[f]&&i&&u+1<=i?n(r[f],s,u+1,l?a.concat(f):a):t(r[f],s,l?a.concat(f):a))}},function(t,u,a,f){var l;if("object"!=typeof t)throw new TypeError("JSUS.split: o must be object. Found: "+t);if("string"!=typeof u||u.trim()==="")throw new TypeError("JSUS.split: key must a non-empty string. Found: "+u);if(a&&("number"!=typeof a||a<0))throw new TypeError("JSUS.split: l must a non-negative number or undefined. Found: "+a);return r=e.clone(t),"object"!=typeof t[u]?[r]:(l=[],s=u,r[u]={},i="undefined"==typeof a?1:a,o=f,n(t[u],l,0,[]),s=undefined,r=undefined,i=undefined,o=undefined,l)}}(),t.melt=function(e,t){var n={},r=t.length;for(var i=0;ir)return}return i},t.randomKey=function(e){var t;if("object"!=typeof e)throw new TypeError("OBJ.randomKey: obj must be object. Found: "+e);return t=Object.keys(e),t[t.length*Math.random()<<0]},t.augment=function(e,n,r){var i,s;r=r||t.keys(e);for(i=0;in:e>=n)?!1:e)},t.isEmail=function(e){var t;return"string"!=typeof e?!1:e.trim().length<5?!1:(t=e.indexOf("@"),t===-1||t===0||t===e.length-1?!1:(t=e.lastIndexOf("."),t===-1||t===e.length-1||t>t+1?!1:!0))},t.range=function(r,i){var s,o,u,a,f,l,c,h,p,d,v;a=[];if("undefined"==typeof r)return a;if("number"==typeof r)r=""+r;else if("string"!=typeof r)throw new TypeError("PARSE.range: expr must be string, number, undefined. Found: "+r);if("undefined"==typeof i)i=r;else if(e.isArray(i)){if(i.length===0)return a;f=Math.min.apply(null,i),l=Math.max.apply(null,i)}else if("object"==typeof i){if("function"!=typeof i.next)throw new TypeError("PARSE.range: available.next must be function. Found: "+i.next);if("function"!=typeof i.isFinished)throw new TypeError("PARSE.range: available.isFinished must be function. Found: "+i.isFinished);if("number"!=typeof i.begin)throw new TypeError("PARSE.range: available.begin must be number. Found: "+i.begin);if("number"!=typeof i.end)throw new TypeError("PARSE.range: available.end must be number. Found: "+i.end);f=i.begin,l=i.end}else{if("string"!=typeof i)throw new TypeError("PARSE.range: available must be string, array, object or undefined. Found: "+i);i=n(i),h=i.match(/([-+]?\d+)/g);if(h===null)throw new Error("PARSE.range: no numbers in available: "+i);c=Math.min.apply(null,h),i=t.range(i,{begin:c,end:Math.max.apply(null,h),value:c,next:function(){return this.value++},isFinished:function(){return this.value>this.end}}),f=Math.min.apply(null,i),l=Math.max.apply(null,i)}r=r.replace(/end/g,parseInt(l,10)),r=r.replace(/begin/g,parseInt(f,10)),r=n(r),r=r.replace(/([-+]?\d+\.\d+)/g,function(e,t){return parseInt(t,10)}),p=/[^ \*\d<>=!\|&\.\[\],\(\)\-\+%]/g;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);r=r.replace(/([^& ]) *& *([^& ])/g,"$1&&$2"),r=r.replace(/([^| ]) *\| *([^| ])/g,"$1||$2"),r=r.replace(/([-+]?\d+)/g,"(x==$1)"),r=r.replace(/% *\(x==([-+]?\d+)\)/,"!(x%$1)"),r=r.replace(/!\(x%([-+]?\d+)\) *={1,} *\(x==([-+]?\d+)\)/g,"(x%$1==$2)"),r=r.replace(/([<>]=?) *\(x==([-+]?\d+)\)/g,"(x$1$2)"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(\+?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==(-\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x<=$1&&x>=$3&&!((x- $1)%$2))"),r=r.replace(/\(x==([-+]?\d+)\)\.{2,}\(x==([-+]?\d+)\)/g,"(x>=$1&&x<=$2)"),r=r.replace(/([(\[]) *\(x==([-+]?\d+)\) *, *\(x==([-+]?\d+)\) *([\])])/g,function(e,t,n,r,i){return"(x>"+(t=="("?"":"=")+n+"&&x<"+(i==")"?"":"=")+r+")"}),r=r.replace("*",1),r=r.replace(/\s/g,""),r=r.replace(/\)[,] *(!*)\(/g,")||$1("),p=/[^ \d<>=!\|&,\(\)\-\+%x\.]/g,d=/[^ &!|\(] *\(/g,v=/\.[^\d]|[^\d]\./;if(r.match(p))throw new Error("PARSE.range: invalid characters found: "+r);if(r.match(d))throw new Error("PARSE.range: invalid character before opending bracket found: "+r);if(r.match(v))throw new Error("PARSE.range: invalid dot found: "+r);if(e.isArray(i)){s=-1,o=i.length;for(;++s1?n[1].trim():""},e.extend(t)}("undefined"!=typeof JSUS?JSUS:module.parent.exports.JSUS),function(e){"use strict";function n(){this.queue=[],this.inProgress={}}var t={};t.getQueue=function(){return new n},n.prototype.isReady=function(){return e.isEmpty(this.inProgress)},n.prototype.onReady=function(t){if("function"!=typeof t)throw new TypeError("Queue.onReady: cb must be function. Found: "+t);e.isEmpty(this.inProgress)?t():this.queue.push(t)},n.prototype.add=function(t){if(t&&"string"!=typeof t)throw new Error("Queue.add: key must be string.");t=e.uniqueKey(this.inProgress,t);if("string"!=typeof t)throw new Error("Queue.add: an error occurred generating unique key.");return this.inProgress[t]=t,t},n.prototype.remove=function(t){if("string"!=typeof t)throw new Error("Queue.remove: key must be string.");delete this.inProgress[t],e.isEmpty(this.inProgress)&&this.executeAndClear()},n.prototype.getRemoveCb=function(e){var t;if("string"!=typeof e)throw new Error("Queue.getRemoveCb: key must be string.");return t=this,function(){t.remove(e)}},n.prototype.executeAndClear=function(){var e,t;e=-1,t=this.queue.length;for(;++e=1?s(o,u):(r=Math.sqrt(-2*Math.log(p)/p),a=c*r,n=h*r,i=!0,u*a+o))}}()},t.nextNormal=t.getNormalGenerator(),t.nextLogNormal=function(e,n){if("number"!=typeof e)throw new TypeError("nextLogNormal: mu must be number.");if("number"!=typeof n)throw new TypeError("nextLogNormal: sigma must be number.");return Math.exp(t.nextNormal(e,n))},t.nextExponential=function(e){if("number"!=typeof e)throw new TypeError("nextExponential: lambda must be number.");if(e<=0)throw new TypeError("nextExponential: lambda must be greater than 0.");return-Math.log(1-Math.random())/e},t.nextBinomial=function(e,t){var n,r;if("number"!=typeof e)throw new TypeError("nextBinomial: p must be number.");if("number"!=typeof t)throw new TypeError("nextBinomial: trials must be number.");if(e<0||e>1)throw new TypeError("nextBinomial: p must between 0 and 1.");if(t<1)throw new TypeError("nextBinomial: trials must be greater than 0.");n=0,r=0;while(n 0 or undefined. Found: "+t);if("undefined"!=typeof n){if("string"!=typeof n||n.trim()==="")throw new Error("randomString: chars must a non-empty string or undefined. Found: "+n)}else if(r)throw new Error("randomString: useChars is TRUE, but chars is undefined.");t=t||6,n=n||"a",i="";if(!r){n.indexOf("a")>-1&&(i+="abcdefghijklmnopqrstuvwxyz"),n.indexOf("A")>-1&&(i+="ABCDEFGHIJKLMNOPQRSTUVWXYZ"),n.indexOf("1")>-1&&(i+="0123456789"),n.indexOf("!")>-1&&(i+="!~`@#$%^&*()_+-={}[]:\";'<>?,./|\\"),u=n.indexOf("_");if(u>-1){u=n.charAt(u+1),u=e.isInt(u,0)||1;if(u===1)i+=" ";else if(u===2)i+=" ";else if(u===3)i+=" ";else{o=-1;for(;++o