eventbrite/dorsal

View on GitHub
src/dorsal.js

Summary

Maintainability
C
1 day
Test Coverage
/*
 * Copyright 2014 Eventbrite
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 *
 * You may obtain a copy of the License at
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

var DorsalCore = function() {};

/**
* @namespace Dorsal
*
* @property {string} Dorsal.VERSION - current Version
* @property {DATA_PREFIX} Dorsal.DATA_PREFIX - prefix for attributes used by Dorsal
* @property {DATA_DORSAL_WIRED} Dorsal.DATA_DORSAL_WIRED - data attribute used for internal management
* @property {GUID_KEY} Dorsal.GUID_KEY - data attribute added to each element wired
* @property {CSS_PREFIX} Dorsal.CSS_PREFIX - prefix for any wirable pluginName
* @property {DEBUG} Dorsal.DEBUG - prefix for any wirable pluginName
*/

DorsalCore.prototype.VERSION = '0.6.3';
DorsalCore.prototype.CSS_PREFIX = '.js-d-';
DorsalCore.prototype.DATA_IGNORE_PREFIX = 'xd';
DorsalCore.prototype.DATA_PREFIX = 'd';
DorsalCore.prototype.DATA_DORSAL_WIRED = 'data-' + DorsalCore.prototype.DATA_IGNORE_PREFIX + '-wired';
DorsalCore.prototype.GUID_KEY = 'dorsal-guid';
DorsalCore.prototype.ELEMENT_TO_PLUGINS_MAP = {};
DorsalCore.prototype.DEBUG = false;
DorsalCore.prototype.plugins = {};

DorsalCore.prototype.registerPlugin = function(pluginName, callback) {
    this.plugins[pluginName] = callback;
};

/**
* @function Dorsal.unregisterPlugin
* @description unregister a given plugin
* @param {string} pluginName Plugin Name
*/
DorsalCore.prototype.unregisterPlugin = function(pluginName) {
    delete this.plugins[pluginName];
};

DorsalCore.prototype._getDatasetAttributes = function(el) {
    var dataset = el.dataset,
        dataAttributes = {};

    for (var key in dataset) {
        if ((new RegExp('^' + this.DATA_PREFIX + '[A-Z]')).test(key)) {
            var name = key.substr(this.DATA_PREFIX.length),
                outputKey = name[0].toLowerCase() + name.substr(1);

            dataAttributes[outputKey] = dataset[key];
        }
    }

    return dataAttributes;
};

DorsalCore.prototype._normalizeDataAttribute =  function(attr) {
    return attr.toUpperCase().replace('-','');
};

/**
 *
 * @function Dorsal._getDataAttributes
 * @param {DomNode} el
 * @return {Object} all the data attributes present in a given node
 * @private
 */
DorsalCore.prototype._getDataAttributes = function(el) {
    var dataAttributes = {},
        attributes = el.attributes,
        attributesLength = attributes.length,
        nameAttribute = 'name',
        i = 0;

    for (i = 0; i < attributesLength; i++) {
        if ((new RegExp('^data-' + this.DATA_PREFIX + '-')).test(attributes[i][nameAttribute])) {
            var name = attributes[i][nameAttribute].substr(5 + this.DATA_PREFIX.length + 1)
                                                   .toLowerCase()
                                                   .replace(/(\-[a-zA-Z])/g, this._normalizeDataAttribute);
            dataAttributes[name] = attributes[i].value;
        }
    }

    return dataAttributes;
};

/**
 * @function Dorsal._getAttributes
 * @param {DomNode} el
 * @returns {Object} all the data attributes present in the given node
 * @private
 */
DorsalCore.prototype._getAttributes = function(el) {
    if (el.dataset) {
        return this._getDatasetAttributes(el);
    }

    return this._getDataAttributes(el);
};

DorsalCore.prototype._runPlugin = function(el, pluginName) {
    // if already initialized, don't reinitialize
    var log = new DorsalLog(this.DEBUG);

    if (el.getAttribute(this.DATA_DORSAL_WIRED) && el.getAttribute(this.DATA_DORSAL_WIRED).indexOf(pluginName) !== -1) {
        log.log('node already wired: ' + el);
        return false;
    }

    var data = this._getAttributes(el),
        wiredAttribute = el.getAttribute(this.DATA_DORSAL_WIRED),
        plugin = this.plugins[pluginName],
        options = {
            el: el,
            data: data
        },
        elementGUID = el.getAttribute(this.GUID_KEY);

    if (!elementGUID) {
        elementGUID = createGUID();
        el.setAttribute(this.GUID_KEY, elementGUID);
        this.ELEMENT_TO_PLUGINS_MAP[elementGUID] = {};
    }

    log.log('plugin execution start', {guid: elementGUID, pluginName: pluginName});

    if (typeof plugin === 'function') {
        this.ELEMENT_TO_PLUGINS_MAP[elementGUID][pluginName] = plugin.call(el, options);
    } else if (typeof plugin === 'object') {
        this.ELEMENT_TO_PLUGINS_MAP[elementGUID][pluginName] = plugin.create.call(el, options);
    }

    log.log('plugin execution end', {guid: elementGUID, pluginName: pluginName});

    if (wiredAttribute) {
        el.setAttribute(this.DATA_DORSAL_WIRED, wiredAttribute + ' ' + pluginName);
    } else {
        el.setAttribute(this.DATA_DORSAL_WIRED, pluginName);
    }

    return elementGUID;
};

/**
 * @function Dorsal.registeredPlugins
 * @description will return each plugin name registered
 * @return {Array} registered plugin names
 */
DorsalCore.prototype.registeredPlugins = function() {
    var pluginKeys = keysFor(this.plugins);

    if (!pluginKeys.length) {
        if (console && console.warn) {
            console.warn('No plugins registered with Dorsal');
        }
    }

    return pluginKeys;
};

/**
 * @function Dorsal._wireElementsFrom
 * @param {DomNode} parentNode
 * @param {Promise} deferred object to proxy to the next method
 * @private
 */
DorsalCore.prototype._wireElementsFrom = function(parentNode) {
    var isValidNode = parentNode && 'querySelectorAll' in parentNode,
        plugins = this.registeredPlugins(),
        index = 0,
        pluginName,
        pluginCSSClass,
        nodes,
        response,
        responses = [];

    if (!isValidNode) {
        log.log('invalid Node: '+ prentNode);
        return;
    }

    pluginName = plugins[index++];

    while(pluginName) {
        nodes = parentNode.querySelectorAll(this.CSS_PREFIX + pluginName);

        if (nodes.length) {
            response = this._wireElements(nodes, [pluginName]);
            responses = responses.concat(response);
        }
        pluginName = plugins[index++];
    }
    return responses;
};

/**
 * @function Dorsal._wireElements
 * @param {DomNode[]} nodes dom nodes to wire
 * @param {Array|String} plugins plugins to wire the given nodes.
 * @param {Promise} deferred object to proxy to the next method
 * @private
 */
DorsalCore.prototype._wireElements = function(nodes, plugins) {
    if (!nodes.length) {
        var log = new DorsalLog(this.DEBUG);

        log.log('no nodes to wire: ' + nodes);
        return;
    }

    var nodeIndex = 0,
        node = nodes[nodeIndex++],
        responses = [];

    while(node) {
        responses.push(this._wireElement(node, plugins));
        node = nodes[nodeIndex++];
    }
    return responses;
};
/**
 * @function Dorsal._wireElement
 * @param {DomNode} nodes DomNodes to wire
 * @param {String|Array} plugins plugins to wire the given nodes.
 * @param {Promise} deferred object to proxy to the next method
 * @private
 */
DorsalCore.prototype._wireElement = function(el, plugins) {
    var self = this,
        dfd = new DorsalDeferred(),
        log = new DorsalLog(this.DEBUG);

    window.setTimeout(function() {
        var validElement = el && 'className' in el,
            pluginCSSClass,
            pluginName,
            pluginResponse,
            index = 0;

        if (!validElement) {
            log.log('invalid node to wire: ' + el);
            return;
        }

        if (!plugins.length) {
            plugins = self.registeredPlugins();
        }

        pluginName = plugins[index++];


        while(pluginName) {
            pluginCSSClass = self.CSS_PREFIX + pluginName;

            if (el.className.indexOf(pluginCSSClass.substr(1)) > -1) {
                pluginResponse = self._runPlugin(el, pluginName);
                dfd.notify(pluginName, pluginResponse, self);
                log.end(pluginResponse);
            }
            pluginName = plugins[index++];
        }

        dfd.resolve();
    }, 0);
    return dfd.promise();
};

/**
 * @function Dorsal._detachPlugin
 * @param {DomNode} el DomNode to unwire
 * @param {String} pluginName plugin to unwire from  the given node.
 * @param {Boolean} hasActuallyDestroyed the unwire status
 * @private
 */
DorsalCore.prototype._detachPlugin = function(el, pluginName) {
    var remainingPlugins,
        hasActuallyDestroyed = false;

    if (typeof el.getAttribute(this.DATA_DORSAL_WIRED) !== 'string') {
        return false;
    }

    if (el.getAttribute(this.DATA_DORSAL_WIRED).indexOf(pluginName) > -1 &&
        this.plugins[pluginName].destroy) {

        this.plugins[pluginName].destroy({
            el: el,
            data: this._getAttributes(el),
            instance: this.ELEMENT_TO_PLUGINS_MAP
                [el.getAttribute(DorsalCore.prototype.GUID_KEY)]
                [pluginName]
        });

        hasActuallyDestroyed = true;
    }

    // remove plugin
    remainingPlugins = el.getAttribute(this.DATA_DORSAL_WIRED).split(' ');
    // remove 1 instance, at the index where the plugin name exists
    remainingPlugins.splice(arrayIndexOf(remainingPlugins, pluginName), 1);
    el.setAttribute(this.DATA_DORSAL_WIRED, remainingPlugins.join(' '));

    return hasActuallyDestroyed;
};

/**
 * @function Dorsal.unwire
 * @description will remove a given el/pluginName
 * @param {DomNode} el node already wired.
 * @param {String} pluginName plugin Name to uwire.
 * @return {Boolean} true if a plugin was detached, false otherwise
 */
DorsalCore.prototype.unwire = function(el, pluginName) {
    // detach a single plugin
    if (pluginName) {
        return this._detachPlugin(el, pluginName);
    }

    var attachedPlugins = el.getAttribute(this.DATA_DORSAL_WIRED).split(' '),
        attachedPluginsCount = attachedPlugins.length,
        hasADetachedPlugin = false,
        iPluginKey,
        i = 0;

    for (; i < attachedPluginsCount; i++) {
        iPluginKey = attachedPlugins[i];

        if (this._detachPlugin(el, iPluginKey)) {
            hasADetachedPlugin =  true;
        }
    }

    return hasADetachedPlugin;
};

/**
 * @function Dorsal.wire
 * @description wire node/nodes
 * wire can be used as follow:<br>
 *
 *  - 0 argument: Will wire each element having the prefix on them.<br>
 *  - 1 argument (node): Will wire all the children elements from a given node.<br>
 *  - 1 argument (Array): Will wire all the elements from a given Collection.<br>
 *  - 2 argument (DomNode, PluginName): Will wire the node/plugin respectively.<br>
 *
 * Wire will not accept an explicitly `undefined` or falsy `el` argument.
 *
 * @param {DomNode|DomNode[]} el a given element or Array to wire
 * @param {String} pluginName plugin name to wire
 * @return {Promise} deferred async wiring of dorsal
 */
DorsalCore.prototype.wire = function(el, pluginName) {
    var deferred = new DorsalDeferred(this.ELEMENT_TO_PLUGINS_MAP),
        responses = [],
        action;

    if (arguments.length && !el) {
        // Bail out if we received a falsy el argument
        return deferred.resolve().promise();
    }

    switch(arguments.length) {
        case 1:
            // if el is Array we wire those given elements
            // otherwise we query elements inside the given element
            if (isDOM(el) || isHTMLElement(el)) {
                responses = this._wireElementsFrom(el);
            } else {
                responses = this._wireElements(el, []);
            }
            break;
        case 2:
            // wiring element/plugin respectively.
            if (isDOM(el)) {
                action = '_wireElementsFrom';
            } else {
                action = isHTMLElement(el) ? '_wireElement' : '_wireElements';
            }

            responses = this[action](el, [pluginName]);
            break;
        default:
            // without arguments, we define document as our parentElement
            responses = this._wireElementsFrom(document);
            break;
    }

    return deferred.when(responses);
};

/**
 * @function Dorsal.rewire
 * @description will remove and re initialize a given node/plugin
 * @param {DomNode} el node to rewire
 * @param {stirng} pluginName plugin Name
 * @return {Promise} deferred async wiring of dorsal
 */
DorsalCore.prototype.rewire = function(el, pluginName) {
    var deferred;

    this.unwire(el, pluginName);

    if (!pluginName) {
        el  = [el];

        deferred = this.wire(el);
    } else {
        deferred = this.wire(el, pluginName);
    }

    return deferred;
};

/**
 * @function Dorsal.get
 * @description will return instances wired to a given node/s
 * @param {DomNode[]} nodes nodes given
 * @return {Array} all object instances stored for given node/s
 */
DorsalCore.prototype.get = function(nodes) {
    var instances = [],
        instance,
        i = 0,
        node;

    if (isHTMLElement(nodes)) {
        nodes = [nodes];
    }

    node = nodes[i++];

    while(node) {
        instance = this._instancesFor(node);
        if (instance) {
            instances.push(instance);
        }

        node = nodes[i++];
    }

    return instances;
};

/**
 * @function Dorsal._instancesFor
 * @param {DomNode} el Node given
 * @return {Object} all stored instances for a particular node
 * @private
*/

DorsalCore.prototype._instancesFor = function(el) {

    var elementGUID = isHTMLElement(el) ?
            el.getAttribute(this.GUID_KEY)
            : el;

    return this.ELEMENT_TO_PLUGINS_MAP[elementGUID];
};

var Dorsal = new DorsalCore();

Dorsal.create = function() {
    return new DorsalCore();
};