Working on cards.

This commit is contained in:
2019-07-15 13:32:45 +02:00
parent 0e8c62eb4b
commit c3477244b9
5 changed files with 447 additions and 51 deletions
+401 -14
View File
@@ -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:
*
* <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!', 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 <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';
/* 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;
}());