UNPKG

prodio

Version:

Simplified project management

1,353 lines (1,181 loc) 457 kB
/** * 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(elOrSelector, options) { this._element = Common.elOrSelector(elOrSelector); this._options = Common.options({ 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 () {}] }, options || {}, this._element); 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(); } Common.registerInstance(this, this._element); } 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 }; } 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); } }); 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 {Boolean} [options.center] Flag to center the carousel horizontally. * @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 */ 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], onInit: ['Function', function () {}], 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(); this._options.onInit.call(this, this); Common.registerInstance(this, this._element); }; Carousel.prototype = { /** * 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 (!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'; } }, /** * 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), 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()); }, /** * 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); } 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; } 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 * <script> * Ink.requireModules(['Ink.UI.Close_1'],function( Close ){ * new Close(); * }); * </script> * * @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); } }); }; 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 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; }; /** * @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: function(o) { return o && typeof o === 'object' && 'nodeType' in o && o.nodeType === 1; }, /** * 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'; } }; 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 * <!-- Imagine the following HTML: --> * <ul> * <li>One</li> * <li>Two</li> * <li id="test">Three</li> * <li>Four</li> * </ul> * * <script> * var testLi = Ink.s('#test'); * Ink.UI.Common.childIndex( testLi ); // Returned value: 3 * </script> */ 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!'; }, /** * AJAX JSON request shortcut method * It provides a more convenient way to do an AJAX request and expect a JSON response.It also offers a callback option, as third parameter, for 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. * @param {Function} cb Callback for the request. * @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'); } } ); }, /** * Gets the current Ink layout. * * @method currentLayout * @static * @return {String} A string representation of the current layout name. * @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); } for (i = 0, f = detectorEl.children.length; i < f; ++i) { el = detectorEl.children[i]; if (Css.getStyle(el, 'display') === 'block') { return el.getAttribute('data-ink-layout'); } } return 'large'; }, /** * Sets 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: <url>#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); }, /** * Removes 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 * <!-- Imagine the following HTML: --> * <ul id="myUl"> * <li>One</li> * <li>Two</li> * <li>Three</li> * <li>Four</li> * </ul> * * <script> * Ink.UI.Common.cleanChildren( Ink.s( '#myUl' ) ); * </script> * * <!-- After running it, the HTML changes to: --> * <ul id="myUl"></ul> */ 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; } }, /** * Stores the id and/or classes of an element in an 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 * <div id="myDiv" class="aClass"></div> * * <script> * var storageObj = {}; * Ink.UI.Common.storeIdAndClasses( Ink.s('#myDiv'), storageObj ); * // storageObj changes to: * { * _id: 'myDiv', * _classes: 'aClass' * } * </script> */ 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; } }, /** * Sets the id and className properties of an element based * * @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. This method uses the same format as the one given in `storeIdAndClasses` * @example * <div></div> * * <script> * var storageObj = { * _id: 'myDiv', * _classes: 'aClass' * }; * * Ink.UI.Common.storeIdAndClasses( Ink.s('div'), storageObj ); * </script> * * <!-- After the code runs the div element changes to: --> * <div id="myDiv" class="aClass"></div> */ 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); } }, _warnDoubleInstantiation: function (elm, newInstance) { var instances = Common.getInstance(elm); if (getName(newInstance) === '') { return; } if (!instances) { return; } var nameWithoutVersion = getName(newInstance); for (var i = 0, len = instances.length; i < len; i++) { if (nameWithoutVersion === getName(instances[i])) { Ink.warn('Creating more than one ' + nameWithoutVersion + '.', '(Was creating a ' + nameWithoutVersion + ' on:', elm, '.' + 'Existing element was: ', instances[i]._element); } } function getName(thing) { return ((thing.constructor && (thing.constructor._name || thing.constructor.name)) || thing._name || '').replace(/_.*?$/, ''); } }, /** * 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. */ registerInstance: function(inst, el) { if (!inst || inst._instanceId) { return; } if (!this.isDOMElement(el)) { throw new TypeError('Ink.UI.Common.registerInstance: The element passed in is not a DOM element!'); } Common._warnDoubleInstantiation(el, inst); var id = '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); }, /** * Deletes 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]; }, /** * Gets