scrawl-canvas
Version:
Responsive, interactive and more accessible HTML5 canvas elements. Scrawl-canvas is a JavaScript library designed to make using the HTML5 canvas element easier, and more fun
324 lines (242 loc) • 10.8 kB
JavaScript
// # Observe and update
// Okay, so I got very bored of writing boilerplate to react to various form elements user interactions across the demos. So I wrote some functions to setup (and take down) batches of DOM event listeners to make my life easier. These are:
// + scrawl.addNativeListener()
// + scrawl.removeNativeListener()
//
// Then there was the use case for reacting to various mouse (and touch) events, so I bundled all those up into a set of complementary functions:
// + scrawl.addListener()
// + scrawl.removeListener()
//
// Even so, there was still a lot of boilerplate code to write, in particular to listening for user interaction with form elements (which can be anywhere on the web page). So I further factorised that code into an __observeAndUpdate__ function which uses the listener functions internally.
//
// I have no idea how useful the observeAndUpdate function will be to anyone else. All I know is that it works for me and my demos, and that makes me happy.
//
// Note: observeAndUpdate() returns a function that will (in theory) remove all the bundled event listeners from their DOM elements when it is invoked. Not yet tested.
// `Exported function` (to modules and the scrawl object). Capture changes in a form and apply them to a target Scrawl-canvas artefact, asset, style, group, etc object
//
// __observeAndUpdate - example__
// ```
// scrawl.observeAndUpdate({
//
// // The input event which will trigger the O&U
// event: ['input', 'change'],
//
// // CSS selector used to gather the DOM elements this O&U will be attached to
// origin: '.controlItem',
//
// // The SC artefact, or Group object, to be updated by user interaction changes
// target: piccy,
//
// // Performs an `addNativeListener` when true; `addListener` when false
// useNativeListener: true,
//
// // Invokes the event listener's `preventDefault` function
// preventDefault: true,
//
// // The SC artefact attributes to be updated by various DOM inputs. See below
// updates: {
//
// copy_start_xPercent: ['copyStartX', '%'],
// copy_start_xAbsolute: ['copyStartX', 'round'],
//
// copy_start_yPercent: ['copyStartY', '%'],
// copy_start_yAbsolute: ['copyStartY', 'round'],
//
// copy_dims_widthPercent: ['copyWidth', '%'],
// copy_dims_widthAbsolute: ['copyWidth', 'round'],
//
// copy_dims_heightPercent: ['copyHeight', '%'],
// copy_dims_heightAbsolute: ['copyHeight', 'round'],
// },
//
// // A callback function to be performed after any attributes updates
// callback: () => myLoom.update(),
//
// // Similarly, we can invoke a function to run before performing the updates
// setup: () => myLoom.update(),
// });
// ```
// #### Imports
import * as library from "../core/library.js";
import { isa_fn, xt, xta, λnull, Ωempty } from "../helper/utilities.js";
import { addListener, addNativeListener, removeListener, removeNativeListener } from "../core/events.js";
// Shared constants
import { _ceil, _floor, _round, T_GROUP, TRUE } from '../helper/shared-vars.js';
// Local constants
const FALSE = 'false';
// __observeAndUpdate__ - exported function
export const observeAndUpdate = function (items = Ωempty) {
if (!xta(items.event, items.origin, items.updates)) return false;
const target = (items.target.substring && items.targetLibrarySection) ?
library[items.targetLibrarySection][items.target] :
items.target;
if (!target) return false;
const event = items.event,
origin = items.origin;
const listener = (items.useNativeListener) ? addNativeListener : addListener,
killListener = (items.useNativeListener) ? removeNativeListener : removeListener;
let stop = λnull;
if (items.preventDefault) {
stop = (e) => {
e.preventDefault();
e.returnValue = false;
};
}
const setup = (isa_fn(items.setup)) ? items.setup : λnull;
const callback = (isa_fn(items.callback)) ? items.callback : λnull;
const func = function (e) {
stop(e);
const id = (e && e.target) ? e.target.id : false;
if (id) {
const updates = items.updates,
actionArray = updates[id];
if (actionArray) {
setup();
const actionAttribute = actionArray[0],
action = actionArray[1],
targetVal = e.target.value;
let actionFlag = true,
val;
switch (action) {
// Supplied value converted to float Number
case 'float' :
val = parseFloat(targetVal);
break;
// Supplied value converted to integer Number
case 'int' :
val = parseInt(targetVal, 10);
break;
// Supplied value rounded up or down to nearest integer Number
case 'round' :
val = _round(targetVal);
break;
// Supplied value rounded down to nearest integer Number
case 'roundDown' :
val = _floor(targetVal);
break;
// Supplied value rounded up to nearest integer Number
case 'roundUp' :
val = _ceil(targetVal);
break;
// Supplied value not modified in any way
case 'raw' :
val = targetVal;
break;
// Supplied value not modified in any way
case 'parse' :
val = (targetVal.substring) ? JSON.parse(targetVal) : targetVal;
break;
// Supplied value converted to String
case 'string' :
val = `${targetVal}`;
break;
// Supplied value converted to Boolean
case 'boolean' :
if (xt(targetVal)) {
if (targetVal.substring) {
if (TRUE === targetVal.toLowerCase()) val = true;
else if (FALSE === targetVal.toLowerCase()) val = false;
else val = (parseFloat(targetVal)) ? true : false;
}
else val = (targetVal) ? true : false;
}
else val = false;
break;
// Supplied value converted to float number, then action value (eg: '%') added to result to output a String
default :
if (action.substring) val = `${parseFloat(targetVal)}${action}`;
else actionFlag = false;
}
// Update - we can apply updates to a Group of artefacts, or to a single artefact
if (actionFlag) {
if (target.type === T_GROUP) {
target.setArtefacts({
[actionAttribute]: val
});
}
else {
target.set({
[actionAttribute]: val
});
}
// Invoke any supplied callback function
callback(e);
}
}
}
};
const kill = function () {
killListener(event, func, origin);
};
listener(event, func, origin);
return kill;
};
export const makeUpdater = (items = Ωempty) => observeAndUpdate(items);
// __initializeDomInputs__ - exported helper function
// + Argument is an Array of arrays, where each element Array holds the following data:
// + `[kind: String, selector: String, value: String | number]`, where
// + `kind` defines the kind of element to be processed - `input`, `select`, `button`, `element`
// + `selector` - for `input`, `select` and `button` elements, that element's unique `id` value; for anything else, a valid CSS query selector string
// + `value` - the initial value to set the input to - for `select` this will be a number
// + Returns an object of elements keyed to the selector string
export const initializeDomInputs = (items) => {
const results = {};
items.forEach(item => {
const [kind, selector, value] = item;
switch (kind) {
case 'input' : {
if (value.substring) {
/** @type {HTMLInputElement} */
const S = document.querySelector(`#${selector}`);
if (S) {
S.value = value;
results[selector] = S;
}
else results[selector] = {};
}
else results[selector] = {};
break;
}
case 'select' : {
if (value.toFixed) {
/** @type {HTMLSelectElement} */
const S = document.querySelector(`#${selector}`);
if (S) {
S.options.selectedIndex = value;
results[selector] = S;
}
else results[selector] = {};
}
else results[selector] = {};
break;
}
case 'button' : {
if (value.substring) {
/** @type {HTMLButtonElement} */
const S = document.querySelector(`#${selector}`);
if (S) {
S.textContent = value;
results[selector] = S;
}
else results[selector] = {};
}
else results[selector] = {};
break;
}
case 'element' : {
/** @type {HTMLElement} */
const S = document.querySelector(`${selector}`);
if (S) results[selector] = S;
else results[selector] = {};
break;
}
default : {
/** @type {HTMLElement} */
const S = document.querySelector(`#${selector}`);
if (S) results[selector] = S;
else results[selector] = {};
}
}
});
return results;
};