Skip to content

Addon Events API and User Defined Functions access #7947

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 11 commits into
base: dev-2.0
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 38 additions & 0 deletions contributor_docs/creating_libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,31 @@ if (typeof p5 !== undefined) {

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.

## Accessing custom actions
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.

The following addon snippet listens to the `click` event on a custom button element.
```js
function myAddon(p5, fn, lifecycles){
lifecycles.presetup = function(){
let customButton = this.createButton('click me');
customButton.elt.addEventListener('click', this._customActions.myAddonButtonClicked);
};
}
```

In a sketch that uses the above addon, a user can define the following:
```js
function setup(){
createCanvas(400, 400);
}

function myAddonButtonClicked(){
// This function will be run each time the button created by the addon is clicked
}
```

This 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.

## Next steps

Expand Down Expand Up @@ -315,6 +340,19 @@ fn.myMethod = function(){

**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.

**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:
```js
function myAddon(p5, fn, lifecycles){
lifecycles.presetup = function(){
// ... Define `target` ...
target.addEventListener('click', function() { }, {
signal: this._removeSignal
});
};
}
```
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.

**Packaging**

**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.
Expand Down
2 changes: 1 addition & 1 deletion docs/parameterData.json
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@
"windowResized": {
"overloads": [
[
"UIEvent?"
"Event?"
]
]
},
Expand Down
22 changes: 17 additions & 5 deletions src/core/environment.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import * as C from './constants';
import { Vector } from '../math/p5.Vector';

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

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

lifecycles.presetup = function(){
const events = [
'resize'
];

for(const event of events){
window.addEventListener(event, this[`_on${event}`].bind(this), {
passive: false,
signal: this._removeSignal
});
}
};

/**
* Displays text in the web browser's console.
*
Expand Down Expand Up @@ -715,7 +728,7 @@ function environment(p5, fn){
* can be used for debugging or other purposes.
*
* @method windowResized
* @param {UIEvent} [event] optional resize Event.
* @param {Event} [event] optional resize Event.
* @example
* <div class="norender">
* <code>
Expand Down Expand Up @@ -770,10 +783,9 @@ function environment(p5, fn){
fn._onresize = function(e) {
this.windowWidth = getWindowWidth();
this.windowHeight = getWindowHeight();
const context = this._isGlobal ? window : this;
let executeDefault;
if (typeof context.windowResized === 'function') {
executeDefault = context.windowResized(e);
if (this._customActions.windowResized) {
executeDefault = this._customActions.windowResized(e);
if (executeDefault !== undefined && !executeDefault) {
e.preventDefault();
}
Expand Down
83 changes: 28 additions & 55 deletions src/core/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -64,37 +64,16 @@ class p5 {
this._startListener = null;
this._initializeInstanceVariables();
this._events = {
// keep track of user-events for unregistering later
pointerdown: null,
pointerup: null,
pointermove: null,
dragend: null,
dragover: null,
click: null,
dblclick: null,
mouseover: null,
mouseout: null,
keydown: null,
keyup: null,
keypress: null,
wheel: null,
resize: null,
blur: null
};
this._removeAbortController = new AbortController();
this._removeSignal = this._removeAbortController.signal;
this._millisStart = -1;
this._recording = false;

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

if (window.DeviceOrientationEvent) {
this._events.deviceorientation = null;
}
if (window.DeviceMotionEvent && !window._isNodeWebkit) {
this._events.devicemotion = null;
}

// ensure correct reporting of window dimensions
this._updateWindowSize();

Expand All @@ -120,7 +99,7 @@ class p5 {
console.log(`You just changed the value of "${property}", which was a p5 global value. This could cause problems later if you're not careful.`);
}
}
})
});
};
// If the user has created a global setup or draw function,
// assume "global" mode and make everything global (i.e. on the window)
Expand Down Expand Up @@ -156,16 +135,6 @@ class p5 {
p5._checkForUserDefinedFunctions(this);
}

// Bind events to window (not using container div bc key events don't work)
for (const e in this._events) {
const f = this[`_on${e}`];
if (f) {
const m = f.bind(this);
window.addEventListener(e, m, { passive: false });
this._events[e] = m;
}
}

const focusHandler = () => {
this.focused = true;
};
Expand Down Expand Up @@ -208,6 +177,20 @@ class p5 {
}
}

#customActions = {};
_customActions = new Proxy({}, {
get: (target, prop) => {
if(!this.#customActions[prop]){
const context = this._isGlobal ? window : this;
if(typeof context[prop] === 'function'){
this.#customActions[prop] = context[prop].bind(this);
}
}

return this.#customActions[prop];
}
});

async #_start() {
if (this.hitCriticalError) return;
// Find node if id given
Expand Down Expand Up @@ -247,18 +230,13 @@ class p5 {
}
if (this.hitCriticalError) return;

// unhide any hidden canvases that were created
const canvases = document.getElementsByTagName('canvas');

// Apply touchAction = 'none' to canvases if pointer events exist
if (Object.keys(this._events).some(event => event.startsWith('pointer'))) {
for (const k of canvases) {
k.style.touchAction = 'none';
}
}


for (const k of canvases) {
// Apply touchAction = 'none' to canvases to prevent scrolling
// when dragging on canvas elements
k.style.touchAction = 'none';

// unhide any hidden canvases that were created
if (k.dataset.hidden === 'true') {
k.style.visibility = '';
delete k.dataset.hidden;
Expand Down Expand Up @@ -380,19 +358,14 @@ class p5 {
window.cancelAnimationFrame(this._requestAnimId);
}

// unregister events sketch-wide
for (const ev in this._events) {
window.removeEventListener(ev, this._events[ev]);
}
// Send sketch remove signal
this._removeAbortController.abort();

// remove DOM elements created by p5, and listeners
// remove DOM elements created by p5
for (const e of this._elements) {
if (e.elt && e.elt.parentNode) {
e.elt.parentNode.removeChild(e.elt);
}
for (const elt_ev in e._events) {
e.elt.removeEventListener(elt_ev, e._events[elt_ev]);
}
}

// Run `remove` hooks
Expand Down Expand Up @@ -422,9 +395,9 @@ class p5 {
}

async _runLifecycleHook(hookName) {
for(const hook of p5.lifecycleHooks[hookName]){
await hook.call(this);
}
await Promise.all(p5.lifecycleHooks[hookName].map(hook => {
return hook.call(this);
}));
}

_initializeInstanceVariables() {
Expand Down
22 changes: 12 additions & 10 deletions src/dom/p5.Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -67,23 +67,22 @@ class Element {
// `_elements` array. But when an element lives inside an off-screen
// `p5.Graphics` layer, `this._pInst` is that wrapper Graphics object
// instead. The wrapper keeps a back–pointer (`_pInst`) to the real
// sketch but has no `_elements` array of its own.
let sketch = this._pInst;
// sketch but has no `_elements` array of its own.

let sketch = this._pInst;

// If `sketch` doesn’t own an `_elements` array it means
// we’re still at the graphics-layer “wrapper”.
// we’re still at the graphics-layer “wrapper”.
// Jump one level up to the real p5 sketch stored in sketch._pInst.

if (sketch && !sketch._elements && sketch._pInst) {
sketch = sketch._pInst; // climb one level up
sketch = sketch._pInst; // climb one level up
}

if (sketch && sketch._elements) { // only if the array exists
const i = sketch._elements.indexOf(this);
if (i !== -1) sketch._elements.splice(i, 1);
}


// deregister events
for (let ev in this._events) {
Expand Down Expand Up @@ -1865,7 +1864,7 @@ class Element {
return this;
}

/**
/**
* Calls a function when a file is dragged over the element.
*
* Calling `myElement.dragOver(false)` disables the function.
Expand Down Expand Up @@ -2416,7 +2415,10 @@ class Element {
Element._detachListener(ev, ctx);
}
const f = fxn.bind(ctx);
ctx.elt.addEventListener(ev, f, false);
ctx.elt.addEventListener(ev, f, {
capture: false,
signal: ctx._pInst._removeSignal
});
ctx._events[ev] = f;
}

Expand Down
Loading
Loading