Added files to make classes better accessible.

This commit is contained in:
Uwe Oestermeier 2019-07-17 09:56:20 +02:00
parent 78108c4090
commit 17bbe1a90e
5 changed files with 2574 additions and 2718 deletions

3870
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 UITest from './uitest.js'
import Card from './card/card.js'
import CardWrapper from './card/wrapper.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'
/* 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.
*/
export class Card {
export default class Card {
static setup(context, modules = []) {
console.log("Setup Card...", modules)
@ -113,7 +113,7 @@ export class Card {
let interactionType = this.interactionType
//Remove the events on the circle.
//Remove the events on the circle.
// These are 'hardcoded' inside the convert.js.
if (element.tagName == "circle") return false
@ -155,11 +155,11 @@ export class Card {
/**
* Transform the relative links to absolute ones.
*
*
* Currently covers:
* Tags: a,img, image, circle
* Attributes: xlink:href,href,src (first occurence only)
*
*
* @static
* @param {DomElement} element - The children of the element are inspected, if there are relative paths, that has to be adjusted to absolute ones.
* @returns
@ -167,7 +167,7 @@ export class Card {
*/
static _adjustRelativeLinks(html) {
const that = this
/*
/*
This RegEx finds all requested tags[1], and all requested attributes[3] and replaces the relative path [4] with the absolute one.
while all other attributes [2],[5] are preserved.
*/
@ -383,7 +383,7 @@ export class Card {
* @param {Point} position - The position, where the popup will be shown.
* @param {object} content - The content of the popup as required by the Popup class.
* @param {object} [options={}] - Additional options for the popup (optional).
* @returns {Promise} - Returns a Promise, which is resolved, when the Popup is fully loaded.
* @returns {Promise} - Returns a Promise, which is resolved, when the Popup is fully loaded.
* @memberof Card
*/
static _createPopup(context, position, content, options = {}) {
@ -399,7 +399,7 @@ export class Card {
posOffset: 10
}, options)))
// Placing the popup when it required loading,
// Placing the popup when it required loading,
// it resulted in flahing up at the default position.
// We manually prevent this here.
popup.element.style.display = "none"
@ -447,7 +447,7 @@ export class Card {
/**
* The cleanup functionality is now covered by the _cleanup function.
* It cleans up zoomables, popups and open image highlights.
*
*
* TEST if this intereferes with the editor.
*/
if (overlay) {
@ -522,14 +522,14 @@ export class Card {
})
let html = popupPage.body.innerHTML
/**
* We do not want to use the popup class again.
* We do not want to use the popup class again.
* This results in problems when styling the popup.
* (You could style the .unselectable.popup, but that
* wouldn't be clean).
*
* (You could style the .unselectable.popup, but that
* wouldn't be clean).
*
* Therefore the selector was changed:
* '.popup' => '.popupHtml'
*
*
* Which describes the construct even better than the
* '.popup' class.
*/
@ -678,8 +678,8 @@ export class Card {
let context = this.getContext(node)
event.stopPropagation()
/**
* This node is the documents body, as events wont work
/**
* This node is the documents body, as events wont work
* on svg elements properly. We need a workaround for that.
*/
let src = node.getAttribute("xlink:href")
@ -746,7 +746,7 @@ export class Card {
*
* @static
* @private
* @param {string} source - Url to a popup file.
* @param {string} source - Url to a popup file.
* @returns {Promise} - Returns a promise, that's resolved when the data is loaded.
* @memberof Card
*/
@ -799,9 +799,9 @@ export class Card {
static zoomableCurrentGeometry(zoomable, wrapper) {
/*
I don't think it's wise, that the zoomable calculation relies on
some icon that may or may not be present. When the same calculation can be
/*
I don't think it's wise, that the zoomable calculation relies on
some icon that may or may not be present. When the same calculation can be
done using the bounding box of the desired element.
- SO
*/
@ -990,7 +990,7 @@ export class Card {
}
/**
* Selects and transforms the zoomicon from a zoomicon to a closeicon
* Selects and transforms the zoomicon from a zoomicon to a closeicon
* or the other way around.
*
* @static
@ -1174,8 +1174,8 @@ export class Card {
/**
* We have to reorder the clone, as it still contains the
* preview text image. And the new html is
* We have to reorder the clone, as it still contains the
* preview text image. And the new html is
* inserted before everything else.
*/
let cloneWrapper = clone.querySelector(".wrapper")
@ -1418,8 +1418,8 @@ export class Card {
}
})
}
// Use the 'tap' event for closing.
// Otherwise the subcard cannot be closed,
// Use the 'tap' event for closing.
// Otherwise the subcard cannot be closed,
// when another subcard is touched.
InteractionMapper.on("tap", iconClone, () => {
if (editable) {
@ -1505,7 +1505,7 @@ export class Card {
let html = xhr.responseText
let parsedHTML = this.postProcessResponseText(html)
// TODO: What is this good for?
// TODO: What is this good for?
// let article = parsedHTML.querySelector('article')
// card.insertAdjacentElement('afterbegin', article)
// TweenMax.set(article, { autoAlpha: 0 })
@ -1542,7 +1542,7 @@ export class Card {
*
* @static
* @param {DomElement} child - A dom element for which the context shall be retrieved.
* @returns {DomElement} - The containing context / info-card.
* @returns {DomElement} - The containing context / info-card.
* @memberof Card
*/
static getContext(child) {
@ -1585,7 +1585,7 @@ export class Card {
/**
* Retrieves an Rectangle for an element in the local space of a provided context.
*
*
* Note: This works also for rotated DomElements unlike the Element.getBoundingClientRectangle method.
*
* @static
@ -1610,7 +1610,7 @@ export class Card {
* Gets a rectangle in global space for a provided element.
*
* Note: This works also for rotated DomElements unlike the Element.getBoundingClientRectangle method.
*
*
* @static
* @param {DomElement} element
* @returns {DomRect} - Returns a rectangle that specifies the location in global space.
@ -1630,7 +1630,7 @@ export class Card {
}
/**
* Adjusts all links
* Adjusts all links
*
* @static
* @param {*} htmlString
@ -1693,7 +1693,7 @@ export class Card {
if (event.target) {
//let column = event.target.closest(".column")
let indexbox = this.closestWithClass(card, 'mainview')
if (indexbox != null) { // column != null ||
if (indexbox != null) { // column != null ||
let links = Array.from(indexbox.getElementsByTagName("a"))
let globalClick = (event.center) ? event.center : { x: event.x, y: event.y }
let localClick = Points.fromPageToNode(indexbox, globalClick)
@ -1842,9 +1842,9 @@ export class Card {
/**
* This getter and setter pair solves the problem,
* when we assign 'relativePath' to a child class, the assign is routed
* to this base class. And we ensure, that we always set the same
* private variable.
*
* to this base class. And we ensure, that we always set the same
* private variable.
*
* Not doing this lead to some cases, when Card-/ScatterCard.func
* was called and depending on context their were different values
* inside the relativePath variable.
@ -1883,680 +1883,3 @@ Card.animation = {
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