diff --git a/dist/iwmlib.js b/dist/iwmlib.js index 2d85747..865b1ff 100644 --- a/dist/iwmlib.js +++ b/dist/iwmlib.js @@ -41,16 +41,20 @@ /** Build the app by registering event handlers, * adding DOM elements, instanciating templates, etc... */ - setup() { return this } + setup() { + return this + } /** Run the application by starting a main loop, ... */ - run() { return this } + run() { + return this + } } class App extends Object { /** Override this method to build your app. - */ + */ setup() { return this } @@ -80,8 +84,7 @@ this.allTests(); var end = performance.now(); return ['ok', end - start] - } - catch(e) { + } catch (e) { console.trace(); return ['Tests failed', e.message] } @@ -4018,7 +4021,7 @@ - class DOMScatter$1 extends AbstractScatter { + class DOMScatter extends AbstractScatter { constructor( element, container, @@ -4305,7 +4308,7 @@ bringToFront() { // this.element.parentNode.appendChild(this.element) // uo: On Chome and Electon appendChild leads to flicker - TweenLite.set(this.element, { zIndex: DOMScatter$1.zIndex++ }); + TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); } onTap(event, interaction, point) { @@ -4435,7 +4438,7 @@ } } - DOMScatter$1.zIndex = 1000; + DOMScatter.zIndex = 1000; class CardLoader { constructor( @@ -4693,7 +4696,7 @@ frontLoaded(loader) { return new Promise((resolve, reject) => { - let scatter = new DOMScatter$1( + let scatter = new DOMScatter( this.cardWrapper, this.domScatterContainer, { @@ -4917,7 +4920,7 @@ bringToFront() { this.scatter.bringToFront(); - TweenLite.set(this.element, { zIndex: DOMScatter$1.zIndex++ }); + TweenLite.set(this.element, { zIndex: DOMScatter.zIndex++ }); } clickInfo() { @@ -7533,6 +7536,1854 @@ } } + /** + * 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 + if (typeof($) != 'undefined') { + $('.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; + + if (typeof($) != 'undefined') { + //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 + }; + /* eslint-disable no-console */ class CardWrapper extends Object { @@ -8100,2001 +9951,6 @@ 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 - if (typeof($) != 'undefined') { - $('.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; - - if (typeof($) != 'undefined') { - //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 - }; - - /** - * 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 = [], - onClose = null - } = {}) { - console.log(basePath); - return new Promise((resolve, reject) => { - let url = basePath + "/" + item + "/index.html"; - console.log("Loading", url); - this.loadHTML(url) - .then(html => { - console.log("Received", 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 */ /** @@ -10163,7 +10019,7 @@ window.ImageLoader = ImageLoader; window.FrameLoader = FrameLoader; - window.DOMScatter = DOMScatter$1; + window.DOMScatter = DOMScatter; window.DOMScatterContainer = DOMScatterContainer; window.Dates = Dates; window.Doctest = Doctest; @@ -10208,7 +10064,7 @@ window.CardWrapper = CardWrapper; window.Card = Card; - window.ScatterCard = ScatterCard; + window.ScatterCard = Card; window.Highlight = Highlight$1; window.Theme = Theme; diff --git a/lib/bundle.js b/lib/bundle.js index 1f1520d..5c708c5 100755 --- a/lib/bundle.js +++ b/lib/bundle.js @@ -19,9 +19,11 @@ import { ITapDelegate, ResizeEvent, DOMScatterContainer, AbstractScatter, DOMSca 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 Card from './card/card.js' import CardWrapper from './card/wrapper.js' import Highlight from './card/highlight.js' -import {Card, ScatterCard} from './card/card.js' +import ScatterCard from './card/card.js' +import { CardPlugin, CardPluginBase } from './card/plugin.js' import Theme from './card/theme.js' /* Needed to ensure that rollup.js includes class definitions and the classes diff --git a/lib/card/card.js b/lib/card/card.js index f8e78b2..374c0f3 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. */ -export class Card { +export default class Card { static setup(context, modules = []) { console.log("Setup Card...", modules) @@ -113,7 +113,7 @@ export class Card { let interactionType = this.interactionType - //Remove the events on the circle. + //Remove the events on the circle. // These are 'hardcoded' inside the convert.js. if (element.tagName == "circle") return false @@ -155,11 +155,11 @@ export class Card { /** * 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 @@ -167,7 +167,7 @@ export class 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. */ @@ -383,7 +383,7 @@ export class Card { * @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. + * @returns {Promise} - Returns a Promise, which is resolved, when the Popup is fully loaded. * @memberof Card */ static _createPopup(context, position, content, options = {}) { @@ -399,7 +399,7 @@ export class Card { posOffset: 10 }, options))) - // Placing the popup when it required loading, + // 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" @@ -431,7 +431,7 @@ export class Card { zIndex: this.zIndices.popup }) - TweenMax.to(popup.element, this.animation.popup, { + TweenLite.to(popup.element, this.animation.popup, { autoAlpha: 1, ease: Power2.easeIn }) @@ -447,11 +447,11 @@ export class Card { /** * 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, { + TweenLite.to(overlay, 0.2, { autoAlpha: 0, onComplete: () => { popup.remove() //this._cleanup(context) @@ -507,9 +507,9 @@ export class Card { if (editable) { if (this.debug) console.log("Append overlay.", context) overlay.classList.add('overlay') - TweenMax.set(overlay, { autoAlpha: 0 }) + TweenLite.set(overlay, { autoAlpha: 0 }) context.appendChild(overlay) - TweenMax.to(overlay, 0.5, { autoAlpha: 0.25 }) + TweenLite.to(overlay, 0.5, { autoAlpha: 0.25 }) } // Extract the body from the Popup site. @@ -522,14 +522,14 @@ export class Card { }) let html = popupPage.body.innerHTML /** - * We do not want to use the popup class again. + * 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). - * + * (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. */ @@ -678,8 +678,8 @@ export class Card { let context = this.getContext(node) event.stopPropagation() - /** - * This node is the documents body, as events wont work + /** + * 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") @@ -746,7 +746,7 @@ export class Card { * * @static * @private - * @param {string} source - Url to a popup file. + * @param {string} source - Url to a popup file. * @returns {Promise} - Returns a promise, that's resolved when the data is loaded. * @memberof Card */ @@ -799,9 +799,9 @@ export class 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 + /* + 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 */ @@ -949,17 +949,17 @@ export class Card { const scaleFactor = 2 const transformOrigin = 'bottom right' - TweenMax.set(zoomedFig, { + TweenLite.set(zoomedFig, { x: current.x, y: current.y, width: current.width + borderX, height: current.height + borderY, transformOrigin }) - TweenMax.set(zoomable, { opacity: 0 }) + TweenLite.set(zoomable, { opacity: 0 }) let icon = zoomedFig.querySelector(".icon") - TweenMax.set(icon, { + TweenLite.set(icon, { transformOrigin }) zoomedFig.style.transformOrigin = "calc(100% - " + parseFloat(zoomedFigStyle.borderRightWidth) + "px) calc(100% - " + parseFloat(zoomedFigStyle.borderBottomWidth) + "px)" @@ -990,7 +990,7 @@ export class Card { } /** - * Selects and transforms the zoomicon from a zoomicon to a closeicon + * Selects and transforms the zoomicon from a zoomicon to a closeicon * or the other way around. * * @static @@ -1029,7 +1029,7 @@ export class Card { 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 }) + TweenLite.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) @@ -1046,14 +1046,14 @@ export class Card { zoomedFig.remove() zoomContainer.remove() zoomParent.appendChild(zoomable) - TweenMax.set(zoomable, { x: 0, y: 0 }) + TweenLite.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 }) + TweenLite.set(iconClone, { x: current.iconPos.x, y: current.iconPos.y }) zoomable.onmousedown = event => { if (this.debug) console.log('mousedown', event.target) @@ -1067,7 +1067,7 @@ export class Card { event.preventDefault() let dx = event.pageX - zoomable.dragStartPos.x let dy = event.pageY - zoomable.dragStartPos.y - TweenMax.set([zoomable, iconClone], { x: '+=' + dx, y: '+=' + dy }) + TweenLite.set([zoomable, iconClone], { x: '+=' + dx, y: '+=' + dy }) zoomable.dragStartPos = { x: event.pageX, y: event.pageY } if (editor) { editor.showControls() @@ -1086,7 +1086,7 @@ export class Card { let zoom = direction ? zoomFactor : 1 / zoomFactor startZoom *= zoom - TweenMax.set(zoomable, { scale: startZoom }) + TweenLite.set(zoomable, { scale: startZoom }) if (editor) { editor.showControls() } @@ -1117,17 +1117,17 @@ export class Card { let zoomedCaption = zoomedFig.querySelector("figcaption.zoomcap") - TweenMax.to(zoomedCaption, this.animation.fade, { + TweenLite.to(zoomedCaption, this.animation.fade, { autoAlpha: 0, }) - TweenMax.to(zoomedFig, this.animation.zoomable, { + TweenLite.to(zoomedFig, this.animation.zoomable, { css: { scaleX: 1, scaleY: 1 }, onComplete: () => { - TweenMax.set(zoomable, { + TweenLite.set(zoomable, { opacity: 1 }) let div = zoomedFig.parentNode @@ -1174,8 +1174,8 @@ export class Card { /** - * We have to reorder the clone, as it still contains the - * preview text image. And the new html is + * 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") @@ -1204,7 +1204,7 @@ export class Card { let padding = parseInt(this.css(indexbox, 'padding')) let maxWidth = this.css(card, 'max-width') - TweenMax.set(clone, { + TweenLite.set(clone, { css: { position: 'absolute', width: globalIndexCardRect.width, @@ -1215,12 +1215,12 @@ export class Card { } }) - TweenMax.set(articleClone, { + TweenLite.set(articleClone, { autoAlpha: 0 }) - TweenMax.set(card, { css: { maxWidth: '100%' } }) - TweenMax.set(clone, { + TweenLite.set(card, { css: { maxWidth: '100%' } }) + TweenLite.set(clone, { x: localOrigin.x - padding, y: localOrigin.y - padding, scaleX, @@ -1244,10 +1244,10 @@ export class Card { /** * Scale the content from 100% to it's target size. */ - // TweenMax.set(subcardContent, { + // TweenLite.set(subcardContent, { // height: "100%" // }) - // TweenMax.to(subcardContent, Card.animation.articleTransition, { + // TweenLite.to(subcardContent, Card.animation.articleTransition, { // height: targetHeight + "px" // }) } @@ -1270,7 +1270,7 @@ export class Card { } let desiredBorderBottomWidth = parseInt(window.getComputedStyle(titlebar).borderBottomWidth) - TweenMax.to(clone, Card.animation.articleTransition, { + TweenLite.to(clone, Card.animation.articleTransition, { x: -padding, y: -padding, ease: ExpoScaleEase.config(scaleX, 1), @@ -1282,10 +1282,10 @@ export class Card { onUpdateParams: ['{self}'], onUpdate: (self) => { let transform = self.target._gsTransform - TweenMax.set(title, { + TweenLite.set(title, { scale: 1 / transform.scaleX }) - TweenMax.set(titlebar, { + TweenLite.set(titlebar, { height: start.height * 1 / transform.scaleY }) @@ -1294,7 +1294,7 @@ export class Card { } }) - TweenMax.to([articleClone], this.animation.articleTransition / 2, { + TweenLite.to([articleClone], this.animation.articleTransition / 2, { delay: this.animation.articleTransition / 2, autoAlpha: 1 }) @@ -1327,11 +1327,11 @@ export class Card { let titlebarStyle = window.getComputedStyle(previewTitlebar) let titlebar = clone.querySelector(".titlebar") - TweenMax.to(titlebar, this.animation.articleTransition, { + TweenLite.to(titlebar, this.animation.articleTransition, { height: parseInt(titlebarStyle.height) }) - TweenMax.to(articleClone, this.animation.articleTransition / 2, { + TweenLite.to(articleClone, this.animation.articleTransition / 2, { autoAlpha: 0 }) @@ -1341,13 +1341,13 @@ export class Card { } if (this.dynamicHeight) { - TweenMax.to(subcardContent, this.animation.articleTransition, { + TweenLite.to(subcardContent, this.animation.articleTransition, { height: "100%" }) } - TweenMax.set(card, { autoAlpha: 1, css: { maxWidth } }) - TweenMax.to(clone, this.animation.articleTransition, { + TweenLite.set(card, { autoAlpha: 1, css: { maxWidth } }) + TweenLite.to(clone, this.animation.articleTransition, { x: localOrigin.x - padding, y: localOrigin.y - padding, scaleX, @@ -1356,7 +1356,7 @@ export class Card { rotation: angle, onComplete: () => { // article.remove() - TweenMax.to(clone, this.animation.fade, + TweenLite.to(clone, this.animation.fade, { //delay: 0.2, autoAlpha: 0, @@ -1373,11 +1373,11 @@ export class Card { onUpdate: function (self) { let transform = self.target._gsTransform - TweenMax.set(title, { + TweenLite.set(title, { scale: 1 / transform.scaleX }) - TweenMax.set(titlebar, { + TweenLite.set(titlebar, { height: original.height * 1 / transform.scaleY }) @@ -1418,8 +1418,8 @@ export class Card { } }) } - // Use the 'tap' event for closing. - // Otherwise the subcard cannot be closed, + // Use the 'tap' event for closing. + // Otherwise the subcard cannot be closed, // when another subcard is touched. InteractionMapper.on("tap", iconClone, () => { if (editable) { @@ -1505,10 +1505,10 @@ export class Card { let html = xhr.responseText let parsedHTML = this.postProcessResponseText(html) - // TODO: What is this good for? + // TODO: What is this good for? // let article = parsedHTML.querySelector('article') // card.insertAdjacentElement('afterbegin', article) - // TweenMax.set(article, { autoAlpha: 0 }) + // TweenLite.set(article, { autoAlpha: 0 }) Card.expandIndexCard(card, parsedHTML, 'article', relativeSource, saveCallback) } @@ -1542,7 +1542,7 @@ export class Card { * * @static * @param {DomElement} child - A dom element for which the context shall be retrieved. - * @returns {DomElement} - The containing context / info-card. + * @returns {DomElement} - The containing context / info-card. * @memberof Card */ static getContext(child) { @@ -1585,7 +1585,7 @@ export class Card { /** * 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 @@ -1610,7 +1610,7 @@ export class Card { * 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. @@ -1630,7 +1630,7 @@ export class Card { } /** - * Adjusts all links + * Adjusts all links * * @static * @param {*} htmlString @@ -1693,7 +1693,7 @@ export class Card { if (event.target) { //let column = event.target.closest(".column") let indexbox = this.closestWithClass(card, 'mainview') - if (indexbox != null) { // column != null || + 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) @@ -1842,9 +1842,9 @@ export class Card { /** * 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. - * + * 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. @@ -1883,680 +1883,3 @@ Card.animation = { zoomable: 0.5 } -/** - * Extends the card with scatter functionality. - * - * @class ScatterCard - */ -export 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 = [], - onClose = null - } = {}) { - console.log(basePath) - return new Promise((resolve, reject) => { - let url = basePath + "/" + item + "/index.html" - console.log("Loading", url) - this.loadHTML(url) - .then(html => { - console.log("Received", 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 - -var CardPlugin = CardPlugin || {} - -class CardPluginBase { - - apply(context) { - if (this.verify(context)) { - this.append(context) - console.log("Plugin " + this.name + " was verified successfully.") - return true - } else console.error("Could not verify module " + this.name + ".") - return false - } - - get name() { - return this.constructor.name - } - - verify(context) { - let funcs = this._getVerificationFunctions(context) - for (let func of funcs) { - if (!func()) return false - } - return true - } - - _verifyElementsExist(context, ...selectors) { - let missing = [] - - for (let selector of selectors) { - let requiredElement = context.querySelector(selector) - if (requiredElement == null) { - missing.push(selector) - } - } - const valid = (missing.length == 0) - if (!valid) console.error("Elements were missing: ", missing.join(", ")) - return valid - } - - - /** - * Appends the Plugin to the context. - * - * @memberof CardPlugin - */ - append(context) { - console.error("Call of abstract method CardPlugin.prototype.append(context). Plugins need to overwrite the append method!") - } - - _getVerificationFunctions(context) { - return [ - this._verifyContext.bind(this, context), - this._verifyRequirements.bind(this, context) - ] - } - - _verifyContext(context) { - if (!(context instanceof HTMLElement)) { - console.error("Context is not of type HTML Element.", context) - return false - } else return true - } - - _verifyRequirements(context) { - let requirements = this._collectAllRequirements() - let missing = [] - - requirements.forEach(module => { - if (context.modules.indexOf(module.name) == -1) { - missing.push(module.name) - } - }) - - const valid = (missing.length == 0) - if (!valid) console.error("Could not apply module '" + this.name + "'. Following modules are required but were missing: " + missing.join(",")) - else console.log("All requirements were met! Well done!") - return valid - } - - - _collectAllRequirements() { - let requirements = [] - let klass = this.__proto__ - while (klass) { - if (klass.require != null) { - requirements = requirements.concat(klass.require) - } - klass = klass.__proto__ - } - return requirements - } -} - - - - -CardPlugin.LightBox = class LightBox extends CardPluginBase { - constructor(className, style = {}) { - super() - this.className = className - this.style = style - } - - append(context) { - let wrapper = document.createElement("div") - wrapper.className = this.className - - Object.assign(wrapper.style, { - zIndex: 1000, - // backgroundColor: "black", - top: 0, - left: 0, - width: "100%", - height: "100%" - }, this.style, { - display: "none", - position: "absolute", - }) - - context.appendChild(wrapper) - } - -} - - -/** - * The Enlargeable Overlay module allows the user to click on the thumbnail image, - * and the images gets enlarged inside the card. - * - * @class EnlargeableThumbnail - * @extends {CardPlugin} - */ -CardPlugin.EnlargeableThumbnail = class EnlargeableThumbnail extends CardPluginBase { - - constructor(wrapperSelector, overlaySelector = null, { - zoomAnimationDuration = 0.4, - fadeAnimationDuration = 0.4, - interactionType = "tap" - } = {}) { - super() - this.wrapperSelector = wrapperSelector - this.overlaySelector = overlaySelector - - this.zoomAnimationDuration = zoomAnimationDuration - this.fadeAnimationDuration = fadeAnimationDuration - this.interactionType = interactionType - } - - get require() { - return [ - CardPlugin.LightBox - ] - } - - _getVerificationFunctions(context) { - let arr = super._getVerificationFunctions(context) - let funcs = [ - this._verifyElementsExist.bind(this, context, this.wrapperSelector, this.overlaySelector) - ] - return arr.concat(funcs) - } - - append(context) { - let source = this._retrieveSource(context) - this.setupEnlargeableThumbnail(context, source) - } - - - /** - * Get the preview image. - * - * It depends on the fact, that the thumbnail image is in the same directory - * - * - * @param {*} context - * @returns - * @memberof EnlargeableThumbnail - */ - _retrieveSource(context) { - let img = context.querySelector(this.wrapperSelector + " img") - let src = img.getAttribute("src") - let parts = src.split("/") - parts.pop() - parts.push(parts[parts.length - 1]) - let imagePath = parts.join("/") + ".jpg" - return imagePath - } - - - setupEnlargeableThumbnail(context, src) { - let wrapper = context.querySelector(this.wrapperSelector) - let overlay = context.querySelector(this.overlaySelector) - - let icon = document.createElement("div") - icon.className = "button corner-button bottom-right icon zoom" - wrapper.appendChild(icon) - - Object.assign(wrapper.style, { - cursor: "pointer" - }) - - InteractionMapper.on(this.interactionType, wrapper, () => { - this.openThumbnailDetail(context, src) - }) - - InteractionMapper.on(this.interactionType, overlay, () => { - this.closeThumnailDetail(context) - }) - } - - openThumbnailDetail(context, src) { - let overlay = context.querySelector(".img-overlay") - overlay.innerHTML = "" - let source = context.querySelector(this.wrapperSelector) - let sourceStyle = window.getComputedStyle(source) - let imageWrapper = source.cloneNode(true) - let image = imageWrapper.querySelector("img") - - Object.assign(imageWrapper.style, { - maxWidth: "none", - maxHeight: "none" - }) - - Object.assign(image.style, { - width: "100%", - height: "100%", - objectFit: "cover" - }) - - this._replaceIcon(imageWrapper) - - image.onload = () => { - let header = context.querySelector("header") - let headerStlye = window.getComputedStyle(header) - - /** - * First the maxFillRatio is considered. - * It describes how much the image is allowed to exceed the context element. - */ - const maxFillRatio = 1.5 - - /** - * The minor side should not exceed the height of the context window. - */ - const maxMinorSize = context.offsetHeight - 2 * parseInt(headerStlye.paddingTop) - 2 * parseInt(headerStlye.marginTop) - - - - const max = { - width: context.offsetWidth * maxFillRatio, - height: context.offsetHeight * maxFillRatio - } - - - let majorSide - let minorSide - const _width = { name: "width", axis: "x" } - const _height = { name: "height", axis: "y" } - if (image.naturalHeight > image.naturalWidth) { - majorSide = _height - minorSide = _width - } else { - majorSide = _width - minorSide = _height - } - - function capitalize(string) { - return string.charAt(0).toUpperCase() + string.slice(1) - } - function getImageSize(side) { - return image["natural" + capitalize(side.name)] - } - - const majorImageSize = getImageSize(majorSide) - // const minorImageSize = getImageSize(minorSide) - - let ratio = getImageSize(minorSide) / getImageSize(majorSide) - let size = (majorImageSize > max[majorSide.name]) ? max[majorSide.name] : majorImageSize - - if (size * ratio > maxMinorSize) { - size = maxMinorSize / ratio - } - - let targetDimensions = { - width: 0, - height: 0 - } - - - let position = Points.fromPageToNode(context, Points.fromNodeToPage(source, { x: 0, y: 0 })) - - let targetOffset = { - x: 0, - y: 0 - } - - targetDimensions[majorSide.name] = size - targetDimensions[minorSide.name] = size * ratio - - targetOffset[majorSide.axis] = (context["offset" + capitalize(majorSide.name)] - targetDimensions[majorSide.name]) / 2 - targetOffset[minorSide.axis] = (context["offset" + capitalize(minorSide.name)] - targetDimensions[minorSide.name]) / 2 - - overlay.appendChild(imageWrapper) - - TweenMax.set(imageWrapper, { - left: 0, - top: 0, - x: position.x, - y: position.y, - position: "absolute", - width: parseInt(sourceStyle.width), - height: parseInt(sourceStyle.height) - }) - - - TweenMax.set(overlay, { - display: "flex", - autoAlpha: 0 - }) - - TweenMax.to(imageWrapper, this.zoomAnimationDuration, { - x: targetOffset.x, - y: targetOffset.y, - width: targetDimensions.width, - height: targetDimensions.height, - }) - TweenMax.to(overlay, this.fadeAnimationTime, { - autoAlpha: 1 - }) - } - - image.src = src - } - - _replaceIcon(clone) { - let zoomIcon = clone.querySelector(".icon.zoom") - zoomIcon.classList.remove("zoom") - zoomIcon.classList.add("close") - } - - getBorderHeight(style) { - const borderWidth = parseInt(style.borderTopWidth) + parseInt(style.borderBottomWidth) - const padding = parseInt(style.paddingTop) + parseInt(style.paddingBottom) - return parseInt(style.width) + borderWidth + padding - } - - getBorderWidth(style) { - const borderWidth = parseInt(style.borderLeftWidth) + parseInt(style.borderRightWidth) - const padding = parseInt(style.paddingLeft) + parseInt(style.paddingRight) - return parseInt(style.width) + borderWidth + padding - } - - closeThumnailDetail(context) { - let overlay = context.querySelector(".img-overlay") - - let timeline = new TimelineLite() - - timeline.to(overlay, this.fadeAnimationDuration, { - autoAlpha: 0 - }).set(overlay, { - display: "none" - }) - } - -} - -CardPlugin.Ui = class UiPlugin extends CardPluginBase { - constructor(className, parent = null) { - super() - this.parent = parent - this.className = className - } - - _getVerificationFunctions(context) { - let arr = super._getVerificationFunctions(context) - let func = [ - this._doesParentExist.bind(this, context, this.parent) - ] - return arr.concat(func) - } - - _doesParentExist(context, parent) { - if (parent == null) return true - let valid = (context.querySelector(parent) != null) - if (!valid) console.error("Could not find parent on context.", context, parent) - return valid - } - - append(context) { - parent = (this.parent == null) ? context : context.querySelector(this.parent).appendChild(container) - let container = document.createElement("div") - container.className = this.className - parent.appendChild(container) - } - -} - -CardPlugin.Speech = class SpeechPlugin extends CardPluginBase { - - constructor(parentSelector, className, interactionType = "tap") { - super() - this.className = className - this.parentSelector = parentSelector - this.interactionType = interactionType - - } - - get require() { - return [ - CardPlugin.Ui - ] - } - - append(context) { - let container = context.querySelector(this.parentSelector) - this.button = document.createElement("div") - this.button.className = "icon button " + this.className - container.appendChild(this.button) - - InteractionMapper.on(this.interactionType, this.button, () => { - let subcard = context.querySelector(".mainview > .subcard") - let target = (subcard) ? subcard : context - - this.speak(target) - - }) - } - - _activate() { - this._disableActive() - this.active = this - this._activateButton() - } - - _activateButton() { - if (this.button) - this.button.classList.add("active") - } - - _deactivate() { - this._deactivateButton() - } - - _deactivateButton() { - if (this.button) - this.button.classList.remove("active") - } - - _isSameNode(node) { - //console.log(this.currentText, node.innerText) - return (this.currentText == node.innerText) - } - - speak(node) { - - console.log(this._isSameNode(node)) - - if (!window.speechSynthesis.speaking) { - console.log("Noone talking!") - this._start(node) - } else if (this._isSameNode(node)) { - console.log("Requested same!") - this._stop() - - } else { - console.log("Requested Different!") - this._stop() - this._start(node) - } - - } - - _disableActive() { - console.log("disableActive:", this.active) - if (this.active) { - this.active._deactivate() - } - } - - _start(node) { - this.currentText = node.innerText - let utterance = new SpeechSynthesisUtterance(node.innerText) - - let voices = window.speechSynthesis.getVoices() - console.log(voices) - let voice = voices.filter((val) => { - //console.log(val) - return val.name == "Microsoft Hedda Desktop - German" - })[0] - - //console.log(voice) - - utterance.voice = voice - console.log("TALK: ", utterance) - window.speechSynthesis.speak(utterance) - this._activate() - window.speechSynthesis.resume() - - - utterance.onboundary = () => { console.log("onboundary", node.innerText); if (this.currentText.substring(0, 5) != node.innerText.substring(0, 5)) { console.log("text for speech synth changed!", this.currentText, node.innerText); this._stop() } } - utterance.onend = () => console.log("onend", node.innerText) - utterance.onerror = () => console.log("onerror", node.innerText) - utterance.onmark = () => console.log("onmark", node.innerText) - utterance.onpause = () => console.log("onpause", node.innerText) - utterance.onresume = () => console.log("onresume", node.innerText) - utterance.onstart = () => console.log("onstart", node.innerText) - utterance.onerror = () => console.log("onerror", node.innerText) - } - - _stop() { - window.speechSynthesis.cancel() - this.currentText = null - this._deactivate() - } - - get active() { - return this.constructor.active - } - - set active(val) { this.constructor.active = val } - - get currentText() { - return this.constructor.text - } - - set currentText(val) { - this.constructor.text = val - } - -} diff --git a/lib/card/plugin.js b/lib/card/plugin.js new file mode 100644 index 0000000..82afa87 --- /dev/null +++ b/lib/card/plugin.js @@ -0,0 +1,530 @@ +export var CardPlugin = CardPlugin || {} + +export class CardPluginBase { + + apply(context) { + if (this.verify(context)) { + this.append(context) + console.log("Plugin " + this.name + " was verified successfully.") + return true + } else console.error("Could not verify module " + this.name + ".") + return false + } + + get name() { + return this.constructor.name + } + + verify(context) { + let funcs = this._getVerificationFunctions(context) + for (let func of funcs) { + if (!func()) return false + } + return true + } + + _verifyElementsExist(context, ...selectors) { + let missing = [] + + for (let selector of selectors) { + let requiredElement = context.querySelector(selector) + if (requiredElement == null) { + missing.push(selector) + } + } + const valid = (missing.length == 0) + if (!valid) console.error("Elements were missing: ", missing.join(", ")) + return valid + } + + + /** + * Appends the Plugin to the context. + * + * @memberof CardPlugin + */ + append(context) { + console.error("Call of abstract method CardPlugin.prototype.append(context). Plugins need to overwrite the append method!") + } + + _getVerificationFunctions(context) { + return [ + this._verifyContext.bind(this, context), + this._verifyRequirements.bind(this, context) + ] + } + + _verifyContext(context) { + if (!(context instanceof HTMLElement)) { + console.error("Context is not of type HTML Element.", context) + return false + } else return true + } + + _verifyRequirements(context) { + let requirements = this._collectAllRequirements() + let missing = [] + + requirements.forEach(module => { + if (context.modules.indexOf(module.name) == -1) { + missing.push(module.name) + } + }) + + const valid = (missing.length == 0) + if (!valid) console.error("Could not apply module '" + this.name + "'. Following modules are required but were missing: " + missing.join(",")) + else console.log("All requirements were met! Well done!") + return valid + } + + + _collectAllRequirements() { + let requirements = [] + let klass = this.__proto__ + while (klass) { + if (klass.require != null) { + requirements = requirements.concat(klass.require) + } + klass = klass.__proto__ + } + return requirements + } +} + + + + +CardPlugin.LightBox = class LightBox extends CardPluginBase { + constructor(className, style = {}) { + super() + this.className = className + this.style = style + } + + append(context) { + let wrapper = document.createElement("div") + wrapper.className = this.className + + Object.assign(wrapper.style, { + zIndex: 1000, + // backgroundColor: "black", + top: 0, + left: 0, + width: "100%", + height: "100%" + }, this.style, { + display: "none", + position: "absolute", + }) + + context.appendChild(wrapper) + } + +} + + +/** + * The Enlargeable Overlay module allows the user to click on the thumbnail image, + * and the images gets enlarged inside the card. + * + * @class EnlargeableThumbnail + * @extends {CardPlugin} + */ +CardPlugin.EnlargeableThumbnail = class EnlargeableThumbnail extends CardPluginBase { + + constructor(wrapperSelector, overlaySelector = null, { + zoomAnimationDuration = 0.4, + fadeAnimationDuration = 0.4, + interactionType = "tap" + } = {}) { + super() + this.wrapperSelector = wrapperSelector + this.overlaySelector = overlaySelector + + this.zoomAnimationDuration = zoomAnimationDuration + this.fadeAnimationDuration = fadeAnimationDuration + this.interactionType = interactionType + } + + get require() { + return [ + CardPlugin.LightBox + ] + } + + _getVerificationFunctions(context) { + let arr = super._getVerificationFunctions(context) + let funcs = [ + this._verifyElementsExist.bind(this, context, this.wrapperSelector, this.overlaySelector) + ] + return arr.concat(funcs) + } + + append(context) { + let source = this._retrieveSource(context) + this.setupEnlargeableThumbnail(context, source) + } + + + /** + * Get the preview image. + * + * It depends on the fact, that the thumbnail image is in the same directory + * + * + * @param {*} context + * @returns + * @memberof EnlargeableThumbnail + */ + _retrieveSource(context) { + let img = context.querySelector(this.wrapperSelector + " img") + let src = img.getAttribute("src") + let parts = src.split("/") + parts.pop() + parts.push(parts[parts.length - 1]) + let imagePath = parts.join("/") + ".jpg" + return imagePath + } + + + setupEnlargeableThumbnail(context, src) { + let wrapper = context.querySelector(this.wrapperSelector) + let overlay = context.querySelector(this.overlaySelector) + + let icon = document.createElement("div") + icon.className = "button corner-button bottom-right icon zoom" + wrapper.appendChild(icon) + + Object.assign(wrapper.style, { + cursor: "pointer" + }) + + InteractionMapper.on(this.interactionType, wrapper, () => { + this.openThumbnailDetail(context, src) + }) + + InteractionMapper.on(this.interactionType, overlay, () => { + this.closeThumnailDetail(context) + }) + } + + openThumbnailDetail(context, src) { + let overlay = context.querySelector(".img-overlay") + overlay.innerHTML = "" + let source = context.querySelector(this.wrapperSelector) + let sourceStyle = window.getComputedStyle(source) + let imageWrapper = source.cloneNode(true) + let image = imageWrapper.querySelector("img") + + Object.assign(imageWrapper.style, { + maxWidth: "none", + maxHeight: "none" + }) + + Object.assign(image.style, { + width: "100%", + height: "100%", + objectFit: "cover" + }) + + this._replaceIcon(imageWrapper) + + image.onload = () => { + let header = context.querySelector("header") + let headerStlye = window.getComputedStyle(header) + + /** + * First the maxFillRatio is considered. + * It describes how much the image is allowed to exceed the context element. + */ + const maxFillRatio = 1.5 + + /** + * The minor side should not exceed the height of the context window. + */ + const maxMinorSize = context.offsetHeight - 2 * parseInt(headerStlye.paddingTop) - 2 * parseInt(headerStlye.marginTop) + + + + const max = { + width: context.offsetWidth * maxFillRatio, + height: context.offsetHeight * maxFillRatio + } + + + let majorSide + let minorSide + const _width = { name: "width", axis: "x" } + const _height = { name: "height", axis: "y" } + if (image.naturalHeight > image.naturalWidth) { + majorSide = _height + minorSide = _width + } else { + majorSide = _width + minorSide = _height + } + + function capitalize(string) { + return string.charAt(0).toUpperCase() + string.slice(1) + } + function getImageSize(side) { + return image["natural" + capitalize(side.name)] + } + + const majorImageSize = getImageSize(majorSide) + // const minorImageSize = getImageSize(minorSide) + + let ratio = getImageSize(minorSide) / getImageSize(majorSide) + let size = (majorImageSize > max[majorSide.name]) ? max[majorSide.name] : majorImageSize + + if (size * ratio > maxMinorSize) { + size = maxMinorSize / ratio + } + + let targetDimensions = { + width: 0, + height: 0 + } + + + let position = Points.fromPageToNode(context, Points.fromNodeToPage(source, { x: 0, y: 0 })) + + let targetOffset = { + x: 0, + y: 0 + } + + targetDimensions[majorSide.name] = size + targetDimensions[minorSide.name] = size * ratio + + targetOffset[majorSide.axis] = (context["offset" + capitalize(majorSide.name)] - targetDimensions[majorSide.name]) / 2 + targetOffset[minorSide.axis] = (context["offset" + capitalize(minorSide.name)] - targetDimensions[minorSide.name]) / 2 + + overlay.appendChild(imageWrapper) + + TweenMax.set(imageWrapper, { + left: 0, + top: 0, + x: position.x, + y: position.y, + position: "absolute", + width: parseInt(sourceStyle.width), + height: parseInt(sourceStyle.height) + }) + + + TweenMax.set(overlay, { + display: "flex", + autoAlpha: 0 + }) + + TweenMax.to(imageWrapper, this.zoomAnimationDuration, { + x: targetOffset.x, + y: targetOffset.y, + width: targetDimensions.width, + height: targetDimensions.height, + }) + TweenMax.to(overlay, this.fadeAnimationTime, { + autoAlpha: 1 + }) + } + + image.src = src + } + + _replaceIcon(clone) { + let zoomIcon = clone.querySelector(".icon.zoom") + zoomIcon.classList.remove("zoom") + zoomIcon.classList.add("close") + } + + getBorderHeight(style) { + const borderWidth = parseInt(style.borderTopWidth) + parseInt(style.borderBottomWidth) + const padding = parseInt(style.paddingTop) + parseInt(style.paddingBottom) + return parseInt(style.width) + borderWidth + padding + } + + getBorderWidth(style) { + const borderWidth = parseInt(style.borderLeftWidth) + parseInt(style.borderRightWidth) + const padding = parseInt(style.paddingLeft) + parseInt(style.paddingRight) + return parseInt(style.width) + borderWidth + padding + } + + closeThumnailDetail(context) { + let overlay = context.querySelector(".img-overlay") + + let timeline = new TimelineLite() + + timeline.to(overlay, this.fadeAnimationDuration, { + autoAlpha: 0 + }).set(overlay, { + display: "none" + }) + } + +} + +CardPlugin.Ui = class UiPlugin extends CardPluginBase { + constructor(className, parent = null) { + super() + this.parent = parent + this.className = className + } + + _getVerificationFunctions(context) { + let arr = super._getVerificationFunctions(context) + let func = [ + this._doesParentExist.bind(this, context, this.parent) + ] + return arr.concat(func) + } + + _doesParentExist(context, parent) { + if (parent == null) return true + let valid = (context.querySelector(parent) != null) + if (!valid) console.error("Could not find parent on context.", context, parent) + return valid + } + + append(context) { + parent = (this.parent == null) ? context : context.querySelector(this.parent).appendChild(container) + let container = document.createElement("div") + container.className = this.className + parent.appendChild(container) + } + +} + +CardPlugin.Speech = class SpeechPlugin extends CardPluginBase { + + constructor(parentSelector, className, interactionType = "tap") { + super() + this.className = className + this.parentSelector = parentSelector + this.interactionType = interactionType + + } + + get require() { + return [ + CardPlugin.Ui + ] + } + + append(context) { + let container = context.querySelector(this.parentSelector) + this.button = document.createElement("div") + this.button.className = "icon button " + this.className + container.appendChild(this.button) + + InteractionMapper.on(this.interactionType, this.button, () => { + let subcard = context.querySelector(".mainview > .subcard") + let target = (subcard) ? subcard : context + + this.speak(target) + + }) + } + + _activate() { + this._disableActive() + this.active = this + this._activateButton() + } + + _activateButton() { + if (this.button) + this.button.classList.add("active") + } + + _deactivate() { + this._deactivateButton() + } + + _deactivateButton() { + if (this.button) + this.button.classList.remove("active") + } + + _isSameNode(node) { + //console.log(this.currentText, node.innerText) + return (this.currentText == node.innerText) + } + + speak(node) { + + console.log(this._isSameNode(node)) + + if (!window.speechSynthesis.speaking) { + console.log("Noone talking!") + this._start(node) + } else if (this._isSameNode(node)) { + console.log("Requested same!") + this._stop() + + } else { + console.log("Requested Different!") + this._stop() + this._start(node) + } + + } + + _disableActive() { + console.log("disableActive:", this.active) + if (this.active) { + this.active._deactivate() + } + } + + _start(node) { + this.currentText = node.innerText + let utterance = new SpeechSynthesisUtterance(node.innerText) + + let voices = window.speechSynthesis.getVoices() + console.log(voices) + let voice = voices.filter((val) => { + //console.log(val) + return val.name == "Microsoft Hedda Desktop - German" + })[0] + + //console.log(voice) + + utterance.voice = voice + console.log("TALK: ", utterance) + window.speechSynthesis.speak(utterance) + this._activate() + window.speechSynthesis.resume() + + + utterance.onboundary = () => { console.log("onboundary", node.innerText); if (this.currentText.substring(0, 5) != node.innerText.substring(0, 5)) { console.log("text for speech synth changed!", this.currentText, node.innerText); this._stop() } } + utterance.onend = () => console.log("onend", node.innerText) + utterance.onerror = () => console.log("onerror", node.innerText) + utterance.onmark = () => console.log("onmark", node.innerText) + utterance.onpause = () => console.log("onpause", node.innerText) + utterance.onresume = () => console.log("onresume", node.innerText) + utterance.onstart = () => console.log("onstart", node.innerText) + utterance.onerror = () => console.log("onerror", node.innerText) + } + + _stop() { + window.speechSynthesis.cancel() + this.currentText = null + this._deactivate() + } + + get active() { + return this.constructor.active + } + + set active(val) { this.constructor.active = val } + + get currentText() { + return this.constructor.text + } + + set currentText(val) { + this.constructor.text = val + } + +} diff --git a/lib/card/scatter.js b/lib/card/scatter.js new file mode 100644 index 0000000..885649b --- /dev/null +++ b/lib/card/scatter.js @@ -0,0 +1,145 @@ +/** + * Extends the card with scatter functionality. + * + * @class ScatterCard + */ +export default 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 = [], + onClose = null + } = {}) { + console.log(basePath) + return new Promise((resolve, reject) => { + let url = basePath + "/" + item + "/index.html" + console.log("Loading", url) + this.loadHTML(url) + .then(html => { + console.log("Received", 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 + } + +} + +ScatterCard.selectedLanguage = 0 +ScatterCard.languages = ["Deutsch", "English"] +ScatterCard.languageTags = { + Deutsch: "de", + English: "en" +} +ScatterCard.scatterContainer = null +