iwmlib/lib/card/plugin.js

559 lines
16 KiB
JavaScript

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