Updated cards from tuesch changes. Incorporated InteractionMapper memory leak fix.
This commit is contained in:
+207
-114
@@ -1,3 +1,6 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
/** To avoid problems with relative URL paths, we use inline data URI to load svg icons. */
|
||||
const closeIconDataURI = `data:image/svg+xml;utf8,
|
||||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
@@ -31,11 +34,35 @@ export default class Card {
|
||||
static setup(context, modules = []) {
|
||||
console.log('Setup Card...', modules)
|
||||
context.modules = []
|
||||
|
||||
context.module = {}
|
||||
|
||||
context.classList.add('info-card')
|
||||
context.setAttribute('data-id', Card.id++)
|
||||
|
||||
modules.forEach(module => {
|
||||
if (module.apply(context)) context.modules.push(module.constructor.name)
|
||||
if (module.apply(context)) {
|
||||
const moduleName = module.constructor.name
|
||||
context.modules.push(moduleName)
|
||||
context.module[moduleName] = module
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static remove(context) {
|
||||
for (let module of Object.values(context.module)) {
|
||||
module.remove()
|
||||
}
|
||||
}
|
||||
|
||||
static eventClose(event) {
|
||||
let context = this.getContext(event.target)
|
||||
|
||||
if (context) {
|
||||
this.constructor.close(context)
|
||||
} else console.error('Could not find context!', event.target)
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*
|
||||
@@ -43,13 +70,12 @@ export default class Card {
|
||||
* @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)
|
||||
static close(context) {
|
||||
console.log('CLOSE CARD!!!')
|
||||
this.unregisterAllEvents(context)
|
||||
if (context.onClose) {
|
||||
context.onClose(event)
|
||||
} else context.parentNode.removeChild(context)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -61,12 +87,12 @@ export default class Card {
|
||||
* @param {*} replaceFunc
|
||||
* @memberof Card
|
||||
*/
|
||||
static _replaceAttributes(html, attribute, replaceFunc) {
|
||||
static _replaceAttributes(context, html, attribute, replaceFunc) {
|
||||
let clickables = html.querySelectorAll(`[${attribute}]`)
|
||||
clickables.forEach(element => {
|
||||
let attributeVal = element.getAttribute(attribute)
|
||||
element.removeAttribute(attribute)
|
||||
replaceFunc.call(this, element, attributeVal)
|
||||
replaceFunc.call(this, context, element, attributeVal)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -80,7 +106,7 @@ export default class Card {
|
||||
* @returns
|
||||
* @memberof Card
|
||||
*/
|
||||
static _replaceCallback(element, attributeVal) {
|
||||
static _replaceCallback(context, element, attributeVal) {
|
||||
if (element.tagName == 'A') {
|
||||
element.addEventListener('click', event => {
|
||||
event.preventDefault()
|
||||
@@ -112,7 +138,7 @@ export default class Card {
|
||||
// These are 'hardcoded' inside the convert.js.
|
||||
if (element.tagName == 'circle') return false
|
||||
|
||||
InteractionMapper.on(interactionType, element, event => {
|
||||
this.registerEvent(context, interactionType, element, event => {
|
||||
/**
|
||||
* Replaces the strings from the listener with the cooresponding variables.
|
||||
*/
|
||||
@@ -164,6 +190,13 @@ export default class Card {
|
||||
/<\s*(a|video|img|image|circle)\s(.*?)(xlink:href|href|src)\s*=\s*["'](\..*?)["']\s*(.*?)>/g,
|
||||
function(data) {
|
||||
let path = that._getRelativePath(arguments[4])
|
||||
|
||||
console.log('REPLACE ', arguments[1])
|
||||
if (arguments[1] == 'a') {
|
||||
console.error('NOT REPLACING LINKS')
|
||||
return ''
|
||||
}
|
||||
|
||||
const tag = `<${arguments[1]} ${arguments[2]} ${arguments[3]}="${path}" ${arguments[5]}>`
|
||||
/* if (that.debug) */ console.log('Adjusted: ', tag)
|
||||
return tag
|
||||
@@ -361,6 +394,7 @@ export default class Card {
|
||||
if (this.debug) console.log('Close Popup.', context, popup)
|
||||
window.popup = popup
|
||||
popup.close()
|
||||
InteractionMapper.off(popup.element)
|
||||
this._unsetPopup(context)
|
||||
} else {
|
||||
console.error('Requested to close popup, but popup was not found.')
|
||||
@@ -730,7 +764,7 @@ export default class Card {
|
||||
// but for simplicity it's just done here for now.
|
||||
// TODO: Adjust to load while animating (Problem: Unload when cancelled).
|
||||
console.log('loadHighlightPopup', src, position, local)
|
||||
this._loadPopupContent(src)
|
||||
this._loadPopupContent(context, src)
|
||||
.then(content => {
|
||||
this._openPopup(context, src, local, content, {
|
||||
highlight: node,
|
||||
@@ -757,14 +791,14 @@ export default class Card {
|
||||
* @returns {Promise} - Returns a promise, that's resolved when the data is loaded.
|
||||
* @memberof Card
|
||||
*/
|
||||
static _loadPopupContent(source) {
|
||||
static _loadPopupContent(context, 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 html = this.postProcessResponseText(context, xhr.responseText)
|
||||
let selector = Card.popupHtmlSelector
|
||||
let content = { html: html.body.innerHTML, selector }
|
||||
resolve(content)
|
||||
@@ -859,6 +893,9 @@ export default class Card {
|
||||
let wrapper = this.getContext(node)
|
||||
|
||||
let zoomable = node.closest('figure')
|
||||
if (zoomable == null) {
|
||||
return
|
||||
}
|
||||
|
||||
// load mainimg - if none exists, there is nothing to open
|
||||
let img = zoomable.querySelector('.mainimg')
|
||||
@@ -1117,18 +1154,18 @@ export default class Card {
|
||||
}
|
||||
|
||||
/**
|
||||
* Closes a zoomable object with animation
|
||||
* Closes an zoomable object with an 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)
|
||||
* @param {DOMElement} context - Context of the zoomable.
|
||||
* @param {*} zoomable
|
||||
* @param {*} zoomedFig
|
||||
* @memberof Card
|
||||
*/
|
||||
static closeZoomable(context, zoomable, zoomedFig) {
|
||||
if (this.debug) console.log('Close Zoomable', context, zoomable, zoomedFig)
|
||||
|
||||
//TODO: Why do I need this check. Shouldn't it be always present?! - SO
|
||||
if (zoomable) {
|
||||
this._unsetZoomable(context)
|
||||
let caption = zoomable.querySelector('figcaption.cap')
|
||||
@@ -1149,12 +1186,15 @@ export default class Card {
|
||||
TweenLite.set(zoomable, {
|
||||
opacity: 1
|
||||
})
|
||||
|
||||
let div = zoomedFig.parentNode
|
||||
let videoElement = div.querySelector('video')
|
||||
if (videoElement) videoElement.pause()
|
||||
div.parentNode.removeChild(div)
|
||||
}
|
||||
})
|
||||
|
||||
InteractionMapper.off(zoomedFig)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1244,6 +1284,7 @@ export default class Card {
|
||||
rotation: angle
|
||||
})
|
||||
indexbox.prepend(clone)
|
||||
clone.setAttribute('data-source', src)
|
||||
|
||||
let titlebar = clone.querySelector('.titlebar')
|
||||
let title = titlebar.querySelector('h2')
|
||||
@@ -1268,7 +1309,7 @@ export default class Card {
|
||||
}
|
||||
|
||||
//jquery hyphenate below
|
||||
if (typeof $ != 'undefined') {
|
||||
if (this.constructor._jQueryIsPresent()) {
|
||||
$('.column')
|
||||
.not('.overview')
|
||||
.children('p')
|
||||
@@ -1323,86 +1364,6 @@ export default class Card {
|
||||
|
||||
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')
|
||||
|
||||
TweenLite.to(titlebar, this.animation.articleTransition, {
|
||||
height: parseInt(titlebarStyle.height)
|
||||
})
|
||||
|
||||
TweenLite.to(articleClone, this.animation.articleTransition / 2, {
|
||||
autoAlpha: 0
|
||||
})
|
||||
|
||||
let title = titlebar.querySelector('h2')
|
||||
let original = {
|
||||
height: parseInt(titlebarStyle.height)
|
||||
}
|
||||
|
||||
if (this.dynamicHeight) {
|
||||
TweenLite.to(subcardContent, this.animation.articleTransition, {
|
||||
height: '100%'
|
||||
})
|
||||
}
|
||||
|
||||
TweenLite.set(card, { autoAlpha: 1, css: { maxWidth } })
|
||||
TweenLite.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()
|
||||
TweenLite.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
|
||||
|
||||
TweenLite.set(title, {
|
||||
scale: 1 / transform.scaleX
|
||||
})
|
||||
|
||||
TweenLite.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')
|
||||
|
||||
@@ -1434,6 +1395,9 @@ export default class Card {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const eventElements = [indexbox, iconClone, clone]
|
||||
|
||||
// Use the 'tap' event for closing.
|
||||
// Otherwise the subcard cannot be closed,
|
||||
// when another subcard is touched.
|
||||
@@ -1443,17 +1407,113 @@ export default class Card {
|
||||
if (isDirty) {
|
||||
mainController.saveNode(html.innerHTML, url => {
|
||||
callback(url)
|
||||
closeAnimation()
|
||||
this._closeIndexCard(context, card,{
|
||||
eventElements,
|
||||
src
|
||||
})
|
||||
})
|
||||
} else {
|
||||
closeAnimation()
|
||||
this._closeIndexCard(context, card)
|
||||
}
|
||||
} else {
|
||||
closeAnimation()
|
||||
this._closeIndexCard(context, card)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static _closeIndexCard(context, card, {
|
||||
eventElements = [],
|
||||
src = null
|
||||
} = []) {
|
||||
//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._subcardChanged(context, true)
|
||||
this._enableCardCloseButton(context)
|
||||
|
||||
let previewTitlebar = card.querySelector('.titlebar')
|
||||
let titlebarStyle = window.getComputedStyle(previewTitlebar)
|
||||
let titlebar = clone.querySelector('.titlebar')
|
||||
|
||||
TweenLite.to(titlebar, this.animation.articleTransition, {
|
||||
height: parseInt(titlebarStyle.height)
|
||||
})
|
||||
|
||||
TweenLite.to(articleClone, this.animation.articleTransition / 2, {
|
||||
autoAlpha: 0
|
||||
})
|
||||
|
||||
let title = titlebar.querySelector('h2')
|
||||
let original = {
|
||||
height: parseInt(titlebarStyle.height)
|
||||
}
|
||||
|
||||
if (this.dynamicHeight) {
|
||||
TweenLite.to(subcardContent, this.animation.articleTransition, {
|
||||
height: '100%'
|
||||
})
|
||||
}
|
||||
|
||||
TweenLite.set(card, { autoAlpha: 1, css: { maxWidth } })
|
||||
TweenLite.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()
|
||||
TweenLite.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
|
||||
|
||||
TweenLite.set(title, {
|
||||
scale: 1 / transform.scaleX
|
||||
})
|
||||
|
||||
TweenLite.set(titlebar, {
|
||||
height: (original.height * 1) / transform.scaleY
|
||||
})
|
||||
|
||||
// Retain the border at same visual thickness.
|
||||
titlebar.style.borderBottomWidth = desiredBorderBottomWidth / transform.scaleY + 'px'
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests if jQuery is properly included in the project.
|
||||
* Otherwise specific features may not work correctly (e.g. hyphenation)
|
||||
*/
|
||||
_jQueryIsPresent() {
|
||||
let jQueryInitialized = typeof $ != 'undefined'
|
||||
if (!jQueryInitialized) console.error('No jQuery is provided. Specific features may fail.')
|
||||
return jQueryInitialized
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens the index card. Called by the zoom icon click handler.
|
||||
* The assumed card structure is as follows:
|
||||
@@ -1516,14 +1576,10 @@ export default class Card {
|
||||
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)
|
||||
// TweenLite.set(article, { autoAlpha: 0 })
|
||||
|
||||
let parsedHTML = this.postProcessResponseText(context, html)
|
||||
Card.expandIndexCard(card, parsedHTML, 'article', relativeSource, saveCallback)
|
||||
|
||||
this._subcardChanged(context)
|
||||
}
|
||||
}
|
||||
xhr.onerror = () => {
|
||||
@@ -1649,7 +1705,8 @@ export default class Card {
|
||||
* @returns
|
||||
* @memberof Card
|
||||
*/
|
||||
static postProcessResponseText(htmlString) {
|
||||
static postProcessResponseText(context, htmlString) {
|
||||
console.error('RUN POSTPROCESS')
|
||||
let editable = this.isEditable()
|
||||
|
||||
htmlString = this._adjustRelativeLinks(htmlString)
|
||||
@@ -1657,7 +1714,7 @@ export default class Card {
|
||||
let parser = new DOMParser()
|
||||
let html = parser.parseFromString(htmlString, 'text/html')
|
||||
if (!editable) {
|
||||
this._replaceAttributes(html, 'onclick', this._replaceCallback)
|
||||
this._replaceAttributes(context, html, 'onclick', this._replaceCallback)
|
||||
}
|
||||
let zoomableWrappers = html.querySelectorAll('.svg-wrapper')
|
||||
zoomableWrappers.forEach(wrapper => {
|
||||
@@ -1778,7 +1835,7 @@ export default class Card {
|
||||
this._setPopupSource(popup, source)
|
||||
context.popup = popup
|
||||
|
||||
if (typeof $ != 'undefined') {
|
||||
if (this.constructor._jQueryIsPresent()) {
|
||||
//jquery hyphenate below
|
||||
console.log('hyphenated popup:', $('span').hyphenate('de'))
|
||||
}
|
||||
@@ -1877,8 +1934,44 @@ export default class Card {
|
||||
static get relativePath() {
|
||||
return Card._relativePath
|
||||
}
|
||||
|
||||
static getRegisteredEvents(context) {
|
||||
return context._registeredEvents == null ? [] : context._registeredEvents
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper method that registers InteractionMapper events on the info card.
|
||||
* Those events are saved on the context element. An they are unregistered when the
|
||||
* card is closed again.
|
||||
*
|
||||
* @static
|
||||
* @param {DOMElement} context - Context of the Card.
|
||||
* @param {string} types
|
||||
* @param {DOMElement} element -Element on which the event is registered.
|
||||
* @param {Function} callback - Function thats called when the event occurs.
|
||||
* @memberof Card
|
||||
*/
|
||||
static registerEvent(context, types, element, callback) {
|
||||
InteractionMapper.on(types, element, callback)
|
||||
if (context._registeredEvents == null) context._registeredEvents = []
|
||||
if (context._registeredEvents.indexOf(element) == -1) context._registeredEvents.push(element)
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregisters all events on the infocard.
|
||||
*
|
||||
*
|
||||
* @static
|
||||
* @param {DomElement} context - Context of the card.
|
||||
* @memberof Card
|
||||
*/
|
||||
static unregisterAllEvents(context) {
|
||||
let eventElements = this.getRegisteredEvents(context)
|
||||
InteractionMapper.off(eventElements)
|
||||
}
|
||||
}
|
||||
|
||||
Card.id = 0
|
||||
Card.debug = true
|
||||
Card._relativePath = ''
|
||||
Card.scatterContainer = null
|
||||
|
||||
Reference in New Issue
Block a user