lib/util/dragdrop.js

Summary

Maintainability
A
2 hrs
Test Coverage
'use strict';

var miloCore = require('milo-core')
    , Messenger = miloCore.Messenger
    , dragDropConfig = require('../config').dragDrop
    , componentMetaRegex = dragDropConfig.dataTypes.componentMetaRegex
    , _ = miloCore.proto
    , base32 = require('./base32');


module.exports = DragDrop;


/**
 * Wrapper for event.dataTransfer of drag-drop HTML API
 *
 * @constructor
 * @param {event} DOM event
 * @return {DragDrop}
 */
function DragDrop(event) {
    this.event = event;
    this.dataTransfer = event.dataTransfer;
    this.types = this.dataTransfer.types;
    var items = this.dataTransfer.items;
    this.items = _.map(items || [], function (i) {
        return { kind: i.kind, type: i.type };
    });
}

/**
 * Usage:
 * var testDT = new DragDrop(event);
 * testDT.setComponentMeta(newComponent, {test: 'test', test2: 'test2'});
 * testDT.getComponentMeta();
 */

_.extend(DragDrop, {
    componentDataType: DragDrop$$componentDataType,
    getDropPositionY: DragDrop$$getDropPositionY
});

_.extendProto(DragDrop, {
    isComponent: DragDrop$isComponent,
    getComponentState: DragDrop$getComponentState,
    setComponentState: DragDrop$setComponentState,
    getComponentMeta: DragDrop$getComponentMeta,
    setComponentMeta: DragDrop$setComponentMeta,
    getAllowedEffects: DragDrop$getAllowedEffects,
    setAllowedEffects: DragDrop$setAllowedEffects,
    getDropEffect: DragDrop$getDropEffect,
    setDropEffect: DragDrop$setDropEffect,
    isEffectAllowed: DragDrop$isEffectAllowed,
    getData: DragDrop$getData,
    setData: DragDrop$setData,
    clearData: DragDrop$clearData
});


function DragDrop$$componentDataType() {
    return dragDropConfig.dataTypes.component;
}

function DragDrop$$getDropPositionY(event, el) {
    var dP = getDropPosition(event, el);
    var isBelow = dP.clientY > dP.targetTop + dP.targetHeight / 2;
    return isBelow ? 'below' : 'above';
}

function getDropPosition(event, el) {
    try {
        var clientRect = el.getBoundingClientRect();
        var targetWidth = clientRect.width;
        var targetHeight = clientRect.height;
        var targetTop = clientRect.top;
        var targetLeft = clientRect.left;
    } catch(e){}
    return {
        clientX: event.clientX,
        clientY: event.clientY,
        targetWidth: targetWidth,
        targetHeight: targetHeight,
        targetTop: targetTop,
        targetLeft: targetLeft
    };
}


function DragDrop$isComponent() {
    return _.indexOf(this.types, DragDrop.componentDataType()) >= 0;
}


function DragDrop$getComponentState() {
    var dataType = DragDrop.componentDataType()
        , stateStr = this.dataTransfer.getData(dataType)
        , state = _.jsonParse(stateStr);

    return state;
}


function DragDrop$setComponentState(component, stateStr){
    if (! stateStr) {
        var state = component.getTransferState({ requestedBy: 'drag' });
        stateStr = JSON.stringify(state);
    }
    var dataType = DragDrop.componentDataType();

    stateStr && this.dataTransfer.setData(dataType, stateStr);
    return stateStr;
}


function DragDrop$setComponentMeta(component, params, data) {
    var meta = _componentMeta(component);

    var paramsStr = JSON.stringify(params || {});
    var dataType = dragDropConfig.dataTypes.componentMetaTemplate
                    .replace('%class', _encode(meta.compClass || ''))
                    .replace('%name', _encode(meta.compName || ''))
                    .replace('%params', _encode(paramsStr || ''));

    if (data && typeof data == 'object') data = JSON.stringify(data);

    this.dataTransfer.setData(dataType, data || '');

    return dataType;
}


function _encode(str) {
    return base32.encode(str).toLowerCase();
}


function _componentMeta(component) {
    return component.transfer
            ? component.transfer.getComponentMeta()
            : {
                compClass: component.constructor.name,
                compName: component.name
            };
}


function DragDrop$getComponentMeta() {
    var match;
    var metaDataType = _.find(this.types, function (dType) {
        match = dType.match(componentMetaRegex);
        return !!match;
    });
    if (!metaDataType) return;

    for (var i=1; i<4; i++)
        match[i] = base32.decode(match[i]);

    return {
        compClass: match[1],
        compName: match[2],
        params: JSON.parse(match[3]),
        metaDataType: metaDataType,
        metaData: _.jsonParse(this.dataTransfer.getData(metaDataType)) || this.dataTransfer.getData(metaDataType)
    };
}


// as defined here: https://developer.mozilla.org/en-US/docs/DragDrop/Drag_Operations#dragstart
function DragDrop$getAllowedEffects() {
    return this.dataTransfer.effectAllowed;
}


function DragDrop$setAllowedEffects(effects) {
    this.dataTransfer.effectAllowed = effects;
}


function DragDrop$getDropEffect() {
    return this.dataTransfer.dropEffect;
}


function DragDrop$setDropEffect(effect) {
    this.dataTransfer.dropEffect = effect;
}


function DragDrop$isEffectAllowed(effect) {
    var allowedEffects = this.getAllowedEffects()
        , isCopy = effect == 'copy'
        , isMove = effect == 'move'
        , isLink = effect == 'link'
        , isAllowed = isCopy || isLink || isMove;

    switch (allowedEffects) {
        case 'copy':
        case 'move':
        case 'link':
            return allowedEffects == effect;
        case 'copyLink':
            return isCopy || isLink;
        case 'copyMove':
            return isCopy || isMove;
        case 'linkMove':
            return isLink || isMove;
        case 'all':
        case 'uninitialized':
            return isAllowed;
        case 'none':
            return false;
    }
}


function DragDrop$getData(dataType) {
    return this.dataTransfer.getData(dataType);
}


function DragDrop$setData(dataType, dataStr) {
    this.dataTransfer.setData(dataType, dataStr);
}


function DragDrop$clearData(dataType) {
    this.dataTransfer.clearData(dataType);
}


/**
 * Drag drop service compensating for the lack of communication from drop target to drag source in DOM API
 */
var dragDropService = new Messenger;

var _currentDragDrop, _currentDragFacet;

_.extend(DragDrop, {
    service: dragDropService,
    destroy: DragDrop_destroy
});


dragDropService.onMessages({
    // data is DragDropDataTransfer instance
    // fired by Drag facet on "dragstart" event
    'dragdropstarted': onDragDropStarted,
    // data is object with at least dropEffect property
    // fired by Drop facet on "drop" event
    'dragdropcompleted': onDragDropCompleted,
    // fired by Drag facet on "dragend" event to complete drag
    // if drop happended in another window or if it was cancelled
    'completedragdrop': onCompleteDragDrop
});


_.extend(dragDropService, {
    getCurrentDragDrop: getCurrentDragDrop,
    getCurrentDragSource: getCurrentDragSource
});


function onDragDropStarted(msg, data) {
    _currentDragDrop = data.dragDrop;
    _currentDragFacet = data.dragFacet;
}


function onDragDropCompleted(msg, data) {
    _currentDragFacet && _currentDragFacet.postMessageSync('dragdropcompleted', data);
    _currentDragDrop = undefined;
    _currentDragFacet = undefined;
}


function onCompleteDragDrop(msg, data) {
    if (_currentDragDrop)
        dragDropService.postMessageSync('dragdropcompleted', data);
}


function getCurrentDragDrop() {
    return _currentDragDrop;
}


function getCurrentDragSource() {
    return _currentDragFacet && _currentDragFacet.owner;
}


function DragDrop_destroy() {
    dragDropService.offAll();
}