Updated cards from tuesch changes. Incorporated InteractionMapper memory leak fix.

This commit is contained in:
2019-08-13 14:31:03 +02:00
parent 03be6673c5
commit 30c7113713
193 changed files with 21151 additions and 474 deletions
+207 -114
View File
@@ -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