components/LogicButton.js

import AABB from '../AABB.js';
import Data from '../Data.js';
import StateMap from '../StateMap.js';
import createComponentClass from '../factory.js';

export default (function () {
    return createComponentClass(/** @lends platypus.components.LogicButton.prototype */{

        id: 'LogicButton',

        properties: {
            /**
             * The event to trigger when pressed.
             *
             * @property onPress
             * @type String
             * @default ""
             */
            "onPress": "",

            /**
             * The event to trigger when released.
             *
             * @property onRelease
             * @type String
             * @default ""
             */
            "onRelease": "",

            /**
             * The event to trigger when cancelled.
             *
             * @property onCancel
             * @type String
             * @default ""
             */
            "onCancel": "",

            /**
             * The event to trigger when the user mouses over the button
             *
             * @property hoverAudio
             * @type String|String[]|Message[]
             * @default ""
             */
            "onHover": "",

            /**
             * Whether this button's actions should be limited to the initial press/release.
             *
             * @property useOnce
             * @type Boolean
             * @default false
             */
            "useOnce": false,

            /**
             * Whether this button should start disabled.
             *
             * @property disabled
             * @type Boolean
             * @default false
             */
            "disabled": false,

            /**
             * Determines whether this button should behave as a toggle.
             *
             * @property toggle
             * @type Boolean
             * @default false
             */
            "toggle": false,

            /**
             * Specifies whether the button starts off 'pressed'; typically only useful for toggle buttons.
             *
             * @property pressed
             * @type Boolean
             * @default false
             */
            "pressed": false,

            /**
             * State that entity match be in for LogicButton to be enabled.
             * 
             * @property validState
             * @type String|StateMap
             * @default '!disabled'
             */
            validState: '!disabled',

            /**
             * State that parent entity match be in for LogicButton to be enabled. Default is "" unless "baton" is true in which case the default is "!baton"
             * 
             * @property validParentState
             * @type String|StateMap
             * @default ''
             */
            validParentState: '',

            /**
             * Whether multitouch is disallowed for this button to work: ie no other "baton: true" button may be currently pressed in order for this button to be enabled. This is useful for instances where a button may cause a conflict if another button is also activated.
             * 
             * @property baton
             * @type Boolean
             * @default false
             */
            baton: false
        },

        publicProperties: {
            /**
             * This sets the distance in world units from the bottom of the camera's world viewport. If set, it will override the entity's y coordinate. This property is accessible on the entity as `entity.bottom`.
             *
             * @property bottom
             * @type Number
             * @default null
             */
            "bottom": null,

            /**
             * This sets the distance in world units from the left of the camera's world viewport. If set, it will override the entity's x coordinate. This property is accessible on the entity as `entity.left`.
             *
             * @property left
             * @type Number
             * @default null
             */
            "left": null,

            /**
             * This sets the distance in world units from the right of the camera's world viewport. If set, it will override the entity's x coordinate. This property is accessible on the entity as `entity.right`.
             *
             * @property right
             * @type Number
             * @default null
             */
            "right": null,

            /**
             * This sets the distance in world units from the top of the camera's world viewport. If set, it will override the entity's y coordinate. This property is accessible on the entity as `entity.top`.
             *
             * @property top
             * @type Number
             * @default null
             */
            "top": null
        },

        /**
         * Provides button functionality for a RenderSprite component.
         *
         * @memberof platypus.components
         * @uses platypus.Component
         * @constructs
         * @listens platypus.Entity#camera-update
         * @listens platypus.Entity#disable
         * @listens platypus.Entity#enable
         * @listens platypus.Entity#handle-logic
         * @listens platypus.Entity#highlight
         * @listens platypus.Entity#pointerdown
         * @listens platypus.Entity#pointerout
         * @listens platypus.Entity#pointerover
         * @listens platypus.Entity#pressup
         * @listens platypus.Entity#toggle-disabled
         * @listens platypus.Entity#toggle-highlight
         * @listens platypus.Entity#unhighlight
         * @fires platypus.Entity#pressed
         * @fires platypus.Entity#cancelled
         * @fires platypus.Entity#released
         */
        initialize: function () {
            const
                {state} = this.owner;
            
            this.aabb = AABB.setUp();
            this.lastBottom = null;
            this.lastLeft = null;
            this.lastRight = null;
            this.lastTop = null;

            this.state = state;
            state.set('disabled', this.disabled);
            state.set('released', !this.pressed);
            state.set('pressed', this.pressed);
            state.set('highlighted', false);
            this.validParentState = StateMap.setUp(this.validParentState);
            this.validState = StateMap.setUp(this.validState);
            if (this.baton) {
                this.validParentState.set('baton', false);
                this.owner.parent.state.set('baton', false);
                this.batonIsMine = false;
            }
            this.owner.container.cursor = this.disabled ? null : 'pointer';
            this.cancelled = false;
            this.readyToToggle = false;
        },

        events: {
            "handle-logic": function () {
                const
                    {bottom, left, right, top} = this;

                if ((this.lastBottom !== bottom) || (this.lastLeft !== left) || (this.lastRight !== right) || (this.lastTop !== top)) {
                    this.updatePosition(this.aabb);
                    this.lastBottom = bottom;
                    this.lastLeft = left;
                    this.lastRight = right;
                    this.lastTop = top;
                }
            },

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

            "pointerdown": function (eventData) {
                if (this.checkStateValidity()) {
                    if (this.toggle) {
                        this.readyToToggle = true;
                    } else {
                        if (this.baton) {
                            this.batonIsMine = true;
                            this.owner.parent.state.set('baton', true);
                        }

                        if (this.onPress) {
                            this.owner.trigger(this.onPress);
                        }

                        /**
                         * This event is triggered when the button is pressed to mimic keypress events. If the button is a toggle button, this only occurs on up-to-down.
                         *
                         * @event platypus.Entity#pressed
                         * @param buttonState {platypus.Data} The state of the button
                         * @param buttonState.pressed {Boolean} This is `true` for the 'pressed' event.
                         * @param buttonState.released {Boolean} This is `false` for the 'pressed' event.
                         * @param buttonState.triggered {Boolean} This is `true` for the 'pressed' event.
                         * @param buttonState.entity {platypus.Entity} The entity for which the original event occurred.
                         */
                        this.updateStateAndTrigger('pressed');
                        if (eventData && eventData.pixiEvent && eventData.pixiEvent.stopPropagation) { // ensure a properly formed event has been sent
                            eventData.pixiEvent.stopPropagation();
                        }
                    }
                }
            },

            "pressup": function (eventData) {
                const
                    {state} = this;

                if (this.baton && this.batonIsMine) {
                    this.batonIsMine = false;
                    this.owner.parent.state.set('baton', false);
                }
                
                if (this.checkStateValidity()) {
                    if (this.cancelled) {
                        if (this.onCancel) {
                            this.owner.trigger(this.onCancel);
                        }

                        /**
                         * This event is triggered when the button is pressed and the mouse/touch is dragged off-target before release.
                         *
                         * @event platypus.Entity#cancelled
                         * @param buttonState {platypus.Data} The state of the button
                         * @param buttonState.pressed {Boolean} This is `false` for the 'cancelled' event.
                         * @param buttonState.released {Boolean} This is `true` for the 'cancelled' event.
                         * @param buttonState.triggered {Boolean} This is `false` for the 'cancelled' event.
                         * @param buttonState.entity {platypus.Entity} The entity for which the original event occurred.
                         */
                        this.updateStateAndTrigger('cancelled');
                    } else if (this.toggle) {
                        if (this.readyToToggle) {
                            if (state.get('pressed')) {
                                this.updateStateAndTrigger('released');
                            } else {
                                this.updateStateAndTrigger('pressed');
                            }
                        }
                    } else {
                        if (this.onRelease) {
                            this.owner.trigger(this.onRelease);
                        }

                        /**
                         * This event is triggered when the button is released, or on the down-to-up change for toggle buttons.
                         *
                         * @event platypus.Entity#released
                         * @param buttonState {platypus.Data} The state of the button
                         * @param buttonState.pressed {Boolean} This is `false` for the 'released' event.
                         * @param buttonState.released {Boolean} This is `true` for the 'released' event.
                         * @param buttonState.triggered {Boolean} This is `false` for the 'released' event.
                         * @param buttonState.entity {platypus.Entity} The entity for which the original event occurred.
                         */
                        this.updateStateAndTrigger('released');
                    }
                    if (eventData && eventData.pixiEvent && eventData.pixiEvent.stopPropagation) { // ensure a properly formed event has been sent
                        eventData.pixiEvent.stopPropagation();
                    }

                    // Doing this prevents the call from reccurring.
                    if (!this.cancelled && this.useOnce && this.removeEventListener) { //Second check is to ensure method exists which won't be the case if a result of the press is the button being destroyed.
                        this.removeEventListener('pointerdown');
                        this.removeEventListener('pressup');
                        this.state.set('disabled', true);
                        this.owner.container.cursor = null;
                    }
                }

                this.cancelled = false;
                this.readyToToggle = false;
            },

            "pointerover": function () {
                if (this.onHover) {
                    this.owner.trigger(this.onHover);
                }
                if (this.state.get('pressed')) {
                    this.cancelled = false;
                }
            },

            "pointerout": function () {
                if (this.state.get('pressed')) {
                    this.cancelled = true;
                }
            },
            
            /**
             * Disables the entity.
             *
             * @event platypus.Entity#disable
             */
            "disable": function () {
                this.state.set('disabled', true);
                this.owner.container.cursor = null;
            },
            
            /**
             * Enables the entity.
             *
             * @event platypus.Entity#enable
             */
            "enable": function () {
                this.state.set('disabled', false);
                this.owner.container.cursor = 'pointer';
            },

            /**
             * Toggles whether the entity is disabled.
             *
             * @event platypus.Entity#toggle-disabled
             */
            "toggle-disabled": function () {
                const
                    value = this.state.get('disabled');
                
                this.owner.container.cursor = value ? 'pointer': null;
                this.state.set('disabled', !value);
            },
            
            /**
             * Sets the entity's highlighted state to `true`.
             *
             * @event platypus.Entity#highlight
             */
            "highlight": function () {
                this.state.set('highlighted', true);
            },
            
            /**
             * Sets the entity's highlighted state to `false`.
             *
             * @event platypus.Entity#unhighlight
             */
            "unhighlight": function () {
                this.state.set('highlighted', false);
            },
            
            /**
             * Toggles the entity's highlighted state.
             *
             * @event platypus.Entity#toggle-highlight
             */
            "toggle-highlight": function () {
                const
                    {state} = this;

                state.set('highlighted', !state.get('highlighted'));
            }
        },
        
        methods: {
            checkStateValidity () {
                const
                    {owner, validParentState, validState} = this;

                return owner.state.includes(validState) && owner.parent.state.includes(validParentState);
            },

            updatePosition (vp) {
                const
                    {bottom, left, owner, right, top} = this;

                if (typeof left === 'number') {
                    owner.x = vp.left + left;
                } else if (typeof right === 'number') {
                    owner.x = vp.right - right;
                }

                if (typeof top === 'number') {
                    owner.y = vp.top + top;
                } else if (typeof bottom === 'number') {
                    owner.y = vp.bottom - bottom;
                }
            },

            updateStateAndTrigger (event) {
                const
                    {owner, state} = this,
                    pressed = state.get('pressed'),
                    released = state.get('released');
                let toggled = false;
                
                if (released && (event === 'pressed')) {
                    state.set('pressed', true);
                    state.set('released', false);
                    toggled = true;
                } else if (pressed && ((event === 'released') || (event === 'cancelled'))) {
                    state.set('pressed', false);
                    state.set('released', true);
                    toggled = true;
                }

                if (toggled) {
                    const
                        message = Data.setUp(
                            'released', pressed,
                            'pressed', released,
                            'triggered', released,
                            'entity', owner
                        );

                    owner.triggerEvent(event, message);
                    message.recycle();
                }
            },

            destroy: function () {
                if (this.baton && this.batonIsMine) {
                    this.batonIsMine = false;
                    this.owner.parent.state.set('baton', false);
                }

                this.aabb.recycle();
                this.aabb = null;
                this.owner.container.cursor = null;
                this.state = null;
            }
        }
    });
}());