UNPKG

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
// # 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 &lt;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; };