UNPKG

prodio

Version:

Simplified project management

766 lines (695 loc) 28 kB
/** * Auxiliar utilities for UI Modules * @module Ink.UI.Common_1 * @version 1 */ Ink.createModule('Ink.UI.Common', '1', ['Ink.Dom.Element_1', 'Ink.Net.Ajax_1','Ink.Dom.Css_1','Ink.Dom.Selector_1','Ink.Util.Url_1'], function(InkElement, Ajax,Css,Selector,Url) { 'use strict'; var instances = {}; var lastIdNum = 0; var nothing = {} /* a marker, for reference comparison. */; var keys = Object.keys || function (obj) { var ret = []; for (var k in obj) if (obj.hasOwnProperty(k)) { ret.push(k); } return ret; }; /** * @namespace Ink.UI.Common_1 */ var Common = { /** * Supported Ink Layouts * * @property Layouts * @type Object * @readOnly */ Layouts: { TINY: 'tiny', SMALL: 'small', MEDIUM: 'medium', LARGE: 'large', XLARGE: 'xlarge' }, /** * Checks if an item is a valid DOM Element. * * @method isDOMElement * @static * @param {Mixed} o The object to be checked. * @return {Boolean} True if it's a valid DOM Element. * @example * var el = Ink.s('#element'); * if( Ink.UI.Common.isDOMElement( el ) === true ){ * // It is a DOM Element. * } else { * // It is NOT a DOM Element. * } */ isDOMElement: function(o) { return o && typeof o === 'object' && 'nodeType' in o && o.nodeType === 1; }, /** * Checks if an item is a valid integer. * * @method isInteger * @static * @param {Mixed} n The value to be checked. * @return {Boolean} True if it's a valid integer. * @example * var value = 1; * if( Ink.UI.Common.isInteger( value ) === true ){ * // It is an integer. * } else { * // It is NOT an integer. * } */ isInteger: function(n) { return (typeof n === 'number' && n % 1 === 0); }, /** * Gets a DOM Element. * * @method elOrSelector * @static * @param {DOMElement|String} elOrSelector DOM Element or CSS Selector * @param {String} fieldName The name of the field. Commonly used for debugging. * @return {DOMElement} Returns the DOMElement passed or the first result of the CSS Selector. Otherwise it throws an exception. * @example * // In case there are several .myInput, it will retrieve the first found * var el = Ink.UI.Common.elOrSelector('.myInput','My Input'); */ elOrSelector: function(elOrSelector, fieldName) { if (!this.isDOMElement(elOrSelector)) { var t = Selector.select(elOrSelector); if (t.length === 0) { Ink.warn(fieldName + ' must either be a DOM Element or a selector expression!\nThe script element must also be after the DOM Element itself.'); return null; } return t[0]; } return elOrSelector; }, /** * Alias for `elOrSelector` but returns an array of elements. * * @method elsOrSelector * * @static * @param {DOMElement|String} elOrSelector DOM Element or CSS Selector * @param {String} fieldName The name of the field. Commonly used for debugging. * @return {DOMElement} Returns the DOMElement passed or the first result of the CSS Selector. Otherwise it throws an exception. * @param {Boolean} required Flag to accept an empty array as output. * @return {Array} The selected DOM Elements. * @example * var elements = Ink.UI.Common.elsOrSelector('input.my-inputs', 'My Input'); */ elsOrSelector: function(elsOrSelector, fieldName, required) { var ret; if (typeof elsOrSelector === 'string') { ret = Selector.select(elsOrSelector); } else if (Common.isDOMElement(elsOrSelector)) { ret = [elsOrSelector]; } else if (elsOrSelector && typeof elsOrSelector === 'object' && typeof elsOrSelector.length === 'number') { ret = elsOrSelector; } if (ret && ret.length) { return ret; } else { if (required) { throw new TypeError(fieldName + ' must either be a DOM Element, an Array of elements, or a selector expression!\nThe script element must also be after the DOM Element itself.'); } else { return []; } } }, /** * Gets options an object and element's metadata. * * The element's data attributes take precedence. Values from the element's data-atrributes are coerced into the required type. * * @method options * * @param {Object} [fieldId] Name to be used in debugging features. * @param {Object} defaults Object with the options' types and defaults. * @param {Object} overrides Options to override the defaults. Usually passed when instantiating an UI module. * @param {DOMElement} [element] Element with data-attributes * * @example * * this._options = Ink.UI.Common.options('MyComponent', { * 'anobject': ['Object', null], // Defaults to null * 'target': ['Element', null], * 'stuff': ['Number', 0.1], * 'stuff2': ['Integer', 0], * 'doKickFlip': ['Boolean', false], * 'targets': ['Elements'], // Required option since no default was given * 'onClick': ['Function', null] * }, options || {}, elm) * * @example * * ### Note about booleans * * Here is how options are read from the markup * data-attributes, for several values`data-a-boolean`. * * Options considered true: * * - `data-a-boolean="true"` * - (Every other value which is not on the list below.) * * Options considered false: * * - `data-a-boolean="false"` * - `data-a-boolean=""` * - `data-a-boolean` * * Options which go to default: * * - (no attribute). When `data-a-boolean` is ommitted, the * option is not considered true nor false, and as such * defaults to what is in the `defaults` argument. * **/ options: function (fieldId, defaults, overrides, element) { if (typeof fieldId !== 'string') { element = overrides; overrides = defaults; defaults = fieldId; fieldId = ''; } overrides = overrides || {}; var out = {}; var dataAttrs = element ? InkElement.data(element) : {}; var fromDataAttrs; var type; var lType; var defaultVal; var invalidStr = function (str) { if (fieldId) { str = fieldId + ': "' + ('' + str).replace(/"/, '\\"') + '"'; } return str; }; var quote = function (str) { return '"' + ('' + str).replace(/"/, '\\"') + '"'; }; var invalidThrow = function (str) { throw new Error(invalidStr(str)); }; var invalid = function (str) { Ink.error(invalidStr(str) + '. Ignoring option.'); }; function optionValue(key) { type = defaults[key][0]; lType = type.toLowerCase(); defaultVal = defaults[key].length === 2 ? defaults[key][1] : nothing; if (!type) { invalidThrow('Ink.UI.Common.options: Always specify a type!'); } if (!(lType in Common._coerce_funcs)) { invalidThrow('Ink.UI.Common.options: ' + defaults[key][0] + ' is not a valid type. Use one of ' + keys(Common._coerce_funcs).join(', ')); } if (!defaults[key].length || defaults[key].length > 2) { invalidThrow('the "defaults" argument must be an object mapping option names to [typestring, optional] arrays.'); } if (key in dataAttrs) { fromDataAttrs = Common._coerce_from_string(lType, dataAttrs[key], key, fieldId); // (above can return `nothing`) } else { fromDataAttrs = nothing; } if (fromDataAttrs !== nothing) { if (!Common._options_validate(fromDataAttrs, lType)) { invalid('(' + key + ' option) Invalid ' + lType + ' ' + quote(fromDataAttrs)); return defaultVal; } else { return fromDataAttrs; } } else if (key in overrides) { return overrides[key]; } else if (defaultVal !== nothing) { return defaultVal; } else { invalidThrow('Option ' + key + ' is required!'); } } for (var key in defaults) { if (defaults.hasOwnProperty(key)) { out[key] = optionValue(key); } } return out; }, _coerce_from_string: function (type, val, paramName, fieldId) { if (type in Common._coerce_funcs) { return Common._coerce_funcs[type](val, paramName, fieldId); } else { return val; } }, _options_validate: function (val, type) { if (type in Common._options_validate_types) { return Common._options_validate_types[type].call(Common, val); } else { // 'object' options cannot be passed through data-attributes. // Json you say? Not any good to embed in HTML. return false; } }, _coerce_funcs: (function () { var ret = { element: function (val) { return Common.elOrSelector(val, ''); }, elements: function (val) { return Common.elsOrSelector(val, '', false /*not required, so don't throw an exception now*/); }, object: function (val) { return val; }, number: function (val) { return parseFloat(val); }, 'boolean': function (val) { return !(val === 'false' || val === '' || val === null); }, string: function (val) { return val; }, 'function': function (val, paramName, fieldId) { Ink.error(fieldId + ': You cannot specify the option "' + paramName + '" through data-attributes because it\'s a function'); return nothing; } }; ret['float'] = ret.integer = ret.number; return ret; }()), _options_validate_types: (function () { var types = { string: function (val) { return typeof val === 'string'; }, number: function (val) { return typeof val === 'number' && !isNaN(val) && isFinite(val); }, integer: function (val) { return val === Math.round(val); }, element: function (val) { return Common.isDOMElement(val); }, elements: function (val) { return val && typeof val === 'object' && typeof val.length === 'number' && val.length; }, 'boolean': function (val) { return typeof val === 'boolean'; } }; types['float'] = types.number; return types; }()), /** * Deep copy (clone) an object. * Note: The object cannot have referece loops. * * @method clone * @static * @param {Object} o The object to be cloned/copied. * @return {Object} Returns the result of the clone/copy. * @example * var originalObj = { * key1: 'value1', * key2: 'value2', * key3: 'value3' * }; * var cloneObj = Ink.UI.Common.clone( originalObj ); */ clone: function(o) { try { return JSON.parse( JSON.stringify(o) ); } catch (ex) { throw new Error('Given object cannot have loops!'); } }, /** * Gets an element's one-base index relative to its parent. * * @method childIndex * @static * @param {DOMElement} childEl Valid DOM Element. * @return {Number} Numerical position of an element relatively to its parent. * @example * <!-- Imagine the following HTML: --> * <ul> * <li>One</li> * <li>Two</li> * <li id="test">Three</li> * <li>Four</li> * </ul> * * <script> * var testLi = Ink.s('#test'); * Ink.UI.Common.childIndex( testLi ); // Returned value: 3 * </script> */ childIndex: function(childEl) { if( Common.isDOMElement(childEl) ){ var els = Selector.select('> *', childEl.parentNode); for (var i = 0, f = els.length; i < f; ++i) { if (els[i] === childEl) { return i; } } } throw 'not found!'; }, /** * AJAX JSON request shortcut method * It provides a more convenient way to do an AJAX request and expect a JSON response.It also offers a callback option, as third parameter, for better async handling. * * @method ajaxJSON * @static * @async * @param {String} endpoint Valid URL to be used as target by the request. * @param {Object} params This field is used in the thrown Exception to identify the parameter. * @param {Function} cb Callback for the request. * @example * // In case there are several .myInput, it will retrieve the first found * var el = Ink.UI.Common.elOrSelector('.myInput','My Input'); */ ajaxJSON: function(endpoint, params, cb) { new Ajax( endpoint, { evalJS: 'force', method: 'POST', parameters: params, onSuccess: function( r) { try { r = r.responseJSON; if (r.status !== 'ok') { throw 'server error: ' + r.message; } cb(null, r); } catch (ex) { cb(ex); } }, onFailure: function() { cb('communication failure'); } } ); }, /** * Gets the current Ink layout. * * @method currentLayout * @static * @return {String} A string representation of the current layout name. * @example * var inkLayout = Ink.UI.Common.currentLayout(); * if (inkLayout === 'small') { * // ... * } */ currentLayout: function() { var i, f, k, v, el, detectorEl = Selector.select('#ink-layout-detector')[0]; if (!detectorEl) { detectorEl = document.createElement('div'); detectorEl.id = 'ink-layout-detector'; for (k in this.Layouts) { if (this.Layouts.hasOwnProperty(k)) { v = this.Layouts[k]; el = document.createElement('div'); el.className = 'show-' + v + ' hide-all'; el.setAttribute('data-ink-layout', v); detectorEl.appendChild(el); } } document.body.appendChild(detectorEl); } for (i = 0, f = detectorEl.children.length; i < f; ++i) { el = detectorEl.children[i]; if (Css.getStyle(el, 'display') === 'block') { return el.getAttribute('data-ink-layout'); } } return 'large'; }, /** * Sets the location's hash (window.location.hash). * * @method hashSet * @static * @param {Object} o Object with the info to be placed in the location's hash. * @example * // It will set the location's hash like: <url>#key1=value1&key2=value2&key3=value3 * Ink.UI.Common.hashSet({ * key1: 'value1', * key2: 'value2', * key3: 'value3' * }); */ hashSet: function(o) { if (typeof o !== 'object') { throw new TypeError('o should be an object!'); } var hashParams = Url.getAnchorString(); hashParams = Ink.extendObj(hashParams, o); window.location.hash = Url.genQueryString('', hashParams).substring(1); }, /** * Removes children nodes from a given object. * This method was initially created to help solve a problem in Internet Explorer(s) that occurred when trying to set the innerHTML of some specific elements like 'table'. * * @method cleanChildren * @static * @param {DOMElement} parentEl Valid DOM Element * @example * <!-- Imagine the following HTML: --> * <ul id="myUl"> * <li>One</li> * <li>Two</li> * <li>Three</li> * <li>Four</li> * </ul> * * <script> * Ink.UI.Common.cleanChildren( Ink.s( '#myUl' ) ); * </script> * * <!-- After running it, the HTML changes to: --> * <ul id="myUl"></ul> */ cleanChildren: function(parentEl) { if( !Common.isDOMElement(parentEl) ){ throw 'Please provide a valid DOMElement'; } var prevEl, el = parentEl.lastChild; while (el) { prevEl = el.previousSibling; parentEl.removeChild(el); el = prevEl; } }, /** * Stores the id and/or classes of an element in an object. * * @method storeIdAndClasses * @static * @param {DOMElement} fromEl Valid DOM Element to get the id and classes from. * @param {Object} inObj Object where the id and classes will be saved. * @example * <div id="myDiv" class="aClass"></div> * * <script> * var storageObj = {}; * Ink.UI.Common.storeIdAndClasses( Ink.s('#myDiv'), storageObj ); * // storageObj changes to: * { * _id: 'myDiv', * _classes: 'aClass' * } * </script> */ storeIdAndClasses: function(fromEl, inObj) { if( !Common.isDOMElement(fromEl) ){ throw 'Please provide a valid DOMElement as first parameter'; } var id = fromEl.id; if (id) { inObj._id = id; } var classes = fromEl.className; if (classes) { inObj._classes = classes; } }, /** * Sets the id and className properties of an element based * * @method restoreIdAndClasses * @static * @param {DOMElement} toEl Valid DOM Element to set the id and classes on. * @param {Object} inObj Object where the id and classes to be set are. This method uses the same format as the one given in `storeIdAndClasses` * @example * <div></div> * * <script> * var storageObj = { * _id: 'myDiv', * _classes: 'aClass' * }; * * Ink.UI.Common.storeIdAndClasses( Ink.s('div'), storageObj ); * </script> * * <!-- After the code runs the div element changes to: --> * <div id="myDiv" class="aClass"></div> */ restoreIdAndClasses: function(toEl, inObj) { if( !Common.isDOMElement(toEl) ){ throw 'Please provide a valid DOMElement as first parameter'; } if (inObj._id && toEl.id !== inObj._id) { toEl.id = inObj._id; } if (inObj._classes && toEl.className.indexOf(inObj._classes) === -1) { if (toEl.className) { toEl.className += ' ' + inObj._classes; } else { toEl.className = inObj._classes; } } if (inObj._instanceId && !toEl.getAttribute('data-instance')) { toEl.setAttribute('data-instance', inObj._instanceId); } }, _warnDoubleInstantiation: function (elm, newInstance) { var instances = Common.getInstance(elm); if (getName(newInstance) === '') { return; } if (!instances) { return; } var nameWithoutVersion = getName(newInstance); for (var i = 0, len = instances.length; i < len; i++) { if (nameWithoutVersion === getName(instances[i])) { Ink.warn('Creating more than one ' + nameWithoutVersion + '.', '(Was creating a ' + nameWithoutVersion + ' on:', elm, '.' + 'Existing element was: ', instances[i]._element); } } function getName(thing) { return ((thing.constructor && (thing.constructor._name || thing.constructor.name)) || thing._name || '').replace(/_.*?$/, ''); } }, /** * Saves a component's instance reference for later retrieval. * * @method registerInstance * @static * @param {Object} inst Object that holds the instance. * @param {DOMElement} el DOM Element to associate with the object. */ registerInstance: function(inst, el) { if (!inst || inst._instanceId) { return; } if (!this.isDOMElement(el)) { throw new TypeError('Ink.UI.Common.registerInstance: The element passed in is not a DOM element!'); } Common._warnDoubleInstantiation(el, inst); var id = 'instance' + (++lastIdNum); instances[id] = inst; inst._instanceId = id; var dataInst = el.getAttribute('data-instance'); dataInst = (dataInst !== null) ? [dataInst, id].join(' ') : id; el.setAttribute('data-instance', dataInst); }, /** * Deletes an instance with a given id. * * @method unregisterInstance * @static * @param {String} id Id of the instance to be destroyed. */ unregisterInstance: function(id) { delete instances[id]; }, /** * Gets an UI instance from an element or instance id. * * @method getInstance * @static * @param {String|DOMElement} instanceIdOrElement Instance's id or DOM Element from which we want the instances. * @return {Object|Array} Returns an instance or a collection of instances. */ getInstance: function(instanceIdOrElement) { var ids; instanceIdOrElement = Common.elOrSelector(instanceIdOrElement); if (instanceIdOrElement) { ids = instanceIdOrElement.getAttribute('data-instance'); if (ids === null) { return null; } } else { ids = instanceIdOrElement; } ids = ids.split(/\s+/g); var inst, id, i, l = ids.length; var res = []; for (i = 0; i < l; ++i) { id = ids[i]; if (!id) { throw new Error('Element is not a JS instance!'); } inst = instances[id]; if (!inst) { throw new Error('Instance "' + id + '" not found!'); } res.push(inst); } return res; }, /** * 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) { var el = Selector.select(selector)[0]; if (!el) { throw new Error('Element not found!'); } return this.getInstance(el); }, /** * 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._instanceId); this._element.parentNode.removeChild(this._element); } }; return Common; });