diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7a24316..ac74225 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -4,10 +4,9 @@ import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; -import MobileDevice from 'ismobilejs'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, interactiveTarget ); @@ -156,26 +155,6 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** @@ -220,20 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * Every update cursor will be reset to this value, if some element wont override it in - * its hitTest. - * - * @member {string} - * @default 'inherit' + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.defaultCursorStyle = 'inherit'; + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -625,7 +616,7 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. @@ -655,16 +646,48 @@ } } - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -866,7 +889,7 @@ // Guaranteed that there will be at least one event in events, and all events must have the same pointer type - if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) + if (this.autoPreventDefault && events[0].isNormalized) { originalEvent.preventDefault(); } @@ -915,7 +938,10 @@ if (hit) { - displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); if (e.type === 'touchstart' || e.pointerType === 'touch') @@ -928,11 +954,11 @@ if (isRightButton) { - displayObject.getTrackedPointers()[id].rightDown = true; + displayObject.trackedPointers[id].rightDown = true; } else { - displayObject.getTrackedPointers()[id].leftDown = true; + displayObject.trackedPointers[id].leftDown = true; } this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); @@ -1006,9 +1032,9 @@ const id = interactionEvent.data.identifier; - if (displayObject.getTrackedPointers()[id] !== undefined) + if (displayObject.trackedPointers[id] !== undefined) { - delete displayObject.getTrackedPointers()[id]; + delete displayObject.trackedPointers[id]; this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); if (e.type === 'touchcancel' || e.pointerType === 'touch') @@ -1043,32 +1069,12 @@ const id = interactionEvent.data.identifier; - const trackingData = displayObject.getTrackedPointers()[id]; + const trackingData = displayObject.trackedPointers[id]; const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); - // Pointers and Touches - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - - if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointertap', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); - } - } - else if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); - } - // Mouse only if (isMouse) { @@ -1093,6 +1099,47 @@ { this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } + } + } + + // Pointers and Touches, and Mouse + if (hit) + { + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1110,7 +1157,7 @@ { this.didMove = true; - this.cursor = this.defaultCursorStyle; + this.cursor = null; } const eventLen = events.length; @@ -1140,10 +1187,9 @@ if (events[0].pointerType === 'mouse') { - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO BUG for parents interactive object (border order issue) @@ -1166,7 +1212,7 @@ const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); - if (e.type !== 'touchmove') + if (isMouse) { this.processPointerOverOut(interactionEvent, displayObject, hit); } @@ -1195,7 +1241,7 @@ if (event.pointerType === 'mouse') { this.mouseOverRenderer = false; - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + this.setCursorMode(null); } const interactionData = this.getInteractionDataForPointerId(event.pointerId); @@ -1229,7 +1275,13 @@ const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); - const trackingData = displayObject.getTrackedPointers()[id]; + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } if (trackingData === undefined) return; @@ -1245,9 +1297,9 @@ } } - if (isMouse && displayObject.buttonMode) + if (isMouse) { - this.cursor = displayObject.defaultCursor; + this.cursor = displayObject.cursor; } } else if (trackingData.over) @@ -1258,6 +1310,11 @@ { this.dispatchEvent(displayObject, 'mouseout', interactionEvent); } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1405,10 +1462,14 @@ if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + normalizedEvents.push(touch); } } - else if (event instanceof MouseEvent) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1420,6 +1481,9 @@ if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + normalizedEvents.push(event); } else diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7a24316..ac74225 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -4,10 +4,9 @@ import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; -import MobileDevice from 'ismobilejs'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, interactiveTarget ); @@ -156,26 +155,6 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** @@ -220,20 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * Every update cursor will be reset to this value, if some element wont override it in - * its hitTest. - * - * @member {string} - * @default 'inherit' + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.defaultCursorStyle = 'inherit'; + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -625,7 +616,7 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. @@ -655,16 +646,48 @@ } } - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -866,7 +889,7 @@ // Guaranteed that there will be at least one event in events, and all events must have the same pointer type - if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) + if (this.autoPreventDefault && events[0].isNormalized) { originalEvent.preventDefault(); } @@ -915,7 +938,10 @@ if (hit) { - displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); if (e.type === 'touchstart' || e.pointerType === 'touch') @@ -928,11 +954,11 @@ if (isRightButton) { - displayObject.getTrackedPointers()[id].rightDown = true; + displayObject.trackedPointers[id].rightDown = true; } else { - displayObject.getTrackedPointers()[id].leftDown = true; + displayObject.trackedPointers[id].leftDown = true; } this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); @@ -1006,9 +1032,9 @@ const id = interactionEvent.data.identifier; - if (displayObject.getTrackedPointers()[id] !== undefined) + if (displayObject.trackedPointers[id] !== undefined) { - delete displayObject.getTrackedPointers()[id]; + delete displayObject.trackedPointers[id]; this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); if (e.type === 'touchcancel' || e.pointerType === 'touch') @@ -1043,32 +1069,12 @@ const id = interactionEvent.data.identifier; - const trackingData = displayObject.getTrackedPointers()[id]; + const trackingData = displayObject.trackedPointers[id]; const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); - // Pointers and Touches - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - - if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointertap', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); - } - } - else if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); - } - // Mouse only if (isMouse) { @@ -1093,6 +1099,47 @@ { this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } + } + } + + // Pointers and Touches, and Mouse + if (hit) + { + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1110,7 +1157,7 @@ { this.didMove = true; - this.cursor = this.defaultCursorStyle; + this.cursor = null; } const eventLen = events.length; @@ -1140,10 +1187,9 @@ if (events[0].pointerType === 'mouse') { - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO BUG for parents interactive object (border order issue) @@ -1166,7 +1212,7 @@ const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); - if (e.type !== 'touchmove') + if (isMouse) { this.processPointerOverOut(interactionEvent, displayObject, hit); } @@ -1195,7 +1241,7 @@ if (event.pointerType === 'mouse') { this.mouseOverRenderer = false; - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + this.setCursorMode(null); } const interactionData = this.getInteractionDataForPointerId(event.pointerId); @@ -1229,7 +1275,13 @@ const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); - const trackingData = displayObject.getTrackedPointers()[id]; + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } if (trackingData === undefined) return; @@ -1245,9 +1297,9 @@ } } - if (isMouse && displayObject.buttonMode) + if (isMouse) { - this.cursor = displayObject.defaultCursor; + this.cursor = displayObject.cursor; } } else if (trackingData.over) @@ -1258,6 +1310,11 @@ { this.dispatchEvent(displayObject, 'mouseout', interactionEvent); } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1405,10 +1462,14 @@ if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + normalizedEvents.push(touch); } } - else if (event instanceof MouseEvent) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1420,6 +1481,9 @@ if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + normalizedEvents.push(event); } else diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 3ce38b6..6a1f69e 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -65,6 +65,17 @@ } /** + * Is the tracked event inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** * Is the tracked event over the DisplayObject? * * @member {boolean} @@ -72,7 +83,7 @@ */ get over() { - return (this._flags | this.constructor.FLAGS.OVER) !== 0; + return (this._flags & this.constructor.FLAGS.OVER) !== 0; } /** @@ -93,7 +104,7 @@ */ get rightDown() { - return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.RIGHT_DOWN) !== 0; } /** @@ -114,7 +125,7 @@ */ get leftDown() { - return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.LEFT_DOWN) !== 0; } /** diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7a24316..ac74225 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -4,10 +4,9 @@ import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; -import MobileDevice from 'ismobilejs'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, interactiveTarget ); @@ -156,26 +155,6 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** @@ -220,20 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * Every update cursor will be reset to this value, if some element wont override it in - * its hitTest. - * - * @member {string} - * @default 'inherit' + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.defaultCursorStyle = 'inherit'; + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -625,7 +616,7 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. @@ -655,16 +646,48 @@ } } - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -866,7 +889,7 @@ // Guaranteed that there will be at least one event in events, and all events must have the same pointer type - if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) + if (this.autoPreventDefault && events[0].isNormalized) { originalEvent.preventDefault(); } @@ -915,7 +938,10 @@ if (hit) { - displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); if (e.type === 'touchstart' || e.pointerType === 'touch') @@ -928,11 +954,11 @@ if (isRightButton) { - displayObject.getTrackedPointers()[id].rightDown = true; + displayObject.trackedPointers[id].rightDown = true; } else { - displayObject.getTrackedPointers()[id].leftDown = true; + displayObject.trackedPointers[id].leftDown = true; } this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); @@ -1006,9 +1032,9 @@ const id = interactionEvent.data.identifier; - if (displayObject.getTrackedPointers()[id] !== undefined) + if (displayObject.trackedPointers[id] !== undefined) { - delete displayObject.getTrackedPointers()[id]; + delete displayObject.trackedPointers[id]; this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); if (e.type === 'touchcancel' || e.pointerType === 'touch') @@ -1043,32 +1069,12 @@ const id = interactionEvent.data.identifier; - const trackingData = displayObject.getTrackedPointers()[id]; + const trackingData = displayObject.trackedPointers[id]; const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); - // Pointers and Touches - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - - if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointertap', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); - } - } - else if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); - } - // Mouse only if (isMouse) { @@ -1093,6 +1099,47 @@ { this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } + } + } + + // Pointers and Touches, and Mouse + if (hit) + { + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1110,7 +1157,7 @@ { this.didMove = true; - this.cursor = this.defaultCursorStyle; + this.cursor = null; } const eventLen = events.length; @@ -1140,10 +1187,9 @@ if (events[0].pointerType === 'mouse') { - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO BUG for parents interactive object (border order issue) @@ -1166,7 +1212,7 @@ const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); - if (e.type !== 'touchmove') + if (isMouse) { this.processPointerOverOut(interactionEvent, displayObject, hit); } @@ -1195,7 +1241,7 @@ if (event.pointerType === 'mouse') { this.mouseOverRenderer = false; - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + this.setCursorMode(null); } const interactionData = this.getInteractionDataForPointerId(event.pointerId); @@ -1229,7 +1275,13 @@ const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); - const trackingData = displayObject.getTrackedPointers()[id]; + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } if (trackingData === undefined) return; @@ -1245,9 +1297,9 @@ } } - if (isMouse && displayObject.buttonMode) + if (isMouse) { - this.cursor = displayObject.defaultCursor; + this.cursor = displayObject.cursor; } } else if (trackingData.over) @@ -1258,6 +1310,11 @@ { this.dispatchEvent(displayObject, 'mouseout', interactionEvent); } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1405,10 +1462,14 @@ if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + normalizedEvents.push(touch); } } - else if (event instanceof MouseEvent) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1420,6 +1481,9 @@ if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + normalizedEvents.push(event); } else diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 3ce38b6..6a1f69e 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -65,6 +65,17 @@ } /** + * Is the tracked event inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** * Is the tracked event over the DisplayObject? * * @member {boolean} @@ -72,7 +83,7 @@ */ get over() { - return (this._flags | this.constructor.FLAGS.OVER) !== 0; + return (this._flags & this.constructor.FLAGS.OVER) !== 0; } /** @@ -93,7 +104,7 @@ */ get rightDown() { - return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.RIGHT_DOWN) !== 0; } /** @@ -114,7 +125,7 @@ */ get leftDown() { - return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.LEFT_DOWN) !== 0; } /** diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index ebfaacc..d252fba 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -38,32 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', + cursor: null, /** * Internal set of all active pointers, by identifier * - * @returns {Map} Map of all tracked pointers, by identifier + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - getTrackedPointers: function getTrackedPointers() + get trackedPointers() { if (this._trackedPointers === undefined) this._trackedPointers = {}; return this._trackedPointers; }, + + /** + * Map of all tracked pointers, by identifier. Use trackedPointers to access. + * + * @private {Map} + */ + _trackedPointers: undefined, }; diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7a24316..ac74225 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -4,10 +4,9 @@ import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; -import MobileDevice from 'ismobilejs'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, interactiveTarget ); @@ -156,26 +155,6 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** @@ -220,20 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * Every update cursor will be reset to this value, if some element wont override it in - * its hitTest. - * - * @member {string} - * @default 'inherit' + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.defaultCursorStyle = 'inherit'; + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -625,7 +616,7 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. @@ -655,16 +646,48 @@ } } - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -866,7 +889,7 @@ // Guaranteed that there will be at least one event in events, and all events must have the same pointer type - if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) + if (this.autoPreventDefault && events[0].isNormalized) { originalEvent.preventDefault(); } @@ -915,7 +938,10 @@ if (hit) { - displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); if (e.type === 'touchstart' || e.pointerType === 'touch') @@ -928,11 +954,11 @@ if (isRightButton) { - displayObject.getTrackedPointers()[id].rightDown = true; + displayObject.trackedPointers[id].rightDown = true; } else { - displayObject.getTrackedPointers()[id].leftDown = true; + displayObject.trackedPointers[id].leftDown = true; } this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); @@ -1006,9 +1032,9 @@ const id = interactionEvent.data.identifier; - if (displayObject.getTrackedPointers()[id] !== undefined) + if (displayObject.trackedPointers[id] !== undefined) { - delete displayObject.getTrackedPointers()[id]; + delete displayObject.trackedPointers[id]; this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); if (e.type === 'touchcancel' || e.pointerType === 'touch') @@ -1043,32 +1069,12 @@ const id = interactionEvent.data.identifier; - const trackingData = displayObject.getTrackedPointers()[id]; + const trackingData = displayObject.trackedPointers[id]; const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); - // Pointers and Touches - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - - if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointertap', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); - } - } - else if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); - } - // Mouse only if (isMouse) { @@ -1093,6 +1099,47 @@ { this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } + } + } + + // Pointers and Touches, and Mouse + if (hit) + { + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1110,7 +1157,7 @@ { this.didMove = true; - this.cursor = this.defaultCursorStyle; + this.cursor = null; } const eventLen = events.length; @@ -1140,10 +1187,9 @@ if (events[0].pointerType === 'mouse') { - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO BUG for parents interactive object (border order issue) @@ -1166,7 +1212,7 @@ const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); - if (e.type !== 'touchmove') + if (isMouse) { this.processPointerOverOut(interactionEvent, displayObject, hit); } @@ -1195,7 +1241,7 @@ if (event.pointerType === 'mouse') { this.mouseOverRenderer = false; - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + this.setCursorMode(null); } const interactionData = this.getInteractionDataForPointerId(event.pointerId); @@ -1229,7 +1275,13 @@ const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); - const trackingData = displayObject.getTrackedPointers()[id]; + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } if (trackingData === undefined) return; @@ -1245,9 +1297,9 @@ } } - if (isMouse && displayObject.buttonMode) + if (isMouse) { - this.cursor = displayObject.defaultCursor; + this.cursor = displayObject.cursor; } } else if (trackingData.over) @@ -1258,6 +1310,11 @@ { this.dispatchEvent(displayObject, 'mouseout', interactionEvent); } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1405,10 +1462,14 @@ if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + normalizedEvents.push(touch); } } - else if (event instanceof MouseEvent) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1420,6 +1481,9 @@ if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + normalizedEvents.push(event); } else diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 3ce38b6..6a1f69e 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -65,6 +65,17 @@ } /** + * Is the tracked event inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** * Is the tracked event over the DisplayObject? * * @member {boolean} @@ -72,7 +83,7 @@ */ get over() { - return (this._flags | this.constructor.FLAGS.OVER) !== 0; + return (this._flags & this.constructor.FLAGS.OVER) !== 0; } /** @@ -93,7 +104,7 @@ */ get rightDown() { - return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.RIGHT_DOWN) !== 0; } /** @@ -114,7 +125,7 @@ */ get leftDown() { - return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.LEFT_DOWN) !== 0; } /** diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index ebfaacc..d252fba 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -38,32 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', + cursor: null, /** * Internal set of all active pointers, by identifier * - * @returns {Map} Map of all tracked pointers, by identifier + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - getTrackedPointers: function getTrackedPointers() + get trackedPointers() { if (this._trackedPointers === undefined) this._trackedPointers = {}; return this._trackedPointers; }, + + /** + * Map of all tracked pointers, by identifier. Use trackedPointers to access. + * + * @private {Map} + */ + _trackedPointers: undefined, }; diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index 8c7601b..9065b7f 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -4,6 +4,101 @@ describe('PIXI.interaction.InteractionManager', function () { + describe('event basics', function () + { + it('should call mousedown handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mousedown', eventSpy); + + pointer.mousedown(10, 10); + + expect(eventSpy).to.have.been.calledOnce; + }); + + it('should call mouseup handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseup', eventSpy); + + pointer.click(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseupoutside handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseupoutside', eventSpy); + + pointer.mousedown(10, 10); + pointer.mouseup(60, 60); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseover handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseover', eventSpy); + + pointer.mousemove(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseout handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseout', eventSpy); + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(eventSpy).to.have.been.called; + }); + }); + describe('onClick', function () { it('should call handler when inside', function () @@ -462,6 +557,150 @@ }); }); + describe('cursor changes', function () + { + it('cursor should be the cursor of interactive item', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default on mouseout', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('should still be the over cursor after a click', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.click(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default when mouse leaves renderer', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(-10, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('cursor callback should be called', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const overSpy = sinon.spy(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = overSpy; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(overSpy).to.have.been.called; + expect(defaultSpy).to.have.been.called; + }); + + it('cursor style object should be fully applied', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = { + cursor: 'none', + display: 'none', + }; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('none'); + expect(pointer.renderer.view.style.display).to.equal('none'); + }); + + it('should not change cursor style if no cursor style provided', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'pointer'; + pointer.interaction.cursorStyles.pointer = null; + pointer.interaction.cursorStyles.default = null; + + pointer.mousemove(10, 10); + expect(pointer.renderer.view.style.cursor).to.equal(''); + + pointer.mousemove(60, 60); + expect(pointer.renderer.view.style.cursor).to.equal(''); + }); + }); + describe('recursive hitTesting', function () { function getScene() diff --git a/src/accessibility/AccessibilityManager.js b/src/accessibility/AccessibilityManager.js index df2716a..f16bcb1 100644 --- a/src/accessibility/AccessibilityManager.js +++ b/src/accessibility/AccessibilityManager.js @@ -3,7 +3,7 @@ import accessibleTarget from './accessibleTarget'; // add some extra variables to the container.. -Object.assign( +core.utils.mixins.delayMixin( core.DisplayObject.prototype, accessibleTarget ); diff --git a/src/core/utils/index.js b/src/core/utils/index.js index 2a7497b..2f9c71b 100644 --- a/src/core/utils/index.js +++ b/src/core/utils/index.js @@ -2,6 +2,7 @@ import settings from '../settings'; import EventEmitter from 'eventemitter3'; import pluginTarget from './pluginTarget'; +import * as mixins from './mixin'; import * as isMobile from 'ismobilejs'; let nextUid = 0; @@ -33,6 +34,7 @@ * @type {mixin} */ pluginTarget, + mixins, }; /** diff --git a/src/core/utils/mixin.js b/src/core/utils/mixin.js new file mode 100644 index 0000000..114de2a --- /dev/null +++ b/src/core/utils/mixin.js @@ -0,0 +1,59 @@ +/** + * Mixes all enumerable properties and methods from a source object to a target object. + * + * @memberof PIXI.utils.mixins + * @function mixin + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function mixin(target, source) +{ + if (!target || !source) return; + // in ES8/ES2017, this would be really easy: + // Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)); + + // get all the enumerable property keys + const keys = Object.keys(source); + + // loop through properties + for (let i = 0; i < keys.length; ++i) + { + const propertyName = keys[i]; + + // Set the property using the property descriptor - this works for accessors and normal value properties + Object.defineProperty(target, propertyName, Object.getOwnPropertyDescriptor(source, propertyName)); + } +} + +const mixins = []; + +/** + * Queues a mixin to be handled towards the end of the initialization of PIXI, so that deprecation + * can take effect. + * + * @memberof PIXI.utils.mixins + * @function delayMixin + * @private + * @param {object} target The prototype or instance that properties and methods should be added to. + * @param {object} source The source of properties and methods to mix in. + */ +export function delayMixin(target, source) +{ + mixins.push(target, source); +} + +/** + * Handles all mixins queued via delayMixin(). + * + * @memberof PIXI.utils.mixins + * @function performMixins + * @private + */ +export function performMixins() +{ + for (let i = 0; i < mixins.length; i += 2) + { + mixin(mixins[i], mixins[i + 1]); + } + mixins.length = 0; +} diff --git a/src/deprecation.js b/src/deprecation.js index 045a19f..ef31f84 100644 --- a/src/deprecation.js +++ b/src/deprecation.js @@ -5,6 +5,7 @@ import * as filters from './filters'; import * as prepare from './prepare'; import * as loaders from './loaders'; +import * as interaction from './interaction'; // provide method to give a stack track for warnings // useful for tracking-down where deprecated methods/properties/classes @@ -1003,3 +1004,69 @@ }, }, }); + +/** + * @name PIXI.interaction.interactiveTarget#defaultCursor + * @static + * @type {number} + * @see PIXI.interaction.interactiveTarget#cursor + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.interactiveTarget, 'defaultCursor', { + set(value) + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + this.cursor = value; + }, + get() + { + warn('Property defaultCursor has been replaced with \'cursor\'. '); + + return this.cursor; + }, + enumerable: true, +}); + +/** + * @name PIXI.interaction.InteractionManager#defaultCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'defaultCursorStyle', { + set(value) + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + this.cursorStyles.default = value; + }, + get() + { + warn('Property defaultCursorStyle has been replaced with \'cursorStyles.default\'. '); + + return this.cursorStyles.default; + }, +}); + +/** + * @name PIXI.interaction.InteractionManager#currentCursorStyle + * @static + * @type {string} + * @see PIXI.interaction.InteractionManager#cursorStyles + * @deprecated since 4.3.0 + */ +Object.defineProperty(interaction.InteractionManager, 'currentCursorStyle', { + set(value) + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + this.currentCursorMode = value; + }, + get() + { + warn('Property currentCursorStyle has been removed.' + + 'See the currentCursorMode property, which works differently.'); + + return this.currentCursorMode; + }, +}); diff --git a/src/index.js b/src/index.js index 59356cf..c553427 100644 --- a/src/index.js +++ b/src/index.js @@ -16,6 +16,10 @@ import * as particles from './particles'; import * as prepare from './prepare'; +// handle mixins now, after all code has been added, including deprecation +import { utils } from './core'; +utils.mixins.performMixins(); + export { accessibility, extract, diff --git a/src/interaction/InteractionManager.js b/src/interaction/InteractionManager.js index 7a24316..ac74225 100644 --- a/src/interaction/InteractionManager.js +++ b/src/interaction/InteractionManager.js @@ -4,10 +4,9 @@ import InteractionTrackingData from './InteractionTrackingData'; import EventEmitter from 'eventemitter3'; import interactiveTarget from './interactiveTarget'; -import MobileDevice from 'ismobilejs'; -// Mix interactiveTarget into core.DisplayObject.prototype -Object.assign( +// Mix interactiveTarget into core.DisplayObject.prototype, after deprecation has been handled +core.utils.mixins.delayMixin( core.DisplayObject.prototype, interactiveTarget ); @@ -156,26 +155,6 @@ */ this.supportsPointerEvents = !!window.PointerEvent; - /** - * Are touch events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a touch screen mobile device, a touchstart would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeTouchEvents = !this.supportsPointerEvents && this.supportsTouchEvents; - - /** - * Are mouse events being 'normalized' and converted into pointer events if pointer events are not supported - * For example, on a desktop pc, a mousedown would also be emitted as a pointerdown - * - * @private - * @readonly - * @member {boolean} - */ - this.normalizeMouseEvents = !this.supportsPointerEvents && !MobileDevice.any; - // this will make it so that you don't have to call bind all the time /** @@ -220,20 +199,32 @@ this.onPointerOver = this.onPointerOver.bind(this); /** - * Every update cursor will be reset to this value, if some element wont override it in - * its hitTest. - * - * @member {string} - * @default 'inherit' + * Dictionary of how different cursor modes are handled. Strings are handled as CSS cursor + * values, objects are handled as dictionaries of CSS values for interactionDOMElement, + * and functions are called instead of changing the CSS. + * Default CSS cursor values are provided for 'default' and 'pointer' modes. + * @member {Object.)>} */ - this.defaultCursorStyle = 'inherit'; + this.cursorStyles = { + default: 'inherit', + pointer: 'pointer', + }; /** - * The css style of the cursor that is being used. + * The mode of the cursor that is being used. + * The value of this is a key from the cursorStyles dictionary. * * @member {string} */ - this.currentCursorStyle = 'inherit'; + this.currentCursorMode = null; + + /** + * Internal cached let. + * + * @private + * @member {string} + */ + this.cursor = null; /** * Internal cached let. @@ -625,7 +616,7 @@ return; } - this.cursor = this.defaultCursorStyle; + this.cursor = null; // Resets the flag as set by a stopPropagation call. This flag is usually reset by a user interaction of any kind, // but there was a scenario of a display object moving under a static mouse cursor. @@ -655,16 +646,48 @@ } } - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO } /** + * Sets the current cursor mode, handling any callbacks or CSS style changes. + * + * @param {string} mode - cursor mode, a key from the cursorStyles dictionary + */ + setCursorMode(mode) + { + mode = mode || 'default'; + this.currentCursorMode = mode; + const style = this.cursorStyles[mode]; + + // only do things if there is a cursor style for it + if (style) + { + switch (typeof style) + { + case 'string': + // string styles are handled as cursor CSS + this.interactionDOMElement.style.cursor = style; + break; + case 'function': + // functions are just called, and passed the cursor mode + style(mode); + break; + case 'object': + // if it is an object, assume that it is a dictionary of CSS styles, + // apply it to the interactionDOMElement + Object.assign(this.interactionDOMElement.style, style); + break; + } + } + } + + /** * Dispatches an event on the display object that was interacted with * * @param {PIXI.Container|PIXI.Sprite|PIXI.extras.TilingSprite} displayObject - the display object in question @@ -866,7 +889,7 @@ // Guaranteed that there will be at least one event in events, and all events must have the same pointer type - if (this.autoPreventDefault && (events[0].pointerType === 'touch' || events[0].pointerType === 'mouse')) + if (this.autoPreventDefault && events[0].isNormalized) { originalEvent.preventDefault(); } @@ -915,7 +938,10 @@ if (hit) { - displayObject.getTrackedPointers()[id] = new InteractionTrackingData(id); + if (!displayObject.trackedPointers[id]) + { + displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } this.dispatchEvent(displayObject, 'pointerdown', interactionEvent); if (e.type === 'touchstart' || e.pointerType === 'touch') @@ -928,11 +954,11 @@ if (isRightButton) { - displayObject.getTrackedPointers()[id].rightDown = true; + displayObject.trackedPointers[id].rightDown = true; } else { - displayObject.getTrackedPointers()[id].leftDown = true; + displayObject.trackedPointers[id].leftDown = true; } this.dispatchEvent(displayObject, isRightButton ? 'rightdown' : 'mousedown', interactionEvent); @@ -1006,9 +1032,9 @@ const id = interactionEvent.data.identifier; - if (displayObject.getTrackedPointers()[id] !== undefined) + if (displayObject.trackedPointers[id] !== undefined) { - delete displayObject.getTrackedPointers()[id]; + delete displayObject.trackedPointers[id]; this.dispatchEvent(displayObject, 'pointercancel', interactionEvent); if (e.type === 'touchcancel' || e.pointerType === 'touch') @@ -1043,32 +1069,12 @@ const id = interactionEvent.data.identifier; - const trackingData = displayObject.getTrackedPointers()[id]; + const trackingData = displayObject.trackedPointers[id]; const isTouch = (e.type === 'touchend' || e.pointerType === 'touch'); const isMouse = (e.type.indexOf('mouse') === 0 || e.pointerType === 'mouse'); - // Pointers and Touches - if (hit) - { - this.dispatchEvent(displayObject, 'pointerup', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); - - if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointertap', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'tap', interactionEvent); - } - } - else if (displayObject.getTrackedPointers()[id] !== undefined) - { - delete displayObject.getTrackedPointers()[id]; - this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); - if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); - } - // Mouse only if (isMouse) { @@ -1093,6 +1099,47 @@ { this.dispatchEvent(displayObject, isRightButton ? 'rightupoutside' : 'mouseupoutside', interactionEvent); } + // update the down state of the tracking data + if (trackingData) + { + if (isRightButton) + { + trackingData.rightDown = hit; + } + else + { + trackingData.leftDown = hit; + } + } + } + + // Pointers and Touches, and Mouse + if (hit) + { + this.dispatchEvent(displayObject, 'pointerup', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchend', interactionEvent); + + if (trackingData) + { + this.dispatchEvent(displayObject, 'pointertap', interactionEvent); + if (isTouch) + { + this.dispatchEvent(displayObject, 'tap', interactionEvent); + // touches are no longer over (if they ever were) when we get the touchend + // so we should ensure that we don't keep pretending that they are + trackingData.over = false; + } + } + } + else if (trackingData) + { + this.dispatchEvent(displayObject, 'pointerupoutside', interactionEvent); + if (isTouch) this.dispatchEvent(displayObject, 'touchendoutside', interactionEvent); + } + // Only remove the tracking data if there is no over/down state still associated with it + if (trackingData && trackingData.none) + { + delete displayObject.trackedPointers[id]; } } @@ -1110,7 +1157,7 @@ { this.didMove = true; - this.cursor = this.defaultCursorStyle; + this.cursor = null; } const eventLen = events.length; @@ -1140,10 +1187,9 @@ if (events[0].pointerType === 'mouse') { - if (this.currentCursorStyle !== this.cursor) + if (this.currentCursorMode !== this.cursor) { - this.currentCursorStyle = this.cursor; - this.interactionDOMElement.style.cursor = this.cursor; + this.setCursorMode(this.cursor); } // TODO BUG for parents interactive object (border order issue) @@ -1166,7 +1212,7 @@ const isMouse = (e.type === 'mousemove' || e.pointerType === 'mouse'); - if (e.type !== 'touchmove') + if (isMouse) { this.processPointerOverOut(interactionEvent, displayObject, hit); } @@ -1195,7 +1241,7 @@ if (event.pointerType === 'mouse') { this.mouseOverRenderer = false; - this.interactionDOMElement.style.cursor = this.defaultCursorStyle; + this.setCursorMode(null); } const interactionData = this.getInteractionDataForPointerId(event.pointerId); @@ -1229,7 +1275,13 @@ const isMouse = (e.type === 'mouseover' || e.type === 'mouseout' || e.pointerType === 'mouse'); - const trackingData = displayObject.getTrackedPointers()[id]; + let trackingData = displayObject.trackedPointers[id]; + + // if we just moused over the display object, then we need to track that state + if (hit && !trackingData) + { + trackingData = displayObject.trackedPointers[id] = new InteractionTrackingData(id); + } if (trackingData === undefined) return; @@ -1245,9 +1297,9 @@ } } - if (isMouse && displayObject.buttonMode) + if (isMouse) { - this.cursor = displayObject.defaultCursor; + this.cursor = displayObject.cursor; } } else if (trackingData.over) @@ -1258,6 +1310,11 @@ { this.dispatchEvent(displayObject, 'mouseout', interactionEvent); } + // if there is no mouse down information for the pointer, then it is safe to delete + if (trackingData.none) + { + delete displayObject.trackedPointers[id]; + } } } @@ -1405,10 +1462,14 @@ if (typeof touch.layerX === 'undefined') touch.layerX = touch.offsetX = touch.clientX; if (typeof touch.layerY === 'undefined') touch.layerY = touch.offsetY = touch.clientY; + // mark the touch as normalized, just so that we know we did it + touch.isNormalized = true; + normalizedEvents.push(touch); } } - else if (event instanceof MouseEvent) + // apparently PointerEvent subclasses MouseEvent, so yay + else if (event instanceof MouseEvent && (!this.supportsPointerEvents || !(event instanceof window.PointerEvent))) { if (typeof event.isPrimary === 'undefined') event.isPrimary = true; if (typeof event.width === 'undefined') event.width = 1; @@ -1420,6 +1481,9 @@ if (typeof event.pressure === 'undefined') event.pressure = 0.5; if (typeof event.rotation === 'undefined') event.rotation = 0; + // mark the mouse event as normalized, just so that we know we did it + event.isNormalized = true; + normalizedEvents.push(event); } else diff --git a/src/interaction/InteractionTrackingData.js b/src/interaction/InteractionTrackingData.js index 3ce38b6..6a1f69e 100644 --- a/src/interaction/InteractionTrackingData.js +++ b/src/interaction/InteractionTrackingData.js @@ -65,6 +65,17 @@ } /** + * Is the tracked event inactive (not over or down)? + * + * @member {number} + * @memberof PIXI.interaction.InteractionTrackingData# + */ + get none() + { + return this._flags === this.constructor.FLAGS.NONE; + } + + /** * Is the tracked event over the DisplayObject? * * @member {boolean} @@ -72,7 +83,7 @@ */ get over() { - return (this._flags | this.constructor.FLAGS.OVER) !== 0; + return (this._flags & this.constructor.FLAGS.OVER) !== 0; } /** @@ -93,7 +104,7 @@ */ get rightDown() { - return (this._flags | this.constructor.FLAGS.RIGHT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.RIGHT_DOWN) !== 0; } /** @@ -114,7 +125,7 @@ */ get leftDown() { - return (this._flags | this.constructor.FLAGS.LEFT_DOWN) !== 0; + return (this._flags & this.constructor.FLAGS.LEFT_DOWN) !== 0; } /** diff --git a/src/interaction/interactiveTarget.js b/src/interaction/interactiveTarget.js index ebfaacc..d252fba 100644 --- a/src/interaction/interactiveTarget.js +++ b/src/interaction/interactiveTarget.js @@ -38,32 +38,56 @@ hitArea: null, /** - * If enabled, the mouse cursor will change when hovered over the displayObject if it is interactive + * If enabled, the mouse cursor use the pointer behavior when hovered over the displayObject if it is interactive + * Setting this changes the 'cursor' property to `'pointer'`. * - * @inner {boolean} + * @member {boolean} + * @memberof PIXI.interaction.interactiveTarget# */ - buttonMode: false, + get buttonMode() + { + return this.cursor === 'pointer'; + }, + set buttonMode(value) + { + if (value) + { + this.cursor = 'pointer'; + } + else if (this.cursor === 'pointer') + { + this.cursor = null; + } + }, /** - * If buttonMode is enabled, this defines what CSS cursor property is used when the mouse cursor - * is hovered over the displayObject + * This defines what cursor mode is used when the mouse cursor + * is hovered over the displayObject. * * @see https://developer.mozilla.org/en/docs/Web/CSS/cursor * * @inner {string} */ - defaultCursor: 'pointer', + cursor: null, /** * Internal set of all active pointers, by identifier * - * @returns {Map} Map of all tracked pointers, by identifier + * @member {Map} + * @memberof PIXI.interaction.interactiveTarget# * @private */ - getTrackedPointers: function getTrackedPointers() + get trackedPointers() { if (this._trackedPointers === undefined) this._trackedPointers = {}; return this._trackedPointers; }, + + /** + * Map of all tracked pointers, by identifier. Use trackedPointers to access. + * + * @private {Map} + */ + _trackedPointers: undefined, }; diff --git a/test/interaction/InteractionManager.js b/test/interaction/InteractionManager.js index 8c7601b..9065b7f 100644 --- a/test/interaction/InteractionManager.js +++ b/test/interaction/InteractionManager.js @@ -4,6 +4,101 @@ describe('PIXI.interaction.InteractionManager', function () { + describe('event basics', function () + { + it('should call mousedown handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mousedown', eventSpy); + + pointer.mousedown(10, 10); + + expect(eventSpy).to.have.been.calledOnce; + }); + + it('should call mouseup handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseup', eventSpy); + + pointer.click(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseupoutside handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseupoutside', eventSpy); + + pointer.mousedown(10, 10); + pointer.mouseup(60, 60); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseover handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseover', eventSpy); + + pointer.mousemove(10, 10); + + expect(eventSpy).to.have.been.called; + }); + + it('should call mouseout handler', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const eventSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.on('mouseout', eventSpy); + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(eventSpy).to.have.been.called; + }); + }); + describe('onClick', function () { it('should call handler when inside', function () @@ -462,6 +557,150 @@ }); }); + describe('cursor changes', function () + { + it('cursor should be the cursor of interactive item', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default on mouseout', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('should still be the over cursor after a click', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.click(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('help'); + }); + + it('should return cursor to default when mouse leaves renderer', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = 'help'; + + pointer.mousemove(10, 10); + pointer.mousemove(-10, 60); + + expect(pointer.renderer.view.style.cursor).to.equal(pointer.interaction.cursorStyles.default); + }); + + it('cursor callback should be called', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const overSpy = sinon.spy(); + const defaultSpy = sinon.spy(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = overSpy; + pointer.interaction.cursorStyles.default = defaultSpy; + + pointer.mousemove(10, 10); + pointer.mousemove(60, 60); + + expect(overSpy).to.have.been.called; + expect(defaultSpy).to.have.been.called; + }); + + it('cursor style object should be fully applied', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'help'; + pointer.interaction.cursorStyles.help = { + cursor: 'none', + display: 'none', + }; + + pointer.mousemove(10, 10); + + expect(pointer.renderer.view.style.cursor).to.equal('none'); + expect(pointer.renderer.view.style.display).to.equal('none'); + }); + + it('should not change cursor style if no cursor style provided', function () + { + const stage = new PIXI.Container(); + const graphics = new PIXI.Graphics(); + const pointer = new MockPointer(stage); + + stage.addChild(graphics); + graphics.beginFill(0xFFFFFF); + graphics.drawRect(0, 0, 50, 50); + graphics.interactive = true; + graphics.cursor = 'pointer'; + pointer.interaction.cursorStyles.pointer = null; + pointer.interaction.cursorStyles.default = null; + + pointer.mousemove(10, 10); + expect(pointer.renderer.view.style.cursor).to.equal(''); + + pointer.mousemove(60, 60); + expect(pointer.renderer.view.style.cursor).to.equal(''); + }); + }); + describe('recursive hitTesting', function () { function getScene() diff --git a/test/interaction/MockPointer.js b/test/interaction/MockPointer.js index 7017656..bf22490 100644 --- a/test/interaction/MockPointer.js +++ b/test/interaction/MockPointer.js @@ -46,6 +46,45 @@ * @param {number} x - pointer x position * @param {number} y - pointer y position */ + mousemove(x, y) + { + const mouseEvent = new MouseEvent('mousemove', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + }); + + this.setPosition(x, y); + this.render(); + // mouseOverRenderer state should be correct, so mouse position to view rect + const rect = new PIXI.Rectangle(0, 0, this.renderer.width, this.renderer.height); + + if (rect.contains(x, y)) + { + if (!this.interaction.mouseOverRenderer) + { + this.interaction.onPointerOver(new MouseEvent('mouseover', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + })); + } + this.interaction.onPointerMove(mouseEvent); + } + else + { + this.interaction.onPointerOut(new MouseEvent('mouseout', { + clientX: x, + clientY: y, + preventDefault: sinon.stub(), + })); + } + } + + /** + * @param {number} x - pointer x position + * @param {number} y - pointer y position + */ click(x, y) { this.mousedown(x, y);