@makefully/platypus 4.1.19 → 4.1.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@makefully/platypus",
3
- "version": "4.1.19",
3
+ "version": "4.1.21",
4
4
  "description": "2D tile-based HTML5 game engine with a component-based entity model",
5
5
  "license": "MIT",
6
6
  "contributors": [
package/src/Component.js CHANGED
@@ -64,9 +64,15 @@ export default class Component {
64
64
  if (this.listener) {
65
65
  const
66
66
  publicMethods = this.publicMethods,
67
- keys = Object.keys(publicMethods);
67
+ keys = Object.keys(publicMethods),
68
+ // Detach first so re-entrant removeEventListener during teardown
69
+ // cannot read a recycled listener's deleted `events` array.
70
+ listener = this.listener,
71
+ {events, messages} = listener;
68
72
  let i = keys.length;
69
73
 
74
+ this.listener = null;
75
+
70
76
  // Handle component's destroy method before removing messaging and methods.
71
77
  if (this._destroy) {
72
78
  this._destroy();
@@ -77,12 +83,21 @@ export default class Component {
77
83
  this.removeMethod(keys[i]);
78
84
  }
79
85
  this.publicMethods.recycle();
80
-
81
- this.removeEventListeners();
82
- arrayCache.recycle(this.listener.events);
83
- arrayCache.recycle(this.listener.messages);
84
- this.listener.recycle();
85
- this.listener = null;
86
+
87
+ if (events && messages) {
88
+ const
89
+ {owner} = this,
90
+ {length} = events;
91
+
92
+ for (let j = 0; j < length; j++) {
93
+ owner.off(events[j], messages[j]);
94
+ }
95
+ events.length = 0;
96
+ messages.length = 0;
97
+ arrayCache.recycle(events);
98
+ arrayCache.recycle(messages);
99
+ }
100
+ listener.recycle();
86
101
  }
87
102
  }
88
103
 
@@ -96,7 +111,13 @@ export default class Component {
96
111
  removeEventListeners (listeners) {
97
112
  if (!listeners) {
98
113
  const
99
- {listener, owner} = this,
114
+ {listener, owner} = this;
115
+
116
+ if (!listener?.events || !listener.messages) {
117
+ return;
118
+ }
119
+
120
+ const
100
121
  {events, messages} = listener,
101
122
  {length} = events;
102
123
 
@@ -188,7 +209,13 @@ export default class Component {
188
209
  */
189
210
  removeEventListener (event, callback) {
190
211
  const
191
- {listener, owner} = this,
212
+ {listener, owner} = this;
213
+
214
+ if (!listener?.events || !listener.messages) {
215
+ return;
216
+ }
217
+
218
+ const
192
219
  {events, messages} = listener;
193
220
 
194
221
  for (let i = events.length - 1; i >= 0; i--) {
package/src/Game.js CHANGED
@@ -224,6 +224,9 @@ class Game extends Messenger {
224
224
  this.ticker.remove(this.tickInstance);
225
225
  this.paused = true;
226
226
  sound.pauseAll();
227
+ // Release held keys so pause does not leave sticky input,
228
+ // and so HandlerController can pass keyboard through to HTML.
229
+ this.triggerOnChildren('blur-control');
227
230
  }
228
231
  } else {
229
232
  if (this.paused) {
@@ -4,6 +4,43 @@ import Gamepad from '../Gamepad.js';
4
4
  import createComponentClass from '../factory.js';
5
5
 
6
6
  const
7
+ /**
8
+ * Whether the event target is (or is inside) an editable field. Used so
9
+ * capture-phase game key handlers do not call preventDefault / emit game
10
+ * input while the user types in HTML overlays (forms, Notefully, etc.).
11
+ *
12
+ * @param {Event} event
13
+ * @return {Boolean}
14
+ */
15
+ isEditableKeyboardTarget = function (event) {
16
+ const target = event?.target;
17
+
18
+ if (!target || typeof target.closest !== 'function') {
19
+ return false;
20
+ }
21
+
22
+ if (target.isContentEditable) {
23
+ return true;
24
+ }
25
+
26
+ const tag = target.tagName;
27
+
28
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') {
29
+ return true;
30
+ }
31
+
32
+ return !!target.closest('input, textarea, select, [contenteditable=""], [contenteditable="true"]');
33
+ },
34
+ /**
35
+ * When the game is paused or focus is in an editable field, let the browser
36
+ * handle keys (no game events, no preventDefault).
37
+ *
38
+ * @param {Event} event
39
+ * @return {Boolean}
40
+ */
41
+ shouldPassThroughKeyboard = function (event) {
42
+ return !!(platypus.game?.paused || isEditableKeyboardTarget(event));
43
+ },
7
44
  broadcastHandleController = function (tick) {
8
45
  if (hasGamepads) {
9
46
  const
@@ -170,25 +207,33 @@ export default createComponentClass(/** @lends platypus.components.HandlerContro
170
207
  */
171
208
  initialize: function () {
172
209
  const
173
- {enableDefaultEvents} = this;
210
+ {enableDefaultEvents} = this,
211
+ wrapKeyboard = (handler) => (event) => {
212
+ if (shouldPassThroughKeyboard(event)) {
213
+ return;
214
+ }
215
+
216
+ handler(event);
217
+ };
174
218
 
175
219
  this.upsToBlur = [];
176
220
 
177
221
  if (platypus.game.settings.debug || enableDefaultEvents === 'all') { // If this is a test build, leave in the browser key combinations so debug tools can be opened as expected.
178
- this.callbackKeyDown = onDown.bind(this, 'keyboard');
179
- this.callbackKeyUp = onUp.bind(this, 'keyboard');
222
+ this.callbackKeyDown = wrapKeyboard(onDown.bind(this, 'keyboard'));
223
+ this.callbackKeyUp = wrapKeyboard(onUp.bind(this, 'keyboard'));
180
224
  } else { // Otherwise remove default browser behavior for key inputs so that they do not interfere with game-play.
181
225
  const
182
226
  preventDefault = Array.isArray(enableDefaultEvents) ? (event) => enableDefaultEvents.indexOf(event.code) === -1 ? event.preventDefault() : null : (event) => event.preventDefault();
183
227
 
184
- this.callbackKeyDown = (event) => {
228
+ this.callbackKeyDown = wrapKeyboard((event) => {
185
229
  onDown.call(this, 'keyboard', event);
186
- preventDefault(event); // default 'none' may be too aggressive - if problems arise, we may need to limit this to certain key combos that get in the way of game-play. Example: (event.metaKey && event.keyCode == 37) causes an accidental cmd key press to send the browser back a page while playing and hitting the left arrow button.
187
- };
188
- this.callbackKeyUp = (event) => {
230
+ // Still aggressive for gameplay keys, but not while paused or typing in HTML.
231
+ preventDefault(event);
232
+ });
233
+ this.callbackKeyUp = wrapKeyboard((event) => {
189
234
  onUp.call(this, 'keyboard', event);
190
235
  preventDefault(event);
191
- };
236
+ });
192
237
  }
193
238
 
194
239
  window.addEventListener('keydown', this.callbackKeyDown, true);
@@ -256,8 +301,8 @@ export default createComponentClass(/** @lends platypus.components.HandlerContro
256
301
  },
257
302
  methods: {
258
303
  destroy: function () {
259
- window.removeEventListener('keydown', this.callbackKeyDown);
260
- window.removeEventListener('keyup', this.callbackKeyUp);
304
+ window.removeEventListener('keydown', this.callbackKeyDown, true);
305
+ window.removeEventListener('keyup', this.callbackKeyUp, true);
261
306
  for (let i = 0; i < this.gamepads.length; i++) {
262
307
  if (this.gamepads[i]) {
263
308
  this.gamepads[i].recycle();