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
637 lines (458 loc) • 20 kB
JavaScript
// # Stack factory
// The Scrawl-canvas Stack/Element system is an attempt to supplement DOM elements with Scrawl-canvas entity positioning and dimensioning functionality.
// #### Imports
import { artefact, constructors, group, purge, stack } from '../core/library.js';
import { addStrings, doCreate, generateUniqueString, isa_canvas, isa_dom, mergeOver, removeItem, xt, λnull, λcloneError, Ωempty } from '../helper/utilities.js';
import { domShow } from '../core/document.js';
import { rootElementsAdd, rootElementsIncludes, rootElementsRemove } from "../helper/document-root-elements.js";
import { uiSubscribedElements } from '../core/user-interaction.js';
import { makeGroup } from './group.js';
import { makeElement } from './element.js';
import baseMix from '../mixin/base.js';
import cascadeMix from '../mixin/cascade.js';
import domMix from '../mixin/dom.js';
import displayMix from '../mixin/display-shape.js';
// Shared constants
import { _computed, _isArray, _values, ABSOLUTE, BORDER_BOX, DATA_SCRAWL_GROUP, DIV, NAME, PC50, RELATIVE, ROOT, SUBSCRIBE, T_STACK, ZERO_STR } from '../helper/shared-vars.js';
// Local constants
const $DATA_SCRAWL_STACK = '[data-scrawl-stack]',
$SCRIPT = 'SCRIPT',
DATA_SCRAWL_STACK = 'data-scrawl-stack',
PERMITTED_STACK_ELEMENTS = ['ARTICLE', 'ASIDE', 'DIV', 'FOOTER', 'HEADER', 'MAIN', 'NAV', 'SECTION'],
STACK = 'stack';
// #### Stack constructor
const Stack = function (items = Ωempty) {
if (items.domElement && PERMITTED_STACK_ELEMENTS.includes(items.domElement.tagName)) {
this.makeName(items.name);
this.register();
this.initializePositions();
this.initializeCascade();
this.dimensions[0] = 300;
this.dimensions[1] = 150;
this.pathCorners = [];
this.css = {};
this.here = {};
this.perspective = {
x: PC50,
y: PC50,
z: 0
};
this.dirtyPerspective = true;
this.initializeDomLayout(items);
const g = makeGroup({
name: this.name,
host: this.name
});
this.addGroups(g.name);
this.set(this.defs);
this.initializeDisplayShapeActions();
this.initializeAccessibility();
this.dirtyDomDimensions = true;
this.dirtyPath = true;
this.rotation = null;
this.currentCornersData = null;
this.currentTransformString = '';
this.dirtyTransform = true;
this.currentTransformOriginString = '';
this.dirtyTransformOrigin = true;
this.domShowRequired = true;
this.dirtyCss = true;
this.dirtyStampOrder = true;
this.localMouseListener = null;
this.currentParentDimensions = [];
this.currentParentStyles = null;
this.set(items);
const el = this.domElement;
if (el) {
const ds = el.dataset;
if (ds.isResponsive) {
this.isResponsive = true;
this.checkForResize = true;
this.currentParentStyles = _computed(el.parentElement);
this.currentParentDimensions.push(0, 0);
}
if (el.getAttribute(DATA_SCRAWL_GROUP) === ROOT) rootElementsAdd(this.name);
}
return this;
}
return null;
};
// #### Stack prototype
const P = Stack.prototype = doCreate();
P.type = T_STACK;
P.lib = STACK;
P.isArtefact = true;
P.isAsset = false;
// #### Mixins
baseMix(P);
cascadeMix(P);
domMix(P);
displayMix(P);
// #### Stack attributes
const defaultAttributes = {
// __position__, __perspective__ - while most of the Stack wrapper's DOM element's attributes are handled through CSS, Scrawl-canvas takes control of some positioning-related attributes. Most of these are defined in the [dom mixin](../mixin/dom.html) - but the position and perspective attributes are managed in this module
position: RELATIVE,
perspective: null,
// __trackHere__
trackHere: SUBSCRIBE,
// TODO: This is all about a mad idea we may have for making stacks 'responsive' to viewport changes. It needs a lot more thinking through. Search on 'isResponsive' to find the relevant function below
isResponsive: false,
containElementsInHeight: false,
};
P.defs = mergeOver(P.defs, defaultAttributes);
// #### Packet/Clone management
// This functionality is disabled for Stack objects
P.stringifyFunction = λnull;
P.processPacketOut = λnull;
P.finalizePacketOut = λnull;
P.saveAsPacket = function () {
return `[${this.name}, ${this.type}, ${this.lib}, {}]`
};
P.clone = λcloneError;
// #### Kill functionality
P.factoryKill = function (removeDomArtefact = true) {
const myname = this.name;
// rootElements and uiSubscribedElements arrays
rootElementsRemove(myname);
removeItem(uiSubscribedElements, myname);
// Groups
if (group[myname]) group[myname].kill();
_values(artefact).forEach(art => {
if (art.host === myname) art.kill();
});
// DOM removals
if (removeDomArtefact) this.domElement.remove();
}
// #### Get, Set, deltaSet
const G = P.getters,
S = P.setters,
D = P.deltaSetters;
// Similar to start, handle, dimensions, etc Scrawl-canvas supplies some pseudo-attributes to help set and manage the Stack wrapper's `perspective` object:
// + `perspectiveX`
// + `perspectiveY`
// + `perspectiveZ`
G.perspectiveX = function () {
return this.perspective.x;
};
G.perspectiveY = function () {
return this.perspective.y;
};
G.perspectiveZ = function () {
return this.perspective.z;
};
S.perspectiveX = function (item) {
this.perspective.x = item;
this.dirtyPerspective = true;
};
S.perspectiveY = function (item) {
this.perspective.y = item;
this.dirtyPerspective = true;
};
S.perspectiveZ = function (item) {
this.perspective.z = item;
this.dirtyPerspective = true;
};
S.perspective = function (item = Ωempty) {
this.perspective.x = (xt(item.x)) ? item.x : this.perspective.x;
this.perspective.y = (xt(item.y)) ? item.y : this.perspective.y;
this.perspective.z = (xt(item.z)) ? item.z : this.perspective.z;
this.dirtyPerspective = true;
};
D.perspectiveX = function (item) {
this.perspective.x = addStrings(this.perspective.x, item);
this.dirtyPerspective = true;
};
D.perspectiveY = function (item) {
this.perspective.y = addStrings(this.perspective.y, item);
this.dirtyPerspective = true;
};
// `group` - get the Stack's namesake group
// + Note that Stack wrappers can action more than one group
G.group = function () {
return group[this.name];
};
// #### Prototype functions
// `updateArtefacts` - internal function. Iterate through all Element and Canvas wrappers associated with the Stack wrapper's Group object and set a range of dirty flags on them, for future processing by each as appropriate.
P.updateArtefacts = function (items = Ωempty) {
this.groupBuckets.forEach(grp => {
grp.artefactCalculateBuckets.forEach(art => {
if (items.dirtyScale) art.dirtyScale = true;
if (items.dirtyDimensions) art.dirtyDimensions = true;
if (items.dirtyLock) art.dirtyLock = true;
if (items.dirtyStart) art.dirtyStart = true;
if (items.dirtyOffset) art.dirtyOffset = true;
if (items.dirtyHandle) art.dirtyHandle = true;
if (items.dirtyRotation) art.dirtyRotation = true;
if (items.dirtyPathObject) art.dirtyPathObject = true;
})
});
};
P.reset = function () {
this.dirtyDimensions = true;
domShow(this.name);
};
// `cleanDimensionsAdditionalActions` - overwrites mixin/position function. Promulgates Stack dimension changes through to all Element and Canvas wrappers associated with the Stack wrapper's Group object.
P.cleanDimensionsAdditionalActions = function () {
if (this.groupBuckets) {
this.updateArtefacts({
dirtyDimensions: true,
dirtyPath: true,
dirtyStart: true,
dirtyHandle: true,
});
}
this.dirtyDomDimensions = true;
this.dirtyPath = true;
this.dirtyStart = true;
this.dirtyHandle = true;
this.dirtyDisplayShape = true;
this.dirtyDisplayArea = true;
};
// `cleanPerspective` - internal function
P.cleanPerspective = function () {
this.dirtyPerspective = false;
const p = this.perspective;
this.domPerspectiveString = `perspective-origin: ${p.x} ${p.y}; perspective: ${p.z}px;`;
this.domShowRequired = true;
if (this.groupBuckets) {
this.updateArtefacts({
dirtyHandle: true,
dirtyPathObject: true,
});
}
};
// Stack elements can be responsive
// - We assume that the stack element needs to be 100% the same size as its parent element
P.checkResponsive = function () {
const pDims = this.currentParentDimensions,
pStyles = this.currentParentStyles;
const [cw, ch] = this.currentParentDimensions;
const pw = parseFloat(pStyles.width),
ph = parseFloat(pStyles.height);
if (cw !== pw || ch !== ph) {
pDims[0] = pw;
pDims[1] = ph;
this.dirtyDomDimensions = true;
this.dirtyDisplayShape = true;
this.dirtyDisplayArea = true;
}
};
// ##### Display cycle functions
// `clear`
P.clear = function () {
if (this.isResponsive) this.checkResponsive();
};
// `compile`
P.compile = function () {
this.sortGroups();
this.prepareStamp()
this.stamp();
this.groupBuckets.forEach(mygroup => mygroup.stamp());
};
// `show`
P.show = function () {
domShow();
};
// `render`
P.render = function () {
this.clear();
this.compile();
this.show();
};
// `addExistingDomElements` - argument is a CSS query search String. All elements in the DOM matching the search will be __moved__ into the Stack wrapper's DOM element and given Scrawl-canvas Element wrappers. While Scrawl-canvas will try its best to respect the elements' CSS attributes, they will be __positioned absolutely__ within the Stack and given start, handle and offset values of `[0, 0]`.
P.addExistingDomElements = function (search) {
if (xt(search)) {
let el, captured, i, iz;
const elements = (search.substring) ? document.querySelectorAll(search) : [].concat(search);
for (i = 0, iz = elements.length; i < iz; i++) {
el = elements[i];
if (isa_dom(el)) {
captured = makeElement({
name: el.id || el.name,
domElement: el,
group: this.name,
host: this.name,
position: ABSOLUTE,
setInitialDimensions: true,
});
if (captured && captured.domElement) this.domElement.appendChild(captured.domElement);
}
}
}
return this;
};
// `addNewElement` - takes an 'items' object as an argument. The only required attribute of the argument is __tag__, which determines the type of element that will be created (for example - setting tag to 'div' will create a new <div> element)
// + Any other Element artefact attribute can also be included in the argument object, including __text__ and __content__ attributes to set the new DOM Element's textContent and innerHTML attributes.
// + If position and dimension values are not included in the argument, the element will be given default values of [0,0] for start, offset and handle; and dimensions of 100px width and height.
// + The new element will also default to a CSS box-sizing style value of 'border-box', unless the argument's __boxSizing__ attribute has been set to 'content-box' - this will override any 'borderBox' attribute value in the argument's __.css__ object (if one has been included)
P.addNewElement = function (items) {
if (items && items.tag) {
items.domElement = document.createElement(items.tag);
items.domElement.setAttribute(DATA_SCRAWL_GROUP, this.name);
if (!xt(items.group)) items.group = this.name;
items.host = this.name;
if (!items.position) items.position = ABSOLUTE;
if (items.dimensions && _isArray(items.dimensions)) {
items.width = items.dimensions[0] || 100;
items.height = items.dimensions[1] || 100;
}
if (!xt(items.width)) items.width = 100;
if (!xt(items.height)) items.height = 100;
const myElement = makeElement(items);
if (myElement && myElement.domElement) {
if (!xt(items.boxSizing)) myElement.domElement.style.boxSizing = BORDER_BOX;
this.domElement.appendChild(myElement.domElement);
}
return myElement;
}
return false;
};
// #### Factory
const makeStack = function (items) {
if (!items) return false;
return new Stack(items);
};
constructors.Stack = Stack;
// #### Stack discovery
// `Exported function` (to modules). Parse the DOM, looking for all elements that have been given a __data-scrawl-stack__ attribute; then create __Stack__ artefact wrappers for each of them.
//
// This function will also create wrappers for all __direct child elements__ (one level down) within the stack, and create appropriate wrappers (Stack, Canvas, Element) for them.
export const getStacks = function (query = $DATA_SCRAWL_STACK) {
document.querySelectorAll(query).forEach(el => addInitialStackElement(el));
};
// Create a __stack__ artefact wrapper for a given stack element.
const addInitialStackElement = function (el) {
let mygroup = el.getAttribute(DATA_SCRAWL_GROUP),
myname = el.id || el.getAttribute(NAME),
position = ABSOLUTE;
if (!mygroup) {
el.setAttribute(DATA_SCRAWL_GROUP, ROOT);
mygroup = ROOT;
position = RELATIVE;
}
if (!myname) {
myname = generateUniqueString();
el.id = myname;
}
const mystack = makeStack({
name: myname,
domElement: el,
group: mygroup,
host: mygroup,
position: position,
setInitialDimensions: true,
});
processNewStackChildren(el, myname);
return mystack;
};
// Helper function for addInitialStackElement()
const processNewStackChildren = function (el, name) {
const hostDims = el.getBoundingClientRect();
let y = 0,
dims, computed, yHeight, args;
// Only go down one level of hierarchy here; stacks don't do hierarchies, only interested in knowing about immediate child elements
Array.from(el.children).forEach(child => {
if (child.getAttribute(DATA_SCRAWL_STACK) == null && !isa_canvas(child) && child.tagName != $SCRIPT) {
dims = child.getBoundingClientRect();
computed = _computed(child);
yHeight = parseFloat(computed.marginTop) + parseFloat(computed.borderTopWidth) + parseFloat(computed.paddingTop) + parseFloat(computed.paddingBottom) + parseFloat(computed.borderBottomWidth) + parseFloat(computed.marginBottom);
y = (!y) ? dims.top - hostDims.top : y;
args = {
name: child.id || child.getAttribute(NAME),
domElement: child,
group: name,
host: name,
position: ABSOLUTE,
width: dims.width,
height: dims.height,
startX: dims.left - hostDims.left,
startY: y,
classes: (child.className) ? child.className : ZERO_STR,
};
y += yHeight + dims.height;
makeElement(args);
}
// No need to worry about processing child stacks - they'll already be in the list of stacks to be processed
else child.setAttribute(DATA_SCRAWL_GROUP, name);
});
};
// `Exported function` (to modules and scrawl object). Use __addStack__ to add a Scrawl-canvas stack element to a web page, or transform an existing element into a stack element. The items argument can include
// + __element__ - the DOM element to be the stack - if not present, will autogenerate a div element
// + __host__ - the host element, either as the DOM element itself, or some sort of CSS search string; if not present, will create the stack at the stack element's current place or, failing all else, add the stack to the end of the document body
// + __name__ - String identifier for the stack; if not included the function will attempt to use the element's existing id or name attribute or, failing that, generate a random name for the stack.
// + any other regular stack attribute
export const addStack = function (items = Ωempty) {
// define variables
let el, host, hostinscrawl, mygroup, name,
position = ABSOLUTE;
// get, or generate a new, stack-to-be element
if (items.element && items.element.substring) el = document.querySelector(items.element);
else if (isa_dom(items.element)) el = items.element;
else el = document.createElement(DIV);
// get element's host (parent-to-be) element
if (items.host && items.host.substring) {
host = document.querySelector(items.host);
if (!host) host = document.body;
}
else if (isa_dom(items.host)) host = items.host;
else if (xt(el.parentElement)) host = el.parentElement;
else host = document.body;
// if dimensions have been set in the argument, apply them to the stack-to-be element
if (xt(items.width)) el.style.width = (items.width.toFixed) ? `${items.width}px` : items.width;
if (xt(items.height)) el.style.height = (items.height.toFixed) ? `${items.height}px` : items.height;
// make sure the stack-to-be element has an id attribute
name = items.name || el.id || el.getAttribute(NAME) || ZERO_STR;
if (!name) name = generateUniqueString();
el.id = name;
// set the 'data-scrawl-stack' attribute on the stack-to-be element
el.setAttribute(DATA_SCRAWL_STACK, DATA_SCRAWL_STACK);
// determine whether the parent element is already known to Scrawl-canvas - affects the stack-to-be element's group
if (host && host.getAttribute(DATA_SCRAWL_STACK) != null) {
hostinscrawl = artefact[host.id];
mygroup = (hostinscrawl) ? hostinscrawl.name : ROOT;
}
else mygroup = ROOT;
// set the 'data-scrawl-group' attribute on the stack-to-be element
el.setAttribute(DATA_SCRAWL_GROUP, mygroup);
// determine what the stack-to-be element's position style attribute will be
if (mygroup === ROOT) position = RELATIVE;
// add (or move) the stack-to-be element to/in the DOM
if (!el.parentElement || host.id !== el.parentElement.id) host.appendChild(el);
// create the Scrawl-canvas Stack artefact
const mystack = makeStack({
name: name,
domElement: el,
group: mygroup,
host: mygroup,
position: position,
setInitialDimensions: true
});
processNewStackChildren(el, name);
// in case any of the child elements were already a Scrawl-canvas stack, un-root them (if required)
Array.from(el.childNodes).forEach(child => {
if (child.id && rootElementsIncludes(child.id)) rootElementsRemove(child.id);
});
// set the new Stack's remaining attributes, clearing out any attributes already handled
delete items.name;
delete items.element;
delete items.host;
delete items.width;
delete items.height;
mystack.set(items);
// tidy up and complete
return mystack;
};
// `Exported function` (to modules and scrawl object). Parse the DOM, looking for a specific element; then create a __Stack__ artefact wrapper for it.
export const getStack = function (search) {
const el = document.querySelector(`#${search}`);
const s = stack[search];
if (s) {
if (el.dataset.scrawlGroup != null) return s;
else purge(search);
}
if (el) {
const newStack = addInitialStackElement(el);
return newStack;
}
return undefined;
};