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