custom_bindings/dragula.js

/**
 * Custom Binding for drag and drop library dragula.js.
 * Supports copying, dragging, dropping and removing fields
 * @module app/custom_bindings/dragula
 * @requires knockout
 * @requires dragula
 * @requires jquery
 * @requires app/models/Field
 *
 * @example
 * <div data-bind="dragula: { data: myObservable }">
 *     //IMPORTANT: The "handle" class is used to determine the drag handle of the field so this is required
 *    <span class="handle">My draggable item</span>
 * </div>
 */
define(['knockout', 'dragula', 'jquery', 'models/field'], function(ko, dragula, $, Field) {

    var modalDisplayed = false;
    var parent = null;
    var published = false;


    var drake = dragula([], {
            copy: function(el, source) {
                return source.classList.contains('source') || source.classList.contains('repeatable-source');
            },
            accepts: function(el, target, source, sibling) {
                return !(contains(el, target)) &&
                    (target.classList.contains('target') ||
                        target.classList.contains('child-fields') ||
                        target.classList.contains('repeatable-target') ||
                        target.classList.contains('child-fields'));
            },
            moves: function(el, source, handle, sibling) {
                return handle.classList.contains('handle');
            },
            invalid: function(el, handle) {
                return !handle.classList.contains('handle');
            },
            revertOnSpill: true,
            copySortSource: false
        }),
        dataField = null;
    ///////////////////////PRIVATE METHODS: START

    // http://ejohn.org/blog/comparing-document-position/
    function contains(a, b) {
        return a.contains ?
            a != b && a.contains(b) :
            !!(a.compareDocumentPosition(b) & 16);
    }

    /**
     * UNUSED
     */
    function updateFieldNumberingFlag(parent, field) {
        return;
        console.log('updateFieldNumberingFlag', parent, field);
        var showNumbering = null;
        if (parent && parent.showNumbering) {
            if (ko.isObservable(parent.showNumbering) && parent.showNumbering()) {
                showNumbering = true;
            } else {
                showNumbering = false;
            }
        } else {
            showNumbering = false;
        }

        if (field && ko.isObservable(field.showNumbering)) {
            field.showNumbering(showNumbering);
        } else {
            field.showNumbering = showNumbering;
        }
    }
    ///////////////////////PRIVATE METHODS: END



    ///////////////////////EVENT LISTENERS: START
    drake.on('drag', function(el, source) {
        var parents = _.uniqBy(ko.contextFor(el).$parents, 'name');
        parent = parents[1];

        $('.modal').modal('hide');

        ko.utils.arrayForEach([].slice.call($('.child-fields'), 0), function(childContainer) {
            if (childContainer !== el) {
                $(childContainer).css({
                    'padding-bottom': '2vh'
                });
            }
        });


        if (!source.classList.contains('target') && !source.classList.contains('child-fields')) {
            ko.postbox.publish('hide-sidebar');
        }
        $(el).collapse('toggle');
    });
    drake.on('dragend', function(el, source) {

        $(el).removeClass('drag-start');


    });


    /**
     * DRAGULA AFTER DROP CALLBACK
     * @param  {DOM} el       - element that is dragged
     * @param  {DOM} target   - target element, will accept the element
     * @param  {DOM} source   - where the dragged element came from
     * @param  {DOM} sibling) - the adjacent element of the dragged item    
     */
    drake.on('drop', function(el, target, source, sibling) {

        // console.log(source.classList.contains('source'));
        if (source.classList.contains('source')) {
            ko.postbox.publish('open-sidebar');
        }
        // console.log(target.classList);
        if (target) {
            published = false;
            var indexOf = ko.utils.arrayIndexOf;
            if (target.classList.contains('target')) {
                console.log('a');
                var field = ko.dataFor(el),
                    fieldContext = ko.contextFor(el),
                    targetData = ko.dataFor(target),
                    fieldData = ko.utils.domData.get(el, 'field'),
                    position = 0;



                if (fieldData && fieldData.constructor.name !== 'Field') {
                    console.log('a.1');
                    ko.postbox.publish('addField', {
                        index: indexOf(target.children, el),
                        data: fieldData
                    });
                } else {
                    console.log('a.2');
                    if (field.parent) {
                        console.log('a.2.0');

                        ko.postbox.publish('removeChildField', {
                            field: fieldData,
                            parent: parent,
                            dragged: true
                        });
                        updateFieldNumberingFlag(parent, fieldData);

                    }
                    console.log('a.2.1');
                    updateFieldNumberingFlag(null, fieldData);

                    ko.postbox.publish('moveField', {
                        position: indexOf(target.children, el),
                        data: fieldData
                    });


                }


            } else {
                var index = indexOf(target.children, el),
                    fieldData = ko.utils.domData.get(el, 'field'),
                    parentElement = $(target).prev()[0],
                    parentData = ko.dataFor(target);

                updateFieldNumberingFlag(parentData, fieldData);

                console.log('b', fieldData);
                if (fieldData.parent && fieldData.parent === parentData.name) {
                    console.log('b.1');
                    ko.postbox.publish('moveChildField', {
                        position: indexOf(target.children, el),
                        parent: parentData,
                        field: fieldData
                    });
                } else {
                    console.log('b.2');
                    console.log(parentData);
                    ko.postbox.publish('addChildField', {
                        targetIndex: indexOf(target.children, el),
                        parent: parentData,
                        field: fieldData
                    });

                }

            }
            target.removeChild(el);
            ko.utils.arrayForEach([].slice.call($('.child-fields'), 0), function(childContainer) {
                $(childContainer).css({
                    'padding-bottom': 'initial'
                });
            });
        }

    });

    /**
     * DRAGULA CALL WHEN COPY IS SET TO TRUE
     * @param  {DOM} clone    - element that is cloned
     * @param  {DOM} original - original item from source
     */
    drake.on('cloned', function(clone, original, type) {
        ko.utils.domData.set(clone, 'field', ko.utils.domData.get(original, 'field'));
    });

    /**
     * THIS WILL ADD A target-highlight CSS CLASS
     * WHICH WILL ADD A DASHED BORDER AND A GREY 
     * BACKGROUND TO THE CONTAINER 
     */
    drake.on('over', function(el, container, source) {
        if (container !== source) {
            $(container).addClass('target-highlight');
        }
    });
    /**
     * THIS WILL REMOVE target-highlight CSS CLASS
     */
    drake.on('out', function(el, container, source) {
        if (container !== source) {
            $(container).removeClass('target-highlight');
        }
    });
    ///////////////////////EVENT LISTENERS: END


    ko.bindingHandlers.dragula = {
        init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            var options = ko.utils.unwrapObservable(valueAccessor()) || {};
            drake.containers.push(element);
            return ko.bindingHandlers.foreach.init(element, valueAccessor, allBindings, viewModel, bindingContext)
        },
        update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
            // this will force update to fire
            // valueAccessor().data();
            ko.unwrap(valueAccessor);
            ko.bindingHandlers.foreach.update(element, valueAccessor, allBindings, viewModel, bindingContext)
        }
    };

    return {
        drake: drake
    };
});