Skip to content

Commit

Permalink
feat: add support for pascal-cased events
Browse files Browse the repository at this point in the history
  • Loading branch information
trezy committed Jun 17, 2024
1 parent ccd81d6 commit e0cf4a7
Show file tree
Hide file tree
Showing 8 changed files with 175 additions and 129 deletions.
36 changes: 36 additions & 0 deletions src/constants/EventPropNames.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// MouseEventHandler<HTMLCanvasElement>
export const EventPropNames = Object.freeze({
onClick: 'onclick',
onGlobalMouseMove: 'onglobalmousemove',
onGlobalPointerMove: 'onglobalpointermove',
onGlobalTouchMove: 'onglobaltouchmove',
onMouseDown: 'onmousedown',
onMouseEnter: 'onmouseenter',
onMouseLeave: 'onmouseleave',
onMouseMove: 'onmousemove',
onMouseOut: 'onmouseout',
onMouseOver: 'onmouseover',
onMouseUp: 'onmouseup',
onMouseUpOutside: 'onmouseupoutside',
onPointerCancel: 'onpointercancel',
onPointerDown: 'onpointerdown',
onPointerEnter: 'onpointerenter',
onPointerLeave: 'onpointerleave',
onPointerMove: 'onpointermove',
onPointerOut: 'onpointerout',
onPointerOver: 'onpointerover',
onPointerTap: 'onpointertap',
onPointerUp: 'onpointerup',
onPointerUpOutside: 'onpointerupoutside',
onRightClick: 'onrightclick',
onRightDown: 'onrightdown',
onRightUp: 'onrightup',
onRightUpOutside: 'onrightupoutside',
onTap: 'ontap',
onTouchCancel: 'ontouchcancel',
onTouchEnd: 'ontouchend',
onTouchEndOutside: 'ontouchendoutside',
onTouchMove: 'ontouchmove',
onTouchStart: 'ontouchstart',
onWheel: 'onwheel',
});
152 changes: 93 additions & 59 deletions src/helpers/applyProps.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,26 @@ import {
Container,
Graphics,
} from 'pixi.js';
import { EventPropNames } from '../constants/EventPropNames.js';
import { diffProps } from './diffProps.js';
import { isDiffSet } from './isDiffSet.js';
// import { pruneKeys } from './pruneKeys.js';
import { log } from './log.js';

/** @typedef {import('pixi.js').FederatedPointerEvent} FederatedPointerEvent */
/** @typedef {import('pixi.js').FederatedWheelEvent} FederatedWheelEvent */

/** @typedef {import('../typedefs/DiffSet.js').DiffSet} DiffSet */
/** @typedef {import('../typedefs/EventHandlers.js').EventHandlers} EventHandlers */
/** @typedef {import('../typedefs/Instance.js').Instance} Instance */
/** @typedef {import('../typedefs/InstanceProps.js').InstanceProps} InstanceProps */
/** @typedef {import('../typedefs/MaybeInstance.js').MaybeInstance} MaybeInstance */

const DEFAULT = '__default';
const DEFAULTS_CONTAINERS = new Map();
const EVENT_PROP_NAMES = Object.keys(EventPropNames);
const PIXI_EVENT_PROP_NAMES = Object.values(EventPropNames);

/** @type {Record<string, boolean>} */
const PIXI_EVENT_PROP_NAME_ERROR_HAS_BEEN_SHOWN = {};

/**
* Apply properties to Pixi.js instance.
Expand All @@ -37,9 +45,9 @@ export function applyProps(instance, data)
while (changeIndex < changes.length)
{
const change = changes[changeIndex];
let hasError = false;

/** @type {keyof Instance} */
let key = /** @type {*} */ (change[0]);
let key = change[0];
let value = change[1];
const isEvent = change[2];

Expand All @@ -48,88 +56,114 @@ export function applyProps(instance, data)

/** @type {Instance} */
let currentInstance = /** @type {*} */ (instance);
let targetProp = currentInstance[key];
let targetProp = /** @type {*} */ (currentInstance[key]);

if ((instance instanceof Graphics) && (key === 'draw') && (value instanceof Function))
if ((key === 'draw') && (typeof value === 'function'))
{
value(instance);
if (instance instanceof Graphics)
{
value(instance);
}
else
{
hasError = true;
log('warn', `The \`draw\` prop was used on a \`${instance.type}\` component, but it's only valid on \`graphics\` components.`);
}
}

// Resolve dashed props
if (keys.length)
{
targetProp = keys.reduce((accumulator, key) => accumulator[key], currentInstance);
const pixiEventPropNameIndex = PIXI_EVENT_PROP_NAMES.findIndex((propName) => propName === key);

// If the target is atomic, it forces us to switch the root
if (!(targetProp && targetProp.set))
if (pixiEventPropNameIndex !== -1)
{
hasError = true;
if (!PIXI_EVENT_PROP_NAME_ERROR_HAS_BEEN_SHOWN[key])
{
const [name, ...reverseEntries] = keys.reverse();
PIXI_EVENT_PROP_NAME_ERROR_HAS_BEEN_SHOWN[key] = true;

currentInstance = reverseEntries.reverse().reduce((accumulator, key) =>
accumulator[key], currentInstance);
const SUGGESTED_PROP_NAME = EVENT_PROP_NAMES[pixiEventPropNameIndex];

key = name;
log('warn', `Event names must be pascal case; instead of \`${key}\`, you probably want \`${SUGGESTED_PROP_NAME}\`.`);
}
}

// https://github.com/mrdoob/three.js/issues/21209
// HMR/fast-refresh relies on the ability to cancel out props, but threejs
// has no means to do this. Hence we curate a small collection of value-classes
// with their respective constructor/set arguments
// For removed props, try to set default values, if possible
if (value === `${DEFAULT}remove`)
if (!hasError)
{
if (currentInstance instanceof Container)
// Resolve dashed props
if (keys.length)
{
// create a blank slate of the instance and copy the particular parameter.
let ctor = DEFAULTS_CONTAINERS.get(currentInstance.constructor);
targetProp = keys.reduce((accumulator, key) => accumulator[key], currentInstance);

if (!ctor)
// If the target is atomic, it forces us to switch the root
if (!(targetProp && targetProp.set))
{
/** @type {Container} */
ctor = /** @type {*} */ (currentInstance.constructor);
const [name, ...reverseEntries] = keys.reverse();

// eslint-disable-next-line new-cap
ctor = new ctor();
currentInstance = reverseEntries.reverse().reduce((accumulator, key) =>
accumulator[key], currentInstance);

DEFAULTS_CONTAINERS.set(currentInstance.constructor, ctor);
key = name;
}

value = ctor[key];
}
else

// https://github.com/mrdoob/three.js/issues/21209
// HMR/fast-refresh relies on the ability to cancel out props, but pixi.js
// has no means to do this. Hence we curate a small collection of value-classes
// with their respective constructor/set arguments
// For removed props, try to set default values, if possible
if (value === `${DEFAULT}remove`)
{
// instance does not have constructor, just set it to 0
value = 0;
}
}
if (currentInstance instanceof Container)
{
// create a blank slate of the instance and copy the particular parameter.
let ctor = DEFAULTS_CONTAINERS.get(currentInstance.constructor);

// Deal with pointer events ...
if (isEvent && localState)
{
/** @type {keyof EventHandlers} */
const typedKey = /** @type {*} */ (key);
if (!ctor)
{
/** @type {Container} */
ctor = /** @type {*} */ (currentInstance.constructor);

if (value)
{
localState.handlers[typedKey] = /** @type {*} */ (value);
// eslint-disable-next-line new-cap
ctor = new ctor();

DEFAULTS_CONTAINERS.set(currentInstance.constructor, ctor);
}

value = ctor[key];
}
else
{
// instance does not have constructor, just set it to 0
value = 0;
}
}
else

// Deal with events ...
if (isEvent && localState)
{
delete localState.handlers[typedKey];
}
/** @type {keyof EventPropNames} */
const typedKey = /** @type {*} */ (key);

localState.eventCount = Object.keys(localState.handlers).length;
}
else
{
const prototype = Object.getPrototypeOf(currentInstance);
const propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, key);
const pixiKey = EventPropNames[typedKey];

if (typeof propertyDescriptor === 'undefined' || propertyDescriptor.set)
if (value)
{
currentInstance[pixiKey] = /** @type {(event: FederatedPointerEvent | FederatedWheelEvent) => void} */ (value);
}
else
{
delete currentInstance[pixiKey];
}
}
else
{
// @ts-expect-error The key is cast to any property of Container, including read-only properties. The check above prevents us from setting read-only properties, but TS doesn't understand it. 🤷🏻‍♂️
currentInstance[key] = value;
const prototype = Object.getPrototypeOf(currentInstance);
const propertyDescriptor = Object.getOwnPropertyDescriptor(prototype, key);

if (typeof propertyDescriptor === 'undefined' || propertyDescriptor.set)
{
// @ts-expect-error The key is cast to any property of Container, including read-only properties. The check above prevents us from setting read-only properties, but TS doesn't understand it. 🤷🏻‍♂️
currentInstance[key] = value;
}
}
}

Expand Down
10 changes: 8 additions & 2 deletions src/helpers/diffProps.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { EventPropNames } from '../constants/EventPropNames.js';
import { isEqual } from './compare.js';
import { gentleCloneProps } from './gentleCloneProps.js';

Expand Down Expand Up @@ -57,8 +58,13 @@ export function diffProps(
return;
}

// // Collect handlers and bail out
// if (/^on(Pointer|Click|DoubleClick|ContextMenu|Wheel)/.test(key)) return changes.push([key, value, true, []])
// Collect handlers and bail out
if (key in EventPropNames)
{
changes.push([key, value, true, []]);

return;
}

// Split dashed props
/** @type {string[]} */
Expand Down
53 changes: 28 additions & 25 deletions src/helpers/getCurrentEventPriority.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,40 @@
import {
ContinuousEventPriority,
DefaultEventPriority,
// DiscreteEventPriority,
// ContinuousEventPriority,
DiscreteEventPriority,
} from 'react-reconciler/constants.js';
import { log } from './log.js';

export function getCurrentEventPriority()
{
log('info', 'lifecycle::getCurrentEventPriority');

return DefaultEventPriority;
// if (typeof window === 'undefined') {
// return DefaultEventPriority;
// }
const globalScope = (typeof self !== 'undefined' && self) || (typeof window !== 'undefined' && window);

// const name = window?.event?.type;
if (!globalScope)
{
return DefaultEventPriority;
}

// switch (name) {
// case 'click':
// case 'contextmenu':
// case 'dblclick':
// case 'pointercancel':
// case 'pointerdown':
// case 'pointerup':
// return DiscreteEventPriority;
// case 'pointermove':
// case 'pointerout':
// case 'pointerover':
// case 'pointerenter':
// case 'pointerleave':
// case 'wheel':
// return ContinuousEventPriority;
// default:
// return DefaultEventPriority;
// }
const name = globalScope.event?.type;

switch (name)
{
case 'click':
case 'contextmenu':
case 'dblclick':
case 'pointercancel':
case 'pointerdown':
case 'pointerup':
return DiscreteEventPriority;
case 'pointermove':
case 'pointerout':
case 'pointerover':
case 'pointerenter':
case 'pointerleave':
case 'wheel':
return ContinuousEventPriority;
default:
return DefaultEventPriority;
}
}
2 changes: 0 additions & 2 deletions src/helpers/prepareInstance.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,6 @@ export function prepareInstance(component, state = {})
const instance = /** @type {*} */ (component);

instance.__pixireact = {
eventCount: 0,
handlers: {},
parent: null,
/** @type {Instance} */
root: /** @type {*} */ (null),
Expand Down
37 changes: 0 additions & 37 deletions src/typedefs/EventHandlers.js

This file was deleted.

Loading

0 comments on commit e0cf4a7

Please sign in to comment.