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
+