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