Added support for DOM cards.
This commit is contained in:
@@ -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
|
||||
@@ -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>
|
||||
@@ -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
|
||||
Reference in New Issue
Block a user