Skip to content

Commit 5787faf

Browse files
authored
Merge pull request #7947 from limzykenneth/events-api
Addon Events API and User Defined Functions access
2 parents be953ca + 3f16918 commit 5787faf

File tree

8 files changed

+190
-113
lines changed

8 files changed

+190
-113
lines changed

contributor_docs/creating_libraries.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,33 @@ if (typeof p5 !== undefined) {
282282

283283
In the above snippet, an additional `if` condition is added around the call to `p5.registerAddon()`. This is done to support both direct usage in ESM modules (where users can directly import your addon function then call `p5.registerAddon()` themselves) and after bundling support regular `<script>` tag usage without your users needing to call `p5.registerAddon()` directly as long as they have included the addon `<script>` tag after the `<script>` tag including p5.js itself.
284284

285+
## Accessing custom actions
286+
In certain circumstances, such as when you have a library that listens to a certain browser event, you may wish to run a function that your user defined on the global scope, much like how a `click` event triggers a user defined `mouseClicked()` function. We call these functions "custom actions" and your addon can access any of them through `this._customActions` object.
287+
288+
The following addon snippet listens to the `click` event on a custom button element.
289+
```js
290+
function myAddon(p5, fn, lifecycles){
291+
lifecycles.presetup = function(){
292+
let customButton = this.createButton('click me');
293+
customButton.elt.addEventListener('click', this._customActions.myAddonButtonClicked);
294+
};
295+
}
296+
```
297+
298+
In a sketch that uses the above addon, a user can define the following:
299+
```js
300+
function setup(){
301+
createCanvas(400, 400);
302+
}
303+
304+
function myAddonButtonClicked(){
305+
// This function will be run each time the button created by the addon is clicked
306+
}
307+
```
308+
309+
Please note that in the above example, if the user does not define `function myAddonButtonClicked()` in their code, `this._customActions.myAddonButtonClicked` will return `undefined`. This means that if you are planning to call the custom action function directly in your code, you should include an `if` statement check to make sure that `this._customActions.myAddonButtonClicked` is defined.
310+
311+
Overall, this custom actions approach supports accessing the custom action functions in both global mode and instance mode with the same code, simplifying your code from what it otherwise may need to be.
285312

286313
## Next steps
287314

@@ -315,6 +342,19 @@ fn.myMethod = function(){
315342

316343
**p5.js library filenames are also prefixed with p5, but the next word is lowercase** to distinguish them from classes. For example, p5.sound.js. You are encouraged to follow this format for naming your file.
317344

345+
**In some cases, you will need to make sure your addon cleans up after itself after a p5.js sketch is removed** by the user calling `remove()`. This means adding relevant clean up code in the `lifecycles.remove` hook. In most circumstances, you don't need to do this with the main exception being cleaning up event handlers: if you are using event handlers (ie. calling `addEventListeners`), you will need to make sure those event handlers are also removed when a sketch is removed. p5.js provides a handy method to automatically remove any registered event handlers with and internal property `this._removeSignal`. When registering an event handler, include `this._removeSignal` as follow:
346+
```js
347+
function myAddon(p5, fn, lifecycles){
348+
lifecycles.presetup = function(){
349+
// ... Define `target` ...
350+
target.addEventListener('click', function() { }, {
351+
signal: this._removeSignal
352+
});
353+
};
354+
}
355+
```
356+
With this you will not need to manually define a clean up actions for event handlers in `lifecycles.remove` and all event handlers associated with the `this._removeSignal` property as above will be automtically cleaned up on sketch removal.
357+
318358
**Packaging**
319359

320360
**Create a single JS file that contains your library.** This makes it easy for users to add it to their projects. We suggest using a [bundler](https://rollupjs.org/) for your library. You may want to provide options for both the normal JavaScript file for sketching/debugging and a [minified](https://terser.org/) version for faster loading.

docs/parameterData.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -325,7 +325,7 @@
325325
"windowResized": {
326326
"overloads": [
327327
[
328-
"UIEvent?"
328+
"Event?"
329329
]
330330
]
331331
},

src/core/environment.js

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
import * as C from './constants';
1010
// import { Vector } from '../math/p5.Vector';
1111

12-
function environment(p5, fn){
12+
function environment(p5, fn, lifecycles){
1313
const standardCursors = [C.ARROW, C.CROSS, C.HAND, C.MOVE, C.TEXT, C.WAIT];
1414

1515
fn._frameRate = 0;
@@ -19,6 +19,19 @@ function environment(p5, fn){
1919
const _windowPrint = window.print;
2020
let windowPrintDisabled = false;
2121

22+
lifecycles.presetup = function(){
23+
const events = [
24+
'resize'
25+
];
26+
27+
for(const event of events){
28+
window.addEventListener(event, this[`_on${event}`].bind(this), {
29+
passive: false,
30+
signal: this._removeSignal
31+
});
32+
}
33+
};
34+
2235
/**
2336
* Displays text in the web browser's console.
2437
*
@@ -715,7 +728,7 @@ function environment(p5, fn){
715728
* can be used for debugging or other purposes.
716729
*
717730
* @method windowResized
718-
* @param {UIEvent} [event] optional resize Event.
731+
* @param {Event} [event] optional resize Event.
719732
* @example
720733
* <div class="norender">
721734
* <code>
@@ -770,10 +783,9 @@ function environment(p5, fn){
770783
fn._onresize = function(e) {
771784
this.windowWidth = getWindowWidth();
772785
this.windowHeight = getWindowHeight();
773-
const context = this._isGlobal ? window : this;
774786
let executeDefault;
775-
if (typeof context.windowResized === 'function') {
776-
executeDefault = context.windowResized(e);
787+
if (typeof this._customActions.windowResized === 'function') {
788+
executeDefault = this._customActions.windowResized(e);
777789
if (executeDefault !== undefined && !executeDefault) {
778790
e.preventDefault();
779791
}

src/core/main.js

Lines changed: 27 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -64,37 +64,16 @@ class p5 {
6464
this._startListener = null;
6565
this._initializeInstanceVariables();
6666
this._events = {
67-
// keep track of user-events for unregistering later
68-
pointerdown: null,
69-
pointerup: null,
70-
pointermove: null,
71-
dragend: null,
72-
dragover: null,
73-
click: null,
74-
dblclick: null,
75-
mouseover: null,
76-
mouseout: null,
77-
keydown: null,
78-
keyup: null,
79-
keypress: null,
80-
wheel: null,
81-
resize: null,
82-
blur: null
8367
};
68+
this._removeAbortController = new AbortController();
69+
this._removeSignal = this._removeAbortController.signal;
8470
this._millisStart = -1;
8571
this._recording = false;
8672

8773
// States used in the custom random generators
8874
this._lcg_random_state = null; // NOTE: move to random.js
8975
this._gaussian_previous = false; // NOTE: move to random.js
9076

91-
if (window.DeviceOrientationEvent) {
92-
this._events.deviceorientation = null;
93-
}
94-
if (window.DeviceMotionEvent && !window._isNodeWebkit) {
95-
this._events.devicemotion = null;
96-
}
97-
9877
// ensure correct reporting of window dimensions
9978
this._updateWindowSize();
10079

@@ -156,16 +135,6 @@ class p5 {
156135
p5._checkForUserDefinedFunctions(this);
157136
}
158137

159-
// Bind events to window (not using container div bc key events don't work)
160-
for (const e in this._events) {
161-
const f = this[`_on${e}`];
162-
if (f) {
163-
const m = f.bind(this);
164-
window.addEventListener(e, m, { passive: false });
165-
this._events[e] = m;
166-
}
167-
}
168-
169138
const focusHandler = () => {
170139
this.focused = true;
171140
};
@@ -208,6 +177,20 @@ class p5 {
208177
}
209178
}
210179

180+
#customActions = {};
181+
_customActions = new Proxy({}, {
182+
get: (target, prop) => {
183+
if(!this.#customActions[prop]){
184+
const context = this._isGlobal ? window : this;
185+
if(typeof context[prop] === 'function'){
186+
this.#customActions[prop] = context[prop].bind(this);
187+
}
188+
}
189+
190+
return this.#customActions[prop];
191+
}
192+
});
193+
211194
async #_start() {
212195
if (this.hitCriticalError) return;
213196
// Find node if id given
@@ -248,18 +231,13 @@ class p5 {
248231
}
249232
if (this.hitCriticalError) return;
250233

251-
// unhide any hidden canvases that were created
252234
const canvases = document.getElementsByTagName('canvas');
253-
254-
// Apply touchAction = 'none' to canvases if pointer events exist
255-
if (Object.keys(this._events).some(event => event.startsWith('pointer'))) {
256-
for (const k of canvases) {
257-
k.style.touchAction = 'none';
258-
}
259-
}
260-
261-
262235
for (const k of canvases) {
236+
// Apply touchAction = 'none' to canvases to prevent scrolling
237+
// when dragging on canvas elements
238+
k.style.touchAction = 'none';
239+
240+
// unhide any hidden canvases that were created
263241
if (k.dataset.hidden === 'true') {
264242
k.style.visibility = '';
265243
delete k.dataset.hidden;
@@ -385,19 +363,14 @@ class p5 {
385363
window.cancelAnimationFrame(this._requestAnimId);
386364
}
387365

388-
// unregister events sketch-wide
389-
for (const ev in this._events) {
390-
window.removeEventListener(ev, this._events[ev]);
391-
}
366+
// Send sketch remove signal
367+
this._removeAbortController.abort();
392368

393-
// remove DOM elements created by p5, and listeners
369+
// remove DOM elements created by p5
394370
for (const e of this._elements) {
395371
if (e.elt && e.elt.parentNode) {
396372
e.elt.parentNode.removeChild(e.elt);
397373
}
398-
for (const elt_ev in e._events) {
399-
e.elt.removeEventListener(elt_ev, e._events[elt_ev]);
400-
}
401374
}
402375

403376
// Run `remove` hooks
@@ -427,9 +400,9 @@ class p5 {
427400
}
428401

429402
async _runLifecycleHook(hookName) {
430-
for(const hook of p5.lifecycleHooks[hookName]){
431-
await hook.call(this);
432-
}
403+
await Promise.all(p5.lifecycleHooks[hookName].map(hook => {
404+
return hook.call(this);
405+
}));
433406
}
434407

435408
_initializeInstanceVariables() {

src/dom/p5.Element.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2424,7 +2424,10 @@ class Element {
24242424
Element._detachListener(ev, ctx);
24252425
}
24262426
const f = fxn.bind(ctx);
2427-
ctx.elt.addEventListener(ev, f, false);
2427+
ctx.elt.addEventListener(ev, f, {
2428+
capture: false,
2429+
signal: ctx._pInst._removeSignal
2430+
});
24282431
ctx._events[ev] = f;
24292432
}
24302433

0 commit comments

Comments
 (0)