diff --git a/dist/iwmlib.js b/dist/iwmlib.js index 4155493..a333796 100644 --- a/dist/iwmlib.js +++ b/dist/iwmlib.js @@ -7548,11 +7548,6 @@ 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)) { @@ -7565,10 +7560,6 @@ 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); @@ -7593,9 +7584,7 @@ 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 } @@ -7658,7 +7647,6 @@ /* 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'); @@ -7691,7 +7679,6 @@ } tap(event, calledBy='unknown') { - console.log("tap", calledBy, event.alreadyTapped, event); if (event.isTrusted) { let node = this.nearestActive(event); this.nodeTapped(node, event); @@ -7715,7 +7702,406 @@ } } - window.CardWrapper = CardWrapper; + + /* 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: + * + * + * + * + * + * + * + * + * + * + * 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!', target); + + if (Highlight._isExpanded(target)) { + console.log('Target is already expanded!'); + return + } else { + + let targetId = target.getAttribute('id'); + console.log(targetId, targetId && targetId.startsWith('@@')); + 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); + } + console.log("_bringToFront"); + 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 }); + + console.log({svgRoot, image, mask, maskImage}); + let center = Highlight._calculateCenterRelativeTo(target, image); + + TweenLite.set(maskImage, { transformOrigin: `${center.x}% ${center.y}%` }); + TweenLite.set(target, { transformOrigin: '50% 50%' }); + + TweenLite.to([target, maskImage], animation, { + scale, + onComplete: onExpanded + }); + + target.classList.add('expanded'); + console.log({target, maskImage, scale, animation}); + console.log(maskImage); + } + + 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 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'; /* Needed to ensure that rollup.js includes class definitions and the classes are visible inside doctests. @@ -7782,5 +8168,6 @@ window.randomFloat = randomFloat; window.CardWrapper = CardWrapper; + window.Highlight = Highlight; }()); diff --git a/lib/bundle.js b/lib/bundle.js index f7a485c..211e458 100755 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -20,6 +20,9 @@ import {Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Stri import UITest from './uitest.js' import {CardWrapper} from './card/wrapper.js' +import {Highlight} from './card/highlight.js' + + /* Needed to ensure that rollup.js includes class definitions and the classes are visible inside doctests. */ @@ -84,4 +87,5 @@ window.debounce = debounce window.randomInt = randomInt window.randomFloat = randomFloat -window.CardWrapper = CardWrapper \ No newline at end of file +window.CardWrapper = CardWrapper +window.Highlight = Highlight \ No newline at end of file diff --git a/lib/card/highlight.js b/lib/card/highlight.js index 27f8c8f..8186bf3 100644 --- a/lib/card/highlight.js +++ b/lib/card/highlight.js @@ -30,7 +30,7 @@ function round(value) { * @class Highlight * @extends {Object} */ -class Highlight extends Object { +export class Highlight extends Object { static disableAnimations() { _HighlightEnabled = false @@ -194,7 +194,7 @@ class Highlight extends Object { onExpanded = null } = {}) { - console.log('Open Highlight!') + console.log('Open Highlight!', target) if (Highlight._isExpanded(target)) { console.log('Target is already expanded!') @@ -202,7 +202,7 @@ class Highlight extends Object { } else { let targetId = target.getAttribute('id') - console.log(targetId) + console.log(targetId, targetId && targetId.startsWith('@@')) if(targetId && targetId.startsWith('@@')){ let id = targetId.slice(2) const imageId = '#maskImage'+id @@ -214,7 +214,7 @@ class Highlight extends Object { }else console.error('Could not find corresponding image element.') }else console.log('Element was no parent:', target) } - + console.log("_bringToFront") this._bringToFront(target) let svgRoot = target.closest('svg') @@ -222,21 +222,21 @@ class Highlight extends Object { // eslint-disable-next-line no-unused-vars let [mask, maskImage] = Highlight._getSVGMask(target, { svgRoot, image }) + + console.log({svgRoot, image, mask, maskImage}) 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') + console.log({target, maskImage, scale, animation}) + console.log(maskImage) } return @@ -397,4 +397,4 @@ class Highlight extends Object { } Highlight.expandedClass = 'expanded' -window.Highlight = Highlight + diff --git a/lib/card/index.html b/lib/card/index.html index 1d28564..82655fb 100644 --- a/lib/card/index.html +++ b/lib/card/index.html @@ -46,7 +46,7 @@

A Demo Card with onclick

- @@ -59,15 +59,23 @@

A Demo Card with selectors

-
- - - - -
-

Lorem ipsum dolor sit + + + + + + + + + + + + +

Lorem ipsum dolor sit amet, consetetur sadipscing elitr.

@@ -84,8 +92,17 @@ const wrapper2 = new CardWrapper(demoCardWithSelector) wrapper2.handleClicksAsTaps() wrapper2.onTap('.highlight', event => { - alert('.highlight clicked') + + Highlight.openHighlight(event.target, { + + onExpanded: () => { + console.log("onExpanded", event.target) + } + }) }) + + // Highlight.animate(event) + wrapper2.onTap('.link', event => { alert('.link clicked') }) @@ -94,7 +111,8 @@

Using Cards within Scatters

-

Cards can be used within scatters. Since the CardWrapper implements the ITapDelegate protocol they can simply +

Cards can be used within scatters. Since the CardWrapper implements the ITapDelegate + protocol they can simply be attached to a DOMScatter object. See the Scatter Doctest.

@@ -104,6 +122,7 @@ References \ No newline at end of file diff --git a/lib/card/wrapper.js b/lib/card/wrapper.js index c515ad3..8ae6c04 100644 --- a/lib/card/wrapper.js +++ b/lib/card/wrapper.js @@ -17,11 +17,6 @@ export class CardWrapper extends Object { 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)) { @@ -34,10 +29,6 @@ export class CardWrapper extends Object { 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) @@ -62,9 +53,7 @@ export class CardWrapper extends Object { 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 } @@ -127,7 +116,6 @@ export class CardWrapper extends Object { /* 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') @@ -160,7 +148,6 @@ export class CardWrapper extends Object { } tap(event, calledBy='unknown') { - console.log("tap", calledBy, event.alreadyTapped, event) if (event.isTrusted) { let node = this.nearestActive(event) this.nodeTapped(node, event) @@ -184,4 +171,3 @@ export class CardWrapper extends Object { } } -window.CardWrapper = CardWrapper