diff --git a/dist/iwmlib.js b/dist/iwmlib.js index ae99164..286c758 100644 --- a/dist/iwmlib.js +++ b/dist/iwmlib.js @@ -987,6 +987,13 @@ return Math.sqrt(dx * dx + dy * dy) } + // Distance == 0.0 indicates an inside relation. + static distanceToRect(p, r) { + var cx = Math.max(Math.min(p.x, r.x + r.width), r.x); + var cy = Math.max(Math.min(p.y, r.y + r.height), r.y); + return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy)) + } + static fromPageToNode(element, p) { // if (window.webkitConvertPointFromPageToNode) { // return window.webkitConvertPointFromPageToNode(element, @@ -5044,7 +5051,7 @@ context.stroke(); } requestAnimationFrame(dt => { - this.showTouches(dt); + this.showTouches(dt, canvas); }); } @@ -5155,7 +5162,8 @@ width = null, // required height = null, // required resizable = false, - simulateClick = false, + clickOnTap = false, + allowClickDistance = 44, verbose = true, onResize = null, touchAction = 'none', @@ -5206,7 +5214,7 @@ this.height = height; this.throwVisibility = Math.min(width, height, throwVisibility); this.container = container; - this.simulateClick = simulateClick; + this.clickOnTap = clickOnTap; this.scale = startScale; this.rotationDegrees = this.startRotationDegrees; this.transformOrigin = transformOrigin; @@ -5219,7 +5227,8 @@ rotation: this.startRotationDegrees, transformOrigin: transformOrigin }; - + this.tapNodes = new Map(); + this.allowClickDistance = allowClickDistance; // For tweenlite we need initial values in _gsTransform TweenLite.set(element, this.initialValues); @@ -5412,16 +5421,99 @@ } onTap(event, interaction, point) { - if (this.simulateClick) { - let p = Points.fromPageToNode(this.element, point); - let element = document.elementFromPoint(p.x, p.y); - if (element != null) { - console.log('tap simulates click'); - element.click(); + + if (this.clickOnTap) { + let directNode = document.elementFromPoint(point.x, point.y); + let nearestNode = this.nearestClickable(event); + + console.log("onTap", directNode, nearestNode.tagName); + if (directNode != null && this.isClickable(directNode)) { + directNode.click(); + } + else { + if (nearestNode.tagName == 'svg' && this.isClickable(nearestNode)) { + let handler = this.tapNodes.get(nearestNode); + console.log("Clicking beneath SVG: to be done", handler); + Events.stop(event); + //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.tagName == 'A') + 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) { + console.log("found closest clickables", closestClickable); + return closestClickable + } + return null + } + isDescendant(parent, child) { let node = child.parentNode; while (node != null) { @@ -5573,12 +5665,12 @@ this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight; this.addedNode = null; console.log({ - + width, height, maxWidth, maxHeight, - + }); } @@ -5741,6 +5833,7 @@ translatable = true, scalable = true, rotatable = true, + clickOnTap = false, onFront = null, onBack = null, onClose = null, @@ -5760,6 +5853,7 @@ this.translatable = translatable; this.scalable = scalable; this.rotatable = rotatable; + this.clickOnTap = clickOnTap; this.onFrontFlipped = onFront; this.onBackFlipped = onBack; this.onClose = onClose; @@ -5814,7 +5908,8 @@ translatable: this.translatable, scalable: this.scalable, rotatable: this.rotatable, - overdoScaling: this.overdoScaling + overdoScaling: this.overdoScaling, + clickOnTap: this.clickOnTap } ); @@ -5823,7 +5918,6 @@ } if (this.closeOnMinScale) { - const removeOnMinScale = function () { if (scatter.scale <= scatter.minScale) { this.flippable.close(); @@ -5838,11 +5932,7 @@ scatter.onTransform.splice(callbackIdx, 1); } } - }.bind(this); - - - scatter.addTransformEventCallback(removeOnMinScale); } @@ -5878,8 +5968,11 @@ } start({ targetCenter = null } = {}) { - console.log('DOMFlip.start', targetCenter); - if (this.preloadBack) this.flippable.start({ duration: this.flipDuration, targetCenter }); + console.log("DOMFlip.start", targetCenter); + if (this.preloadBack) { + + this.flippable.start({ duration: this.flipDuration, targetCenter }); + } else { let back = this.cardWrapper.querySelector('.back'); let flippable = this.flippable; @@ -5931,7 +6024,7 @@ this.flipDuration = flip.flipDuration; this.fadeDuration = flip.fadeDuration; scatter.addTransformEventCallback(this.scatterTransformed.bind(this)); - console.log('lib.DOMFlippable', 5000); + TweenLite.set(this.element, { perspective: 5000 }); TweenLite.set(this.card, { transformStyle: 'preserve-3d' }); TweenLite.set(this.back, { rotationY: -180 }); @@ -5944,15 +6037,24 @@ this.backBtn = element.querySelector('.backBtn'); this.closeBtn = element.querySelector('.closeBtn'); /* Buttons are not guaranteed to exist. */ + if (this.infoBtn) { - InteractionMapper$1.on('tap', this.infoBtn, event => this.flip.start()); + scatter.addTapListener(this.infoBtn, event => { + console.log("within click handler", this); + this.flip.start(); + }); this.enable(this.infoBtn); } if (this.backBtn) { - InteractionMapper$1.on('tap', this.backBtn, event => this.start()); + scatter.addTapListener(this.backBtn, event => { + console.log("within click handler", this); + this.start(); + }); } if (this.closeBtn) { - InteractionMapper$1.on('tap', this.closeBtn, event => this.close()); + scatter.addTapListener(this.closeBtn, event => { + this.close(); + }); this.enable(this.closeBtn); } this.scaleButtons(); @@ -6002,18 +6104,6 @@ } scaleButtons() { - //This also works for svgs. - // if (this.infoBtn) - // this.infoBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.backBtn) - // this.backBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.closeBtn) - // this.closeBtn.style.transform = "scale(" + this.buttonScale + ")" - - console.log(this.buttonScale); - //// This did not work with svgs! TweenLite.set([this.infoBtn, this.backBtn, this.closeBtn], { scale: this.buttonScale }); @@ -6026,6 +6116,7 @@ clickInfo() { this.bringToFront(); + console.log("clickInfo"); this.infoBtn.click(); } @@ -6067,8 +6158,6 @@ } } - - enable(button) { this.show(button, this.fadeDuration); if (button) { @@ -6078,9 +6167,6 @@ disable(button) { this.hide(button, this.fadeDuration); - if (button) { - TweenLite.set(button, { pointerEvents: 'none' }); - } } start({ targetCenter = null } = {}) { @@ -6124,7 +6210,6 @@ let x = this.flipped ? xx : this.startX; let y = this.flipped ? yy : this.startY; - console.log("DOMFlippable.start", this.flipped, targetCenter, x, y, this.saved); let onUpdate = this.onUpdate !== null ? () => this.onUpdate(this) : null; console.log(this.flipDuration); TweenLite.to(this.card, this.flipDuration, { diff --git a/dist/iwmlib.pixi.js b/dist/iwmlib.pixi.js index be3c34c..eb0f1d2 100644 --- a/dist/iwmlib.pixi.js +++ b/dist/iwmlib.pixi.js @@ -3263,6 +3263,13 @@ return Math.sqrt(dx * dx + dy * dy) } + // Distance == 0.0 indicates an inside relation. + static distanceToRect(p, r) { + var cx = Math.max(Math.min(p.x, r.x + r.width), r.x); + var cy = Math.max(Math.min(p.y, r.y + r.height), r.y); + return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy)) + } + static fromPageToNode(element, p) { // if (window.webkitConvertPointFromPageToNode) { // return window.webkitConvertPointFromPageToNode(element, @@ -6917,7 +6924,8 @@ width = null, // required height = null, // required resizable = false, - simulateClick = false, + clickOnTap = false, + allowClickDistance = 44, verbose = true, onResize = null, touchAction = 'none', @@ -6968,7 +6976,7 @@ this.height = height; this.throwVisibility = Math.min(width, height, throwVisibility); this.container = container; - this.simulateClick = simulateClick; + this.clickOnTap = clickOnTap; this.scale = startScale; this.rotationDegrees = this.startRotationDegrees; this.transformOrigin = transformOrigin; @@ -6981,7 +6989,8 @@ rotation: this.startRotationDegrees, transformOrigin: transformOrigin }; - + this.tapNodes = new Map(); + this.allowClickDistance = allowClickDistance; // For tweenlite we need initial values in _gsTransform TweenLite.set(element, this.initialValues); @@ -7174,16 +7183,99 @@ } onTap(event, interaction, point) { - if (this.simulateClick) { - let p = Points.fromPageToNode(this.element, point); - let element = document.elementFromPoint(p.x, p.y); - if (element != null) { - console.log('tap simulates click'); - element.click(); + + if (this.clickOnTap) { + let directNode = document.elementFromPoint(point.x, point.y); + let nearestNode = this.nearestClickable(event); + + console.log("onTap", directNode, nearestNode.tagName); + if (directNode != null && this.isClickable(directNode)) { + directNode.click(); + } + else { + if (nearestNode.tagName == 'svg' && this.isClickable(nearestNode)) { + let handler = this.tapNodes.get(nearestNode); + console.log("Clicking beneath SVG: to be done", handler); + Events$1.stop(event); + //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.tagName == 'A') + 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) { + console.log("found closest clickables", closestClickable); + return closestClickable + } + return null + } + isDescendant(parent, child) { let node = child.parentNode; while (node != null) { @@ -7335,12 +7427,12 @@ this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight; this.addedNode = null; console.log({ - + width, height, maxWidth, maxHeight, - + }); } @@ -7369,6 +7461,7 @@ translatable = true, scalable = true, rotatable = true, + clickOnTap = false, onFront = null, onBack = null, onClose = null, @@ -7388,6 +7481,7 @@ this.translatable = translatable; this.scalable = scalable; this.rotatable = rotatable; + this.clickOnTap = clickOnTap; this.onFrontFlipped = onFront; this.onBackFlipped = onBack; this.onClose = onClose; @@ -7442,7 +7536,8 @@ translatable: this.translatable, scalable: this.scalable, rotatable: this.rotatable, - overdoScaling: this.overdoScaling + overdoScaling: this.overdoScaling, + clickOnTap: this.clickOnTap } ); @@ -7451,7 +7546,6 @@ } if (this.closeOnMinScale) { - const removeOnMinScale = function () { if (scatter.scale <= scatter.minScale) { this.flippable.close(); @@ -7466,11 +7560,7 @@ scatter.onTransform.splice(callbackIdx, 1); } } - }.bind(this); - - - scatter.addTransformEventCallback(removeOnMinScale); } @@ -7506,8 +7596,11 @@ } start({ targetCenter = null } = {}) { - console.log('DOMFlip.start', targetCenter); - if (this.preloadBack) this.flippable.start({ duration: this.flipDuration, targetCenter }); + console.log("DOMFlip.start", targetCenter); + if (this.preloadBack) { + + this.flippable.start({ duration: this.flipDuration, targetCenter }); + } else { let back = this.cardWrapper.querySelector('.back'); let flippable = this.flippable; @@ -7559,7 +7652,7 @@ this.flipDuration = flip.flipDuration; this.fadeDuration = flip.fadeDuration; scatter.addTransformEventCallback(this.scatterTransformed.bind(this)); - console.log('lib.DOMFlippable', 5000); + TweenLite.set(this.element, { perspective: 5000 }); TweenLite.set(this.card, { transformStyle: 'preserve-3d' }); TweenLite.set(this.back, { rotationY: -180 }); @@ -7572,15 +7665,24 @@ this.backBtn = element.querySelector('.backBtn'); this.closeBtn = element.querySelector('.closeBtn'); /* Buttons are not guaranteed to exist. */ + if (this.infoBtn) { - InteractionMapper$1.on('tap', this.infoBtn, event => this.flip.start()); + scatter.addTapListener(this.infoBtn, event => { + console.log("within click handler", this); + this.flip.start(); + }); this.enable(this.infoBtn); } if (this.backBtn) { - InteractionMapper$1.on('tap', this.backBtn, event => this.start()); + scatter.addTapListener(this.backBtn, event => { + console.log("within click handler", this); + this.start(); + }); } if (this.closeBtn) { - InteractionMapper$1.on('tap', this.closeBtn, event => this.close()); + scatter.addTapListener(this.closeBtn, event => { + this.close(); + }); this.enable(this.closeBtn); } this.scaleButtons(); @@ -7630,18 +7732,6 @@ } scaleButtons() { - //This also works for svgs. - // if (this.infoBtn) - // this.infoBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.backBtn) - // this.backBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.closeBtn) - // this.closeBtn.style.transform = "scale(" + this.buttonScale + ")" - - console.log(this.buttonScale); - //// This did not work with svgs! TweenLite.set([this.infoBtn, this.backBtn, this.closeBtn], { scale: this.buttonScale }); @@ -7654,6 +7744,7 @@ clickInfo() { this.bringToFront(); + console.log("clickInfo"); this.infoBtn.click(); } @@ -7695,8 +7786,6 @@ } } - - enable(button) { this.show(button, this.fadeDuration); if (button) { @@ -7706,9 +7795,6 @@ disable(button) { this.hide(button, this.fadeDuration); - if (button) { - TweenLite.set(button, { pointerEvents: 'none' }); - } } start({ targetCenter = null } = {}) { @@ -7752,7 +7838,6 @@ let x = this.flipped ? xx : this.startX; let y = this.flipped ? yy : this.startY; - console.log("DOMFlippable.start", this.flipped, targetCenter, x, y, this.saved); let onUpdate = this.onUpdate !== null ? () => this.onUpdate(this) : null; console.log(this.flipDuration); TweenLite.to(this.card, this.flipDuration, { diff --git a/lib/flippable.html b/lib/flippable.html index ed7d0b2..83cb984 100644 --- a/lib/flippable.html +++ b/lib/flippable.html @@ -72,7 +72,7 @@ if (Capabilities.supportsTemplate()) { flipTemplate, new ImageLoader('./examples/king.jpeg'), new ImageLoader('./examples/women.jpeg'), - { onUpdate: e => console.log(e)}) + { clickOnTap: true}) flip.load().then((flip) => { flip.centerAt({ x: 150, y: 120}) }) @@ -80,10 +80,5 @@ if (Capabilities.supportsTemplate()) { else { alert("Templates not supported, use Edge, Chrome, Safari or Firefox.") } - -setTimeout(function() { - const infoBtn = document.querySelector('.infoBtn') - InteractionMapper.on('tap', infoBtn, event => console.log('go')) -}, 2000) diff --git a/lib/flippable.js b/lib/flippable.js index 836c40c..826aee9 100644 --- a/lib/flippable.js +++ b/lib/flippable.js @@ -31,12 +31,12 @@ export class CardLoader { this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight this.addedNode = null console.log({ - + width, height, maxWidth, maxHeight, - + }) } @@ -199,6 +199,7 @@ export class DOMFlip { translatable = true, scalable = true, rotatable = true, + clickOnTap = false, onFront = null, onBack = null, onClose = null, @@ -218,6 +219,7 @@ export class DOMFlip { this.translatable = translatable this.scalable = scalable this.rotatable = rotatable + this.clickOnTap = clickOnTap this.onFrontFlipped = onFront this.onBackFlipped = onBack this.onClose = onClose @@ -272,7 +274,8 @@ export class DOMFlip { translatable: this.translatable, scalable: this.scalable, rotatable: this.rotatable, - overdoScaling: this.overdoScaling + overdoScaling: this.overdoScaling, + clickOnTap: this.clickOnTap } ) @@ -281,7 +284,6 @@ export class DOMFlip { } if (this.closeOnMinScale) { - const removeOnMinScale = function () { if (scatter.scale <= scatter.minScale) { this.flippable.close() @@ -296,11 +298,7 @@ export class DOMFlip { scatter.onTransform.splice(callbackIdx, 1) } } - }.bind(this) - - - scatter.addTransformEventCallback(removeOnMinScale) } @@ -336,8 +334,11 @@ export class DOMFlip { } start({ targetCenter = null } = {}) { - console.log('DOMFlip.start', targetCenter) - if (this.preloadBack) this.flippable.start({ duration: this.flipDuration, targetCenter }) + console.log("DOMFlip.start", targetCenter) + if (this.preloadBack) { + + this.flippable.start({ duration: this.flipDuration, targetCenter }) + } else { let back = this.cardWrapper.querySelector('.back') let flippable = this.flippable @@ -389,7 +390,7 @@ export class DOMFlippable { this.flipDuration = flip.flipDuration this.fadeDuration = flip.fadeDuration scatter.addTransformEventCallback(this.scatterTransformed.bind(this)) - console.log('lib.DOMFlippable', 5000) + TweenLite.set(this.element, { perspective: 5000 }) TweenLite.set(this.card, { transformStyle: 'preserve-3d' }) TweenLite.set(this.back, { rotationY: -180 }) @@ -402,15 +403,24 @@ export class DOMFlippable { this.backBtn = element.querySelector('.backBtn') this.closeBtn = element.querySelector('.closeBtn') /* Buttons are not guaranteed to exist. */ + if (this.infoBtn) { - InteractionMapper.on('tap', this.infoBtn, event => this.flip.start()) + scatter.addTapListener(this.infoBtn, event => { + console.log("within click handler", this) + this.flip.start() + }) this.enable(this.infoBtn) } if (this.backBtn) { - InteractionMapper.on('tap', this.backBtn, event => this.start()) + scatter.addTapListener(this.backBtn, event => { + console.log("within click handler", this) + this.start() + }) } if (this.closeBtn) { - InteractionMapper.on('tap', this.closeBtn, event => this.close()) + scatter.addTapListener(this.closeBtn, event => { + this.close() + }) this.enable(this.closeBtn) } this.scaleButtons() @@ -460,18 +470,6 @@ export class DOMFlippable { } scaleButtons() { - //This also works for svgs. - // if (this.infoBtn) - // this.infoBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.backBtn) - // this.backBtn.style.transform = "scale(" + this.buttonScale + ")" - - // if (this.closeBtn) - // this.closeBtn.style.transform = "scale(" + this.buttonScale + ")" - - console.log(this.buttonScale) - //// This did not work with svgs! TweenLite.set([this.infoBtn, this.backBtn, this.closeBtn], { scale: this.buttonScale }) @@ -484,6 +482,7 @@ export class DOMFlippable { clickInfo() { this.bringToFront() + console.log("clickInfo") this.infoBtn.click() } @@ -525,8 +524,6 @@ export class DOMFlippable { } } - - enable(button) { this.show(button, this.fadeDuration) if (button) { @@ -537,7 +534,7 @@ export class DOMFlippable { disable(button) { this.hide(button, this.fadeDuration) if (button) { - TweenLite.set(button, { pointerEvents: 'none' }) + // TweenLite.set(button, { pointerEvents: 'none' }) } } @@ -582,7 +579,6 @@ export class DOMFlippable { let x = this.flipped ? xx : this.startX let y = this.flipped ? yy : this.startY - console.log("DOMFlippable.start", this.flipped, targetCenter, x, y, this.saved) let onUpdate = this.onUpdate !== null ? () => this.onUpdate(this) : null console.log(this.flipDuration) TweenLite.to(this.card, this.flipDuration, { diff --git a/lib/scatter.html b/lib/scatter.html index fe2c595..4a3ad99 100644 --- a/lib/scatter.html +++ b/lib/scatter.html @@ -15,10 +15,10 @@ context.clearRect(0, 0, debugCanvas.width, debugCanvas.height) let stage = scatterContainer.polygon - stage.draw(context, { stroke: '#FF0000'}) + stage.draw(context, { stroke: '#0000FF'}) for(let scatter of scatterContainer.scatter.values()) { let polygon = scatter.polygon - polygon.draw(context, { stroke: '#FF0000'}) + polygon.draw(context, { stroke: '#0000FF'}) } } @@ -48,7 +48,7 @@ we describe the more basic DOM scatter. - + Canvas not supported. @@ -81,4 +81,39 @@ app.run() animatePolygons() +

+ Interactive Content +

+

+Scatter objects may contain interactive HTML structures. There is one major flag that allows +to simulate click events by using taps. If the scatter detects a tap it looks for clickable +elements under or nearby the event position and calls the click handler. Thus gestures +can be disambiguated as moves, zooms. or taps. +

+ +
+ +
+ + A Link +
A Div with click handler
+
+
+ + + diff --git a/lib/scatter.js b/lib/scatter.js index f4d1098..b947225 100644 --- a/lib/scatter.js +++ b/lib/scatter.js @@ -931,7 +931,7 @@ export class DOMScatterContainer { context.stroke() } requestAnimationFrame(dt => { - this.showTouches(dt) + this.showTouches(dt, canvas) }) } @@ -1042,7 +1042,8 @@ export class DOMScatter extends AbstractScatter { width = null, // required height = null, // required resizable = false, - simulateClick = false, + clickOnTap = false, + allowClickDistance = 44, verbose = true, onResize = null, touchAction = 'none', @@ -1093,7 +1094,7 @@ export class DOMScatter extends AbstractScatter { this.height = height this.throwVisibility = Math.min(width, height, throwVisibility) this.container = container - this.simulateClick = simulateClick + this.clickOnTap = clickOnTap this.scale = startScale this.rotationDegrees = this.startRotationDegrees this.transformOrigin = transformOrigin @@ -1106,7 +1107,8 @@ export class DOMScatter extends AbstractScatter { rotation: this.startRotationDegrees, transformOrigin: transformOrigin } - + this.tapNodes = new Map() + this.allowClickDistance = allowClickDistance // For tweenlite we need initial values in _gsTransform TweenLite.set(element, this.initialValues) @@ -1299,16 +1301,99 @@ export class DOMScatter extends AbstractScatter { } onTap(event, interaction, point) { - if (this.simulateClick) { - let p = Points.fromPageToNode(this.element, point) - let element = document.elementFromPoint(p.x, p.y) - if (element != null) { - console.log('tap simulates click') - element.click() + + if (this.clickOnTap) { + let directNode = document.elementFromPoint(point.x, point.y) + let nearestNode = this.nearestClickable(event) + + console.log("onTap", directNode, nearestNode.tagName) + if (directNode != null && this.isClickable(directNode)) { + directNode.click() + } + else { + if (nearestNode.tagName == 'svg' && this.isClickable(nearestNode)) { + let handler = this.tapNodes.get(nearestNode) + console.log("Clicking beneath SVG: to be done", handler) + Events.stop(event) + //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.tagName == 'A') + 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) { + console.log("found closest clickables", closestClickable) + return closestClickable + } + return null + } + isDescendant(parent, child) { let node = child.parentNode while (node != null) { diff --git a/lib/utils.js b/lib/utils.js index 0c9ef66..5b550a1 100755 --- a/lib/utils.js +++ b/lib/utils.js @@ -402,6 +402,13 @@ export class Points { return Math.sqrt(dx * dx + dy * dy) } + // Distance == 0.0 indicates an inside relation. + static distanceToRect(p, r) { + var cx = Math.max(Math.min(p.x, r.x + r.width), r.x) + var cy = Math.max(Math.min(p.y, r.y + r.height), r.y) + return Math.sqrt((p.x - cx) * (p.x - cx) + (p.y - cy) * (p.y - cy)) + } + static fromPageToNode(element, p) { // if (window.webkitConvertPointFromPageToNode) { // return window.webkitConvertPointFromPageToNode(element,