/** * 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; });