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
314 lines (224 loc) • 10.7 kB
JavaScript
// # Anchor factory
// In Scrawl-canvas, an Anchor object holds all the data and functionality required to turn an artefact into a link. That functionality gets defined in this file.
//
// Scrawl-canvas uses the [Anchor mixin](../mixin/anchor.html) to add anchor functionality to artefacts - in particular canvas entitys. This (alongside Button objects) gives us a interactive canvas containing dynamic, clickable regions.
//
// NOTE - generating an anchor will have an impact on the DOM document code, as an (off-viewport) <a> element will be added to it.
//
// The __makeAnchor__ function is not exposed to the 'scrawl' object, thus objects can only be created indirectly. Anchors can be saved, cloned and killed as part of wider save/kill/clone functionality.
// #### Imports
import { constructors } from '../core/library.js';
import { doCreate, isa_fn, mergeOver, pushUnique, Ωempty } from '../helper/utilities.js';
import baseMix from '../mixin/base.js';
// Shared constants
import { _keys, ANCHOR, BLUR, CLICK, DATA_TAB_ORDER, DOWNLOAD, FOCUS, HREF, HREFLANG, NAME, PING, REFERRERPOLICY, REL, TARGET, UNDEF, TYPE, ZERO_STR } from '../helper/shared-vars.js';
// Local constants
const _A = 'a',
T_ANCHOR = 'Anchor';
// #### Anchor constructor
const Anchor = function (items = Ωempty) {
this.makeName(items.name);
this.register();
this.set(this.defs);
this.host = items.host;
this.controller = items.controller;
this.hold = items.hold;
this.clickAction = null;
this.domElement = null;
this.hasBeenRecentlyClicked = null;
this.set(items);
this.dirtyAnchor = true;
return this;
};
// #### Anchor prototype
const P = Anchor.prototype = doCreate();
P.type = T_ANCHOR;
P.lib = ANCHOR;
P.isArtefact = false;
P.isAsset = false;
// #### Mixins
baseMix(P);
// #### Anchor attributes
const defaultAttributes = {
// __host__ - Every anchor will belong to exactly one Artefact.
host: null,
// __description__ - The text that Scrawl-canvas will include between the anchor tags, when building the anchor. __Always include a description__ for accessibility.
description: ZERO_STR,
// __disabled__ - When set to true, will prevent the anchor <a> element from being added to the <canvas> element's <nav> element on the next build cycle.
disabled: false,
// __tabOrder__ - All hidden Anchor <a> elements have a default tabOrder attribute value of 0. SC does not touch this attribute. Instead, to order Anchor (and Button) DOM elements within the host <canvas> element's <nav> element we set a `data-tab-order` attribute with the tabOrder value, which the Canvas wrapper can then use to reorder the elements as part of the Display cycle.
tabOrder: 0,
// The following attributes are detailed in [MDN's <a> reference page](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/a).
download: ZERO_STR,
href: ZERO_STR,
hreflang: ZERO_STR,
ping: ZERO_STR,
referrerpolicy: ZERO_STR,
rel: 'noreferrer',
target: '_blank',
anchorType: ZERO_STR,
// __clickAction__ - function - actions to be performed when user tabs to the hidden <a> element and presses the keyboard return button. Function cannot take any arguments.
clickAction: null,
// We can instruct the anchor to add event listeners for focus and blur events using the __focusAction__ and __blurAction__ Boolean flags. When set to true, the ___focus___ event listener will invoke the host entity's `onEnter` function; the ___blur___ event listener invokes the `onLeave` function.
focusAction: true,
blurAction: true,
};
P.defs = mergeOver(P.defs, defaultAttributes);
// ## Packet management
P.packetExclusions = pushUnique(P.packetExclusions, ['domElement']);
P.packetObjects = pushUnique(P.packetObjects, ['host']);
P.packetFunctions = pushUnique(P.packetFunctions, ['clickAction']);
// #### Clone management
// No additional clone functionality required
// #### Kill management
P.demolish = function () {
const { host, controller, domElement, hold, clickAction, focusAction, blurAction } = this;
if (domElement && clickAction) domElement.removeEventListener(CLICK, clickAction, false);
if (host && domElement && focusAction) domElement.removeEventListener(FOCUS, () => host.onEnter(), false);
if (host && domElement && blurAction) domElement.removeEventListener(BLUR, () => host.onLeave(), false);
if (hold && domElement) hold.removeChild(domElement);
if (controller) controller.dirtyNavigationTabOrder = true;
if (host) host.anchor = null;
this.deregister();
};
// #### Get, Set, deltaSet
// While the Scrawl-canvas anchor object keeps copies of all of its <a> DOM element's attributes locally, they also need to be updated on that element.
//
// The artefact with which an anchor object is associated maps these attributes to itself as follows:
// ```
// anchor.anchorType ~~> artefact.anchorType
// anchor.description ~~> artefact.anchorDescription
// anchor.download ~~> artefact.anchorDownload
// anchor.href ~~> artefact.anchorHref
// anchor.hreflang ~~> artefact.anchorHreflang
// anchor.ping ~~> artefact.anchorPing
// anchor.referrerPolicy ~~> artefact.anchorReferrerPolicy
// anchor.rel ~~> artefact.anchorRel
// anchor.tabOrder ~~> artefact.anchorTabOrder
// anchor.target ~~> artefact.anchorTarget
// ```
// One or more of these attributes can also be set (in the artefact factory argument, or when invoking artefact.set) using an 'anchor' attribute.
P.set = function (items = Ωempty) {
let i, key, val, fn;
const keys = _keys(items),
keysLen = keys.length;
if (keysLen) {
const setters = this.setters,
defs = this.defs;
for (i = 0; i < keysLen; i++) {
key = keys[i];
val = items[key];
if (key && key !== NAME && val != null) {
fn = setters[key];
if (fn) fn.call(this, val);
else if (typeof defs[key] !== UNDEF) this[key] = val;
}
}
this.dirtyAnchor = true;
}
return this;
};
// #### Prototype functions
// The `build` function builds the <a> element and adds it to the DOM
P.build = function () {
const { host } = this;
if (host) {
if (!this.controller) this.controller = host.getCanvasWrapper();
if (!this.hold) this.hold = host.getCanvasNavElement();
const { hold, controller } = this;
if (hold && controller) {
const { anchorType, blurAction, clickAction, description, disabled, download, focusAction, href, hreflang, name, ping, referrerpolicy, rel, tabOrder, target } = this;
let link = this.domElement;
if (link) {
if (clickAction) link.removeEventListener(CLICK, clickAction, false);
if (focusAction) link.removeEventListener(FOCUS, () => host.onEnter(), false);
if (blurAction) link.removeEventListener(BLUR, () => host.onLeave(), false);
hold.removeChild(link);
this.domElement = null;
}
if (!disabled) {
link = document.createElement(_A);
link.id = name;
if (download) link.setAttribute(DOWNLOAD, download);
if (href) link.setAttribute(HREF, href);
if (hreflang) link.setAttribute(HREFLANG, hreflang);
if (ping) link.setAttribute(PING, ping);
if (referrerpolicy) link.setAttribute(REFERRERPOLICY, referrerpolicy);
if (rel) link.setAttribute(REL, rel);
if (target) link.setAttribute(TARGET, target);
if (anchorType) link.setAttribute(TYPE, anchorType);
link.setAttribute(DATA_TAB_ORDER, tabOrder);
if (clickAction && isa_fn(clickAction)) link.addEventListener(CLICK, clickAction, false);
if (description) link.textContent = description;
if (focusAction) link.addEventListener(FOCUS, () => host.onEnter(), false);
if (blurAction) link.addEventListener(BLUR, () => host.onLeave(), false);
this.domElement = link;
hold.appendChild(link);
}
controller.dirtyNavigationTabOrder = true;
}
}
};
// `rebuild` - called as part of the Display cycle
P.rebuild = function () {
if (this.dirtyAnchor) {
this.build();
this.dirtyAnchor = false;
}
}
// To action a user `click` on an artifact with an associated anchor object, we generate a DOM MouseEvent originating from the anchor element which the browser can act on in the usual manner (browser/device dependent)
P.click = function () {
if (!this.hasBeenRecentlyClicked) {
const e = new MouseEvent(CLICK, {
view: window,
bubbles: true,
cancelable: true
});
// This choke mechanism is intended to prevent "Maximum call stack size exceeded" errors occurring
// + Was causing an issue in Demo [Canvas-027](../../demo/canvas-027.html), where two entitys share the same anchor
this.hasBeenRecentlyClicked = true;
const self = this;
setTimeout(() => self.hasBeenRecentlyClicked = false, 200);
return this.domElement.dispatchEvent(e);
}
else return false;
};
// #### Factory
// To create an anchor, include an anchor definition object in any artefact object's factory argument:
// ```
// // get a handle on the canvas where the block/link will be defined
// // (in this case a canvas with id="mycanvas")
// let canvas = scrawl.library.artefact.mycanvas;
// canvas.setAsCurrentCanvas();
//
// // Define a block entity
// scrawl.makeBlock({
//
// name: 'demo-anchor-block',
//
// width: '40%',
// height: '40%',
//
// startX: '25%',
// startY: '25%',
//
// // Define the anchor object's attributes
// anchor: {
// name: 'wikipedia-water-link',
// href: 'https://en.wikipedia.org/wiki/Water',
// description: 'Link to the Wikipedia article on water (opens in new tab)',
// },
//
// // Add an action to take when user clicks on the block entity
// onUp: this.clickAnchor,
// });
//
// // Add a listener to propagate DOM-detected click events on our canvas
// // back into the Scrawl-canvas event system
// scrawl.addListener('up', () => canvas.cascadeEventAction('up'), canvas.domElement);
// ```
export const makeAnchor = function (items) {
if (!items) return false;
return new Anchor(items);
};
constructors.Anchor = Anchor;