prodio
Version:
Simplified project management
766 lines (695 loc) • 28 kB
JavaScript
/**
* 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;
});