lib/components/c_facets/Drag.js

Summary

Maintainability
B
4 hrs
Test Coverage
'use strict';

// <a name="components-facets-drag"></a>
// ###drag facet

var ComponentFacet = require('../c_facet')
    , facetsRegistry = require('./cf_registry')
    , DOMEventsSource = require('../msg_src/dom_events')
    , DragDrop = require('../../util/dragdrop')
    , miloCore = require('milo-core')
    , _ = miloCore.proto
    , logger = miloCore.util.logger;


/**
 * `milo.registry.facets.get('Drag')`
 * Facet for components that can be dragged
 * Drag facet supports the following configuration parameters:
 *
 *  - meta: object with properties
 *      - params: object of key-value pairs that will be passed in metadata data type (can also be function or method name that returns this object). See config.dragDrop.dataTypes.componentMetaTemplate
 *      - data: data that will be stored in the above meta data type (or function)
 *  - allowedEffects: string (or function) as specified here: https://developer.mozilla.org/en-US/docs/DragDrop/Drag_Operations#dragstart
 *  - dragImage:
 *      - url: path to image to display when dragging, instead of the owner element
 *      - x: x offset for the image
 *      - y: y offset for the image
 *  - dragCls: CSS class to apply to the component being dragged
 *  - dataTypes: map of additional data types the component will supply to data transfer object, key is data type, value is a function that returns it, component will be passed as the context to this function
 *
 * If function is specified in any parameter it will be called with the component as the context
 */
var Drag = _.createSubclass(ComponentFacet, 'Drag');

_.extendProto(Drag, {
    init: Drag$init,
    start: Drag$start,
    setHandle: Drag$setHandle
});

facetsRegistry.add(Drag);

module.exports = Drag;


function Drag$init() {
    ComponentFacet.prototype.init.apply(this, arguments);

    this._createMessageSourceWithAPI(DOMEventsSource);
    this._dragData = {};

    var dataTypeInfo = this.config._dataTypeInfo || '';
    this._dataTypeInfo = typeof dataTypeInfo == 'function'
                            ? dataTypeInfo
                            : function() { return dataTypeInfo; };
}


/**
 * Drag facet instance method
 * Sets the drag handle element of component. This element has to be dragged for the component to be dragged.
 *
 * @param {Element} handleEl
 */
function Drag$setHandle(handleEl) {
    if (! this.owner.el.contains(handleEl))
        return logger.warn('drag handle should be inside element to be dragged');
    this._dragHandle = handleEl;
}


function Drag$start() {
    ComponentFacet.prototype.start.apply(this, arguments);
    _addDragAttribute.call(this);
    _createDragImage.call(this);
    _toggleDragCls.call(this, false);

    this.onMessages({
        'mousedown': onMouseDown,
        'mouseenter mouseleave mousemove': onMouseMovement,
        'dragstart': onDragStart,
        'drag': onDragging,
        'dragend': onDragEnd
    });

    this.owner.onMessages({
        'getstatestarted': { context: this, subscriber: _removeDragAttribute },
        'getstatecompleted': { context: this, subscriber: _addDragAttribute }
    });
}


/**
 * Adds draggable attribute to component's element
 *
 * @private
 */
function _addDragAttribute() {
    if (!this.config.off && this.owner.el) this.owner.el.setAttribute('draggable', true);
}


function _removeDragAttribute() {
    if (this.owner.el) this.owner.el.removeAttribute('draggable');
}


function _createDragImage() {
    var dragImage = this.config.dragImage;
    if (dragImage) {
        this._dragElement = new Image();
        this._dragElement.src = dragImage.url;
    }
}


function onMouseDown(eventType, event) {
    this.__mouseDownTarget = event.target;
    if (targetInDragHandle.call(this)) {
        window.getSelection().empty();
        event.stopPropagation();
    }
}


function onMouseMovement(eventType, event) {
    var shouldBeDraggable = !this.config.off && targetInDragHandle.call(this);
    this.owner.el.setAttribute('draggable', shouldBeDraggable);
    if (document.body.getAttribute('data-dragEnableEvent') != 'false')
        event.stopPropagation();
}


function onDragStart(eventType, event) {
    event.stopPropagation();

    if (this.config.off || ! targetInDragHandle.call(this)) {
        event.preventDefault();
        return;
    }

    var dragImage = this.config.dragImage;
    if (dragImage)
        event.dataTransfer.setDragImage(this._dragElement, dragImage.x || 0, dragImage.y || 0);

    var owner = this.owner;
    var dt = new DragDrop(event);

    this._dragData = dt.setComponentState(owner);
    setMeta.call(this);
    setAdditionalDataTypes.call(this);
    _setAllowedEffects.call(this, dt);

    _toggleDragCls.call(this, true);

    DragDrop.service.postMessageSync('dragdropstarted', {
        eventType: 'dragstart',
        dragDrop: dt,
        dragFacet: this
    });

    function setMeta() {
        var params = getMetaData.call(this, 'params')
            , data = getMetaData.call(this, 'data');

        this._dragMetaDataType = dt.setComponentMeta(owner, params, data);
        this._dragMetaData = data;
    }

    function getMetaData(property) {
        try { var func = this.config.meta[property]; } catch(e) {}
        if (typeof func == 'string') func = owner[func];
        return _.result(func, owner);
    }

    function setAdditionalDataTypes() {
        if (this.config.dataTypes) {
            this._dataTypesData = _.mapKeys(this.config.dataTypes, function (getDataFunc, dataType) {
                if (typeof getDataFunc == 'string') getDataFunc = owner[getDataFunc];
                var data = getDataFunc.call(this.owner, dataType);
                if (typeof data == 'object') data = JSON.stringify(data);
                if (data) dt.setData(dataType, data);
                return data;
            }, this);
        }
    }
}


function onDragging(eventType, event) {
    if (_dragIsDisabled.call(this, event)) return;

    var dt = new DragDrop(event);
    dt.setComponentState(this.owner, this._dragData);
    dt.setData(this._dragMetaDataType, this._dragMetaData);
    if (this._dataTypesData) {
        _.eachKey(this._dataTypesData, function(data, dataType) {
            if (data) dt.setData(dataType, data);
        });
    }

    _setAllowedEffects.call(this, dt);
}


function onDragEnd(eventType, event) {
    if (_dragIsDisabled.call(this, event)) return;
    event.stopPropagation();

    _toggleDragCls.call(this, false);

    var dt = new DragDrop(event);
    DragDrop.service.postMessageSync('completedragdrop', {
        eventType: 'dragend',
        dragDrop: dt,
        dragFacet: this
    });
}


function _toggleDragCls(showHide) {
    if (this.config.dragCls)
        this.owner.el.classList.toggle(this.config.dragCls, showHide);
}


function _setAllowedEffects(DragDrop) {
    var effects = _.result(this.config.allowedEffects, this.owner);
    DragDrop.setAllowedEffects(effects);
}


function targetInDragHandle() {
    return ! this._dragHandle || this._dragHandle.contains(this.__mouseDownTarget);
}


function _dragIsDisabled(event) {
    if (this.config.off) {
        event.preventDefault();
        return true;
    }
    return false;
}