components/LogicDragDrop.js

/* global platypus */
import AABB from '../AABB.js';
import {Rectangle} from 'pixi.js';
import createComponentClass from '../factory.js';

const
    claimHitArea = new Rectangle(-2000, -2000, 4000, 4000);

export default createComponentClass(/** @lends platypus.components.LogicDragDrop.prototype */{
    id: 'LogicDragDrop',
    
    properties: {
        /**
         * Sets the renderParent while being dragged.
         *
         * @property dragRenderParent
         * @type string
         * @default ''
         */
        dragRenderParent: '',
        
        /**
         * Sets whether a click-move should start the dragging behavior in addition to click-drag. This value is ignored for mobile devices.
         *
         * @property stickyClick
         * @type Boolean
         * @default false
         */
        stickyClick: false
    },
    
    /**
     * A component that allows an object to be dragged and dropped. Can use collision to prevent dropping the objects in certain locations.
     *
     * @memberof platypus.components
     * @uses platypus.Component
     * @constructs
     * @listens platypus.Entity#camera-update
     * @listens platypus.Entity#component-added
     * @listens platypus.Entity#handle-logic
     * @listens platypus.Entity#handle-post-collision-logic
     * @listens platypus.Entity#no-drop
     * @listens platypus.Entity#pointerdown
     * @listens platypus.Entity#pointermove
     * @listens platypus.Entity#prepare-logic
     * @listens platypus.Entity#pressmove
     * @listens platypus.Entity#pressup
     */
    initialize: function () {
        this.aabb = AABB.setUp();
        this.nextX = this.owner.x;
        this.nextY = this.owner.y;
        this.lastZ = this.owner.z;
        this.grabOffsetX = 0;
        this.grabOffsetY = 0;
        this.state = this.owner.state;
        this.state.set('dragging', false);
        this.state.set('noDrop', false);
        this.tryDrop = false;
        this.hitSomething = false;
        this.hasCollision = false;
        
        if (platypus.supports.mobile) {
            this.stickyClick = false;
        }
    },

    events: {
        "camera-update": function (camera) {
            this.aabb.set(camera.viewport);
            this.checkCamera();
        },

        "component-added": function (component) {
            if (component.type === 'CollisionBasic') {
                this.hasCollision = true;
            }
        },
        
        "prepare-logic": function () {
            this.checkCamera(); // may end dragging
        },

        "handle-logic": function () {
            if (this.state.get('dragging')) {
                this.owner.x = this.nextX;
                this.owner.y = this.nextY;
                this.owner.triggerEvent('hovering');
            }
            
            this.state.set('noDrop', false);
        },

        "handle-post-collision-logic": function () {
            if (this.tryDrop) {
                this.tryDrop = false;
                if (this.hitSomething) {
                    this.dropFailed = false;
                    this.state.set('noDrop', true);
                    this.state.set('dragging', true);
                    this.owner.dragMode = true;
                } else {
                    this.state.set('noDrop', false);
                    this.state.set('dragging', false);
                    this.owner.dragMode = false;
                }
            } else if (this.hitSomething) {
                this.state.set('noDrop', true);
            }
            this.hitSomething = false;
        },

        "pointerdown": function (eventData) {
            if (this.sticking) {
                this.sticking = false;
                this.releasePointer();
                this.release();
            } else {
                this.nextX = this.owner.x;
                this.nextY = this.owner.y;
                this.lastZ = this.owner.z;
                this.grabOffsetX = (eventData.x >> 0) - this.owner.x;
                this.grabOffsetY = (eventData.y >> 0) - this.owner.y;
                this.state.set('dragging', true);
                if (this.dragRenderParent !== this.owner.renderParent) {
                    this.originalRenderParent = this.owner.renderParent;

                    /**
                     * Sets the parent render container of an entity to that of the given entity or entity with the given id.
                     *
                     * @method platypus.Entity#set-parent-render-container
                     * @param entity {Object} The entity to relocate.
                     * @param container {Entity|String|PIXI.Container} The entity, id of the entity, or PIXI.Container that will act as the parent container.
                     */
                    this.owner.parent.triggerEvent("set-parent-render-container", this.owner, this.dragRenderParent);
                }
                this.owner.dragMode = true;
                this.sticking = this.stickyClick;
                if (this.sticking) {
                    this.claimPointer();
                }
            }
            
            eventData.pixiEvent.stopPropagation();
        },

        "pressup": function (eventData) {
            if (!this.sticking) {
                this.release();
            }
            
            eventData.pixiEvent.stopPropagation();
        },

        "pointermove": function (eventData) {
            if (this.sticking) {
                this.nextX = eventData.x - this.grabOffsetX;
                this.nextY = eventData.y - this.grabOffsetY;
                
                eventData.event.preventDefault();
                eventData.pixiEvent.stopPropagation();
            }
        },

        "pressmove": function (eventData) {
            this.nextX = eventData.x - this.grabOffsetX;
            this.nextY = eventData.y - this.grabOffsetY;
            if (this.sticking && (this.nextX !== this.owner.x || this.nextY !== this.owner.y)) {
                this.sticking = false;
                this.releasePointer();
            }
            
            eventData.event.preventDefault();
            eventData.pixiEvent.stopPropagation();
        },

        /**
         * This message comes from the collision system letting us know the object is currently in a location that it cannot be dropped.
         *
         * @event platypus.Entity#no-drop
         */
        "no-drop": function () {
            this.hitSomething = true;
        }
    },
    
    methods: {// These are methods that are called by this component.
        checkCamera: function () {
            if (this.state && this.state.get('dragging') && !this.aabb.containsPoint(this.nextX + this.grabOffsetX, this.nextY + this.grabOffsetY)) {
                if (this.sticking) {
                    this.sticking = false;
                    this.releasePointer();
                }
                this.release();
            }
        },

        claimPointer: function () {
            this.lastHitArea = this.owner.container.hitArea;

            this.owner.container.hitArea = claimHitArea; // capture all the clicks!
        },

        releasePointer: function () {
            this.owner.container.hitArea = this.lastHitArea;
        },

        release: function () {
            if (this.hasCollision) {
                this.tryDrop = true;
            } else {
                this.state.set('noDrop', false);
                this.state.set('dragging', false);
                if (this.originalRenderParent) {
                    this.owner.parent.triggerEvent("set-parent-render-container", this.owner, this.originalRenderParent);
                }
                this.owner.dragMode = false;
                this.owner.z = this.lastZ;
            }
            this.owner.triggerEvent('dropped', this.owner);
        },
        
        destroy: function () {
            this.state.set('dragging', false);
            this.owner.dragMode = false;
            this.state.set('noDrop', false);
            this.state = null;
            this.aabb.recycle();
            this.aabb = null;
            this.owner.z = this.lastZ;
        }
    }
});