From e87c8c9e1e19c1e21cb6c5e102cc90780bb022ca Mon Sep 17 00:00:00 2001 From: Uwe Oestermeier Date: Fri, 5 Jul 2019 09:39:01 +0200 Subject: [PATCH 1/2] Renamed simulateClick to clickOnTap --- dist/iwmlib.js | 6 +++--- dist/iwmlib.pixi.js | 6 +++--- lib/scatter.js | 6 +++--- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/dist/iwmlib.js b/dist/iwmlib.js index 50a3b26..3c9f519 100644 --- a/dist/iwmlib.js +++ b/dist/iwmlib.js @@ -5149,7 +5149,7 @@ width = null, // required height = null, // required resizable = false, - simulateClick = false, + clickOnTap = false, verbose = true, onResize = null, touchAction = 'none', @@ -5200,7 +5200,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; @@ -5406,7 +5406,7 @@ } onTap(event, interaction, point) { - if (this.simulateClick) { + if (this.clickOnTap) { let p = Points.fromPageToNode(this.element, point); let element = document.elementFromPoint(p.x, p.y); if (element != null) { diff --git a/dist/iwmlib.pixi.js b/dist/iwmlib.pixi.js index 16d53ae..8216093 100644 --- a/dist/iwmlib.pixi.js +++ b/dist/iwmlib.pixi.js @@ -6920,7 +6920,7 @@ width = null, // required height = null, // required resizable = false, - simulateClick = false, + clickOnTap = false, verbose = true, onResize = null, touchAction = 'none', @@ -6971,7 +6971,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; @@ -7177,7 +7177,7 @@ } onTap(event, interaction, point) { - if (this.simulateClick) { + if (this.clickOnTap) { let p = Points.fromPageToNode(this.element, point); let element = document.elementFromPoint(p.x, p.y); if (element != null) { diff --git a/lib/scatter.js b/lib/scatter.js index 02a65ac..d5d1c25 100644 --- a/lib/scatter.js +++ b/lib/scatter.js @@ -1037,7 +1037,7 @@ export class DOMScatter extends AbstractScatter { width = null, // required height = null, // required resizable = false, - simulateClick = false, + clickOnTap = false, verbose = true, onResize = null, touchAction = 'none', @@ -1088,7 +1088,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 @@ -1294,7 +1294,7 @@ export class DOMScatter extends AbstractScatter { } onTap(event, interaction, point) { - if (this.simulateClick) { + if (this.clickOnTap) { let p = Points.fromPageToNode(this.element, point) let element = document.elementFromPoint(p.x, p.y) if (element != null) { From 20ac5c387ef50977d0670879e71ef4eaebdca1b9 Mon Sep 17 00:00:00 2001 From: Uwe Oestermeier Date: Fri, 5 Jul 2019 13:43:39 +0200 Subject: [PATCH 2/2] Working on tap behavior of scatter and flippables. --- dist/iwmlib.js | 248 +++++++++++++++++++++++--------------------- dist/iwmlib.pixi.js | 161 +++++++++++++++++++++------- lib/flippable.html | 7 +- lib/flippable.js | 56 +++++----- lib/scatter.html | 41 +++++++- lib/scatter.js | 99 ++++++++++++++++-- lib/utils.js | 7 ++ 7 files changed, 416 insertions(+), 203 deletions(-) diff --git a/dist/iwmlib.js b/dist/iwmlib.js index 42752a8..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, @@ -3815,38 +3822,6 @@ onMove = null } = {} ) { -<<<<<<< HEAD - this.onCapture = null; - this.element = element; - if (stopEvents === 'auto') { - /* - The events have to be stopped in Safari, otherwise the whole page will be zoomed with - a pinch gesture (preventDefault in method preventPinch). In order to enable the - movement of scatter objects, the touchmove event has to be bound again. - */ - if (Capabilities.isSafari) { - document.addEventListener( - 'touchmove', - event => this.preventPinch(event), - false - ); - stopEvents = false; - } else { - stopEvents = true; - } - } - this.stopEvents = stopEvents; - this.claimEvents = claimEvents; - if (touchAction !== null) { - Elements$1.setStyle(element, { touchAction }); - } - this.scatter = new Map(); - this.delegate = new InteractionMapper$1(element, this, { - useCapture, - mouseWheelElement: window - }); -======= ->>>>>>> a3f7eb0b3cc9f48cfee3ce6c45887bf25617d1bc let notchPosition = (switchPos && point.y < 50) ? "topCenter" : "bottomCenter"; @@ -4753,52 +4728,6 @@ } } -<<<<<<< HEAD - class DOMFlippable { - constructor(element, scatter, flip) { - // Set log to console.log or a custom log function - // define data structures to store our touchpoints in - - this.element = element; - this.flip = flip; - this.card = element.querySelector('.flipCard'); - this.front = element.querySelector('.front'); - this.back = element.querySelector('.back'); - this.flipped = false; - this.scatter = scatter; - this.onFrontFlipped = flip.onFrontFlipped; - this.onBackFlipped = flip.onBackFlipped; - this.onClose = flip.onClose; - this.onRemoved = flip.onRemoved; - this.onUpdate = flip.onUpdate; - - 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 }); - TweenLite.set([this.back, this.front], { - backfaceVisibility: 'hidden', - perspective: 5000 - }); - TweenLite.set(this.front, { visibility: 'visible' }); - this.infoBtn = element.querySelector('.infoBtn'); - 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()); - this.enable(this.infoBtn); - } - if (this.backBtn) { - InteractionMapper$1.on('tap', this.backBtn, event => this.start()); - } - if (this.closeBtn) { - InteractionMapper$1.on('tap', this.closeBtn, event => this.close()); - this.enable(this.closeBtn); -======= /** * For a given zoom, a new scale is calculated, taking * min and max scale into account. @@ -4814,7 +4743,6 @@ if (scale < minScale) { scale = minScale; zoom = scale / this.scale; ->>>>>>> a3f7eb0b3cc9f48cfee3ce6c45887bf25617d1bc } if (scale > maxScale) { scale = maxScale; @@ -5065,6 +4993,11 @@ this.onCapture = null; this.element = element; if (stopEvents === 'auto') { + /* + The events have to be stopped in Safari, otherwise the whole page will be zoomed with + a pinch gesture (preventDefault in method preventPinch). In order to enable the + movement of scatter objects, the touchmove event has to be bound again. + */ if (Capabilities.isSafari) { document.addEventListener( 'touchmove', @@ -5118,7 +5051,7 @@ context.stroke(); } requestAnimationFrame(dt => { - this.showTouches(dt); + this.showTouches(dt, canvas); }); } @@ -5230,6 +5163,7 @@ height = null, // required resizable = false, clickOnTap = false, + allowClickDistance = 44, verbose = true, onResize = null, touchAction = 'none', @@ -5293,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); @@ -5486,16 +5421,99 @@ } onTap(event, interaction, point) { + if (this.clickOnTap) { - 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(); + 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) { @@ -5647,12 +5665,12 @@ this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight; this.addedNode = null; console.log({ - + width, height, maxWidth, maxHeight, - + }); } @@ -5815,6 +5833,7 @@ translatable = true, scalable = true, rotatable = true, + clickOnTap = false, onFront = null, onBack = null, onClose = null, @@ -5834,6 +5853,7 @@ this.translatable = translatable; this.scalable = scalable; this.rotatable = rotatable; + this.clickOnTap = clickOnTap; this.onFrontFlipped = onFront; this.onBackFlipped = onBack; this.onClose = onClose; @@ -5888,7 +5908,8 @@ translatable: this.translatable, scalable: this.scalable, rotatable: this.rotatable, - overdoScaling: this.overdoScaling + overdoScaling: this.overdoScaling, + clickOnTap: this.clickOnTap } ); @@ -5897,7 +5918,6 @@ } if (this.closeOnMinScale) { - const removeOnMinScale = function () { if (scatter.scale <= scatter.minScale) { this.flippable.close(); @@ -5912,11 +5932,7 @@ scatter.onTransform.splice(callbackIdx, 1); } } - }.bind(this); - - - scatter.addTransformEventCallback(removeOnMinScale); } @@ -5952,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; @@ -6005,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 }); @@ -6018,16 +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()); + if (this.infoBtn) { + 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(); @@ -6077,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 }); @@ -6101,6 +6116,7 @@ clickInfo() { this.bringToFront(); + console.log("clickInfo"); this.infoBtn.click(); } @@ -6142,8 +6158,6 @@ } } - - enable(button) { this.show(button, this.fadeDuration); if (button) { @@ -6153,9 +6167,6 @@ disable(button) { this.hide(button, this.fadeDuration); - if (button) { - TweenLite.set(button, { pointerEvents: 'none' }); - } } start({ targetCenter = null } = {}) { @@ -6199,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 b0fc860..ef8ed40 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, @@ -6922,6 +6929,7 @@ height = null, // required resizable = false, clickOnTap = false, + allowClickDistance = 44, verbose = true, onResize = null, touchAction = 'none', @@ -6985,7 +6993,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); @@ -7178,16 +7187,99 @@ } onTap(event, interaction, point) { + if (this.clickOnTap) { - 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(); + 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) { @@ -7339,12 +7431,12 @@ this.maxHeight = maxHeight != null ? maxHeight : window.innerHeight; this.addedNode = null; console.log({ - + width, height, maxWidth, maxHeight, - + }); } @@ -7373,6 +7465,7 @@ translatable = true, scalable = true, rotatable = true, + clickOnTap = false, onFront = null, onBack = null, onClose = null, @@ -7392,6 +7485,7 @@ this.translatable = translatable; this.scalable = scalable; this.rotatable = rotatable; + this.clickOnTap = clickOnTap; this.onFrontFlipped = onFront; this.onBackFlipped = onBack; this.onClose = onClose; @@ -7446,7 +7540,8 @@ translatable: this.translatable, scalable: this.scalable, rotatable: this.rotatable, - overdoScaling: this.overdoScaling + overdoScaling: this.overdoScaling, + clickOnTap: this.clickOnTap } ); @@ -7455,7 +7550,6 @@ } if (this.closeOnMinScale) { - const removeOnMinScale = function () { if (scatter.scale <= scatter.minScale) { this.flippable.close(); @@ -7470,11 +7564,7 @@ scatter.onTransform.splice(callbackIdx, 1); } } - }.bind(this); - - - scatter.addTransformEventCallback(removeOnMinScale); } @@ -7510,8 +7600,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; @@ -7563,7 +7656,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 }); @@ -7576,15 +7669,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(); @@ -7634,18 +7736,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 }); @@ -7658,6 +7748,7 @@ clickInfo() { this.bringToFront(); + console.log("clickInfo"); this.infoBtn.click(); } @@ -7699,8 +7790,6 @@ } } - - enable(button) { this.show(button, this.fadeDuration); if (button) { @@ -7710,9 +7799,6 @@ disable(button) { this.hide(button, this.fadeDuration); - if (button) { - TweenLite.set(button, { pointerEvents: 'none' }); - } } start({ targetCenter = null } = {}) { @@ -7756,7 +7842,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 02824e9..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) }) } @@ -1043,6 +1043,7 @@ export class DOMScatter extends AbstractScatter { height = null, // required resizable = false, clickOnTap = false, + allowClickDistance = 44, verbose = true, onResize = null, touchAction = 'none', @@ -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.clickOnTap) { - 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() + 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,