Added support for DOM cards.
This commit is contained in:
parent
d04f92ee7f
commit
e1b5c45b52
325
dist/iwmlib.js
vendored
325
dist/iwmlib.js
vendored
@ -4003,6 +4003,7 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
class DOMScatter extends AbstractScatter {
|
||||
constructor(
|
||||
element,
|
||||
@ -4028,7 +4029,7 @@
|
||||
width = null, // required
|
||||
height = null, // required
|
||||
resizable = false,
|
||||
clickOnTap = false,
|
||||
tapDelegate = null,
|
||||
triggerSVGClicks = false,
|
||||
allowClickDistance = 44,
|
||||
verbose = true,
|
||||
@ -4085,8 +4086,7 @@
|
||||
this.height = height;
|
||||
this.throwVisibility = Math.min(width, height, throwVisibility);
|
||||
this.container = container;
|
||||
this.clickOnTap = clickOnTap;
|
||||
this.triggerSVGClicks = triggerSVGClicks;
|
||||
this.tapDelegate = tapDelegate;
|
||||
this.scale = startScale;
|
||||
this.rotationDegrees = this.startRotationDegrees;
|
||||
this.transformOrigin = transformOrigin;
|
||||
@ -4100,8 +4100,7 @@
|
||||
transformOrigin: transformOrigin
|
||||
};
|
||||
this.tapNodes = new Map();
|
||||
this.allowClickDistance = allowClickDistance;
|
||||
|
||||
|
||||
// For tweenlite we need initial values in _gsTransform
|
||||
TweenLite.set(element, this.initialValues);
|
||||
this.onResize = onResize;
|
||||
@ -4133,24 +4132,8 @@
|
||||
});
|
||||
this.resizeButton = button;
|
||||
}
|
||||
if (clickOnTap) {
|
||||
/* Since the tap triggers a synthetic click event
|
||||
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);
|
||||
if (tapDelegate) {
|
||||
tapDelegate.handleClicks();
|
||||
}
|
||||
container.add(this);
|
||||
}
|
||||
@ -4312,110 +4295,12 @@
|
||||
}
|
||||
|
||||
onTap(event, interaction, point) {
|
||||
|
||||
if (this.clickOnTap) {
|
||||
let directNode = document.elementFromPoint(event.clientX, event.clientY);
|
||||
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();
|
||||
}
|
||||
}
|
||||
if (this.tapDelegate) {
|
||||
Events.stop(event);
|
||||
this.tapDelegate.tap(event, "scatter");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let node = child.parentNode;
|
||||
while (node != null) {
|
||||
@ -4735,7 +4620,7 @@
|
||||
translatable = true,
|
||||
scalable = true,
|
||||
rotatable = true,
|
||||
clickOnTap = false,
|
||||
tapDelegateFactory = null,
|
||||
onFront = null,
|
||||
onBack = null,
|
||||
onClose = null,
|
||||
@ -4755,7 +4640,7 @@
|
||||
this.translatable = translatable;
|
||||
this.scalable = scalable;
|
||||
this.rotatable = rotatable;
|
||||
this.clickOnTap = clickOnTap;
|
||||
this.tapDelegateFactory = tapDelegateFactory;
|
||||
this.onFrontFlipped = onFront;
|
||||
this.onBackFlipped = onBack;
|
||||
this.onClose = onClose;
|
||||
@ -4811,7 +4696,7 @@
|
||||
scalable: this.scalable,
|
||||
rotatable: this.rotatable,
|
||||
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
|
||||
are visible inside doctests.
|
||||
*/
|
||||
@ -7697,4 +7766,6 @@
|
||||
window.randomInt = randomInt;
|
||||
window.randomFloat = randomFloat;
|
||||
|
||||
window.CardWrapper = CardWrapper;
|
||||
|
||||
}());
|
||||
|
139
dist/iwmlib.pixi.js
vendored
139
dist/iwmlib.pixi.js
vendored
@ -7003,6 +7003,7 @@
|
||||
}
|
||||
|
||||
|
||||
|
||||
class DOMScatter extends AbstractScatter {
|
||||
constructor(
|
||||
element,
|
||||
@ -7028,7 +7029,7 @@
|
||||
width = null, // required
|
||||
height = null, // required
|
||||
resizable = false,
|
||||
clickOnTap = false,
|
||||
tapDelegate = null,
|
||||
triggerSVGClicks = false,
|
||||
allowClickDistance = 44,
|
||||
verbose = true,
|
||||
@ -7085,8 +7086,7 @@
|
||||
this.height = height;
|
||||
this.throwVisibility = Math.min(width, height, throwVisibility);
|
||||
this.container = container;
|
||||
this.clickOnTap = clickOnTap;
|
||||
this.triggerSVGClicks = triggerSVGClicks;
|
||||
this.tapDelegate = tapDelegate;
|
||||
this.scale = startScale;
|
||||
this.rotationDegrees = this.startRotationDegrees;
|
||||
this.transformOrigin = transformOrigin;
|
||||
@ -7100,8 +7100,7 @@
|
||||
transformOrigin: transformOrigin
|
||||
};
|
||||
this.tapNodes = new Map();
|
||||
this.allowClickDistance = allowClickDistance;
|
||||
|
||||
|
||||
// For tweenlite we need initial values in _gsTransform
|
||||
TweenLite.set(element, this.initialValues);
|
||||
this.onResize = onResize;
|
||||
@ -7133,24 +7132,8 @@
|
||||
});
|
||||
this.resizeButton = button;
|
||||
}
|
||||
if (clickOnTap) {
|
||||
/* Since the tap triggers a synthetic click event
|
||||
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);
|
||||
if (tapDelegate) {
|
||||
tapDelegate.handleClicks();
|
||||
}
|
||||
container.add(this);
|
||||
}
|
||||
@ -7312,110 +7295,12 @@
|
||||
}
|
||||
|
||||
onTap(event, interaction, point) {
|
||||
|
||||
if (this.clickOnTap) {
|
||||
let directNode = document.elementFromPoint(event.clientX, event.clientY);
|
||||
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();
|
||||
}
|
||||
}
|
||||
if (this.tapDelegate) {
|
||||
Events$1.stop(event);
|
||||
this.tapDelegate.tap(event, "scatter");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let node = child.parentNode;
|
||||
while (node != null) {
|
||||
@ -7601,7 +7486,7 @@
|
||||
translatable = true,
|
||||
scalable = true,
|
||||
rotatable = true,
|
||||
clickOnTap = false,
|
||||
tapDelegateFactory = null,
|
||||
onFront = null,
|
||||
onBack = null,
|
||||
onClose = null,
|
||||
@ -7621,7 +7506,7 @@
|
||||
this.translatable = translatable;
|
||||
this.scalable = scalable;
|
||||
this.rotatable = rotatable;
|
||||
this.clickOnTap = clickOnTap;
|
||||
this.tapDelegateFactory = tapDelegateFactory;
|
||||
this.onFrontFlipped = onFront;
|
||||
this.onBackFlipped = onBack;
|
||||
this.onClose = onClose;
|
||||
@ -7677,7 +7562,7 @@
|
||||
scalable: this.scalable,
|
||||
rotatable: this.rotatable,
|
||||
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 UITest from './uitest.js'
|
||||
|
||||
import {CardWrapper} from './card/wrapper.js'
|
||||
/* Needed to ensure that rollup.js includes class definitions and the classes
|
||||
are visible inside doctests.
|
||||
*/
|
||||
@ -81,3 +82,5 @@ window.lerp = lerp
|
||||
window.debounce = debounce
|
||||
window.randomInt = randomInt
|
||||
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">
|
||||
let scatterContainer = new DOMScatterContainer(main, {stopEvents: false})
|
||||
if (Capabilities.supportsTemplate()) {
|
||||
|
||||
let flip = new DOMFlip(scatterContainer,
|
||||
flipTemplate,
|
||||
new ImageLoader('./examples/king.jpeg'),
|
||||
new ImageLoader('./examples/women.jpeg'),
|
||||
{ clickOnTap: true})
|
||||
{ tapDelegateFactory: CardWrapper})
|
||||
flip.load().then((flip) => {
|
||||
flip.centerAt({ x: 150, y: 120})
|
||||
})
|
||||
|
@ -199,7 +199,7 @@ export class DOMFlip {
|
||||
translatable = true,
|
||||
scalable = true,
|
||||
rotatable = true,
|
||||
clickOnTap = false,
|
||||
tapDelegateFactory = null,
|
||||
onFront = null,
|
||||
onBack = null,
|
||||
onClose = null,
|
||||
@ -219,7 +219,7 @@ export class DOMFlip {
|
||||
this.translatable = translatable
|
||||
this.scalable = scalable
|
||||
this.rotatable = rotatable
|
||||
this.clickOnTap = clickOnTap
|
||||
this.tapDelegateFactory = tapDelegateFactory
|
||||
this.onFrontFlipped = onFront
|
||||
this.onBackFlipped = onBack
|
||||
this.onClose = onClose
|
||||
@ -275,7 +275,7 @@ export class DOMFlip {
|
||||
scalable: this.scalable,
|
||||
rotatable: this.rotatable,
|
||||
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
|
||||
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
|
||||
can be configured by allowClickDistance. The default value is 44px.
|
||||
Note that on touch devices you can tap beside the object if you use an object that implements the ITapDelegate interface.
|
||||
An ITapDelegate allowes a distance that can be configured by allowClickDistance. The default value is 44px.
|
||||
</p>
|
||||
|
||||
<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">
|
||||
|
||||
let contentContainer = new DOMScatterContainer(contentExample)
|
||||
|
||||
let tapDelegate = new CardWrapper(interactiveContent)
|
||||
new DOMScatter(interactiveContent, contentContainer, {
|
||||
x: 44,
|
||||
y: 44,
|
||||
width: 274,
|
||||
height: 184,
|
||||
clickOnTap: true,
|
||||
tapDelegate,
|
||||
throwVisibility: 88,
|
||||
triggerSVGClicks: true,
|
||||
minScale: 0.5,
|
||||
maxScale: 1.5})
|
||||
|
||||
|
148
lib/scatter.js
148
lib/scatter.js
@ -1,11 +1,26 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
/* eslint-disable no-console */
|
||||
/* globals TweenLite debugCanvas */
|
||||
import Interface from './interface.js'
|
||||
import { Points, Polygon, Angle, Elements, LowPassFilter } from './utils.js'
|
||||
import Events from './events.js'
|
||||
import { InteractionMapper } from './interaction.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.
|
||||
*
|
||||
@ -1035,6 +1050,7 @@ export class DOMScatterContainer {
|
||||
}
|
||||
|
||||
|
||||
|
||||
export class DOMScatter extends AbstractScatter {
|
||||
constructor(
|
||||
element,
|
||||
@ -1060,7 +1076,7 @@ export class DOMScatter extends AbstractScatter {
|
||||
width = null, // required
|
||||
height = null, // required
|
||||
resizable = false,
|
||||
clickOnTap = false,
|
||||
tapDelegate = null,
|
||||
triggerSVGClicks = false,
|
||||
allowClickDistance = 44,
|
||||
verbose = true,
|
||||
@ -1117,8 +1133,7 @@ export class DOMScatter extends AbstractScatter {
|
||||
this.height = height
|
||||
this.throwVisibility = Math.min(width, height, throwVisibility)
|
||||
this.container = container
|
||||
this.clickOnTap = clickOnTap
|
||||
this.triggerSVGClicks = triggerSVGClicks
|
||||
this.tapDelegate = tapDelegate
|
||||
this.scale = startScale
|
||||
this.rotationDegrees = this.startRotationDegrees
|
||||
this.transformOrigin = transformOrigin
|
||||
@ -1132,8 +1147,7 @@ export class DOMScatter extends AbstractScatter {
|
||||
transformOrigin: transformOrigin
|
||||
}
|
||||
this.tapNodes = new Map()
|
||||
this.allowClickDistance = allowClickDistance
|
||||
|
||||
|
||||
// For tweenlite we need initial values in _gsTransform
|
||||
TweenLite.set(element, this.initialValues)
|
||||
this.onResize = onResize
|
||||
@ -1165,24 +1179,8 @@ export class DOMScatter extends AbstractScatter {
|
||||
})
|
||||
this.resizeButton = button
|
||||
}
|
||||
if (clickOnTap) {
|
||||
/* Since the tap triggers a synthetic click event
|
||||
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)
|
||||
if (tapDelegate) {
|
||||
tapDelegate.handleClicks()
|
||||
}
|
||||
container.add(this)
|
||||
}
|
||||
@ -1344,110 +1342,12 @@ export class DOMScatter extends AbstractScatter {
|
||||
}
|
||||
|
||||
onTap(event, interaction, point) {
|
||||
|
||||
if (this.clickOnTap) {
|
||||
let directNode = document.elementFromPoint(event.clientX, event.clientY)
|
||||
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()
|
||||
}
|
||||
}
|
||||
if (this.tapDelegate) {
|
||||
Events.stop(event)
|
||||
this.tapDelegate.tap(event, "scatter")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
let node = child.parentNode
|
||||
while (node != null) {
|
||||
|
Loading…
Reference in New Issue
Block a user