Merge branch 'master' of gitea.iwm-tuebingen.de:IWMBrowser/iwmlib

# Bitte geben Sie eine Commit-Beschreibung ein, um zu erklären, warum dieser
# Merge erforderlich ist, insbesondere wenn es einen aktualisierten
# Upstream-Branch mit einem Thema-Branch zusammenführt.
#
# Zeilen, die mit '#' beginnen, werden ignoriert,
# und eine leere Beschreibung bricht den Commit ab.
Merged.
This commit is contained in:
Sebastian Kupke 2019-07-17 10:06:26 +02:00
commit 7f00898767
5 changed files with 2608 additions and 2752 deletions

1236
dist/iwmlib.js vendored

File diff suppressed because it is too large Load Diff

View File

@ -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 { 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 UITest from './uitest.js'
import Card from './card/card.js'
import CardWrapper from './card/wrapper.js' import CardWrapper from './card/wrapper.js'
import Highlight from './card/highlight.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' import Theme from './card/theme.js'
/* Needed to ensure that rollup.js includes class definitions and the classes /* Needed to ensure that rollup.js includes class definitions and the classes

View File

@ -29,7 +29,7 @@ const enableNearestNeighborTaps = false
* *
* The class is used as a namespace and should never called with new. * The class is used as a namespace and should never called with new.
*/ */
export class Card { export default class Card {
static setup(context, modules = []) { static setup(context, modules = []) {
console.log("Setup Card...", modules) console.log("Setup Card...", modules)
@ -431,7 +431,7 @@ export class Card {
zIndex: this.zIndices.popup zIndex: this.zIndices.popup
}) })
TweenMax.to(popup.element, this.animation.popup, { TweenLite.to(popup.element, this.animation.popup, {
autoAlpha: 1, autoAlpha: 1,
ease: Power2.easeIn ease: Power2.easeIn
}) })
@ -451,7 +451,7 @@ export class Card {
* TEST if this intereferes with the editor. * TEST if this intereferes with the editor.
*/ */
if (overlay) { if (overlay) {
TweenMax.to(overlay, 0.2, { TweenLite.to(overlay, 0.2, {
autoAlpha: 0, onComplete: () => { autoAlpha: 0, onComplete: () => {
popup.remove() popup.remove()
//this._cleanup(context) //this._cleanup(context)
@ -507,9 +507,9 @@ export class Card {
if (editable) { if (editable) {
if (this.debug) console.log("Append overlay.", context) if (this.debug) console.log("Append overlay.", context)
overlay.classList.add('overlay') overlay.classList.add('overlay')
TweenMax.set(overlay, { autoAlpha: 0 }) TweenLite.set(overlay, { autoAlpha: 0 })
context.appendChild(overlay) 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. // Extract the body from the Popup site.
@ -949,17 +949,17 @@ export class Card {
const scaleFactor = 2 const scaleFactor = 2
const transformOrigin = 'bottom right' const transformOrigin = 'bottom right'
TweenMax.set(zoomedFig, { TweenLite.set(zoomedFig, {
x: current.x, x: current.x,
y: current.y, y: current.y,
width: current.width + borderX, width: current.width + borderX,
height: current.height + borderY, height: current.height + borderY,
transformOrigin transformOrigin
}) })
TweenMax.set(zoomable, { opacity: 0 }) TweenLite.set(zoomable, { opacity: 0 })
let icon = zoomedFig.querySelector(".icon") let icon = zoomedFig.querySelector(".icon")
TweenMax.set(icon, { TweenLite.set(icon, {
transformOrigin transformOrigin
}) })
zoomedFig.style.transformOrigin = "calc(100% - " + parseFloat(zoomedFigStyle.borderRightWidth) + "px) calc(100% - " + parseFloat(zoomedFigStyle.borderBottomWidth) + "px)" zoomedFig.style.transformOrigin = "calc(100% - " + parseFloat(zoomedFigStyle.borderRightWidth) + "px) calc(100% - " + parseFloat(zoomedFigStyle.borderBottomWidth) + "px)"
@ -1029,7 +1029,7 @@ export class Card {
zoomParent.appendChild(zoomedFig) zoomParent.appendChild(zoomedFig)
zoomedFig.style.opacity = 0.5 zoomedFig.style.opacity = 0.5
zoomContainer.appendChild(zoomable) 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 editor = mainController.topController().ensureEditor(img)
let savedDisplay = zoomIcon.style.display let savedDisplay = zoomIcon.style.display
let iconClone = zoomIcon.cloneNode(true) let iconClone = zoomIcon.cloneNode(true)
@ -1046,14 +1046,14 @@ export class Card {
zoomedFig.remove() zoomedFig.remove()
zoomContainer.remove() zoomContainer.remove()
zoomParent.appendChild(zoomable) zoomParent.appendChild(zoomable)
TweenMax.set(zoomable, { x: 0, y: 0 }) TweenLite.set(zoomable, { x: 0, y: 0 })
zoomable.onmousedown = null zoomable.onmousedown = null
zoomable.onmousemove = null zoomable.onmousemove = null
zoomable.onmouseup = null zoomable.onmouseup = null
zoomable.onmousewheel = null zoomable.onmousewheel = null
} }
wrapper.appendChild(iconClone) 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 => { zoomable.onmousedown = event => {
if (this.debug) console.log('mousedown', event.target) if (this.debug) console.log('mousedown', event.target)
@ -1067,7 +1067,7 @@ export class Card {
event.preventDefault() event.preventDefault()
let dx = event.pageX - zoomable.dragStartPos.x let dx = event.pageX - zoomable.dragStartPos.x
let dy = event.pageY - zoomable.dragStartPos.y 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 } zoomable.dragStartPos = { x: event.pageX, y: event.pageY }
if (editor) { if (editor) {
editor.showControls() editor.showControls()
@ -1086,7 +1086,7 @@ export class Card {
let zoom = direction ? zoomFactor : 1 / zoomFactor let zoom = direction ? zoomFactor : 1 / zoomFactor
startZoom *= zoom startZoom *= zoom
TweenMax.set(zoomable, { scale: startZoom }) TweenLite.set(zoomable, { scale: startZoom })
if (editor) { if (editor) {
editor.showControls() editor.showControls()
} }
@ -1117,17 +1117,17 @@ export class Card {
let zoomedCaption = zoomedFig.querySelector("figcaption.zoomcap") let zoomedCaption = zoomedFig.querySelector("figcaption.zoomcap")
TweenMax.to(zoomedCaption, this.animation.fade, { TweenLite.to(zoomedCaption, this.animation.fade, {
autoAlpha: 0, autoAlpha: 0,
}) })
TweenMax.to(zoomedFig, this.animation.zoomable, { TweenLite.to(zoomedFig, this.animation.zoomable, {
css: { css: {
scaleX: 1, scaleX: 1,
scaleY: 1 scaleY: 1
}, },
onComplete: () => { onComplete: () => {
TweenMax.set(zoomable, { TweenLite.set(zoomable, {
opacity: 1 opacity: 1
}) })
let div = zoomedFig.parentNode let div = zoomedFig.parentNode
@ -1204,7 +1204,7 @@ export class Card {
let padding = parseInt(this.css(indexbox, 'padding')) let padding = parseInt(this.css(indexbox, 'padding'))
let maxWidth = this.css(card, 'max-width') let maxWidth = this.css(card, 'max-width')
TweenMax.set(clone, { TweenLite.set(clone, {
css: { css: {
position: 'absolute', position: 'absolute',
width: globalIndexCardRect.width, width: globalIndexCardRect.width,
@ -1215,12 +1215,12 @@ export class Card {
} }
}) })
TweenMax.set(articleClone, { TweenLite.set(articleClone, {
autoAlpha: 0 autoAlpha: 0
}) })
TweenMax.set(card, { css: { maxWidth: '100%' } }) TweenLite.set(card, { css: { maxWidth: '100%' } })
TweenMax.set(clone, { TweenLite.set(clone, {
x: localOrigin.x - padding, x: localOrigin.x - padding,
y: localOrigin.y - padding, y: localOrigin.y - padding,
scaleX, scaleX,
@ -1244,10 +1244,10 @@ export class Card {
/** /**
* Scale the content from 100% to it's target size. * Scale the content from 100% to it's target size.
*/ */
// TweenMax.set(subcardContent, { // TweenLite.set(subcardContent, {
// height: "100%" // height: "100%"
// }) // })
// TweenMax.to(subcardContent, Card.animation.articleTransition, { // TweenLite.to(subcardContent, Card.animation.articleTransition, {
// height: targetHeight + "px" // height: targetHeight + "px"
// }) // })
} }
@ -1270,7 +1270,7 @@ export class Card {
} }
let desiredBorderBottomWidth = parseInt(window.getComputedStyle(titlebar).borderBottomWidth) let desiredBorderBottomWidth = parseInt(window.getComputedStyle(titlebar).borderBottomWidth)
TweenMax.to(clone, Card.animation.articleTransition, { TweenLite.to(clone, Card.animation.articleTransition, {
x: -padding, x: -padding,
y: -padding, y: -padding,
ease: ExpoScaleEase.config(scaleX, 1), ease: ExpoScaleEase.config(scaleX, 1),
@ -1282,10 +1282,10 @@ export class Card {
onUpdateParams: ['{self}'], onUpdateParams: ['{self}'],
onUpdate: (self) => { onUpdate: (self) => {
let transform = self.target._gsTransform let transform = self.target._gsTransform
TweenMax.set(title, { TweenLite.set(title, {
scale: 1 / transform.scaleX scale: 1 / transform.scaleX
}) })
TweenMax.set(titlebar, { TweenLite.set(titlebar, {
height: start.height * 1 / transform.scaleY 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, delay: this.animation.articleTransition / 2,
autoAlpha: 1 autoAlpha: 1
}) })
@ -1327,11 +1327,11 @@ export class Card {
let titlebarStyle = window.getComputedStyle(previewTitlebar) let titlebarStyle = window.getComputedStyle(previewTitlebar)
let titlebar = clone.querySelector(".titlebar") let titlebar = clone.querySelector(".titlebar")
TweenMax.to(titlebar, this.animation.articleTransition, { TweenLite.to(titlebar, this.animation.articleTransition, {
height: parseInt(titlebarStyle.height) height: parseInt(titlebarStyle.height)
}) })
TweenMax.to(articleClone, this.animation.articleTransition / 2, { TweenLite.to(articleClone, this.animation.articleTransition / 2, {
autoAlpha: 0 autoAlpha: 0
}) })
@ -1341,13 +1341,13 @@ export class Card {
} }
if (this.dynamicHeight) { if (this.dynamicHeight) {
TweenMax.to(subcardContent, this.animation.articleTransition, { TweenLite.to(subcardContent, this.animation.articleTransition, {
height: "100%" height: "100%"
}) })
} }
TweenMax.set(card, { autoAlpha: 1, css: { maxWidth } }) TweenLite.set(card, { autoAlpha: 1, css: { maxWidth } })
TweenMax.to(clone, this.animation.articleTransition, { TweenLite.to(clone, this.animation.articleTransition, {
x: localOrigin.x - padding, x: localOrigin.x - padding,
y: localOrigin.y - padding, y: localOrigin.y - padding,
scaleX, scaleX,
@ -1356,7 +1356,7 @@ export class Card {
rotation: angle, rotation: angle,
onComplete: () => { onComplete: () => {
// article.remove() // article.remove()
TweenMax.to(clone, this.animation.fade, TweenLite.to(clone, this.animation.fade,
{ {
//delay: 0.2, //delay: 0.2,
autoAlpha: 0, autoAlpha: 0,
@ -1373,11 +1373,11 @@ export class Card {
onUpdate: function (self) { onUpdate: function (self) {
let transform = self.target._gsTransform let transform = self.target._gsTransform
TweenMax.set(title, { TweenLite.set(title, {
scale: 1 / transform.scaleX scale: 1 / transform.scaleX
}) })
TweenMax.set(titlebar, { TweenLite.set(titlebar, {
height: original.height * 1 / transform.scaleY height: original.height * 1 / transform.scaleY
}) })
@ -1508,7 +1508,7 @@ export class Card {
// TODO: What is this good for? // TODO: What is this good for?
// let article = parsedHTML.querySelector('article') // let article = parsedHTML.querySelector('article')
// card.insertAdjacentElement('afterbegin', article) // card.insertAdjacentElement('afterbegin', article)
// TweenMax.set(article, { autoAlpha: 0 }) // TweenLite.set(article, { autoAlpha: 0 })
Card.expandIndexCard(card, parsedHTML, 'article', relativeSource, saveCallback) Card.expandIndexCard(card, parsedHTML, 'article', relativeSource, saveCallback)
} }
@ -1883,680 +1883,3 @@ Card.animation = {
zoomable: 0.5 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
}
}

530
lib/card/plugin.js Normal file
View File

@ -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
}
}

145
lib/card/scatter.js Normal file
View File

@ -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