Added support for DOM cards.
This commit is contained in:
parent
d04f92ee7f
commit
e1b5c45b52
323
dist/iwmlib.js
vendored
323
dist/iwmlib.js
vendored
@ -4003,6 +4003,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DOMScatter extends AbstractScatter {
|
class DOMScatter extends AbstractScatter {
|
||||||
constructor(
|
constructor(
|
||||||
element,
|
element,
|
||||||
@ -4028,7 +4029,7 @@
|
|||||||
width = null, // required
|
width = null, // required
|
||||||
height = null, // required
|
height = null, // required
|
||||||
resizable = false,
|
resizable = false,
|
||||||
clickOnTap = false,
|
tapDelegate = null,
|
||||||
triggerSVGClicks = false,
|
triggerSVGClicks = false,
|
||||||
allowClickDistance = 44,
|
allowClickDistance = 44,
|
||||||
verbose = true,
|
verbose = true,
|
||||||
@ -4085,8 +4086,7 @@
|
|||||||
this.height = height;
|
this.height = height;
|
||||||
this.throwVisibility = Math.min(width, height, throwVisibility);
|
this.throwVisibility = Math.min(width, height, throwVisibility);
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.clickOnTap = clickOnTap;
|
this.tapDelegate = tapDelegate;
|
||||||
this.triggerSVGClicks = triggerSVGClicks;
|
|
||||||
this.scale = startScale;
|
this.scale = startScale;
|
||||||
this.rotationDegrees = this.startRotationDegrees;
|
this.rotationDegrees = this.startRotationDegrees;
|
||||||
this.transformOrigin = transformOrigin;
|
this.transformOrigin = transformOrigin;
|
||||||
@ -4100,7 +4100,6 @@
|
|||||||
transformOrigin: transformOrigin
|
transformOrigin: transformOrigin
|
||||||
};
|
};
|
||||||
this.tapNodes = new Map();
|
this.tapNodes = new Map();
|
||||||
this.allowClickDistance = allowClickDistance;
|
|
||||||
|
|
||||||
// For tweenlite we need initial values in _gsTransform
|
// For tweenlite we need initial values in _gsTransform
|
||||||
TweenLite.set(element, this.initialValues);
|
TweenLite.set(element, this.initialValues);
|
||||||
@ -4133,24 +4132,8 @@
|
|||||||
});
|
});
|
||||||
this.resizeButton = button;
|
this.resizeButton = button;
|
||||||
}
|
}
|
||||||
if (clickOnTap) {
|
if (tapDelegate) {
|
||||||
/* Since the tap triggers a synthetic click event
|
tapDelegate.handleClicks();
|
||||||
we must prevent the original trusted click event which
|
|
||||||
is also dispatched by the system.
|
|
||||||
*/
|
|
||||||
element.addEventListener('click', event => {
|
|
||||||
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
|
||||||
Therefore we make an exception and let the original click event through.
|
|
||||||
*/
|
|
||||||
if (event.target.ownerSVGElement) {
|
|
||||||
if (this.triggerSVGClicks) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (event.isTrusted) {
|
|
||||||
Events.stop(event);
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
container.add(this);
|
container.add(this);
|
||||||
}
|
}
|
||||||
@ -4312,110 +4295,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTap(event, interaction, point) {
|
onTap(event, interaction, point) {
|
||||||
|
if (this.tapDelegate) {
|
||||||
if (this.clickOnTap) {
|
Events.stop(event);
|
||||||
let directNode = document.elementFromPoint(event.clientX, event.clientY);
|
this.tapDelegate.tap(event, "scatter");
|
||||||
console.log("onTap", event);
|
|
||||||
if (this.isClickable(directNode)) {
|
|
||||||
directNode.click();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let nearestNode = this.nearestClickable(event);
|
|
||||||
if (this.isClickable(nearestNode)) {
|
|
||||||
/* https://stackoverflow.com/questions/49564905/is-it-possible-to-use-click-function-on-svg-tags-i-tried-element-click-on-a
|
|
||||||
proposes the dispatchEvent solution. But this leads to problems in flippable.html hiding the back page.
|
|
||||||
Therefore we use the original click event (see constructor).
|
|
||||||
*/
|
|
||||||
if (nearestNode.tagName == 'svg') {
|
|
||||||
let handler = this.tapNodes.get(nearestNode);
|
|
||||||
console.log("Clicking near SVG: to be done", handler);
|
|
||||||
if (this.triggerSVGClicks)
|
|
||||||
nearestNode.dispatchEvent(new Event('click'));
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log("nearestNode clicked");
|
|
||||||
nearestNode.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a click or tap behavior to the node. Uses
|
|
||||||
* either the scatter clickOnTap version which requires click handlers
|
|
||||||
* or uses the hammer.js driven tap handler.
|
|
||||||
*
|
|
||||||
* @param {*} node
|
|
||||||
* @param {*} handler
|
|
||||||
* @memberof DOMScatter
|
|
||||||
*/
|
|
||||||
|
|
||||||
addTapListener(node, handler) {
|
|
||||||
if (this.clickOnTap) {
|
|
||||||
node.addEventListener('click', handler);
|
|
||||||
this.tapNodes.set(node, handler);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
InteractionMapper$1.on('tap', node, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isClickable(node) {
|
|
||||||
if (node == null)
|
|
||||||
return false
|
|
||||||
if (node.tagName == 'A')
|
|
||||||
return true
|
|
||||||
if (node.hasAttribute("onclick"))
|
|
||||||
return true
|
|
||||||
if (this.tapNodes.has(node))
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of all clickable nodes.
|
|
||||||
* Unfortunately we cannot search for all nodes with an attached 'click' event listener
|
|
||||||
* See https://stackoverflow.com/questions/11455515/how-to-check-whether-dynamically-attached-event-listener-exists-or-not
|
|
||||||
* Therefore we can only detect the following standard cases:
|
|
||||||
* I. All clickable objects like clickables
|
|
||||||
* II. Objects that have been attached a click handler by the scatter itself via
|
|
||||||
*/
|
|
||||||
clickableNodes() {
|
|
||||||
let result = [];
|
|
||||||
for (let node of this.element.querySelectorAll("*")) {
|
|
||||||
if (this.isClickable(node))
|
|
||||||
result.push(node);
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
nearestClickable(event) {
|
|
||||||
let element = this.element;
|
|
||||||
let clickables = this.clickableNodes();
|
|
||||||
let globalClick = (event.center) ? event.center : { x: event.x, y: event.y };
|
|
||||||
let localClick = Points.fromPageToNode(element, globalClick);
|
|
||||||
|
|
||||||
let clickRects = clickables.map(link => {
|
|
||||||
let rect = link.getBoundingClientRect();
|
|
||||||
let topLeft = Points.fromPageToNode(element, rect);
|
|
||||||
let center = Points.fromPageToNode(element, { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 });
|
|
||||||
return { x: topLeft.x, y: topLeft.y, width: rect.width, height: rect.height, center, link }
|
|
||||||
});
|
|
||||||
|
|
||||||
let distances = [];
|
|
||||||
clickRects.forEach(rect => {
|
|
||||||
let distance = Points.distanceToRect(localClick, rect);
|
|
||||||
distances.push(parseInt(distance));
|
|
||||||
});
|
|
||||||
|
|
||||||
let closestClickIndex = distances.indexOf(Math.min(...distances));
|
|
||||||
let closestClickable = clickables[closestClickIndex];
|
|
||||||
if (distances[closestClickIndex] < this.allowClickDistance) {
|
|
||||||
return closestClickable
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
isDescendant(parent, child) {
|
isDescendant(parent, child) {
|
||||||
let node = child.parentNode;
|
let node = child.parentNode;
|
||||||
while (node != null) {
|
while (node != null) {
|
||||||
@ -4735,7 +4620,7 @@
|
|||||||
translatable = true,
|
translatable = true,
|
||||||
scalable = true,
|
scalable = true,
|
||||||
rotatable = true,
|
rotatable = true,
|
||||||
clickOnTap = false,
|
tapDelegateFactory = null,
|
||||||
onFront = null,
|
onFront = null,
|
||||||
onBack = null,
|
onBack = null,
|
||||||
onClose = null,
|
onClose = null,
|
||||||
@ -4755,7 +4640,7 @@
|
|||||||
this.translatable = translatable;
|
this.translatable = translatable;
|
||||||
this.scalable = scalable;
|
this.scalable = scalable;
|
||||||
this.rotatable = rotatable;
|
this.rotatable = rotatable;
|
||||||
this.clickOnTap = clickOnTap;
|
this.tapDelegateFactory = tapDelegateFactory;
|
||||||
this.onFrontFlipped = onFront;
|
this.onFrontFlipped = onFront;
|
||||||
this.onBackFlipped = onBack;
|
this.onBackFlipped = onBack;
|
||||||
this.onClose = onClose;
|
this.onClose = onClose;
|
||||||
@ -4811,7 +4696,7 @@
|
|||||||
scalable: this.scalable,
|
scalable: this.scalable,
|
||||||
rotatable: this.rotatable,
|
rotatable: this.rotatable,
|
||||||
overdoScaling: this.overdoScaling,
|
overdoScaling: this.overdoScaling,
|
||||||
clickOnTap: this.clickOnTap
|
tapDelegate: (this.tapDelegateFactory) ? this.tapDelegateFactory(this.cardWrapper) : null
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -7634,6 +7519,190 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* eslint-disable no-console */
|
||||||
|
|
||||||
|
class CardWrapper extends Object {
|
||||||
|
|
||||||
|
constructor(domNode, { triggerSVGClicks = true, allowClickDistance = 44 } = {}) {
|
||||||
|
super();
|
||||||
|
this.domNode = domNode;
|
||||||
|
this.triggerSVGClicks = triggerSVGClicks;
|
||||||
|
this.allowClickDistance = allowClickDistance;
|
||||||
|
this.tapNodes = new Map();
|
||||||
|
this.tapHandler = new Map();
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClicks() {
|
||||||
|
this.domNode.addEventListener('click', event => {
|
||||||
|
console.log('handleClicks', event.isTrusted);
|
||||||
|
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
||||||
|
Therefore we make an exception and let the original click event through.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (event.isTrusted) {
|
||||||
|
Events.stop(event);
|
||||||
|
if (this.triggerSVGClicks && this.isSVGNode(event.target)) {
|
||||||
|
this.tap(event, "triggerSVGClicks");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClicksAsTaps() {
|
||||||
|
this.domNode.addEventListener('click', event => {
|
||||||
|
console.log('handleClicksAsTaps', event.isTrusted);
|
||||||
|
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
||||||
|
Therefore we make an exception and let the original click event through.
|
||||||
|
*/
|
||||||
|
if (event.isTrusted) {
|
||||||
|
Events.stop(event);
|
||||||
|
|
||||||
|
}
|
||||||
|
this.tap(event);
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
isClickable(node) {
|
||||||
|
if (node == null)
|
||||||
|
return false
|
||||||
|
if (node.tagName == 'A' && node.hasAttribute("href"))
|
||||||
|
return true
|
||||||
|
if (node.hasAttribute("onclick"))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hasClickHandler(node) {
|
||||||
|
if (node == null)
|
||||||
|
return false
|
||||||
|
if (this.tapNodes.has(node))
|
||||||
|
return true
|
||||||
|
for (let [selector, handler] of this.tapHandler.entries()) {
|
||||||
|
console.log("selector", this.domNode.querySelectorAll(selector));
|
||||||
|
for (let obj of this.domNode.querySelectorAll(selector)) {
|
||||||
|
console.log("selector2", node, obj);
|
||||||
|
if (node == obj) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of all active nodes.
|
||||||
|
* Unfortunately we cannot search for all nodes with an attached 'click' event listener
|
||||||
|
* See https://stackoverflow.com/questions/11455515/how-to-check-whether-dynamically-attached-event-listener-exists-or-not
|
||||||
|
* Therefore we can only detect the following standard cases:
|
||||||
|
* I. All clickable objects like activeNodes
|
||||||
|
* II. Objects that have been attached a click handler by the scatter itself via
|
||||||
|
*/
|
||||||
|
activeNodes() {
|
||||||
|
let result = [];
|
||||||
|
for (let node of this.domNode.querySelectorAll("*")) {
|
||||||
|
if (this.isClickable(node))
|
||||||
|
result.push(node);
|
||||||
|
if (this.hasClickHandler(node))
|
||||||
|
result.push(node);
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestActive(event) {
|
||||||
|
let element = this.domNode;
|
||||||
|
let activeNodes = this.activeNodes();
|
||||||
|
let globalClick = (event.center) ? event.center : { x: event.x, y: event.y };
|
||||||
|
let localClick = Points.fromPageToNode(element, globalClick);
|
||||||
|
|
||||||
|
let clickRects = activeNodes.map(link => {
|
||||||
|
let rect = link.getBoundingClientRect();
|
||||||
|
let topLeft = Points.fromPageToNode(element, rect);
|
||||||
|
let center = Points.fromPageToNode(element, { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 });
|
||||||
|
return { x: topLeft.x, y: topLeft.y, width: rect.width, height: rect.height, center, link }
|
||||||
|
});
|
||||||
|
|
||||||
|
let distances = [];
|
||||||
|
clickRects.forEach(rect => {
|
||||||
|
let distance = Points.distanceToRect(localClick, rect);
|
||||||
|
distances.push(parseInt(distance));
|
||||||
|
});
|
||||||
|
|
||||||
|
let closestClickIndex = distances.indexOf(Math.min(...distances));
|
||||||
|
let closestClickable = activeNodes[closestClickIndex];
|
||||||
|
if (distances[closestClickIndex] < this.allowClickDistance) {
|
||||||
|
return closestClickable
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
isSVGNode(node) {
|
||||||
|
return node.ownerSVGElement || node.tagName == 'svg'
|
||||||
|
}
|
||||||
|
|
||||||
|
simulateClick(node, event) {
|
||||||
|
/* https://stackoverflow.com/questions/49564905/is-it-possible-to-use-click-function-on-svg-tags-i-tried-element-click-on-a
|
||||||
|
proposes the dispatchEvent solution. But this leads to problems in flippable.html hiding the back page.
|
||||||
|
Therefore we use the original click event (see constructor). */
|
||||||
|
console.log("simulateClick", node, node.ownerSVGElement);
|
||||||
|
if (this.isSVGNode(node)) {
|
||||||
|
if (this.triggerSVGClicks) {
|
||||||
|
let click = new Event('click');
|
||||||
|
node.dispatchEvent(click);
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeTapped(node, event) {
|
||||||
|
if (this.isClickable(node)) {
|
||||||
|
this.simulateClick(node, event);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this.tapNodes.has(node)) {
|
||||||
|
handler = this.tapNodes.get(node);
|
||||||
|
handler(event);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for (let [selector, handler] of this.tapHandler.entries()) {
|
||||||
|
for (let obj of this.domNode.querySelectorAll(selector)) {
|
||||||
|
if (node == obj) {
|
||||||
|
handler(event);
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tap(event, calledBy='unknown') {
|
||||||
|
console.log("tap", calledBy, event.alreadyTapped, event);
|
||||||
|
if (event.isTrusted) {
|
||||||
|
let node = this.nearestActive(event);
|
||||||
|
this.nodeTapped(node, event);
|
||||||
|
|
||||||
|
/* let node = document.elementFromPoint(event.clientX, event.clientY)
|
||||||
|
if (!this.nodeTapped(node, event)) {
|
||||||
|
node = this.nearestActive(event)
|
||||||
|
this.nodeTapped(node, event)
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTap(objOrSelector, handler) {
|
||||||
|
if (typeof (objOrSelector) == 'string') {
|
||||||
|
this.tapHandler.set(objOrSelector, handler);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.tapNodes.set(objOrSelector, handler);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
window.CardWrapper = CardWrapper;
|
||||||
|
|
||||||
/* Needed to ensure that rollup.js includes class definitions and the classes
|
/* Needed to ensure that rollup.js includes class definitions and the classes
|
||||||
are visible inside doctests.
|
are visible inside doctests.
|
||||||
*/
|
*/
|
||||||
@ -7697,4 +7766,6 @@
|
|||||||
window.randomInt = randomInt;
|
window.randomInt = randomInt;
|
||||||
window.randomFloat = randomFloat;
|
window.randomFloat = randomFloat;
|
||||||
|
|
||||||
|
window.CardWrapper = CardWrapper;
|
||||||
|
|
||||||
}());
|
}());
|
||||||
|
137
dist/iwmlib.pixi.js
vendored
137
dist/iwmlib.pixi.js
vendored
@ -7003,6 +7003,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class DOMScatter extends AbstractScatter {
|
class DOMScatter extends AbstractScatter {
|
||||||
constructor(
|
constructor(
|
||||||
element,
|
element,
|
||||||
@ -7028,7 +7029,7 @@
|
|||||||
width = null, // required
|
width = null, // required
|
||||||
height = null, // required
|
height = null, // required
|
||||||
resizable = false,
|
resizable = false,
|
||||||
clickOnTap = false,
|
tapDelegate = null,
|
||||||
triggerSVGClicks = false,
|
triggerSVGClicks = false,
|
||||||
allowClickDistance = 44,
|
allowClickDistance = 44,
|
||||||
verbose = true,
|
verbose = true,
|
||||||
@ -7085,8 +7086,7 @@
|
|||||||
this.height = height;
|
this.height = height;
|
||||||
this.throwVisibility = Math.min(width, height, throwVisibility);
|
this.throwVisibility = Math.min(width, height, throwVisibility);
|
||||||
this.container = container;
|
this.container = container;
|
||||||
this.clickOnTap = clickOnTap;
|
this.tapDelegate = tapDelegate;
|
||||||
this.triggerSVGClicks = triggerSVGClicks;
|
|
||||||
this.scale = startScale;
|
this.scale = startScale;
|
||||||
this.rotationDegrees = this.startRotationDegrees;
|
this.rotationDegrees = this.startRotationDegrees;
|
||||||
this.transformOrigin = transformOrigin;
|
this.transformOrigin = transformOrigin;
|
||||||
@ -7100,7 +7100,6 @@
|
|||||||
transformOrigin: transformOrigin
|
transformOrigin: transformOrigin
|
||||||
};
|
};
|
||||||
this.tapNodes = new Map();
|
this.tapNodes = new Map();
|
||||||
this.allowClickDistance = allowClickDistance;
|
|
||||||
|
|
||||||
// For tweenlite we need initial values in _gsTransform
|
// For tweenlite we need initial values in _gsTransform
|
||||||
TweenLite.set(element, this.initialValues);
|
TweenLite.set(element, this.initialValues);
|
||||||
@ -7133,24 +7132,8 @@
|
|||||||
});
|
});
|
||||||
this.resizeButton = button;
|
this.resizeButton = button;
|
||||||
}
|
}
|
||||||
if (clickOnTap) {
|
if (tapDelegate) {
|
||||||
/* Since the tap triggers a synthetic click event
|
tapDelegate.handleClicks();
|
||||||
we must prevent the original trusted click event which
|
|
||||||
is also dispatched by the system.
|
|
||||||
*/
|
|
||||||
element.addEventListener('click', event => {
|
|
||||||
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
|
||||||
Therefore we make an exception and let the original click event through.
|
|
||||||
*/
|
|
||||||
if (event.target.ownerSVGElement) {
|
|
||||||
if (this.triggerSVGClicks) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (event.isTrusted) {
|
|
||||||
Events$1.stop(event);
|
|
||||||
}
|
|
||||||
}, true);
|
|
||||||
}
|
}
|
||||||
container.add(this);
|
container.add(this);
|
||||||
}
|
}
|
||||||
@ -7312,110 +7295,12 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTap(event, interaction, point) {
|
onTap(event, interaction, point) {
|
||||||
|
if (this.tapDelegate) {
|
||||||
if (this.clickOnTap) {
|
Events$1.stop(event);
|
||||||
let directNode = document.elementFromPoint(event.clientX, event.clientY);
|
this.tapDelegate.tap(event, "scatter");
|
||||||
console.log("onTap", event);
|
|
||||||
if (this.isClickable(directNode)) {
|
|
||||||
directNode.click();
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let nearestNode = this.nearestClickable(event);
|
|
||||||
if (this.isClickable(nearestNode)) {
|
|
||||||
/* https://stackoverflow.com/questions/49564905/is-it-possible-to-use-click-function-on-svg-tags-i-tried-element-click-on-a
|
|
||||||
proposes the dispatchEvent solution. But this leads to problems in flippable.html hiding the back page.
|
|
||||||
Therefore we use the original click event (see constructor).
|
|
||||||
*/
|
|
||||||
if (nearestNode.tagName == 'svg') {
|
|
||||||
let handler = this.tapNodes.get(nearestNode);
|
|
||||||
console.log("Clicking near SVG: to be done", handler);
|
|
||||||
if (this.triggerSVGClicks)
|
|
||||||
nearestNode.dispatchEvent(new Event('click'));
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log("nearestNode clicked");
|
|
||||||
nearestNode.click();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a click or tap behavior to the node. Uses
|
|
||||||
* either the scatter clickOnTap version which requires click handlers
|
|
||||||
* or uses the hammer.js driven tap handler.
|
|
||||||
*
|
|
||||||
* @param {*} node
|
|
||||||
* @param {*} handler
|
|
||||||
* @memberof DOMScatter
|
|
||||||
*/
|
|
||||||
|
|
||||||
addTapListener(node, handler) {
|
|
||||||
if (this.clickOnTap) {
|
|
||||||
node.addEventListener('click', handler);
|
|
||||||
this.tapNodes.set(node, handler);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
InteractionMapper$1.on('tap', node, handler);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isClickable(node) {
|
|
||||||
if (node == null)
|
|
||||||
return false
|
|
||||||
if (node.tagName == 'A')
|
|
||||||
return true
|
|
||||||
if (node.hasAttribute("onclick"))
|
|
||||||
return true
|
|
||||||
if (this.tapNodes.has(node))
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of all clickable nodes.
|
|
||||||
* Unfortunately we cannot search for all nodes with an attached 'click' event listener
|
|
||||||
* See https://stackoverflow.com/questions/11455515/how-to-check-whether-dynamically-attached-event-listener-exists-or-not
|
|
||||||
* Therefore we can only detect the following standard cases:
|
|
||||||
* I. All clickable objects like clickables
|
|
||||||
* II. Objects that have been attached a click handler by the scatter itself via
|
|
||||||
*/
|
|
||||||
clickableNodes() {
|
|
||||||
let result = [];
|
|
||||||
for (let node of this.element.querySelectorAll("*")) {
|
|
||||||
if (this.isClickable(node))
|
|
||||||
result.push(node);
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
nearestClickable(event) {
|
|
||||||
let element = this.element;
|
|
||||||
let clickables = this.clickableNodes();
|
|
||||||
let globalClick = (event.center) ? event.center : { x: event.x, y: event.y };
|
|
||||||
let localClick = Points.fromPageToNode(element, globalClick);
|
|
||||||
|
|
||||||
let clickRects = clickables.map(link => {
|
|
||||||
let rect = link.getBoundingClientRect();
|
|
||||||
let topLeft = Points.fromPageToNode(element, rect);
|
|
||||||
let center = Points.fromPageToNode(element, { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 });
|
|
||||||
return { x: topLeft.x, y: topLeft.y, width: rect.width, height: rect.height, center, link }
|
|
||||||
});
|
|
||||||
|
|
||||||
let distances = [];
|
|
||||||
clickRects.forEach(rect => {
|
|
||||||
let distance = Points.distanceToRect(localClick, rect);
|
|
||||||
distances.push(parseInt(distance));
|
|
||||||
});
|
|
||||||
|
|
||||||
let closestClickIndex = distances.indexOf(Math.min(...distances));
|
|
||||||
let closestClickable = clickables[closestClickIndex];
|
|
||||||
if (distances[closestClickIndex] < this.allowClickDistance) {
|
|
||||||
return closestClickable
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
isDescendant(parent, child) {
|
isDescendant(parent, child) {
|
||||||
let node = child.parentNode;
|
let node = child.parentNode;
|
||||||
while (node != null) {
|
while (node != null) {
|
||||||
@ -7601,7 +7486,7 @@
|
|||||||
translatable = true,
|
translatable = true,
|
||||||
scalable = true,
|
scalable = true,
|
||||||
rotatable = true,
|
rotatable = true,
|
||||||
clickOnTap = false,
|
tapDelegateFactory = null,
|
||||||
onFront = null,
|
onFront = null,
|
||||||
onBack = null,
|
onBack = null,
|
||||||
onClose = null,
|
onClose = null,
|
||||||
@ -7621,7 +7506,7 @@
|
|||||||
this.translatable = translatable;
|
this.translatable = translatable;
|
||||||
this.scalable = scalable;
|
this.scalable = scalable;
|
||||||
this.rotatable = rotatable;
|
this.rotatable = rotatable;
|
||||||
this.clickOnTap = clickOnTap;
|
this.tapDelegateFactory = tapDelegateFactory;
|
||||||
this.onFrontFlipped = onFront;
|
this.onFrontFlipped = onFront;
|
||||||
this.onBackFlipped = onBack;
|
this.onBackFlipped = onBack;
|
||||||
this.onClose = onClose;
|
this.onClose = onClose;
|
||||||
@ -7677,7 +7562,7 @@
|
|||||||
scalable: this.scalable,
|
scalable: this.scalable,
|
||||||
rotatable: this.rotatable,
|
rotatable: this.rotatable,
|
||||||
overdoScaling: this.overdoScaling,
|
overdoScaling: this.overdoScaling,
|
||||||
clickOnTap: this.clickOnTap
|
tapDelegate: (this.tapDelegateFactory) ? this.tapDelegateFactory(this.cardWrapper) : null
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -19,6 +19,7 @@ import {ResizeEvent, DOMScatterContainer, AbstractScatter, DOMScatter, ScatterEv
|
|||||||
import {Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Strings, isEmpty, getId, lerp, debounce, randomInt, randomFloat, LowPassFilter} from './utils.js'
|
import {Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Strings, isEmpty, getId, lerp, debounce, randomInt, randomFloat, LowPassFilter} from './utils.js'
|
||||||
import UITest from './uitest.js'
|
import UITest from './uitest.js'
|
||||||
|
|
||||||
|
import {CardWrapper} from './card/wrapper.js'
|
||||||
/* Needed to ensure that rollup.js includes class definitions and the classes
|
/* Needed to ensure that rollup.js includes class definitions and the classes
|
||||||
are visible inside doctests.
|
are visible inside doctests.
|
||||||
*/
|
*/
|
||||||
@ -81,3 +82,5 @@ window.lerp = lerp
|
|||||||
window.debounce = debounce
|
window.debounce = debounce
|
||||||
window.randomInt = randomInt
|
window.randomInt = randomInt
|
||||||
window.randomFloat = randomFloat
|
window.randomFloat = randomFloat
|
||||||
|
|
||||||
|
window.CardWrapper = CardWrapper
|
400
lib/card/highlight.js
Normal file
400
lib/card/highlight.js
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
/* global TweenLite */
|
||||||
|
|
||||||
|
let _HighlightEnabled = true
|
||||||
|
let _CircleIds = 0
|
||||||
|
|
||||||
|
|
||||||
|
/** Helper method to round values with one digit precision */
|
||||||
|
function round(value) {
|
||||||
|
return Math.round(parseFloat(value) * 10) / 10
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A namespace with static functions to expand and shrink highlighted image regions.
|
||||||
|
* Assumes an SVG image with the following structure:
|
||||||
|
*
|
||||||
|
* <svg viewbox="0 0 100 100">
|
||||||
|
* <!-- The defs section must be defined and cannot be generated in JavaScript -->
|
||||||
|
* <defs>
|
||||||
|
* </defs>
|
||||||
|
* <image width="100" height="100" xlink:href="../assets/chess.jpg"/>
|
||||||
|
* <circle onclick="Highlight.animateZoom(event)" cx="47" cy="18" r="8" stroke-width="0.5" />
|
||||||
|
* <circle onclick="Highlight.animateZoom(event)" cx="60" cy="67" r="8" stroke-width="0.5" />
|
||||||
|
* </svg>
|
||||||
|
*
|
||||||
|
* The SVG root element should use a viewbox with 0 0 100 100 to ensure that the positions and size of the
|
||||||
|
* circles can be represnted in percent.
|
||||||
|
*
|
||||||
|
* @class Highlight
|
||||||
|
* @extends {Object}
|
||||||
|
*/
|
||||||
|
class Highlight extends Object {
|
||||||
|
|
||||||
|
static disableAnimations() {
|
||||||
|
_HighlightEnabled = false
|
||||||
|
let expanded = document.querySelectorAll('.expanded')
|
||||||
|
for (let obj of expanded) {
|
||||||
|
this.shrink(obj)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static enableAnimations() {
|
||||||
|
_HighlightEnabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
static removeAnimations(svgRoot) {
|
||||||
|
let expanded = svgRoot.querySelectorAll('.expanded')
|
||||||
|
for (let obj of expanded) {
|
||||||
|
TweenLite.set(obj, { scale: 1 })
|
||||||
|
obj.classList.remove('zooming')
|
||||||
|
obj.classList.remove('expanded')
|
||||||
|
}
|
||||||
|
let defs = svgRoot.querySelector('defs')
|
||||||
|
while (defs.firstChild) {
|
||||||
|
defs.firstChild.remove()
|
||||||
|
}
|
||||||
|
let maskImages = svgRoot.querySelectorAll('.addedImage')
|
||||||
|
for (let m of maskImages) {
|
||||||
|
m.remove()
|
||||||
|
}
|
||||||
|
let circles = svgRoot.querySelectorAll('circle')
|
||||||
|
for (let circle of circles) {
|
||||||
|
if (circle.classList.length == 0) {
|
||||||
|
circle.removeAttribute('class')
|
||||||
|
}
|
||||||
|
if (circle.hasAttribute('id') && circle.getAttribute('id').startsWith('@@')) {
|
||||||
|
circle.removeAttribute('id')
|
||||||
|
}
|
||||||
|
circle.removeAttribute('data-svg-origin')
|
||||||
|
circle.removeAttribute('transform')
|
||||||
|
circle.removeAttribute('style')
|
||||||
|
let cx = circle.getAttribute('cx')
|
||||||
|
let cy = circle.getAttribute('cy')
|
||||||
|
let r = circle.getAttribute('r')
|
||||||
|
circle.setAttribute('cx', round(cx))
|
||||||
|
circle.setAttribute('cy', round(cy))
|
||||||
|
circle.setAttribute('r', round(r))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static expand(obj, { scale = 2, duration = 3, stroke = 2, onComplete = null } = {}) {
|
||||||
|
if (obj == null)
|
||||||
|
return
|
||||||
|
//console.log("expand")
|
||||||
|
obj.classList.add('zooming')
|
||||||
|
TweenLite.to(obj, duration, {
|
||||||
|
scale: scale,
|
||||||
|
onUpdate: () => {
|
||||||
|
let scale = obj._gsTransform.scaleX
|
||||||
|
obj.setAttribute('stroke-width', stroke / scale)
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
console.log('expand complete')
|
||||||
|
obj.classList.remove('zooming')
|
||||||
|
obj.classList.add('expanded')
|
||||||
|
obj.setAttribute('stroke-width', stroke / scale)
|
||||||
|
if (onComplete) onComplete()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static shrink(obj, { duration = 0.5, stroke = 2 } = {}) {
|
||||||
|
//console.log("shrink")
|
||||||
|
if (obj == null)
|
||||||
|
return
|
||||||
|
obj.classList.add('zooming')
|
||||||
|
TweenLite.to(obj, duration, {
|
||||||
|
scale: 1,
|
||||||
|
onUpdate: () => {
|
||||||
|
let scale = obj._gsTransform.scaleX
|
||||||
|
obj.setAttribute('stroke-width', stroke / scale)
|
||||||
|
},
|
||||||
|
onComplete: () => {
|
||||||
|
//console.log("shrink complete")
|
||||||
|
obj.classList.remove('zooming')
|
||||||
|
obj.classList.remove('expanded')
|
||||||
|
obj.setAttribute('stroke-width', stroke)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static animateCircle(target, callback) {
|
||||||
|
console.log('ANIMATE CIRCLE', this)
|
||||||
|
// ** DEBUG OUTPUTS **
|
||||||
|
|
||||||
|
let circle = target
|
||||||
|
// We need a unique id to ensure correspondence between circle, mask, and maskImage
|
||||||
|
if (!circle.hasAttribute('id')) {
|
||||||
|
_CircleIds += 1
|
||||||
|
circle.setAttribute('id', '@@' + _CircleIds)
|
||||||
|
}
|
||||||
|
let id = circle.getAttribute('id')
|
||||||
|
TweenLite.set(circle, { transformOrigin: '50% 50%' })
|
||||||
|
/*if (circle.classList.contains('zooming')) {
|
||||||
|
console.log("already zooming")
|
||||||
|
return
|
||||||
|
}*/
|
||||||
|
|
||||||
|
let svgRoot = circle.closest('svg')
|
||||||
|
let circleGroup = circle.parentNode
|
||||||
|
let image = svgRoot.querySelector('image')
|
||||||
|
|
||||||
|
|
||||||
|
let stroke = parseFloat(circleGroup.getAttribute('stroke-width') || 6)
|
||||||
|
|
||||||
|
let defs = svgRoot.querySelector('defs')
|
||||||
|
if (defs == null) {
|
||||||
|
defs = document.createElementNS(svgRoot, 'defs')
|
||||||
|
svgRoot.insertBefore(defs, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
// // We need direct children, therefore we cannot use querySelectorAll
|
||||||
|
let maskImageId = 'maskImage' + id
|
||||||
|
let maskImage = svgRoot.getElementById(maskImageId)
|
||||||
|
|
||||||
|
if (circle.classList.contains('expanded')) {
|
||||||
|
if (!circle.classList.contains('zooming')) {
|
||||||
|
this.shrink(circle, { stroke })
|
||||||
|
this.shrink(maskImage, { stroke })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
//console.log("animate called while zooming out -> expand")
|
||||||
|
}
|
||||||
|
else if (circle.classList.contains('zooming')) {
|
||||||
|
//console.log("animate called while zooming in -> shrink")
|
||||||
|
this.shrink(circle, { stroke })
|
||||||
|
this.shrink(maskImage, { stroke })
|
||||||
|
return
|
||||||
|
}
|
||||||
|
let circles = Array.from(circleGroup.children).filter(e => e.tagName == 'circle')
|
||||||
|
for (let c of circles) {
|
||||||
|
//console.log("shrinking all circles")
|
||||||
|
this.shrink(c, { stroke })
|
||||||
|
}
|
||||||
|
let maskImages = circleGroup.querySelectorAll('.addedImage')
|
||||||
|
for (let m of maskImages) {
|
||||||
|
this.shrink(m, { stroke })
|
||||||
|
}
|
||||||
|
|
||||||
|
Highlight._createSVGMask(svgRoot, image, id)
|
||||||
|
|
||||||
|
// TweenLite.set(maskImage, { transformOrigin: `${tx}% ${ty}%` })
|
||||||
|
|
||||||
|
this.expand(circle, { stroke, onComplete: callback })
|
||||||
|
this.expand(maskImage)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
static openHighlight(target, {
|
||||||
|
animation = 0.5,
|
||||||
|
scale = 2,
|
||||||
|
onExpanded = null
|
||||||
|
} = {}) {
|
||||||
|
|
||||||
|
console.log('Open Highlight!')
|
||||||
|
|
||||||
|
if (Highlight._isExpanded(target)) {
|
||||||
|
console.log('Target is already expanded!')
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let targetId = target.getAttribute('id')
|
||||||
|
console.log(targetId)
|
||||||
|
if(targetId && targetId.startsWith('@@')){
|
||||||
|
let id = targetId.slice(2)
|
||||||
|
const imageId = '#maskImage'+id
|
||||||
|
const parent = target.parentNode
|
||||||
|
if(parent != null){
|
||||||
|
let image = parent.querySelector(imageId)
|
||||||
|
if(image){
|
||||||
|
this._bringToFront(image)
|
||||||
|
}else console.error('Could not find corresponding image element.')
|
||||||
|
}else console.log('Element was no parent:', target)
|
||||||
|
}
|
||||||
|
|
||||||
|
this._bringToFront(target)
|
||||||
|
|
||||||
|
let svgRoot = target.closest('svg')
|
||||||
|
let image = svgRoot.querySelector('image')
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let [mask, maskImage] = Highlight._getSVGMask(target, { svgRoot, image })
|
||||||
|
let center = Highlight._calculateCenterRelativeTo(target, image)
|
||||||
|
|
||||||
|
TweenLite.set(maskImage, { transformOrigin: `${center.x}% ${center.y}%` })
|
||||||
|
TweenLite.set(target, { transformOrigin: '50% 50%' })
|
||||||
|
|
||||||
|
TweenLite.to(target, 2, {
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
TweenLite.to([target, maskImage], animation, {
|
||||||
|
scale,
|
||||||
|
onComplete: onExpanded
|
||||||
|
})
|
||||||
|
|
||||||
|
target.classList.add('expanded')
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
static _bringToFront(target){
|
||||||
|
const parent = target.parentNode
|
||||||
|
if(target && parent){
|
||||||
|
parent.removeChild(target)
|
||||||
|
parent.appendChild(target)
|
||||||
|
}else console.error('Could not bring to front. Either no target or no parent.', target, parent)
|
||||||
|
}
|
||||||
|
|
||||||
|
static _getSVGMask(circle, { svgRoot = null, image = null } = {}) {
|
||||||
|
const id = this._retrieveId(circle)
|
||||||
|
const maskId = 'mask' + id
|
||||||
|
const maskImageId = 'maskImage' + id
|
||||||
|
|
||||||
|
if (!svgRoot) svgRoot = circle.closest('svg')
|
||||||
|
|
||||||
|
let mask = svgRoot.getElementById(maskId)
|
||||||
|
let maskImage = svgRoot.getElementById(maskImageId)
|
||||||
|
|
||||||
|
if (!mask || !maskImage)
|
||||||
|
[mask, maskImage] = Highlight._createSVGMask(circle, { svgRoot, image, id })
|
||||||
|
|
||||||
|
return [mask, maskImage]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates an SVG mask for a provided svgElement.
|
||||||
|
*
|
||||||
|
* @static
|
||||||
|
* @param {SVGElement} element - Element that should be masked.
|
||||||
|
* @param {object} opts - Optional parameters to avoid unnecessary fetching of elements.
|
||||||
|
* @param {SVGElement} opts.svgRoot - The root <svg> element of the element.
|
||||||
|
* @param {SVGImageElement} opts.image - The image that is used in the mask.
|
||||||
|
* @param {number} opts.id - The id of the mask.
|
||||||
|
* @returns
|
||||||
|
* @memberof Highlight
|
||||||
|
*/
|
||||||
|
static _createSVGMask(element, { svgRoot = null, image = null, id = null } = {}) {
|
||||||
|
|
||||||
|
// We can fetch these values here, but it's more efficient to
|
||||||
|
// simply pass them in, as it's likely they were already retrieved beforehand.
|
||||||
|
if (svgRoot == null) svgRoot = element.closest('svg')
|
||||||
|
if (image == null) image = svgRoot.querySelector('image')
|
||||||
|
if (id == null) id = this._retrieveId(element)
|
||||||
|
|
||||||
|
let svg = 'http://www.w3.org/2000/svg'
|
||||||
|
let xlink = 'http://www.w3.org/1999/xlink'
|
||||||
|
|
||||||
|
let svgGroup = element.parentNode
|
||||||
|
|
||||||
|
let src = image.getAttributeNS(xlink, 'href')
|
||||||
|
|
||||||
|
let maskId = 'mask' + id
|
||||||
|
let maskImageId = 'maskImage' + id
|
||||||
|
let mask = svgRoot.getElementById(maskId)
|
||||||
|
let maskImage = svgRoot.getElementById(maskImageId)
|
||||||
|
|
||||||
|
let defs = svgRoot.querySelector('defs')
|
||||||
|
if (defs == null) {
|
||||||
|
defs = document.createElementNS(svgRoot, 'defs')
|
||||||
|
svgRoot.insertBefore(defs, image)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mask == null) {
|
||||||
|
mask = document.createElementNS(svg, 'mask')
|
||||||
|
mask.setAttribute('id', maskId)
|
||||||
|
let maskCircle = element.cloneNode(true)
|
||||||
|
mask.appendChild(maskCircle)
|
||||||
|
defs.appendChild(mask)
|
||||||
|
}
|
||||||
|
|
||||||
|
let bbox = svgRoot.getElementsByTagName('image')[0].getBBox()
|
||||||
|
let width = bbox.width
|
||||||
|
let height = bbox.height
|
||||||
|
|
||||||
|
if (maskImage == null) {
|
||||||
|
|
||||||
|
maskImage = document.createElementNS(svg, 'image')
|
||||||
|
maskImage.style.pointerEvents = 'none'
|
||||||
|
maskImage.setAttribute('id', maskImageId)
|
||||||
|
maskImage.setAttributeNS(xlink, 'href', src)
|
||||||
|
maskImage.setAttribute('width', width)
|
||||||
|
maskImage.setAttribute('height', height)
|
||||||
|
maskImage.setAttribute('class', 'addedImage')
|
||||||
|
svgGroup.insertBefore(maskImage, element) // image.nextSibling)
|
||||||
|
TweenLite.set(maskImage, { scale: 1 })
|
||||||
|
maskImage.style.mask = 'url(#' + maskId + ')'
|
||||||
|
}
|
||||||
|
|
||||||
|
svgGroup.appendChild(maskImage)
|
||||||
|
// svgGroup.appendChild(element)
|
||||||
|
|
||||||
|
return [mask, maskImage]
|
||||||
|
}
|
||||||
|
|
||||||
|
static _calculateCenterRelativeTo(target, image) {
|
||||||
|
let bbox = image.getBBox()
|
||||||
|
let width = bbox.width
|
||||||
|
let height = bbox.height
|
||||||
|
let cx = target.getAttribute('cx')
|
||||||
|
let cy = target.getAttribute('cy')
|
||||||
|
return { x: (cx / width) * 100, y: (cy / height) * 100 }
|
||||||
|
}
|
||||||
|
|
||||||
|
static _isExpanded(target) {
|
||||||
|
return target.classList.contains(Highlight.expandedClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
static _setExpanded(target) {
|
||||||
|
target.classList.add(Highlight.expandedClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
static _notExpanded(target) {
|
||||||
|
target.classList.remove(Highlight.expandedClass)
|
||||||
|
}
|
||||||
|
|
||||||
|
static closeHighlight(target, { animation = 0.5 } = {}) {
|
||||||
|
console.log('Close Highlight')
|
||||||
|
Highlight._notExpanded(target)
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
let [mask, maskImage] = Highlight._getSVGMask(target)
|
||||||
|
|
||||||
|
TweenLite.to([target, maskImage], animation, {
|
||||||
|
scale: 1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
static animate(event) {
|
||||||
|
if (!_HighlightEnabled)
|
||||||
|
return
|
||||||
|
|
||||||
|
event.stopPropagation()
|
||||||
|
Highlight.animateCircle(event.target)
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
static _retrieveId(target) {
|
||||||
|
let id = target.getAttribute('id')
|
||||||
|
// We need a unique id to ensure correspondence between circle, mask, and maskImage
|
||||||
|
if (!id) {
|
||||||
|
_CircleIds += 1
|
||||||
|
target.setAttribute('id', '@@' + _CircleIds)
|
||||||
|
id = _CircleIds
|
||||||
|
} else {
|
||||||
|
id = parseInt(id.substring(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
return id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Highlight.expandedClass = 'expanded'
|
||||||
|
window.Highlight = Highlight
|
107
lib/card/index.html
Normal file
107
lib/card/index.html
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||||
|
<title>Doctests Cards</title>
|
||||||
|
<link rel="stylesheet" href="../3rdparty/highlight/styles/default.css">
|
||||||
|
<link rel="stylesheet" href="../../css/doctest.css">
|
||||||
|
<script src="../3rdparty/highlight/highlight.pack.js"></script>
|
||||||
|
<script src="../../dist/iwmlib.js"></script>
|
||||||
|
<script src="../../dist/iwmlib.3rdparty.js"></script>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body onload="Doctest.run();">
|
||||||
|
<h1>
|
||||||
|
Cards
|
||||||
|
</h1>
|
||||||
|
<p>
|
||||||
|
Cards implement a central UI metaphor for multiuser applications. They allow users to explore information spaces
|
||||||
|
independently from each other. Most of the time a card lives within a scatter and can be moved, rotated, and
|
||||||
|
scaled.
|
||||||
|
The scatter and it's interaction mapper are also responsible to detect and distinguish tap events form other
|
||||||
|
interactions.
|
||||||
|
But in preview programs and editors this is not necessary. Therefore we provide a central CardWrapper class that
|
||||||
|
turns any DOM node into a card.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
The wrapper's main task is to handle tap events and trigger the corresponding actions. There are three main
|
||||||
|
ways to define these actions.
|
||||||
|
<ol>
|
||||||
|
<li>Define onclick handlers in HTML and SVG, e.g. <code><a onclick="alert(1)">link<a></code>
|
||||||
|
</li>
|
||||||
|
<li>Use onTap with a DOM node as event target, e.g. <code>wrapper.onTap(node, event => {})</code></li>
|
||||||
|
<li>Use onTap with a CSS selector, e.g. <code>wrapper.onTap('.link', event => {})</code></li>
|
||||||
|
</li>
|
||||||
|
</ol>
|
||||||
|
The order in which these possibilities are testet is 1, 2, 3.
|
||||||
|
</p>
|
||||||
|
<p>
|
||||||
|
Note that the objects can also be actived by clicking nearby and not directly on the DOM node.
|
||||||
|
This solves a major problem on large tabletops with a parallaxis in the display.
|
||||||
|
</p>
|
||||||
|
<div class="interactive grayBorder" style="position: relative;">
|
||||||
|
<article id="demoCardWithOnClick"
|
||||||
|
style="position: relative; left: 0%; padding: 16px; margin: 16px; border: 1px solid gray; width: 320px; height: 240px;">
|
||||||
|
<h1 style="color: gray;">A Demo Card with onclick</h1>
|
||||||
|
<figure style="position: relative;">
|
||||||
|
<img width="75%" src="../examples/women.jpeg">
|
||||||
|
<svg id="overlayBase" style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
|
||||||
|
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<circle cx="35" cy="50" r="35" class="highlight" onclick="alert('Highlight clicked')" stroke="white"
|
||||||
|
fill="transparent" stroke-width="1" />
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
<p>Lorem ipsum <a class="link" style="color:blue;" onclick="alert('Link clicked')">dolor</a> sit amet,
|
||||||
|
consetetur sadipscing elitr.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
<article id="demoCardWithSelector"
|
||||||
|
style="position: absolute; left: 50%; top: 0%; padding: 16px; margin: 16px; border: 1px solid gray; width: 320px; height: 240px;">
|
||||||
|
<h1 style="color: gray;">A Demo Card with selectors</h1>
|
||||||
|
<figure style="position: relative;">
|
||||||
|
<img width="75%" src="../examples/king.jpeg">
|
||||||
|
<svg id="overlayBase" style="position: absolute; left: 0px; top: 0px; width: 100%; height: 100%;"
|
||||||
|
viewBox="0 0 100 100" preserveAspectRatio="xMidYMid meet">
|
||||||
|
<circle cx="35" cy="50" r="35" class="highlight" stroke="white" fill="transparent"
|
||||||
|
stroke-width="1" />
|
||||||
|
</svg>
|
||||||
|
</figure>
|
||||||
|
<p>Lorem ipsum <a class="link" href="javascript:alert('Link clicked via href')" style="color:blue;">dolor</a> sit
|
||||||
|
amet,
|
||||||
|
consetetur sadipscing elitr.</p>
|
||||||
|
</article>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script class="doctest">
|
||||||
|
|
||||||
|
const wrapper1 = new CardWrapper(demoCardWithOnClick)
|
||||||
|
wrapper1.handleClicksAsTaps()
|
||||||
|
|
||||||
|
const wrapper2 = new CardWrapper(demoCardWithSelector)
|
||||||
|
wrapper2.handleClicksAsTaps()
|
||||||
|
wrapper2.onTap('.highlight', event => {
|
||||||
|
alert('.highlight clicked')
|
||||||
|
})
|
||||||
|
wrapper2.onTap('.link', event => {
|
||||||
|
alert('.link clicked')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
Using Cards within Scatters
|
||||||
|
</h2>
|
||||||
|
<p>Cards can be used within scatters. Since the CardWrapper implements the TapDelegate protocol they can simply
|
||||||
|
be attached to a DOMScatter object.
|
||||||
|
</p>
|
||||||
|
<div class="interactive grayBorder" style="position: relative;">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h2>
|
||||||
|
References
|
||||||
|
</h2>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://uicookies.com/css-card-design/">30 Visually Stunning CSS Cards Inspirations For Every Type Of Websites 2019</a></li>
|
||||||
|
</ul>
|
||||||
|
</body>
|
187
lib/card/wrapper.js
Normal file
187
lib/card/wrapper.js
Normal file
@ -0,0 +1,187 @@
|
|||||||
|
/* eslint-disable no-console */
|
||||||
|
/* global TweenLite */
|
||||||
|
|
||||||
|
import Events from '../events.js'
|
||||||
|
import { Points } from '../utils.js'
|
||||||
|
|
||||||
|
export class CardWrapper extends Object {
|
||||||
|
|
||||||
|
constructor(domNode, { triggerSVGClicks = true, allowClickDistance = 44 } = {}) {
|
||||||
|
super()
|
||||||
|
this.domNode = domNode
|
||||||
|
this.triggerSVGClicks = triggerSVGClicks
|
||||||
|
this.allowClickDistance = allowClickDistance
|
||||||
|
this.tapNodes = new Map()
|
||||||
|
this.tapHandler = new Map()
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClicks() {
|
||||||
|
this.domNode.addEventListener('click', event => {
|
||||||
|
console.log('handleClicks', event.isTrusted)
|
||||||
|
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
||||||
|
Therefore we make an exception and let the original click event through.
|
||||||
|
*/
|
||||||
|
|
||||||
|
if (event.isTrusted) {
|
||||||
|
Events.stop(event)
|
||||||
|
if (this.triggerSVGClicks && this.isSVGNode(event.target)) {
|
||||||
|
this.tap(event, "triggerSVGClicks")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
handleClicksAsTaps() {
|
||||||
|
this.domNode.addEventListener('click', event => {
|
||||||
|
console.log('handleClicksAsTaps', event.isTrusted)
|
||||||
|
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
||||||
|
Therefore we make an exception and let the original click event through.
|
||||||
|
*/
|
||||||
|
if (event.isTrusted) {
|
||||||
|
Events.stop(event)
|
||||||
|
|
||||||
|
}
|
||||||
|
this.tap(event)
|
||||||
|
}, true)
|
||||||
|
}
|
||||||
|
|
||||||
|
isClickable(node) {
|
||||||
|
if (node == null)
|
||||||
|
return false
|
||||||
|
if (node.tagName == 'A' && node.hasAttribute("href"))
|
||||||
|
return true
|
||||||
|
if (node.hasAttribute("onclick"))
|
||||||
|
return true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
hasClickHandler(node) {
|
||||||
|
if (node == null)
|
||||||
|
return false
|
||||||
|
if (this.tapNodes.has(node))
|
||||||
|
return true
|
||||||
|
for (let [selector, handler] of this.tapHandler.entries()) {
|
||||||
|
console.log("selector", this.domNode.querySelectorAll(selector))
|
||||||
|
for (let obj of this.domNode.querySelectorAll(selector)) {
|
||||||
|
console.log("selector2", node, obj)
|
||||||
|
if (node == obj) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of all active nodes.
|
||||||
|
* Unfortunately we cannot search for all nodes with an attached 'click' event listener
|
||||||
|
* See https://stackoverflow.com/questions/11455515/how-to-check-whether-dynamically-attached-event-listener-exists-or-not
|
||||||
|
* Therefore we can only detect the following standard cases:
|
||||||
|
* I. All clickable objects like activeNodes
|
||||||
|
* II. Objects that have been attached a click handler by the scatter itself via
|
||||||
|
*/
|
||||||
|
activeNodes() {
|
||||||
|
let result = []
|
||||||
|
for (let node of this.domNode.querySelectorAll("*")) {
|
||||||
|
if (this.isClickable(node))
|
||||||
|
result.push(node)
|
||||||
|
if (this.hasClickHandler(node))
|
||||||
|
result.push(node)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
nearestActive(event) {
|
||||||
|
let element = this.domNode
|
||||||
|
let activeNodes = this.activeNodes()
|
||||||
|
let globalClick = (event.center) ? event.center : { x: event.x, y: event.y }
|
||||||
|
let localClick = Points.fromPageToNode(element, globalClick)
|
||||||
|
|
||||||
|
let clickRects = activeNodes.map(link => {
|
||||||
|
let rect = link.getBoundingClientRect()
|
||||||
|
let topLeft = Points.fromPageToNode(element, rect)
|
||||||
|
let center = Points.fromPageToNode(element, { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 })
|
||||||
|
return { x: topLeft.x, y: topLeft.y, width: rect.width, height: rect.height, center, link }
|
||||||
|
})
|
||||||
|
|
||||||
|
let distances = []
|
||||||
|
clickRects.forEach(rect => {
|
||||||
|
let distance = Points.distanceToRect(localClick, rect)
|
||||||
|
distances.push(parseInt(distance))
|
||||||
|
})
|
||||||
|
|
||||||
|
let closestClickIndex = distances.indexOf(Math.min(...distances))
|
||||||
|
let closestClickable = activeNodes[closestClickIndex]
|
||||||
|
if (distances[closestClickIndex] < this.allowClickDistance) {
|
||||||
|
return closestClickable
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
isSVGNode(node) {
|
||||||
|
return node.ownerSVGElement || node.tagName == 'svg'
|
||||||
|
}
|
||||||
|
|
||||||
|
simulateClick(node, event) {
|
||||||
|
/* https://stackoverflow.com/questions/49564905/is-it-possible-to-use-click-function-on-svg-tags-i-tried-element-click-on-a
|
||||||
|
proposes the dispatchEvent solution. But this leads to problems in flippable.html hiding the back page.
|
||||||
|
Therefore we use the original click event (see constructor). */
|
||||||
|
console.log("simulateClick", node, node.ownerSVGElement)
|
||||||
|
if (this.isSVGNode(node)) {
|
||||||
|
if (this.triggerSVGClicks) {
|
||||||
|
let click = new Event('click')
|
||||||
|
node.dispatchEvent(click)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
node.click()
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeTapped(node, event) {
|
||||||
|
if (this.isClickable(node)) {
|
||||||
|
this.simulateClick(node, event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
if (this.tapNodes.has(node)) {
|
||||||
|
handler = this.tapNodes.get(node)
|
||||||
|
handler(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for (let [selector, handler] of this.tapHandler.entries()) {
|
||||||
|
for (let obj of this.domNode.querySelectorAll(selector)) {
|
||||||
|
if (node == obj) {
|
||||||
|
handler(event)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
tap(event, calledBy='unknown') {
|
||||||
|
console.log("tap", calledBy, event.alreadyTapped, event)
|
||||||
|
if (event.isTrusted) {
|
||||||
|
let node = this.nearestActive(event)
|
||||||
|
this.nodeTapped(node, event)
|
||||||
|
|
||||||
|
/* let node = document.elementFromPoint(event.clientX, event.clientY)
|
||||||
|
if (!this.nodeTapped(node, event)) {
|
||||||
|
node = this.nearestActive(event)
|
||||||
|
this.nodeTapped(node, event)
|
||||||
|
} */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onTap(objOrSelector, handler) {
|
||||||
|
if (typeof (objOrSelector) == 'string') {
|
||||||
|
this.tapHandler.set(objOrSelector, handler)
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.tapNodes.set(objOrSelector, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
window.CardWrapper = CardWrapper
|
@ -68,11 +68,12 @@ templates.
|
|||||||
<script class="doctest">
|
<script class="doctest">
|
||||||
let scatterContainer = new DOMScatterContainer(main, {stopEvents: false})
|
let scatterContainer = new DOMScatterContainer(main, {stopEvents: false})
|
||||||
if (Capabilities.supportsTemplate()) {
|
if (Capabilities.supportsTemplate()) {
|
||||||
|
|
||||||
let flip = new DOMFlip(scatterContainer,
|
let flip = new DOMFlip(scatterContainer,
|
||||||
flipTemplate,
|
flipTemplate,
|
||||||
new ImageLoader('./examples/king.jpeg'),
|
new ImageLoader('./examples/king.jpeg'),
|
||||||
new ImageLoader('./examples/women.jpeg'),
|
new ImageLoader('./examples/women.jpeg'),
|
||||||
{ clickOnTap: true})
|
{ tapDelegateFactory: CardWrapper})
|
||||||
flip.load().then((flip) => {
|
flip.load().then((flip) => {
|
||||||
flip.centerAt({ x: 150, y: 120})
|
flip.centerAt({ x: 150, y: 120})
|
||||||
})
|
})
|
||||||
|
@ -199,7 +199,7 @@ export class DOMFlip {
|
|||||||
translatable = true,
|
translatable = true,
|
||||||
scalable = true,
|
scalable = true,
|
||||||
rotatable = true,
|
rotatable = true,
|
||||||
clickOnTap = false,
|
tapDelegateFactory = null,
|
||||||
onFront = null,
|
onFront = null,
|
||||||
onBack = null,
|
onBack = null,
|
||||||
onClose = null,
|
onClose = null,
|
||||||
@ -219,7 +219,7 @@ export class DOMFlip {
|
|||||||
this.translatable = translatable
|
this.translatable = translatable
|
||||||
this.scalable = scalable
|
this.scalable = scalable
|
||||||
this.rotatable = rotatable
|
this.rotatable = rotatable
|
||||||
this.clickOnTap = clickOnTap
|
this.tapDelegateFactory = tapDelegateFactory
|
||||||
this.onFrontFlipped = onFront
|
this.onFrontFlipped = onFront
|
||||||
this.onBackFlipped = onBack
|
this.onBackFlipped = onBack
|
||||||
this.onClose = onClose
|
this.onClose = onClose
|
||||||
@ -275,7 +275,7 @@ export class DOMFlip {
|
|||||||
scalable: this.scalable,
|
scalable: this.scalable,
|
||||||
rotatable: this.rotatable,
|
rotatable: this.rotatable,
|
||||||
overdoScaling: this.overdoScaling,
|
overdoScaling: this.overdoScaling,
|
||||||
clickOnTap: this.clickOnTap
|
tapDelegate: (this.tapDelegateFactory) ? this.tapDelegateFactory(this.cardWrapper) : null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -89,8 +89,8 @@ to simulate click events by using taps. If the scatter detects a tap it looks fo
|
|||||||
elements under or nearby the event position and calls the click handler. Thus gestures
|
elements under or nearby the event position and calls the click handler. Thus gestures
|
||||||
can be disambiguated as moves, zooms. or taps.
|
can be disambiguated as moves, zooms. or taps.
|
||||||
|
|
||||||
Note that on touch devices you can tap beside the object if you use clickOnTap. The allowed distance
|
Note that on touch devices you can tap beside the object if you use an object that implements the ITapDelegate interface.
|
||||||
can be configured by allowClickDistance. The default value is 44px.
|
An ITapDelegate allowes a distance that can be configured by allowClickDistance. The default value is 44px.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div id="contentExample" class="grayBorder interactive" style="position: relative; width: 100%; height: 280px;">
|
<div id="contentExample" class="grayBorder interactive" style="position: relative; width: 100%; height: 280px;">
|
||||||
@ -110,14 +110,15 @@ can be configured by allowClickDistance. The default value is 44px.
|
|||||||
<script class="doctest">
|
<script class="doctest">
|
||||||
|
|
||||||
let contentContainer = new DOMScatterContainer(contentExample)
|
let contentContainer = new DOMScatterContainer(contentExample)
|
||||||
|
|
||||||
|
let tapDelegate = new CardWrapper(interactiveContent)
|
||||||
new DOMScatter(interactiveContent, contentContainer, {
|
new DOMScatter(interactiveContent, contentContainer, {
|
||||||
x: 44,
|
x: 44,
|
||||||
y: 44,
|
y: 44,
|
||||||
width: 274,
|
width: 274,
|
||||||
height: 184,
|
height: 184,
|
||||||
clickOnTap: true,
|
tapDelegate,
|
||||||
throwVisibility: 88,
|
throwVisibility: 88,
|
||||||
triggerSVGClicks: true,
|
|
||||||
minScale: 0.5,
|
minScale: 0.5,
|
||||||
maxScale: 1.5})
|
maxScale: 1.5})
|
||||||
|
|
||||||
|
146
lib/scatter.js
146
lib/scatter.js
@ -1,11 +1,26 @@
|
|||||||
/* eslint-disable no-unused-vars */
|
/* eslint-disable no-unused-vars */
|
||||||
/* eslint-disable no-console */
|
/* eslint-disable no-console */
|
||||||
/* globals TweenLite debugCanvas */
|
/* globals TweenLite debugCanvas */
|
||||||
|
import Interface from './interface.js'
|
||||||
import { Points, Polygon, Angle, Elements, LowPassFilter } from './utils.js'
|
import { Points, Polygon, Angle, Elements, LowPassFilter } from './utils.js'
|
||||||
import Events from './events.js'
|
import Events from './events.js'
|
||||||
import { InteractionMapper } from './interaction.js'
|
import { InteractionMapper } from './interaction.js'
|
||||||
import { Capabilities } from './capabilities.js'
|
import { Capabilities } from './capabilities.js'
|
||||||
|
|
||||||
|
/** This interface allows scatters to delegate tap events to other objects. */
|
||||||
|
export class ITapDelegate extends Interface {
|
||||||
|
|
||||||
|
/** This method must be defined by the delegate. It handles the tap event. */
|
||||||
|
tap(event) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Tells the delegate that it should handle standard click events. */
|
||||||
|
handleClicks() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A base class for scatter specific events.
|
* A base class for scatter specific events.
|
||||||
*
|
*
|
||||||
@ -1035,6 +1050,7 @@ export class DOMScatterContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export class DOMScatter extends AbstractScatter {
|
export class DOMScatter extends AbstractScatter {
|
||||||
constructor(
|
constructor(
|
||||||
element,
|
element,
|
||||||
@ -1060,7 +1076,7 @@ export class DOMScatter extends AbstractScatter {
|
|||||||
width = null, // required
|
width = null, // required
|
||||||
height = null, // required
|
height = null, // required
|
||||||
resizable = false,
|
resizable = false,
|
||||||
clickOnTap = false,
|
tapDelegate = null,
|
||||||
triggerSVGClicks = false,
|
triggerSVGClicks = false,
|
||||||
allowClickDistance = 44,
|
allowClickDistance = 44,
|
||||||
verbose = true,
|
verbose = true,
|
||||||
@ -1117,8 +1133,7 @@ export class DOMScatter extends AbstractScatter {
|
|||||||
this.height = height
|
this.height = height
|
||||||
this.throwVisibility = Math.min(width, height, throwVisibility)
|
this.throwVisibility = Math.min(width, height, throwVisibility)
|
||||||
this.container = container
|
this.container = container
|
||||||
this.clickOnTap = clickOnTap
|
this.tapDelegate = tapDelegate
|
||||||
this.triggerSVGClicks = triggerSVGClicks
|
|
||||||
this.scale = startScale
|
this.scale = startScale
|
||||||
this.rotationDegrees = this.startRotationDegrees
|
this.rotationDegrees = this.startRotationDegrees
|
||||||
this.transformOrigin = transformOrigin
|
this.transformOrigin = transformOrigin
|
||||||
@ -1132,7 +1147,6 @@ export class DOMScatter extends AbstractScatter {
|
|||||||
transformOrigin: transformOrigin
|
transformOrigin: transformOrigin
|
||||||
}
|
}
|
||||||
this.tapNodes = new Map()
|
this.tapNodes = new Map()
|
||||||
this.allowClickDistance = allowClickDistance
|
|
||||||
|
|
||||||
// For tweenlite we need initial values in _gsTransform
|
// For tweenlite we need initial values in _gsTransform
|
||||||
TweenLite.set(element, this.initialValues)
|
TweenLite.set(element, this.initialValues)
|
||||||
@ -1165,24 +1179,8 @@ export class DOMScatter extends AbstractScatter {
|
|||||||
})
|
})
|
||||||
this.resizeButton = button
|
this.resizeButton = button
|
||||||
}
|
}
|
||||||
if (clickOnTap) {
|
if (tapDelegate) {
|
||||||
/* Since the tap triggers a synthetic click event
|
tapDelegate.handleClicks()
|
||||||
we must prevent the original trusted click event which
|
|
||||||
is also dispatched by the system.
|
|
||||||
*/
|
|
||||||
element.addEventListener('click', event => {
|
|
||||||
/* Currently we cannot send synthesized click events to SVG elements without unwanted side effects.
|
|
||||||
Therefore we make an exception and let the original click event through.
|
|
||||||
*/
|
|
||||||
if (event.target.ownerSVGElement) {
|
|
||||||
if (this.triggerSVGClicks) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else if (event.isTrusted) {
|
|
||||||
Events.stop(event)
|
|
||||||
}
|
|
||||||
}, true)
|
|
||||||
}
|
}
|
||||||
container.add(this)
|
container.add(this)
|
||||||
}
|
}
|
||||||
@ -1344,110 +1342,12 @@ export class DOMScatter extends AbstractScatter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onTap(event, interaction, point) {
|
onTap(event, interaction, point) {
|
||||||
|
if (this.tapDelegate) {
|
||||||
if (this.clickOnTap) {
|
Events.stop(event)
|
||||||
let directNode = document.elementFromPoint(event.clientX, event.clientY)
|
this.tapDelegate.tap(event, "scatter")
|
||||||
console.log("onTap", event)
|
|
||||||
if (this.isClickable(directNode)) {
|
|
||||||
directNode.click()
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let nearestNode = this.nearestClickable(event)
|
|
||||||
if (this.isClickable(nearestNode)) {
|
|
||||||
/* https://stackoverflow.com/questions/49564905/is-it-possible-to-use-click-function-on-svg-tags-i-tried-element-click-on-a
|
|
||||||
proposes the dispatchEvent solution. But this leads to problems in flippable.html hiding the back page.
|
|
||||||
Therefore we use the original click event (see constructor).
|
|
||||||
*/
|
|
||||||
if (nearestNode.tagName == 'svg') {
|
|
||||||
let handler = this.tapNodes.get(nearestNode)
|
|
||||||
console.log("Clicking near SVG: to be done", handler)
|
|
||||||
if (this.triggerSVGClicks)
|
|
||||||
nearestNode.dispatchEvent(new Event('click'))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
console.log("nearestNode clicked")
|
|
||||||
nearestNode.click()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a click or tap behavior to the node. Uses
|
|
||||||
* either the scatter clickOnTap version which requires click handlers
|
|
||||||
* or uses the hammer.js driven tap handler.
|
|
||||||
*
|
|
||||||
* @param {*} node
|
|
||||||
* @param {*} handler
|
|
||||||
* @memberof DOMScatter
|
|
||||||
*/
|
|
||||||
|
|
||||||
addTapListener(node, handler) {
|
|
||||||
if (this.clickOnTap) {
|
|
||||||
node.addEventListener('click', handler)
|
|
||||||
this.tapNodes.set(node, handler)
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
InteractionMapper.on('tap', node, handler)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isClickable(node) {
|
|
||||||
if (node == null)
|
|
||||||
return false
|
|
||||||
if (node.tagName == 'A')
|
|
||||||
return true
|
|
||||||
if (node.hasAttribute("onclick"))
|
|
||||||
return true
|
|
||||||
if (this.tapNodes.has(node))
|
|
||||||
return true
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns an array of all clickable nodes.
|
|
||||||
* Unfortunately we cannot search for all nodes with an attached 'click' event listener
|
|
||||||
* See https://stackoverflow.com/questions/11455515/how-to-check-whether-dynamically-attached-event-listener-exists-or-not
|
|
||||||
* Therefore we can only detect the following standard cases:
|
|
||||||
* I. All clickable objects like clickables
|
|
||||||
* II. Objects that have been attached a click handler by the scatter itself via
|
|
||||||
*/
|
|
||||||
clickableNodes() {
|
|
||||||
let result = []
|
|
||||||
for (let node of this.element.querySelectorAll("*")) {
|
|
||||||
if (this.isClickable(node))
|
|
||||||
result.push(node)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
nearestClickable(event) {
|
|
||||||
let element = this.element
|
|
||||||
let clickables = this.clickableNodes()
|
|
||||||
let globalClick = (event.center) ? event.center : { x: event.x, y: event.y }
|
|
||||||
let localClick = Points.fromPageToNode(element, globalClick)
|
|
||||||
|
|
||||||
let clickRects = clickables.map(link => {
|
|
||||||
let rect = link.getBoundingClientRect()
|
|
||||||
let topLeft = Points.fromPageToNode(element, rect)
|
|
||||||
let center = Points.fromPageToNode(element, { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 })
|
|
||||||
return { x: topLeft.x, y: topLeft.y, width: rect.width, height: rect.height, center, link }
|
|
||||||
})
|
|
||||||
|
|
||||||
let distances = []
|
|
||||||
clickRects.forEach(rect => {
|
|
||||||
let distance = Points.distanceToRect(localClick, rect)
|
|
||||||
distances.push(parseInt(distance))
|
|
||||||
})
|
|
||||||
|
|
||||||
let closestClickIndex = distances.indexOf(Math.min(...distances))
|
|
||||||
let closestClickable = clickables[closestClickIndex]
|
|
||||||
if (distances[closestClickIndex] < this.allowClickDistance) {
|
|
||||||
return closestClickable
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
isDescendant(parent, child) {
|
isDescendant(parent, child) {
|
||||||
let node = child.parentNode
|
let node = child.parentNode
|
||||||
while (node != null) {
|
while (node != null) {
|
||||||
|
Loading…
Reference in New Issue
Block a user