Skip to content
Merged
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
40 changes: 40 additions & 0 deletions contributor_docs/creating_libraries.md
Original file line number Diff line number Diff line change
Expand Up @@ -282,6 +282,33 @@ 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);
Comment thread
limzykenneth marked this conversation as resolved.
};
}
```

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
}
```

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.

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.

## Next steps

Expand Down Expand Up @@ -315,6 +342,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 (typeof this._customActions.windowResized === 'function') {
executeDefault = this._customActions.windowResized(e);
if (executeDefault !== undefined && !executeDefault) {
e.preventDefault();
}
Expand Down
81 changes: 27 additions & 54 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 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 @@ -248,18 +231,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 @@ -385,19 +363,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 @@ -427,9 +400,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
5 changes: 4 additions & 1 deletion src/dom/p5.Element.js
Original file line number Diff line number Diff line change
Expand Up @@ -2424,7 +2424,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