+ *
+ *
+ */
+ setStyle: function(elm, style) {
+ elm = Ink.i(elm);
+ if (elm === null) { return; }
+ if (typeof style === 'string') {
+ elm.style.cssText += '; '+style;
+
+ if (style.indexOf('opacity') !== -1) {
+ this.setOpacity(elm, style.match(/opacity:\s*(\d?\.?\d*)/)[1]);
+ }
+ }
+ else {
+ for (var prop in style) {
+ if (style.hasOwnProperty(prop)){
+ if (prop === 'opacity') {
+ this.setOpacity(elm, style[prop]);
+ }
+ else if (prop === 'float' || prop === 'cssFloat') {
+ if (typeof elm.style.styleFloat === 'undefined') {
+ elm.style.cssFloat = style[prop];
+ }
+ else {
+ elm.style.styleFloat = style[prop];
+ }
+ } else {
+ elm.style[prop] = style[prop];
+ }
+ }
+ }
+ }
+ },
+
+
+ /**
+ * Makes an element visible
+ *
+ * @method show
+ * @param {DOMElement|String} elm DOM element or element id
+ * @param {String} forceDisplayProperty Css display property to apply on show
+ */
+ show: function(elm, forceDisplayProperty) {
+ elm = Ink.i(elm);
+ if (elm !== null) {
+ elm.style.display = (forceDisplayProperty) ? forceDisplayProperty : '';
+ }
+ },
+
+ /**
+ * Hides an element
+ *
+ * @method hide
+ * @param {DOMElement|String} elm DOM element or element id
+ */
+ hide: function(elm) {
+ elm = Ink.i(elm);
+ if (elm !== null) {
+ elm.style.display = 'none';
+ }
+ },
+
+ /**
+ * shows or hides according to param show
+ *
+ * @method showHide
+ * @param {DOMElement|String} elm DOM element or element id
+ * @param {boolean} [show=false] Whether to show or hide `elm`.
+ */
+ showHide: function(elm, show) {
+ elm = Ink.i(elm);
+ if (elm) {
+ elm.style.display = show ? '' : 'none';
+ }
+ },
+
+ /**
+ * Shows or hides an element depending on current state
+ * @method toggle
+ * @param {DOMElement|String} elm DOM element or element id
+ * @param {Boolean} forceShow Forces showing if element is hidden
+ */
+ toggle: function(elm, forceShow) {
+ elm = Ink.i(elm);
+ if (elm !== null) {
+ if (typeof forceShow !== 'undefined') {
+ if (forceShow === true) {
+ this.show(elm);
+ } else {
+ this.hide(elm);
+ }
+ } else {
+ if (this.getStyle(elm,'display').toLowerCase() === 'none') {
+ this.show(elm);
+ }
+ else {
+ this.hide(elm);
+ }
+ }
+ }
+ },
+
+ _getRefTag: function(head){
+ if (head.firstElementChild) {
+ return head.firstElementChild;
+ }
+
+ for (var child = head.firstChild; child; child = child.nextSibling){
+ if (child.nodeType === 1){
+ return child;
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Adds css style tags to the head section of a page
+ *
+ * @method appendStyleTag
+ * @param {String} selector The css selector for the rule
+ * @param {String} style The content of the style rule
+ * @param {Object} options Options for the tag
+ * @param {String} [options.type] file type
+ * @param {Boolean} [options.force] if true, style tag will be appended to end of head
+ */
+ appendStyleTag: function(selector, style, options){
+ options = Ink.extendObj({
+ type: 'text/css',
+ force: false
+ }, options || {});
+
+ var styles = document.getElementsByTagName("style"),
+ oldStyle = false, setStyle = true, i, l;
+
+ for (i=0, l=styles.length; i= 0) {
+ setStyle = false;
+ }
+ }
+
+ if (setStyle) {
+ var defStyle = document.createElement("style"),
+ head = document.getElementsByTagName("head")[0],
+ refTag = false, styleStr = '';
+
+ defStyle.type = options.type;
+
+ styleStr += selector +" {";
+ styleStr += style;
+ styleStr += "} ";
+
+ if (typeof defStyle.styleSheet !== "undefined") {
+ defStyle.styleSheet.cssText = styleStr;
+ } else {
+ defStyle.appendChild(document.createTextNode(styleStr));
+ }
+
+ if (options.force){
+ head.appendChild(defStyle);
+ } else {
+ refTag = this._getRefTag(head);
+ if (refTag){
+ head.insertBefore(defStyle, refTag);
+ }
+ }
+ }
+ },
+
+ /**
+ * Adds a link tag for a stylesheet to the head section of a page
+ *
+ * @method appendStylesheet
+ * @param {String} path File path
+ * @param {Object} options Options for the tag
+ * @param {String} [options.media='screen'] media type
+ * @param {String} [options.type='text/css'] file type
+ * @param {Boolean} [options.force=false] if true, tag will be appended to end of head
+ */
+ appendStylesheet: function(path, options){
+ options = Ink.extendObj({
+ media: 'screen',
+ type: 'text/css',
+ force: false
+ }, options || {});
+
+ var refTag,
+ style = document.createElement("link"),
+ head = document.getElementsByTagName("head")[0];
+
+ style.media = options.media;
+ style.type = options.type;
+ style.href = path;
+ style.rel = "Stylesheet";
+
+ if (options.force){
+ head.appendChild(style);
+ }
+ else {
+ refTag = this._getRefTag(head);
+ if (refTag){
+ head.insertBefore(style, refTag);
+ }
+ }
+ },
+
+ /**
+ * Loads CSS via LINK element inclusion in HEAD (skips append if already there)
+ *
+ * Works similarly to appendStylesheet but:
+ * a) supports all browsers;
+ * b) supports optional callback which gets invoked once the CSS has been applied
+ *
+ * @method appendStylesheetCb
+ * @param {String} cssURI URI of the CSS to load, if empty ignores and just calls back directly
+ * @param {Function(cssURI)} [callback] optional callback which will be called once the CSS is loaded
+ */
+ _loadingCSSFiles: {},
+ _loadedCSSFiles: {},
+ appendStylesheetCb: function(url, callback) {
+ if (!url) {
+ return callback(url);
+ }
+
+ if (this._loadedCSSFiles[url]) {
+ return callback(url);
+ }
+
+ var cbs = this._loadingCSSFiles[url];
+ if (cbs) {
+ return cbs.push(callback);
+ }
+
+ this._loadingCSSFiles[url] = [callback];
+
+ var linkEl = document.createElement('link');
+ linkEl.type = 'text/css';
+ linkEl.rel = 'stylesheet';
+ linkEl.href = url;
+
+ var headEl = document.getElementsByTagName('head')[0];
+ headEl.appendChild(linkEl);
+
+ var imgEl = document.createElement('img');
+ /*
+ var _self = this;
+ (function(_url) {
+ imgEl.onerror = function() {
+ //var url = this;
+ var url = _url;
+ _self._loadedCSSFiles[url] = true;
+ var callbacks = _self._loadingCSSFiles[url];
+ for (var i = 0, f = callbacks.length; i < f; ++i) {
+ callbacks[i](url);
+ }
+ delete _self._loadingCSSFiles[url];
+ };
+ })(url);
+ */
+ imgEl.onerror = Ink.bindEvent(function(event, _url) {
+ //var url = this;
+ var url = _url;
+ this._loadedCSSFiles[url] = true;
+ var callbacks = this._loadingCSSFiles[url];
+ for (var i = 0, f = callbacks.length; i < f; ++i) {
+ callbacks[i](url);
+ }
+ delete this._loadingCSSFiles[url];
+ }, this, url);
+ imgEl.src = url;
+ },
+
+ /**
+ * Converts decimal to hexadecimal values, for use with colors
+ *
+ * @method decToHex
+ * @param {String} dec Either a single decimal value,
+ * an rgb(r, g, b) string or an Object with r, g and b properties
+ * @return Hexadecimal value
+ */
+ decToHex: function(dec) {
+ var normalizeTo2 = function(val) {
+ if (val.length === 1) {
+ val = '0' + val;
+ }
+ val = val.toUpperCase();
+ return val;
+ };
+
+ if (typeof dec === 'object') {
+ var rDec = normalizeTo2(parseInt(dec.r, 10).toString(16));
+ var gDec = normalizeTo2(parseInt(dec.g, 10).toString(16));
+ var bDec = normalizeTo2(parseInt(dec.b, 10).toString(16));
+ return rDec+gDec+bDec;
+ }
+ else {
+ dec += '';
+ var rgb = dec.match(/\((\d+),\s?(\d+),\s?(\d+)\)/);
+ if (rgb !== null) {
+ return normalizeTo2(parseInt(rgb[1], 10).toString(16)) +
+ normalizeTo2(parseInt(rgb[2], 10).toString(16)) +
+ normalizeTo2(parseInt(rgb[3], 10).toString(16));
+ }
+ else {
+ return normalizeTo2(parseInt(dec, 10).toString(16));
+ }
+ }
+ },
+
+ /**
+ * Converts hexadecimal values to decimal, for use with colors
+ *
+ * @method hexToDec
+ * @param {String} hex hexadecimal value with 6, 3, 2 or 1 characters
+ * @return {Number} Object with properties r, g, b if length of number is >= 3 or decimal value instead.
+ */
+ hexToDec: function(hex){
+ if (hex.indexOf('#') === 0) {
+ hex = hex.substr(1);
+ }
+ if (hex.length === 6) { // will return object RGB
+ return {
+ r: parseInt(hex.substr(0,2), 16),
+ g: parseInt(hex.substr(2,2), 16),
+ b: parseInt(hex.substr(4,2), 16)
+ };
+ }
+ else if (hex.length === 3) { // will return object RGB
+ return {
+ r: parseInt(hex.charAt(0) + hex.charAt(0), 16),
+ g: parseInt(hex.charAt(1) + hex.charAt(1), 16),
+ b: parseInt(hex.charAt(2) + hex.charAt(2), 16)
+ };
+ }
+ else if (hex.length <= 2) { // will return int
+ return parseInt(hex, 16);
+ }
+ },
+
+ /**
+ * use this to obtain the value of a CSS property (searched from loaded CSS documents)
+ *
+ * @method getPropertyFromStylesheet
+ * @param {String} selector a CSS rule. must be an exact match
+ * @param {String} property a CSS property
+ * @return {String} value of the found property, or null if it wasn't matched
+ */
+ getPropertyFromStylesheet: function(selector, property) {
+ var rule = this.getRuleFromStylesheet(selector);
+ if (rule) {
+ return rule.style[property];
+ }
+ return null;
+ },
+
+ getPropertyFromStylesheet2: function(selector, property) {
+ var rules = this.getRulesFromStylesheet(selector);
+ /*
+ rules.forEach(function(rule) {
+ var x = rule.style[property];
+ if (x !== null && x !== undefined) {
+ return x;
+ }
+ });
+ */
+ var x;
+ for(var i=0, t=rules.length; i < t; i++) {
+ x = rules[i].style[property];
+ if (x !== null && x !== undefined) {
+ return x;
+ }
+ }
+ return null;
+ },
+
+ getRuleFromStylesheet: function(selector) {
+ var sheet, rules, ri, rf, rule;
+ var s = document.styleSheets;
+ if (!s) {
+ return null;
+ }
+
+ for (var si = 0, sf = document.styleSheets.length; si < sf; ++si) {
+ sheet = document.styleSheets[si];
+ rules = sheet.rules ? sheet.rules : sheet.cssRules;
+ if (!rules) { return null; }
+
+ for (ri = 0, rf = rules.length; ri < rf; ++ri) {
+ rule = rules[ri];
+ if (!rule.selectorText) { continue; }
+ if (rule.selectorText === selector) {
+ return rule;
+ }
+ }
+ }
+
+ return null;
+ },
+
+ getRulesFromStylesheet: function(selector) {
+ var res = [];
+ var sheet, rules, ri, rf, rule;
+ var s = document.styleSheets;
+ if (!s) { return res; }
+
+ for (var si = 0, sf = document.styleSheets.length; si < sf; ++si) {
+ sheet = document.styleSheets[si];
+ rules = sheet.rules ? sheet.rules : sheet.cssRules;
+ if (!rules) {
+ return null;
+ }
+
+ for (ri = 0, rf = rules.length; ri < rf; ++ri) {
+ rule = rules[ri];
+ if (!rule.selectorText) { continue; }
+ if (rule.selectorText === selector) {
+ res.push(rule);
+ }
+ }
+ }
+
+ return res;
+ },
+
+ getPropertiesFromRule: function(selector) {
+ var rule = this.getRuleFromStylesheet(selector);
+ var props = {};
+ var prop, i, f;
+
+ /*if (typeof rule.style.length === 'snumber') {
+ for (i = 0, f = rule.style.length; i < f; ++i) {
+ prop = this._camelCase( rule.style[i] );
+ props[prop] = rule.style[prop];
+ }
+ }
+ else { // HANDLES IE 8, FIREFOX RULE JOINING... */
+ rule = rule.style.cssText;
+ var parts = rule.split(';');
+ var steps, val, pre, pos;
+ for (i = 0, f = parts.length; i < f; ++i) {
+ if (parts[i].charAt(0) === ' ') {
+ parts[i] = parts[i].substring(1);
+ }
+ steps = parts[i].split(':');
+ prop = this._camelCase( steps[0].toLowerCase() );
+ val = steps[1];
+ if (val) {
+ val = val.substring(1);
+
+ if (prop === 'padding' || prop === 'margin' || prop === 'borderWidth') {
+
+ if (prop === 'borderWidth') { pre = 'border'; pos = 'Width'; }
+ else { pre = prop; pos = ''; }
+
+ if (val.indexOf(' ') !== -1) {
+ val = val.split(' ');
+ props[pre + 'Top' + pos] = val[0];
+ props[pre + 'Bottom'+ pos] = val[0];
+ props[pre + 'Left' + pos] = val[1];
+ props[pre + 'Right' + pos] = val[1];
+ }
+ else {
+ props[pre + 'Top' + pos] = val;
+ props[pre + 'Bottom'+ pos] = val;
+ props[pre + 'Left' + pos] = val;
+ props[pre + 'Right' + pos] = val;
+ }
+ }
+ else if (prop === 'borderRadius') {
+ if (val.indexOf(' ') !== -1) {
+ val = val.split(' ');
+ props.borderTopLeftRadius = val[0];
+ props.borderBottomRightRadius = val[0];
+ props.borderTopRightRadius = val[1];
+ props.borderBottomLeftRadius = val[1];
+ }
+ else {
+ props.borderTopLeftRadius = val;
+ props.borderTopRightRadius = val;
+ props.borderBottomLeftRadius = val;
+ props.borderBottomRightRadius = val;
+ }
+ }
+ else {
+ props[prop] = val;
+ }
+ }
+ }
+ //}
+ //console.log(props);
+
+ return props;
+ },
+
+ /**
+ * Changes the font size of the elements which match the given CSS rule
+ * For this function to work, the CSS file must be in the same domain than the host page, otherwise JS can't access it.
+ *
+ * @method changeFontSize
+ * @param {String} selector CSS selector rule
+ * @param {Number} delta number of pixels to change on font-size
+ * @param {String} [op] supported operations are '+' and '*'. defaults to '+'
+ * @param {Number} [minVal] if result gets smaller than minVal, change does not occurr
+ * @param {Number} [maxVal] if result gets bigger than maxVal, change does not occurr
+ */
+ changeFontSize: function(selector, delta, op, minVal, maxVal) {
+ var that = this;
+ Ink.requireModules(['Ink.Dom.Selector_1'], function(Selector) {
+ var e;
+ if (typeof selector !== 'string') { e = '1st argument must be a CSS selector rule.'; }
+ else if (typeof delta !== 'number') { e = '2nd argument must be a number.'; }
+ else if (op !== undefined && op !== '+' && op !== '*') { e = '3rd argument must be one of "+", "*".'; }
+ else if (minVal !== undefined && (typeof minVal !== 'number' || minVal <= 0)) { e = '4th argument must be a positive number.'; }
+ else if (maxVal !== undefined && (typeof maxVal !== 'number' || maxVal < maxVal)) { e = '5th argument must be a positive number greater than minValue.'; }
+ if (e) { throw new TypeError(e); }
+
+ var val, el, els = Selector.select(selector);
+ if (minVal === undefined) { minVal = 1; }
+ op = (op === '*') ? function(a,b){return a*b;} : function(a,b){return a+b;};
+ for (var i = 0, f = els.length; i < f; ++i) {
+ el = els[i];
+ val = parseFloat( that.getStyle(el, 'fontSize'));
+ val = op(val, delta);
+ if (val < minVal) { continue; }
+ if (typeof maxVal === 'number' && val > maxVal) { continue; }
+ el.style.fontSize = val + 'px';
+ }
+ });
+ }
+
+ };
+
+ return Css;
+
+});
+
+/**
+ * @author inkdev AT sapo.pt
+ */
+
+Ink.createModule('Ink.Dom.Element', 1, [], function() {
+
+ 'use strict';
+
+ var createContextualFragmentSupport = (
+ typeof document.createRange === 'function' &&
+ typeof window.Range.prototype.createContextualFragment === 'function');
+
+ var deleteThisTbodyToken = 'Ink.Dom.Element tbody: ' + Math.random();
+ var browserCreatesTbodies = (function () {
+ var div = document.createElement('div');
+ div.innerHTML = '
';
+ return div.getElementsByTagName('tbody').length !== 0;
+ }());
+
+ function rect(elem){
+ var dimensions = {};
+ try {
+ dimensions = elem.getBoundingClientRect();
+ } catch(e){
+ dimensions = { top: elem.offsetTop, left: elem.offsetLeft };
+ }
+ return dimensions;
+ }
+
+ /**
+ * @module Ink.Dom.Element_1
+ */
+
+ /**
+ * @class Ink.Dom.Element
+ */
+
+ var InkElement = {
+
+ /**
+ * Shortcut for `document.getElementById`
+ *
+ * @method get
+ * @param {String|DOMElement} elm Either an ID of an element, or an element.
+ * @return {DOMElement|null} The DOM element with the given id or null when it was not found
+ */
+ get: function(elm) {
+ if(typeof elm !== 'undefined') {
+ if(typeof elm === 'string') {
+ return document.getElementById(elm);
+ }
+ return elm;
+ }
+ return null;
+ },
+
+ /**
+ * Creates a DOM element
+ *
+ * @method create
+ * @param {String} tag tag name
+ * @param {Object} properties object with properties to be set on the element. You can also call other functions in Ink.Dom.Element like this
+ *
+ * @example
+ *
+ * var myPanel = InkElement.create('div', {
+ * appendHTML: '',
+ * className: 'classes to add',
+ * insertAfter: anotherPane
+ * };
+ *
+ * myPanel.innerHTML; // -> '
+ * myPanel.parentNode; // -> anotherPane
+ */
+ create: function(tag, properties) {
+ var el = document.createElement(tag);
+ //Ink.extendObj(el, properties);
+ for(var property in properties) {
+ if(properties.hasOwnProperty(property)) {
+ if (property in InkElement) {
+ InkElement[property](el, properties[property]);
+ } else {
+ if(property === 'className' || property === 'class') {
+ el.className = properties.className || properties['class'];
+ } else {
+ el.setAttribute(property, properties[property]);
+ }
+ }
+ }
+ }
+ return el;
+ },
+
+ /**
+ * Removes a DOM Element from the DOM
+ *
+ * @method remove
+ * @param {DOMElement} elm The element to remove
+ */
+ remove: function(el) {
+ var parEl;
+ if (el && (parEl = el.parentNode)) {
+ parEl.removeChild(el);
+ }
+ },
+
+ /**
+ * Scrolls the window to an element
+ *
+ * @method scrollTo
+ * @param {DOMElement|String} elm Element where to scroll
+ */
+ scrollTo: function(elm) {
+ elm = InkElement.get(elm);
+ if(elm) {
+ if (elm.scrollIntoView) {
+ return elm.scrollIntoView();
+ }
+
+ var elmOffset = {},
+ elmTop = 0, elmLeft = 0;
+
+ do {
+ elmTop += elm.offsetTop || 0;
+ elmLeft += elm.offsetLeft || 0;
+
+ elm = elm.offsetParent;
+ } while(elm);
+
+ elmOffset = {x: elmLeft, y: elmTop};
+
+ window.scrollTo(elmOffset.x, elmOffset.y);
+ }
+ },
+
+ /**
+ * Gets the top cumulative offset for an element
+ *
+ * Requires Ink.Dom.Browser
+ *
+ * @method offsetTop
+ * @param {DOMElement|String} elm target element
+ * @return {Number} Offset from the target element to the top of the document
+ */
+ offsetTop: function(elm) {
+ return InkElement.offset(elm)[1];
+ },
+
+ /**
+ * Gets the left cumulative offset for an element
+ *
+ * Requires Ink.Dom.Browser
+ *
+ * @method offsetLeft
+ * @param {DOMElement|String} elm target element
+ * @return {Number} Offset from the target element to the left of the document
+ */
+ offsetLeft: function(elm) {
+ return InkElement.offset(elm)[0];
+ },
+
+ /**
+ * Gets the element offset relative to its closest positioned ancestor
+ *
+ * @method positionedOffset
+ * @param {DOMElement|String} elm target element
+ * @return {Array} Array with the element offsetleft and offsettop relative to the closest positioned ancestor
+ */
+ positionedOffset: function(element) {
+ var valueTop = 0, valueLeft = 0;
+ element = InkElement.get(element);
+ do {
+ valueTop += element.offsetTop || 0;
+ valueLeft += element.offsetLeft || 0;
+ element = element.offsetParent;
+ if (element) {
+ if (element.tagName.toLowerCase() === 'body') { break; }
+
+ var value = element.style.position;
+ if (!value && element.currentStyle) {
+ value = element.currentStyle.position;
+ }
+ if ((!value || value === 'auto') && typeof getComputedStyle !== 'undefined') {
+ var css = getComputedStyle(element, null);
+ value = css ? css.position : null;
+ }
+ if (value === 'relative' || value === 'absolute') { break; }
+ }
+ } while (element);
+ return [valueLeft, valueTop];
+ },
+
+ /**
+ * Gets the cumulative offset for an element
+ *
+ * Returns the top left position of the element on the page
+ *
+ * Requires Ink.Dom.Browser
+ *
+ * @method offset
+ * @param {DOMElement|String} elm Target element
+ * @return {[Number, Number]} Array with pixel distance from the target element to the top left corner of the document
+ */
+ offset: function(el) {
+ /*jshint boss:true */
+ el = Ink.i(el);
+ var res = [0, 0];
+ var doc = el.ownerDocument,
+ docElem = doc.documentElement,
+ box = rect(el),
+ body = doc.body,
+ clientTop = docElem.clientTop || body.clientTop || 0,
+ clientLeft = docElem.clientLeft || body.clientLeft || 0,
+ scrollTop = doc.pageYOffset || docElem.scrollTop || body.scrollTop,
+ scrollLeft = doc.pageXOffset || docElem.scrollLeft || body.scrollLeft,
+ top = box.top + scrollTop - clientTop,
+ left = box.left + scrollLeft - clientLeft;
+ res = [left, top];
+ return res;
+ },
+
+ /**
+ * Gets the scroll of the element
+ *
+ * @method scroll
+ * @param {DOMElement|String} [elm] target element or document.body
+ * @returns {Array} offset values for x and y scroll
+ */
+ scroll: function(elm) {
+ elm = elm ? Ink.i(elm) : document.body;
+ return [
+ ( ( !window.pageXOffset ) ? elm.scrollLeft : window.pageXOffset ),
+ ( ( !window.pageYOffset ) ? elm.scrollTop : window.pageYOffset )
+ ];
+ },
+
+ _getPropPx: function(cs, prop) {
+ var n, c;
+ var val = cs.getPropertyValue ? cs.getPropertyValue(prop) : cs[prop];
+ if (!val) { n = 0; }
+ else {
+ c = val.indexOf('px');
+ if (c === -1) { n = 0; }
+ else {
+ n = parseFloat(val, 10);
+ }
+ }
+
+ //console.log([prop, ' "', val, '" ', n].join(''));
+
+ return n;
+ },
+
+ /**
+ * Alias for offset()
+ *
+ * @method offset2
+ * @deprecated Kept for historic reasons. Use offset() instead.
+ */
+ offset2: function(el) {
+ return InkElement.offset(el);
+ },
+
+ /**
+ * Verifies the existence of an attribute
+ *
+ * @method hasAttribute
+ * @param {Object} elm target element
+ * @param {String} attr attribute name
+ * @return {Boolean} Boolean based on existance of attribute
+ */
+ hasAttribute: function(elm, attr){
+ return elm.hasAttribute ? elm.hasAttribute(attr) : !!elm.getAttribute(attr);
+ },
+ /**
+ * Inserts a element immediately after a target element
+ *
+ * @method insertAfter
+ * @param {DOMElement} newElm element to be inserted
+ * @param {DOMElement|String} targetElm key element
+ */
+ insertAfter: function(newElm, targetElm) {
+ /*jshint boss:true */
+ if (targetElm = InkElement.get(targetElm)) {
+ if (targetElm.nextSibling !== null) {
+ targetElm.parentNode.insertBefore(newElm, targetElm.nextSibling);
+ } else {
+ targetElm.parentNode.appendChild(newElm);
+ }
+ }
+ },
+
+ /**
+ * Inserts an element before a target element
+ *
+ * @method insertBefore
+ * @param {DOMElement} newElm element to be inserted
+ * @param {DOMElement|String} targetElm key element
+ */
+ insertBefore: function (newElm, targetElm) {
+ /*jshint boss:true */
+ if ( (targetElm = InkElement.get(targetElm)) ) {
+ targetElm.parentNode.insertBefore(newElm, targetElm);
+ }
+ },
+
+ /**
+ * Inserts a element at the top of the childNodes of a target element
+ *
+ * @method insertTop
+ * @param {DOMElement} newElm element to be inserted
+ * @param {DOMElement|String} targetElm key element
+ */
+ insertTop: function(newElm,targetElm) {
+ /*jshint boss:true */
+ if (targetElm = InkElement.get(targetElm)) {
+ if (targetElm.firstChild) {
+ targetElm.insertBefore(newElm, targetElm.firstChild);
+ } else {
+ targetElm.appendChild(newElm);
+ }
+ }
+ },
+
+ /**
+ * Inserts an element after all the child nodes of another element
+ *
+ * @method insertBottom
+ * @param {DOMElement} newElm element to be inserted
+ * @param {DOMElement|String} targetElm key element
+ */
+ insertBottom: function(newElm,targetElm) {
+ /*jshint boss:true */
+ targetElm.appendChild(newElm);
+ },
+
+ /**
+ * Retrieves textContent from node
+ *
+ * @method innerText
+ * @param {DOMNode} node from which to retreive text from. Can be any node type.
+ * @return {String} the text
+ */
+ textContent: function(node){
+ node = Ink.i(node);
+ var text, k, cs, m;
+
+ switch(node && node.nodeType) {
+ case 9: /*DOCUMENT_NODE*/
+ // IE quirks mode does not have documentElement
+ return InkElement.textContent(node.documentElement || node.body && node.body.parentNode || node.body);
+
+ case 1: /*ELEMENT_NODE*/
+ text = node.innerText;
+ if (typeof text !== 'undefined') {
+ return text;
+ }
+ /* falls through */
+ case 11: /*DOCUMENT_FRAGMENT_NODE*/
+ text = node.textContent;
+ if (typeof text !== 'undefined') {
+ return text;
+ }
+
+ if (node.firstChild === node.lastChild) {
+ // Common case: 0 or 1 children
+ return InkElement.textContent(node.firstChild);
+ }
+
+ text = [];
+ cs = node.childNodes;
+ for (k = 0, m = cs.length; k < m; ++k) {
+ text.push( InkElement.textContent( cs[k] ) );
+ }
+ return text.join('');
+
+ case 3: /*TEXT_NODE*/
+ case 4: /*CDATA_SECTION_NODE*/
+ return node.nodeValue;
+ }
+ return '';
+ },
+
+ /**
+ * Removes all nodes children and adds the text
+ *
+ * @method setTextContent
+ * @param {DOMNode} node node to add the text to. Can be any node type.
+ * @param {String} text text to be appended to the node.
+ */
+ setTextContent: function(node, text){
+ node = Ink.i(node);
+ switch(node && node.nodeType)
+ {
+ case 1: /*ELEMENT_NODE*/
+ if ('innerText' in node) {
+ node.innerText = text;
+ break;
+ }
+ /* falls through */
+ case 11: /*DOCUMENT_FRAGMENT_NODE*/
+ if ('textContent' in node) {
+ node.textContent = text;
+ break;
+ }
+ /* falls through */
+ case 9: /*DOCUMENT_NODE*/
+ while(node.firstChild) {
+ node.removeChild(node.firstChild);
+ }
+ if (text !== '') {
+ var doc = node.ownerDocument || node;
+ node.appendChild(doc.createTextNode(text));
+ }
+ break;
+
+ case 3: /*TEXT_NODE*/
+ case 4: /*CDATA_SECTION_NODE*/
+ node.nodeValue = text;
+ break;
+ }
+ },
+
+ /**
+ * Tells if element is a clickable link
+ *
+ * @method isLink
+ * @param {DOMNode} node node to check if it's link
+ * @return {Boolean}
+ */
+ isLink: function(element){
+ var b = element && element.nodeType === 1 && ((/^a|area$/i).test(element.tagName) ||
+ element.hasAttributeNS && element.hasAttributeNS('http://www.w3.org/1999/xlink','href'));
+ return !!b;
+ },
+
+ /**
+ * Tells if ancestor is ancestor of node
+ *
+ * @method isAncestorOf
+ * @param {DOMNode} ancestor ancestor node
+ * @param {DOMNode} node descendant node
+ * @return {Boolean}
+ */
+ isAncestorOf: function(ancestor, node){
+ /*jshint boss:true */
+ if (!node || !ancestor) {
+ return false;
+ }
+ if (node.compareDocumentPosition) {
+ return (ancestor.compareDocumentPosition(node) & 0x10) !== 0;/*Node.DOCUMENT_POSITION_CONTAINED_BY*/
+ }
+ while (node = node.parentNode){
+ if (node === ancestor){
+ return true;
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Tells if descendant is descendant of node
+ *
+ * @method descendantOf
+ * @param {DOMNode} node the ancestor
+ * @param {DOMNode} descendant the descendant
+ * @return {Boolean} true if 'descendant' is descendant of 'node'
+ */
+ descendantOf: function(node, descendant){
+ return node !== descendant && InkElement.isAncestorOf(node, descendant);
+ },
+
+ /**
+ * Get first child in document order of node type 1
+ * @method firstElementChild
+ * @param {DOMNode} elm parent node
+ * @return {DOMNode} the element child
+ */
+ firstElementChild: function(elm){
+ if(!elm) {
+ return null;
+ }
+ if ('firstElementChild' in elm) {
+ return elm.firstElementChild;
+ }
+ var child = elm.firstChild;
+ while(child && child.nodeType !== 1) {
+ child = child.nextSibling;
+ }
+ return child;
+ },
+
+ /**
+ * Get last child in document order of node type 1
+ * @method lastElementChild
+ * @param {DOMNode} elm parent node
+ * @return {DOMNode} the element child
+ */
+ lastElementChild: function(elm){
+ if(!elm) {
+ return null;
+ }
+ if ('lastElementChild' in elm) {
+ return elm.lastElementChild;
+ }
+ var child = elm.lastChild;
+ while(child && child.nodeType !== 1) {
+ child = child.previousSibling;
+ }
+ return child;
+ },
+
+ /**
+ * Get the first element sibling after the node
+ *
+ * @method nextElementSibling
+ * @param {DOMNode} node current node
+ * @return {DOMNode|Null} the first element sibling after node or null if none is found
+ */
+ nextElementSibling: function(node){
+ var sibling = null;
+
+ if(!node){ return sibling; }
+
+ if("nextElementSibling" in node){
+ return node.nextElementSibling;
+ } else {
+ sibling = node.nextSibling;
+
+ // 1 === Node.ELEMENT_NODE
+ while(sibling && sibling.nodeType !== 1){
+ sibling = sibling.nextSibling;
+ }
+
+ return sibling;
+ }
+ },
+
+ /**
+ * Get the first element sibling before the node
+ *
+ * @method previousElementSibling
+ * @param {DOMNode} node current node
+ * @return {DOMNode|Null} the first element sibling before node or null if none is found
+ */
+ previousElementSibling: function(node){
+ var sibling = null;
+
+ if(!node){ return sibling; }
+
+ if("previousElementSibling" in node){
+ return node.previousElementSibling;
+ } else {
+ sibling = node.previousSibling;
+
+ // 1 === Node.ELEMENT_NODE
+ while(sibling && sibling.nodeType !== 1){
+ sibling = sibling.previousSibling;
+ }
+
+ return sibling;
+ }
+ },
+
+ /**
+ * Returns the width of the given element, in pixels
+ *
+ * @method elementWidth
+ * @param {DOMElement|string} element target DOM element or target ID
+ * @return {Number} the element's width
+ */
+ elementWidth: function(element) {
+ if(typeof element === "string") {
+ element = document.getElementById(element);
+ }
+ return element.offsetWidth;
+ },
+
+ /**
+ * Returns the height of the given element, in pixels
+ *
+ * @method elementHeight
+ * @param {DOMElement|string} element target DOM element or target ID
+ * @return {Number} the element's height
+ */
+ elementHeight: function(element) {
+ if(typeof element === "string") {
+ element = document.getElementById(element);
+ }
+ return element.offsetHeight;
+ },
+
+ /**
+ * Returns the element's left position in pixels
+ *
+ * @method elementLeft
+ * @param {DOMElement|string} element target DOM element or target ID
+ * @return {Number} element's left position
+ */
+ elementLeft: function(element) {
+ if(typeof element === "string") {
+ element = document.getElementById(element);
+ }
+ return element.offsetLeft;
+ },
+
+ /**
+ * Returns the element's top position in pixels
+ *
+ * @method elementTop
+ * @param {DOMElement|string} element target DOM element or target ID
+ * @return {Number} element's top position
+ */
+ elementTop: function(element) {
+ if(typeof element === "string") {
+ element = document.getElementById(element);
+ }
+ return element.offsetTop;
+ },
+
+ /**
+ * Returns the dimensions of the given element, in pixels
+ *
+ * @method elementDimensions
+ * @param {element} element target element
+ * @return {Array} array with element's width and height
+ */
+ elementDimensions: function(element) {
+ element = Ink.i(element);
+ return [element.offsetWidth, element.offsetHeight];
+ },
+
+ /**
+ * Returns the outer (width + margin + padding included) dimensions of an element, in pixels.
+ *
+ * Requires Ink.Dom.Css
+ *
+ * @method outerDimensions
+ * @param {DOMElement} element Target element
+ * @return {Array} Array with element width and height.
+ */
+ outerDimensions: function (element) {
+ var bbox = rect(element);
+
+ var Css = Ink.getModule('Ink.Dom.Css_1');
+ var getStyle = Ink.bindMethod(Css, 'getStyle', element);
+
+ return [
+ bbox.right - bbox.left + parseFloat(getStyle('marginLeft') || 0) + parseFloat(getStyle('marginRight') || 0), // w
+ bbox.bottom - bbox.top + parseFloat(getStyle('marginTop') || 0) + parseFloat(getStyle('marginBottom') || 0) // h
+ ];
+ },
+
+ /**
+ * Check whether an element is inside the viewport
+ *
+ * @method inViewport
+ * @param {DOMElement} element Element to check
+ * @param {Boolean} [partial=false] Return `true` even if it is only partially visible.
+ * @return {Boolean}
+ */
+ inViewport: function (element, partial) {
+ var dims = rect(Ink.i(element));
+ if (partial) {
+ return dims.bottom > 0 && // from the top
+ dims.left < InkElement.viewportWidth() && // from the right
+ dims.top < InkElement.viewportHeight() && // from the bottom
+ dims.right > 0; // from the left
+ } else {
+ return dims.top > 0 && // from the top
+ dims.right < InkElement.viewportWidth() && // from the right
+ dims.bottom < InkElement.viewportHeight() && // from the bottom
+ dims.left > 0; // from the left
+ }
+ },
+
+ /**
+ * Check whether an element is hidden
+ * Taken from Mootools Element extras ( https://gist.github.com/cheeaun/73342 )
+ * Does not take into account visibility:hidden
+ * @method isHidden
+ * @param {DOMElement} element Element to check
+ * @return {Boolean}
+ */
+
+ isHidden: function (element) {
+ var w = element.offsetWidth,
+ h = element.offsetHeight,
+ force = (element.tagName.toLowerCase() === 'tr');
+
+ var Css = Ink.getModule('Ink.Dom.Css_1');
+
+ return (w===0 && h===0 && !force) ? true :
+ (w!==0 && h!==0 && !force) ? false :
+ Css.getStyle(element, 'display').toLowerCase() === 'none';
+ },
+
+ /**
+ * Check whether an element is visible
+ *
+ * @method isVisible
+ * @param {DOMElement} element Element to check
+ * @return {Boolean}
+ */
+
+ isVisible: function (element) {
+ return !this.isHidden(element);
+ },
+
+ /**
+ * Applies the cloneFrom's dimensions to cloneTo
+ *
+ * @method clonePosition
+ * @param {DOMElement} cloneTo element to be position cloned
+ * @param {DOMElement} cloneFrom element to get the cloned position
+ * @return {DOMElement} the element with positionClone
+ */
+ clonePosition: function(cloneTo, cloneFrom){
+ var pos = InkElement.offset(cloneFrom);
+ cloneTo.style.left = pos[0]+'px';
+ cloneTo.style.top = pos[1]+'px';
+
+ return cloneTo;
+ },
+
+ /**
+ * Slices off a piece of text at the end of the element and adds the ellipsis
+ * so all text fits in the element.
+ *
+ * @method ellipsizeText
+ * @param {DOMElement} element which text is to add the ellipsis
+ * @param {String} [ellipsis='\u2026'] String to append to the chopped text
+ */
+ ellipsizeText: function(element, ellipsis){
+ /*jshint boss:true */
+ if (element = Ink.i(element)){
+ while (element && element.scrollHeight > (element.offsetHeight + 8)) {
+ element.textContent = element.textContent.replace(/(\s+\S+)\s*$/, ellipsis || '\u2026');
+ }
+ }
+ },
+
+ /**
+ * Searches up the DOM tree for an element fulfilling the boolTest function (returning trueish)
+ *
+ * @method findUpwardsHaving
+ * @param {HtmlElement} element
+ * @param {Function} boolTest
+ * @return {HtmlElement|false} the matched element or false if did not match
+ */
+ findUpwardsHaving: function(element, boolTest) {
+ while (element && element.nodeType === 1) {
+ if (boolTest(element)) {
+ return element;
+ }
+ element = element.parentNode;
+ }
+ return false;
+ },
+
+ /**
+ * Śearches up the DOM tree for an element of specified class name
+ *
+ * @method findUpwardsByClass
+ * @param {HtmlElement} element
+ * @param {String} className
+ * @returns {HtmlElement|false} the matched element or false if did not match
+ */
+ findUpwardsByClass: function(element, className) {
+ var re = new RegExp("(^|\\s)" + className + "(\\s|$)");
+ var tst = function(el) {
+ var cls = el.className;
+ return cls && re.test(cls);
+ };
+ return InkElement.findUpwardsHaving(element, tst);
+ },
+
+ /**
+ * Śearches up the DOM tree for an element of specified tag
+ *
+ * @method findUpwardsByTag
+ * @param {HtmlElement} element
+ * @param {String} tag
+ * @returns {HtmlElement|false} the matched element or false if did not match
+ */
+ findUpwardsByTag: function(element, tag) {
+ tag = tag.toUpperCase();
+ var tst = function(el) {
+ return el.nodeName && el.nodeName.toUpperCase() === tag;
+ };
+ return InkElement.findUpwardsHaving(element, tst);
+ },
+
+ /**
+ * Śearches up the DOM tree for an element of specified id
+ *
+ * @method findUpwardsById
+ * @param {HtmlElement} element
+ * @param {String} id
+ * @returns {HtmlElement|false} the matched element or false if did not match
+ */
+ findUpwardsById: function(element, id) {
+ var tst = function(el) {
+ return el.id === id;
+ };
+ return InkElement.findUpwardsHaving(element, tst);
+ },
+
+ /**
+ * Śearches up the DOM tree for an element matching the given selector
+ *
+ * @method findUpwardsBySelector
+ * @param {HtmlElement} element
+ * @param {String} sel
+ * @returns {HtmlElement|false} the matched element or false if did not match
+ */
+ findUpwardsBySelector: function(element, sel) {
+ var Selector = Ink.getModule('Ink.Dom.Selector');
+ if (!Selector) {
+ throw new Error('This method requires Ink.Dom.Selector');
+ }
+ var tst = function(el) {
+ return Selector.matchesSelector(el, sel);
+ };
+ return InkElement.findUpwardsHaving(element, tst);
+ },
+
+ /**
+ * Returns trimmed text content of descendants
+ *
+ * @method getChildrenText
+ * @param {DOMElement} el element being seeked
+ * @param {Boolean} [removeIt] whether to remove the found text nodes or not
+ * @return {String} text found
+ */
+ getChildrenText: function(el, removeIt) {
+ var node,
+ j,
+ part,
+ nodes = el.childNodes,
+ jLen = nodes.length,
+ text = '';
+
+ if (!el) {
+ return text;
+ }
+
+ for (j = 0; j < jLen; ++j) {
+ node = nodes[j];
+ if (!node) { continue; }
+ if (node.nodeType === 3) { // TEXT NODE
+ part = InkElement._trimString( String(node.data) );
+ if (part.length > 0) {
+ text += part;
+ if (removeIt) { el.removeChild(node); }
+ }
+ else { el.removeChild(node); }
+ }
+ }
+
+ return text;
+ },
+
+ /**
+ * String trim implementation
+ * Used by getChildrenText
+ *
+ * function _trimString
+ * param {String} text
+ * return {String} trimmed text
+ */
+ _trimString: function(text) {
+ return (String.prototype.trim) ? text.trim() : text.replace(/^\s*/, '').replace(/\s*$/, '');
+ },
+
+ /**
+ * Returns the values of a select element
+ *
+ * @method getSelectValues
+ * @param {DomElement|String} select element
+ * @return {Array} selected values
+ */
+ getSelectValues: function (select) {
+ var selectEl = Ink.i(select);
+ var values = [];
+ for (var i = 0; i < selectEl.options.length; ++i) {
+ values.push( selectEl.options[i].value );
+ }
+ return values;
+ },
+
+
+ /* used by fills */
+ _normalizeData: function(data) {
+ var d, data2 = [];
+ for (var i = 0, f = data.length; i < f; ++i) {
+ d = data[i];
+
+ if (!(d instanceof Array)) { // if not array, wraps primitive twice: val -> [val, val]
+ d = [d, d];
+ }
+ else if (d.length === 1) { // if 1 element array: [val] -> [val, val]
+ d.push(d[0]);
+ }
+ data2.push(d);
+ }
+ return data2;
+ },
+
+
+ /**
+ * Fills select element with choices
+ *
+ * @method fillSelect
+ * @param {DomElement|String} container select element which will get filled
+ * @param {Array} data data which will populate the component
+ * @param {Boolean} [skipEmpty] true to skip empty option
+ * @param {String|Number} [defaultValue] primitive value to select at beginning
+ */
+ fillSelect: function(container, data, skipEmpty, defaultValue) {
+ var containerEl = Ink.i(container);
+ if (!containerEl) { return; }
+
+ containerEl.innerHTML = '';
+ var d, optionEl;
+
+ if (!skipEmpty) {
+ // add initial empty option
+ optionEl = document.createElement('option');
+ optionEl.setAttribute('value', '');
+ containerEl.appendChild(optionEl);
+ }
+
+ data = InkElement._normalizeData(data);
+
+ for (var i = 0, f = data.length; i < f; ++i) {
+ d = data[i];
+
+ optionEl = document.createElement('option');
+ optionEl.setAttribute('value', d[0]);
+ if (d.length > 2) {
+ optionEl.setAttribute('extra', d[2]);
+ }
+ optionEl.appendChild( document.createTextNode(d[1]) );
+
+ if (d[0] === defaultValue) {
+ optionEl.setAttribute('selected', 'selected');
+ }
+
+ containerEl.appendChild(optionEl);
+ }
+ },
+
+
+ /**
+ * Select element on steroids - allows the creation of new values
+ *
+ * @method fillSelect2
+ * @param {DomElement|String} ctn select element which will get filled
+ * @param {Object} opts
+ * @param {Array} [opts.data] data which will populate the component
+ * @param {Boolean} [opts.skipEmpty] if true empty option is not created (defaults to false)
+ * @param {String} [opts.emptyLabel] label to display on empty option
+ * @param {String} [opts.createLabel] label to display on create option
+ * @param {String} [opts.optionsGroupLabel] text to display on group surrounding value options
+ * @param {String} [opts.defaultValue] option to select initially
+ * @param {Function(selEl, addOptFn)} [opts.onCreate] callback that gets called once user selects the create option
+ */
+ fillSelect2: function(ctn, opts) {
+ ctn = Ink.i(ctn);
+ ctn.innerHTML = '';
+
+ var defs = {
+ skipEmpty: false,
+ skipCreate: false,
+ emptyLabel: 'none',
+ createLabel: 'create',
+ optionsGroupLabel: 'groups',
+ emptyOptionsGroupLabel: 'none exist',
+ defaultValue: ''
+ };
+ if (!opts) { throw 'param opts is a requirement!'; }
+ if (!opts.data) { throw 'opts.data is a requirement!'; }
+ opts = Ink.extendObj(defs, opts);
+
+ var optionEl, d;
+
+ var optGroupValuesEl = document.createElement('optgroup');
+ optGroupValuesEl.setAttribute('label', opts.optionsGroupLabel);
+
+ opts.data = InkElement._normalizeData(opts.data);
+
+ if (!opts.skipCreate) {
+ opts.data.unshift(['$create$', opts.createLabel]);
+ }
+
+ if (!opts.skipEmpty) {
+ opts.data.unshift(['', opts.emptyLabel]);
+ }
+
+ for (var i = 0, f = opts.data.length; i < f; ++i) {
+ d = opts.data[i];
+
+ optionEl = document.createElement('option');
+ optionEl.setAttribute('value', d[0]);
+ optionEl.appendChild( document.createTextNode(d[1]) );
+
+ if (d[0] === opts.defaultValue) { optionEl.setAttribute('selected', 'selected'); }
+
+ if (d[0] === '' || d[0] === '$create$') {
+ ctn.appendChild(optionEl);
+ }
+ else {
+ optGroupValuesEl.appendChild(optionEl);
+ }
+ }
+
+ var lastValIsNotOption = function(data) {
+ var lastVal = data[data.length-1][0];
+ return (lastVal === '' || lastVal === '$create$');
+ };
+
+ if (lastValIsNotOption(opts.data)) {
+ optionEl = document.createElement('option');
+ optionEl.setAttribute('value', '$dummy$');
+ optionEl.setAttribute('disabled', 'disabled');
+ optionEl.appendChild( document.createTextNode(opts.emptyOptionsGroupLabel) );
+ optGroupValuesEl.appendChild(optionEl);
+ }
+
+ ctn.appendChild(optGroupValuesEl);
+
+ var addOption = function(v, l) {
+ var optionEl = ctn.options[ctn.options.length - 1];
+ if (optionEl.getAttribute('disabled')) {
+ optionEl.parentNode.removeChild(optionEl);
+ }
+
+ // create it
+ optionEl = document.createElement('option');
+ optionEl.setAttribute('value', v);
+ optionEl.appendChild( document.createTextNode(l) );
+ optGroupValuesEl.appendChild(optionEl);
+
+ // select it
+ ctn.options[ctn.options.length - 1].setAttribute('selected', true);
+ };
+
+ if (!opts.skipCreate) {
+ ctn.onchange = function() {
+ if ((ctn.value === '$create$') && (typeof opts.onCreate === 'function')) { opts.onCreate(ctn, addOption); }
+ };
+ }
+ },
+
+
+ /**
+ * Creates set of radio buttons, returns wrapper
+ *
+ * @method fillRadios
+ * @param {DomElement|String} insertAfterEl element which will precede the input elements
+ * @param {String} name name to give to the form field ([] is added if not as suffix already)
+ * @param {Array} data data which will populate the component
+ * @param {Boolean} [skipEmpty] true to skip empty option
+ * @param {String|Number} [defaultValue] primitive value to select at beginning
+ * @param {String} [splitEl] name of element to add after each input element (example: 'br')
+ * @return {DOMElement} wrapper element around radio buttons
+ */
+ fillRadios: function(insertAfterEl, name, data, skipEmpty, defaultValue, splitEl) {
+ var afterEl = Ink.i(insertAfterEl);
+ afterEl = afterEl.nextSibling;
+ while (afterEl && afterEl.nodeType !== 1) {
+ afterEl = afterEl.nextSibling;
+ }
+ var containerEl = document.createElement('span');
+ if (afterEl) {
+ afterEl.parentNode.insertBefore(containerEl, afterEl);
+ } else {
+ Ink.i(insertAfterEl).appendChild(containerEl);
+ }
+
+ data = InkElement._normalizeData(data);
+
+ if (name.substring(name.length - 1) !== ']') {
+ name += '[]';
+ }
+
+ var d, inputEl;
+
+ if (!skipEmpty) {
+ // add initial empty option
+ inputEl = document.createElement('input');
+ inputEl.setAttribute('type', 'radio');
+ inputEl.setAttribute('name', name);
+ inputEl.setAttribute('value', '');
+ containerEl.appendChild(inputEl);
+ if (splitEl) { containerEl.appendChild( document.createElement(splitEl) ); }
+ }
+
+ for (var i = 0; i < data.length; ++i) {
+ d = data[i];
+
+ inputEl = document.createElement('input');
+ inputEl.setAttribute('type', 'radio');
+ inputEl.setAttribute('name', name);
+ inputEl.setAttribute('value', d[0]);
+ containerEl.appendChild(inputEl);
+ containerEl.appendChild( document.createTextNode(d[1]) );
+ if (splitEl) { containerEl.appendChild( document.createElement(splitEl) ); }
+
+ if (d[0] === defaultValue) {
+ inputEl.checked = true;
+ }
+ }
+
+ return containerEl;
+ },
+
+
+ /**
+ * Creates set of checkbox buttons, returns wrapper
+ *
+ * @method fillChecks
+ * @param {DomElement|String} insertAfterEl element which will precede the input elements
+ * @param {String} name name to give to the form field ([] is added if not as suffix already)
+ * @param {Array} data data which will populate the component
+ * @param {Boolean} [skipEmpty] true to skip empty option
+ * @param {String|Number} [defaultValue] primitive value to select at beginning
+ * @param {String} [splitEl] name of element to add after each input element (example: 'br')
+ * @return {DOMElement} wrapper element around checkboxes
+ */
+ fillChecks: function(insertAfterEl, name, data, defaultValue, splitEl) {
+ var afterEl = Ink.i(insertAfterEl);
+ afterEl = afterEl.nextSibling;
+ while (afterEl && afterEl.nodeType !== 1) {
+ afterEl = afterEl.nextSibling;
+ }
+ var containerEl = document.createElement('span');
+ if (afterEl) {
+ afterEl.parentNode.insertBefore(containerEl, afterEl);
+ } else {
+ Ink.i(insertAfterEl).appendChild(containerEl);
+ }
+
+ data = InkElement._normalizeData(data);
+
+ if (name.substring(name.length - 1) !== ']') {
+ name += '[]';
+ }
+
+ var d, inputEl;
+
+ for (var i = 0; i < data.length; ++i) {
+ d = data[i];
+
+ inputEl = document.createElement('input');
+ inputEl.setAttribute('type', 'checkbox');
+ inputEl.setAttribute('name', name);
+ inputEl.setAttribute('value', d[0]);
+ containerEl.appendChild(inputEl);
+ containerEl.appendChild( document.createTextNode(d[1]) );
+ if (splitEl) { containerEl.appendChild( document.createElement(splitEl) ); }
+
+ if (d[0] === defaultValue) {
+ inputEl.checked = true;
+ }
+ }
+
+ return containerEl;
+ },
+
+
+ /**
+ * Returns index of element from parent, -1 if not child of parent...
+ *
+ * @method parentIndexOf
+ * @param {DOMElement} parentEl Element to parse
+ * @param {DOMElement} childEl Child Element to look for
+ * @return {Number}
+ */
+ parentIndexOf: function(parentEl, childEl) {
+ var node, idx = 0;
+ for (var i = 0, f = parentEl.childNodes.length; i < f; ++i) {
+ node = parentEl.childNodes[i];
+ if (node.nodeType === 1) { // ELEMENT
+ if (node === childEl) { return idx; }
+ ++idx;
+ }
+ }
+ return -1;
+ },
+
+
+ /**
+ * Returns an array of elements - the next siblings
+ *
+ * @method nextSiblings
+ * @param {String|DomElement} elm element
+ * @return {Array} Array of next sibling elements
+ */
+ nextSiblings: function(elm) {
+ if(typeof(elm) === "string") {
+ elm = document.getElementById(elm);
+ }
+ if(typeof(elm) === 'object' && elm !== null && elm.nodeType && elm.nodeType === 1) {
+ var elements = [],
+ siblings = elm.parentNode.children,
+ index = InkElement.parentIndexOf(elm.parentNode, elm);
+
+ for(var i = ++index, len = siblings.length; i
';
+ return div.firstChild.firstChild.firstChild;
+ }
+ },
+
+ /**
+ * Gets a wrapper DIV with a certain HTML content for being inserted in `elm`.
+ * Necessary for appendHTML,prependHTML functions, because they need a container element to copy the children from.
+ *
+ * Works around IE table quirks
+ * @method _getWrapper
+ * @private
+ * @param elm
+ * @param html
+ */
+ _getWrapper: function (elm, html) {
+ var nodeName = elm.nodeName && elm.nodeName.toUpperCase();
+ var wrapper = document.createElement('div');
+ var wrapFunc = InkElement._wrapElements[nodeName];
+
+ if ( !wrapFunc ) {
+ wrapper.innerHTML = html;
+ return wrapper;
+ }
+ // special cases
+ wrapper = wrapFunc(wrapper, html);
+ // worst case: tbody auto-creation even when our HTML has a tbody.
+ if (browserCreatesTbodies && nodeName === 'TABLE') {
+ // terrible case. Deal with tbody creation too.
+ var tds = wrapper.getElementsByTagName('td');
+ for (var i = 0, len = tds.length; i < len; i++) {
+ if (tds[i].innerHTML === deleteThisTbodyToken) {
+ var tbody = tds[i].parentNode.parentNode;
+ tbody.parentNode.removeChild(tbody);
+ }
+ }
+ }
+ return wrapper;
+ },
+
+ /**
+ * parses and appends an html string to a container, not destroying its contents
+ *
+ * @method appendHTML
+ * @param {String|DomElement} elm element
+ * @param {String} html markup string
+ */
+ appendHTML: function(elm, html){
+ var wrapper = InkElement._getWrapper(elm, html);
+ while (wrapper.firstChild) {
+ elm.appendChild(wrapper.firstChild);
+ }
+ },
+
+ /**
+ * parses and prepends an html string to a container, not destroying its contents
+ *
+ * @method prependHTML
+ * @param {String|DomElement} elm element
+ * @param {String} html markup string
+ */
+ prependHTML: function(elm, html){
+ var wrapper = InkElement._getWrapper(elm, html);
+ while (wrapper.lastChild) {
+ elm.insertBefore(wrapper.lastChild, elm.firstChild);
+ }
+ },
+
+ /**
+ * sets the Inner HTML of an element to the given HTML string
+ *
+ * @method setHTML
+ * @param {String|DomElement} elm element
+ * @param {String} html markup string
+ */
+ setHTML: function (elm, html) {
+ try {
+ elm.innerHTML = html;
+ } catch (e) {
+ // Tables in IE7
+ while (elm.firstChild) {
+ elm.removeChild(elm.firstChild);
+ }
+ InkElement.appendHTML(elm, html);
+ }
+ },
+
+ /**
+ * Wraps an element inside a container.
+ *
+ * The container may or may not be in the document yet.
+ *
+ * @method wrap
+ * @param {String|DomElement} target Element to be wrapped
+ * @param {String|DomElement} container Element to wrap the target
+ * @return Container element
+ *
+ * @example
+ * before:
+ *
+ *
+ *
+ * call this function to wrap #target with a wrapper div.
+ *
+ * InkElement.wrap('target', InkElement.create('div', {id: 'container'});
+ *
+ * after:
+ *
+ *
+ */
+ wrap: function (target, container) {
+ target = Ink.i(target);
+ container = Ink.i(container);
+
+ var nextNode = target.nextSibling;
+ var parent = target.parentNode;
+
+ container.appendChild(target);
+
+ if (nextNode !== null) {
+ parent.insertBefore(container, nextNode);
+ } else {
+ parent.appendChild(container);
+ }
+
+ return container;
+ },
+
+ /**
+ * Pulls an element out of its wrapper.
+ *
+ * @method unwrap
+ * @param elem The element you're trying to unwrap. This should be a child of the wrapper.
+ * @param {String} [wrapperSelector] Use this if you want to find your wrapper (if `elem` is inside several layers of wrappers).
+ *
+ * @example
+ *
+ * When you have this:
+ *
+ *
+ *
+ *
+ *
+ * If you do this:
+ *
+ * InkElement.unwrap('unwrapMe');
+ *
+ * You get this:
+ *
+ *
+ *
+ *
+ **/
+ unwrap: function (elem, wrapperSelector) {
+ elem = Ink.i(elem);
+ var wrapper;
+ if (wrapperSelector) {
+ wrapper = InkElement.findUpwardsBySelector(elem, wrapperSelector);
+ } else {
+ wrapper = elem.parentNode;
+ }
+ if (!wrapper || !wrapper.parentNode) { return; }
+
+ InkElement.insertBefore(elem, wrapper);
+ },
+
+ /**
+ * Replaces an element with another.
+ *
+ * @method replace
+ * @param element The element to be replaced.
+ * @param replacement The new element.
+ *
+ * @example
+ * var newelement1 = InkElement.create('div');
+ * // ...
+ * replace(Ink.i('element1'), newelement1);
+ */
+ replace: function (element, replacement) {
+ InkElement.insertBefore(replacement, element);
+ InkElement.remove(element);
+ },
+
+ /**
+ * Removes direct children on type text.
+ * Useful to remove nasty layout gaps generated by whitespace on the markup.
+ *
+ * @method removeTextNodeChildren
+ * @param {DOMElement} el
+ */
+ removeTextNodeChildren: function(el) {
+ var prevEl, toRemove, parent = el;
+ el = el.firstChild;
+ while (el) {
+ toRemove = (el.nodeType === 3);
+ prevEl = el;
+ el = el.nextSibling;
+ if (toRemove) {
+ parent.removeChild(prevEl);
+ }
+ }
+ },
+
+ /**
+ * Pass an HTML string and receive a documentFragment with the corresponding elements
+ * @method htmlToFragment
+ * @param {String} html html string
+ * @return {DocumentFragment} DocumentFragment containing all of the elements from the html string
+ */
+ htmlToFragment: (createContextualFragmentSupport ?
+ function(html){
+ var range;
+
+ if(typeof html !== 'string'){ return document.createDocumentFragment(); }
+
+ range = document.createRange();
+
+ // set the context to document.body (firefox does this already, webkit doesn't)
+ range.selectNode(document.body);
+
+ return range.createContextualFragment(html);
+ } : function (html) {
+ var fragment = document.createDocumentFragment(),
+ tempElement,
+ current;
+
+ if(typeof html !== 'string'){ return fragment; }
+
+ tempElement = document.createElement('div');
+ tempElement.innerHTML = html;
+
+ // append child removes elements from the original parent
+ while( (current = tempElement.firstChild) ){ // intentional assignment
+ fragment.appendChild(current);
+ }
+
+ return fragment;
+ }),
+
+ _camelCase: function(str)
+ {
+ return str ? str.replace(/-(\w)/g, function (_, $1){
+ return $1.toUpperCase();
+ }) : str;
+ },
+
+ /**
+ * Gets all of the data attributes from an element
+ *
+ * @method data
+ * @param {String|DomElement} selector Element or CSS selector
+ * @return {Object} Object with the data-* properties. If no data-attributes are present, an empty object is returned.
+ */
+ data: function(selector) {
+ var el;
+ if (typeof selector !== 'object' && typeof selector !== 'string') {
+ throw '[Ink.Dom.Element.data] :: Invalid selector defined';
+ }
+
+ if (typeof selector === 'object') {
+ el = selector;
+ }
+ else {
+ var InkDomSelector = Ink.getModule('Ink.Dom.Selector', 1);
+ if (!InkDomSelector) {
+ throw "[Ink.Dom.Element.data] :: this method requires Ink.Dom.Selector - v1";
+ }
+ el = InkDomSelector.select(selector);
+ if (el.length <= 0) {
+ throw "[Ink.Dom.Element.data] :: Can't find any element with the specified selector";
+ }
+ el = el[0];
+ }
+
+ var dataset = {};
+ var attrs = el.attributes || [];
+
+ var curAttr, curAttrName, curAttrValue;
+ if (attrs) {
+ for (var i = 0, total = attrs.length; i < total; ++i) {
+ curAttr = attrs[i];
+ curAttrName = curAttr.name;
+ curAttrValue = curAttr.value;
+ if (curAttrName && curAttrName.indexOf('data-') === 0) {
+ dataset[InkElement._camelCase(curAttrName.replace('data-', ''))] = curAttrValue;
+ }
+ }
+ }
+
+ return dataset;
+ },
+
+ /**
+ * @method moveCursorTo
+ * @param {Input|Textarea} el
+ * @param {Number} t
+ */
+ moveCursorTo: function(el, t) {
+ if (el.setSelectionRange) {
+ el.setSelectionRange(t, t);
+ //el.focus();
+ }
+ else {
+ var range = el.createTextRange();
+ range.collapse(true);
+ range.moveEnd( 'character', t);
+ range.moveStart('character', t);
+ range.select();
+ }
+ },
+
+ /**
+ * @method pageWidth
+ * @return {Number} page width
+ */
+ pageWidth: function() {
+ var xScroll;
+
+ if (window.innerWidth && window.scrollMaxX) {
+ xScroll = window.innerWidth + window.scrollMaxX;
+ } else if (document.body.scrollWidth > document.body.offsetWidth){
+ xScroll = document.body.scrollWidth;
+ } else {
+ xScroll = document.body.offsetWidth;
+ }
+
+ var windowWidth;
+
+ if (window.self.innerWidth) {
+ if(document.documentElement.clientWidth){
+ windowWidth = document.documentElement.clientWidth;
+ } else {
+ windowWidth = window.self.innerWidth;
+ }
+ } else if (document.documentElement && document.documentElement.clientWidth) {
+ windowWidth = document.documentElement.clientWidth;
+ } else if (document.body) {
+ windowWidth = document.body.clientWidth;
+ }
+
+ if(xScroll < windowWidth){
+ return xScroll;
+ } else {
+ return windowWidth;
+ }
+ },
+
+ /**
+ * @method pageHeight
+ * @return {Number} page height
+ */
+ pageHeight: function() {
+ var yScroll;
+
+ if (window.innerHeight && window.scrollMaxY) {
+ yScroll = window.innerHeight + window.scrollMaxY;
+ } else if (document.body.scrollHeight > document.body.offsetHeight){
+ yScroll = document.body.scrollHeight;
+ } else {
+ yScroll = document.body.offsetHeight;
+ }
+
+ var windowHeight;
+
+ if (window.self.innerHeight) {
+ windowHeight = window.self.innerHeight;
+ } else if (document.documentElement && document.documentElement.clientHeight) {
+ windowHeight = document.documentElement.clientHeight;
+ } else if (document.body) {
+ windowHeight = document.body.clientHeight;
+ }
+
+ if(yScroll < windowHeight){
+ return windowHeight;
+ } else {
+ return yScroll;
+ }
+ },
+
+ /**
+ * @method viewportWidth
+ * @return {Number} viewport width in pixels
+ */
+ viewportWidth: function() {
+ if(typeof window.innerWidth !== "undefined") {
+ return window.innerWidth;
+ }
+ if (document.documentElement && typeof document.documentElement.offsetWidth !== "undefined") {
+ return document.documentElement.offsetWidth;
+ }
+ },
+
+ /**
+ * @method viewportHeight
+ * @return {Number} viewport height
+ */
+ viewportHeight: function() {
+ if (typeof window.innerHeight !== "undefined") {
+ return window.innerHeight;
+ }
+ if (document.documentElement && typeof document.documentElement.offsetHeight !== "undefined") {
+ return document.documentElement.offsetHeight;
+ }
+ },
+
+ /**
+ * @method scrollWidth
+ * @return {Number} scroll width
+ */
+ scrollWidth: function() {
+ if (typeof window.self.pageXOffset !== 'undefined') {
+ return window.self.pageXOffset;
+ }
+ if (typeof document.documentElement !== 'undefined' && typeof document.documentElement.scrollLeft !== 'undefined') {
+ return document.documentElement.scrollLeft;
+ }
+ return document.body.scrollLeft;
+ },
+
+ /**
+ * @method scrollHeight
+ * @return {Number} scroll height
+ */
+ scrollHeight: function() {
+ if (typeof window.self.pageYOffset !== 'undefined') {
+ return window.self.pageYOffset;
+ }
+ if (typeof document.documentElement !== 'undefined' && typeof document.documentElement.scrollTop !== 'undefined') {
+ return document.documentElement.scrollTop;
+ }
+ return document.body.scrollTop;
+ }
+ };
+
+ return InkElement;
+
+});
+
+/**
+ * @author inkdev AT sapo.pt
+ */
+
+Ink.createModule('Ink.Dom.Event', 1, [], function() {
+ /* jshint
+ asi:true,
+ strict:false,
+ laxcomma:true,
+ eqeqeq:false,
+ laxbreak:true,
+ boss:true,
+ curly:false,
+ expr:true
+ */
+
+ /*!
+ * Bean - copyright (c) Jacob Thornton 2011-2012
+ * https://github.com/fat/bean
+ * MIT license
+ */
+ var bean = (function (name, context, definition) {
+ return definition()
+ })('bean', this, function (name, context) {
+ name = name || 'bean'
+ context = context || this
+
+ var win = window
+ , old = context[name]
+ , namespaceRegex = /[^\.]*(?=\..*)\.|.*/
+ , nameRegex = /\..*/
+ , addEvent = 'addEventListener'
+ , removeEvent = 'removeEventListener'
+ , doc = document || {}
+ , root = doc.documentElement || {}
+ , W3C_MODEL = root[addEvent]
+ , eventSupport = W3C_MODEL ? addEvent : 'attachEvent'
+ , ONE = {} // singleton for quick matching making add() do one()
+
+ , slice = Array.prototype.slice
+ , str2arr = function (s, d) { return s.split(d || ' ') }
+ , isString = function (o) { return typeof o == 'string' }
+ , isFunction = function (o) { return typeof o == 'function' }
+
+ // events that we consider to be 'native', anything not in this list will
+ // be treated as a custom event
+ , standardNativeEvents =
+ 'click dblclick mouseup mousedown contextmenu ' + // mouse buttons
+ 'mousewheel mousemultiwheel DOMMouseScroll ' + // mouse wheel
+ 'mouseover mouseout mousemove selectstart selectend ' + // mouse movement
+ 'keydown keypress keyup ' + // keyboard
+ 'orientationchange ' + // mobile
+ 'focus blur change reset select submit ' + // form elements
+ 'load unload beforeunload resize move DOMContentLoaded ' + // window
+ 'readystatechange message ' + // window
+ 'error abort scroll ' // misc
+ // element.fireEvent('onXYZ'... is not forgiving if we try to fire an event
+ // that doesn't actually exist, so make sure we only do these on newer browsers
+ , w3cNativeEvents =
+ 'show ' + // mouse buttons
+ 'input invalid ' + // form elements
+ 'touchstart touchmove touchend touchcancel ' + // touch
+ 'gesturestart gesturechange gestureend ' + // gesture
+ 'textinput' + // TextEvent
+ 'readystatechange pageshow pagehide popstate ' + // window
+ 'hashchange offline online ' + // window
+ 'afterprint beforeprint ' + // printing
+ 'dragstart dragenter dragover dragleave drag drop dragend ' + // dnd
+ 'loadstart progress suspend emptied stalled loadmetadata ' + // media
+ 'loadeddata canplay canplaythrough playing waiting seeking ' + // media
+ 'seeked ended durationchange timeupdate play pause ratechange ' + // media
+ 'volumechange cuechange ' + // media
+ 'checking noupdate downloading cached updateready obsolete ' // appcache
+
+ // convert to a hash for quick lookups
+ , nativeEvents = (function (hash, events, i) {
+ for (i = 0; i < events.length; i++) events[i] && (hash[events[i]] = 1)
+ return hash
+ }({}, str2arr(standardNativeEvents + (W3C_MODEL ? w3cNativeEvents : ''))))
+
+ // custom events are events that we *fake*, they are not provided natively but
+ // we can use native events to generate them
+ , customEvents = (function () {
+ var isAncestor = 'compareDocumentPosition' in root
+ ? function (element, container) {
+ return container.compareDocumentPosition && (container.compareDocumentPosition(element) & 16) === 16
+ }
+ : 'contains' in root
+ ? function (element, container) {
+ container = container.nodeType === 9 || container === window ? root : container
+ return container !== element && container.contains(element)
+ }
+ : function (element, container) {
+ while (element = element.parentNode) if (element === container) return 1
+ return 0
+ }
+ , check = function (event) {
+ var related = event.relatedTarget
+ return !related
+ ? related == null
+ : (related !== this && related.prefix !== 'xul' && !/document/.test(this.toString())
+ && !isAncestor(related, this))
+ }
+
+ return {
+ mouseenter: { base: 'mouseover', condition: check }
+ , mouseleave: { base: 'mouseout', condition: check }
+ , mousewheel: { base: /Firefox/.test(navigator.userAgent) ? 'DOMMouseScroll' : 'mousewheel' }
+ }
+ }())
+
+ // we provide a consistent Event object across browsers by taking the actual DOM
+ // event object and generating a new one from its properties.
+ , Event = (function () {
+ // a whitelist of properties (for different event types) tells us what to check for and copy
+ var commonProps = str2arr('altKey attrChange attrName bubbles cancelable ctrlKey currentTarget ' +
+ 'detail eventPhase getModifierState isTrusted metaKey relatedNode relatedTarget shiftKey ' +
+ 'srcElement target timeStamp type view which propertyName')
+ , mouseProps = commonProps.concat(str2arr('button buttons clientX clientY dataTransfer ' +
+ 'fromElement offsetX offsetY pageX pageY screenX screenY toElement'))
+ , mouseWheelProps = mouseProps.concat(str2arr('wheelDelta wheelDeltaX wheelDeltaY wheelDeltaZ ' +
+ 'axis')) // 'axis' is FF specific
+ , keyProps = commonProps.concat(str2arr('char charCode key keyCode keyIdentifier ' +
+ 'keyLocation location'))
+ , textProps = commonProps.concat(str2arr('data'))
+ , touchProps = commonProps.concat(str2arr('touches targetTouches changedTouches scale rotation'))
+ , messageProps = commonProps.concat(str2arr('data origin source'))
+ , stateProps = commonProps.concat(str2arr('state'))
+ , overOutRegex = /over|out/
+ // some event types need special handling and some need special properties, do that all here
+ , typeFixers = [
+ { // key events
+ reg: /key/i
+ , fix: function (event, newEvent) {
+ newEvent.keyCode = event.keyCode || event.which
+ return keyProps
+ }
+ }
+ , { // mouse events
+ reg: /click|mouse(?!(.*wheel|scroll))|menu|drag|drop/i
+ , fix: function (event, newEvent, type) {
+ newEvent.rightClick = event.which === 3 || event.button === 2
+ newEvent.pos = { x: 0, y: 0 }
+ if (event.pageX || event.pageY) {
+ newEvent.clientX = event.pageX
+ newEvent.clientY = event.pageY
+ } else if (event.clientX || event.clientY) {
+ newEvent.clientX = event.clientX + doc.body.scrollLeft + root.scrollLeft
+ newEvent.clientY = event.clientY + doc.body.scrollTop + root.scrollTop
+ }
+ if (overOutRegex.test(type)) {
+ newEvent.relatedTarget = event.relatedTarget
+ || event[(type == 'mouseover' ? 'from' : 'to') + 'Element']
+ }
+ return mouseProps
+ }
+ }
+ , { // mouse wheel events
+ reg: /mouse.*(wheel|scroll)/i
+ , fix: function () { return mouseWheelProps }
+ }
+ , { // TextEvent
+ reg: /^text/i
+ , fix: function () { return textProps }
+ }
+ , { // touch and gesture events
+ reg: /^touch|^gesture/i
+ , fix: function () { return touchProps }
+ }
+ , { // message events
+ reg: /^message$/i
+ , fix: function () { return messageProps }
+ }
+ , { // popstate events
+ reg: /^popstate$/i
+ , fix: function () { return stateProps }
+ }
+ , { // everything else
+ reg: /.*/
+ , fix: function () { return commonProps }
+ }
+ ]
+ , typeFixerMap = {} // used to map event types to fixer functions (above), a basic cache mechanism
+
+ , Event = function (event, element, isNative) {
+ if (!arguments.length) return
+ event = event || ((element.ownerDocument || element.document || element).parentWindow || win).event
+ this.originalEvent = event
+ this.isNative = isNative
+ this.isBean = true
+
+ if (!event) return
+
+ var type = event.type
+ , target = event.target || event.srcElement
+ , i, l, p, props, fixer
+
+ this.target = target && target.nodeType === 3 ? target.parentNode : target
+
+ if (isNative) { // we only need basic augmentation on custom events, the rest expensive & pointless
+ fixer = typeFixerMap[type]
+ if (!fixer) { // haven't encountered this event type before, map a fixer function for it
+ for (i = 0, l = typeFixers.length; i < l; i++) {
+ if (typeFixers[i].reg.test(type)) { // guaranteed to match at least one, last is .*
+ typeFixerMap[type] = fixer = typeFixers[i].fix
+ break
+ }
+ }
+ }
+
+ props = fixer(event, this, type)
+ for (i = props.length; i--;) {
+ if (!((p = props[i]) in this) && p in event) this[p] = event[p]
+ }
+ }
+ }
+
+ // preventDefault() and stopPropagation() are a consistent interface to those functions
+ // on the DOM, stop() is an alias for both of them together
+ Event.prototype.preventDefault = function () {
+ if (this.originalEvent.preventDefault) this.originalEvent.preventDefault()
+ else this.originalEvent.returnValue = false
+ }
+ Event.prototype.stopPropagation = function () {
+ if (this.originalEvent.stopPropagation) this.originalEvent.stopPropagation()
+ else this.originalEvent.cancelBubble = true
+ }
+ Event.prototype.stop = function () {
+ this.preventDefault()
+ this.stopPropagation()
+ this.stopped = true
+ }
+ // stopImmediatePropagation() has to be handled internally because we manage the event list for
+ // each element
+ // note that originalElement may be a Bean#Event object in some situations
+ Event.prototype.stopImmediatePropagation = function () {
+ if (this.originalEvent.stopImmediatePropagation) this.originalEvent.stopImmediatePropagation()
+ this.isImmediatePropagationStopped = function () { return true }
+ }
+ Event.prototype.isImmediatePropagationStopped = function () {
+ return this.originalEvent.isImmediatePropagationStopped && this.originalEvent.isImmediatePropagationStopped()
+ }
+ Event.prototype.clone = function (currentTarget) {
+ //TODO: this is ripe for optimisation, new events are *expensive*
+ // improving this will speed up delegated events
+ var ne = new Event(this, this.element, this.isNative)
+ ne.currentTarget = currentTarget
+ return ne
+ }
+
+ return Event
+ }())
+
+ // if we're in old IE we can't do onpropertychange on doc or win so we use doc.documentElement for both
+ , targetElement = function (element, isNative) {
+ return !W3C_MODEL && !isNative && (element === doc || element === win) ? root : element
+ }
+
+ /**
+ * Bean maintains an internal registry for event listeners. We don't touch elements, objects
+ * or functions to identify them, instead we store everything in the registry.
+ * Each event listener has a RegEntry object, we have one 'registry' for the whole instance.
+ */
+ , RegEntry = (function () {
+ // each handler is wrapped so we can handle delegation and custom events
+ var wrappedHandler = function (element, fn, condition, args) {
+ var call = function (event, eargs) {
+ return fn.apply(element, args ? slice.call(eargs, event ? 0 : 1).concat(args) : eargs)
+ }
+ , findTarget = function (event, eventElement) {
+ return fn.__beanDel ? fn.__beanDel.ft(event.target, element) : eventElement
+ }
+ , handler = condition
+ ? function (event) {
+ var target = findTarget(event, this) // deleated event
+ if (condition.apply(target, arguments)) {
+ if (event) event.currentTarget = target
+ return call(event, arguments)
+ }
+ }
+ : function (event) {
+ if (fn.__beanDel) event = event.clone(findTarget(event)) // delegated event, fix the fix
+ return call(event, arguments)
+ }
+ handler.__beanDel = fn.__beanDel
+ return handler
+ }
+
+ , RegEntry = function (element, type, handler, original, namespaces, args, root) {
+ var customType = customEvents[type]
+ , isNative
+
+ if (type == 'unload') {
+ // self clean-up
+ handler = once(removeListener, element, type, handler, original)
+ }
+
+ if (customType) {
+ if (customType.condition) {
+ handler = wrappedHandler(element, handler, customType.condition, args)
+ }
+ type = customType.base || type
+ }
+
+ this.isNative = isNative = nativeEvents[type] && !!element[eventSupport]
+ this.customType = !W3C_MODEL && !isNative && type
+ this.element = element
+ this.type = type
+ this.original = original
+ this.namespaces = namespaces
+ this.eventType = W3C_MODEL || isNative ? type : 'propertychange'
+ this.target = targetElement(element, isNative)
+ this[eventSupport] = !!this.target[eventSupport]
+ this.root = root
+ this.handler = wrappedHandler(element, handler, null, args)
+ }
+
+ // given a list of namespaces, is our entry in any of them?
+ RegEntry.prototype.inNamespaces = function (checkNamespaces) {
+ var i, j, c = 0
+ if (!checkNamespaces) return true
+ if (!this.namespaces) return false
+ for (i = checkNamespaces.length; i--;) {
+ for (j = this.namespaces.length; j--;) {
+ if (checkNamespaces[i] == this.namespaces[j]) c++
+ }
+ }
+ return checkNamespaces.length === c
+ }
+
+ // match by element, original fn (opt), handler fn (opt)
+ RegEntry.prototype.matches = function (checkElement, checkOriginal, checkHandler) {
+ return this.element === checkElement &&
+ (!checkOriginal || this.original === checkOriginal) &&
+ (!checkHandler || this.handler === checkHandler)
+ }
+
+ return RegEntry
+ }())
+
+ , registry = (function () {
+ // our map stores arrays by event type, just because it's better than storing
+ // everything in a single array.
+ // uses '$' as a prefix for the keys for safety and 'r' as a special prefix for
+ // rootListeners so we can look them up fast
+ var map = {}
+
+ // generic functional search of our registry for matching listeners,
+ // `fn` returns false to break out of the loop
+ , forAll = function (element, type, original, handler, root, fn) {
+ var pfx = root ? 'r' : '$'
+ if (!type || type == '*') {
+ // search the whole registry
+ for (var t in map) {
+ if (t.charAt(0) == pfx) {
+ forAll(element, t.substr(1), original, handler, root, fn)
+ }
+ }
+ } else {
+ var i = 0, l, list = map[pfx + type], all = element == '*'
+ if (!list) return
+ for (l = list.length; i < l; i++) {
+ if ((all || list[i].matches(element, original, handler)) && !fn(list[i], list, i, type)) return
+ }
+ }
+ }
+
+ , has = function (element, type, original, root) {
+ // we're not using forAll here simply because it's a bit slower and this
+ // needs to be fast
+ var i, list = map[(root ? 'r' : '$') + type]
+ if (list) {
+ for (i = list.length; i--;) {
+ if (!list[i].root && list[i].matches(element, original, null)) return true
+ }
+ }
+ return false
+ }
+
+ , get = function (element, type, original, root) {
+ var entries = []
+ forAll(element, type, original, null, root, function (entry) {
+ return entries.push(entry)
+ })
+ return entries
+ }
+
+ , put = function (entry) {
+ var has = !entry.root && !this.has(entry.element, entry.type, null, false)
+ , key = (entry.root ? 'r' : '$') + entry.type
+ ;(map[key] || (map[key] = [])).push(entry)
+ return has
+ }
+
+ , del = function (entry) {
+ forAll(entry.element, entry.type, null, entry.handler, entry.root, function (entry, list, i) {
+ list.splice(i, 1)
+ entry.removed = true
+ if (list.length === 0) delete map[(entry.root ? 'r' : '$') + entry.type]
+ return false
+ })
+ }
+
+ // dump all entries, used for onunload
+ , entries = function () {
+ var t, entries = []
+ for (t in map) {
+ if (t.charAt(0) == '$') entries = entries.concat(map[t])
+ }
+ return entries
+ }
+
+ return { has: has, get: get, put: put, del: del, entries: entries }
+ }())
+
+ // we need a selector engine for delegated events, use querySelectorAll if it exists
+ // but for older browsers we need Qwery, Sizzle or similar
+ , selectorEngine
+ , setSelectorEngine = function (e) {
+ if (!arguments.length) {
+ selectorEngine = doc.querySelectorAll
+ ? function (s, r) {
+ return r.querySelectorAll(s)
+ }
+ : function () {
+ throw new Error('Bean: No selector engine installed') // eeek
+ }
+ } else {
+ selectorEngine = e
+ }
+ }
+
+ // we attach this listener to each DOM event that we need to listen to, only once
+ // per event type per DOM element
+ , rootListener = function (event, type) {
+ if (!W3C_MODEL && type && event && event.propertyName != '_on' + type) return
+
+ var listeners = registry.get(this, type || event.type, null, false)
+ , l = listeners.length
+ , i = 0
+
+ event = new Event(event, this, true)
+ if (type) event.type = type
+
+ // iterate through all handlers registered for this type, calling them unless they have
+ // been removed by a previous handler or stopImmediatePropagation() has been called
+ for (; i < l && !event.isImmediatePropagationStopped(); i++) {
+ if (!listeners[i].removed) listeners[i].handler.call(this, event)
+ }
+ }
+
+ // add and remove listeners to DOM elements
+ , listener = W3C_MODEL
+ ? function (element, type, add) {
+ // new browsers
+ element[add ? addEvent : removeEvent](type, rootListener, false)
+ }
+ : function (element, type, add, custom) {
+ // IE8 and below, use attachEvent/detachEvent and we have to piggy-back propertychange events
+ // to simulate event bubbling etc.
+ var entry
+ if (add) {
+ registry.put(entry = new RegEntry(
+ element
+ , custom || type
+ , function (event) { // handler
+ rootListener.call(element, event, custom)
+ }
+ , rootListener
+ , null
+ , null
+ , true // is root
+ ))
+ if (custom && element['_on' + custom] == null) element['_on' + custom] = 0
+ entry.target.attachEvent('on' + entry.eventType, entry.handler)
+ } else {
+ entry = registry.get(element, custom || type, rootListener, true)[0]
+ if (entry) {
+ entry.target.detachEvent('on' + entry.eventType, entry.handler)
+ registry.del(entry)
+ }
+ }
+ }
+
+ , once = function (rm, element, type, fn, originalFn) {
+ // wrap the handler in a handler that does a remove as well
+ return function () {
+ fn.apply(this, arguments)
+ rm(element, type, originalFn)
+ }
+ }
+
+ , removeListener = function (element, orgType, handler, namespaces) {
+ var type = orgType && orgType.replace(nameRegex, '')
+ , handlers = registry.get(element, type, null, false)
+ , removed = {}
+ , i, l
+
+ for (i = 0, l = handlers.length; i < l; i++) {
+ if ((!handler || handlers[i].original === handler) && handlers[i].inNamespaces(namespaces)) {
+ // TODO: this is problematic, we have a registry.get() and registry.del() that
+ // both do registry searches so we waste cycles doing this. Needs to be rolled into
+ // a single registry.forAll(fn) that removes while finding, but the catch is that
+ // we'll be splicing the arrays that we're iterating over. Needs extra tests to
+ // make sure we don't screw it up. @rvagg
+ registry.del(handlers[i])
+ if (!removed[handlers[i].eventType] && handlers[i][eventSupport])
+ removed[handlers[i].eventType] = { t: handlers[i].eventType, c: handlers[i].type }
+ }
+ }
+ // check each type/element for removed listeners and remove the rootListener where it's no longer needed
+ for (i in removed) {
+ if (!registry.has(element, removed[i].t, null, false)) {
+ // last listener of this type, remove the rootListener
+ listener(element, removed[i].t, false, removed[i].c)
+ }
+ }
+ }
+
+ // set up a delegate helper using the given selector, wrap the handler function
+ , delegate = function (selector, fn) {
+ //TODO: findTarget (therefore $) is called twice, once for match and once for
+ // setting e.currentTarget, fix this so it's only needed once
+ var findTarget = function (target, root) {
+ var i, array = isString(selector) ? selectorEngine(selector, root) : selector
+ for (; target && target !== root; target = target.parentNode) {
+ for (i = array.length; i--;) {
+ if (array[i] === target) return target
+ }
+ }
+ }
+ , handler = function (e) {
+ var match = findTarget(e.target, this)
+ if (match) fn.apply(match, arguments)
+ }
+
+ // __beanDel isn't pleasant but it's a private function, not exposed outside of Bean
+ handler.__beanDel = {
+ ft : findTarget // attach it here for customEvents to use too
+ , selector : selector
+ }
+ return handler
+ }
+
+ , fireListener = W3C_MODEL ? function (isNative, type, element) {
+ // modern browsers, do a proper dispatchEvent()
+ var evt = doc.createEvent(isNative ? 'HTMLEvents' : 'UIEvents')
+ evt[isNative ? 'initEvent' : 'initUIEvent'](type, true, true, win, 1)
+ element.dispatchEvent(evt)
+ } : function (isNative, type, element) {
+ // old browser use onpropertychange, just increment a custom property to trigger the event
+ element = targetElement(element, isNative)
+ isNative ? element.fireEvent('on' + type, doc.createEventObject()) : element['_on' + type]++
+ }
+
+ /**
+ * Public API: off(), on(), add(), (remove()), one(), fire(), clone()
+ */
+
+ /**
+ * off(element[, eventType(s)[, handler ]])
+ */
+ , off = function (element, typeSpec, fn) {
+ var isTypeStr = isString(typeSpec)
+ , k, type, namespaces, i
+
+ if (isTypeStr && typeSpec.indexOf(' ') > 0) {
+ // off(el, 't1 t2 t3', fn) or off(el, 't1 t2 t3')
+ typeSpec = str2arr(typeSpec)
+ for (i = typeSpec.length; i--;)
+ off(element, typeSpec[i], fn)
+ return element
+ }
+
+ type = isTypeStr && typeSpec.replace(nameRegex, '')
+ if (type && customEvents[type]) type = customEvents[type].base
+
+ if (!typeSpec || isTypeStr) {
+ // off(el) or off(el, t1.ns) or off(el, .ns) or off(el, .ns1.ns2.ns3)
+ if (namespaces = isTypeStr && typeSpec.replace(namespaceRegex, '')) namespaces = str2arr(namespaces, '.')
+ removeListener(element, type, fn, namespaces)
+ } else if (isFunction(typeSpec)) {
+ // off(el, fn)
+ removeListener(element, null, typeSpec)
+ } else {
+ // off(el, { t1: fn1, t2, fn2 })
+ for (k in typeSpec) {
+ if (typeSpec.hasOwnProperty(k)) off(element, k, typeSpec[k])
+ }
+ }
+
+ return element
+ }
+
+ /**
+ * on(element, eventType(s)[, selector], handler[, args ])
+ */
+ , on = function(element, events, selector, fn) {
+ var originalFn, type, types, i, args, entry, first
+
+ //TODO: the undefined check means you can't pass an 'args' argument, fix this perhaps?
+ if (selector === undefined && typeof events == 'object') {
+ //TODO: this can't handle delegated events
+ for (type in events) {
+ if (events.hasOwnProperty(type)) {
+ on.call(this, element, type, events[type])
+ }
+ }
+ return
+ }
+
+ if (!isFunction(selector)) {
+ // delegated event
+ originalFn = fn
+ args = slice.call(arguments, 4)
+ fn = delegate(selector, originalFn, selectorEngine)
+ } else {
+ args = slice.call(arguments, 3)
+ fn = originalFn = selector
+ }
+
+ types = str2arr(events)
+
+ // special case for one(), wrap in a self-removing handler
+ if (this === ONE) {
+ fn = once(off, element, events, fn, originalFn)
+ }
+
+ for (i = types.length; i--;) {
+ // add new handler to the registry and check if it's the first for this element/type
+ first = registry.put(entry = new RegEntry(
+ element
+ , types[i].replace(nameRegex, '') // event type
+ , fn
+ , originalFn
+ , str2arr(types[i].replace(namespaceRegex, ''), '.') // namespaces
+ , args
+ , false // not root
+ ))
+ if (entry[eventSupport] && first) {
+ // first event of this type on this element, add root listener
+ listener(element, entry.eventType, true, entry.customType)
+ }
+ }
+
+ return element
+ }
+
+ /**
+ * add(element[, selector], eventType(s), handler[, args ])
+ *
+ * Deprecated: kept (for now) for backward-compatibility
+ */
+ , add = function (element, events, fn, delfn) {
+ return on.apply(
+ null
+ , !isString(fn)
+ ? slice.call(arguments)
+ : [ element, fn, events, delfn ].concat(arguments.length > 3 ? slice.call(arguments, 5) : [])
+ )
+ }
+
+ /**
+ * one(element, eventType(s)[, selector], handler[, args ])
+ */
+ , one = function () {
+ return on.apply(ONE, arguments)
+ }
+
+ /**
+ * fire(element, eventType(s)[, args ])
+ *
+ * The optional 'args' argument must be an array, if no 'args' argument is provided
+ * then we can use the browser's DOM event system, otherwise we trigger handlers manually
+ */
+ , fire = function (element, type, args) {
+ var types = str2arr(type)
+ , i, j, l, names, handlers
+
+ for (i = types.length; i--;) {
+ type = types[i].replace(nameRegex, '')
+ if (names = types[i].replace(namespaceRegex, '')) names = str2arr(names, '.')
+ if (!names && !args && element[eventSupport]) {
+ fireListener(nativeEvents[type], type, element)
+ } else {
+ // non-native event, either because of a namespace, arguments or a non DOM element
+ // iterate over all listeners and manually 'fire'
+ handlers = registry.get(element, type, null, false)
+ args = [false].concat(args)
+ for (j = 0, l = handlers.length; j < l; j++) {
+ if (handlers[j].inNamespaces(names)) {
+ handlers[j].handler.apply(element, args)
+ }
+ }
+ }
+ }
+ return element
+ }
+
+ /**
+ * clone(dstElement, srcElement[, eventType ])
+ *
+ * TODO: perhaps for consistency we should allow the same flexibility in type specifiers?
+ */
+ , clone = function (element, from, type) {
+ var handlers = registry.get(from, type, null, false)
+ , l = handlers.length
+ , i = 0
+ , args, beanDel
+
+ for (; i < l; i++) {
+ if (handlers[i].original) {
+ args = [ element, handlers[i].type ]
+ if (beanDel = handlers[i].handler.__beanDel) args.push(beanDel.selector)
+ args.push(handlers[i].original)
+ on.apply(null, args)
+ }
+ }
+ return element
+ }
+
+ , bean = {
+ 'on' : on
+ , 'add' : add
+ , 'one' : one
+ , 'off' : off
+ , 'remove' : off
+ , 'clone' : clone
+ , 'fire' : fire
+ , 'Event' : Event
+ , 'setSelectorEngine' : setSelectorEngine
+ , 'noConflict' : function () {
+ context[name] = old
+ return this
+ }
+ }
+
+ // for IE, clean up on unload to avoid leaks
+ if (win.attachEvent) {
+ var cleanup = function () {
+ var i, entries = registry.entries()
+ for (i in entries) {
+ if (entries[i].type && entries[i].type !== 'unload') off(entries[i].element, entries[i].type)
+ }
+ win.detachEvent('onunload', cleanup)
+ win.CollectGarbage && win.CollectGarbage()
+ }
+ win.attachEvent('onunload', cleanup)
+ }
+
+ // initialize selector engine to internal default (qSA or throw Error)
+ setSelectorEngine(Ink.ss)
+
+ return bean
+ });
+
+ /**
+ * Keep this declaration here and off Bean as it extends the Event
+ * object and some properties are readonly in strict mode
+ */
+ 'use strict';
+
+ /**
+ * Instantiate browser native events array
+ */
+
+ /**
+ * @module Ink.Dom.Event_1
+ */
+
+ /**
+ * @class Ink.Dom.Event
+ */
+
+ var InkEvent = {
+
+ KEY_BACKSPACE: 8,
+ KEY_TAB: 9,
+ KEY_RETURN: 13,
+ KEY_ESC: 27,
+ KEY_LEFT: 37,
+ KEY_UP: 38,
+ KEY_RIGHT: 39,
+ KEY_DOWN: 40,
+ KEY_DELETE: 46,
+ KEY_HOME: 36,
+ KEY_END: 35,
+ KEY_PAGEUP: 33,
+ KEY_PAGEDOWN: 34,
+ KEY_INSERT: 45,
+
+ /**
+ * Returns a function which calls `func`, waiting at least `wait`
+ * milliseconds between calls. This is useful for events such as `scroll`
+ * or `resize`, which can be triggered too many times per second, slowing
+ * down the browser with needless function calls.
+ *
+ * *note:* This does not delay the first function call to the function.
+ *
+ * @method throttle
+ * @param {Function} func Function to call. Arguments and context are both passed.
+ * @param {Number} [wait=0] Milliseconds to wait between calls.
+ *
+ * @example
+ *
+ * Suppose you are observing the `scroll` event, but your application is lagging because `scroll` is triggered too many times.
+ *
+ * // BEFORE
+ * InkEvent.observe(window, 'scroll', function () {
+ * ...
+ * }); // When scrolling on mobile devices or on firefox's smooth scroll
+ * // this is expensive because onscroll is called many times
+ *
+ * // AFTER
+ * InkEvent.observe(window, 'scroll', InkEvent.throttle(function () {
+ * ...
+ * }, 100)); // The event handler is called only every 100ms. Problem solved.
+ *
+ * @example
+ * var handler = InkEvent.throttle(function () {
+ * ...
+ * }, 100);
+ *
+ * InkEvent.observe(window, 'scroll', handler);
+ * InkEvent.observe(window, 'resize', handler);
+ *
+ * // on resize, both the "scroll" and the "resize" events are triggered
+ * // a LOT of times. This prevents both of them being called a lot of
+ * // times when the window is being resized by a user.
+ *
+ **/
+ throttle: function (func, wait) {
+ wait = wait || 0;
+ var lastCall = 0; // Warning: This breaks on Jan 1st 1970 0:00
+ var timeout;
+ var throttled = function () {
+ var now = +new Date();
+ var timeDiff = now - lastCall;
+ if (timeDiff >= wait) {
+ lastCall = now;
+ return func.apply(this, [].slice.call(arguments));
+ } else {
+ var that = this;
+ var args = [].slice.call(arguments);
+ if (!timeout) {
+ timeout = setTimeout(function () {
+ timeout = null;
+ return throttled.apply(that, args);
+ }, wait - timeDiff);
+ }
+ }
+ };
+ return throttled;
+ },
+
+ /**
+ * Returns the target of the event object
+ *
+ * @method element
+ * @param {Object} ev event object
+ * @return {Node} The target
+ */
+ element: function(ev) {
+ var node = ev.delegationTarget ||
+ ev.target ||
+ // IE stuff
+ (ev.type === 'mouseout' && ev.fromElement) ||
+ (ev.type === 'mouseleave' && ev.fromElement) ||
+ (ev.type === 'mouseover' && ev.toElement) ||
+ (ev.type === 'mouseenter' && ev.toElement) ||
+ ev.srcElement ||
+ null;
+ return node && (node.nodeType === 3 || node.nodeType === 4) ? node.parentNode : node;
+ },
+
+ /**
+ * Returns the related target of the event object
+ *
+ * @method relatedTarget
+ * @param {Object} ev event object
+ * @return {Node} The related target
+ */
+ relatedTarget: function(ev){
+ var node = ev.relatedTarget ||
+ // IE stuff
+ (ev.type === 'mouseout' && ev.toElement) ||
+ (ev.type === 'mouseleave' && ev.toElement) ||
+ (ev.type === 'mouseover' && ev.fromElement) ||
+ (ev.type === 'mouseenter' && ev.fromElement) ||
+ null;
+ return node && (node.nodeType === 3 || node.nodeType === 4) ? node.parentNode : node;
+ },
+
+ /**
+ * Navigate up the DOM tree, looking for a tag with the name `elmTagName`.
+ *
+ * If such tag is not found, `document` is returned.
+ *
+ * @method findElement
+ * @param {Object} ev event object
+ * @param {String} elmTagName tag name to find
+ * @param {Boolean} [force=false] If this is true, never return `document`, and returns `false` instead.
+ * @return {DOMElement} the first element which matches given tag name or the document element if the wanted tag is not found
+ */
+ findElement: function(ev, elmTagName, force)
+ {
+ var node = this.element(ev);
+ while(true) {
+ if(node.nodeName.toLowerCase() === elmTagName.toLowerCase()) {
+ return node;
+ } else {
+ node = node.parentNode;
+ if(!node) {
+ if(force) {
+ return false;
+ }
+ return document;
+ }
+ if(!node.parentNode){
+ if(force){ return false; }
+ return document;
+ }
+ }
+ }
+ },
+
+ /**
+ * Attaches an event to element
+ *
+ * @method observe
+ * @param {DOMElement|String} element Element id or element
+ * @param {String} eventName Event name
+ * @param {Function} callBack Receives event object as a
+ * parameter. If you're manually firing custom events, check the
+ * eventName property of the event object to make sure you're handling
+ * the right event.
+ * @param {Boolean} [useCapture] Set to true to change event listening from bubbling to capture.
+ * @return {Function} The event handler used. Hang on to this if you want to `stopObserving` later.
+ */
+ observe: function(element, eventName, callBack, useCapture) {
+ element = Ink.i(element);
+ if(element) {
+ if(element.addEventListener) {
+ element.addEventListener(eventName, callBack, !!useCapture);
+ } else {
+ element.attachEvent('on' + eventName, (callBack = Ink.bind(callBack, element)));
+ }
+ return callBack;
+ }
+ },
+
+ /**
+ * Like observe, but listen to the event only once.
+ *
+ * @method observeOnce
+ * @param {DOMElement|String} element Element id or element
+ * @param {String} eventName Event name
+ * @param {Function} callBack Receives event object as a
+ * parameter. If you're manually firing custom events, check the
+ * eventName property of the event object to make sure you're handling
+ * the right event.
+ * @param {Boolean} [useCapture] Set to true to change event listening from bubbling to capture.
+ * @return {Function} The event handler used. Hang on to this if you want to `stopObserving` later.
+ */
+ observeOnce: function (element, eventName, callBack, useCapture) {
+ var onceBack = function () {
+ InkEvent.stopObserving(element, eventName, onceBack);
+ return callBack();
+ };
+ return InkEvent.observe(element, eventName, onceBack, useCapture);
+ },
+
+ /**
+ * Attaches an event to a selector or array of elements.
+ *
+ * Requires Ink.Dom.Selector
+ *
+ * @method observeMulti
+ * @param {Array|String} elements
+ * @param ... See the `observe` function.
+ * @return {Function} The used callback.
+ */
+ observeMulti: function (elements, eventName, callBack, useCapture) {
+ if (typeof elements === 'string') {
+ elements = Ink.ss(elements);
+ } else if ( /* is an element */ elements && elements.nodeType === 1) {
+ elements = [elements];
+ }
+ if (!elements[0]) { return false; }
+
+ for (var i = 0, len = elements.length; i < len; i++) {
+ this.observe(elements[i], eventName, callBack, useCapture);
+ }
+ return callBack;
+ },
+
+ /**
+ * Observe an event on the given element and every children which matches the selector string (if provided).
+ *
+ * Requires Ink.Dom.Selector if you need to use a selector.
+ *
+ * @method observeDelegated
+ * @param {DOMElement|String} element Element to observe.
+ * @param {String} eventName Event name to observe.
+ * @param {String} selector Child element selector. When null, finds any element.
+ * @param {Function} callback Callback to be called when the event is fired
+ * @return {Function} The used callback, for ceasing to listen to the event later.
+ **/
+ observeDelegated: function (element, eventName, selector, callback) {
+ return InkEvent.observe(element, eventName, function (event) {
+ var fromElement = InkEvent.element(event);
+ if (!fromElement || fromElement === element) { return; }
+
+ var cursor = fromElement;
+
+ // Go up the document tree until we hit the element itself.
+ while (cursor !== element && cursor !== document && cursor) {
+ if (Ink.Dom.Selector_1.matchesSelector(cursor, selector)) {
+ event.delegationTarget = cursor;
+ return callback(event);
+ }
+ cursor = cursor.parentNode;
+ }
+ });
+ },
+
+ /**
+ * Remove an event attached to an element
+ *
+ * @method stopObserving
+ * @param {DOMElement|String} element element id or element
+ * @param {String} eventName event name
+ * @param {Function} callBack callback function
+ * @param {Boolean} [useCapture] set to true if the event was being observed with useCapture set to true as well.
+ */
+ stopObserving: function(element, eventName, callBack, useCapture) {
+ element = Ink.i(element);
+
+ if(element) {
+ if(element.removeEventListener) {
+ element.removeEventListener(eventName, callBack, !!useCapture);
+ } else {
+ element.detachEvent('on' + eventName, callBack);
+ }
+ }
+ },
+
+ /**
+ * Stops event propagation and bubbling
+ *
+ * @method stop
+ * @param {Object} event event handle
+ */
+ stop: function(event)
+ {
+ if(event.cancelBubble !== null) {
+ event.cancelBubble = true;
+ }
+ if(event.stopPropagation) {
+ event.stopPropagation();
+ }
+ if(event.preventDefault) {
+ event.preventDefault();
+ }
+ if(window.attachEvent) {
+ event.returnValue = false;
+ }
+ if(event.cancel !== null) {
+ event.cancel = true;
+ }
+ },
+
+ /**
+ * Stops event propagation
+ *
+ * @method stopPropagation
+ * @param {Object} event event handle
+ */
+ stopPropagation: function(event) {
+ if(event.cancelBubble !== null) {
+ event.cancelBubble = true;
+ }
+ if(event.stopPropagation) {
+ event.stopPropagation();
+ }
+ },
+
+ /**
+ * Stops event default behaviour
+ *
+ * @method stopDefault
+ * @param {Object} event event handle
+ */
+ stopDefault: function(event)
+ {
+ if(event.preventDefault) {
+ event.preventDefault();
+ }
+ if(window.attachEvent) {
+ event.returnValue = false;
+ }
+ if(event.cancel !== null) {
+ event.cancel = true;
+ }
+ },
+
+ /**
+ * @method pointer
+ * @param {Object} ev event object
+ * @return {Object} an object with the mouse X and Y position
+ */
+ pointer: function(ev)
+ {
+ return {
+ x: this.pointerX(ev),
+ y: this.pointerY(ev)
+ };
+ },
+
+ /**
+ * @method pointerX
+ * @param {Object} ev event object
+ * @return {Number} mouse X position
+ */
+ pointerX: function(ev)
+ {
+ return (ev.touches && ev.touches[0] && ev.touches[0].pageX) ||
+ (ev.pageX) ||
+ (ev.clientX + (document.documentElement.scrollLeft || document.body.scrollLeft));
+ },
+
+ /**
+ * @method pointerY
+ * @param {Object} ev event object
+ * @return {Number} mouse Y position
+ */
+ pointerY: function(ev)
+ {
+ return (ev.touches && ev.touches[0] && ev.touches[0].pageY) ||
+ (ev.pageY) ||
+ (ev.clientY + (document.documentElement.scrollTop || document.body.scrollTop));
+ },
+
+ /**
+ * @method isLeftClick
+ * @param {Object} ev event object
+ * @return {Boolean} True if the event is a left mouse click
+ */
+ isLeftClick: function(ev) {
+ if (window.addEventListener) {
+ if(ev.button === 0){
+ return true;
+ } else if(ev.type === 'touchend' && ev.button === null){
+ // [todo] do the above check for pointerEvents too
+ return true;
+ }
+ }
+ else {
+ if(ev.button === 1){ return true; }
+ }
+ return false;
+ },
+
+ /**
+ * @method isRightClick
+ * @param {Object} ev event object
+ * @return {Boolean} True if there is a right click on the event
+ */
+ isRightClick: function(ev) {
+ return (ev.button === 2);
+ },
+
+ /**
+ * @method isMiddleClick
+ * @param {Object} ev event object
+ * @return {Boolean} True if there is a middle click on the event
+ */
+ isMiddleClick: function(ev) {
+ if (window.addEventListener) {
+ return (ev.button === 1);
+ }
+ else {
+ return (ev.button === 4);
+ }
+ return false;
+ },
+
+ /**
+ * Work in Progress.
+ * Used in SAPO.Component.MaskedInput
+ *
+ * @method getCharFromKeyboardEvent
+ * @param {KeyboardEvent} event keyboard event
+ * @param {optional Boolean} [changeCasing] if true uppercases, if false lowercases, otherwise keeps casing
+ * @return {String} character representation of pressed key combination
+ */
+ getCharFromKeyboardEvent: function(event, changeCasing) {
+ var k = event.keyCode;
+ var c = String.fromCharCode(k);
+
+ var shiftOn = event.shiftKey;
+ if (k >= 65 && k <= 90) { // A-Z
+ if (typeof changeCasing === 'boolean') {
+ shiftOn = changeCasing;
+ }
+ return (shiftOn) ? c : c.toLowerCase();
+ }
+ else if (k >= 96 && k <= 105) { // numpad digits
+ return String.fromCharCode( 48 + (k-96) );
+ }
+ switch (k) {
+ case 109: case 189: return '-';
+ case 107: case 187: return '+';
+ }
+ return c;
+ },
+
+ debug: function(){}
+};
+
+/**
+ * bean.on() lets you attach event listeners to both elements and objects.
+ * http://github.com/fat/bean#on
+ *
+ * @method on
+ * @param {DOMElement|Object} element An HTML DOM element or any JavaScript Object
+ * @param {String} eventType An Event (or multiple events, space separated) to listen to
+ * @param {String} [selector] A CSS DOM Element selector string to bind the listener to child elements matching the selector
+ * @param {Function} [handler] The callback function
+ * @param {Object} [args...] Additional arguments to pass to the callback function when triggered
+ *
+ * @return {DOMElement|Object} Returns the original DOM Element or Javascript Object
+ */
+
+/**
+ * bean.one() is an alias for bean.on() except that the handler will only be executed once and then removed for the event type(s).
+ * http://github.com/fat/bean#one
+ *
+ * @method one
+ * @param {DOMElement|Object} element An HTML DOM element or any JavaScript Object
+ * @param {String} eventType An Event (or multiple events, space separated) to listen to
+ * @param {String} [selector] A CSS DOM Element selector string to bind the listener to child elements matching the selector
+ * @param {Function} [handler] The callback function
+ * @param [args...] Additional arguments to pass to the callback function when triggered
+ *
+ * @return {DOMElement|Object} Returns the original DOM Element or Javascript Object
+ */
+
+/**
+ * bean.off() is how you get rid of handlers once you no longer want them active. It's also a good idea to call off on elements before you remove them from your DOM; this gives Bean a chance to clean up some things and prevents memory leaks.
+ * http://github.com/fat/bean#off
+ *
+ * @method off
+ * @param {DOMElement|Object} element An HTML DOM element or any JavaScript Object
+ * @param {String} eventType An Event (or multiple events, space separated) to remove
+ * @param {Function} [handler] The specific callback function to remove
+ *
+ * @return {DOMElement|Object} Returns the original DOM Element or Javascript Object
+ */
+
+/**
+ * bean.clone() is a method for cloning events from one DOM element or object to another.
+ * http://github.com/fat/bean#clone
+ *
+ * @method clone
+ * @param {DOMElement|Object} destElement An HTML DOM element or any JavaScript Object to copy events to
+ * @param {String} srcElement An HTML DOM element or any JavaScript Object to copy events from
+ * @param {String} [eventType] An Event (or multiple events, space separated) to clone
+ *
+ * @return {DOMElement|Object} Returns the original DOM Element or Javascript Object
+ */
+
+/**
+ * bean.fire() gives you the ability to trigger events.
+ * http://github.com/fat/bean#fire
+ *
+ * @method fire
+ * @param {DOMElement|Object} destElement An HTML DOM element or any JavaScript Object fire the event on
+ * @param {String} eventType An Event (or multiple events, space separated) to fire
+ * @param [args...] Additional arguments to pass to the callback function when triggered
+ *
+ * @return {DOMElement|Object} Returns the original DOM Element or Javascript Object
+ */
+
+return Ink.extendObj(InkEvent, bean);
+
+});
+
+/**
+ * @module Ink.Dom.FormSerialize
+ * @author inkdev AT sapo.pt
+ */
+
+Ink.createModule('Ink.Dom.FormSerialize', 1, [], function () {
+ 'use strict';
+ /**
+ * Supports serialization of form data to/from javascript Objects.
+ *
+ * Valid applications are ad hoc AJAX/syndicated submission of forms, restoring form values from server side state, etc.
+ *
+ * @class Ink.Dom.FormSerialize
+ *
+ */
+ var FormSerialize = {
+
+ /**
+ * Serializes a form into an object, turning field names into keys, and field values into values.
+ *
+ * note: Multi-select and checkboxes with multiple values will yield arrays
+ *
+ * @method serialize
+ * @return {Object} map of fieldName -> String|String[]|Boolean
+ * @param {DomElement|String} form form element from which the extraction is to occur
+ *
+ * @example
+ *
+ *
+ */
+ serialize: function(form) {
+ form = Ink.i(form);
+ var map = this._getFieldNameInputsMap(form);
+
+ var map2 = {};
+ for (var k in map) if (map.hasOwnProperty(k)) {
+ if(k !== null) {
+ var tmpK = k.replace(/\[\]$/, '');
+ map2[tmpK] = this._getValuesOfField( map[k] );
+ } else {
+ map2[k] = this._getValuesOfField( map[k] );
+ }
+ }
+
+ delete map2['null']; // this can occur. if so, delete it...
+ return map2;
+ },
+
+
+
+
+ /**
+ * Sets form elements's values with values given from object
+ *
+ * One cannot restore the values of an input with `type="file"` (browser prohibits it)
+ *
+ * @method fillIn
+ * @param {DomElement|String} form form element which is to be populated
+ * @param {Object} map2 map of fieldName -> String|String[]|Boolean
+ * @example
+ *
+ *
+ */
+ fillIn: function(form, map2) {
+ form = Ink.i(form);
+ var map = this._getFieldNameInputsMap(form);
+ delete map['null']; // this can occur. if so, delete it...
+
+ for (var k in map2) if (map2.hasOwnProperty(k)) {
+ this._setValuesOfField( map[k], map2[k] );
+ }
+ },
+
+
+
+ _getFieldNameInputsMap: function(formEl) {
+ var name, nodeName, el, map = {};
+ for (var i = 0, f = formEl.elements.length; i < f; ++i) {
+ el = formEl.elements[i];
+ name = el.getAttribute('name');
+ nodeName = el.nodeName.toLowerCase();
+ if (nodeName === 'fieldset') {
+ continue;
+ } else if (map[name] === undefined) {
+ map[name] = [el];
+ } else {
+ map[name].push(el);
+ }
+ }
+ return map;
+ },
+
+
+
+ _getValuesOfField: function(fieldInputs) {
+ var nodeName = fieldInputs[0].nodeName.toLowerCase();
+ var type = fieldInputs[0].getAttribute('type');
+ var value = fieldInputs[0].value;
+ var i, f, j, o, el, m, res = [];
+
+ switch(nodeName) {
+ case 'select':
+ for (i = 0, f = fieldInputs.length; i < f; ++i) {
+ res[i] = [];
+ m = fieldInputs[i].getAttribute('multiple');
+ for (j = 0, o = fieldInputs[i].options.length; j < o; ++j) {
+ el = fieldInputs[i].options[j];
+ if (el.selected) {
+ if (m) {
+ res[i].push(el.value);
+ } else {
+ res[i] = el.value;
+ break;
+ }
+ }
+ }
+ }
+ return ((fieldInputs.length > 0 && /\[[^\]]*\]$/.test(fieldInputs[0].getAttribute('name'))) ? res : res[0]);
+
+ case 'textarea':
+ case 'input':
+ if (type === 'checkbox' || type === 'radio') {
+ for (i = 0, f = fieldInputs.length; i < f; ++i) {
+ el = fieldInputs[i];
+ if (el.checked) {
+ res.push( el.value );
+ }
+ }
+ if (type === 'checkbox') {
+ return (fieldInputs.length > 1) ? res : !!(res.length);
+ }
+ return (fieldInputs.length > 1) ? res[0] : !!(res.length); // on radios only 1 option is selected at most
+ }
+ else {
+ //if (fieldInputs.length > 1) { throw 'Got multiple input elements with same name!'; }
+ if(fieldInputs.length > 0 && /\[[^\]]*\]$/.test(fieldInputs[0].getAttribute('name'))) {
+ var tmpValues = [];
+ for(i=0, f = fieldInputs.length; i < f; ++i) {
+ tmpValues.push(fieldInputs[i].value);
+ }
+ return tmpValues;
+ } else {
+ return value;
+ }
+ }
+ break; // to keep JSHint happy... (reply to this comment by gamboa: - ROTFL)
+
+ default:
+ //throw 'Unsupported element: "' + nodeName + '"!';
+ return undefined;
+ }
+ },
+
+
+
+ _valInArray: function(val, arr) {
+ for (var i = 0, f = arr.length; i < f; ++i) {
+ if (arr[i] === val) { return true; }
+ }
+ return false;
+ },
+
+
+
+ _setValuesOfField: function(fieldInputs, fieldValues) {
+ if (!fieldInputs) { return; }
+ var nodeName = fieldInputs[0].nodeName.toLowerCase();
+ var type = fieldInputs[0].getAttribute('type');
+ var i, f, el;
+
+ switch(nodeName) {
+ case 'select':
+ if (fieldInputs.length > 1) { throw 'Got multiple select elements with same name!'; }
+ for (i = 0, f = fieldInputs[0].options.length; i < f; ++i) {
+ el = fieldInputs[0].options[i];
+ el.selected = (fieldValues instanceof Array) ? this._valInArray(el.value, fieldValues) : el.value === fieldValues;
+ }
+ break;
+ case 'textarea':
+ case 'input':
+ if (type === 'checkbox' || type === 'radio') {
+ for (i = 0, f = fieldInputs.length; i < f; ++i) {
+ el = fieldInputs[i];
+ //el.checked = (fieldValues instanceof Array) ? this._valInArray(el.value, fieldValues) : el.value === fieldValues;
+ el.checked = (fieldValues instanceof Array) ? this._valInArray(el.value, fieldValues) : (fieldInputs.length > 1 ? el.value === fieldValues : !!fieldValues);
+ }
+ }
+ else {
+ if (fieldInputs.length > 1) { throw 'Got multiple input elements with same name!'; }
+ if (type !== 'file') {
+ fieldInputs[0].value = fieldValues;
+ }
+ }
+ break;
+
+ default:
+ throw 'Unsupported element: "' + nodeName + '"!';
+ }
+ }
+ };
+
+ return FormSerialize;
+});
+
+/**
+ * @module Ink.Dom.Loaded_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Dom.Loaded', 1, [], function() {
+
+ 'use strict';
+
+ /**
+ * The Loaded class provides a method that allows developers to queue functions to run when
+ * the page is loaded (document is ready).
+ *
+ * @class Ink.Dom.Loaded
+ * @version 1
+ * @static
+ */
+ var Loaded = {
+
+ /**
+ * Callbacks and their contexts. Array of 2-arrays.
+ *
+ * []
+ *
+ * @attribute _contexts Array
+ * @private
+ *
+ */
+ _contexts: [], // Callbacks' queue
+
+ /**
+ * Adds a new function that will be invoked once the document is ready
+ *
+ * @method run
+ * @param {Object} [win=window] Window object to attach/add the event
+ * @param {Function} fn Callback function to be run after the page is loaded
+ * @public
+ * @example
+ * Ink.requireModules(['Ink.Dom.Loaded_1'], function(Loaded){
+ * Loaded.run(function(){
+ * console.log('This will run when the page/document is ready/loaded');
+ * });
+ * });
+ */
+ run: function(win, fn) {
+ if (!fn) {
+ fn = win;
+ win = window;
+ }
+
+ var context;
+
+ for (var i = 0, len = this._contexts.length; i < len; i++) {
+ if (this._contexts[i][0] === win) {
+ context = this._contexts[i][1];
+ break;
+ }
+ }
+ if (!context) {
+ context = {
+ cbQueue: [],
+ win: win,
+ doc: win.document,
+ root: win.document.documentElement,
+ done: false,
+ top: true
+ };
+ context.handlers = {
+ checkState: Ink.bindEvent(this._checkState, this, context),
+ poll: Ink.bind(this._poll, this, context)
+ };
+ this._contexts.push(
+ [win, context] // Javascript Objects cannot map different windows to
+ // different values.
+ );
+ }
+
+ var ael = context.doc.addEventListener;
+ context.add = ael ? 'addEventListener' : 'attachEvent';
+ context.rem = ael ? 'removeEventListener' : 'detachEvent';
+ context.pre = ael ? '' : 'on';
+ context.det = ael ? 'DOMContentLoaded' : 'onreadystatechange';
+ context.wet = context.pre + 'load';
+
+ var csf = context.handlers.checkState;
+ var alreadyLoaded = (
+ context.doc.readyState === 'complete' &&
+ context.win.location.toString() !== 'about:blank'); // https://code.google.com/p/chromium/issues/detail?id=32357
+
+ if (alreadyLoaded){
+ setTimeout(Ink.bind(function () {
+ fn.call(context.win, 'lazy');
+ }, this), 0);
+ } else {
+ context.cbQueue.push(fn);
+
+ context.doc[context.add]( context.det , csf );
+ context.win[context.add]( context.wet , csf );
+
+ var frameElement = 1;
+ try{
+ frameElement = context.win.frameElement;
+ } catch(e) {}
+ if ( !ael && context.root && context.root.doScroll ) { // IE HACK
+ try {
+ context.top = !frameElement;
+ } catch(e) { }
+ if (context.top) {
+ this._poll(context);
+ }
+ }
+ }
+ },
+
+ /**
+ * Function that will be running the callbacks after the page is loaded
+ *
+ * @method _checkState
+ * @param {Event} event Triggered event
+ * @private
+ */
+ _checkState: function(event, context) {
+ if ( !event || (event.type === 'readystatechange' && context.doc.readyState !== 'complete')) {
+ return;
+ }
+ var where = (event.type === 'load') ? context.win : context.doc;
+ where[context.rem](context.pre+event.type, context.handlers.checkState, false);
+ this._ready(context);
+ },
+
+ /**
+ * Polls the load progress of the page to see if it has already loaded or not
+ *
+ * @method _poll
+ * @private
+ */
+
+ /**
+ *
+ * function _poll
+ */
+ _poll: function(context) {
+ try {
+ context.root.doScroll('left');
+ } catch(e) {
+ return setTimeout(context.handlers.poll, 50);
+ }
+ this._ready(context);
+ },
+
+ /**
+ * Function that runs the callbacks from the queue when the document is ready.
+ *
+ * @method _ready
+ * @private
+ */
+ _ready: function(context) {
+ if (!context.done) {
+ context.done = true;
+ for (var i = 0; i < context.cbQueue.length; ++i) {
+ context.cbQueue[i].call(context.win);
+ }
+ context.cbQueue = [];
+ }
+ }
+ };
+
+ return Loaded;
+
+});
+
+/**
+ * @module Ink.Dom.Selector_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Dom.Selector', 1, [], function() {
+ /*jshint forin:false, eqnull:true, noempty:false, expr:true, boss:true, maxdepth:false*/
+ 'use strict';
+
+ /**
+ * @class Ink.Dom.Selector
+ * @static
+ * @version 1
+ */
+
+
+/*!
+ * Sizzle CSS Selector Engine
+ * Copyright 2013 jQuery Foundation and other contributors
+ * Released under the MIT license
+ * http://sizzlejs.com/
+ */
+
+var i,
+ cachedruns,
+ Expr,
+ getText,
+ isXML,
+ compile,
+ outermostContext,
+ recompare,
+ sortInput,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + -(new Date()),
+ preferredDoc = window.document,
+ support = {},
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ hasDuplicate = false,
+ sortOrder = function() { return 0; },
+
+ // General-purpose constants
+ strundefined = typeof undefined,
+ MAX_NEGATIVE = 1 << 31,
+
+ // Array methods
+ arr = [],
+ pop = arr.pop,
+ push_native = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+ // Use a stripped-down indexOf if we can't use a native one
+ indexOf = arr.indexOf || function( elem ) {
+ var i = 0,
+ len = this.length;
+ for ( ; i < len; i++ ) {
+ if ( this[i] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+
+ // Regular expressions
+
+ // Whitespace characters http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+ // http://www.w3.org/TR/css3-syntax/#characters
+ characterEncoding = "(?:\\\\.|[\\w-]|[^\\x00-\\xa0])+",
+
+ // Loosely modeled on CSS identifier characters
+ // An unquoted value should be a CSS identifier http://www.w3.org/TR/css3-selectors/#attribute-selectors
+ // Proper syntax: http://www.w3.org/TR/CSS21/syndata.html#value-def-identifier
+ identifier = characterEncoding.replace( "w", "w#" ),
+
+ // Acceptable operators http://www.w3.org/TR/selectors/#attribute-selectors
+ operators = "([*^$|!~]?=)",
+ attributes = "\\[" + whitespace + "*(" + characterEncoding + ")" + whitespace +
+ "*(?:" + operators + whitespace + "*(?:(['\"])((?:\\\\.|[^\\\\])*?)\\3|(" + identifier + ")|)|)" + whitespace + "*\\]",
+
+ // Prefer arguments quoted,
+ // then not containing pseudos/brackets,
+ // then attribute selectors/non-parenthetical expressions,
+ // then anything else
+ // These preferences are here to reduce the number of selectors
+ // needing tokenize in the PSEUDO preFilter
+ pseudos = ":(" + characterEncoding + ")(?:\\(((['\"])((?:\\\\.|[^\\\\])*?)\\3|((?:\\\\.|[^\\\\()[\\]]|" + attributes.replace( 3, 8 ) + ")*)|.*)\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" + whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rcombinators = new RegExp( "^" + whitespace + "*([\\x20\\t\\r\\n\\f>+~])" + whitespace + "*" ),
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + characterEncoding + ")" ),
+ "CLASS": new RegExp( "^\\.(" + characterEncoding + ")" ),
+ "NAME": new RegExp( "^\\[name=['\"]?(" + characterEncoding + ")['\"]?\\]" ),
+ "TAG": new RegExp( "^(" + characterEncoding.replace( "w", "w*" ) + ")" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" + whitespace +
+ "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" + whitespace +
+ "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace + "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" +
+ whitespace + "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rsibling = /[\x20\t\r\n\f]*[+~]/,
+
+ rnative = /^[^{]+\{\s*\[native code/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rescape = /'|\\/g,
+ rattributeQuotes = /\=[\x20\t\r\n\f]*([^'"\]]*)[\x20\t\r\n\f]*\]/g,
+
+ // CSS escapes http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = /\\([\da-fA-F]{1,6}[\x20\t\r\n\f]?|.)/g,
+ funescape = function( _, escaped ) {
+ var high = "0x" + escaped - 0x10000;
+ // NaN means non-codepoint
+ return high !== high ?
+ escaped :
+ // BMP codepoint
+ high < 0 ?
+ String.fromCharCode( high + 0x10000 ) :
+ // Supplemental Plane codepoint (surrogate pair)
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ };
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ (arr = slice.call( preferredDoc.childNodes )),
+ preferredDoc.childNodes
+ );
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ push_native.apply( target, slice.call(els) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+ // Can't trust NodeList.length
+ while ( (target[j++] = els[i++]) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+/*
+ * For feature detection
+ * @param {Function} fn The function to test for native support
+ */
+function isNative( fn ) {
+ return rnative.test( fn + "" );
+}
+
+/*
+ * Create key-value caches of limited size
+ * @returns {Function(string, Object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var cache,
+ keys = [];
+
+ return (cache = function( key, value ) {
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key += " " ) > Expr.cacheLength ) {
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return (cache[ key ] = value);
+ });
+}
+
+/*
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/*
+ * Support testing using an element
+ * @param {Function} fn Passed the created div and expects a boolean result
+ */
+function assert( fn ) {
+ var div = document.createElement("div");
+
+ try {
+ return !!fn( div );
+ } catch (e) {
+ return false;
+ } finally {
+ // release memory in IE
+ div = null;
+ }
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var match, elem, m, nodeType,
+ // QSA vars
+ i, groups, old, nid, newContext, newSelector;
+
+ if ( ( context ? context.ownerDocument || context : preferredDoc ) !== document ) {
+ setDocument( context );
+ }
+
+ context = context || document;
+ results = results || [];
+
+ if ( !selector || typeof selector !== "string" ) {
+ return results;
+ }
+
+ if ( (nodeType = context.nodeType) !== 1 && nodeType !== 9 ) {
+ return [];
+ }
+
+ if ( documentIsHTML && !seed ) {
+
+ // Shortcuts
+ if ( (match = rquickExpr.exec( selector )) ) {
+ // Speed-up: Sizzle("#ID")
+ if ( (m = match[1]) ) {
+ if ( nodeType === 9 ) {
+ elem = context.getElementById( m );
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ if ( elem && elem.parentNode ) {
+ // Handle the case where IE, Opera, and Webkit return items
+ // by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+ } else {
+ // Context is not a document
+ if ( context.ownerDocument && (elem = context.ownerDocument.getElementById( m )) &&
+ contains( context, elem ) && elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Speed-up: Sizzle("TAG")
+ } else if ( match[2] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Speed-up: Sizzle(".CLASS")
+ } else if ( (m = match[3]) && support.getElementsByClassName && context.getElementsByClassName ) {
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // QSA path
+ if ( support.qsa && !rbuggyQSA.test(selector) ) {
+ old = true;
+ nid = expando;
+ newContext = context;
+ newSelector = nodeType === 9 && selector;
+
+ // qSA works strangely on Element-rooted queries
+ // We can work around this by specifying an extra ID on the root
+ // and working up from there (Thanks to Andrew Dupont for the technique)
+ // IE 8 doesn't work on object elements
+ if ( nodeType === 1 && context.nodeName.toLowerCase() !== "object" ) {
+ groups = tokenize( selector );
+
+ if ( (old = context.getAttribute("id")) ) {
+ nid = old.replace( rescape, "\\$&" );
+ } else {
+ context.setAttribute( "id", nid );
+ }
+ nid = "[id='" + nid + "'] ";
+
+ i = groups.length;
+ while ( i-- ) {
+ groups[i] = nid + toSelector( groups[i] );
+ }
+ newContext = rsibling.test( selector ) && context.parentNode || context;
+ newSelector = groups.join(",");
+ }
+
+ if ( newSelector ) {
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch(qsaError) {
+ } finally {
+ if ( !old ) {
+ context.removeAttribute("id");
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/*
+ * Detect xml
+ * @param {Element|Object} elem An element or a document
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ // documentElement is verified for cases where it doesn't yet exist
+ // (such as loading iframes in IE - #4833)
+ var documentElement = elem && (elem.ownerDocument || elem).documentElement;
+ return documentElement ? documentElement.nodeName !== "HTML" : false;
+};
+
+/*
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // If no document and documentElement is available, return
+ if ( doc === document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Set our document
+ document = doc;
+ docElem = doc.documentElement;
+
+ // Support tests
+ documentIsHTML = !isXML( doc );
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert(function( div ) {
+ div.appendChild( doc.createComment("") );
+ return !div.getElementsByTagName("*").length;
+ });
+
+ // Check if attributes should be retrieved by attribute nodes
+ support.attributes = assert(function( div ) {
+ div.innerHTML = "";
+ var type = typeof div.lastChild.getAttribute("multiple");
+ // IE8 returns a string for some attributes even when not present
+ return type !== "boolean" && type !== "string";
+ });
+
+ // Check if getElementsByClassName can be trusted
+ support.getElementsByClassName = assert(function( div ) {
+ // Opera can't find a second classname (in 9.6)
+ div.innerHTML = "";
+ if ( !div.getElementsByClassName || !div.getElementsByClassName("e").length ) {
+ return false;
+ }
+
+ // Safari 3.2 caches class attributes and doesn't catch changes
+ div.lastChild.className = "e";
+ return div.getElementsByClassName("e").length === 2;
+ });
+
+ // Check if getElementsByName privileges form controls or returns elements by ID
+ // If so, assume (for broader support) that getElementById returns elements by name
+ support.getByName = assert(function( div ) {
+ // Inject content
+ div.id = expando + 0;
+ // Support: Windows 8 Native Apps
+ // Assigning innerHTML with "name" attributes throws uncatchable exceptions
+ // http://msdn.microsoft.com/en-us/library/ie/hh465388.aspx
+ div.appendChild( document.createElement("a") ).setAttribute( "name", expando );
+ div.appendChild( document.createElement("i") ).setAttribute( "name", expando );
+ docElem.appendChild( div );
+
+ // Test
+ var pass = doc.getElementsByName &&
+ // buggy browsers will return fewer than the correct 2
+ doc.getElementsByName( expando ).length === 2 +
+ // buggy browsers will return more than the correct 0
+ doc.getElementsByName( expando + 0 ).length;
+
+ // Cleanup
+ docElem.removeChild( div );
+
+ return pass;
+ });
+
+ // Support: Webkit<537.32
+ // Detached nodes confoundingly follow *each other*
+ support.sortDetached = assert(function( div1 ) {
+ return div1.compareDocumentPosition &&
+ // Should return 1, but Webkit returns 4 (following)
+ (div1.compareDocumentPosition( document.createElement("div") ) & 1);
+ });
+
+ // IE6/7 return modified attributes
+ Expr.attrHandle = assert(function( div ) {
+ div.innerHTML = "";
+ return div.firstChild && typeof div.firstChild.getAttribute !== strundefined &&
+ div.firstChild.getAttribute("href") === "#";
+ }) ?
+ {} :
+ {
+ "href": function( elem ) {
+ return elem.getAttribute( "href", 2 );
+ },
+ "type": function( elem ) {
+ return elem.getAttribute("type");
+ }
+ };
+
+ // ID find and filter
+ if ( support.getByName ) {
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
+ var m = context.getElementById( id );
+ // Check parentNode to catch when Blackberry 4.6 returns
+ // nodes that are no longer in the document #6963
+ return m && m.parentNode ? [m] : [];
+ }
+ };
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute("id") === attrId;
+ };
+ };
+ } else {
+ Expr.find["ID"] = function( id, context ) {
+ if ( typeof context.getElementById !== strundefined && documentIsHTML ) {
+ var m = context.getElementById( id );
+
+ return m ?
+ m.id === id || typeof m.getAttributeNode !== strundefined && m.getAttributeNode("id").value === id ?
+ [m] :
+ undefined :
+ [];
+ }
+ };
+ Expr.filter["ID"] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== strundefined && elem.getAttributeNode("id");
+ return node && node.value === attrId;
+ };
+ };
+ }
+
+ // Tag
+ Expr.find["TAG"] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== strundefined ) {
+ return context.getElementsByTagName( tag );
+ }
+ } :
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Name
+ Expr.find["NAME"] = support.getByName && function( tag, context ) {
+ if ( typeof context.getElementsByName !== strundefined ) {
+ return context.getElementsByName( name );
+ }
+ };
+
+ // Class
+ Expr.find["CLASS"] = support.getElementsByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== strundefined && documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21),
+ // no need to also add to buggyMatches since matches checks buggyQSA
+ // A support test would require too much code (would include document ready)
+ rbuggyQSA = [ ":focus" ];
+
+ if ( (support.qsa = isNative(doc.querySelectorAll)) ) {
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert(function( div ) {
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // http://bugs.jquery.com/ticket/12359
+ div.innerHTML = "";
+
+ // IE8 - Some boolean attributes are not treated correctly
+ if ( !div.querySelectorAll("[selected]").length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:checked|disabled|ismap|multiple|readonly|selected|value)" );
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":checked").length ) {
+ rbuggyQSA.push(":checked");
+ }
+ });
+
+ assert(function( div ) {
+
+ // Opera 10-12/IE8 - ^= $= *= and empty values
+ // Should not select anything
+ div.innerHTML = "";
+ if ( div.querySelectorAll("[i^='']").length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:\"\"|'')" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( !div.querySelectorAll(":enabled").length ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ div.querySelectorAll("*,:x");
+ rbuggyQSA.push(",.*:");
+ });
+ }
+
+ if ( (support.matchesSelector = isNative( (matches = docElem.matchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.webkitMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector) )) ) {
+
+ assert(function( div ) {
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( div, "div" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( div, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ });
+ }
+
+ rbuggyQSA = new RegExp( rbuggyQSA.join("|") );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join("|") );
+
+ // Element contains another
+ // Purposefully does not implement inclusive descendent
+ // As in, an element does not contain itself
+ contains = isNative(docElem.contains) || docElem.compareDocumentPosition ?
+ function( a, b ) {
+ var adown = a.nodeType === 9 ? a.documentElement : a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ));
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( (b = b.parentNode) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ // Document order sorting
+ sortOrder = docElem.compareDocumentPosition ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var compare = b.compareDocumentPosition && a.compareDocumentPosition && a.compareDocumentPosition( b );
+
+ if ( compare ) {
+ // Disconnected nodes
+ if ( compare & 1 ||
+ (recompare && b.compareDocumentPosition( a ) === compare) ) {
+
+ // Choose the first element that is related to our preferred document
+ if ( a === doc || contains(preferredDoc, a) ) {
+ return -1;
+ }
+ if ( b === doc || contains(preferredDoc, b) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf.call( sortInput, a ) - indexOf.call( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ }
+
+ // Not directly comparable, sort on existence of method
+ return a.compareDocumentPosition ? -1 : 1;
+ } :
+ function( a, b ) {
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+
+ // Parentless nodes are either documents or disconnected
+ } else if ( !aup || !bup ) {
+ return a === doc ? -1 :
+ b === doc ? 1 :
+ aup ? -1 :
+ bup ? 1 :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( (cur = cur.parentNode) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( (cur = cur.parentNode) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[i] === bp[i] ) {
+ i++;
+ }
+
+ return i ?
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[i], bp[i] ) :
+
+ // Otherwise nodes in our document sort first
+ ap[i] === preferredDoc ? -1 :
+ bp[i] === preferredDoc ? 1 :
+ 0;
+ };
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ // Make sure that attribute selectors are quoted
+ expr = expr.replace( rattributeQuotes, "='$1']" );
+
+ // rbuggyQSA always contains :focus, so no need for an existence check
+ if ( support.matchesSelector && documentIsHTML && (!rbuggyMatches || !rbuggyMatches.test(expr)) && !rbuggyQSA.test(expr) ) {
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch(e) {}
+ }
+
+ return Sizzle( expr, document, null, [elem] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+ // Set document vars if needed
+ if ( ( context.ownerDocument || context ) !== document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+ var val;
+
+ // Set document vars if needed
+ if ( ( elem.ownerDocument || elem ) !== document ) {
+ setDocument( elem );
+ }
+
+ if ( documentIsHTML ) {
+ name = name.toLowerCase();
+ }
+ if ( (val = Expr.attrHandle[ name ]) ) {
+ return val( elem );
+ }
+ if ( !documentIsHTML || support.attributes ) {
+ return elem.getAttribute( name );
+ }
+ return ( (val = elem.getAttributeNode( name )) || elem.getAttribute( name ) ) && elem[ name ] === true ?
+ name :
+ val && val.specified ? val.value : null;
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+// Document sorting and removing duplicates
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ // Compensate for sort limitations
+ recompare = !support.sortDetached;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( (elem = results[i++]) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ return results;
+};
+
+/*
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns Returns -1 if a precedes b, 1 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && ( ~b.sourceIndex || MAX_NEGATIVE ) - ( ~a.sourceIndex || MAX_NEGATIVE );
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( (cur = cur.nextSibling) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+// Returns a function to use in pseudos for input types
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+// Returns a function to use in pseudos for buttons
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return (name === "input" || name === "button") && elem.type === type;
+ };
+}
+
+// Returns a function to use in pseudos for positionals
+function createPositionalPseudo( fn ) {
+ return markFunction(function( argument ) {
+ argument = +argument;
+ return markFunction(function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ (j = matchIndexes[i]) ] ) {
+ seed[j] = !(matches[j] = seed[j]);
+ }
+ }
+ });
+ });
+}
+
+/*
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+ // If no nodeType, this is expected to be an array
+ for ( ; (node = elem[i]); i++ ) {
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (see #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[1] = match[1].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[3] = ( match[4] || match[5] || "" ).replace( runescape, funescape );
+
+ if ( match[2] === "~=" ) {
+ match[3] = " " + match[3] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[1] = match[1].toLowerCase();
+
+ if ( match[1].slice( 0, 3 ) === "nth" ) {
+ // nth-* requires argument
+ if ( !match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[4] = +( match[4] ? match[5] + (match[6] || 1) : 2 * ( match[3] === "even" || match[3] === "odd" ) );
+ match[5] = +( ( match[7] + match[8] ) || match[3] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[3] ) {
+ Sizzle.error( match[0] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[5] && match[2];
+
+ if ( matchExpr["CHILD"].test( match[0] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[4] ) {
+ match[2] = match[4];
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+ // Get excess from tokenize (recursively)
+ (excess = tokenize( unquoted, true )) &&
+ // advance to the next closing parenthesis
+ (excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length) ) {
+
+ // excess is a negative index
+ match[0] = match[0].slice( 0, excess );
+ match[2] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeName ) {
+ if ( nodeName === "*" ) {
+ return function() { return true; };
+ }
+
+ nodeName = nodeName.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ (pattern = new RegExp( "(^|" + whitespace + ")" + className + "(" + whitespace + "|$)" )) &&
+ classCache( className, function( elem ) {
+ return pattern.test( elem.className || (typeof elem.getAttribute !== strundefined && elem.getAttribute("class")) || "" );
+ });
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ };
+ },
+
+ "CHILD": function( type, what, argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, context, xml ) {
+ var cache, outerCache, node, diff, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( (node = node[ dir ]) ) {
+ if ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) {
+ return false;
+ }
+ }
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+ // Seek `elem` from a previously-cached index
+ outerCache = parent[ expando ] || (parent[ expando ] = {});
+ cache = outerCache[ type ] || [];
+ nodeIndex = cache[0] === dirruns && cache[1];
+ diff = cache[0] === dirruns && cache[2];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ outerCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ // Use previously-cached element index if available
+ } else if ( useCache && (cache = (elem[ expando ] || (elem[ expando ] = {}))[ type ]) && cache[0] === dirruns ) {
+ diff = cache[1];
+
+ // xml :nth-child(...) or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ } else {
+ // Use the same loop as above to seek `elem` from the start
+ while ( (node = ++nodeIndex && node && node[ dir ] ||
+ (diff = nodeIndex = 0) || start.pop()) ) {
+
+ if ( ( ofType ? node.nodeName.toLowerCase() === name : node.nodeType === 1 ) && ++diff ) {
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ (node[ expando ] || (node[ expando ] = {}))[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction(function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf.call( seed, matched[i] );
+ seed[ idx ] = !( matches[ idx ] = matched[i] );
+ }
+ }) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+ // Potentially complex pseudos
+ "not": markFunction(function( selector ) {
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction(function( seed, matches, context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( (elem = unmatched[i]) ) {
+ seed[i] = !(matches[i] = elem);
+ }
+ }
+ }) :
+ function( elem, context, xml ) {
+ input[0] = elem;
+ matcher( input, null, xml, results );
+ return !results.pop();
+ };
+ }),
+
+ "has": markFunction(function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ }),
+
+ "contains": markFunction(function( text ) {
+ return function( elem ) {
+ return ( elem.textContent || elem.innerText || getText( elem ) ).indexOf( text ) > -1;
+ };
+ }),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+ // lang value must be a valid identifier
+ if ( !ridentifier.test(lang || "") ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( (elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute("xml:lang") || elem.getAttribute("lang")) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( (elem = elem.parentNode) && elem.nodeType === 1 );
+ return false;
+ };
+ }),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement && (!document.hasFocus || document.hasFocus()) && !!(elem.type || elem.href || ~elem.tabIndex);
+ },
+
+ // Boolean properties
+ "enabled": function( elem ) {
+ return elem.disabled === false;
+ },
+
+ "disabled": function( elem ) {
+ return elem.disabled === true;
+ },
+
+ "checked": function( elem ) {
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return (nodeName === "input" && !!elem.checked) || (nodeName === "option" && !!elem.selected);
+ },
+
+ "selected": function( elem ) {
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is only affected by element nodes and content nodes(including text(3), cdata(4)),
+ // not comment, processing instructions, or others
+ // Thanks to Diego Perini for the nodeName shortcut
+ // Greater than "@" means alpha characters (specifically not starting with "#" or "?")
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeName > "@" || elem.nodeType === 3 || elem.nodeType === 4 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos["empty"]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ // IE6 and 7 will map elem.type to 'text' for new HTML5 types (search, etc)
+ // use getAttribute instead to test this case
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+ ( (attr = elem.getAttribute("type")) == null || attr.toLowerCase() === elem.type );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo(function() {
+ return [ 0 ];
+ }),
+
+ "last": createPositionalPseudo(function( matchIndexes, length ) {
+ return [ length - 1 ];
+ }),
+
+ "eq": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ }),
+
+ "even": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "odd": createPositionalPseudo(function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "lt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ }),
+
+ "gt": createPositionalPseudo(function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ })
+ }
+};
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+function tokenize( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || (match = rcomma.exec( soFar )) ) {
+ if ( match ) {
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[0].length ) || soFar;
+ }
+ groups.push( tokens = [] );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( (match = rcombinators.exec( soFar )) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+ // Cast descendant combinators to space
+ type: match[0].replace( rtrim, " " )
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( (match = matchExpr[ type ].exec( soFar )) && (!preFilters[ type ] ||
+ (match = preFilters[ type ]( match ))) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+ type: type,
+ matches: match
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+}
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[i].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ checkNonElements = base && dir === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var data, cache, outerCache,
+ dirkey = dirruns + " " + doneName;
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from dir caching
+ if ( xml ) {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( (elem = elem[ dir ]) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || (elem[ expando ] = {});
+ if ( (cache = outerCache[ dir ]) && cache[0] === dirkey ) {
+ if ( (data = cache[1]) === true || data === cachedruns ) {
+ return data === true;
+ }
+ } else {
+ cache = outerCache[ dir ] = [ dirkey ];
+ cache[1] = matcher( elem, context, xml ) || cachedruns;
+ if ( cache[1] === true ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[i]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[0];
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( (elem = unmatched[i]) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction(function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts( selector || "*", context.nodeType ? [ context ] : context, [] ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( (elem = temp[i]) ) {
+ matcherOut[ postMap[i] ] = !(matcherIn[ postMap[i] ] = elem);
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) ) {
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( (matcherIn[i] = elem) );
+ }
+ }
+ postFinder( null, (matcherOut = []), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( (elem = matcherOut[i]) &&
+ (temp = postFinder ? indexOf.call( seed, elem ) : preMap[i]) > -1 ) {
+
+ seed[temp] = !(results[temp] = elem);
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ });
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[0].type ],
+ implicitRelative = leadingRelative || Expr.relative[" "],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf.call( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ return ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ (checkContext = context).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( (matcher = Expr.relative[ tokens[i].type ]) ) {
+ matchers = [ addCombinator(elementMatcher( matchers ), matcher) ];
+ } else {
+ matcher = Expr.filter[ tokens[i].type ].apply( null, tokens[i].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[j].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector( tokens.slice( 0, i - 1 ) ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( (tokens = tokens.slice( j )) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ // A counter to specify which element is currently being matched
+ var matcherCachedRuns = 0,
+ bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, expandContext ) {
+ var elem, j, matcher,
+ setMatched = [],
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ outermost = expandContext != null,
+ contextBackup = outermostContext,
+ // We must always have either seed elements or context
+ elems = seed || byElement && Expr.find["TAG"]( "*", expandContext && context.parentNode || context ),
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = (dirruns += contextBackup == null ? 1 : Math.random() || 0.1);
+
+ if ( outermost ) {
+ outermostContext = context !== document && context;
+ cachedruns = matcherCachedRuns;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Keep `i` a string if there are no elements so `matchedCount` will be "00" below
+ for ( ; (elem = elems[i]) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+ while ( (matcher = elementMatchers[j++]) ) {
+ if ( matcher( elem, context, xml ) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ cachedruns = ++matcherCachedRuns;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+ // They will have gone through all possible matchers
+ if ( (elem = !matcher && elem) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // Apply set filters to unmatched elements
+ matchedCount += i;
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( (matcher = setMatchers[j++]) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !(unmatched[i] || setMatched[i]) ) {
+ setMatched[i] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, group /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !group ) {
+ group = tokenize( selector );
+ }
+ i = group.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( group[i] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache( selector, matcherFromGroupMatchers( elementMatchers, setMatchers ) );
+ }
+ return cached;
+};
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[i], results );
+ }
+ return results;
+}
+
+function select( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ match = tokenize( selector );
+
+ if ( !seed ) {
+ // Try to minimize operations if there is only one group
+ if ( match.length === 1 ) {
+
+ // Take a shortcut and set the context if the root selector is an ID
+ tokens = match[0] = match[0].slice( 0 );
+ if ( tokens.length > 2 && (token = tokens[0]).type === "ID" &&
+ context.nodeType === 9 && documentIsHTML &&
+ Expr.relative[ tokens[1].type ] ) {
+
+ context = ( Expr.find["ID"]( token.matches[0].replace(runescape, funescape), context ) || [] )[0];
+ if ( !context ) {
+ return results;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr["needsContext"].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[i];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ (type = token.type) ] ) {
+ break;
+ }
+ if ( (find = Expr.find[ type ]) ) {
+ // Search, expanding context for leading sibling combinators
+ if ( (seed = find(
+ token.matches[0].replace( runescape, funescape ),
+ rsibling.test( tokens[0].type ) && context.parentNode || context
+ )) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function
+ // Provide `match` to avoid retokenization if we modified the selector above
+ compile( selector, match )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ rsibling.test( selector )
+ );
+ return results;
+}
+
+// Deprecated
+Expr.pseudos["nth"] = Expr.pseudos["eq"];
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+// Check sort stability
+support.sortStable = expando.split("").sort( sortOrder ).join("") === expando;
+
+// Initialize with the default document
+setDocument();
+
+// Always assume the presence of duplicates if sort doesn't
+// pass them to our comparison function (as in Google Chrome).
+[0, 0].sort( sortOrder );
+support.detectDuplicates = hasDuplicate;
+
+// EXPOSE
+/*if ( typeof define === "function" && define.amd ) {
+ define(function() { return Sizzle; });
+} else {
+ window.Sizzle = Sizzle;
+}*/
+// EXPOSE
+
+/**
+ * Alias for the Sizzle selector engine
+ *
+ * @method select
+ * @param {String} selector CSS selector to search for elements
+ * @param {DOMElement} [context] By default the search is done in the document element. However, you can specify an element as search context
+ * @param {Array} [results] By default this is considered an empty array. But if you want to merge it with other searches you did, pass their result array through here.
+ * @param {Object} [seed]
+ * @return {Array} Array of resulting DOM Elements
+ */
+
+/**
+ * Returns elements which match with the second argument to the function.
+ *
+ * @method matches
+ * @param {String} selector CSS selector to search for elements
+ * @param {Array} matches Elements to be 'matched' with
+ * @return {Array} Elements that matched
+ */
+
+/**
+ * Returns true iif element matches given selector
+ *
+ * @method matchesSelector
+ * @param {DOMElement} element to test
+ * @param {String} selector CSS selector to test the element with
+ * @return {Boolean} true iif element matches the CSS selector
+ */
+
+return {
+ select: Sizzle,
+ matches: Sizzle.matches,
+ matchesSelector: Sizzle.matchesSelector
+};
+
+
+}); //( window );
+
+/**
+ * @module Ink.Util.Array_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Util.Array', '1', [], function() {
+
+ 'use strict';
+
+ var arrayProto = Array.prototype;
+
+ /**
+ * Utility functions to use with Arrays
+ *
+ * @class Ink.Util.Array
+ * @version 1
+ * @static
+ */
+ var InkArray = {
+
+ /**
+ * Checks if value exists in array
+ *
+ * @method inArray
+ * @param {Mixed} value
+ * @param {Array} arr
+ * @return {Boolean} True if value exists in the array
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray = [ 'value1', 'value2', 'value3' ];
+ * if( InkArray.inArray( 'value2', testArray ) === true ){
+ * console.log( "Yep it's in the array." );
+ * } else {
+ * console.log( "No it's NOT in the array." );
+ * }
+ * });
+ */
+ inArray: function(value, arr) {
+ if (typeof arr === 'object') {
+ for (var i = 0, f = arr.length; i < f; ++i) {
+ if (arr[i] === value) {
+ return true;
+ }
+ }
+ }
+ return false;
+ },
+
+ /**
+ * Sorts an array of object by an object property
+ *
+ * @method sortMulti
+ * @param {Array} arr array of objects to sort
+ * @param {String} key property to sort by
+ * @return {Array|Boolean} False if it's not an array, returns a sorted array if it's an array.
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray = [
+ * { 'myKey': 'value1' },
+ * { 'myKey': 'value2' },
+ * { 'myKey': 'value3' }
+ * ];
+ *
+ * InkArray.sortMulti( testArray, 'myKey' );
+ * });
+ */
+ sortMulti: function(arr, key) {
+ if (typeof arr === 'undefined' || arr.constructor !== Array) { return false; }
+ if (typeof key !== 'string') { return arr.sort(); }
+ if (arr.length > 0) {
+ if (typeof(arr[0][key]) === 'undefined') { return false; }
+ arr.sort(function(a, b){
+ var x = a[key];
+ var y = b[key];
+ return ((x < y) ? -1 : ((x > y) ? 1 : 0));
+ });
+ }
+ return arr;
+ },
+
+ /**
+ * Returns the associated key of an array value
+ *
+ * @method keyValue
+ * @param {String} value Value to search for
+ * @param {Array} arr Array where the search will run
+ * @param {Boolean} [first] Flag that determines if the search stops at first occurrence. It also returns an index number instead of an array of indexes.
+ * @return {Boolean|Number|Array} False if not exists | number if exists and 3rd input param is true | array if exists and 3rd input param is not set or it is !== true
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray = [ 'value1', 'value2', 'value3', 'value2' ];
+ * console.log( InkArray.keyValue( 'value2', testArray, true ) ); // Result: 1
+ * console.log( InkArray.keyValue( 'value2', testArray ) ); // Result: [1, 3]
+ * });
+ */
+ keyValue: function(value, arr, first) {
+ if (typeof value !== 'undefined' && typeof arr === 'object' && this.inArray(value, arr)) {
+ var aKeys = [];
+ for (var i = 0, f = arr.length; i < f; ++i) {
+ if (arr[i] === value) {
+ if (typeof first !== 'undefined' && first === true) {
+ return i;
+ } else {
+ aKeys.push(i);
+ }
+ }
+ }
+ return aKeys;
+ }
+ return false;
+ },
+
+ /**
+ * Returns the array shuffled, false if the param is not an array
+ *
+ * @method shuffle
+ * @param {Array} arr Array to shuffle
+ * @return {Boolean|Number|Array} False if not an array | Array shuffled
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray = [ 'value1', 'value2', 'value3', 'value2' ];
+ * console.log( InkArray.shuffle( testArray ) ); // Result example: [ 'value3', 'value2', 'value2', 'value1' ]
+ * });
+ */
+ shuffle: function(arr) {
+ if (typeof(arr) !== 'undefined' && arr.constructor !== Array) { return false; }
+ var total = arr.length,
+ tmp1 = false,
+ rnd = false;
+
+ while (total--) {
+ rnd = Math.floor(Math.random() * (total + 1));
+ tmp1 = arr[total];
+ arr[total] = arr[rnd];
+ arr[rnd] = tmp1;
+ }
+ return arr;
+ },
+
+ /**
+ * Runs a function through each of the elements of an array
+ *
+ * @method forEach
+ * @param {Array} arr Array to be cycled/iterated
+ * @param {Function} cb The function receives as arguments the value, index and array.
+ * @return {Array} Array iterated.
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray = [ 'value1', 'value2', 'value3', 'value2' ];
+ * InkArray.forEach( testArray, function( value, index, arr ){
+ * console.log( 'The value is: ' + value + ' | The index is: ' + index );
+ * });
+ * });
+ */
+ forEach: function(array, callback, context) {
+ if (arrayProto.forEach) {
+ return arrayProto.forEach.call(array, callback, context);
+ }
+ for (var i = 0, len = array.length >>> 0; i < len; i++) {
+ callback.call(context, array[i], i, array);
+ }
+ },
+
+ /**
+ * Alias for backwards compatibility. See forEach
+ *
+ * @method forEach
+ */
+ each: function () {
+ InkArray.forEach.apply(InkArray, [].slice.call(arguments));
+ },
+
+ /**
+ * Run a `map` function for each item in the array. The function will receive each item as argument and its return value will change the corresponding array item.
+ * @method map
+ * @param {Array} array The array to map over
+ * @param {Function} map The map function. Will take `(item, index, array)` and `this` will be the `context` argument.
+ * @param {Object} [context] Object to be `this` in the map function.
+ *
+ * @example
+ * InkArray.map([1, 2, 3, 4], function (item) {
+ * return item + 1;
+ * }); // -> [2, 3, 4, 5]
+ */
+ map: function (array, callback, context) {
+ if (arrayProto.map) {
+ return arrayProto.map.call(array, callback, context);
+ }
+ var mapped = new Array(len);
+ for (var i = 0, len = array.length >>> 0; i < len; i++) {
+ mapped[i] = callback.call(context, array[i], i, array);
+ }
+ return mapped;
+ },
+
+ /**
+ * Run a test function through all the input array. Items which pass the test function (for which the test function returned `true`) are kept in the array. Other items are removed.
+ * @param {Array} array
+ * @param {Function} test A test function taking `(item, index, array)`
+ * @param {Object} [context] Object to be `this` in the test function.
+ * @return filtered array
+ *
+ * @example
+ * InkArray.filter([1, 2, 3, 4, 5], function (val) {
+ * return val > 2;
+ * }) // -> [3, 4, 5]
+ */
+ filter: function (array, test, context) {
+ if (arrayProto.filter) {
+ return arrayProto.filter.call(array, test, context);
+ }
+ var filtered = [],
+ val = null;
+ for (var i = 0, len = array.length; i < len; i++) {
+ val = array[i]; // it might be mutated
+ if (test.call(context, val, i, array)) {
+ filtered.push(val);
+ }
+ }
+ return filtered;
+ },
+
+ /**
+ * Runs a callback function, which should return true or false.
+ * If one of the 'runs' returns true, it will return. Otherwise if none returns true, it will return false.
+ * See more at: https://developer.mozilla.org/en-US/docs/JavaScript/Reference/Global_Objects/Array/some (MDN)
+ *
+ * @method some
+ * @param {Array} arr The array you walk to iterate through
+ * @param {Function} cb The callback that will be called on the array's elements. It receives the value, the index and the array as arguments.
+ * @param {Object} Context object of the callback function
+ * @return {Boolean} True if the callback returns true at any point, false otherwise
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray1 = [ 10, 20, 50, 100, 30 ];
+ * var testArray2 = [ 1, 2, 3, 4, 5 ];
+ *
+ * function myTestFunction( value, index, arr ){
+ * if( value > 90 ){
+ * return true;
+ * }
+ * return false;
+ * }
+ * console.log( InkArray.some( testArray1, myTestFunction, null ) ); // Result: true
+ * console.log( InkArray.some( testArray2, myTestFunction, null ) ); // Result: false
+ * });
+ */
+ some: function(arr, cb, context){
+
+ if (arr === null){
+ throw new TypeError('First argument is invalid.');
+ }
+
+ var t = Object(arr);
+ var len = t.length >>> 0;
+ if (typeof cb !== "function"){ throw new TypeError('Second argument must be a function.'); }
+
+ for (var i = 0; i < len; i++) {
+ if (i in t && cb.call(context, t[i], i, t)){ return true; }
+ }
+
+ return false;
+ },
+
+ /**
+ * Returns an array containing every item that is shared between the two given arrays
+ *
+ * @method intersect
+ * @param {Array} arr Array1 to be intersected with Array2
+ * @param {Array} arr Array2 to be intersected with Array1
+ * @return {Array} Empty array if one of the arrays is false (or do not intersect) | Array with the intersected values
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Array_1'], function( InkArray ){
+ * var testArray1 = [ 'value1', 'value2', 'value3' ];
+ * var testArray2 = [ 'value2', 'value3', 'value4', 'value5', 'value6' ];
+ * console.log( InkArray.intersect( testArray1,testArray2 ) ); // Result: [ 'value2', 'value3' ]
+ * });
+ */
+ intersect: function(arr1, arr2) {
+ if (!arr1 || !arr2 || arr1 instanceof Array === false || arr2 instanceof Array === false) {
+ return [];
+ }
+
+ var shared = [];
+ for (var i = 0, I = arr1.length; i= from && i < from + rLen){
+ continue;
+ }
+
+ output.push(arr[i]);
+ }
+
+ return output;
+ }
+ };
+
+ return InkArray;
+
+});
+
+
+
+/**
+ * @module Ink.Util.BinPack_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Util.BinPack', '1', [], function() {
+
+ 'use strict';
+
+ /*jshint boss:true */
+
+ // https://github.com/jakesgordon/bin-packing/
+
+ /*
+ Copyright (c) 2011, 2012, 2013 Jake Gordon and contributors
+
+ Permission is hereby granted, free of charge, to any person obtaining a copy
+ of this software and associated documentation files (the "Software"), to deal
+ in the Software without restriction, including without limitation the rights
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ copies of the Software, and to permit persons to whom the Software is
+ furnished to do so, subject to the following conditions:
+
+ The above copyright notice and this permission notice shall be included in all
+ copies or substantial portions of the Software.
+
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ SOFTWARE.
+ */
+
+
+
+ var Packer = function(w, h) {
+ this.init(w, h);
+ };
+
+ Packer.prototype = {
+
+ init: function(w, h) {
+ this.root = { x: 0, y: 0, w: w, h: h };
+ },
+
+ fit: function(blocks) {
+ var n, node, block;
+ for (n = 0; n < blocks.length; ++n) {
+ block = blocks[n];
+ if (node = this.findNode(this.root, block.w, block.h)) {
+ block.fit = this.splitNode(node, block.w, block.h);
+ }
+ }
+ },
+
+ findNode: function(root, w, h) {
+ if (root.used) {
+ return this.findNode(root.right, w, h) || this.findNode(root.down, w, h);
+ }
+ else if ((w <= root.w) && (h <= root.h)) {
+ return root;
+ }
+ else {
+ return null;
+ }
+ },
+
+ splitNode: function(node, w, h) {
+ node.used = true;
+ node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h };
+ node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h };
+ return node;
+ }
+
+ };
+
+
+
+ var GrowingPacker = function() {};
+
+ GrowingPacker.prototype = {
+
+ fit: function(blocks) {
+ var n, node, block, len = blocks.length;
+ var w = len > 0 ? blocks[0].w : 0;
+ var h = len > 0 ? blocks[0].h : 0;
+ this.root = { x: 0, y: 0, w: w, h: h };
+ for (n = 0; n < len ; n++) {
+ block = blocks[n];
+ if (node = this.findNode(this.root, block.w, block.h)) {
+ block.fit = this.splitNode(node, block.w, block.h);
+ }
+ else {
+ block.fit = this.growNode(block.w, block.h);
+ }
+ }
+ },
+
+ findNode: function(root, w, h) {
+ if (root.used) {
+ return this.findNode(root.right, w, h) || this.findNode(root.down, w, h);
+ }
+ else if ((w <= root.w) && (h <= root.h)) {
+ return root;
+ }
+ else {
+ return null;
+ }
+ },
+
+ splitNode: function(node, w, h) {
+ node.used = true;
+ node.down = { x: node.x, y: node.y + h, w: node.w, h: node.h - h };
+ node.right = { x: node.x + w, y: node.y, w: node.w - w, h: h };
+ return node;
+ },
+
+ growNode: function(w, h) {
+ var canGrowDown = (w <= this.root.w);
+ var canGrowRight = (h <= this.root.h);
+
+ var shouldGrowRight = canGrowRight && (this.root.h >= (this.root.w + w)); // attempt to keep square-ish by growing right when height is much greater than width
+ var shouldGrowDown = canGrowDown && (this.root.w >= (this.root.h + h)); // attempt to keep square-ish by growing down when width is much greater than height
+
+ if (shouldGrowRight) {
+ return this.growRight(w, h);
+ }
+ else if (shouldGrowDown) {
+ return this.growDown(w, h);
+ }
+ else if (canGrowRight) {
+ return this.growRight(w, h);
+ }
+ else if (canGrowDown) {
+ return this.growDown(w, h);
+ }
+ else {
+ return null; // need to ensure sensible root starting size to avoid this happening
+ }
+ },
+
+ growRight: function(w, h) {
+ this.root = {
+ used: true,
+ x: 0,
+ y: 0,
+ w: this.root.w + w,
+ h: this.root.h,
+ down: this.root,
+ right: { x: this.root.w, y: 0, w: w, h: this.root.h }
+ };
+ var node;
+ if (node = this.findNode(this.root, w, h)) {
+ return this.splitNode(node, w, h);
+ }
+ else {
+ return null;
+ }
+ },
+
+ growDown: function(w, h) {
+ this.root = {
+ used: true,
+ x: 0,
+ y: 0,
+ w: this.root.w,
+ h: this.root.h + h,
+ down: { x: 0, y: this.root.h, w: this.root.w, h: h },
+ right: this.root
+ };
+ var node;
+ if (node = this.findNode(this.root, w, h)) {
+ return this.splitNode(node, w, h);
+ }
+ else {
+ return null;
+ }
+ }
+
+ };
+
+
+
+ var sorts = {
+ random: function() { return Math.random() - 0.5; },
+ w: function(a, b) { return b.w - a.w; },
+ h: function(a, b) { return b.h - a.h; },
+ a: function(a, b) { return b.area - a.area; },
+ max: function(a, b) { return Math.max(b.w, b.h) - Math.max(a.w, a.h); },
+ min: function(a, b) { return Math.min(b.w, b.h) - Math.min(a.w, a.h); },
+ height: function(a, b) { return sorts.msort(a, b, ['h', 'w']); },
+ width: function(a, b) { return sorts.msort(a, b, ['w', 'h']); },
+ area: function(a, b) { return sorts.msort(a, b, ['a', 'h', 'w']); },
+ maxside: function(a, b) { return sorts.msort(a, b, ['max', 'min', 'h', 'w']); },
+ msort: function(a, b, criteria) { /* sort by multiple criteria */
+ var diff, n;
+ for (n = 0; n < criteria.length; ++n) {
+ diff = sorts[ criteria[n] ](a, b);
+ if (diff !== 0) {
+ return diff;
+ }
+ }
+ return 0;
+ }
+ };
+
+
+
+ // end of Jake's code
+
+
+
+ // aux, used to display blocks in unfitted property
+ var toString = function() {
+ return [this.w, ' x ', this.h].join('');
+ };
+
+
+
+ /**
+ * Binary Packing algorithm implementation
+ *
+ * Based on the work of Jake Gordon
+ *
+ * see https://github.com/jakesgordon/bin-packing/
+ *
+ * @class Ink.Util.BinPack
+ * @version 1
+ * @static
+ */
+ var BinPack = {
+
+ /**
+ * @method binPack
+ * @param {Object} o options
+ * @param {Object[]} o.blocks array of items with w and h integer attributes.
+ * @param {Number[2]} [o.dimensions] if passed, container has fixed dimensions
+ * @param {String} [o.sorter] sorter function. one of: random, height, width, area, maxside
+ * @return {Object}
+ * * {Number[2]} dimensions - resulted container size,
+ * * {Number} filled - filled ratio,
+ * * {Object[]} fitted,
+ * * {Object[]} unfitted,
+ * * {Object[]} blocks
+ * @static
+ */
+ binPack: function(o) {
+ var i, f, bl;
+
+
+
+ // calculate area if not there already
+ for (i = 0, f = o.blocks.length; i < f; ++i) {
+ bl = o.blocks[i];
+ if (! ('area' in bl) ) {
+ bl.area = bl.w * bl.h;
+ }
+ }
+
+
+
+ // apply algorithm
+ var packer = o.dimensions ? new Packer(o.dimensions[0], o.dimensions[1]) : new GrowingPacker();
+
+ if (!o.sorter) { o.sorter = 'maxside'; }
+
+ o.blocks.sort( sorts[ o.sorter ] );
+
+ packer.fit(o.blocks);
+
+ var dims2 = [packer.root.w, packer.root.h];
+
+
+
+ // layout is done here, generating report data...
+ var fitted = [];
+ var unfitted = [];
+
+ for (i = 0, f = o.blocks.length; i < f; ++i) {
+ bl = o.blocks[i];
+ if (bl.fit) {
+ fitted.push(bl);
+ }
+ else {
+ bl.toString = toString; // TO AID SERIALIZATION
+ unfitted.push(bl);
+ }
+ }
+
+ var area = dims2[0] * dims2[1];
+ var fit = 0;
+ for (i = 0, f = fitted.length; i < f; ++i) {
+ bl = fitted[i];
+ fit += bl.area;
+ }
+
+ return {
+ dimensions: dims2,
+ filled: fit / area,
+ blocks: o.blocks,
+ fitted: fitted,
+ unfitted: unfitted
+ };
+ }
+ };
+
+
+
+ return BinPack;
+
+});
+
+/**
+ * @module Ink.Util.Cookie_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Util.Cookie', '1', [], function() {
+
+ 'use strict';
+
+ /**
+ * Utilities for Cookie handling
+ *
+ * @class Ink.Util.Cookie
+ * @version 1
+ * @static
+ */
+ var Cookie = {
+
+ /**
+ * Gets an object with current page cookies
+ *
+ * @method get
+ * @param {String} name
+ * @return {String|Object} If the name is specified, it returns the value related to that property. Otherwise it returns the full cookie object
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Cookie_1'], function( InkCookie ){
+ * var myCookieValue = InkCookie.get('someVarThere');
+ * console.log( myCookieValue ); // This will output the value of the cookie 'someVarThere', from the cookie object.
+ * });
+ */
+ get: function(name)
+ {
+ var cookie = document.cookie || false;
+
+ var _Cookie = {};
+ if(cookie) {
+ cookie = cookie.replace(new RegExp("; ", "g"), ';');
+ var aCookie = cookie.split(';');
+ var aItem = [];
+ if(aCookie.length > 0) {
+ for(var i=0; i < aCookie.length; i++) {
+ aItem = aCookie[i].split('=');
+ if(aItem.length === 2) {
+ _Cookie[aItem[0]] = decodeURIComponent(aItem[1]);
+ }
+ aItem = [];
+ }
+ }
+ }
+ if(name) {
+ if(typeof(_Cookie[name]) !== 'undefined') {
+ return _Cookie[name];
+ } else {
+ return null;
+ }
+ }
+ return _Cookie;
+ },
+
+ /**
+ * Sets a cookie
+ *
+ * @method set
+ * @param {String} name Cookie name
+ * @param {String} value Cookie value
+ * @param {Number} [expires] Number to add to current Date in seconds
+ * @param {String} [path] Path to sets cookie (default '/')
+ * @param {String} [domain] Domain to sets cookie (default current hostname)
+ * @param {Boolean} [secure] True if wants secure, default 'false'
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Cookie_1'], function( InkCookie ){
+ * var expireDate = new Date( 2014,00,01, 0,0,0);
+ * InkCookie.set( 'someVarThere', 'anyValueHere', expireDate.getTime() );
+ * });
+ */
+ set: function(name, value, expires, path, domain, secure)
+ {
+ var sName;
+ if(!name || value===false || typeof(name) === 'undefined' || typeof(value) === 'undefined') {
+ return false;
+ } else {
+ sName = name+'='+encodeURIComponent(value);
+ }
+ var sExpires = false;
+ var sPath = false;
+ var sDomain = false;
+ var sSecure = false;
+
+ if(expires && typeof(expires) !== 'undefined' && !isNaN(expires)) {
+ var oDate = new Date();
+ var sDate = (parseInt(Number(oDate.valueOf()), 10) + (Number(parseInt(expires, 10)) * 1000));
+
+ var nDate = new Date(sDate);
+ var expiresString = nDate.toGMTString();
+
+ var re = new RegExp("([^\\s]+)(\\s\\d\\d)\\s(\\w\\w\\w)\\s(.*)");
+ expiresString = expiresString.replace(re, "$1$2-$3-$4");
+
+ sExpires = 'expires='+expiresString;
+ } else {
+ if(typeof(expires) !== 'undefined' && !isNaN(expires) && Number(parseInt(expires, 10))===0) {
+ sExpires = '';
+ } else {
+ sExpires = 'expires=Thu, 01-Jan-2037 00:00:01 GMT';
+ }
+ }
+
+ if(path && typeof(path) !== 'undefined') {
+ sPath = 'path='+path;
+ } else {
+ sPath = 'path=/';
+ }
+
+ if(domain && typeof(domain) !== 'undefined') {
+ sDomain = 'domain='+domain;
+ } else {
+ var portClean = new RegExp(":(.*)");
+ sDomain = 'domain='+window.location.host;
+ sDomain = sDomain.replace(portClean,"");
+ }
+
+ if(secure && typeof(secure) !== 'undefined') {
+ sSecure = secure;
+ } else {
+ sSecure = false;
+ }
+
+ document.cookie = sName+'; '+sExpires+'; '+sPath+'; '+sDomain+'; '+sSecure;
+ },
+
+ /**
+ * Delete a cookie
+ *
+ * @method remove
+ * @param {String} cookieName Cookie name
+ * @param {String} [path] Path of the cookie (default '/')
+ * @param {String} [domain] Domain of the cookie (default current hostname)
+ * @public
+ * @static
+ * @example
+ * Ink.requireModules(['Ink.Util.Cookie_1'], function( InkCookie ){
+ * InkCookie.remove( 'someVarThere' );
+ * });
+ */
+ remove: function(cookieName, path, domain)
+ {
+ //var expiresDate = 'Thu, 01-Jan-1970 00:00:01 GMT';
+ var sPath = false;
+ var sDomain = false;
+ var expiresDate = -999999999;
+
+ if(path && typeof(path) !== 'undefined') {
+ sPath = path;
+ } else {
+ sPath = '/';
+ }
+
+ if(domain && typeof(domain) !== 'undefined') {
+ sDomain = domain;
+ } else {
+ sDomain = window.location.host;
+ }
+
+ this.set(cookieName, 'deleted', expiresDate, sPath, sDomain);
+ }
+ };
+
+ return Cookie;
+
+});
+
+/**
+ * @module Ink.Util.Date_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Util.Date', '1', [], function() {
+
+ 'use strict';
+
+ /**
+ * Class to provide the same features that php date does
+ *
+ * @class Ink.Util.Date
+ * @version 1
+ * @static
+ */
+ var InkDate = {
+
+ /**
+ * Function that returns the string representation of the month [PT only]
+ *
+ * @method _months
+ * @param {Number} index Month javascript (0 to 11)
+ * @return {String} The month's name
+ * @private
+ * @static
+ * @example
+ * console.log( InkDate._months(0) ); // Result: Janeiro
+ */
+ _months: function(index){
+ var _m = ['Janeiro', 'Fevereiro', 'Março', 'Abril', 'Maio', 'Junho', 'Julho', 'Agosto', 'Setembro', 'Outubro', 'Novembro', 'Dezembro'];
+ return _m[index];
+ },
+
+ /**
+ * Function that returns the month [PT only] ( 0 to 11 )
+ *
+ * @method _iMonth
+ * @param {String} month Month javascript (0 to 11)
+ * @return {Number} The month's number
+ * @private
+ * @static
+ * @example
+ * console.log( InkDate._iMonth('maio') ); // Result: 4
+ */
+ _iMonth : function( month )
+ {
+ if ( Number( month ) ) { return +month - 1; }
+ return {
+ 'janeiro' : 0 ,
+ 'jan' : 0 ,
+ 'fevereiro' : 1 ,
+ 'fev' : 1 ,
+ 'março' : 2 ,
+ 'mar' : 2 ,
+ 'abril' : 3 ,
+ 'abr' : 3 ,
+ 'maio' : 4 ,
+ 'mai' : 4 ,
+ 'junho' : 5 ,
+ 'jun' : 5 ,
+ 'julho' : 6 ,
+ 'jul' : 6 ,
+ 'agosto' : 7 ,
+ 'ago' : 7 ,
+ 'setembro' : 8 ,
+ 'set' : 8 ,
+ 'outubro' : 9 ,
+ 'out' : 9 ,
+ 'novembro' : 10 ,
+ 'nov' : 10 ,
+ 'dezembro' : 11 ,
+ 'dez' : 11
+ }[ month.toLowerCase( ) ];
+ } ,
+
+ /**
+ * Function that returns the representation the day of the week [PT Only]
+ *
+ * @method _wDays
+ * @param {Number} index Week's day index
+ * @return {String} The week's day name
+ * @private
+ * @static
+ * @example
+ * console.log( InkDate._wDays(0) ); // Result: Domingo
+ */
+ _wDays: function(index){
+ var _d = ['Domingo', 'Segunda', 'Terça', 'Quarta', 'Quinta', 'Sexta', 'Sábado'];
+ return _d[index];
+ },
+
+ /**
+ * Function that returns day of the week in javascript 1 to 7
+ *
+ * @method _iWeek
+ * @param {String} week Week's day name
+ * @return {Number} The week's day index
+ * @private
+ * @static
+ * @example
+ * console.log( InkDate._iWeek('quarta') ); // Result: 3
+ */
+ _iWeek: function( week )
+ {
+ if ( Number( week ) ) { return +week || 7; }
+ return {
+ 'segunda' : 1 ,
+ 'seg' : 1 ,
+ 'terça' : 2 ,
+ 'ter' : 2 ,
+ 'quarta' : 3 ,
+ 'qua' : 3 ,
+ 'quinta' : 4 ,
+ 'qui' : 4 ,
+ 'sexta' : 5 ,
+ 'sex' : 5 ,
+ 'sábado' : 6 ,
+ 'sáb' : 6 ,
+ 'domingo' : 7 ,
+ 'dom' : 7
+ }[ week.toLowerCase( ) ];
+ },
+
+ /**
+ * Function that returns the number of days of a given month (m) on a given year (y)
+ *
+ * @method _daysInMonth
+ * @param {Number} _m Month
+ * @param {Number} _y Year
+ * @return {Number} Number of days of a give month on a given year
+ * @private
+ * @static
+ * @example
+ * console.log( InkDate._daysInMonth(2,2013) ); // Result: 28
+ */
+ _daysInMonth: function(_m,_y){
+ var nDays;
+
+ if(_m===1 || _m===3 || _m===5 || _m===7 || _m===8 || _m===10 || _m===12)
+ {
+ nDays= 31;
+ }
+ else if ( _m===4 || _m===6 || _m===9 || _m===11)
+ {
+ nDays = 30;
+ }
+ else
+ {
+ if((_y%400===0) || (_y%4===0 && _y%100!==0))
+ {
+ nDays = 29;
+ }
+ else
+ {
+ nDays = 28;
+ }
+ }
+ return nDays;
+ },
+
+ /**
+ * Function that works exactly as php date() function
+ * Works like PHP 5.2.2 PHP Date function
+ *
+ * @method get
+ * @param {String} format - as the string in which the date it will be formatted - mandatory
+ * @param {Date} [_date] - the date to format. If undefined it will do it on now() date. Can receive unix timestamp or a date object
+ * @return {String} Formatted date
+ * @public
+ * @static
+ * @example
+ *
+ */
+ get: function(format, _date){
+ /*jshint maxcomplexity:65 */
+ if(typeof(format) === 'undefined' || format === ''){
+ format = "Y-m-d";
+ }
+
+
+ var iFormat = format.split("");
+ var result = new Array(iFormat.length);
+ var escapeChar = "\\";
+ var jsDate;
+
+ if (typeof(_date) === 'undefined'){
+ jsDate = new Date();
+ } else if (typeof(_date)==='number'){
+ jsDate = new Date(_date*1000);
+ } else {
+ jsDate = new Date(_date);
+ }
+
+ var jsFirstDay, jsThisDay, jsHour;
+ /* This switch is presented in the same order as in php date function (PHP 5.2.2) */
+ for (var i = 0; i < iFormat.length; i++) {
+ switch(iFormat[i]) {
+ case escapeChar:
+ result[i] = iFormat[i+1];
+ i++;
+ break;
+
+
+ /* DAY */
+ case "d": /* Day of the month, 2 digits with leading zeros; ex: 01 to 31 */
+ var jsDay = jsDate.getDate();
+ result[i] = (String(jsDay).length > 1) ? jsDay : "0" + jsDay;
+ break;
+
+ case "D": /* A textual representation of a day, three letters; Seg to Dom */
+ result[i] = this._wDays(jsDate.getDay()).substring(0, 3);
+ break;
+
+ case "j": /* Day of the month without leading zeros; ex: 1 to 31 */
+ result[i] = jsDate.getDate();
+ break;
+
+ case "l": /* A full textual representation of the day of the week; Domingo to Sabado */
+ result[i] = this._wDays(jsDate.getDay());
+ break;
+
+ case "N": /* ISO-8601 numeric representation of the day of the week; 1 (Segunda) to 7 (Domingo) */
+ result[i] = jsDate.getDay() || 7;
+ break;
+
+ case "S": /* English ordinal suffix for the day of the month, 2 characters; st, nd, rd or th. Works well with j */
+ var temp = jsDate.getDate();
+ var suffixes = ["st", "nd", "rd"];
+ var suffix = "";
+
+ if (temp >= 11 && temp <= 13) {
+ result[i] = "th";
+ } else {
+ result[i] = (suffix = suffixes[String(temp).substr(-1) - 1]) ? (suffix) : ("th");
+ }
+ break;
+
+ case "w": /* Numeric representation of the day of the week; 0 (for Sunday) through 6 (for Saturday) */
+ result[i] = jsDate.getDay();
+ break;
+
+ case "z": /* The day of the year (starting from 0); 0 to 365 */
+ jsFirstDay = Date.UTC(jsDate.getFullYear(), 0, 0);
+ jsThisDay = Date.UTC(jsDate.getFullYear(), jsDate.getMonth(), jsDate.getDate());
+ result[i] = Math.floor((jsThisDay - jsFirstDay) / (1000 * 60 * 60 * 24));
+ break;
+
+ /* WEEK */
+ case "W": /* ISO-8601 week number of year, weeks starting on Monday; ex: 42 (the 42nd week in the year) */
+ var jsYearStart = new Date( jsDate.getFullYear( ) , 0 , 1 );
+ jsFirstDay = jsYearStart.getDay() || 7;
+
+ var days = Math.floor( ( jsDate - jsYearStart ) / ( 24 * 60 * 60 * 1000 ) + 1 );
+
+ result[ i ] = Math.ceil( ( days - ( 8 - jsFirstDay ) ) / 7 ) + 1;
+ break;
+
+
+ /* MONTH */
+ case "F": /* A full textual representation of a month, such as Janeiro or Marco; Janeiro a Dezembro */
+ result[i] = this._months(jsDate.getMonth());
+ break;
+
+ case "m": /* Numeric representation of a month, with leading zeros; 01 to 12 */
+ var jsMonth = String(jsDate.getMonth() + 1);
+ result[i] = (jsMonth.length > 1) ? jsMonth : "0" + jsMonth;
+ break;
+
+ case "M": /* A short textual representation of a month, three letters; Jan a Dez */
+ result[i] = this._months(jsDate.getMonth()).substring(0,3);
+ break;
+
+ case "n": /* Numeric representation of a month, without leading zeros; 1 a 12 */
+ result[i] = jsDate.getMonth() + 1;
+ break;
+
+ case "t": /* Number of days in the given month; ex: 28 */
+ result[i] = this._daysInMonth(jsDate.getMonth()+1,jsDate.getYear());
+ break;
+
+ /* YEAR */
+ case "L": /* Whether it's a leap year; 1 if it is a leap year, 0 otherwise. */
+ var jsYear = jsDate.getFullYear();
+ result[i] = (jsYear % 4) ? false : ( (jsYear % 100) ? true : ( (jsYear % 400) ? false : true ) );
+ break;
+
+ case "o": /* ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead. */
+ throw '"o" not implemented!';
+
+ case "Y": /* A full numeric representation of a year, 4 digits; 1999 */
+ result[i] = jsDate.getFullYear();
+ break;
+
+ case "y": /* A two digit representation of a year; 99 */
+ result[i] = String(jsDate.getFullYear()).substring(2);
+ break;
+
+ /* TIME */
+ case "a": /* Lowercase Ante meridiem and Post meridiem; am or pm */
+ result[i] = (jsDate.getHours() < 12) ? "am" : "pm";
+ break;
+
+ case "A": /* Uppercase Ante meridiem and Post meridiem; AM or PM */
+ result[i] = (jsDate.getHours < 12) ? "AM" : "PM";
+ break;
+
+ case "B": /* Swatch Internet time; 000 through 999 */
+ throw '"B" not implemented!';
+
+ case "g": /* 12-hour format of an hour without leading zeros; 1 to 12 */
+ jsHour = jsDate.getHours();
+ result[i] = (jsHour <= 12) ? jsHour : (jsHour - 12);
+ break;
+
+ case "G": /* 24-hour format of an hour without leading zeros; 1 to 23 */
+ result[i] = String(jsDate.getHours());
+ break;
+
+ case "h": /* 12-hour format of an hour with leading zeros; 01 to 12 */
+ jsHour = String(jsDate.getHours());
+ jsHour = (jsHour <= 12) ? jsHour : (jsHour - 12);
+ result[i] = (jsHour.length > 1) ? jsHour : "0" + jsHour;
+ break;
+
+ case "H": /* 24-hour format of an hour with leading zeros; 01 to 24 */
+ jsHour = String(jsDate.getHours());
+ result[i] = (jsHour.length > 1) ? jsHour : "0" + jsHour;
+ break;
+
+ case "i": /* Minutes with leading zeros; 00 to 59 */
+ var jsMinute = String(jsDate.getMinutes());
+ result[i] = (jsMinute.length > 1) ? jsMinute : "0" + jsMinute;
+ break;
+
+ case "s": /* Seconds with leading zeros; 00 to 59; */
+ var jsSecond = String(jsDate.getSeconds());
+ result[i] = (jsSecond.length > 1) ? jsSecond : "0" + jsSecond;
+ break;
+
+ case "u": /* Microseconds */
+ throw '"u" not implemented!';
+
+
+ /* TIMEZONE */
+
+ case "e": /* Timezone identifier */
+ throw '"e" not implemented!';
+
+ case "I": /* "1" if Daylight Savings Time, "0" otherwise. Works only on the northern hemisphere */
+ jsFirstDay = new Date(jsDate.getFullYear(), 0, 1);
+ result[i] = (jsDate.getTimezoneOffset() !== jsFirstDay.getTimezoneOffset()) ? (1) : (0);
+ break;
+
+ case "O": /* Difference to Greenwich time (GMT) in hours */
+ var jsMinZone = jsDate.getTimezoneOffset();
+ var jsMinutes = jsMinZone % 60;
+ jsHour = String(((jsMinZone - jsMinutes) / 60) * -1);
+
+ if (jsHour.charAt(0) !== "-") {
+ jsHour = "+" + jsHour;
+ }
+
+ jsHour = (jsHour.length === 3) ? (jsHour) : (jsHour.replace(/([+\-])(\d)/, "$1" + 0 + "$2"));
+ result[i] = jsHour + jsMinutes + "0";
+ break;
+
+ case "P": /* Difference to Greenwich time (GMT) with colon between hours and minutes */
+ throw '"P" not implemented!';
+
+ case "T": /* Timezone abbreviation */
+ throw '"T" not implemented!';
+
+ case "Z": /* Timezone offset in seconds. The offset for timezones west of UTC is always negative, and for those east of UTC is always positive. */
+ result[i] = jsDate.getTimezoneOffset() * 60;
+ break;
+
+
+ /* FULL DATE/TIME */
+
+ case "c": /* ISO 8601 date */
+ throw '"c" not implemented!';
+
+ case "r": /* RFC 2822 formatted date */
+ var jsDayName = this._wDays(jsDate.getDay()).substr(0, 3);
+ var jsMonthName = this._months(jsDate.getMonth()).substr(0, 3);
+ result[i] = jsDayName + ", " + jsDate.getDate() + " " + jsMonthName + this.get(" Y H:i:s O",jsDate);
+ break;
+
+ case "U": /* Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) */
+ result[i] = Math.floor(jsDate.getTime() / 1000);
+ break;
+
+ default:
+ result[i] = iFormat[i];
+ }
+ }
+
+ return result.join('');
+
+ },
+
+ /**
+ * Functions that works like php date() function but return a date based on the formatted string
+ * Works like PHP 5.2.2 PHP Date function
+ *
+ * @method set
+ * @param {String} [format] As the string in which the date it will be formatted. By default is 'Y-m-d'
+ * @param {String} str_date The date formatted - Mandatory.
+ * @return {Date} Date object based on the formatted date
+ * @public
+ * @static
+ */
+ set : function( format , str_date ) {
+ if ( typeof str_date === 'undefined' ) { return ; }
+ if ( typeof format === 'undefined' || format === '' ) { format = "Y-m-d"; }
+
+ var iFormat = format.split("");
+ var result = new Array( iFormat.length );
+ var escapeChar = "\\";
+ var mList;
+
+ var objIndex = {
+ year : undefined ,
+ month : undefined ,
+ day : undefined ,
+ dayY : undefined ,
+ dayW : undefined ,
+ week : undefined ,
+ hour : undefined ,
+ hourD : undefined ,
+ min : undefined ,
+ sec : undefined ,
+ msec : undefined ,
+ ampm : undefined ,
+ diffM : undefined ,
+ diffH : undefined ,
+ date : undefined
+ };
+
+ var matches = 0;
+
+ /* This switch is presented in the same order as in php date function (PHP 5.2.2) */
+ for ( var i = 0; i < iFormat.length; i++) {
+ switch( iFormat[ i ] ) {
+ case escapeChar:
+ result[i] = iFormat[ i + 1 ];
+ i++;
+ break;
+
+ /* DAY */
+ case "d": /* Day of the month, 2 digits with leading zeros; ex: 01 to 31 */
+ result[ i ] = '(\\d{2})';
+ objIndex.day = { original : i , match : matches++ };
+ break;
+
+ case "j": /* Day of the month without leading zeros; ex: 1 to 31 */
+ result[ i ] = '(\\d{1,2})';
+ objIndex.day = { original : i , match : matches++ };
+ break;
+
+ case "D": /* A textual representation of a day, three letters; Seg to Dom */
+ result[ i ] = '([\\wá]{3})';
+ objIndex.dayW = { original : i , match : matches++ };
+ break;
+
+ case "l": /* A full textual representation of the day of the week; Domingo to Sabado */
+ result[i] = '([\\wá]{5,7})';
+ objIndex.dayW = { original : i , match : matches++ };
+ break;
+
+ case "N": /* ISO-8601 numeric representation of the day of the week; 1 (Segunda) to 7 (Domingo) */
+ result[ i ] = '(\\d)';
+ objIndex.dayW = { original : i , match : matches++ };
+ break;
+
+ case "w": /* Numeric representation of the day of the week; 0 (for Sunday) through 6 (for Saturday) */
+ result[ i ] = '(\\d)';
+ objIndex.dayW = { original : i , match : matches++ };
+ break;
+
+ case "S": /* English ordinal suffix for the day of the month, 2 characters; st, nd, rd or th. Works well with j */
+ result[ i ] = '\\w{2}';
+ break;
+
+ case "z": /* The day of the year (starting from 0); 0 to 365 */
+ result[ i ] = '(\\d{1,3})';
+ objIndex.dayY = { original : i , match : matches++ };
+ break;
+
+ /* WEEK */
+ case "W": /* ISO-8601 week number of year, weeks starting on Monday; ex: 42 (the 42nd week in the year) */
+ result[ i ] = '(\\d{1,2})';
+ objIndex.week = { original : i , match : matches++ };
+ break;
+
+ /* MONTH */
+ case "F": /* A full textual representation of a month, such as Janeiro or Marco; Janeiro a Dezembro */
+ result[ i ] = '([\\wç]{4,9})';
+ objIndex.month = { original : i , match : matches++ };
+ break;
+
+ case "M": /* A short textual representation of a month, three letters; Jan a Dez */
+ result[ i ] = '(\\w{3})';
+ objIndex.month = { original : i , match : matches++ };
+ break;
+
+ case "m": /* Numeric representation of a month, with leading zeros; 01 to 12 */
+ result[ i ] = '(\\d{2})';
+ objIndex.month = { original : i , match : matches++ };
+ break;
+
+ case "n": /* Numeric representation of a month, without leading zeros; 1 a 12 */
+ result[ i ] = '(\\d{1,2})';
+ objIndex.month = { original : i , match : matches++ };
+ break;
+
+ case "t": /* Number of days in the given month; ex: 28 */
+ result[ i ] = '\\d{2}';
+ break;
+
+ /* YEAR */
+ case "L": /* Whether it's a leap year; 1 if it is a leap year, 0 otherwise. */
+ result[ i ] = '\\w{4,5}';
+ break;
+
+ case "o": /* ISO-8601 year number. This has the same value as Y, except that if the ISO week number (W) belongs to the previous or next year, that year is used instead. */
+ throw '"o" not implemented!';
+
+ case "Y": /* A full numeric representation of a year, 4 digits; 1999 */
+ result[ i ] = '(\\d{4})';
+ objIndex.year = { original : i , match : matches++ };
+ break;
+
+ case "y": /* A two digit representation of a year; 99 */
+ result[ i ] = '(\\d{2})';
+ if ( typeof objIndex.year === 'undefined' || iFormat[ objIndex.year.original ] !== 'Y' ) {
+ objIndex.year = { original : i , match : matches++ };
+ }
+ break;
+
+ /* TIME */
+ case "a": /* Lowercase Ante meridiem and Post meridiem; am or pm */
+ result[ i ] = '(am|pm)';
+ objIndex.ampm = { original : i , match : matches++ };
+ break;
+
+ case "A": /* Uppercase Ante meridiem and Post meridiem; AM or PM */
+ result[ i ] = '(AM|PM)';
+ objIndex.ampm = { original : i , match : matches++ };
+ break;
+
+ case "B": /* Swatch Internet time; 000 through 999 */
+ throw '"B" not implemented!';
+
+ case "g": /* 12-hour format of an hour without leading zeros; 1 to 12 */
+ result[ i ] = '(\\d{1,2})';
+ objIndex.hourD = { original : i , match : matches++ };
+ break;
+
+ case "G": /* 24-hour format of an hour without leading zeros; 1 to 23 */
+ result[ i ] = '(\\d{1,2})';
+ objIndex.hour = { original : i , match : matches++ };
+ break;
+
+ case "h": /* 12-hour format of an hour with leading zeros; 01 to 12 */
+ result[ i ] = '(\\d{2})';
+ objIndex.hourD = { original : i , match : matches++ };
+ break;
+
+ case "H": /* 24-hour format of an hour with leading zeros; 01 to 24 */
+ result[ i ] = '(\\d{2})';
+ objIndex.hour = { original : i , match : matches++ };
+ break;
+
+ case "i": /* Minutes with leading zeros; 00 to 59 */
+ result[ i ] = '(\\d{2})';
+ objIndex.min = { original : i , match : matches++ };
+ break;
+
+ case "s": /* Seconds with leading zeros; 00 to 59; */
+ result[ i ] = '(\\d{2})';
+ objIndex.sec = { original : i , match : matches++ };
+ break;
+
+ case "u": /* Microseconds */
+ throw '"u" not implemented!';
+
+ /* TIMEZONE */
+ case "e": /* Timezone identifier */
+ throw '"e" not implemented!';
+
+ case "I": /* "1" if Daylight Savings Time, "0" otherwise. Works only on the northern hemisphere */
+ result[i] = '\\d';
+ break;
+
+ case "O": /* Difference to Greenwich time (GMT) in hours */
+ result[ i ] = '([-+]\\d{4})';
+ objIndex.diffH = { original : i , match : matches++ };
+ break;
+
+ case "P": /* Difference to Greenwich time (GMT) with colon between hours and minutes */
+ throw '"P" not implemented!';
+
+ case "T": /* Timezone abbreviation */
+ throw '"T" not implemented!';
+
+ case "Z": /* Timezone offset in seconds. The offset for timezones west of UTC is always negative, and for those east of UTC is always positive. */
+ result[ i ] = '(\\-?\\d{1,5})';
+ objIndex.diffM = { original : i , match : matches++ };
+ break;
+
+ /* FULL DATE/TIME */
+ case "c": /* ISO 8601 date */
+ throw '"c" not implemented!';
+
+ case "r": /* RFC 2822 formatted date */
+ result[ i ] = '([\\wá]{3}, \\d{1,2} \\w{3} \\d{4} \\d{2}:\\d{2}:\\d{2} [+\\-]\\d{4})';
+ objIndex.date = { original : i , match : matches++ };
+ break;
+
+ case "U": /* Seconds since the Unix Epoch (January 1 1970 00:00:00 GMT) */
+ result[ i ] = '(\\d{1,13})';
+ objIndex.date = { original : i , match : matches++ };
+ break;
+
+ default:
+ result[ i ] = iFormat[ i ];
+ }
+ }
+
+ var pattr = new RegExp( result.join('') );
+
+ try {
+ mList = str_date.match( pattr );
+ if ( !mList ) { return; }
+ }
+ catch ( e ) { return ; }
+
+ var _haveDatetime = typeof objIndex.date !== 'undefined';
+
+ var _haveYear = typeof objIndex.year !== 'undefined';
+
+ var _haveYDay = typeof objIndex.dayY !== 'undefined';
+
+ var _haveDay = typeof objIndex.day !== 'undefined';
+ var _haveMonth = typeof objIndex.month !== 'undefined';
+ var _haveMonthDay = _haveMonth && _haveDay;
+ var _haveOnlyDay = !_haveMonth && _haveDay;
+
+ var _haveWDay = typeof objIndex.dayW !== 'undefined';
+ var _haveWeek = typeof objIndex.week !== 'undefined';
+ var _haveWeekWDay = _haveWeek && _haveWDay;
+ var _haveOnlyWDay = !_haveWeek && _haveWDay;
+
+ var _validDate = _haveYDay || _haveMonthDay || !_haveYear && _haveOnlyDay || _haveWeekWDay || !_haveYear && _haveOnlyWDay;
+ var _noDate = !_haveYear && !_haveYDay && !_haveDay && !_haveMonth && !_haveWDay && !_haveWeek;
+
+ var _haveHour12 = typeof objIndex.hourD !== 'undefined' && typeof objIndex.ampm !== 'undefined';
+ var _haveHour24 = typeof objIndex.hour !== 'undefined';
+ var _haveHour = _haveHour12 || _haveHour24;
+
+ var _haveMin = typeof objIndex.min !== 'undefined';
+ var _haveSec = typeof objIndex.sec !== 'undefined';
+ var _haveMSec = typeof objIndex.msec !== 'undefined';
+
+ var _haveMoreM = !_noDate || _haveHour;
+ var _haveMoreS = _haveMoreM || _haveMin;
+
+ var _haveDiffM = typeof objIndex.diffM !== 'undefined';
+ var _haveDiffH = typeof objIndex.diffH !== 'undefined';
+ //var _haveGMT = _haveDiffM || _haveDiffH;
+ var hour;
+ var min;
+
+ if ( _haveDatetime ) {
+ if ( iFormat[ objIndex.date.original ] === 'U' ) {
+ return new Date( +mList[ objIndex.date.match + 1 ] * 1000 );
+ }
+
+ var dList = mList[ objIndex.date.match + 1 ].match( /\w{3}, (\d{1,2}) (\w{3}) (\d{4}) (\d{2}):(\d{2}):(\d{2}) ([+\-]\d{4})/ );
+ hour = +dList[ 4 ] + ( +dList[ 7 ].slice( 0 , 3 ) );
+ min = +dList[ 5 ] + ( dList[ 7 ].slice( 0 , 1 ) + dList[ 7 ].slice( 3 ) ) / 100 * 60;
+
+ return new Date( dList[ 3 ] , this._iMonth( dList[ 2 ] ) , dList[ 1 ] , hour , min , dList[ 6 ] );
+ }
+
+ var _d = new Date( );
+ var year;
+ var month;
+ var day;
+ var sec;
+ var msec;
+ var gmt;
+
+ if ( !_validDate && !_noDate ) { return ; }
+
+ if ( _validDate ) {
+ if ( _haveYear ) {
+ var _y = _d.getFullYear( ) - 50 + '';
+ year = mList[ objIndex.year.match + 1 ];
+ if ( iFormat[ objIndex.year.original ] === 'y' ) {
+ year = +_y.slice( 0 , 2 ) + ( year >= ( _y ).slice( 2 ) ? 0 : 1 ) + year;
+ }
+ } else {
+ year = _d.getFullYear();
+ }
+
+ if ( _haveYDay ) {
+ month = 0;
+ day = mList[ objIndex.dayY.match + 1 ];
+ } else if ( _haveDay ) {
+ if ( _haveMonth ) {
+ month = this._iMonth( mList[ objIndex.month.match + 1 ] );
+ } else {
+ month = _d.getMonth( );
+ }
+
+ day = mList[ objIndex.day.match + 1 ];
+ } else {
+ month = 0;
+
+ var week;
+ if ( _haveWeek ) {
+ week = mList[ objIndex.week.match + 1 ];
+ } else {
+ week = this.get( 'W' , _d );
+ }
+
+ day = ( week - 2 ) * 7 + ( 8 - ( ( new Date( year , 0 , 1 ) ).getDay( ) || 7 ) ) + this._iWeek( mList[ objIndex.week.match + 1 ] );
+ }
+
+ if ( month === 0 && day > 31 ) {
+ var aux = new Date( year , month , day );
+ month = aux.getMonth( );
+ day = aux.getDate( );
+ }
+ }
+ else {
+ year = _d.getFullYear( );
+ month = _d.getMonth( );
+ day = _d.getDate( );
+ }
+
+ if ( _haveHour12 ) { hour = +mList[ objIndex.hourD.match + 1 ] + ( mList[ objIndex.ampm.match + 1 ] === 'pm' ? 12 : 0 ); }
+ else if ( _haveHour24 ) { hour = mList[ objIndex.hour.match + 1 ]; }
+ else if ( _noDate ) { hour = _d.getHours( ); }
+ else { hour = '00'; }
+
+ if ( _haveMin ) { min = mList[ objIndex.min.match + 1 ]; }
+ else if ( !_haveMoreM ) { min = _d.getMinutes( ); }
+ else { min = '00'; }
+
+ if ( _haveSec ) { sec = mList[ objIndex.sec.match + 1 ]; }
+ else if ( !_haveMoreS ) { sec = _d.getSeconds( ); }
+ else { sec = '00'; }
+
+ if ( _haveMSec ) { msec = mList[ objIndex.msec.match + 1 ]; }
+ else { msec = '000'; }
+
+ if ( _haveDiffH ) { gmt = mList[ objIndex.diffH.match + 1 ]; }
+ else if ( _haveDiffM ) { gmt = String( -1 * mList[ objIndex.diffM.match + 1 ] / 60 * 100 ).replace( /^(\d)/ , '+$1' ).replace( /(^[\-+])(\d{3}$)/ , '$10$2' ); }
+ else { gmt = '+0000'; }
+
+ return new Date( year, month, day, hour, min, sec );
+ }
+ };
+
+
+ return InkDate;
+
+});
+
+/**
+ * @module Ink.Util.Dumper_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.Util.Dumper', '1', [], function() {
+
+ 'use strict';
+
+ /**
+ * Dump/Profiling Utilities
+ *
+ * @class Ink.Util.Dumper
+ * @version 1
+ * @static
+ */
+ var Dumper = {
+
+ /**
+ * Hex code for the 'tab'
+ *
+ * @property _tab
+ * @type {String}
+ * @private
+ * @readOnly
+ * @static
+ *
+ */
+ _tab: '\xA0\xA0\xA0\xA0',
+
+ /**
+ * Function that returns the argument passed formatted
+ *
+ * @method _formatParam
+ * @param {Mixed} param
+ * @return {String} The argument passed formatted
+ * @private
+ * @static
+ */
+ _formatParam: function(param)
+ {
+ var formated = '';
+
+ switch(typeof(param)) {
+ case 'string':
+ formated = '(string) '+param;
+ break;
+ case 'number':
+ formated = '(number) '+param;
+ break;
+ case 'boolean':
+ formated = '(boolean) '+param;
+ break;
+ case 'object':
+ if(param !== null) {
+ if(param.constructor === Array) {
+ formated = 'Array \n{\n' + this._outputFormat(param, 0) + '\n}';
+ } else {
+ formated = 'Object \n{\n' + this._outputFormat(param, 0) + '\n}';
+ }
+ } else {
+ formated = 'null';
+ }
+ break;
+ default:
+ formated = false;
+ }
+
+ return formated;
+ },
+
+ /**
+ * Function that returns the tabs concatenated
+ *
+ * @method _getTabs
+ * @param {Number} numberOfTabs Number of Tabs
+ * @return {String} Tabs concatenated
+ * @private
+ * @static
+ */
+ _getTabs: function(numberOfTabs)
+ {
+ var tabs = '';
+ for(var _i = 0; _i < numberOfTabs; _i++) {
+ tabs += this._tab;
+ }
+ return tabs;
+ },
+
+ /**
+ * Function that formats the parameter to display
+ *
+ * @method _outputFormat
+ * @param {Any} param
+ * @param {Number} dim
+ * @return {String} The parameter passed formatted to displat
+ * @private
+ * @static
+ */
+ _outputFormat: function(param, dim)
+ {
+ var formated = '';
+ //var _strVal = false;
+ var _typeof = false;
+ for(var key in param) {
+ if(param[key] !== null) {
+ if(typeof(param[key]) === 'object' && (param[key].constructor === Array || param[key].constructor === Object)) {
+ if(param[key].constructor === Array) {
+ _typeof = 'Array';
+ } else if(param[key].constructor === Object) {
+ _typeof = 'Object';
+ }
+ formated += this._tab + this._getTabs(dim) + '[' + key + '] => '+_typeof+'\n';
+ formated += this._tab + this._getTabs(dim) + '{\n';
+ formated += this._outputFormat(param[key], dim + 1) + this._tab + this._getTabs(dim) + '}\n';
+ } else if(param[key].constructor === Function) {
+ continue;
+ } else {
+ formated = formated + this._tab + this._getTabs(dim) + '[' + key + '] => ' + param[key] + '\n';
+ }
+ } else {
+ formated = formated + this._tab + this._getTabs(dim) + '[' + key + '] => null \n';
+ }
+ }
+ return formated;
+ },
+
+ /**
+ * Print variable structure. Can be passed an output target
+ *
+ * @method printDump
+ * @param {Object|String|Boolean} param
+ * @param {optional String|Object} target (can be an element ID or an element)
+ * @public
+ * @static
+ */
+ printDump: function(param, target)
+ {
+ /*jshint evil:true */
+ if(!target || typeof(target) === 'undefined') {
+ document.write('
` element to add pagination markup to, or an `Ink.UI.Pagination` instance to use.
+ * @param {Function} [options.onChange] Callback for when the page is changed.
+ */
+ var Carousel = function(selector, options) {
+ this._handlers = {
+ paginationChange: Ink.bindMethod(this, '_onPaginationChange'),
+ windowResize: InkEvent.throttle(Ink.bindMethod(this, 'refit'), 200)
+ };
+
+ InkEvent.observe(window, 'resize', this._handlers.windowResize);
+
+ var element = this._element = Common.elOrSelector(selector, '1st argument');
+
+ var opts = this._options = Common.options({
+ autoAdvance: ['Integer', 0],
+ axis: ['String', 'x'],
+ initialPage: ['Integer', 0],
+ spaceAfterLastSlide: ['Boolean', true],
+ hideLast: ['Boolean', false],
+ center: ['Boolean', false],
+ keyboardSupport:['Boolean', false],
+ pagination: ['String', null],
+ onChange: ['Function', null],
+ swipe: ['Boolean', true]
+ // TODO exponential swipe
+ // TODO specify break point for next page when moving finger
+ }, options || {}, element, this);
+
+ this._isY = (opts.axis === 'y');
+
+ var ulEl = Ink.s('ul.stage', element);
+ this._ulEl = ulEl;
+
+ InkElement.removeTextNodeChildren(ulEl);
+
+ if (this._options.pagination == null) {
+ this._currentPage = this._options.initialPage;
+ }
+
+ this.refit(); // recalculate this._numPages
+
+ if (this._isY) {
+ // Override white-space: no-wrap which is only necessary to make sure horizontal stuff stays horizontal, but breaks stuff intended to be vertical.
+ this._ulEl.style.whiteSpace = 'normal';
+ }
+
+ if (opts.swipe) {
+ InkEvent.observe(element, 'touchstart', Ink.bindMethod(this, '_onTouchStart'));
+ InkEvent.observe(element, 'touchmove', Ink.bindMethod(this, '_onTouchMove'));
+ InkEvent.observe(element, 'touchend', Ink.bindMethod(this, '_onTouchEnd'));
+ }
+
+ this._setUpPagination();
+ this._setUpAutoAdvance();
+ this._setUpHider();
+ };
+
+ Carousel.prototype = {
+ /**
+ * Measure the carousel once again, adjusting the involved elements'
+ * sizes. Called automatically when the window resizes, in order to
+ * cater for changes from responsive media queries, for instance.
+ *
+ * @method refit
+ * @public
+ */
+ refit: function() {
+ var _isY = this._isY;
+
+ var size = function (elm, perpendicular) {
+ if (!perpendicular) {
+ return InkElement.outerDimensions(elm)[_isY ? 1 : 0];
+ } else {
+ return InkElement.outerDimensions(elm)[_isY ? 0 : 1];
+ }
+ };
+
+ this._liEls = Ink.ss('li.slide', this._ulEl);
+ var numSlides = this._liEls.length;
+
+ var contRect = this._ulEl.getBoundingClientRect();
+ this._ctnLength = _isY ? contRect.bottom - contRect.top : contRect.right - contRect.left;
+ this._elLength = size(this._liEls[0]);
+ this._slidesPerPage = Math.floor( this._ctnLength / this._elLength ) || 1;
+
+ var numPages = Math.ceil( numSlides / this._slidesPerPage );
+ var numPagesChanged = this._numPages !== numPages;
+ this._numPages = numPages;
+ this._deltaLength = this._slidesPerPage * this._elLength;
+
+ this._center();
+ this._updateHider();
+ this._IE7();
+
+ if (this._pagination && numPagesChanged) {
+ this._pagination.setSize(this._numPages);
+ }
+ this.setPage(limitRange(this.getPage(), 0, this._numPages));
+ },
+
+ _setUpPagination: function () {
+ if (this._options.pagination) {
+ if (Common.isDOMElement(this._options.pagination) ||
+ typeof this._options.pagination === 'string') {
+ // if dom element or css selector string...
+ this._pagination = new Pagination(this._options.pagination, {
+ size: this._numPages,
+ onChange: this._handlers.paginationChange
+ });
+ } else {
+ // assumes instantiated pagination
+ this._pagination = this._options.pagination;
+ this._pagination._options.onChange = this._handlers.paginationChange;
+ this._pagination.setSize(this._numPages);
+ }
+ this._pagination.setCurrent(this._options.initialPage || 0);
+ } else {
+ this._currentPage = this._options.initialPage || 0;
+ }
+ },
+
+ _setUpAutoAdvance: function () {
+ if (!this._options.autoAdvance) { return; }
+ var self = this;
+
+ setTimeout(function autoAdvance() {
+ self.nextPage(true /* wrap */);
+ setTimeout(autoAdvance, self._options.autoAdvance);
+ }, this._options.autoAdvance);
+ },
+
+ _setUpHider: function () {
+ if (this._options.hideLast) {
+ var hiderEl = InkElement.create('div', {
+ className: 'hider',
+ insertBottom: this._element
+ });
+ hiderEl.style.position = 'absolute';
+ hiderEl.style[ this._isY ? 'left' : 'top' ] = '0'; // fix to top..
+ hiderEl.style[ this._isY ? 'right' : 'bottom' ] = '0'; // and bottom...
+ hiderEl.style[ this._isY ? 'bottom' : 'right' ] = '0'; // and move to the end.
+ this._hiderEl = hiderEl;
+ }
+ },
+
+ _center: function() {
+ if (!this._options.center) { return; }
+ var gap = Math.floor( (this._ctnLength - (this._elLength * this._slidesPerPage) ) / 2 );
+
+ var pad;
+ if (this._isY) {
+ pad = [gap, 'px 0'];
+ } else {
+ pad = ['0 ', gap, 'px'];
+ }
+
+ this._ulEl.style.padding = pad.join('');
+ },
+
+ _updateHider: function() {
+ if (!this._hiderEl) { return; }
+ if (this.getPage() === 0) {
+ var gap = Math.floor( this._ctnLength - (this._elLength * this._slidesPerPage) );
+ if (this._options.center) {
+ gap /= 2;
+ }
+ this._hiderEl.style[ this._isY ? 'height' : 'width' ] = gap + 'px';
+ } else {
+ this._hiderEl.style[ this._isY ? 'height' : 'width' ] = '0px';
+ }
+ },
+
+ /**
+ * Refit stuff for IE7 because it won't support inline-block.
+ *
+ * @method _IE7
+ * @private
+ */
+ _IE7: function () {
+ if (Browser.IE && '' + Browser.version.split('.')[0] === '7') {
+ // var numPages = this._numPages;
+ var slides = Ink.ss('li.slide', this._ulEl);
+ var stl = function (prop, val) {slides[i].style[prop] = val; };
+ for (var i = 0, len = slides.length; i < len; i++) {
+ stl('position', 'absolute');
+ stl(this._isY ? 'top' : 'left', (i * this._elLength) + 'px');
+ }
+ }
+ },
+
+ _onTouchStart: function (event) {
+ if (event.touches.length > 1) { return; }
+
+ this._swipeData = {
+ x: InkEvent.pointerX(event),
+ y: InkEvent.pointerY(event),
+ lastUlPos: null
+ };
+
+ var ulRect = this._ulEl.getBoundingClientRect();
+
+ this._swipeData.inUlX = this._swipeData.x - ulRect.left;
+ this._swipeData.inUlY = this._swipeData.y - ulRect.top;
+
+ setTransitionProperty(this._ulEl, 'none');
+
+ this._touchMoveIsFirstTouchMove = true;
+
+ // InkEvent.stopDefault(event);
+ InkEvent.stopPropagation(event);
+ },
+
+ _onTouchMove: function (event) {
+ if (event.touches.length > 1) { return; /* multitouch event, not my problem. */ }
+
+ var pointerX = InkEvent.pointerX(event);
+ var pointerY = InkEvent.pointerY(event);
+
+ var deltaY = Math.abs(pointerY - this._swipeData.y);
+ var deltaX = Math.abs(pointerX - this._swipeData.x);
+
+ if (this._touchMoveIsFirstTouchMove) {
+ this._touchMoveIsFirstTouchMove = undefined;
+ this._scrolling = this._isY ?
+ deltaX > deltaY :
+ deltaY > deltaX ;
+
+ if (!this._scrolling) {
+ this._onAnimationFrame();
+ }
+ }
+
+ if (!this._scrolling && this._swipeData) {
+ InkEvent.stopDefault(event);
+
+ this._swipeData.pointerPos = this._isY ? pointerY : pointerX;
+ }
+
+ InkEvent.stopPropagation(event);
+ },
+
+ _onAnimationFrame: function () {
+ var swipeData = this._swipeData;
+
+ if (!swipeData || this._scrolling || this._touchMoveIsFirstTouchMove) { return; }
+
+ var elRect = this._element.getBoundingClientRect();
+
+ var newPos;
+
+ if (!this._isY) {
+ newPos = swipeData.pointerPos - swipeData.inUlX - elRect.left;
+ } else {
+ newPos = swipeData.pointerPos - swipeData.inUlY - elRect.top;
+ }
+
+ this._ulEl.style[this._isY ? 'top' : 'left'] = newPos + 'px';
+
+ swipeData.lastUlPos = newPos;
+
+ requestAnimationFrame(Ink.bindMethod(this, '_onAnimationFrame'));
+ },
+
+ _onTouchEnd: function (event) {
+ if (this._swipeData && this._swipeData.pointerPos && !this._scrolling && !this._touchMoveIsFirstTouchMove) {
+ var snapToNext = 0.1; // swipe 10% of the way to change page
+ var progress = - this._swipeData.lastUlPos;
+
+ var curPage = this.getPage();
+ var estimatedPage = progress / this._elLength / this._slidesPerPage;
+
+ if (Math.round(estimatedPage) === curPage) {
+ var diff = estimatedPage - curPage;
+ if (Math.abs(diff) > snapToNext) {
+ diff = diff > 0 ? 1 : -1;
+ curPage += diff;
+ }
+ } else {
+ curPage = Math.round(estimatedPage);
+ }
+
+ this.setPage(curPage);
+
+ InkEvent.stopPropagation(event);
+ InkEvent.stopDefault(event);
+ }
+
+ setTransitionProperty(this._ulEl, null /* transition: left, top */);
+ this._swipeData = null;
+ this._touchMoveIsFirstTouchMove = undefined;
+ this._scrolling = undefined;
+ },
+
+ _onPaginationChange: function(pgn) {
+ this._setPage(pgn.getCurrent());
+ },
+
+ /**
+ * Get the currently displayed page.
+ * @method getPage
+ **/
+ getPage: function () {
+ if (this._pagination) {
+ return this._pagination.getCurrent();
+ } else {
+ return this._currentPage || 0;
+ }
+ },
+
+ /**
+ * Set the current page to `page`
+ * @method setPage
+ * @param page
+ * @param [wrap=false] Wrap over the first and last pages. (For example, going to the 5th page when there are only 4 pages goes to the 1st page)
+ **/
+ setPage: function (page, wrap) {
+ if (wrap) {
+ // Pages outside the range [0..this._numPages] are wrapped.
+ page = page % this._numPages;
+ if (page < 0) { page = this._numPages - page; }
+ }
+ page = limitRange(page, 0, this._numPages - 1);
+
+ if (this._pagination) {
+ this._pagination.setCurrent(page);
+ } else {
+ this._setPage(page);
+ }
+ },
+
+ _setPage: function (page) {
+ var _lengthToGo = page * this._deltaLength;
+ var isLastPage = page === (this._numPages - 1);
+
+ if (!this._options.spaceAfterLastSlide && isLastPage && page > 0) {
+ var _itemsInLastPage = this._liEls.length - (page * this._slidesPerPage);
+ if(_itemsInLastPage < this._slidesPerPage) {
+ _lengthToGo = ((page - 1) * this._deltaLength) + (_itemsInLastPage * this._elLength);
+ }
+ }
+
+ this._ulEl.style[ this._isY ? 'top' : 'left'] =
+ ['-', _lengthToGo, 'px'].join('');
+
+ if (this._options.onChange) {
+ this._options.onChange.call(this, page);
+ }
+
+ this._currentPage = page;
+
+ this._updateHider();
+ },
+
+ /**
+ * Change to the next page
+ * @method nextPage
+ * @param {Boolean} [wrap=false] If true, going to the page after the last page takes you to the first page.
+ **/
+ nextPage: function (wrap) {
+ this.setPage(this.getPage() + 1, wrap);
+ },
+
+ /**
+ * Change to the previous page
+ * @method previousPage
+ * @param {Boolean} [wrap=false] If true, going to the page after the last page takes you to the first page.
+ **/
+ previousPage: function (wrap) { this.setPage(this.getPage() - 1, wrap); }
+ };
+
+ function setTransitionProperty(el, newTransition) {
+ el.style.transitionProperty =
+ el.style.oTransitionProperty =
+ el.style.msTransitionProperty =
+ el.style.mozTransitionProperty =
+ el.style.webkitTransitionProperty = newTransition;
+ }
+
+ return Carousel;
+
+});
+
+/**
+ * @module Ink.UI.Close_1
+ * @author inkdev AT sapo.pt
+ */
+Ink.createModule('Ink.UI.Close', '1', ['Ink.Dom.Event_1','Ink.Dom.Element_1'], function(InkEvent, InkElement) {
+ 'use strict';
+
+ /**
+ * Subscribes clicks on the document.body. If and only if you clicked on an element
+ * having class "ink-close" or "ink-dismiss", will go up the DOM hierarchy looking for an element with any
+ * of the following classes: "ink-alert", "ink-alert-block".
+ * If it is found, it is removed from the DOM.
+ *
+ * One should call close once per page (full page refresh).
+ *
+ * @class Ink.UI.Close
+ * @constructor
+ * @example
+ *
+ */
+ var Close = function() {
+ InkEvent.observe(document.body, 'click', function(ev) {
+ var el = InkEvent.element(ev);
+
+ el = InkElement.findUpwardsByClass(el, 'ink-close') ||
+ InkElement.findUpwardsByClass(el, 'ink-dismiss');
+
+ if (!el) {
+ return; // ink-close or ink-dismiss class not found
+ }
+
+ var toRemove = el;
+ toRemove = InkElement.findUpwardsByClass(el, 'ink-alert') ||
+ InkElement.findUpwardsByClass(el, 'ink-alert-block');
+
+ if (toRemove) {
+ InkEvent.stop(ev);
+ InkElement.remove(toRemove);
+ }
+ });
+ };
+
+ return Close;
+
+});
+
+/**
+ * @module Ink.UI.Common_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.UI.Common', '1', ['Ink.Dom.Element_1', 'Ink.Net.Ajax_1','Ink.Dom.Css_1','Ink.Dom.Selector_1','Ink.Util.Url_1'], function(InkElement, Ajax,Css,Selector,Url) {
+
+ 'use strict';
+
+ var instances = {};
+ var lastIdNum = 0;
+ var nothing = {} /* a marker, for reference comparison. */;
+
+ var keys = Object.keys || function (obj) {
+ var ret = [];
+ for (var k in obj) if (obj.hasOwnProperty(k)) {
+ ret.push(k);
+ }
+ return ret;
+ };
+
+ /**
+ * The Common class provides auxiliar methods to ease some of the most common/repetitive UI tasks.
+ *
+ * @class Ink.UI.Common
+ * @version 1
+ * @static
+ */
+ var Common = {
+
+ /**
+ * Supported Ink Layouts
+ *
+ * @property Layouts
+ * @type Object
+ * @readOnly
+ */
+ Layouts: {
+ SMALL: 'small',
+ MEDIUM: 'medium',
+ LARGE: 'large'
+ },
+
+ /**
+ * Method to check if an item is a valid DOM Element.
+ *
+ * @method isDOMElement
+ * @static
+ * @param {Mixed} o The object to be checked.
+ * @return {Boolean} True if it's a valid DOM Element.
+ * @example
+ * var el = Ink.s('#element');
+ * if( Ink.UI.Common.isDOMElement( el ) === true ){
+ * // It is a DOM Element.
+ * } else {
+ * // It is NOT a DOM Element.
+ * }
+ */
+ isDOMElement: function(o) {
+ return o && typeof o === 'object' && 'nodeType' in o && o.nodeType === 1;
+ },
+
+ /**
+ * Method to check if an item is a valid integer.
+ *
+ * @method isInteger
+ * @static
+ * @param {Mixed} n The value to be checked.
+ * @return {Boolean} True if 'n' is a valid integer.
+ * @example
+ * var value = 1;
+ * if( Ink.UI.Common.isInteger( value ) === true ){
+ * // It is an integer.
+ * } else {
+ * // It is NOT an integer.
+ * }
+ */
+ isInteger: function(n) {
+ return (typeof n === 'number' && n % 1 === 0);
+ },
+
+ /**
+ * Method to get a DOM Element. The first parameter should be either a DOM Element or a valid CSS Selector.
+ * If not, then it will throw an exception. Otherwise, it returns a DOM Element.
+ *
+ * @method elOrSelector
+ * @static
+ * @param {DOMElement|String} elOrSelector DOM Element or CSS Selector
+ * @param {String} fieldName This field is used in the thrown Exception to identify the parameter.
+ * @return {DOMElement} Returns the DOMElement passed or the first result of the CSS Selector. Otherwise it throws an exception.
+ * @example
+ * // In case there are several .myInput, it will retrieve the first found
+ * var el = Ink.UI.Common.elOrSelector('.myInput','My Input');
+ */
+ elOrSelector: function(elOrSelector, fieldName) {
+ if (!this.isDOMElement(elOrSelector)) {
+ var t = Selector.select(elOrSelector);
+ if (t.length === 0) { throw new TypeError(fieldName + ' must either be a DOM Element or a selector expression!\nThe script element must also be after the DOM Element itself.'); }
+ return t[0];
+ }
+ return elOrSelector;
+ },
+
+ /**
+ * Does the same as `elOrSelector` but returns an array of elements.
+ *
+ * see elOrSelector
+ *
+ * @method elsOrSelector
+ *
+ * @static
+ *
+ * @param ... (See elOrSelector's params)
+ * @param {Boolean} required If true, accept an empty array as output.
+ * @return {Array} The selected DOM Elements.
+ * @example
+ * var elements = Ink.UI.Common.elsOrSelector('input.my-inputs', 'My Input');
+ */
+ elsOrSelector: function(elsOrSelector, fieldName, required) {
+ var ret;
+ if (typeof elsOrSelector === 'string') {
+ ret = Selector.select(elsOrSelector);
+ } else if (Common.isDOMElement(elsOrSelector)) {
+ ret = [elsOrSelector];
+ } else if (elsOrSelector && typeof elsOrSelector === 'object' && typeof elsOrSelector.length === 'number') {
+ ret = elsOrSelector;
+ }
+
+ if (ret && ret.length) {
+ return ret;
+ } else {
+ if (required || arguments.length === 2) {
+ throw new TypeError(fieldName + ' must either be a DOM Element, an Array of elements, or a selector expression!\nThe script element must also be after the DOM Element itself.');
+ } else {
+ return [];
+ }
+ }
+ },
+
+ /**
+ * Get options from an "options" object and the Element's data attributes.
+ *
+ * The element's data attributes take precedence.
+ *
+ * Values from the element's data-atrributes are coerced into the required type.
+ *
+ * Mainly for Ink UI.* modules
+ *
+ * @method options
+ *
+ * @param {Object} [fieldId=''] Tag used in thrown exceptions: ": ..."
+ * @param {Object} defaults Object with arrays of option defaults
+ * @param {Object} overrides Object containing options given from user through JavaScript
+ * @param {DOMElement} [element] Element with data-attributes
+ *
+ * @example
+ *
+ * this._options = Ink.UI.Common.options('MyComponent', {
+ * 'anobject': ['Object', null], // Defaults to null
+ * 'target': ['Element', null],
+ * 'stuff': ['Number', 0.1],
+ * 'stuff2': ['Integer', 0],
+ * 'doKickFlip': ['Boolean', false],
+ * 'targets': ['Elements'], // Required option since no default was given
+ * 'onClick': ['Function', null]
+ * }, options || {}, elm)
+ *
+ * @example
+ *
+ * ### Note about booleans
+ *
+ * Here is how options are read from the markup
+ * data-attributes, for several values`data-a-boolean`.
+ *
+ * Options considered true:
+ *
+ * - `data-a-boolean="true"`
+ * - (Every other value which is not on the list below.)
+ *
+ * Options considered false:
+ *
+ * - `data-a-boolean="false"`
+ * - `data-a-boolean=""`
+ * - `data-a-boolean`
+ *
+ * Options which go to default:
+ *
+ * - (no attribute). When `data-a-boolean` is ommitted, the
+ * option is not considered true nor false, and as such
+ * defaults to what is in the `defaults` argument.
+ *
+ **/
+ options: function (fieldId, defaults, overrides, element) {
+ if (typeof fieldId !== 'string') {
+ element = overrides;
+ overrides = defaults;
+ defaults = fieldId;
+ fieldId = '';
+ }
+ overrides = overrides || {};
+ var out = {};
+ var dataAttrs = element ? InkElement.data(element) : {};
+ var fromDataAttrs;
+ var type;
+ var lType;
+ var defaultVal;
+
+ var invalidStr = function (str) {
+ if (fieldId) { str = fieldId + ': "' + ('' + str).replace(/"/, '\\"') + '"'; }
+ return str;
+ };
+
+ var quote = function (str) {
+ return '"' + ('' + str).replace(/"/, '\\"') + '"';
+ };
+
+ var invalidThrow = function (str) {
+ throw new Error(invalidStr(str));
+ };
+
+ var invalid = function (str) {
+ Ink.error(invalidStr(str) + '. Ignoring option.');
+ };
+
+ function optionValue(key) {
+ type = defaults[key][0];
+ lType = type.toLowerCase();
+ defaultVal = defaults[key].length === 2 ? defaults[key][1] : nothing;
+
+ if (!type) {
+ invalidThrow('Ink.UI.Common.options: Always specify a type!');
+ }
+ if (!(lType in Common._coerce_funcs)) {
+ invalidThrow('Ink.UI.Common.options: ' + defaults[key][0] + ' is not a valid type. Use one of ' + keys(Common._coerce_funcs).join(', '));
+
+ }
+ if (!defaults[key].length || defaults[key].length > 2) {
+ invalidThrow('the "defaults" argument must be an object mapping option names to [typestring, optional] arrays.');
+ }
+
+ if (key in dataAttrs) {
+ fromDataAttrs = Common._coerce_from_string(lType, dataAttrs[key], key, fieldId);
+ // (above can return `nothing`)
+ } else {
+ fromDataAttrs = nothing;
+ }
+
+ if (fromDataAttrs !== nothing) {
+ if (!Common._options_validate(fromDataAttrs, lType)) {
+ invalid('(' + key + ' option) Invalid ' + lType + ' ' + quote(fromDataAttrs));
+ return defaultVal;
+ } else {
+ return fromDataAttrs;
+ }
+ } else if (key in overrides) {
+ return overrides[key];
+ } else if (defaultVal !== nothing) {
+ return defaultVal;
+ } else {
+ invalidThrow('Option ' + key + ' is required!');
+ }
+ }
+
+ for (var key in defaults) {
+ if (defaults.hasOwnProperty(key)) {
+ out[key] = optionValue(key);
+ }
+ }
+
+ return out;
+ },
+
+ _coerce_from_string: function (type, val, paramName, fieldId) {
+ if (type in Common._coerce_funcs) {
+ return Common._coerce_funcs[type](val, paramName, fieldId);
+ } else {
+ return val;
+ }
+ },
+
+ _options_validate: function (val, type) {
+ if (type in Common._options_validate_types) {
+ return Common._options_validate_types[type].call(Common, val);
+ } else {
+ // 'object' options cannot be passed through data-attributes.
+ // Json you say? Not any good to embed in HTML.
+ return false;
+ }
+ },
+
+ _coerce_funcs: (function () {
+ var ret = {
+ element: function (val) {
+ return Common.elOrSelector(val, '');
+ },
+ elements: function (val) {
+ return Common.elsOrSelector(val, '', false /*not required, so don't throw an exception now*/);
+ },
+ object: function (val) { return val; },
+ number: function (val) { return parseFloat(val); },
+ 'boolean': function (val) {
+ return !(val === 'false' || val === '' || val === null);
+ },
+ string: function (val) { return val; },
+ 'function': function (val, paramName, fieldId) {
+ Ink.error(fieldId + ': You cannot specify the option "' + paramName + '" through data-attributes because it\'s a function');
+ return nothing;
+ }
+ };
+ ret['float'] = ret.integer = ret.number;
+ return ret;
+ }()),
+
+ _options_validate_types: (function () {
+ var types = {
+ string: function (val) {
+ return typeof val === 'string';
+ },
+ number: function (val) {
+ return typeof val === 'number' && !isNaN(val) && isFinite(val);
+ },
+ integer: function (val) {
+ return val === Math.round(val);
+ },
+ element: function (val) {
+ return Common.isDOMElement(val);
+ },
+ elements: function (val) {
+ return val && typeof val === 'object' && typeof val.length === 'number' && val.length;
+ },
+ 'boolean': function (val) {
+ return typeof val === 'boolean';
+ }
+ };
+ types['float'] = types.number;
+ return types;
+ }()),
+
+ /**
+ * Method to make a deep copy (clone) of an object.
+ * Note: The object cannot have loops.
+ *
+ * @method clone
+ * @static
+ * @param {Object} o The object to be cloned/copied.
+ * @return {Object} Returns the result of the clone/copy.
+ * @example
+ * var originalObj = {
+ * key1: 'value1',
+ * key2: 'value2',
+ * key3: 'value3'
+ * };
+ * var cloneObj = Ink.UI.Common.clone( originalObj );
+ */
+ clone: function(o) {
+ try {
+ return JSON.parse( JSON.stringify(o) );
+ } catch (ex) {
+ throw new Error('Given object cannot have loops!');
+ }
+ },
+
+
+ /**
+ * Method to return the 'nth' position that an element occupies relatively to its parent.
+ *
+ * @method childIndex
+ * @static
+ * @param {DOMElement} childEl Valid DOM Element.
+ * @return {Number} Numerical position of an element relatively to its parent.
+ * @example
+ *
+ *
+ *
One
+ *
Two
+ *
Three
+ *
Four
+ *
+ *
+ *
+ */
+ childIndex: function(childEl) {
+ if( Common.isDOMElement(childEl) ){
+ var els = Selector.select('> *', childEl.parentNode);
+ for (var i = 0, f = els.length; i < f; ++i) {
+ if (els[i] === childEl) {
+ return i;
+ }
+ }
+ }
+ throw 'not found!';
+ },
+
+
+ /**
+ * This method provides a more convenient way to do an async AJAX request and expect a JSON response.
+ * It offers a callback option, as third paramenter, for a better async handling.
+ *
+ * @method ajaxJSON
+ * @static
+ * @async
+ * @param {String} endpoint Valid URL to be used as target by the request.
+ * @param {Object} params This field is used in the thrown Exception to identify the parameter.
+ * @example
+ * // In case there are several .myInput, it will retrieve the first found
+ * var el = Ink.UI.Common.elOrSelector('.myInput','My Input');
+ */
+ ajaxJSON: function(endpoint, params, cb) {
+ new Ajax(
+ endpoint,
+ {
+ evalJS: 'force',
+ method: 'POST',
+ parameters: params,
+
+ onSuccess: function( r) {
+ try {
+ r = r.responseJSON;
+ if (r.status !== 'ok') {
+ throw 'server error: ' + r.message;
+ }
+ cb(null, r);
+ } catch (ex) {
+ cb(ex);
+ }
+ },
+
+ onFailure: function() {
+ cb('communication failure');
+ }
+ }
+ );
+ },
+
+
+ /**
+ * Method to get the current Ink layout applied.
+ *
+ * @method currentLayout
+ * @static
+ * @return {String} 'small', 'medium' or 'large'
+ * @example
+ * var inkLayout = Ink.UI.Common.currentLayout();
+ * if (inkLayout === 'small') {
+ * // ...
+ * }
+ */
+ currentLayout: function() {
+ var i, f, k, v, el, detectorEl = Selector.select('#ink-layout-detector')[0];
+ if (!detectorEl) {
+ detectorEl = document.createElement('div');
+ detectorEl.id = 'ink-layout-detector';
+ for (k in this.Layouts) {
+ if (this.Layouts.hasOwnProperty(k)) {
+ v = this.Layouts[k];
+ el = document.createElement('div');
+ el.className = 'show-' + v + ' hide-all';
+ el.setAttribute('data-ink-layout', v);
+ detectorEl.appendChild(el);
+ }
+ }
+ document.body.appendChild(detectorEl);
+ }
+
+ var result = '';
+ var resultCount = 0;
+ for (i = 0, f = detectorEl.childNodes.length; i < f; ++i) {
+ el = detectorEl.childNodes[i];
+ if (Css.getStyle(el, 'display') === 'block') {
+ result = el.getAttribute('data-ink-layout');
+ resultCount += 1;
+ }
+ }
+
+ if (resultCount === 1) {
+ return result;
+ } else {
+ return 'large';
+ }
+ },
+
+
+ /**
+ * Method to set the location's hash (window.location.hash).
+ *
+ * @method hashSet
+ * @static
+ * @param {Object} o Object with the info to be placed in the location's hash.
+ * @example
+ * // It will set the location's hash like: #key1=value1&key2=value2&key3=value3
+ * Ink.UI.Common.hashSet({
+ * key1: 'value1',
+ * key2: 'value2',
+ * key3: 'value3'
+ * });
+ */
+ hashSet: function(o) {
+ if (typeof o !== 'object') { throw new TypeError('o should be an object!'); }
+ var hashParams = Url.getAnchorString();
+ hashParams = Ink.extendObj(hashParams, o);
+ window.location.hash = Url.genQueryString('', hashParams).substring(1);
+ },
+
+ /**
+ * Method to remove children nodes from a given object.
+ * This method was initially created to help solve a problem in Internet Explorer(s) that occurred when trying
+ * to set the innerHTML of some specific elements like 'table'.
+ *
+ * @method cleanChildren
+ * @static
+ * @param {DOMElement} parentEl Valid DOM Element
+ * @example
+ *
+ *
+ *
One
+ *
Two
+ *
Three
+ *
Four
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ cleanChildren: function(parentEl) {
+ if( !Common.isDOMElement(parentEl) ){
+ throw 'Please provide a valid DOMElement';
+ }
+ var prevEl, el = parentEl.lastChild;
+ while (el) {
+ prevEl = el.previousSibling;
+ parentEl.removeChild(el);
+ el = prevEl;
+ }
+ },
+
+ /**
+ * This method stores the id and/or the classes of a given element in a given object.
+ *
+ * @method storeIdAndClasses
+ * @static
+ * @param {DOMElement} fromEl Valid DOM Element to get the id and classes from.
+ * @param {Object} inObj Object where the id and classes will be saved.
+ * @example
+ *
+ *
+ *
+ */
+ storeIdAndClasses: function(fromEl, inObj) {
+ if( !Common.isDOMElement(fromEl) ){
+ throw 'Please provide a valid DOMElement as first parameter';
+ }
+
+ var id = fromEl.id;
+ if (id) {
+ inObj._id = id;
+ }
+
+ var classes = fromEl.className;
+ if (classes) {
+ inObj._classes = classes;
+ }
+ },
+
+ /**
+ * This method sets the id and className properties of a given DOM Element based on a given similar object
+ * resultant of the previous function 'storeIdAndClasses'.
+ *
+ * @method restoreIdAndClasses
+ * @static
+ * @param {DOMElement} toEl Valid DOM Element to set the id and classes on.
+ * @param {Object} inObj Object where the id and classes to be set are.
+ * @example
+ *
+ *
+ *
+ *
+ *
+ *
+ */
+ restoreIdAndClasses: function(toEl, inObj) {
+
+ if( !Common.isDOMElement(toEl) ){
+ throw 'Please provide a valid DOMElement as first parameter';
+ }
+
+ if (inObj._id && toEl.id !== inObj._id) {
+ toEl.id = inObj._id;
+ }
+
+ if (inObj._classes && toEl.className.indexOf(inObj._classes) === -1) {
+ if (toEl.className) { toEl.className += ' ' + inObj._classes; }
+ else { toEl.className = inObj._classes; }
+ }
+
+ if (inObj._instanceId && !toEl.getAttribute('data-instance')) {
+ toEl.setAttribute('data-instance', inObj._instanceId);
+ }
+ },
+
+ /**
+ * This method saves a component's instance reference for later retrieval.
+ *
+ * @method registerInstance
+ * @static
+ * @param {Object} inst Object that holds the instance.
+ * @param {DOMElement} el DOM Element to associate with the object.
+ * @param {Object} [optionalPrefix] Defaults to 'instance'
+ */
+ registerInstance: function(inst, el, optionalPrefix) {
+ if (inst._instanceId) { return; }
+
+ if (typeof inst !== 'object') { throw new TypeError('1st argument must be a JavaScript object!'); }
+
+ if (inst._options && inst._options.skipRegister) { return; }
+
+ if (!this.isDOMElement(el)) { throw new TypeError('2nd argument must be a DOM element!'); }
+ if (optionalPrefix !== undefined && typeof optionalPrefix !== 'string') { throw new TypeError('3rd argument must be a string!'); }
+ var id = (optionalPrefix || 'instance') + (++lastIdNum);
+ instances[id] = inst;
+ inst._instanceId = id;
+ var dataInst = el.getAttribute('data-instance');
+ dataInst = (dataInst !== null) ? [dataInst, id].join(' ') : id;
+ el.setAttribute('data-instance', dataInst);
+ },
+
+ /**
+ * This method deletes/destroy an instance with a given id.
+ *
+ * @method unregisterInstance
+ * @static
+ * @param {String} id Id of the instance to be destroyed.
+ */
+ unregisterInstance: function(id) {
+ delete instances[id];
+ },
+
+ /**
+ * This method retrieves the registered instance(s) of a given element or instance id.
+ *
+ * @method getInstance
+ * @static
+ * @param {String|DOMElement} instanceIdOrElement Instance's id or DOM Element from which we want the instances.
+ * @return {Object|Object[]} Returns an instance or a collection of instances.
+ */
+ getInstance: function(instanceIdOrElement) {
+ var ids;
+ if (this.isDOMElement(instanceIdOrElement)) {
+ ids = instanceIdOrElement.getAttribute('data-instance');
+ if (ids === null) { throw new Error('argument is not a DOM instance element!'); }
+ }
+ else {
+ ids = instanceIdOrElement;
+ }
+
+ ids = ids.split(' ');
+ var inst, id, i, l = ids.length;
+
+ var res = [];
+ for (i = 0; i < l; ++i) {
+ id = ids[i];
+ if (!id) { throw new Error('Element is not a JS instance!'); }
+ inst = instances[id];
+ if (!inst) { throw new Error('Instance "' + id + '" not found!'); }
+ res.push(inst);
+ }
+
+ return (l === 1) ? res[0] : res;
+ },
+
+ /**
+ * This method retrieves the registered instance(s) of an element based on the given selector.
+ *
+ * @method getInstanceFromSelector
+ * @static
+ * @param {String} selector CSS selector to define the element from which it'll get the instance(s).
+ * @return {Object|Object[]} Returns an instance or a collection of instances.
+ */
+ getInstanceFromSelector: function(selector) {
+ var el = Selector.select(selector)[0];
+ if (!el) { throw new Error('Element not found!'); }
+ return this.getInstance(el);
+ },
+
+ /**
+ * This method retrieves the registered instances' ids of all instances.
+ *
+ * @method getInstanceIds
+ * @static
+ * @return {String[]} Id or collection of ids of all existing instances.
+ */
+ getInstanceIds: function() {
+ var res = [];
+ for (var id in instances) {
+ if (instances.hasOwnProperty(id)) {
+ res.push( id );
+ }
+ }
+ return res;
+ },
+
+ /**
+ * This method retrieves all existing instances.
+ *
+ * @method getInstances
+ * @static
+ * @return {Object[]} Collection of existing instances.
+ */
+ getInstances: function() {
+ var res = [];
+ for (var id in instances) {
+ if (instances.hasOwnProperty(id)) {
+ res.push( instances[id] );
+ }
+ }
+ return res;
+ },
+
+ /**
+ * This method is not to supposed to be invoked by the Common component.
+ * Components should copy this method as its destroy method.
+ *
+ * @method destroyComponent
+ * @static
+ */
+ destroyComponent: function() {
+ Common.unregisterInstance(this._instanceId);
+ this._element.parentNode.removeChild(this._element);
+ }
+
+ };
+
+ return Common;
+
+});
+
+/**
+ * @module Ink.UI.DatePicker_1
+ * @author inkdev AT sapo.pt
+ * @version 1
+ */
+Ink.createModule('Ink.UI.DatePicker', '1', ['Ink.UI.Common_1','Ink.Dom.Event_1','Ink.Dom.Css_1','Ink.Dom.Element_1','Ink.Dom.Selector_1','Ink.Util.Array_1','Ink.Util.Date_1', 'Ink.Dom.Browser_1'], function(Common, Event, Css, InkElement, Selector, InkArray, InkDate ) {
+ 'use strict';
+
+ // Repeat a string. Long version of (new Array(n)).join(str);
+ function strRepeat(n, str) {
+ var ret = '';
+ for (var i = 0; i < n; i++) {
+ ret += str;
+ }
+ return ret;
+ }
+
+ // Clamp a number into a min/max limit
+ function clamp(n, min, max) {
+ if (n > max) { n = max; }
+ if (n < min) { n = min; }
+
+ return n;
+ }
+
+ function dateishFromYMDString(YMD) {
+ var split = YMD.split('-');
+ return dateishFromYMD(+split[0], +split[1] - 1, +split[2]);
+ }
+
+ function dateishFromYMD(year, month, day) {
+ return {_year: year, _month: month, _day: day};
+ }
+
+ function dateishFromDate(date) {
+ return {_year: date.getFullYear(), _month: date.getMonth(), _day: date.getDate()};
+ }
+
+ /**
+ * @class Ink.UI.DatePicker
+ * @constructor
+ * @version 1
+ *
+ * @param {String|DOMElement} selector
+ * @param {Object} [options] Options
+ * @param {Boolean} [options.autoOpen=false] set to `true` to automatically open the datepicker.
+ * @param {String} [options.cleanText] text to display on clean button. defaults to 'Limpar'
+ * @param {String} [options.closeText] text to display on close button. defaults to 'Fechar'
+ * @param {String} [options.cssClass] CSS class to be applied to the datepicker
+ * @param {String} [options.dateRange] enforce limits to year, month and day for the Date, ex: '1990-08-25:2020-11'
+ * @param {Boolean} [options.displayInSelect] whether to display the component in a select. defaults to false.
+ * @param {String|DOMElement} [options.dayField] (if using options.displayInSelect) `