/** * Animate.css Utility * * This module is a wrapper around animate.css's CSS classes to produce animation. * It contains options to ease common tasks, like listen to the "animationend" event with all necessary prefixes, remove the necessary class names when the animation finishes, or configure the duration of your animation with the necessary browser prefix. * * @module Ink.UI.Animate_1 * @version 1 */ Ink.createModule('Ink.UI.Animate', 1, ['Ink.UI.Common_1', 'Ink.Dom.Event_1', 'Ink.Dom.Css_1'], function (Common, InkEvent, Css) { 'use strict'; var animationPrefix = (function (el) { return ('animationName' in el.style) ? 'animation' : ('oAnimationName' in el.style) ? 'oAnimation' : ('msAnimationName' in el.style) ? 'msAnimation' : ('webkitAnimationName' in el.style) ? 'webkitAnimation' : null; }(document.createElement('div'))); var animationEndEventName = { animation: 'animationend', oAnimation: 'oanimationend', msAnimation: 'MSAnimationEnd', webkitAnimation: 'webkitAnimationEnd' }[animationPrefix]; /** * @class Ink.UI.Animate_1 * @constructor * * @param {DOMElement} element Animated element * @param {Object} options Options object * @param {String} options.animation Animation name * @param {String|Number} [options.duration] Duration name (fast|medium|slow) or duration in milliseconds. Defaults to 'medium'. * @param {Boolean} [options.removeClass] Flag to remove the CSS class when finished animating. Defaults to false. * @param {Function} [options.onEnd] Callback for the animation end * * @sample Ink_UI_Animate_1.html * **/ function Animate() { Common.BaseUIComponent.apply(this, arguments); } Animate._name = 'Animate_1'; Animate._optionDefinition = { trigger: ['Element', null], duration: ['String', 'slow'], // Actually a string with a duration name, or a number of ms animation: ['String'], removeClass: ['Boolean', true], onEnd: ['Function', function () {}] }; Animate.prototype._init = function () { if (!isNaN(parseInt(this._options.duration, 10))) { this._options.duration = parseInt(this._options.duration, 10); } if (this._options.trigger) { InkEvent.observe(this._options.trigger, 'click', Ink.bind(function () { this.animate(); }, this)); // later } else { this.animate(); } }; Animate.prototype.animate = function () { Animate.animate(this._element, this._options.animation, this._options); }; Ink.extendObj(Animate, { /** * Browser prefix for the CSS animations. * * @property _animationPrefix * @private **/ _animationPrefix: animationPrefix, /** * Boolean which says whether this browser has CSS3 animation support. * * @property animationSupported **/ animationSupported: !!animationPrefix, /** * Prefixed 'animationend' event name. * * @property animationEndEventName **/ animationEndEventName: animationEndEventName, /** * Animate an element using one of the animate.css classes * * **Note: This is a utility method inside the `Animate` class, which you can access through `Animate.animate()`. Do not mix these up.** * * @static * @method animate * @param element {DOMElement} animated element * @param animation {String} animation name * @param [options] {Object} * @param [options.onEnd=null] {Function} callback for animation end * @param [options.removeClass=false] {Boolean} whether to remove the Css class when finished * @param [options.duration=medium] {String|Number} duration name (fast|medium|slow) or duration in ms * * @sample Ink_UI_Animate_1_animate.html **/ animate: function (element, animation, options) { element = Common.elOrSelector(element); if (typeof options === 'number' || typeof options === 'string') { options = { duration: options }; } else if (!options) { options = {}; } if (typeof arguments[3] === 'function') { options.onEnd = arguments[3]; } if (typeof options.duration !== 'number' && typeof options.duration !== 'string') { options.duration = 400; } if (!Animate.animationSupported) { if (options.onEnd) { setTimeout(function () { options.onEnd(null); }, 0); } return; } if (typeof options.duration === 'number') { element.style[animationPrefix + 'Duration'] = options.duration + 'ms'; } else if (typeof options.duration === 'string') { Css.addClassName(element, options.duration); } Css.addClassName(element, ['animated', animation]); function onAnimationEnd(event) { if (event.target !== element) { return; } if (event.animationName !== animation) { return; } if (options.onEnd) { options.onEnd(event); } if (options.removeClass) { Css.removeClassName(element, animation); } if (typeof options.duration === 'string') { Css.removeClassName(element, options.duration); } element.removeEventListener(animationEndEventName, onAnimationEnd, false); } element.addEventListener(animationEndEventName, onAnimationEnd, false); } }); Common.createUIComponent(Animate); return Animate; }); /** * Flexible Carousel * @module Ink.UI.Carousel_1 * @version 1 */ Ink.createModule('Ink.UI.Carousel', '1', ['Ink.UI.Common_1', 'Ink.Dom.Event_1', 'Ink.Dom.Css_1', 'Ink.Dom.Element_1', 'Ink.UI.Pagination_1', 'Ink.Dom.Browser_1', 'Ink.Dom.Selector_1'], function(Common, InkEvent, Css, InkElement, Pagination, Browser/*, Selector*/) { 'use strict'; /* * TODO: * keyboardSupport */ function limitRange(n, min, max) { return Math.min(max, Math.max(min, n)); } var requestAnimationFrame = window.requestAnimationFrame || window.mozRequestAnimationFrame || window.webkitRequestAnimationFrame || function (cb) {return setTimeout(cb, 1000 / 30); }; /** * @class Ink.UI.Carousel_1 * @constructor * * @param {String|DOMElement} selector DOM element or element id * @param {Object} [options] Carousel Options * @param {Integer} [options.autoAdvance] Milliseconds to wait before auto-advancing pages. Set to 0 to disable auto-advance. Defaults to 0. * @param {String} [options.axis] Axis of the carousel. Set to 'y' for a vertical carousel. Defaults to 'x'. * @param {Number} [options.initialPage] Initial index page of the carousel. Defaults to 0. * @param {Boolean} [options.spaceAfterLastSlide=true] If there are not enough slides to fill the full width of the last page, leave white space. Defaults to `true`. * @param {Boolean} [options.swipe] Enable swipe support if available. Defaults to true. * @param {Mixed} [options.pagination] Either an ul element to add pagination markup to or an `Ink.UI.Pagination` instance to use. * @param {Function} [options.onChange] Callback to be called when the page changes. * * @sample Ink_UI_Carousel_1.html */ function Carousel() { Common.BaseUIComponent.apply(this, arguments); } Carousel._name = 'Carousel_1'; Carousel._optionDefinition = { autoAdvance: ['Integer', 0], axis: ['String', 'x'], initialPage: ['Integer', 0], spaceAfterLastSlide: ['Boolean', true], hideLast: ['Boolean', false], // [3.1.0] Deprecate "center". It is only needed when things are of unknown widths. center: ['Boolean', false], keyboardSupport:['Boolean', false], pagination: ['String', null], onChange: ['Function', null], onInit: ['Function', function () {}], swipe: ['Boolean', true] // TODO exponential swipe // TODO specify break point for next page when moving finger }; Carousel.prototype = { _init: function () { this._handlers = { paginationChange: Ink.bindMethod(this, '_onPaginationChange'), windowResize: InkEvent.throttle(Ink.bindMethod(this, 'refit'), 200) }; InkEvent.observe(window, 'resize', this._handlers.windowResize); this._isY = (this._options.axis === 'y'); var ulEl = Ink.s('ul.stage', this._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 (this._options.swipe) { InkEvent.observe(this._element, 'touchstart', Ink.bindMethod(this, '_onTouchStart')); InkEvent.observe(this._element, 'touchmove', Ink.bindMethod(this, '_onTouchMove')); InkEvent.observe(this._element, 'touchend', Ink.bindMethod(this, '_onTouchEnd')); } this._setUpPagination(); this._setUpAutoAdvance(); this._setUpHider(); this._options.onInit.call(this, this); }, /** * Repositions elements around. * Measure the carousel once again, adjusting the involved elements' sizes. This is 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 (!elm) { return 0; } 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; if (!isFinite(this._slidesPerPage)) { this._slidesPerPage = 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; } }, // [3.1.0] Deprecate this already _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(''); }, // [3.1.0] Deprecate this already _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'; } }, /** * Refits elements for IE7 because it doesn'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) }; var ulRect = this._ulEl.getBoundingClientRect(); this._swipeData.firstUlPos = ulRect[this._isY ? 'top' : 'left']; this._swipeData.inUlX = this._swipeData.x - ulRect.left; this._swipeData.inUlY = this._swipeData.y - ulRect.top; setTransitionProperty(this._ulEl, 'none'); this._touchMoveIsFirstTouchMove = true; }, _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; } }, _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'; 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 relProgress = this._swipeData.firstUlPos - this._ulEl.getBoundingClientRect()[this._isY ? 'top' : 'left']; var curPage = this.getPage(); // How many pages were advanced? May be fractional. var progressInPages = relProgress / this._elLength / this._slidesPerPage; // Have we advanced enough to change page? if (Math.abs(progressInPages) > snapToNext) { curPage += Math[ relProgress < 0 ? 'floor' : 'ceil' ](progressInPages); } // If something used to calculate progressInPages was zero, we get NaN here. if (!isNaN(curPage)) { this.setPage(curPage); } 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()); }, /** * Gets the current page index * @method getPage * @return The current page number **/ getPage: function () { if (this._pagination) { return this._pagination.getCurrent(); } else { return this._currentPage || 0; } }, /** * Sets the current page index * @method setPage * @param {Number} page Index of the destination page. * @param {Boolean} [wrap] Flag to activate circular counting. **/ 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); // _setPage is called by pagination because it listens to its Change event. } 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(); }, /** * Goes to the next page * @method nextPage * @param {Boolean} [wrap] Flag to loop from last page to first page. **/ nextPage: function (wrap) { this.setPage(this.getPage() + 1, wrap); }, /** * Goes to the previous page * @method previousPage * @param {Boolean} [wrap] Flag to loop from first page to last page. **/ previousPage: function (wrap) { this.setPage(this.getPage() - 1, wrap); }, /** * Returns how many slides fit into a page * @method getSlidesPerPage * @return {Number} The number of slides per page * @public */ getSlidesPerPage: function() { return this._slidesPerPage; }, /** * Get the amount of pages in the carousel. * @method getTotalPages * @return {Number} The number of pages * @public */ getTotalPages: function() { return this._numPages; }, /** * Get the stage element (your UL with the class ".stage"). * @method getStageElm * @public * @return {DOMElement} Stage element **/ getStageElm: function() { return this._ulEl; }, /** * Get a list of your slides (elements with the ".slide" class inside your stage) * @method getSlidesList * @return {DOMElement[]} Array containing the slides. * @public */ getSlidesList: function() { return this._liEls; }, /** * Get the total number of slides * @method getTotalSlides * @return {Number} The number of slides * @public */ getTotalSlides: function() { return this.getSlidesList().length; } }; function setTransitionProperty(el, newTransition) { el.style.transitionProperty = el.style.oTransitionProperty = el.style.msTransitionProperty = el.style.mozTransitionProperty = el.style.webkitTransitionProperty = newTransition; } Common.createUIComponent(Carousel); return Carousel; }); /** * Closing utilities * @module Ink.UI.Close_1 * @version 1 */ 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. * Whenever an element with the classes ".ink-close" or ".ink-dismiss" is clicked, this module finds an ancestor ".ink-alert" or ".ink-alert-block" element and removes it from the DOM. * This module should be created only once per page. * * @class Ink.UI.Close * @constructor * @example * * * @sample Ink_UI_Close_1.html */ 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 = InkElement.findUpwardsByClass(el, 'ink-alert') || InkElement.findUpwardsByClass(el, 'ink-alert-block') || el; if (toRemove) { InkEvent.stop(ev); InkElement.remove(toRemove); } }); }; Close._name = 'Close_1'; return Close; }); /** * Auxiliar utilities for UI Modules * @module Ink.UI.Common_1 * @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 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; }; var es6WeakMapSupport = 'WeakMap' in window; var instances = es6WeakMapSupport ? new WeakMap() : null; var domRegistry = { get: function get(el) { return es6WeakMapSupport ? instances.get(el) : el.__InkInstances; }, set: function set(el, thing) { if (es6WeakMapSupport) { instances.set(el, thing); } else { el.__InkInstances = thing; } } }; /** * @namespace Ink.UI.Common_1 */ var Common = { /** * Supported Ink Layouts * * @property Layouts * @type Object * @readOnly */ Layouts: { TINY: 'tiny', SMALL: 'small', MEDIUM: 'medium', LARGE: 'large', XLARGE: 'xlarge' }, /** * Checks 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: InkElement.isDOMElement, /** * Checks if an item is a valid integer. * * @method isInteger * @static * @param {Mixed} n The value to be checked. * @return {Boolean} True if it's 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); }, /** * Gets a DOM Element. * * @method elOrSelector * @static * @param {DOMElement|String} elOrSelector DOM Element or CSS Selector * @param {String} fieldName The name of the field. Commonly used for debugging. * @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) { Ink.warn(fieldName + ' must either be a DOM Element or a selector expression!\nThe script element must also be after the DOM Element itself.'); return null; } return t[0]; } return elOrSelector; }, /** * Alias for `elOrSelector` but returns an array of elements. * * @method elsOrSelector * * @static * @param {DOMElement|String} elOrSelector DOM Element or CSS Selector * @param {String} fieldName The name of the field. Commonly used for debugging. * @return {DOMElement} Returns the DOMElement passed or the first result of the CSS Selector. Otherwise it throws an exception. * @param {Boolean} required Flag to 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) { 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 []; } } }, /** * Gets options an object and element's metadata. * * The element's data attributes take precedence. Values from the element's data-atrributes are coerced into the required type. * * @method options * * @param {Object} [fieldId] Name to be used in debugging features. * @param {Object} defaults Object with the options' types and defaults. * @param {Object} overrides Options to override the defaults. Usually passed when instantiating an UI module. * @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'; }, object: function () { return true; } }; types['float'] = types.number; return types; }()), /** * Deep copy (clone) an object. * Note: The object cannot have referece 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!'); } }, /** * Gets an element's one-base index relative to its parent. * * @method childIndex * @static * @param {DOMElement} childEl Valid DOM Element. * @return {Number} Numerical position of an element relatively to its parent. * @example * *