import { constructors, artefact } from '../core/library.js';
import { mergeOver, pushUnique, isa_fn, isa_dom } from '../core/utilities.js';
import baseMix from '../mixin/base.js';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 to add anchor functionality to artefacts - in particular canvas entitys.
This gives us a interactive canvas containing dynamic, clickable regions.
Note that while anchors are primarily for generating URL links to (external site) web pages, they can also be used to trigger any other desired action. This can be achieved by setting the anchor object’s clickAction attribute to a function. For instance:
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.
import { constructors, artefact } from '../core/library.js';
import { mergeOver, pushUnique, isa_fn, isa_dom } from '../core/utilities.js';
import baseMix from '../mixin/base.js';const Anchor = function (items = {}) {
this.makeName(items.name);
this.register();
this.set(this.defs);
this.set(items);
this.build();
return this;
};let P = Anchor.prototype = Object.create(Object.prototype);
P.type = 'Anchor';
P.lib = 'anchor';
P.isArtefact = false;
P.isAsset = false;P = baseMix(P);let 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: '',The following attributes are detailed in MDN’s <a> reference page
type attribute is stored in the Scrawl-canvas Anchor object using the key anchorType. download: '',
href: '',
hreflang: '',
ping: '',
referrerpolicy: '',
rel: 'noreferrer',
target: '_blank',
anchorType: '',The clickAction attribute is a function which returns a string command which in turn gets attached to the anchor DOM element’s onclick attribute. Invoking the result is handled entirely by the browser (as is normal).
This doesn’t work! The browser will generate an error, rather than output an update to the console, when the user clicks on the canvas entity associated with the anchor (although navigation will still occur - the wikipedia page will open in a new browser tab):
anchor: {
name: 'wikipedia-box-link',
href: 'https://en.wikipedia.org/wiki/Box',
description: 'Link to the Wikipedia article on boxes (opens in new tab)',
clickAction: function () { console.log('box clicked') },
}
This works as expected - the function returns a string which can then be attached to the <a> DOM element’s onclick attribute:
anchor: {
name: 'wikipedia-box-link',
href: 'https://en.wikipedia.org/wiki/Box',
description: 'Link to the Wikipedia article on boxes (opens in new tab)',
clickAction: function () { return `console.log('box clicked')` },
},
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. Default is to ignore these events
focusAction: false,
blurAction: false,
};
P.defs = mergeOver(P.defs, defaultAttributes);P.packetExclusions = pushUnique(P.packetExclusions, ['domElement']);
P.packetObjects = pushUnique(P.packetExclusions, ['host']);
P.packetFunctions = pushUnique(P.packetFunctions, ['clickAction']);P.demolish = function () {
if (this.domElement && this.hold) this.hold.removeChild(this.domElement);
this.deregister();
};let S = P.setters;Value should be the artefact object, or its name-String
S.host = function (item) {
let h = (item.substring) ? artefact[item] : item;
if (h && h.name) this.host = h;
};Used internally - do not set directly! A reference to the hidden DOM hold <div> elemenbt where the anchors <a> element is kept.
S.hold = function (item) {
if (isa_dom(item)) {
if (this.domElement && this.hold) this.hold.removeChild(this.domElement);
this.hold = item;
if (this.domElement) this.hold.appendChild(this.domElement);
}
};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. Most of the setter functions manage this using the anchor.update() helper function.
The artefact with which an anchor object is associated maps these attributes to itself as follows:
anchor.description ~~> artefact.anchorDescription
anchor.type ~~> artefact.anchorType
anchor.target ~~> artefact.anchorTarget
anchor.rel ~~> artefact.anchorRel
anchor.referrerPolicy ~~> artefact.anchorReferrerPolicy
anchor.ping ~~> artefact.anchorPing
anchor.hreflang ~~> artefact.anchorHreflang
anchor.href ~~> artefact.anchorHref
anchor.download ~~> artefact.anchorDownload
One or more of these attributes can also be set (in the artefact factory argument, or when invoking artefact.set) using an ‘anchor’ attribute:
artefact.set({
anchor: {
description: 'value',
type: 'value',
target: 'value',
rel: 'value',
referrerPolicy: 'value',
ping: 'value',
hreflang: 'value',
href: 'value',
download: 'value',
},
});
S.download = function (item) {
this.download = item;
if (this.domElement) this.update('download');
};
S.href = function (item) {
this.href = item;
if (this.domElement) this.update('href');
};
S.hreflang = function (item) {
this.hreflang = item;
if (this.domElement) this.update('hreflang');
};
S.ping = function (item) {
this.ping = item;
if (this.domElement) this.update('ping');
};
S.referrerpolicy = function (item) {
this.referrerpolicy = item;
if (this.domElement) this.update('referrerpolicy');
};
S.rel = function (item) {
this.rel = item;
if (this.domElement) this.update('rel');
};
S.target = function (item) {
this.target = item;
if (this.domElement) this.update('target');
};
S.anchorType = function (item) {
this.anchorType = item;
if (this.domElement) this.update('type');
};These last setters do not follow previous behaviour because Scrawl-canvas anchor objects save the values for each under a different attribute key, compared to the DOM element’s attribute key:
anchor.description -> a.textContent - this is the text between the <a> element’s opening and closing tagsanchor.clickAction -> a.onclick - a function that returns an string which is added to the DOM element’s ‘onclick’ attributeS.description = function (item) {
this.description = item;
if (this.domElement) this.domElement.textContent = item;
};
S.clickAction = function (item) {
if (isa_fn(item)) {
this.clickAction = item;
if (this.domElement) this.domElement.setAttribute('onclick', item());
}
};The build function builds the <a> element and adds it to the DOM
Scrawl-canvas generated anchor links are kept in hidden <nav> elements - either the Canvas object’s nav, or the Scrawl-canvas default nav (referenced by scrawlNavigationHold) which Scrawl-canvas automatically generates and adds to the top of the DOM <body> element when it first runs.
This is done to give screen readers access to link URLs and descriptions associated with Canvas graphical entitys (which visually impaired users may not be able to see). It also allows links to be tabbed through and invoked in the normal way (which may vary dependent on how browsers implement tab focus functionality)
P.build = function () {
if (this.domElement && this.hold) this.hold.removeChild(this.domElement);
let link = document.createElement('a');
link.id = this.name;
if (this.download) link.setAttribute('download', this.download);
if (this.href) link.setAttribute('href', this.href);
if (this.hreflang) link.setAttribute('hreflang', this.hreflang);
if (this.ping) link.setAttribute('ping', this.ping);
if (this.referrerpolicy) link.setAttribute('referrerpolicy', this.referrerpolicy);
if (this.rel) link.setAttribute('rel', this.rel);
if (this.target) link.setAttribute('target', this.target);
if (this.anchorType) link.setAttribute('type', this.anchorType);
if (this.clickAction && isa_fn(this.clickAction)) link.setAttribute('onclick', this.clickAction());
if (this.description) link.textContent = this.description;
if (this.focusAction) link.addEventListener('focus', (e) => this.host.onEnter(), false);
if (this.blurAction) link.addEventListener('blur', (e) => this.host.onLeave(), false);
this.domElement = link;
if (this.hold) this.hold.appendChild(link);
};Internal function - update the DOM element attribute
P.update = function (item) {
if (this.domElement) this.domElement.setAttribute(item, this[item]);
};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) {
let e = new MouseEvent('click', {
view: window,
bubbles: true,
cancelable: true
});This choke mechanism is intended to prevent “Maximum call stack size exceeded” errors occurring
this.hasBeenRecentlyClicked = true;
let self = this;
setTimeout(() => self.hasBeenRecentlyClicked = false, 200);
return this.domElement.dispatchEvent(e);
}
else return false;
};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);
const makeAnchor = function (items) {
return new Anchor(items);
};
constructors.Anchor = Anchor;export {
makeAnchor,
};