diff --git a/dist/iwmlib.js b/dist/iwmlib.js index 5112503..561296d 100644 --- a/dist/iwmlib.js +++ b/dist/iwmlib.js @@ -924,7 +924,7 @@ /** Static methods to compute 2D points with x and y coordinates. */ - class Points { + class Points$1 { static length(a) { return Math.sqrt(a.x * a.x + a.y * a.y) } @@ -1286,7 +1286,7 @@ absolutePoints() { let result = new Array(); for (let p of this.points) { - result.push(Points.add(p, this.center)); + result.push(Points$1.add(p, this.center)); } return result } @@ -1294,7 +1294,7 @@ flatAbsolutePoints() { let result = new Array(); for (let p of this.points) { - let a = Points.add(p, this.center); + let a = Points$1.add(p, this.center); result.push(a.x); result.push(a.y); } @@ -1338,10 +1338,10 @@ } multiplyScalar(scale) { - let center = Points.multiplyScalar(this.center, scale); + let center = Points$1.multiplyScalar(this.center, scale); let clone = new Polygon(center); for (let p of this.points) { - clone.addPoint(Points.multiplyScalar(p, scale)); + clone.addPoint(Points$1.multiplyScalar(p, scale)); } return clone } @@ -1485,7 +1485,7 @@ min.y = Math.min(p.y, min.y); max.y = Math.max(p.y, max.y); } - let center = Points.mean(min, max); + let center = Points$1.mean(min, max); let polygon = new Polygon(center); for (let p of points) { polygon.addAbsolutePoint(p); @@ -1498,7 +1498,7 @@ /** * Util functions to deal with DOMRects. */ - class Rect { + class Rect$1 { /** @@ -1667,7 +1667,7 @@ * The logging handlers can be overwritten by calling the static * setup method. */ - class Logging { + class Logging$1 { /** Static log function. * @param {*} message @@ -1806,7 +1806,7 @@ } } let sorted = pairs.sort((a, b) => { - return Points.distance(b[0], b[1]) - Points.distance(a[0], a[1]) + return Points$1.distance(b[0], b[1]) - Points$1.distance(a[0], a[1]) }); return sorted[0] } @@ -1873,13 +1873,13 @@ moved(key) { let current = this.current.get(key); let previous = this.previous.get(key); - return Points.subtract(current, previous) + return Points$1.subtract(current, previous) } move() { let current = this.current.mean(); let previous = this.previous.mean(); - return Points.subtract(current, previous) + return Points$1.subtract(current, previous) } /** @@ -1916,21 +1916,21 @@ let p1 = this.previous.get(c1.key); let p2 = this.previous.get(c2.key); - let d1 = Points.subtract(c1, p1); - let d2 = Points.subtract(c2, p2); - let cm = Points.mean(c1, c2); + let d1 = Points$1.subtract(c1, p1); + let d2 = Points$1.subtract(c2, p2); + let cm = Points$1.mean(c1, c2); // Using the mean leads to jumps between time slices with 3 and 2 fingers // We use the mean of deltas instead - let delta = Points.mean(d1, d2); + let delta = Points$1.mean(d1, d2); let zoom = 1.0; - let distance1 = Points.distance(p1, p2); - let distance2 = Points.distance(c1, c2); + let distance1 = Points$1.distance(p1, p2); + let distance2 = Points$1.distance(c1, c2); if (distance1 != 0 && distance2 != 0) { zoom = distance2 / distance1; } - let currentAngle = Points.angle(c1, c2); - let previousAngle = Points.angle(p1, p2); + let currentAngle = Points$1.angle(c1, c2); + let previousAngle = Points$1.angle(p1, p2); let alpha = this.diffAngle(currentAngle, previousAngle); return new InteractionDelta(delta.x, delta.y, zoom, alpha, cm, csize) } else if (csize == 1 && psize == 1 && this.current.firstKey() == this.previous.firstKey()) { @@ -1938,7 +1938,7 @@ // can jump let current = this.current.first(); let previous = this.previous.first(); - let delta = Points.subtract(current, previous); + let delta = Points$1.subtract(current, previous); return new InteractionDelta(delta.x, delta.y, 1.0, 0.0, current, csize) } return null @@ -2097,7 +2097,7 @@ if ( start && ended && - Points.distance(ended, start) < this.tapDistance + Points$1.distance(ended, start) < this.tapDistance ) { let t1 = this.timestamps.get(key); let tookLong = performance.now() > t1 + this.longPressTime; @@ -2116,7 +2116,7 @@ } if (this.tapPositions.has(key)) { let pos = this.tapPositions.get(key); - if (Points.distance(ended, pos) > this.tapDistance) { + if (Points$1.distance(ended, pos) > this.tapDistance) { this.unregisterTap(key); } } @@ -2149,7 +2149,7 @@ if ( start && ended && - Points.distance(ended, start) < this.tapDistance + Points$1.distance(ended, start) < this.tapDistance ) { let t1 = this.timestamps.get(key); let tookLong = performance.now() > t1 + this.longPressTime; @@ -2656,7 +2656,7 @@ let size = this.interaction.current.size; let limit = this.logInteractionsAbove; if (size > limit) { - Logging.log(`Number of interactions ${size} exceeds ${limit}`); + Logging$1.log(`Number of interactions ${size} exceeds ${limit}`); } } @@ -3129,14 +3129,14 @@ let v = this.velocities[i]; t += v.dt; let nv = { x: v.dx / v.dt, y: v.dy / v.dt }; - sum = Points.add(sum, nv); + sum = Points$1.add(sum, nv); count += 1; if (t > milliseconds) { break } } if (count === 0) return sum // empty vector - return Points.multiplyScalar(sum, 1 / count) + return Points$1.multiplyScalar(sum, 1 / count) } killAnimation() { @@ -3168,15 +3168,15 @@ let dt = this._throwDeltaTime(); // console.log("animateThrow", dt) let next = this.nextVelocity(this.velocity); - let prevLength = Points.length(this.velocity); - let nextLength = Points.length(next); + let prevLength = Points$1.length(this.velocity); + let nextLength = Points$1.length(next); if (nextLength > prevLength) { let factor = nextLength / prevLength; - next = Points.multiplyScalar(next, 1 / factor); + next = Points$1.multiplyScalar(next, 1 / factor); console.log('Prevent acceleration', factor, this.velocity, next); } this.velocity = next; - let d = Points.multiplyScalar(this.velocity, dt); + let d = Points$1.multiplyScalar(this.velocity, dt); this._move(d); this.onDragUpdate(d); @@ -3200,13 +3200,13 @@ if (this.velocity == null) { return false } - return Points.length(this.velocity) > 0.01 + return Points$1.length(this.velocity) > 0.01 } nextVelocity(velocity) { // Must be overwritten: computes the changed velocity. Implement // damping, collison detection, etc. here - let next = Points.multiplyScalar(velocity, this.throwDamping); + let next = Points$1.multiplyScalar(velocity, this.throwDamping); return { x: (this.movableX) ? next.x : 0, y: (this.movableY) ? next.y : 0 @@ -3390,8 +3390,8 @@ // towards the center of the stage let center = this.center; let target = this.container.center; - let delta = Points.subtract(target, center); - return Points.normalize(delta) + let delta = Points$1.subtract(target, center); + return Points$1.normalize(delta) } nextVelocity(velocity) { @@ -3453,7 +3453,7 @@ factor = collision; } // } - return Points.multiplyScalar({ x: dx, y: dy }, factor) + return Points$1.multiplyScalar({ x: dx, y: dy }, factor) } return super.nextVelocity(velocity) } @@ -3505,13 +3505,13 @@ moveTo(p, { animate = 0 } = {}) { let c = this.origin; - let delta = Points.subtract(p, c); + let delta = Points$1.subtract(p, c); this.move(delta, { animate: animate }); } centerAt(p, { animate = 0 } = {}) { let c = this.center; - let delta = Points.subtract(p, c); + let delta = Points$1.subtract(p, c); this.move(delta, { animate: animate }); } @@ -3574,19 +3574,19 @@ return } let origin = this.rotationOrigin; - let beta = Points.angle(origin, anchor); - let distance = Points.distance(origin, anchor); + let beta = Points$1.angle(origin, anchor); + let distance = Points$1.distance(origin, anchor); let { scale: newScale, zoom: thresholdedZoom } = this.calculateScale(zoom); - let newOrigin = Points.arc(anchor, beta + rotate, distance * thresholdedZoom); - let extra = Points.subtract(newOrigin, origin); - let offset = Points.subtract(anchor, origin); + let newOrigin = Points$1.arc(anchor, beta + rotate, distance * thresholdedZoom); + let extra = Points$1.subtract(newOrigin, origin); + let offset = Points$1.subtract(anchor, origin); this._move(offset); this.scale = newScale; this.rotation += rotate; - offset = Points.negate(offset); - offset = Points.add(offset, extra); - offset = Points.add(offset, translate); + offset = Points$1.negate(offset); + offset = Points$1.add(offset, extra); + offset = Points$1.add(offset, translate); this._move(offset); delta.x += extra.x; @@ -3957,7 +3957,7 @@ } mapPositionToPoint(point) { - return Points.fromPageToNode(this.element, point) + return Points$1.fromPageToNode(this.element, point) } isDescendant(parent, child, clickable = false) { @@ -4018,7 +4018,7 @@ - class DOMScatter extends AbstractScatter { + class DOMScatter$1 extends AbstractScatter { constructor( element, container, @@ -4199,7 +4199,7 @@ get origin() { let p = this.fromNodeToPage(0, 0); - return Points.fromPageToNode(this.container.element, p) + return Points$1.fromPageToNode(this.container.element, p) } get bounds() { @@ -4305,7 +4305,7 @@ bringToFront() { // this.element.parentNode.appendChild(this.element) // uo: On Chome and Electon appendChild leads to flicker - TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); + TweenLite.set(this.element, { zIndex: DOMScatter$1.zIndex++ }); } onTap(event, interaction, point) { @@ -4327,11 +4327,11 @@ } fromPageToNode(x, y) { - return Points.fromPageToNode(this.element, { x, y }) + return Points$1.fromPageToNode(this.element, { x, y }) } fromNodeToPage(x, y) { - return Points.fromNodeToPage(this.element, { x, y }) + return Points$1.fromNodeToPage(this.element, { x, y }) } _move(delta) { @@ -4370,7 +4370,7 @@ let newPostition = { x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top }; - let offset = Points.subtract(oldPostition, newPostition); + let offset = Points$1.subtract(oldPostition, newPostition); this.oldX = e.clientX; this.oldY = e.clientY; @@ -4424,7 +4424,7 @@ let oldPostition = { x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top }; this.element.style.transformOrigin = '50% 50%'; let newPostition = { x: this.element.getBoundingClientRect().left, y: this.element.getBoundingClientRect().top }; - let offset = Points.subtract(oldPostition, newPostition); + let offset = Points$1.subtract(oldPostition, newPostition); TweenLite.to(this.element, 0, { css: { left: '+=' + offset.x + 'px' } }); TweenLite.to(this.element, 0, { css: { top: '+=' + offset.y + 'px' } }); @@ -4435,7 +4435,7 @@ } } - DOMScatter.zIndex = 1000; + DOMScatter$1.zIndex = 1000; class CardLoader { constructor( @@ -4693,7 +4693,7 @@ frontLoaded(loader) { return new Promise((resolve, reject) => { - let scatter = new DOMScatter( + let scatter = new DOMScatter$1( this.cardWrapper, this.domScatterContainer, { @@ -4917,7 +4917,7 @@ bringToFront() { this.scatter.bringToFront(); - TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); + TweenLite.set(this.element, { zIndex: DOMScatter$1.zIndex++ }); } clickInfo() { @@ -5231,7 +5231,7 @@ /** A Popup that shows text labels, images, or html */ - class Popup extends Poppable { + class Popup$1 extends Poppable { /** * Creates an instance of Popup. * @param {any} [{ @@ -6095,7 +6095,7 @@ let notchPosition = (switchPos && point.y < 50) ? "topCenter" : "bottomCenter"; - let popup = new Popup({ + let popup = new Popup$1({ parent, context, fontFamily, @@ -6183,7 +6183,7 @@ let target = event.target; let x = event.pageX; let y = event.pageY; - let rect = Popup.targetRect(event); + let rect = Popup$1.targetRect(event); if (rect != null) { x = rect.left + rect.width / 2; y = rect.top + rect.height / 2; @@ -6194,7 +6194,7 @@ /** A Popup Menu that shows text labels in a vertical row. */ - class PopupMenu extends Popup { + class PopupMenu extends Popup$1 { /** * The constructor. * @constructor @@ -6411,7 +6411,7 @@ let found = document.elementFromPoint(global.x, global.y); let iframe = found.querySelector('iframe'); if (iframe) { - let p = Points.fromPageToNode(found, global); + let p = Points$1.fromPageToNode(found, global); let doc = iframe.contentWindow.document; let target = doc.elementFromPoint(p.x, p.y); if (target != null) { @@ -6436,7 +6436,7 @@ } simulateMouseEvent(type, point) { - let p = Points.fromPageToNode(this.frame, point); + let p = Points$1.fromPageToNode(this.frame, point); let event = new MouseEvent(type, { view: this.frame.contentWindow, bubbles: true, @@ -6451,7 +6451,7 @@ let doc = this.frame.contentWindow.document; for(let key of pointMap.keys()) { let point = pointMap.get(key); - let p = Points.fromPageToNode(this.frame, point); + let p = Points$1.fromPageToNode(this.frame, point); let touchTarget = doc.elementFromPoint(p.x, p.y); let touch = new Touch(undefined, touchTarget, key, p.x, p.y, p.x, p.y); @@ -6462,7 +6462,7 @@ simulateTouchEventChrome(type, point, pointMap) { let doc = this.frame.contentWindow.document; - let p = Points.fromPageToNode(this.frame, point); + let p = Points$1.fromPageToNode(this.frame, point); let touchTarget = doc.elementFromPoint(p.x, p.y); const touchObj = new Touch({ identifier: Date.now(), @@ -6490,7 +6490,7 @@ } simulateTouchEventSafari(type, point, pointMap, touchEventKey='targetTouches') { - let p = Points.fromPageToNode(this.frame, point); + let p = Points$1.fromPageToNode(this.frame, point); let data = { view: this.frame.contentWindow, bubbles: true, cancelable: true, @@ -7616,18 +7616,18 @@ 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 localClick = Points$1.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 }); + let topLeft = Points$1.fromPageToNode(element, rect); + let center = Points$1.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); + let distance = Points$1.distanceToRect(localClick, rect); distances.push(parseInt(distance)); }); @@ -7735,7 +7735,7 @@ * @class Highlight * @extends {Object} */ - class Highlight extends Object { + class Highlight$1 extends Object { static disableAnimations() { _HighlightEnabled = false; @@ -7883,7 +7883,7 @@ this.shrink(m, { stroke }); } - Highlight._createSVGMask(svgRoot, image, id); + Highlight$1._createSVGMask(svgRoot, image, id); // TweenLite.set(maskImage, { transformOrigin: `${tx}% ${ty}%` }) @@ -7898,7 +7898,7 @@ scale = 2, onExpanded = null } = {}) { - if (Highlight._isExpanded(target)) { + if (Highlight$1._isExpanded(target)) { console.log('Target is already expanded!'); return } else { @@ -7920,8 +7920,8 @@ 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); + let [mask, maskImage] = Highlight$1._getSVGMask(target, { svgRoot, image }); + let center = Highlight$1._calculateCenterRelativeTo(target, image); TweenLite.set(maskImage, { transformOrigin: `${center.x}% ${center.y}%` }); TweenLite.set(target, { transformOrigin: '50% 50%' }); @@ -7936,11 +7936,11 @@ } static toggleHighlight(node) { - if (Highlight._isExpanded(node)) { - Highlight.closeHighlight(node); + if (Highlight$1._isExpanded(node)) { + Highlight$1.closeHighlight(node); } else { - Highlight.openHighlight(node); + Highlight$1.openHighlight(node); } } @@ -7963,7 +7963,7 @@ let maskImage = svgRoot.getElementById(maskImageId); if (!mask || !maskImage) - [mask, maskImage] = Highlight._createSVGMask(circle, { svgRoot, image, id }); + [mask, maskImage] = Highlight$1._createSVGMask(circle, { svgRoot, image, id }); return [mask, maskImage] } @@ -8050,23 +8050,23 @@ } static _isExpanded(target) { - return target.classList.contains(Highlight.expandedClass) + return target.classList.contains(Highlight$1.expandedClass) } static _setExpanded(target) { - target.classList.add(Highlight.expandedClass); + target.classList.add(Highlight$1.expandedClass); } static _notExpanded(target) { - target.classList.remove(Highlight.expandedClass); + target.classList.remove(Highlight$1.expandedClass); } static closeHighlight(target, { animation = 0.5 } = {}) { console.log('Close Highlight'); - Highlight._notExpanded(target); + Highlight$1._notExpanded(target); // eslint-disable-next-line no-unused-vars - let [mask, maskImage] = Highlight._getSVGMask(target); + let [mask, maskImage] = Highlight$1._getSVGMask(target); TweenLite.to([target, maskImage], animation, { scale: 1 @@ -8078,7 +8078,7 @@ return event.stopPropagation(); - Highlight.animateCircle(event.target); + Highlight$1.animateCircle(event.target); return false } @@ -8098,7 +8098,2046 @@ } } - Highlight.expandedClass = 'expanded'; + Highlight$1.expandedClass = 'expanded'; + + /** + * A class that collects static methods to maintain the states and parts of + * EyeVisit like cards. + * + * The class is used as a namespace and should never called with new. + */ + class Card { + + static setup(context, modules = []) { + console.log("Setup Card...", modules); + context.modules = []; + modules.forEach(module => { + if (module.apply(context)) + context.modules.push(module.constructor.name); + }); + } + + /** + * + * + * @static + * @param {*} event + * @memberof Card + */ + static close(event) { + let context = this.getContext(event.target); + if (context) { + if (context.onClose) { + context.onClose(event); + } else context.parentNode.removeChild(context); + } else console.error("Could not find context!", event.target); + } + + /** + * Replaces a set of attributes using a provided function. + * + * @static + * @param {*} html + * @param {*} attribute + * @param {*} replaceFunc + * @memberof Card + */ + static _replaceAttributes(html, attribute, replaceFunc) { + let clickables = html.querySelectorAll(`[${attribute}]`); + clickables.forEach(element => { + + let attributeVal = element.getAttribute(attribute); + element.removeAttribute(attribute); + replaceFunc.call(this, element, attributeVal); + }); + } + + /** + * Replaces the onClick callback of the element with an + * Interaction mapper event. + * + * @static + * @param {*} element + * @param {*} attributeVal + * @returns + * @memberof Card + */ + static _replaceCallback(element, attributeVal) { + + if (element.tagName == "A") { + element.addEventListener("click", event => { event.preventDefault(); }); + } + + + let callbackParts = attributeVal.split("("); + let funcPart = callbackParts[0].trim(); + let trimmedArgs = callbackParts[1].trim(); + + //Remove the closing ')' + trimmedArgs = trimmedArgs.substring(0, trimmedArgs.length - 1); + + + let callParts = funcPart.split("."); + let argsStrings = trimmedArgs.split(",").filter(entry => { return entry.trim() != "" }); + + + let callStack = window; + do { + callStack = callStack[callParts.shift().trim()]; + } while (callParts.length > 0) + + let that = this; + + let interactionType = this.interactionType; + + //Remove the events on the circle. + // These are 'hardcoded' inside the convert.js. + if (element.tagName == "circle") return false + + + InteractionMapper.on(interactionType, element, (event) => { + + /** + * Replaces the strings from the listener with the cooresponding variables. + */ + let args = []; + argsStrings.forEach(arg => { + arg = arg.trim(); + if (arg == "this") + args.push(event.target); + else if (arg == "event") + args.push(event); + else { + const firstCharacter = arg[0]; + if (firstCharacter == "\"" || firstCharacter == "'") { + arg = arg.substring(1); + } + const lastCharacter = arg[arg.length - 1]; + if (lastCharacter == "\"" || lastCharacter == "'") { + arg = arg.substring(0, arg.length - 1); + } + + args.push(arg); + } + }); + event.stopPropagation(); + if (callStack) + callStack.call(that, ...args); + else { + console.error("Could not call callback function " + attributeVal, ...args); + } + }); + } + + + /** + * Transform the relative links to absolute ones. + * + * Currently covers: + * Tags: a,img, image, circle + * Attributes: xlink:href,href,src (first occurence only) + * + * @static + * @param {DomElement} element - The children of the element are inspected, if there are relative paths, that has to be adjusted to absolute ones. + * @returns + * @memberof Card + */ + static _adjustRelativeLinks(html) { + const that = this; + /* + This RegEx finds all requested tags[1], and all requested attributes[3] and replaces the relative path [4] with the absolute one. + while all other attributes [2],[5] are preserved. + */ + return html.replace(/<\s*(a|video|img|image|circle)\s(.*?)(xlink:href|href|src)\s*=\s*["'](\..*?)["']\s*(.*?)>/g, function (data) { + let path = that._getRelativePath(arguments[4]); + const tag = `<${arguments[1]} ${arguments[2]} ${arguments[3]}="${path}" ${arguments[5]}>`; + /* if (that.debug) */ console.log("Adjusted: ", tag); + return tag + }) + } + + /** + * Concats the given path with the relative path specified in the Card (as static variable). + */ + static _getRelativePath(src) { + let path = (this.relativePath != "") ? this.relativePath + "/" + src : src; + return path + } + + + /** + * Loads the card text using an ajax request. + * + * @static + * @param {*} path + * @returns + * @memberof Card + */ + static loadHTML(path) { + return new Promise((resolve, reject) => { + let request = new XMLHttpRequest(); + + request.onreadystatechange = function () { + if (this.readyState == 4) { + if (this.status == 200 || Card._isLocal()) { + try { + resolve(this.responseText); + } catch (e) { + reject(e); + } + } else reject(`Request failed '${path}'. Returned status ${this.status} and ready state ${this.readyState}.`); + } + }; + + request.open("GET", path, true); + request.send(); + }) + } + + /** + * TODO: Maybe put this in a utility script. + */ + static _isLocal() { + return (window.location.protocol == "file:") + } + + /** + * Returns true if the card belongs to an editable page / context. + * + * @static + * @returns + * @memberof Card + */ + static isEditable() { + return typeof mainController != 'undefined' && mainController.mode + } + + /** + * Finds the closest element with the given css class name along the parent chain. + * + * @static + * @param {any} node - The node to start on + * @param {any} klass - The css class name + * @returns {element} - The DOM element with the given class name + * @memberof Card + */ + static closestWithClass(node, klass) { + if (node && node.classList) { + if (node.classList.contains(klass)) + return node + return this.closestWithClass(node.parentNode, klass) + } + return null + } + + /** + * Returns a computed style property. Similar to the jQuery.css method. + * + * @param {any} node + * @param {any} attr + * @returns {any} + * @memberof Card + */ + static css(node, attr) { + return window.getComputedStyle(node, null).getPropertyValue(attr) + } + + /** + * Defines the standard drag behavior of cards. Copies the outer HTML of + * the card to the dataTransfer object of the drag event. + * + * Since dragover does not have the right to see the content we must specify + * additional type information to ensure that only accepted content + * can be dropped. Use the optional type parameter to specify custom types. + * + * The optional effectAllowed is set to 'all'. This is necessary to ensure + * that elements can be copied and moved. Unfortunately this + * adds a plus marker to the mouse that might be misleading. Use + * DataTransfer.dropEffect in dragover events to provide correct feedback instead. + * + * @static + * @param {any} event + * @param {string} [type="card"] + * @param {string} [effectAllowed="all"] + * @memberof Card + */ + static dragStart(event, type = "card", effectAllowed = 'all') { + event.dataTransfer.effectAllowed = effectAllowed; + let html = event.target.outerHTML; + event.dataTransfer.setData("text/html", html); + // https://stackoverflow.com/questions/11065803/determine-what-is-being-dragged-from-dragenter-dragover-events + event.dataTransfer.setData('iwmbrowser/' + type, ''); + } + + /** + * Opens a popup by calling the Popup.open method. + * + * @static + * @param {DOM element} parent - The DOM node to add the popup + * @param {any} position - The x,y position of the popup + * @param {any} content - The content to show within the popup + * @param {any} callback - A callback that is called on closing the popup + * @memberof Card + */ + static _openPopup(context, src, position, content, options = {}) { + if (this.debug) console.log("Card._openPopup", position); + + //logging + if (src) { + let strparts = src.split("/"); + let cardID = strparts[strparts.length - 2]; + let cardName = strparts[strparts.length - 1]; + //console.log('open popup:',cardID,cardName,context,content) + let msg = 'ShowPopup: ' + cardID + '/' + cardName; + console.log('Logging:', msg); + Logging.log(msg); + } + + let highlight = null; + if (options.highlight) { + highlight = options.highlight; + delete options.highlight; + } + + this._createPopup(context, position, content, options).then((popup) => { + if ( + //Test if meanwhile another popup was registered... + this._getPopup(context) || + // Or if an highlight was loaded, if the highlight already was closed. + highlight !== null && !this._isHighlightActive(context, highlight) + ) { + //.. if so remove the create popup instantly. + popup.remove(); + } else { + // Otherwise set the popup regularly. + let popupParagraphs = popup.element.querySelectorAll(".popupContent > *"); + + // Remove a design error of naming two adjacent elements popup. + // Todo: fix this properly and remove this code. + // let unnecessaryPopupElement = popup.element.querySelector(".popupContent > .popup") + // unnecessaryPopupElement.classList.remove("popup") + + popupParagraphs.forEach(popupParagraph => { + popupParagraph.setAttribute("draggable", false); + popupParagraph.addEventListener("mousedown", (event) => { + event.preventDefault(); + }); + }); + + + this._setPopup(context, popup, src); + } + }).catch(e => console.error(e)); + } + + + /** + * Closes a provided popup and unsets it on the context. + * + * @static + * @param {*} context + * @param {*} popup + * @memberof Card + */ + static closePopup(context, popup) { + if (popup) { + if (this.debug) console.log("Close Popup.", context, popup); + window.popup = popup; + popup.close(); + this._unsetPopup(context); + } else { + console.error("Requested to close popup, but popup was not found."); + } + } + + /** + * Creates a new Popup instance. + * + * @static + * @param {DomElement} context - The element on which the Popup should be created. + * @param {Point} position - The position, where the popup will be shown. + * @param {object} content - The content of the popup as required by the Popup class. + * @param {object} [options={}] - Additional options for the popup (optional). + * @returns {Promise} - Returns a Promise, which is resolved, when the Popup is fully loaded. + * @memberof Card + */ + static _createPopup(context, position, content, options = {}) { + + if (this.debug) console.log("Create Popup.", context, position, content, options); + let popup = new Popup(Object.assign({ + parent: context, + content + }, Object.assign({ + noStyle: true, + // TODO: Remove offset when positioning according to element position + // is working. + posOffset: 10 + }, options))); + + // Placing the popup when it required loading, + // it resulted in flahing up at the default position. + // We manually prevent this here. + popup.element.style.display = "none"; + + + let promise = new Promise((resolve) => { + if (popup.loaded) + resolve(popup); + else { + popup.onload = () => { + resolve(popup); + }; + } + }); + + promise.then((popup) => { + + popup.element.style.display = "block"; + popup.element.style.visibility = "hidden"; + popup.element.style.opacity = 0; + + popup.placeAt(position); + + InteractionMapper.on(this.interactionType, popup.element, e => { + this._cleanup(context); + }); + + Object.assign(popup.element.style, { + zIndex: this.zIndices.popup + }); + + TweenMax.to(popup.element, this.animation.popup, { + autoAlpha: 1, + ease: Power2.easeIn + }); + + }); + + return promise + } + + + static _overlayCleanup(context, overlay) { + + /** + * The cleanup functionality is now covered by the _cleanup function. + * It cleans up zoomables, popups and open image highlights. + * + * TEST if this intereferes with the editor. + */ + if (overlay) { + TweenMax.to(overlay, 0.2, { + autoAlpha: 0, onComplete: () => { + popup.remove(); + //this._cleanup(context) + //overlay.parentNode.removeChild(overlay) + } + }); + } + } + + /** + * Opens a popup for a glossary. Typically used as a onlick handler of a link. + * + * @static + * @param {any} event - The trigger event, typically a click event + * @returns {bool} false - Returns false to prevent default click action + * @memberof Card + */ + static loadPopup(event, context = null, node = null, local = null) { + + let editable = Card.isEditable(); + if (context == null) { + context = this.getContext(event.target); + } + if (node == null) { + node = event.target; + } + if (local == null) { + let globalClick = (event.center) ? event.center : { x: event.x, y: event.y }; + local = Points.fromPageToNode(context, globalClick); + } + if (this.debug) console.log("loadPopup", event); + + // Prevents loading the link in the current tab. + // Prevents loading the link in the current tab. + if (event.type != "Follow") + event.preventDefault(); + + if (editable && event.type == 'click') { + return false + } + + let overlay = document.createElement('div'); + let src = node.getAttribute("href"); + let parentArticle = node.closest('article'); + + const that = this; + let xhr = new XMLHttpRequest(); + xhr.open('get', src, true); + xhr.onreadystatechange = () => { + + if (this.debug) console.log("Popup Source: ", src); + if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 0)) { + if (editable) { + if (this.debug) console.log("Append overlay.", context); + overlay.classList.add('overlay'); + TweenMax.set(overlay, { autoAlpha: 0 }); + context.appendChild(overlay); + TweenMax.to(overlay, 0.5, { autoAlpha: 0.25 }); + } + + // Extract the body from the Popup site. + let parser = new DOMParser(); + let popupPage = parser.parseFromString(xhr.responseText, "text/html"); + + //Fix the relative path of loaded images in the popup. + popupPage.querySelectorAll("img").forEach(node => { + node.setAttribute("src", that._getRelativePath(node.getAttribute("src"))); + }); + let html = popupPage.body.innerHTML; + /** + * We do not want to use the popup class again. + * This results in problems when styling the popup. + * (You could style the .unselectable.popup, but that + * wouldn't be clean). + * + * Therefore the selector was changed: + * '.popup' => '.popupHtml' + * + * Which describes the construct even better than the + * '.popup' class. + */ + let selector = Card.popupHtmlSelector; + let content = { html, selector }; + + + let isSame = Card._checkForActiveSource(context, src); + Card._cleanup(context); + + + if (!isSame) { + Card._activateCorrespondingHighlights(context, node, parentArticle); + + + let callback = (popup, callback) => { + if (this.debug) console.log("Close popup (Editable = " + editable + ").", popup); + if (editable) { + let isDirty = mainController.askSaveNode(); + if (isDirty) + mainController.saveNode(html.innerHTML, url => { + if (callback) { + callback(); + } + saved(url); + this._overlayCleanup(context, overlay); + }); + else { + if (callback) { + callback(); + } + this._overlayCleanup(context, overlay); + } + mainController.popController(); + } + /** This may be in conflice with the cleanup method. */ + else { + //this._overlayCleanup(context, overlay) + popup.remove(); + } + }; + + Card._openPopup(context, src, local, content, { + closeCommand: callback + }); + } + } + + overlay.onclick = e => { + if (editable) + e.preventDefault(); + }; + //console.log("onreadystatechange", editable) + if (editable) { + if (this.debug) console.log("pushController", src, popup.insertedNode); + mainController.pushController(popup.insertedNode, src); + } + }; + xhr.send(); + return false + } + + /** + * When an highlight link is clicked, this method activates all + * corresponding highlights. + * + * @static + * @param {DomElement} context - The context of the element. + * @param {DomElement} node - The node that (may) contain a highlightId. + * @param {DomElement} parent - The parent element that may contain more highlightIds. + * @memberof Card + */ + static _activateCorrespondingHighlights(context, node, parent) { + let highlightId = node.getAttribute('data-highlight-id'); + // console.log("Request Highlight: " + highlightId) + + let correspondingHighlights = []; + if (highlightId) { + let queryString = 'circle[data-highlight-id="' + highlightId + '"]'; + if (parent) { + correspondingHighlights = parent.querySelectorAll(queryString); + } + if (correspondingHighlights.length > 0) { + for (let highlight of correspondingHighlights) { + if (highlight.parentNode && highlight.parentNode.nodeName.toLowerCase() == 'g') { + Highlight.openHighlight(highlight, { animation: Card.highlightAnimation }); + this._addHighlight(context, highlight); + } + } + } + } + } + + /** + * Tests if any open item already contains the requested Source. + * + * @static + * @param {DomElement} context - Dom context we are in. + * @param {string} src - Source as dataUrl. + * @returns {boolean} - True if source is already active, false otherwise. + * @memberof Card + */ + static _checkForActiveSource(context, src) { + + let requestedSame = false; + let activePopup = Card._getPopup(context); + let activeHighlights = Card._getHighlights(context); + + if (activePopup) { + let activeSrc = Card._getPopupSource(activePopup); + if (activeSrc && src == activeSrc) { + requestedSame = true; + } + } + + if (!requestedSame) { + for (let highlight of activeHighlights) { + let activeSrc = Card._getHighlightSource(highlight); + if (activeSrc == src) { + requestedSame = true; + break + } + } + } + + return requestedSame + } + + /** + * Opens a popup for a highlight. Typically used as a onlick handler of a link. + * + * @static + * @param {any} event - The trigger event, typically a click event + * @returns {bool} false - Returns false to prevent default click action + * @memberof Card + */ + static loadHighlightPopup(event) { + + if (this.debug) console.log("Load Highlight Popup: ", event); + let node; + if (event.firstTarget) { + node = event.firstTarget; + } else { + node = event.target; + } + let context = this.getContext(node); + event.stopPropagation(); + + /** + * This node is the documents body, as events wont work + * on svg elements properly. We need a workaround for that. + */ + let src = node.getAttribute("xlink:href"); + let isSame = this._checkForActiveSource(context, src); + + this._cleanup(context); + if (!isSame) { + this._addHighlight(context, node); + + Highlight.openHighlight(node, { + animation: Card.highlightAnimation, + onExpanded: () => { + // We assume it's always a circle. This may break, when other svg shapes are used. + let x = node.getAttribute("cx"); + let y = node.getAttribute("cy"); + let position = { x, y }; + + let radius = parseFloat(node.getAttribute("r")); + + /* + As the popup is appended directly to the card. We have to + transform the location from the subcard-/svg-space to the + card space. + */ + + let svgRoot = node.closest("svg"); + + let svgPoint = svgRoot.createSVGPoint(); + svgPoint.x = position.x; + svgPoint.y = position.y - radius; + let matrix = node.getCTM(); + + let point = svgPoint.matrixTransform(matrix); + let global = Points.fromNodeToPage(node.closest("div"), point); + let local = Points.fromPageToNode(context, global); + + let overlay = document.createElement('div'); + //TODO: Check if this is necessary for the editor. + // overlay.onclick = e =>{} + + // This is not really efficient, as + // we could load the data while the circle is animating. + // but for simplicity it's just done here for now. + // TODO: Adjust to load while animating (Problem: Unload when cancelled). + console.log(src); + this._loadPopupContent(src) + .then(content => { + this._openPopup(context, src, local, content, { + highlight: node, + closeCommand: (popup, callback) => { + this._overlayCleanup(context, overlay); + popup.remove(); + } + }); + }) + .catch(err => { console.error(err); }); + } + }); + } + } + + /** + * Loads the popup from a provided source. + * + * @static + * @private + * @param {string} source - Url to a popup file. + * @returns {Promise} - Returns a promise, that's resolved when the data is loaded. + * @memberof Card + */ + static _loadPopupContent(source) { + return new Promise((resolve, reject) => { + let xhr = new XMLHttpRequest(); + xhr.open('get', source, true); + xhr.onreadystatechange = () => { + if (xhr.readyState == 4) { + + if ((xhr.status == 200 || xhr.status == 0)) { + let html = this.postProcessResponseText(xhr.responseText); + let selector = Card.popupHtmlSelector; + let content = { html: html.body.innerHTML, selector }; + resolve(content); + } else { + reject(` Popup request failed (Code: ${xhr.status}): Could not load resource: ${src}`); + } + } + }; + xhr.send(); + }) + } + + static openZoomable(event) { + + let node = event.target; + + //console.log("Open zoomable: ", node, node.classList) + + let editable = Card.isEditable(); + if (editable) { + if (node.classList.contains('zoomable-icon')) + this._openZoomable(event); + return + } + this._openZoomable(event); + } + + + /** + * Retrieve a Rectangle object from the a given zoomable. + * + * @static + * @param {DomElement} zoomable - The zoomable, the rectangle should be calculated for. + * @param {DomElement} wrapper - Specifies the coordinate system of the generated rectangle. + * @returns {object} - Returns an object of the rectangle in the wrapper's coordinate system in the form of {x ,y ,width , height}. + * @memberof Card + */ + static zoomableCurrentGeometry(zoomable, wrapper) { + + + /* + I don't think it's wise, that the zoomable calculation relies on + some icon that may or may not be present. When the same calculation can be + done using the bounding box of the desired element. + - SO + */ + + //The div is cloned and animated, therefore we need it's style! + let actuallyZoomedItem = zoomable.querySelector("div"); + + let zoomableStyle = window.getComputedStyle(actuallyZoomedItem); + + let svgElement = zoomable.querySelector("svg"); + let videoElement = zoomable.querySelector("video"); + let curStyle = null; + if (svgElement) { + curStyle = window.getComputedStyle(svgElement); + } + if (videoElement) { + curStyle = window.getComputedStyle(videoElement); + } + + let imageWidth = parseFloat(curStyle.width); + let imageHeight = parseFloat(curStyle.height); + + let globalBottomRight = { + x: imageWidth, + y: imageHeight + }; + + globalBottomRight = Points.fromNodeToPage(zoomable, globalBottomRight); + let globalFigurePos = Points.fromNodeToPage(zoomable, { x: 0, y: 0 }); + let localFigurePos = Points.fromPageToNode(wrapper, globalFigurePos); + let relativeBottomRight = Points.fromPageToNode(zoomable, globalBottomRight); + + let width = relativeBottomRight.x + parseFloat(zoomableStyle.borderLeftWidth) + parseFloat(zoomableStyle.borderRightWidth);/*+ zoomIconRight*/ + let height = relativeBottomRight.y + parseFloat(zoomableStyle.borderTopWidth) + parseFloat(zoomableStyle.borderBottomWidth); /*+ zoomIconRight*/ + return { x: localFigurePos.x, y: localFigurePos.y, width, height } + } + + + /** + * Opens a zoomable object, which can be a figure containing an image or video or an image group + * + * @static + * @param {any} event - The trigger event, typically a click event + * @memberof Card + */ + static _openZoomable(event, editable = false) { + event.stopPropagation(); + let node = event.target; + let wrapper = this.getContext(node); + + let zoomable = node.closest('figure'); + + // load mainimg - if none exists, there is nothing to open + let img = zoomable.querySelector('.mainimg'); + if (img == null) { + return + } + + //close other open elements before opening the zoomable: + //let removeDelay = this.removeActiveZoomable(wrapper, true) ? 0 : 0 + this._cleanup(wrapper); + + // Clone the zoomable and create a clone that is zoomed instead. + let current = this.zoomableCurrentGeometry(zoomable, wrapper); + let zoomedFig = zoomable.cloneNode(true); + let caption = zoomedFig.querySelector("figcaption.cap"); + caption.parentNode.removeChild(caption); + this._toggleNextIcon(zoomedFig); + this._setZoomable(wrapper, zoomable, zoomedFig); + + if (!editable) { + /** + * I don't remember why we need yet another wrapper for the zoomable. + * This should be investigated. + * -SO + */ + let zoomContainer = document.createElement('div'); + zoomContainer.classList.add("zoomable-wrapper"); + Object.assign(zoomContainer.style, { + position: "absolute", + top: 0, + left: 0, + zIndex: 200 + }); + + wrapper.appendChild(zoomContainer); + + //Note: Remaned ZoomedFigure to zoomed-figure + zoomedFig.className = 'zoomed-figure'; + zoomedFig.style.zIndex = this.zIndices.zoomedFigure; + let zoomedG = zoomedFig.querySelector('g'); + if (zoomedG && !editable) + zoomedG.parentNode.removeChild(zoomedG); + zoomContainer.appendChild(zoomedFig); + + let zoomedImg = zoomedFig.querySelector('.mainimg'); + zoomedImg.removeAttribute('onclick'); + + //set the values of corresponding zoomable element (needed when the zoomable is closed indirectly via removeActiveZoomable) + zoomedFig.zoomable = zoomable; + zoomedFig.zoomableGeometry = current; + + //play video + let videoElement = zoomedFig.querySelector("video"); + if (videoElement) { + videoElement.play(); + } + + //logging + let imgElt = zoomedFig.querySelector("image"); + let videoElt = zoomedFig.querySelector("video"); + //console.log('open zoomable:',imgElt,videoElt) + if (imgElt) { + let imgSrc = imgElt.getAttribute('xlink:href'); + let strparts = imgSrc.split("/"); + let cardID = strparts[strparts.length - 2]; + let cardName = strparts[strparts.length - 1]; + let msg = 'ShowImage: ' + cardID + '/' + cardName; + console.log('Logging:', msg); + Logging.log(msg); + } + if (videoElt) { + let videoSrc = videoElt.getAttribute('src'); + let strparts = videoSrc.split("/"); + let cardID = strparts[strparts.length - 2]; + let cardName = strparts[strparts.length - 1]; + let msg = 'ShowVideo: ' + cardID + '/' + cardName; + console.log('Logging:', msg); + Logging.log(msg); + } + + // (At least in this style) We don't want the user to 'hit' the icon + // It's just an indicator that an action is possible. The click must be + // captured from the whole subcard. + + InteractionMapper.on(this.interactionType, zoomedFig, (event) => { + this._cleanup(wrapper); + }); + + let zoomedFigStyle = window.getComputedStyle(zoomedFig); + let borderX = parseFloat(zoomedFigStyle.borderLeftWidth) + parseFloat(zoomedFigStyle.borderRightWidth); + let borderY = parseFloat(zoomedFigStyle.borderBottomWidth) + parseFloat(zoomedFigStyle.borderTopWidth); + + + const scaleFactor = 2; + const transformOrigin = 'bottom right'; + + TweenMax.set(zoomedFig, { + x: current.x, + y: current.y, + width: current.width + borderX, + height: current.height + borderY, + transformOrigin + }); + TweenMax.set(zoomable, { opacity: 0 }); + + let icon = zoomedFig.querySelector(".icon"); + TweenMax.set(icon, { + transformOrigin + }); + zoomedFig.style.transformOrigin = "calc(100% - " + parseFloat(zoomedFigStyle.borderRightWidth) + "px) calc(100% - " + parseFloat(zoomedFigStyle.borderBottomWidth) + "px)"; + + + let tl = new TimelineLite(); + + let zoomCaption = zoomedFig.querySelector(".zoomcap"); + + tl.to(zoomedFig, Card.animation.zoomable, { + ease: Power2.easeIn, + css: { + scaleX: scaleFactor, + scaleY: scaleFactor + } + }, 0).set(zoomCaption, { + css: { + display: "block", + opacity: 0, + x: -parseFloat(zoomedFigStyle.borderLeftWidth), + width: current.width + borderX + } + }).to(zoomCaption, this.animation.fade, { + autoAlpha: 1 + }); + + } else this._openZoomableEditorBehaviour(wrapper, img, zoomable, zoomedFig, current); + } + + /** + * Selects and transforms the zoomicon from a zoomicon to a closeicon + * or the other way around. + * + * @static + * @param {DomElement} parent - Parent to be searched for an zoomicon. + * @memberof Card + */ + static _toggleNextIcon(parent) { + let zoomIcon = Card._findNextIcon(parent); + const closeClass = "close"; + const zoomClass = "zoom"; + + if (zoomIcon.classList.contains(closeClass)) { + zoomIcon.classList.remove(closeClass); + zoomIcon.classList.add(zoomClass); + } else if (zoomIcon.classList.contains(zoomClass)) { + zoomIcon.classList.remove(zoomClass); + zoomIcon.classList.add(closeClass); + } else { + console.error(`Error Toggleing Zoomicon: It did neither contain a class named ${closeClass} or ${zoomClass}.`); + } + } + + static _findNextIcon(parent) { + return parent.querySelector('.icon') + } + + + static _openZoomableEditorBehaviour(wrapper, img, zoomable, zoomedFig, current) { + + let zoomContainer = document.createElement('div'); + let zoomIcon = zoomable.querySelector('.zoom-icon'); + zoomContainer.style.position = 'relative'; + zoomContainer.style.zIndex = this.zIndices.zoomable; + wrapper.appendChild(zoomContainer); + let zoomParent = zoomable.parentNode; + zoomParent.appendChild(zoomedFig); + zoomedFig.style.opacity = 0.5; + zoomContainer.appendChild(zoomable); + TweenMax.set(zoomable, { x: current.x, y: current.y, width: current.width, height: current.height }); + let editor = mainController.topController().ensureEditor(img); + let savedDisplay = zoomIcon.style.display; + let iconClone = zoomIcon.cloneNode(true); + zoomIcon.style.display = 'none'; + let savedOnClick = zoomIcon.getAttribute('onclick'); + zoomIcon.removeAttribute('onclick'); + iconClone.src = iconClone.src.replace('info.svg', 'close.svg'); + iconClone.style.zIndex = 1000; + iconClone.classList.add('zoomed-icon'); + iconClone.onclick = event => { + zoomIcon.style.display = savedDisplay; + iconClone.remove(); + zoomIcon.setAttribute('onclick', savedOnClick); + zoomedFig.remove(); + zoomContainer.remove(); + zoomParent.appendChild(zoomable); + TweenMax.set(zoomable, { x: 0, y: 0 }); + zoomable.onmousedown = null; + zoomable.onmousemove = null; + zoomable.onmouseup = null; + zoomable.onmousewheel = null; + }; + wrapper.appendChild(iconClone); + TweenMax.set(iconClone, { x: current.iconPos.x, y: current.iconPos.y }); + + zoomable.onmousedown = event => { + if (this.debug) console.log('mousedown', event.target); + event.preventDefault(); + zoomable.dragging = true; + zoomable.dragStartPos = { x: event.pageX, y: event.pageY }; + }; + zoomable.onmousemove = event => { + if (this.debug) console.log('onmousemove', event.target); + if (zoomable.dragging) { + event.preventDefault(); + let dx = event.pageX - zoomable.dragStartPos.x; + let dy = event.pageY - zoomable.dragStartPos.y; + TweenMax.set([zoomable, iconClone], { x: '+=' + dx, y: '+=' + dy }); + zoomable.dragStartPos = { x: event.pageX, y: event.pageY }; + if (editor) { + editor.showControls(); + } + } + }; + zoomable.onmouseup = event => { + zoomable.dragging = false; + }; + + let startZoom = 1; + zoomable.onmousewheel = event => { + event.preventDefault(); + let direction = event.detail < 0 || event.wheelDelta > 0; + const zoomFactor = 1.1; + let zoom = direction ? zoomFactor : 1 / zoomFactor; + startZoom *= zoom; + + TweenMax.set(zoomable, { scale: startZoom }); + if (editor) { + editor.showControls(); + } + }; + return + } + + + /** + * Closes a zoomable object with animation + * + * @static + * @param {any} wrapper - the wrapper containing the index card + * @param {any} div - the figure containing the relevant elements + * @param {any} zoomable - the zoomable element, from which the zoomed figure originates + * @param {any} rect - the target rect for the tween (typically the top left width height of the zoomable) + * @memberof Card + */ + static closeZoomable(context, zoomable, zoomedFig) { + + if (this.debug) console.log("Close Zoomable", context, zoomable, zoomedFig); + + if (zoomable) { + this._unsetZoomable(context); + let caption = zoomable.querySelector("figcaption.cap"); + zoomable.removeChild(caption); + zoomable.appendChild(caption); + let zoomedCaption = zoomedFig.querySelector("figcaption.zoomcap"); + + + TweenMax.to(zoomedCaption, this.animation.fade, { + autoAlpha: 0, + }); + + TweenMax.to(zoomedFig, this.animation.zoomable, { + css: { + scaleX: 1, + scaleY: 1 + }, + onComplete: () => { + TweenMax.set(zoomable, { + opacity: 1 + }); + let div = zoomedFig.parentNode; + let videoElement = div.querySelector('video'); + if (videoElement) + videoElement.pause(); + div.parentNode.removeChild(div); + } + }); + } + } + + /** + * Starts and controls the expansion animation of an index card. + * The assumed card structure is as follows: + *
+ *
+ *

...

+ *
+ *
...
+ * + *
+ *
+ *
+ * The basic idea is that the card is cloned. In the animation the clone si expanded while the original is faded out. + * + * @param {*} card - The card to expand + * @param {string} html - The original HTML of the card + * @param {*} tagName - The tagname of the element that is used as exanded element + * @param {*} src - The src of the expanded element + * @param {*} callback - A callback that is called when the expanded element is closed + */ + static expandIndexCard(card, html, tagName = 'article', src = null, callback = null) { + let editable = Card.isEditable(); + + let context = this.getContext(card); + let indexbox = this.closestWithClass(card, 'mainview'); + + this._cleanup(context); + + + let angle = 0; // UO: out of context, to be computed from the scatter + let clone = card.cloneNode(true); + + + /** + * We have to reorder the clone, as it still contains the + * preview text image. And the new html is + * inserted before everything else. + */ + let cloneWrapper = clone.querySelector(".wrapper"); + const article = html.body.querySelector("article"); + + let subcardContent = clone.querySelector(".subcard-content"); + subcardContent.appendChild(article); + cloneWrapper.parentNode.removeChild(cloneWrapper); + + /* Removes the 'default' cleanup on the card */ + clone.removeAttribute("onclick"); + InteractionMapper.on(this.interactionType, clone, (event) => { + this._cleanup(context); + }); + + let articleClone = clone.querySelector(tagName); + + + let globalPreviewRect = Card._getGlobalRect(card); + let globalIndexCardRect = Card._getGlobalRect(indexbox); + let localOrigin = Points.fromPageToNode(indexbox, Rect.getPosition(globalPreviewRect)); + + let scaleX = globalPreviewRect.width / globalIndexCardRect.width; + let scaleY = globalPreviewRect.height / globalIndexCardRect.height; + + let padding = parseInt(this.css(indexbox, 'padding')); + let maxWidth = this.css(card, 'max-width'); + + TweenMax.set(clone, { + css: { + position: 'absolute', + width: globalIndexCardRect.width, + height: globalIndexCardRect.height, + maxWidth: '100%', + margin: 0, + zIndex: this.zIndices.article + } + }); + + TweenMax.set(articleClone, { + autoAlpha: 0 + }); + + TweenMax.set(card, { css: { maxWidth: '100%' } }); + TweenMax.set(clone, { + x: localOrigin.x - padding, + y: localOrigin.y - padding, + scaleX, + scaleY, + transformOrigin: '0% 0%', + rotation: angle, + }); + indexbox.prepend(clone); + + let titlebar = clone.querySelector(".titlebar"); + let title = titlebar.querySelector("h2"); + let titlebarStyle = window.getComputedStyle(titlebar); + let start = { + height: parseInt(titlebarStyle.height) + }; + + if (this.dynamicHeight) { + let targetHeight = subcardContent.offsetHeight; + console.log(targetHeight); + subcardContent.classList.add("dynamic-height"); + /** + * Scale the content from 100% to it's target size. + */ + // TweenMax.set(subcardContent, { + // height: "100%" + // }) + // TweenMax.to(subcardContent, Card.animation.articleTransition, { + // height: targetHeight + "px" + // }) + } + + //jquery hyphenate below + $('.column').not('.overview').children('p').hyphenate('de'); + + //logging + if (src) { + let strparts = src.split("/"); + let cardID = strparts[strparts.length - 2]; + let cardName = strparts[strparts.length - 1]; + strparts = card.className.split(" "); + let cardType = strparts[1]; + let msg = 'Card: ' + cardID + ': openTopic: ' + cardType + ', ' + cardName; + console.log('Logging:', msg); + Logging.log(msg); + } + + let desiredBorderBottomWidth = parseInt(window.getComputedStyle(titlebar).borderBottomWidth); + TweenMax.to(clone, Card.animation.articleTransition, { + x: -padding, + y: -padding, + ease: ExpoScaleEase.config(scaleX, 1), + scale: 1, + rotation: 0, + onComplete: () => { + card.classList.add("visited"); + }, + onUpdateParams: ['{self}'], + onUpdate: (self) => { + let transform = self.target._gsTransform; + TweenMax.set(title, { + scale: 1 / transform.scaleX + }); + TweenMax.set(titlebar, { + height: start.height * 1 / transform.scaleY + }); + + // Retain the border at same visual thickness. + titlebar.style.borderBottomWidth = desiredBorderBottomWidth / transform.scaleY + "px"; + } + }); + + TweenMax.to([articleClone], this.animation.articleTransition / 2, { + delay: this.animation.articleTransition / 2, + autoAlpha: 1 + }); + + if (editable) { + mainController.pushController(articleClone, src); + } + + Card._disableCardCloseButton(context); + + const closeAnimation = () => { + //logging + if (src) { + let strparts = src.split("/"); + let cardID = strparts[strparts.length - 2]; + let cardName = strparts[strparts.length - 1]; + strparts = card.className.split(" "); + let cardType = strparts[1]; + let msg = 'Card: ' + cardID + ': closeTopic: ' + cardType + ', ' + cardName; + console.log('Logging:', msg); + Logging.log(msg); + } + + Card._cleanup(context); + Card._unsetSubcard(context); + + this._enableCardCloseButton(context); + + let previewTitlebar = card.querySelector(".titlebar"); + let titlebarStyle = window.getComputedStyle(previewTitlebar); + let titlebar = clone.querySelector(".titlebar"); + + TweenMax.to(titlebar, this.animation.articleTransition, { + height: parseInt(titlebarStyle.height) + }); + + TweenMax.to(articleClone, this.animation.articleTransition / 2, { + autoAlpha: 0 + }); + + let title = titlebar.querySelector("h2"); + let original = { + height: parseInt(titlebarStyle.height) + }; + + if (this.dynamicHeight) { + TweenMax.to(subcardContent, this.animation.articleTransition, { + height: "100%" + }); + } + + TweenMax.set(card, { autoAlpha: 1, css: { maxWidth } }); + TweenMax.to(clone, this.animation.articleTransition, { + x: localOrigin.x - padding, + y: localOrigin.y - padding, + scaleX, + scaleY, + ease: ExpoScaleEase.config(1, scaleX), + rotation: angle, + onComplete: () => { + // article.remove() + TweenMax.to(clone, this.animation.fade, + { + //delay: 0.2, + autoAlpha: 0, + onComplete: + () => { + if (editable) { + mainController.popController(); + } + clone.remove(); + } + }); + }, + onUpdateParams: ["{self}"], + onUpdate: function (self) { + let transform = self.target._gsTransform; + + TweenMax.set(title, { + scale: 1 / transform.scaleX + }); + + TweenMax.set(titlebar, { + height: original.height * 1 / transform.scaleY + }); + + // Retain the border at same visual thickness. + titlebar.style.borderBottomWidth = desiredBorderBottomWidth / transform.scaleY + "px"; + } + }); + }; + + //TODO consider renaming it to something more intuitive. + let iconClone = clone.querySelector('.card-icon'); + + if (iconClone == null) { + iconClone = clone.querySelector(".cardicon"); + console.warn("Legacy selector. Change it to 'card-icon' and find a more suitable name."); + } + + if (iconClone.tagName == "img") { + iconClone.src = iconClone.src.replace('info.svg', 'close.svg'); + } + //console.log("ICON: ", iconClone) + iconClone.classList.remove("info"); + iconClone.classList.add("close", "view-button", "transparent-background"); + + // We append the icon clone to the subcard-content. + // Then it's always on the content and not on the background + // (If there is one). + if (this.dynamicHeight) { + article.appendChild(iconClone); + } + // Use the 'tap' event for closing. + // Otherwise the subcard cannot be closed, + // when another subcard is touched. + InteractionMapper.on("tap", iconClone, () => { + if (editable) { + let isDirty = mainController.askSaveNode(); + if (isDirty) { + mainController.saveNode(html.innerHTML, url => { + callback(url); + closeAnimation(); + }); + } + else { + closeAnimation(); + } + } + else { + closeAnimation(); + } + }); + } + + /** + * Opens the index card. Called by the zoom icon click handler. + * The assumed card structure is as follows: + *
+ *
+ *

...

+ *
+ *
...
+ * + *
+ *
+ *
+ * Loads the DOM element from a file source and adds it before the card-content to ensure that the article content + * can be positioned below the heading. + * + *
+ *
INSERTED CONTENT
+ *
+ * ... + *
+ *
+ * + * @static + * @param {any} event - The trigger event, typically a click event + * @param {string} src - The file source, typically a relative url + * @memberof Card + */ + static openIndexCard(event, src) { + //console.log("openIndexCard", src) + /* + * Called by the expandIndexCard(...) + */ + let target = event.target; + const saveCallback = (url) => { + let handler = `Card.openIndexCard(event, '${url}')`; + console.log("File has changed", target, handler); + + //TODO If this is required, it should be accessing the interaction type. + target.setAttribute("onclick", handler); + }; + + let context = this.getContext(target); + let subcard = this._getSubcard(context); + //console.log("openIndexCard", { context, subcard }) + //Dont proceeed if a subcard is active + if (subcard != null) + return + + // In edit mode we only accept icon clicks + let editable = Card.isEditable(); + if (editable && !target.classList.contains('icon')) { + return + } + + let card = this.closestWithClass(target, 'subcard'); + this._setSubcard(context, card); + const relativeSource = this._getRelativePath(src); + + let xhr = new XMLHttpRequest(); + xhr.open('get', relativeSource, true); + xhr.onreadystatechange = () => { + if (xhr.readyState == 4 && (xhr.status == 200 || xhr.status == 0)) { + let html = xhr.responseText; + let parsedHTML = this.postProcessResponseText(html); + + // TODO: What is this good for? + // let article = parsedHTML.querySelector('article') + // card.insertAdjacentElement('afterbegin', article) + // TweenMax.set(article, { autoAlpha: 0 }) + + Card.expandIndexCard(card, parsedHTML, 'article', relativeSource, saveCallback); + } + }; + xhr.onerror = () => { + alert('Unable to load ' + src); + }; + xhr.send(); + } + + + static _selectCardCloseButton(context) { + return context.querySelector(".mainview > .button.close") + } + + static _enableCardCloseButton(context) { + //console.log("ENABLE") + let btn = this._selectCardCloseButton(context); + //console.log(btn) + btn.classList.remove("disabled"); + } + + static _disableCardCloseButton(context) { + let btn = this._selectCardCloseButton(context); + btn.classList.add("disabled"); + } + + + /** + * Provides the nearest info-card of the provided item. + * + * @static + * @param {DomElement} child - A dom element for which the context shall be retrieved. + * @returns {DomElement} - The containing context / info-card. + * @memberof Card + */ + static getContext(child) { + let dom = child.closest(".info-card"); + if (!dom) { + dom = child.querySelector(".wrapper"); + console.warn("Change the 'wrapper' class to 'info-card' it's more suitable."); + } + return dom // (dom == null) ? document.body : dom + } + + /** + * The cleanup method closes all active Popups, Zoomables and Highlights. + * + * @static + * @param {DomElement} context - The dom element to inspect. + * @memberof Card + */ + static _cleanup(context) { + + let [zoomable, zoomedFig] = this._getZoomable(context); + if (zoomable && zoomedFig) { + this.closeZoomable(context, zoomable, zoomedFig); + } + + let popup = this._getPopup(context); + if (popup) { + this.closePopup(context, popup); + } + + let highlights = this._getHighlights(context); + highlights.forEach(highlight => { + Highlight.closeHighlight(highlight, { animation: Card.highlightAnimation }); + }); + + this._unsetHighlights(context); + } + + + + /** + * Retrieves an Rectangle for an element in the local space of a provided context. + * + * Note: This works also for rotated DomElements unlike the Element.getBoundingClientRectangle method. + * + * @static + * @param {DomElement} context - The element that defines the space, we want the rectangle to be in. + * @param {DomElement} element - The element, we want to get the rectangle for. + * @returns {DomRect} - Returns a DomRect representing the element outline in the space of context. + * @memberof Card + */ + static _getContextRect(context, element) { + let global = this._getGlobalRect(element); + let localPosition = Points.fromPageToNode(context, { x: global.x, y: global.y }); + return DOMRectReadOnly.fromRect({ + x: localPosition.x, + y: localPosition.y, + width: global.width, + height: global.height + }) + } + + + /** + * Gets a rectangle in global space for a provided element. + * + * Note: This works also for rotated DomElements unlike the Element.getBoundingClientRectangle method. + * + * @static + * @param {DomElement} element + * @returns {DomRect} - Returns a rectangle that specifies the location in global space. + * @memberof Card + */ + static _getGlobalRect(element) { + let cardStyle = window.getComputedStyle(element); + let cardWidth = parseInt(cardStyle.width); + let cardHeight = parseInt(cardStyle.height); + let globalPosition = Points.fromNodeToPage(element, { x: 0, y: 0 }); + return DOMRectReadOnly.fromRect({ + x: globalPosition.x, + y: globalPosition.y, + width: cardWidth, + height: cardHeight + }) + } + + /** + * Adjusts all links + * + * @static + * @param {*} htmlString + * @returns + * @memberof Card + */ + static postProcessResponseText(htmlString) { + let editable = this.isEditable(); + + htmlString = this._adjustRelativeLinks(htmlString); + //console.log(htmlString) + let parser = new DOMParser(); + let html = parser.parseFromString(htmlString, "text/html"); + if (!editable) { + this._replaceAttributes(html, "onclick", this._replaceCallback); + } + let zoomableWrappers = html.querySelectorAll(".svg-wrapper"); + zoomableWrappers.forEach(wrapper => { + let svg = wrapper.querySelector("svg"); + Object.assign(wrapper.style, { + width: svg.getAttribute("width") + "px", + height: svg.getAttribute("height") + "px" + }); + }); + let zoomableVideoWrappers = html.querySelectorAll(".video-wrapper"); + zoomableVideoWrappers.forEach(wrapper => { + let video = wrapper.querySelector("video"); + Object.assign(wrapper.style, { + width: video.getAttribute("width") + "px", + height: video.getAttribute("height") + "px" + }); + }); + + return html + } + + /** + * There occured a problem with the tap event, as it was canceled by the event on the svg element + * therefore we restructured the interaction, that only the svg listens for events and if the target is + * a circle (more are not supported yet) the target is adjusted using the .firstTarget of the event, + * that is provided by the Hammer.propergate plugin. + */ + static openPopupOrZoomable(event) { + let target = this._preferFirstTarget(event); + if (target.tagName == "circle") { + Card.loadHighlightPopup(event); + event.stopPropagation(); + } else { + Card.openZoomable(event); + } + } + + static pointRectDist(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 findNearbyPopups(event, card) { + if (event.target) { + //let column = event.target.closest(".column") + let indexbox = this.closestWithClass(card, 'mainview'); + if (indexbox != null) { // column != null || + let links = Array.from(indexbox.getElementsByTagName("a")); + let globalClick = (event.center) ? event.center : { x: event.x, y: event.y }; + let localClick = Points.fromPageToNode(indexbox, globalClick); + + let linkRects = links.map(link => { + let rect = link.getBoundingClientRect(); + let topLeft = Points.fromPageToNode(indexbox, rect); + let center = Points.fromPageToNode(indexbox, {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 = []; + linkRects.forEach(rect => { + let distance = Card.pointRectDist(localClick, rect); + if (distance == 0.0) { + // Distance == 0.0 indicates an inside relation. Since these + // are already handled by the links themselves we put these cases + // to the end of the sorted list below + distance = 100000.0; + } + distances.push(parseInt(distance)); + }); + + let closestLinkIndex = distances.indexOf(Math.min(...distances)); + let closestLink = links[closestLinkIndex]; + + console.log("finding closest links", closestLink, closestLink.getClientRects()); + + if (distances[closestLinkIndex] < 44 && closestLink.getAttribute("href")) { + // Adapt context and local position + let context = this.getContext(closestLink); + let local = Points.fromPageToNode(context, globalClick); + let rect = linkRects[closestLinkIndex]; + local.y = rect.center.y; + + Card.loadPopup(event, context, closestLink, local); + } + } + } + } + + + /** + * Evaluates an event, if it has a first target. If so it returns the first target, + * otherwise it returns the target. + */ + static _preferFirstTarget(event) { + return (event.firstTarget) ? event.firstTarget : event.target + } + + + /** + * Getter, Setter and 'Unsetter' for the properties hooked onto the context element. + */ + + static _getPopup(context) { + return context.popup + } + + static _setPopup(context, popup, source) { + this._setPopupSource(popup, source); + context.popup = popup; + + //jquery hyphenate below + console.log('hyphenated popup:', $('span').hyphenate('de')); + } + + static _unsetPopup(context) { + context.popup = null; + } + + static _setPopupSource(popup, source) { + popup.element.setAttribute("data-src", source); + } + + static _getPopupSource(popup) { + return popup.element.getAttribute("data-src") + } + + static _unsetZoomable(context) { + context.zoomable = null; + } + + static _setZoomable(context, zoomable, zoomedFig) { + context.zoomable = [zoomable, zoomedFig]; + } + + static _getZoomable(context) { + return (context.zoomable) ? context.zoomable : [] + } + + /** + * Helper function to determine if a proided element is still + * an active highlight. + */ + static _isHighlightActive(context, element) { + let highlights = this._getHighlights(context); + return (highlights.indexOf(element) != -1) + } + + static _getHighlights(context) { + return (context.highlights) ? context.highlights : [] + } + + static _addHighlight(context, highlight) { + if (context.highlights == null) this._unsetHighlight(context); + context.highlights.push(highlight); + } + + static _unsetHighlights(context) { + context.highlights = []; + } + + static _getHighlightSource(highlight) { + return highlight.getAttribute("xlink:href") + } + + static _getActiveSubcard(context) { + return context.querySelector(".mainview > .subcard") + } + + static _setSubcard(context, subcard) { + //console.trace("_setSubcard", context) + context.subcard = subcard; + } + + static _getSubcard(context) { + return context.subcard + } + + static _unsetSubcard(context) { + context.subcard = null; + } + + static incrementZIndex(context) { + if (!context.zIndex) context.zIndex = 0; + context.zIndex++; + } + + static getZIndex(context) { + return (context.zIndex || 0) + } + + /** + * This getter and setter pair solves the problem, + * when we assign 'relativePath' to a child class, the assign is routed + * to this base class. And we ensure, that we always set the same + * private variable. + * + * Not doing this lead to some cases, when Card-/ScatterCard.func + * was called and depending on context their were different values + * inside the relativePath variable. + */ + static set relativePath(value) { + Card._relativePath = value; + } + + static get relativePath() { + return Card._relativePath + } + + } + + Card.debug = true; + Card._relativePath = ""; + Card.scatterContainer = null; + Card.interactionType = "tap"; + Card.popupHtmlSelector = ".popupHtml"; + Card.dynamicHeight = false; + + Card.popupYOffset = -15; + + Card.zIndices = { + article: 10, + popup: 100, + zoomable: 101, + zoomedFigure: 102 + }; + + Card.animation = { + articleTransition: 0.6, + fade: 0.2, + popup: 0.1, + highlight: 0.4, + zoomable: 0.5 + }; + + + /** Make the class available in the global namespace */ + window.Card = Card; + + /** + * Extends the card with scatter functionality. + * + * @class ScatterCard + */ + class ScatterCard extends Card { + + + /** + * TODO: Find a more suitable name. + * Adjusts the HTML to work in the new context. + * + * @static + * @param {*} domElement + * @param {*} htmlString + * @param {*} basePath + * @param {*} [opts={}] + * @memberof Card + */ + static setup(context, htmlString, { + basePath = "./", + modules = [] + } = {}) { + context.classList.add("info-card"); + + this.relativePath = basePath; + htmlString = this._adjustRelativeLinks(htmlString); + + let parser = new DOMParser(); + let html = parser.parseFromString(htmlString, "text/html"); + + /** + * Conflicts with the FindTarget method of the Abstract scatter. + */ + this._replaceAttributes(html, "onclick", this._replaceCallback); + + + let content = html.querySelector(".mainview"); + context.appendChild(content); + + super.setup(context, modules); + return context + } + + + /** + * Appends a close listener to the scatter element. + * + * @static + * @param {*} element + * @param {*} callback + * @memberof Card + */ + static addOnCloseListener(element, callback) { + if (callback) { + element.onClose = callback; + } + } + + + /** + * Creates a scatter for the card and applies the card to it, + * + * @static + * @param {*} html + * @param {*} scatterContainer + * @param {string} [basePath=""] + * @param {*} [opts={}] + * @returns + * @memberof Card + */ + static createCardScatter(html, scatterContainer, { + basePath = "./", + modules = [] + } = {}) { + let element = document.createElement("div"); + + scatterContainer.element.appendChild(element); + new DOMScatter(element, scatterContainer, { + width: 1400, + height: 1200 + }); + + this.setup(element, html, { + basePath, + modules + }); + return element + } + + + + /** + *Utility function to create a fully functional card scatter. + * + * @static + * @param {*} scatterContainer + * @param {*} path + * @param {string} [basePath="."] + * @param {*} opts + * @returns + * @memberof CardScatter + */ + static loadAndCreateScatterCard(scatterContainer, item, { + basePath = "../", + modules = null, + onClose = null + } = {}) { + console.log(basePath); + return new Promise((resolve, reject) => { + this.loadHTML(basePath + "/" + item + "/index.html") + .then(html => { + let element = this.createCardScatter(html, scatterContainer, { + basePath, + modules + }); + if (onClose) + this.addOnCloseListener(element, onClose); + resolve(element); + }) + .catch(e => reject(e)); + }) + } + + static _setLanguage(context, language) { + context.language = language; + } + + static _getLanguage(context) { + return context.language + } + + } + + window.ScatterCard = ScatterCard; + + ScatterCard.selectedLanguage = 0; + ScatterCard.languages = ["Deutsch", "English"]; + ScatterCard.languageTags = { + Deutsch: "de", + English: "en" + }; + ScatterCard.scatterContainer = null; + + /* eslint-disable no-unused-vars */ + + /** + * The Theme class helps manage the card themes. + * Usually it overrides or enhances aspects of the card.js. + * + * @class Theme + */ + class Theme { + + /** + * Loads a config file and parses it to JSON. + * + * @static + * @param {string} [path=null] - Optional: A path where the config file is located at (including the filename and extension). + * @returns {Promise} - Returns a promise, that returns the parsed json file when resolved. + * @memberof Theme + */ + static loadConfig(path = null) { + return new Promise((resolve, reject) => { + path = (path) ? path : './config.json'; + + let xhttp = new XMLHttpRequest(); + xhttp.onreadystatechange = function () { + if (this.readyState == 4) { + + if (this.status == 200 || Theme._isLocal()) { + try { + const json = JSON.parse(this.responseText); + resolve(json); + + } catch (e) { + reject(e); + } + } else reject('Invalid request : ' + this.status); + } + }; + xhttp.open('GET', path, true); + xhttp.send(); + }) + } + + static _isLocal() { + return (window.location.protocol == 'file:') + } + } /* Needed to ensure that rollup.js includes class definitions and the classes are visible inside doctests. @@ -8121,7 +10160,7 @@ window.ImageLoader = ImageLoader; window.FrameLoader = FrameLoader; - window.DOMScatter = DOMScatter; + window.DOMScatter = DOMScatter$1; window.DOMScatterContainer = DOMScatterContainer; window.Dates = Dates; window.Doctest = Doctest; @@ -8143,14 +10182,14 @@ window.InteractionMapper = InteractionMapper$1; window.InteractionPoints = InteractionPoints; window.Interface = Interface; - window.Logging = Logging; + window.Logging = Logging$1; window.LowPassFilter = LowPassFilter; window.PointMap = PointMap; - window.Rect = Rect; - window.Points = Points; + window.Rect = Rect$1; + window.Points = Points$1; window.Polygon = Polygon; window.Poppable = Poppable; - window.Popup = Popup; + window.Popup = Popup$1; window.PopupMenu = PopupMenu; window.ResizeEvent = ResizeEvent; window.ScatterEvent = ScatterEvent; @@ -8165,6 +10204,8 @@ window.randomFloat = randomFloat; window.CardWrapper = CardWrapper; - window.Highlight = Highlight; + window.Card = Card; + window.Highlight = Highlight$1; + window.Theme = Theme; }()); diff --git a/lib/bundle.js b/lib/bundle.js index 211e458..1ea570c 100755 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -2,26 +2,27 @@ import App from './app.js' import Doctest from './doctest.js' import Errors from './errors.js' import Events from './events.js' -import {DOMFlip, DOMFlippable, CardLoader, PDFLoader, ImageLoader, FrameLoader, HTMLLoader} from './flippable.js' +import { DOMFlip, DOMFlippable, CardLoader, PDFLoader, ImageLoader, FrameLoader, HTMLLoader } from './flippable.js' import Index from './index.js' import Interface from './interface.js' import Logging from './logging.js' import Poppable from './poppable.js' import PopupMenu from './popupmenu.js' import Popup from './popup.js' -import {IApp} from './app.js' -import {Capabilities, CapabilitiesTests} from './capabilities.js' -import {EventRecorder} from './events.js' -import {FrameContainer, FrameTarget} from './frames.js' -import {Inspect} from './inspect.js' -import {PointMap, InteractionPoints, Interaction, IInteractionTarget, InteractionDelta, InteractionMapper, InteractionDelegate, IInteractionMapperTarget} from './interaction.js' -import {ITapDelegate, ResizeEvent, DOMScatterContainer, AbstractScatter, DOMScatter, ScatterEvent, BaseEvent} from './scatter.js' -import {Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Strings, isEmpty, getId, lerp, debounce, randomInt, randomFloat, LowPassFilter} from './utils.js' +import { IApp } from './app.js' +import { Capabilities, CapabilitiesTests } from './capabilities.js' +import { EventRecorder } from './events.js' +import { FrameContainer, FrameTarget } from './frames.js' +import { Inspect } from './inspect.js' +import { PointMap, InteractionPoints, Interaction, IInteractionTarget, InteractionDelta, InteractionMapper, InteractionDelegate, IInteractionMapperTarget } from './interaction.js' +import { ITapDelegate, ResizeEvent, DOMScatterContainer, AbstractScatter, DOMScatter, ScatterEvent, BaseEvent } from './scatter.js' +import { Cycle, Colors, Elements, Angle, Dates, Points, Polygon, Rect, Sets, Strings, isEmpty, getId, lerp, debounce, randomInt, randomFloat, LowPassFilter } from './utils.js' import UITest from './uitest.js' -import {CardWrapper} from './card/wrapper.js' -import {Highlight} from './card/highlight.js' - +import CardWrapper from './card/wrapper.js' +import Highlight from './card/highlight.js' +import Card from './card/card.js' +import Theme from './card/theme.js' /* Needed to ensure that rollup.js includes class definitions and the classes are visible inside doctests. @@ -88,4 +89,6 @@ window.randomInt = randomInt window.randomFloat = randomFloat window.CardWrapper = CardWrapper -window.Highlight = Highlight \ No newline at end of file +window.Card = Card +window.Highlight = Highlight +window.Theme = Theme diff --git a/lib/card/card.js b/lib/card/card.js index dbad014..8a48426 100644 --- a/lib/card/card.js +++ b/lib/card/card.js @@ -29,7 +29,7 @@ const enableNearestNeighborTaps = false * * The class is used as a namespace and should never called with new. */ -class Card { +export default class Card { static setup(context, modules = []) { console.log("Setup Card...", modules) diff --git a/lib/card/highlight.js b/lib/card/highlight.js index 2e63bc4..b559c79 100644 --- a/lib/card/highlight.js +++ b/lib/card/highlight.js @@ -30,7 +30,7 @@ function round(value) { * @class Highlight * @extends {Object} */ -export class Highlight extends Object { +export default class Highlight extends Object { static disableAnimations() { _HighlightEnabled = false diff --git a/lib/card/theme.js b/lib/card/theme.js index 76b4381..9ff54e3 100644 --- a/lib/card/theme.js +++ b/lib/card/theme.js @@ -6,7 +6,7 @@ * * @class Theme */ -class Theme { +export default class Theme { /** * Loads a config file and parses it to JSON. diff --git a/lib/card/wrapper.js b/lib/card/wrapper.js index 8ae6c04..7aeef4c 100644 --- a/lib/card/wrapper.js +++ b/lib/card/wrapper.js @@ -4,7 +4,7 @@ import Events from '../events.js' import { Points } from '../utils.js' -export class CardWrapper extends Object { +export default class CardWrapper extends Object { constructor(domNode, { triggerSVGClicks = true, allowClickDistance = 44 } = {}) { super()