/** * 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 * * * * */ 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: #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 * * * * * * * */ 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 *
* * */ 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 *
* * * * *
*/ 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); if (!nameWithoutVersion) { return; } for (var i = 0, len = instances.length; i < len; i++) { if (nameWithoutVersion === getName(instances[i])) { // Yes, I am using + to concatenate and , to split // arguments. // // Elements can't be concatenated with strings, but if // they are passed in an argument, modern debuggers will // pretty-print them and make it easy to find them in the // element inspector. // // On the other hand, if strings are passed as different // arguments, they get pretty printed. And the pretty // print of a string has quotes around it. // // If some day people find out that strings are not // just text and they start preserving contextual // information, then by all means change this to a // regular concatenation. // // But they won't. So don't change this. Ink.warn('Creating more than one ' + nameWithoutVersion + '.', '(Was creating a ' + nameWithoutVersion + ' on:', elm, ').'); return false; } } function getName(thing) { return ((thing.constructor && (thing.constructor._name)) || thing._name || '').replace(/_.*?$/, ''); } return true; }, /** * 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) { return; } if (!Common.isDOMElement(el)) { throw new TypeError('Ink.UI.Common.registerInstance: The element passed in is not a DOM element!'); } // [todo] this belongs in the BaseUIComponent's initialization if (Common._warnDoubleInstantiation(el, inst) === false) { return false; } var instances = domRegistry.get(el); if (!instances) { instances = []; domRegistry.set(el, instances); } instances.push(inst); return true; }, /** * Deletes an instance with a given id. * * @method unregisterInstance * @static * @param {String} id Id of the instance to be destroyed. */ unregisterInstance: function(inst) { if (!inst || !inst._element) { return; } var instances = domRegistry.get(inst._element); for (var i = 0, len = instances.length; i < len; i++) { if (instances[i] === inst) { instances.splice(i, 1); } } }, /** * Gets an UI instance from an element or instance id. * * @method getInstance * @static * @param {String|DOMElement} el DOM Element from which we want the instances. * @return {Object|Array} Returns an instance or a collection of instances. */ getInstance: function(el, UIComponent) { el = Common.elOrSelector(el); var instances = domRegistry.get(el); if (!instances) { instances = []; } if (typeof UIComponent !== 'function') { return instances; } for (var i = 0, len = instances.length; i < len; i++) { if (instances[i] instanceof UIComponent) { return instances[i]; } } return null; }, /** * Gets an instance based on a selector. * * @method getInstanceFromSelector * @static * @param {String} selector CSS selector to get the instances from. * @return {Object|Array} Returns an instance or a collection of instances. */ getInstanceFromSelector: function(selector) { return Common.getInstance(Common.elOrSelector(selector)); }, /** * Gets all the instance ids * * @method getInstanceIds * @static * @return {Array} Collection of instance ids */ getInstanceIds: function() { var res = []; for (var id in instances) { if (instances.hasOwnProperty(id)) { res.push( id ); } } return res; }, /** * Gets all the instances * * @method getInstances * @static * @return {Array} Collection of existing instances. */ getInstances: function() { var res = []; for (var id in instances) { if (instances.hasOwnProperty(id)) { res.push( instances[id] ); } } return res; }, /** * Boilerplate method to destroy a component. * Components should copy this method as its destroy method and modify it. * * @method destroyComponent * @static */ destroyComponent: function() { Common.unregisterInstance(this); this._element.parentNode.removeChild(this._element); } }; /** * Ink UI Base Class **/ function warnStub() { /* jshint validthis: true */ if (!this || this === window || typeof this.constructor !== 'function') { return; } Ink.warn('You called a method on an incorrectly instantiated ' + this.constructor._name + ' component. Check the warnings above to see what went wrong.'); } function stub(prototype, obj) { for (var k in prototype) if (prototype.hasOwnProperty(k)) { if (k === 'constructor') { continue; } if (typeof obj[k] === 'function') { obj[k] = warnStub; } } } /** * Ink UI Base Class * * You don't use this class directly, or inherit from it directly. * * See createUIComponent() (in this module) for how to create a UI component and inherit from this. It's not plain old JS inheritance, for several reasons. * * @class Ink.UI.Common.BaseUIComponent * @constructor * * @param element * @param options **/ function BaseUIComponent(element, options) { var constructor = this.constructor; var _name = constructor._name; if (!this || this === window) { throw new Error('Use "new InkComponent()" instead of "InkComponent()"'); } if (this && !(this instanceof BaseUIComponent)) { throw new Error('You forgot to call Ink.UI.Common.createUIComponent() on this module!'); } if (!element && !constructor._componentOptions.elementIsOptional) { Ink.error(new Error(_name + ': You need to pass an element or a selector as the first argument to "new ' + _name + '()"')); return; } else { this._element = Common.elsOrSelector(element, _name + ': An element with the selector "' + element + '" was not found!')[0]; } if (!this._element && !constructor._componentOptions.elementIsOptional) { isValidInstance = false; Ink.error(new Error(element + ' does not match an element on the page. You need to pass a valid selector to "new ' + _name + '".')); } // TODO Change Common.options's signature? the below looks better, more manageable // var options = Common.options({ // element: this._element, // modName: constructor._name, // options: constructor._optionDefinition, // defaults: constructor._globalDefaults // }); this._options = Common.options(_name, constructor._optionDefinition, options, this._element); var isValidInstance = BaseUIComponent._validateInstance(this) === true; if (isValidInstance && typeof this._init === 'function') { try { this._init.apply(this, arguments); } catch(e) { isValidInstance = false; Ink.error(e); } } if (!isValidInstance) { BaseUIComponent._stubInstance(this, constructor, _name); } else if (this._element) { Common.registerInstance(this, this._element); } } /** * Calls the `instance`'s _validate() method so it can validate itself. * * Returns false if the method exists, was called, but no Error was returned or thrown. * * @method _validateInstance * @private */ BaseUIComponent._validateInstance = function (instance) { var err; if (typeof instance._validate !== 'function') { return true; } try { err = instance._validate(); } catch (e) { err = e; } if (err instanceof Error) { instance._validationError = err; return false; } return true; }; /** * Replaces every method in the instance with stub functions which just call Ink.warn(). * * This avoids breaking the page when there are errors. * * @method _stubInstance * @param instance * @param constructor * @param name * @private */ BaseUIComponent._stubInstance = function (instance, constructor, name) { stub(constructor.prototype, instance); stub(BaseUIComponent.prototype, instance); Ink.warn(name + ' was not correctly created. ' + (instance._validationError || '')); }; // TODO BaseUIComponent.setGlobalOptions = function () {} // TODO BaseUIComponent.createMany = function (selector) {} BaseUIComponent.getInstance = function (elOrSelector) { elOrSelector = Common.elOrSelector(elOrSelector); return Common.getInstance(elOrSelector, this /* get instance by constructor */); }; Ink.extendObj(BaseUIComponent.prototype, { /** * Get an UI component's option's value. * * @method getOption * @param name * * @return The option value, or undefined if nothing is found. * * @example * * var myUIComponent = new Modal('#element', { trigger: '#trigger' }); // or anything else inheriting BaseUIComponent * myUIComponent.getOption('trigger'); // -> The trigger element (not the selector string, mind you) * **/ getOption: function (name) { if (this.constructor && !(name in this.constructor._optionDefinition)) { Ink.error('"' + name + '" is not an option for ' + this.constructor._name); return undefined; } return this._options[name]; }, /** * Sets an option's value * * @method getOption * @param name * @param value * * @example * * var myUIComponent = new Modal(...); * myUIComponent.setOption('trigger', '#some-element'); **/ setOption: function (name, value) { if (this.constructor && !(name in this.constructor._optionDefinition)) { Ink.error('"' + name + ' is not an option for ' + this.constructor._name); return; } this._options[name] = value; }, /** * Get the element associated with an UI component (IE the one you used in the constructor) * * @method getElement * @return {Element} The component's element. * * @example * var myUIComponent = new Modal('#element'); // or anything else inheriting BaseUIComponent * myUIComponent.getElement(); // -> The '#element' (not the selector string, mind you). * **/ getElement: function () { return this._element; } }); Common.BaseUIComponent = BaseUIComponent; /** * @method createUIComponent * @param theConstructor UI component constructor. It should have an _init function in its prototype, an _optionDefinition object, and a _name property indicating its name. * @param options * @param [options.elementIsOptional=false] Whether the element argument is optional (For example, when the component might work on existing markup or create its own). **/ Common.createUIComponent = function createUIComponent(theConstructor, options) { theConstructor._componentOptions = options || {}; function assert(test, msg) { if (!test) { throw new Error('Ink.UI_1.createUIComponent: ' + msg); } } function assertProp(prop, propType, message) { var propVal = theConstructor[prop]; // Check that the property was passed assert(typeof propVal !== 'undefined', theConstructor + ' doesn\'t have a "' + prop + '" property. ' + message); // Check that its type is correct assert(propType && typeof propVal === propType, 'typeof ' + theConstructor + '.' + prop + ' is not "' + propType + '". ' + message); } assert(typeof theConstructor === 'function', 'constructor argument is not a function!'); assertProp('_name', 'string', 'This property is used for error ' + 'messages. Set it to the full module path and version (Ink.My.Module_1).'); assertProp('_optionDefinition', 'object', 'This property contains the ' + 'option names, types and defaults. See Ink.UI.Common.options() for reference.'); // Extend the instance methods and props var _oldProto = theConstructor.prototype; if (typeof Object.create === 'function') { theConstructor.prototype = Object.create(BaseUIComponent.prototype); } else { theConstructor.prototype = (function hideF() { function F() {} F.prototype = BaseUIComponent.prototype; return new F(); }()); } Ink.extendObj(theConstructor.prototype, _oldProto); theConstructor.prototype.constructor = theConstructor; // Extend static methods Ink.extendObj(theConstructor, BaseUIComponent); }; return Common; }); /** * Date selector * @module Ink.UI.DatePicker_1 * @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'; // 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] Flag to automatically open the datepicker. * @param {String} [options.cleanText] Text for the clean button. Defaults to 'Clear'. * @param {String} [options.closeText] Text for the close button. Defaults to 'Close'. * @param {String} [options.cssClass] CSS class to be applied on the datepicker * @param {String|DOMElement} [options.pickerField] (if not using in an input[type="text"]) Element which displays the DatePicker when clicked. Defaults to an "open" link. * @param {String} [options.dateRange] Enforce limits to year, month and day for the Date, ex: '1990-08-25:2020-11' * @param {Boolean} [options.displayInSelect] Flag to display the component in a select element. * @param {String|DOMElement} [options.dayField] (if using options.displayInSelect) `select` field with days. * @param {String|DOMElement} [options.monthField] (if using options.displayInSelect) `select` field with months. * @param {String|DOMElement} [options.yearField] (if using options.displayInSelect) `select` field with years. * @param {String} [options.format] Date format string * @param {Object} [options.month] Hash of month names. Defaults to portuguese month names. January is 1. * @param {String} [options.nextLinkText] Text for the previous button. Defaults to '«'. * @param {String} [options.ofText] Text to show between month and year. Defaults to ' of '. * @param {Boolean} [options.onFocus] If the datepicker should open when the target element is focused. Defaults to true. * @param {Function} [options.onMonthSelected] Callback to execute when the month is selected. * @param {Function} [options.onSetDate] Callback to execute when the date is set. * @param {Function} [options.onYearSelected] Callback to execute when the year is selected. * @param {String} [options.position] Position for the datepicker. Either 'right' or 'bottom'. Defaults to 'right'. * @param {String} [options.prevLinkText] Text for the previous button. Defaults to '«'. * @param {Boolean} [options.showClean] If the clean button should be visible. Defaults to true. * @param {Boolean} [options.showClose] If the close button should be visible. Defaults to true. * @param {Boolean} [options.shy] If the datepicker should start automatically. Defaults to true. * @param {String} [options.startDate] Date to define initial month. Must be in yyyy-mm-dd format. * @param {Number} [options.startWeekDay] First day of the week. Sunday is zero. Defaults to 1 (Monday). * @param {Function} [options.validYearFn] Callback to execute when 'rendering' the month (in the month view) * @param {Function} [options.validMonthFn] Callback to execute when 'rendering' the month (in the month view) * @param {Function} [options.validDayFn] Callback to execute when 'rendering' the day (in the month view) * @param {Function} [options.nextValidDateFn] Function to calculate the next valid date, given the current. Useful when there's invalid dates or time frames. * @param {Function} [options.prevValidDateFn] Function to calculate the previous valid date, given the current. Useful when there's invalid dates or time frames. * @param {Object} [options.wDay] Hash of week day names. Sunday is 0. Defaults to { 0:'Sunday', 1:'Monday', etc... * @param {String} [options.yearRange] Enforce limits to year for the Date, ex: '1990:2020' (deprecated) * * @sample Ink_UI_DatePicker_1.html */ var DatePicker = function() { Common.BaseUIComponent.apply(this, arguments); }; DatePicker._name = 'DatePicker_1'; DatePicker._optionDefinition = { autoOpen: ['Boolean', false], cleanText: ['String', 'Clear'], closeText: ['String', 'Close'], pickerField: ['Element', null], containerElement:['Element', null], cssClass: ['String', 'ink-calendar bottom'], dateRange: ['String', null], // use this in a * * By applying this UI class to the above input, you get a tag field with the tags "initial" and "value". The class preserves the original input element. It remains hidden and is updated with new tag information dynamically, so regular HTML form logic still applies. * * Below "input" refers to the current value of the input tag (updated as the user enters text, of course), and "output" refers to the value which this class writes back to said input tag. * * @class Ink.UI.TagField * @version 1 * @constructor * @param {String|DOMElement} element Selector or DOM Input Element. * @param {Object} [options] Options object * @param {String|Array} [options.tags] Initial tags in the input * @param {Boolean} [options.allowRepeated] Flag to allow user to input several tags. Defaults to true. * @param {RegExp} [options.separator] Split the input by this RegExp. Defaults to /[,;(space)]+/g (spaces, commas and semicolons) * @param {String} [options.outSeparator] Use this string to separate each tag from the next in the output. Defaults to ','. * @param {Boolean} [options.autoSplit] Flag to activate tag creation when the user types a separator. Defaults to true. * @param {Integer} [options.maxTags] Maximum number of tags allowed. Set to -1 for no limit. Defaults to -1. * @example */ function TagField() { Common.BaseUIComponent.apply(this, arguments); } TagField._name = 'TagField_1'; TagField._optionDefinition = { tags: ['String', []], tagQuery: ['Object', null], tagQueryAsync: ['Object', null], allowRepeated: ['Boolean', false], maxTags: ['Integer', -1], outSeparator: ['String', ','], separator: ['String', /[,; ]+/g], autoSplit: ['Boolean', true] }; TagField.prototype = { /** * Init function called by the constructor * * @method _init * @private */ _init: function() { var o = this._options; if (typeof o.separator === 'string') { o.separator = new RegExp(o.separator, 'g'); } if (typeof o.tags === 'string') { // coerce to array using the separator o.tags = this._readInput(o.tags); } Css.addClassName(this._element, 'hide-all'); this._viewElm = InkElement.create('div', { className: 'ink-tagfield', insertAfter: this._element }); this._input = InkElement.create('input', { type: 'text', className: 'new-tag-input', insertBottom: this._viewElm }); var tags = [].concat(o.tags, this._tagsFromMarkup(this._element)); this._tags = []; InkArray.each(tags, Ink.bindMethod(this, '_addTag')); InkEvent.observe(this._input, 'keyup', Ink.bindEvent(this._onKeyUp, this)); InkEvent.observe(this._input, 'change', Ink.bindEvent(this._onKeyUp, this)); InkEvent.observe(this._input, 'keydown', Ink.bindEvent(this._onKeyDown, this)); InkEvent.observe(this._input, 'blur', Ink.bindEvent(this._onBlur, this)); InkEvent.observe(this._viewElm, 'click', Ink.bindEvent(this._refocus, this)); }, destroy: function () { InkElement.remove(this._viewElm); Css.removeClassName(this._element, 'hide-all'); }, _tagsFromMarkup: function (element) { var tagname = element.tagName.toLowerCase(); if (tagname === 'input') { return this._readInput(element.value); } else if (tagname === 'select') { return InkArray.map(element.getElementsByTagName('option'), function (option) { return InkElement.textContent(option); }); } else { throw new Error('Cannot read tags from a ' + tagname + ' tag. Unknown tag'); } }, _tagsToMarkup: function (tags, element) { var tagname = element.tagName.toLowerCase(); if (tagname === 'input') { if (this._options.separator) { element.value = tags.join(this._options.outSeparator); } } else if (tagname === 'select') { element.innerHTML = ''; InkArray.each(tags, function (tag) { var opt = InkElement.create('option', {selected: 'selected'}); InkElement.setTextContent(opt, tag); element.appendChild(opt); }); } else { throw new Error('TagField: Cannot read tags from a ' + tagname + ' tag. Unknown tag'); } }, _addTag: function (tag) { if (this._options.maxTags !== -1 && this._tags.length >= this._options.maxTags) { return; } if ((!this._options.allowRepeated && InkArray.inArray(tag, this._tags, tag)) || !tag) { return false; } var elm = InkElement.create('span', { className: 'ink-tag', setTextContent: tag + ' ' }); var remove = InkElement.create('span', { className: 'remove fa fa-times', insertBottom: elm }); InkEvent.observe(remove, 'click', Ink.bindEvent(this._removeTag, this, null)); var spc = document.createTextNode(' '); this._tags.push(tag); this._viewElm.insertBefore(elm, this._input); this._viewElm.insertBefore(spc, this._input); this._tagsToMarkup(this._tags, this._element); }, _readInput: function (text) { if (this._options.separator) { return InkArray.filter(text.split(this._options.separator), isTruthy); } else { return [text]; } }, _onKeyUp: function () { // TODO control input box size if (!this._options.autoSplit) { return; } var split = this._input.value.split(this._options.separator); if (split.length <= 1) { return; } var last = split[split.length - 1]; split = split.splice(0, split.length - 1); split = InkArray.filter(split, isTruthy); InkArray.each(split, Ink.bind(this._addTag, this)); this._input.value = last; }, _onKeyDown: function (event) { if (event.which === enterKey) { return this._onEnterKeyDown(event); } else if (event.which === backspaceKey) { return this._onBackspaceKeyDown(); } else if (this._removeConfirm) { // user pressed another key, cancel removal from a backspace key this._unsetRemovingVisual(this._tags.length - 1); } }, /** * When the user presses backspace twice on the empty input, we delete the last tag on the field. * @method onBackspaceKeyDown * @private */ _onBackspaceKeyDown: function () { if (this._input.value) { return; } if (this._removeConfirm) { this._unsetRemovingVisual(this._tags.length - 1); this._removeTag(this._tags.length - 1); this._removeConfirm = null; } else { this._setRemovingVisual(this._tags.length - 1); } }, _onEnterKeyDown: function (event) { var tag = this._input.value; if (tag) { this._addTag(tag); this._input.value = ''; } InkEvent.stopDefault(event); }, _onBlur: function () { this._addTag(this._input.value); this._input.value = ''; }, /* For when the user presses backspace. * Set the style of the tag so that it seems like it's going to be removed * if they press backspace again. */ _setRemovingVisual: function (tagIndex) { var elm = this._viewElm.children[tagIndex]; if (!elm) { return; } Css.addClassName(elm, 'tag-deleting'); this._removeRemovingVisualTimeout = setTimeout(Ink.bindMethod(this, '_unsetRemovingVisual', tagIndex), 4000); InkEvent.observe(this._input, 'blur', Ink.bindMethod(this, '_unsetRemovingVisual', tagIndex)); this._removeConfirm = true; }, _unsetRemovingVisual: function (tagIndex) { var elm = this._viewElm.children[tagIndex]; if (elm) { Css.removeClassName(elm, 'tag-deleting'); clearTimeout(this._removeRemovingVisualTimeout); } this._removeConfirm = null; }, _removeTag: function (event) { var index; if (typeof event === 'object') { // click event on close button var elm = InkEvent.element(event).parentNode; index = InkElement.parentIndexOf(this._viewElm, elm); } else if (typeof event === 'number') { // manual removal index = event; } this._tags = InkArray.remove(this._tags, index, 1); InkElement.remove(this._viewElm.children[index]); this._tagsToMarkup(this._tags, this._element); }, _refocus: function (event) { this._input.focus(); InkEvent.stop(event); return false; } }; Common.createUIComponent(TagField); return TagField; }); /** * Toggle the visibility of elements. * @module Ink.UI.Toggle_1 * @version 1 */ Ink.createModule('Ink.UI.Toggle', '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'], function(Common, InkEvent, Css, InkElement, Selector, InkArray ) { 'use strict'; /** * * You need two elements to use Toggle: the `trigger` element, and the `target` element (or elements). The default behaviour is to toggle the `target`(s) when you click the `trigger`. * * The toggle has a state. It is either "on" or "off". It works by switching between the CSS classes in `classNameOn` and `classNameOff` according to the current state. * * When you initialize the Toggle, it will check if the targets are visible to figure out what the initial state is. You can force the toggle to consider itself turned "on" or "off" by setting the `initialState` option to `true` or `false`, respectively. * * You can get the current state of the Toggle by calling `getState`, or by checking if your `trigger` element has the "active" class. * The state can be changed through JavaScript. Just call `setState(true)` * to turn the Toggle on (or `setState(false)` to turn it off). * * @class Ink.UI.Toggle * @constructor * @version 1 * @param {String|DOMElement} selector Trigger element. By clicking this, the target (or targets) are triggered. * @param {Object} [options] Options object, containing: * * @param {String} options.target CSS Selector that specifies the elements that this component will toggle * @param {String} [options.classNameOn] CSS class to toggle when on. Defaults to 'show-all'. * @param {String} [options.classNameOff] CSS class to toggle when off. Defaults to 'hide-all'. * @param {String} [options.triggerEvent] Event that will trigger the toggling. Defaults to 'click'. * @param {Boolean} [options.closeOnClick] Flag to toggle the targe off when clicking outside the toggled content. Defaults to true. * @param {String} [options.closeOnInsideClick] Toggle off when a child element matching this selector is clicked. Set to null to deactivate the check. Defaults to 'a[href]'. * @param {Boolean} [options.initialState] Flag to define initial state. false: off, true: on, null: markup. Defaults to null. * @param {Function} [options.onChangeState] Callback when the toggle state changes. Return `false` to cancel the event. * * @sample Ink_UI_Toggle_1_constructor.html */ function Toggle(){ Common.BaseUIComponent.apply(this, arguments); } Toggle._name = 'Toggle_1'; Toggle._optionDefinition = { target: ['Elements'], triggerEvent: ['String', 'click'], closeOnClick: ['Boolean', true], isAccordion: ['Boolean', false], initialState: ['Boolean', null], // May be true, false, or null to be what it is right now classNameOn: ['String', 'show-all'], classNameOff: ['String', 'hide-all'], closeOnInsideClick: ['String', 'a[href]'], // closes the toggle when a target is clicked and it is a link onChangeState: ['Function', null] }; Toggle.prototype = { /** * Init function called by the constructor * * @method _init * @private */ _init: function(){ var i, len; this._targets = Common.elsOrSelector(this._options.target); // Boolean option handling this._options.closeOnClick = this._options.closeOnClick.toString() === 'true'; // Actually a throolean if (this._options.initialState !== null){ this._options.initialState = this._options.initialState.toString() === 'true'; } else { this._options.initialState = Css.getStyle(this._targets[0], 'display') !== 'none'; } if (this._options.classNameOn !== 'show-all' || this._options.classNameOff !== 'hide-all') { for (i = 0, len = this._targets.length; i < len; i++) { Css.removeClassName(this._targets[i], 'show-all'); Css.removeClassName(this._targets[i], 'hide-all'); } } this._accordion = ( Css.hasClassName(this._element.parentNode,'accordion') || Css.hasClassName(this._targets[0].parentNode,'accordion') ); this._firstTime = true; this._bindEvents(); if (this._options.initialState !== null) { this.setState(this._options.initialState, true); } else { // Add initial classes matching the current "display" of the object. var state = Css.getStyle(this._targets[0], 'display') !== 'none'; this.setState(state, true); } // Aditionally, remove any inline "display" style. for (i = 0, len = this._targets.length; i < len; i++) { if (this._targets[i].style.display) { this._targets[i].style.display = ''; // becomes default } } this._element.setAttribute('data-is-toggle-trigger', 'true'); }, /** * @method _bindEvents * @private */ _bindEvents: function () { if ( this._options.triggerEvent ) { InkEvent.observe( this._element, this._options.triggerEvent, Ink.bind(this._onTriggerEvent, this)); } if( this._options.closeOnClick ){ InkEvent.observe( document, 'click', Ink.bind(this._onOutsideClick, this)); } if( this._options.closeOnInsideClick && this._options.closeOnInsideClick !== 'false') { var sel = this._options.closeOnInsideClick; if (sel.toString() === 'true') { sel = '*'; } InkEvent.observeMulti(this._targets, 'click', Ink.bind(function (e) { if ( InkElement.findUpwardsBySelector(InkEvent.element(e), sel) ) { this.setState(false, true); } }, this)); } }, /** * Event handler. It's responsible for handling the `triggerEvent` as defined in the options. * * This will trigger the toggle. * * @method _onTriggerEvent * @param {Event} event * @private */ _onTriggerEvent: function( event ){ // When the togglee is a child of the toggler, we get the togglee's events here. We have to check that this event is for us. var target = InkEvent.element(event); var isAncestorOfClickedElement = InkArray.some(this._targets, function (thisOne) { return thisOne === target || InkElement.isAncestorOf(thisOne, target); }); if (isAncestorOfClickedElement) { return; } if (this._accordion) { this._updateAccordion(); } var has = this.getState(); this.setState(!has, true); if (!has && this._firstTime) { this._firstTime = false; } InkEvent.stopDefault(event); }, /** * Be compatible with accordions * * @method _updateAccordion **/ _updateAccordion: function () { var elms, accordionElement; if( Css.hasClassName(this._targets[0].parentNode,'accordion') ){ accordionElement = this._targets[0].parentNode; } else { accordionElement = this._targets[0].parentNode.parentNode; } elms = Selector.select('.toggle, .ink-toggle',accordionElement); for(var i=0; i 0) && (targetElm[0] !== this._targets[0]) ){ targetElm[0].style.display = 'none'; } } }, /** * Click handler. Will handle clicks outside the toggle component. * * @method _onOutsideClick * @param {Event} event * @private */ _onOutsideClick: function( event ){ var tgtEl = InkEvent.element(event), shades; if (InkElement.findUpwardsBySelector(tgtEl, '[data-is-toggle-trigger="true"]')) return; var ancestorOfTargets = InkArray.some(this._targets, function (target) { return InkElement.isAncestorOf(target, tgtEl) || target === tgtEl; }); if( (this._element === tgtEl) || InkElement.isAncestorOf(this._element, tgtEl) || ancestorOfTargets) { return; } else if( (shades = Ink.ss('.ink-shade')).length ) { var shadesLength = shades.length; for( var i = 0; i < shadesLength; i++ ){ if( InkElement.isAncestorOf(shades[i],tgtEl) && InkElement.isAncestorOf(shades[i],this._element) ){ return; } } } this.setState(false, true); // dismiss }, /** * Sets the state of the toggle. (on/off) * * @method setState * @param newState {Boolean} New state (on/off) */ setState: function (on, callHandler) { if (on === this.getState()) { return; } if (callHandler && typeof this._options.onChangeState === 'function') { var ret = this._options.onChangeState(on); if (ret === false) { return false; } // Canceled by the event handler } for (var i = 0, len = this._targets.length; i < len; i++) { Css.addRemoveClassName(this._targets[i], this._options.classNameOn, on); Css.addRemoveClassName(this._targets[i], this._options.classNameOff, !on); } Css.addRemoveClassName(this._element, 'active', on); }, /** * Gets the state of the toggle. (on/off) * * @method getState * * @return {Boolean} whether the toggle is toggled on. */ getState: function () { return Css.hasClassName(this._element, 'active'); } }; Common.createUIComponent(Toggle); return Toggle; }); /** * Content Tooltips * @module Ink.UI.Tooltip_1 * @version 1 */ Ink.createModule('Ink.UI.Tooltip', '1', ['Ink.UI.Common_1', 'Ink.Dom.Event_1', 'Ink.Dom.Element_1', 'Ink.Dom.Selector_1', 'Ink.Util.Array_1', 'Ink.Dom.Css_1', 'Ink.Dom.Browser_1'], function (Common, InkEvent, InkElement, Selector, InkArray, Css) { 'use strict'; /** * Tooltips are useful as a means to display information about functionality while avoiding clutter. * * Tooltips show up when you hover elements which "have" tooltips. * * This class will "give" a tooltip to many elements, selected by its first argument (`target`). This is contrary to the other UI modules in Ink, which are created once per element. * * You can define options either through the second argument of the Tooltip constructor, or as data-attributes in each `target` element. Options set through data-attributes all start with "data-tip", and override options passed into the Tooltip constructor. * * @class Ink.UI.Tooltip * @constructor * * @param {DOMElement|String} target Target element or selector of elements, to display the tooltips on. * @param {Object} [options] Options object * @param {String} [options.text] Text content for the tooltip. * @param {String} [options.html] HTML for the tooltip. Same as above, but won't escape HTML. * @param {String} [options.where] Positioning for the tooltip. Options are 'up', 'down', 'left', 'right', 'mousemove' (follows the cursor), and 'mousefix' (stays fixed). Defaults to 'up'. * * @param {String} [options.color] Color of the tooltip. Options are red, orange, blue, green and black. Default is white. * @param {Number} [options.fade] Number of seconds to fade in/out. Defaults to 0.3. * @param {Boolean} [options.forever] Flag to prevent the tooltip from being erased when the mouse hovers away from the target. * @param {Number} [options.timeout] Number of seconds the tooltip will stay open. Useful together with options.forever. Defaults to 0. * @param {Number} [options.delay] Time the tooltip waits until it is displayed. Useful to avoid getting the attention of the user unnecessarily * @param {DOMElement|Selector} [options.template] Element or selector containing HTML to be cloned into the tooltips. Can be a hidden element, because CSS `display` is set to `block`. * @param {String} [options.templatefield] Selector within the template element to choose where the text is inserted into the tooltip. Useful when a wrapper DIV is required. * @param {Number} [options.left] Spacing from the target to the tooltip, when `where` is `mousemove` or `mousefix`. Defaults to 10. * @param {Number} [options.top] Spacing from the target to the tooltip, when `where` is `mousemove` or `mousefix`. Defaults to 10. * @param {Number} [options.spacing] Spacing between the tooltip and the target element, when `where` is not `mousemove` or `mousefix`. Defaults to 8. * * @sample Ink_UI_Tooltip_1.html */ function Tooltip(element, options) { this._init(element, options || {}); } function EachTooltip(root, elm) { this._init(root, elm); } var transitionDurationName, transitionPropertyName, transitionTimingFunctionName; (function () { // Feature detection var test = document.createElement('DIV'); var names = ['transition', 'oTransition', 'msTransition', 'mozTransition', 'webkitTransition']; for (var i = 0; i < names.length; i++) { if (typeof test.style[names[i] + 'Duration'] !== 'undefined') { transitionDurationName = names[i] + 'Duration'; transitionPropertyName = names[i] + 'Property'; transitionTimingFunctionName = names[i] + 'TimingFunction'; break; } } }()); // Body or documentElement var bodies = document.getElementsByTagName('body'); var body = bodies.length ? bodies[0] : document.documentElement; Tooltip.prototype = { _init: function(element, options) { var elements; this.options = Ink.extendObj({ where: 'up', zIndex: 10000, left: 10, top: 10, spacing: 8, forever: 0, color: '', timeout: 0, delay: 0, template: null, templatefield: null, fade: 0.3, text: '' }, options || {}); if (typeof element === 'string') { elements = Selector.select(element); } else if (typeof element === 'object') { elements = [element]; } else { throw 'Element expected'; } this.tooltips = []; for (var i = 0, len = elements.length; i < len; i++) { this.tooltips[i] = new EachTooltip(this, elements[i]); } }, /** * Destroys the tooltips created by this instance * * @method destroy */ destroy: function () { InkArray.each(this.tooltips, function (tooltip) { tooltip._destroy(); }); this.tooltips = null; this.options = null; } }; EachTooltip.prototype = { _oppositeDirections: { left: 'right', right: 'left', up: 'down', down: 'up' }, _init: function(root, elm) { InkEvent.observe(elm, 'mouseover', Ink.bindEvent(this._onMouseOver, this)); InkEvent.observe(elm, 'mouseout', Ink.bindEvent(this._onMouseOut, this)); InkEvent.observe(elm, 'mousemove', Ink.bindEvent(this._onMouseMove, this)); this.root = root; this.element = elm; this._delayTimeout = null; this.tooltip = null; Common.registerInstance(this, this.element); }, _makeTooltip: function (mousePosition) { if (!this._getOpt('text') && !this._getOpt('html') && !InkElement.hasAttribute(this.element, 'title')) { return false; } var tooltip = this._createTooltipElement(); if (this.tooltip) { this._removeTooltip(); } this.tooltip = tooltip; this._fadeInTooltipElement(tooltip); this._placeTooltipElement(tooltip, mousePosition); InkEvent.observe(tooltip, 'mouseover', Ink.bindEvent(this._onTooltipMouseOver, this)); var timeout = this._getFloatOpt('timeout'); if (timeout) { setTimeout(Ink.bind(function () { if (this.tooltip === tooltip) { this._removeTooltip(); } }, this), timeout * 1000); } }, _createTooltipElement: function () { var template = this._getOpt('template'), // User template instead of our HTML templatefield = this._getOpt('templatefield'), tooltip, // The element we float field; // Element where we write our message. Child or same as the above if (template) { // The user told us of a template to use. We copy it. var temp = document.createElement('DIV'); temp.innerHTML = Common.elOrSelector(template, 'options.template').outerHTML; tooltip = temp.firstChild; if (templatefield) { field = Selector.select(templatefield, tooltip); if (field) { field = field[0]; } else { throw 'options.templatefield must be a valid selector within options.template'; } } else { field = tooltip; // Assume same element if user did not specify a field } } else { // We create the default structure tooltip = document.createElement('DIV'); Css.addClassName(tooltip, 'ink-tooltip'); Css.addClassName(tooltip, this._getOpt('color')); field = document.createElement('DIV'); Css.addClassName(field, 'content'); tooltip.appendChild(field); } if (this._getOpt('html')) { field.innerHTML = this._getOpt('html'); } else if (this._getOpt('text')) { InkElement.setTextContent(field, this._getOpt('text')); } else { InkElement.setTextContent(field, this.element.getAttribute('title')); } tooltip.style.display = 'block'; tooltip.style.position = 'absolute'; tooltip.style.zIndex = this._getIntOpt('zIndex'); return tooltip; }, _fadeInTooltipElement: function (tooltip) { var fadeTime = this._getFloatOpt('fade'); if (transitionDurationName && fadeTime) { tooltip.style.opacity = '0'; tooltip.style[transitionDurationName] = fadeTime + 's'; tooltip.style[transitionPropertyName] = 'opacity'; tooltip.style[transitionTimingFunctionName] = 'ease-in-out'; setTimeout(function () { tooltip.style.opacity = '1'; }, 0); // Wait a tick } }, _placeTooltipElement: function (tooltip, mousePosition) { var where = this._getOpt('where'); if (where === 'mousemove' || where === 'mousefix') { var mPos = mousePosition; this._setPos(mPos[0], mPos[1]); body.appendChild(tooltip); } else if (where.match(/(up|down|left|right)/)) { body.appendChild(tooltip); var targetElementPos = InkElement.offset(this.element); var tleft = targetElementPos[0], ttop = targetElementPos[1]; var centerh = (InkElement.elementWidth(this.element) / 2) - (InkElement.elementWidth(tooltip) / 2), centerv = (InkElement.elementHeight(this.element) / 2) - (InkElement.elementHeight(tooltip) / 2); var spacing = this._getIntOpt('spacing'); var tooltipDims = InkElement.elementDimensions(tooltip); var elementDims = InkElement.elementDimensions(this.element); var maxX = InkElement.scrollWidth() + InkElement.viewportWidth(); var maxY = InkElement.scrollHeight() + InkElement.viewportHeight(); where = this._getWhereValueInsideViewport(where, { left: tleft - tooltipDims[0], right: tleft + tooltipDims[0], top: ttop + tooltipDims[1], bottom: ttop + tooltipDims[1] }, { right: maxX, bottom: maxY }); if (where === 'up') { ttop -= tooltipDims[1]; ttop -= spacing; tleft += centerh; } else if (where === 'down') { ttop += elementDims[1]; ttop += spacing; tleft += centerh; } else if (where === 'left') { tleft -= tooltipDims[0]; tleft -= spacing; ttop += centerv; } else if (where === 'right') { tleft += elementDims[0]; tleft += spacing; ttop += centerv; } var arrow = null; if (where.match(/(up|down|left|right)/)) { arrow = document.createElement('SPAN'); Css.addClassName(arrow, 'arrow'); Css.addClassName(arrow, this._oppositeDirections[where]); tooltip.appendChild(arrow); } var tooltipLeft = tleft; var tooltipTop = ttop; var toBottom = (tooltipTop + tooltipDims[1]) - maxY; var toRight = (tooltipLeft + tooltipDims[0]) - maxX; var toLeft = 0 - tooltipLeft; var toTop = 0 - tooltipTop; if (toBottom > 0) { if (arrow) { arrow.style.top = (tooltipDims[1] / 2) + toBottom + 'px'; } tooltipTop -= toBottom; } else if (toTop > 0) { if (arrow) { arrow.style.top = (tooltipDims[1] / 2) - toTop + 'px'; } tooltipTop += toTop; } else if (toRight > 0) { if (arrow) { arrow.style.left = (tooltipDims[0] / 2) + toRight + 'px'; } tooltipLeft -= toRight; } else if (toLeft > 0) { if (arrow) { arrow.style.left = (tooltipDims[0] / 2) - toLeft + 'px'; } tooltipLeft += toLeft; } tooltip.style.left = tooltipLeft + 'px'; tooltip.style.top = tooltipTop + 'px'; } }, /** * Get a value for "where" (left/right/up/down) which doesn't put the * tooltip off the screen * * @method _getWhereValueInsideViewport * @param where {String} "where" value which was given by the user and we might change * @param bbox {BoundingBox} A bounding box like what you get from getBoundingClientRect ({top, bottom, left, right}) with pixel positions from the top left corner of the viewport. * @param viewport {BoundingBox} Bounding box for the viewport. "top" and "left" are omitted because these coordinates are relative to the top-left corner of the viewport so they are zero. * * @TODO: we can't use getBoundingClientRect in this case because it returns {0,0,0,0} on our uncreated tooltip. */ _getWhereValueInsideViewport: function (where, bbox, viewport) { if (where === 'left' && bbox.left < 0) { return 'right'; } else if (where === 'right' && bbox.right > viewport.right) { return 'left'; } else if (where === 'up' && bbox.top < 0) { return 'down'; } else if (where === 'down' && bbox.bottom > viewport.bottom) { return 'up'; } return where; }, _removeTooltip: function() { var tooltip = this.tooltip; if (!tooltip) {return;} var remove = Ink.bind(InkElement.remove, {}, tooltip); if (this._getOpt('where') !== 'mousemove' && transitionDurationName) { tooltip.style.opacity = 0; // remove() will operate on correct tooltip, although this.tooltip === null then setTimeout(remove, this._getFloatOpt('fade') * 1000); } else { remove(); } this.tooltip = null; }, _getOpt: function (option) { var dataAttrVal = InkElement.data(this.element)[InkElement._camelCase('tip-' + option)]; if (dataAttrVal /* either null or "" may signify the absense of this attribute*/) { return dataAttrVal; } var instanceOption = this.root.options[option]; if (typeof instanceOption !== 'undefined') { return instanceOption; } }, _getIntOpt: function (option) { return parseInt(this._getOpt(option), 10); }, _getFloatOpt: function (option) { return parseFloat(this._getOpt(option), 10); }, _destroy: function () { if (this.tooltip) { InkElement.remove(this.tooltip); } this.root = null; // Cyclic reference = memory leaks this.element = null; this.tooltip = null; }, _onMouseOver: function(e) { // on IE < 10 you can't access the mouse event not even a tick after it fired var mousePosition = this._getMousePosition(e); var delay = this._getFloatOpt('delay'); if (delay) { this._delayTimeout = setTimeout(Ink.bind(function () { if (!this.tooltip) { this._makeTooltip(mousePosition); } this._delayTimeout = null; }, this), delay * 1000); } else { this._makeTooltip(mousePosition); } }, _onMouseMove: function(e) { if (this._getOpt('where') === 'mousemove' && this.tooltip) { var mPos = this._getMousePosition(e); this._setPos(mPos[0], mPos[1]); } }, _onMouseOut: function () { if (!this._getIntOpt('forever')) { this._removeTooltip(); } if (this._delayTimeout) { clearTimeout(this._delayTimeout); this._delayTimeout = null; } }, _onTooltipMouseOver: function () { if (this.tooltip) { // If tooltip is already being removed, this has no effect this._removeTooltip(); } }, _setPos: function(left, top) { left += this._getIntOpt('left'); top += this._getIntOpt('top'); var pageDims = this._getPageXY(); if (this.tooltip) { var elmDims = [InkElement.elementWidth(this.tooltip), InkElement.elementHeight(this.tooltip)]; var scrollDim = this._getScroll(); if((elmDims[0] + left - scrollDim[0]) >= (pageDims[0] - 20)) { left = (left - elmDims[0] - this._getIntOpt('left') - 10); } if((elmDims[1] + top - scrollDim[1]) >= (pageDims[1] - 20)) { top = (top - elmDims[1] - this._getIntOpt('top') - 10); } this.tooltip.style.left = left + 'px'; this.tooltip.style.top = top + 'px'; } }, _getPageXY: function() { var cWidth = 0; var cHeight = 0; if( typeof( window.innerWidth ) === 'number' ) { cWidth = window.innerWidth; cHeight = window.innerHeight; } else if( document.documentElement && ( document.documentElement.clientWidth || document.documentElement.clientHeight ) ) { cWidth = document.documentElement.clientWidth; cHeight = document.documentElement.clientHeight; } else if( document.body && ( document.body.clientWidth || document.body.clientHeight ) ) { cWidth = document.body.clientWidth; cHeight = document.body.clientHeight; } return [parseInt(cWidth, 10), parseInt(cHeight, 10)]; }, _getScroll: function() { var dd = document.documentElement, db = document.body; if (dd && (dd.scrollLeft || dd.scrollTop)) { return [dd.scrollLeft, dd.scrollTop]; } else if (db) { return [db.scrollLeft, db.scrollTop]; } else { return [0, 0]; } }, _getMousePosition: function(e) { return [parseInt(InkEvent.pointerX(e), 10), parseInt(InkEvent.pointerY(e), 10)]; } }; return Tooltip; }); /** * Elements in a tree structure * @module Ink.UI.TreeView_1 * @version 1 */ Ink.createModule('Ink.UI.TreeView', '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'], function(Common, Event, Css, Element, Selector, InkArray ) { 'use strict'; /** * Shows elements in a tree structure which can be expanded and contracted. * A TreeView is built with "node"s and "children". "node"s are `li` tags, and "children" are `ul` tags. * You can build your TreeView out of a regular UL and LI element structure which you already use to display lists with several levels. * If you want a node to be open when the TreeView is built, just add the data-open="true" attribute to it. * * @class Ink.UI.TreeView * @constructor * @version 1 * @param {String|DOMElement} selector Element or selector. * @param {String} [options] Options object, containing: * @param {String} [options.node] Selector for the nodes. Defaults to 'li'. * @param {String} [options.children] Selector for the children. Defaults to 'ul'. * @param {String} [options.parentClass] CSS classes to be added to parent nodes. Defaults to 'parent'. * @param {String} [options.openClass] CSS classes to be added to the icon when a parent is open. Defaults to 'fa fa-minus-circle'. * @param {String} [options.closedClass] CSS classes to be added to the icon when a parent is closed. Defaults to 'fa fa-plus-circle'. * @param {String} [options.hideClass] CSS Class to toggle visibility of the children. Defaults to 'hide-all'. * @param {String} [options.iconTag] The name of icon tag. The component tries to find a tag with that name as a direct child of the node. If it doesn't find it, it creates it. Defaults to 'i'. * @param {Boolean} [options.stopDefault] Flag to stops the default behavior of the click handler. Defaults to true. * @example * * * * @sample Ink_UI_TreeView_1.html */ function TreeView() { Common.BaseUIComponent.apply(this, arguments); } TreeView._name = 'TreeView_1'; TreeView._optionDefinition = { 'node': ['String', 'li'], // [3.0.1] Deprecate this terrible, terrible name 'child': ['String',null], 'children': ['String','ul'], 'parentClass': ['String','parent'], 'openNodeClass': ['String', 'open'], 'openClass': ['String','fa fa-minus-circle'], 'closedClass': ['String','fa fa-plus-circle'], 'hideClass': ['String','hide-all'], 'iconTag': ['String', 'i'], 'stopDefault' : ['Boolean', true] }; TreeView.prototype = { /** * Init function called by the constructor. Sets the necessary event handlers. * * @method _init * @private */ _init: function(){ if (this._options.child) { Ink.warn('Ink.UI.TreeView: options.child is being renamed to options.children.'); this._options.children = this._options.child; } this._handlers = { click: Ink.bindEvent(this._onClick,this) }; Event.on(this._element, 'click', this._options.node, this._handlers.click); InkArray.each(Ink.ss(this._options.node, this._element), Ink.bind(function(item){ if( this.isParent(item) ) { Css.addClassName(item, this._options.parentClass); var isOpen = this.isOpen(item); if( !this._getIcon(item) ){ Element.create(this._options.iconTag, { insertTop: item }); } this._setNodeOpen(item, isOpen); } },this)); }, _getIcon: function (node) { return Ink.s('> ' + this._options.iconTag, node); }, /** * Checks if a node is open. * * @method isOpen * @param {DOMElement} node The tree node to check **/ isOpen: function (node) { if (!this._getChild(node)) { throw new Error('not a node!'); } return Element.data(node).open === 'true' || Css.hasClassName(node, this._options.openNodeClass); }, /** * Checks if a node is a parent. * * @method isParent * @param {DOMElement} node Node to check **/ isParent: function (node) { return Css.hasClassName(node, this._options.parentClass) || this._getChild(node) != null; }, _setNodeOpen: function (node, beOpen) { var child = this._getChild(node); if (child) { Css.setClassName(child, this._options.hideClass, !beOpen); var icon = this._getIcon(node); node.setAttribute('data-open', beOpen); /* * Don't refactor this to * * setClassName(el, className, status); setClassName(el, className, !status); * * because it won't work with multiple classes. * * Doing: * setClassName(el, 'fa fa-whatever', true);setClassName(el, 'fa fa-whatever-else', false); * * will remove 'fa' although it is a class we want. */ var toAdd = beOpen ? this._options.openClass : this._options.closedClass; var toRemove = beOpen ? this._options.closedClass : this._options.openClass; Css.removeClassName(icon, toRemove); Css.addClassName(icon, toAdd); Css.setClassName(node, this._options.openNodeClass, beOpen); } else { Ink.error('Ink.UI.TreeView: node', node, 'is not a node!'); } }, /** * Opens one of the tree nodes * * Make sure you pass the node's DOMElement * @method open * @param {DOMElement} node The node you wish to open. **/ open: function (node) { this._setNodeOpen(node, true); }, /** * Closes one of the tree nodes * * Make sure you pass the node's DOMElement * @method close * @param {DOMElement} node The node you wish to close. **/ close: function (node) { this._setNodeOpen(node, false); }, /** * Toggles a node state * * @method toggle * @param {DOMElement} node The node to toggle. **/ toggle: function (node) { if (this.isOpen(node)) { this.close(node); } else { this.open(node); } }, _getChild: function (node) { return Selector.select(this._options.children, node)[0] || null; }, /** * Handles the click event (as specified in the _init function). * * @method _onClick * @param {Event} event * @private */ _onClick: function(ev){ /** * Summary: * If the clicked element is a "node" as defined in the options, will check if it has any "child". * If so, will toggle its state and stop the event's default behavior if the stopDefault option is true. **/ if (!this.isParent(ev.currentTarget) || Selector.matchesSelector(ev.target, this._options.node) || Selector.matchesSelector(ev.target, this._options.children)) { return; } if (this._options.stopDefault){ ev.preventDefault(); } this.toggle(ev.currentTarget); } }; Common.createUIComponent(TreeView); return TreeView; }); Ink.createModule('Ink.UI.Upload', '1', [ 'Ink.Dom.Event_1', 'Ink.Dom.Element_1', 'Ink.Dom.Browser_1', 'Ink.UI.Common_1' ], function(Event, Element, Browser, Common) { 'use strict'; var DirectoryReader = function(options) { this.init(options); }; DirectoryReader.prototype = { init: function(options) { this._options = Ink.extendObj({ entry: undefined, maxDepth: 10 }, options || {}); try { this._read(); } catch(e) { Ink.error(e); } }, _read: function() { if(!this._options.entry) { Ink.error('You must specify the entry!'); return; } try { this._readDirectories(); } catch(e) { Ink.error(e); } }, _readDirectories: function() { var entries = [], running = false, maxDepth = 0; /* TODO return as tree because much better well */ var _readEntries = Ink.bind(function(currentEntry) { var dir = currentEntry.createReader(); running = true; dir.readEntries(Ink.bind(function(res) { if(res.length > 0) { for(var i = 0, len = res.length; i=0; i--) { if(typeof(arr[i]) === 'undefined' || arr[i] === null || arr[i] === '') { arr.splice(i, 1); } } return arr; } }; var Queue = { lists: [], items: [], /** * Create new queue list * @function create * @public * @param {String} list name * @param {Function} function to iterate on items * @return {Object} list id */ create: function(name) { var id; name = String(name); this.lists.push({name: name}); id = this.lists.length - 1; return id; }, getItems: function(parentId) { if(!parentId) { return this.items; } var items = []; for(var i = 0, len = this.items.length; i=0; i--) { if(this.items[i] && id === this.items[i].parentId) { this.remove(this.items[i].parentId, this.items[i].pid); } } if(!keepList) { this.lists.splice(id, 1); } return true; } catch(e) { Ink.error('Purge: invalid id'); return false; } }, /** * add an item to a list * @function add * @public * @param {String} name * @param {Object} item * @return {Number} pid */ add: function(parentId, item, priority) { if(!this.lists[parentId]) { return false; } if(typeof(item) !== 'object') { item = String(item); } var pid = parseInt(Math.round(Math.random() * 100000) + "" + Math.round(Math.random() * 100000), 10); priority = priority || 0; this.items.push({parentId: parentId, item: item, priority: priority || 0, pid: pid}); return pid; }, /** * View list * @function view * @public * @param {Number} list id * @param {Number} process id * @return {Object} item */ view: function(parentId, pid) { var id = this._searchByPid(parentId, pid); if(id === false) { return false; } return this.items[id]; }, /** * Remove an item * @function remove * @public * @param {Object} item * @return {Object|Boolean} removed item or false if not found */ remove: function(parentId, pid) { try { var id = this._searchByPid(parentId, pid); if(id === false) { return false; } this.items.splice(id, 1); return true; } catch(e) { Ink.error('Remove: invalid id'); return false; } }, _searchByPid: function(parentId, pid) { if(!parentId && typeof(parentId) === 'boolean' || !pid) { return false; } parentId = parseInt(parentId, 10); pid = parseInt(pid, 10); if(isNaN(parentId) || isNaN(pid)) { return false; } for(var i = 0, len = this.items.length; i this._options.minSizeToUseChunks; }, _dropEventHandler: function(ev) { Event.stop(ev); this.publish('DropComplete', ev.dataTransfer); var data = ev.dataTransfer; if(!data || !data.files || !data.files.length) { return false; } this._files = data.files; this._files = Array.prototype.slice.call(this._files || [], 0); // check if webkitGetAsEntry exists on first item if(data.items && data.items[0] && data.items[0].webkitGetAsEntry) { if(!this._options.foldersEnabled) { return setTimeout(Ink.bind(this._addFilesToQueue, this, this._files), 0); } var entry, folders = []; for(var i = ev.dataTransfer.items.length-1; i>=0; i--) { entry = ev.dataTransfer.items[i].webkitGetAsEntry(); if(entry && entry.isDirectory) { folders.push(entry); this._files[i].isDirectory = true; this._files.splice(i, 1); } } // starting callback hell this._addFolderToQueue(folders, Ink.bind(function() { setTimeout(Ink.bind(this._addFilesToQueue, this, this._files), 0); }, this)); } else { setTimeout(Ink.bind(this._addFilesToQueue, this, this._files), 0); } return true; }, _addFolderToQueue: function(folders, cb) { var files = [], invalidFolders = {}; if(!folders || !folders.length) { cb(); return files; } var getFiles = function(entries) { var files = []; for(var i = 0, len = entries.length; i this._options.maxFilesize) { this.publish('MaxSizeFailure', file, this._options.maxFilesize); continue; } fileID = parseInt(Math.round(Math.random() * 100000) + "" + Math.round(Math.random() * 100000), 10); o = { id: i, data: file, fileID: fileID, directory: file.isDirectory }; Queue.add(this._queueId, o); this.publish('FileAddedToQueue', o); } this._processQueue(true); this._files = []; }, _processQueue: function(internalUpload) { if(this._queueRunning) { return false; } this.running = 0; var max = 1, i = 0, items, queueLen = Queue.items.length; this._queueRunning = true; this.interval = setInterval(Ink.bind(function() { if(Queue.items.length === i && this.running === 0) { Queue.purge(this._queueId, true); this._queueRunning = false; clearInterval(this.interval); this.publish('QueueEnd', this._queueId, queueLen); } items = Queue.getItems(this._queueId); if(this.running < max && items[i]) { if(!items[i].canceled) { _doRequest.call(this, items[i].pid, items[i].item.data, items[i].item.fileID, items[i].item.directory, internalUpload); this.running++; i++; } else { var j = i; while(items[j] && items[j].canceled) { i++; j++; } } return true; } return false; }, this), 100); var _doRequest = function(pid, data, fileID, directory, internalUpload) { var o = { file: data, fileID: fileID, cb: Ink.bind(function() { this.running--; }, this) }; if(internalUpload) { if(directory) { // do magic o.cb(); } else { this._upload(o); } } }; return true; }, _upload: function(o) { var file = o.file, xhr = new XMLHttpRequest(), fileID = o.fileID; this.publish('BeforeUpload', file, this._options.extraData, fileID, xhr, this._supportChunks(file.size)); var forceAbort = function(showError) { if(o.cb && typeof(o.cb === 'function')) { o.cb(); } this.publish('OnProgress', { length: file.size, lengthComputable: true, loaded: file.size, total: file.size }, file, fileID); this.publish('EndUpload', file, fileID, (showError ? { error: true } : true)); this.publish('InvalidFile', file, 'name'); xhr.abort(); }; if(this._options.INVALID_FILE_NAME && this._options.INVALID_FILE_NAME instanceof RegExp) { if(this._options.INVALID_FILE_NAME.test(o.file.name)) { forceAbort.call(this); return; } } // If file was renamed, abort it // FU OPERA: Opera always return lastModified date as null if(!file.lastModifiedDate && !Ink.Dom.Browser.OPERA) { forceAbort.call(this, true); return; } xhr.upload.onprogress = Ink.bind(this.publish, this, 'OnProgress', file, fileID); var endpoint, method; if(this._supportChunks(file.size)) { if(file.size <= file.chunk_offset) { endpoint = this._options.endpointChunkCommit; method = 'POST'; } else { endpoint = this._options.endpointChunk; if(file.chunk_upload_id) { endpoint += '?upload_id=' + file.chunk_upload_id; } if(file.chunk_offset) { endpoint += '&offset=' + file.chunk_offset; } method = 'PUT'; } } else { endpoint = this._options.endpoint; method = 'POST'; } xhr.open(method, endpoint, true); xhr.withCredentials = true; xhr.setRequestHeader("x-requested-with", "XMLHttpRequest"); if(this._supportChunks(file.size)) { xhr.setRequestHeader('Content-type', 'application/x-www-form-urlencoded'); } var fd = new FormData(), blob; if("Blob" in window && typeof Blob === 'function') { blob = new Blob([file], { type: file.type }); if(this._supportChunks(file.size)) { file.chunk_offset = file.chunk_offset || 0; blob = blob.slice(file.chunk_offset, file.chunk_offset + this._options.chunkSize); } else { fd.append(this._options.fileFormName, blob, file.name); } } else { fd.append(this._options.fileFormName, file); } if(!this._supportChunks(file.size)) { for(var k in this._options.extraData) { if(this._options.extraData.hasOwnProperty(k)) { fd.append(k, this._options.extraData[k]); } } } else { fd.append('upload_id', file.chunk_upload_id); fd.append('path', file.upload_path); } if(!file.hasParent) { if(!this._supportChunks(file.size)) { xhr.send(fd); } else { if(file.size <= file.chunk_offset) { xhr.send('upload_id=' + file.chunk_upload_id + '&path=' + file.upload_path + '/' + file.name); } else { xhr.send(blob); } } } else { this.publish('cbCreateFolder', file.parentID, file.fullPath, this._options.extraData, this._folders, file.rootPath, Ink.bind(function() { if(!this._supportChunks(file.size)) { xhr.send(fd); } else { if(file.size <= file.chunk_offset) { xhr.send('upload_id=' + file.chunk_upload_id + '&path=' + file.upload_path + '/' + file.name); } else { xhr.send(blob); } } }, this)); } xhr.onload = Ink.bindEvent(function() { /* jshint boss:true */ if(this._supportChunks(file.size) && file.size > file.chunk_offset) { if(xhr.response) { var response = JSON.parse(xhr.response); // check expected offset var invalidOffset = file.chunk_offset && response.offset !== (file.chunk_offset + this._options.chunkSize) && file.size !== response.offset; if(invalidOffset) { if(o.cb) { o.cb(); } this.publish('ErrorUpload', file, fileID); } else { file.chunk_upload_id = response.upload_id; file.chunk_offset = response.offset; file.chunk_expires = response.expires; this._upload(o); } } else { if(o.cb) { o.cb(); } this.publish('ErrorUpload', file, fileID); } return (xhr = null); } if(o.cb) { o.cb(); } if(xhr.responseText && xhr['status'] < 400) { this.publish('EndUpload', file, fileID, xhr.responseText); } else { this.publish('ErrorUpload', file, fileID); } return (xhr = null); }, this); xhr.onerror = Ink.bindEvent(function() { if(o.cb) { o.cb(); } this.publish('ErrorUpload', file, fileID); }, this); xhr.onabort = Ink.bindEvent(function() { if(o.cb) { o.cb(); } this.publish('AbortUpload', file, fileID, { abortAll: Ink.bind(this.abortAll, this), abortOne: Ink.bind(this.abortOne, this) }); }, this); }, abortAll: function() { if(!this._queueRunning) { return false; } clearInterval(this.interval); this._queueRunning = false; Queue.purge(this._queueId, true); return true; }, abortOne: function(id, cb) { var items = Queue.getItems(0), o; for(var i = 0, len = items.length; i