2019-07-16 09:21:28 +02:00
/** To avoid problems with relative URL paths, we use inline data URI to load svg icons. */
const closeIconDataURI = ` data:image/svg+xml;utf8,
< ? xml version = "1.0" encoding = "utf-8" ? >
< ! DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 100 100" preserveAspectRatio = "xMidYMid meet" >
< line x1 = "20" y1 = "20" x2 = "80" y2 = "80" stroke = "black" stroke - width = "8" / >
< line x1 = "80" y1 = "20" x2 = "20" y2 = "80" stroke = "black" stroke - width = "8" / >
< / s v g >
`
const resizeIconDataURI = ` data:image/svg+xml;utf8,
< ? xml version = "1.0" encoding = "utf-8" ? >
< ! DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"
"http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd" >
< svg xmlns = "http://www.w3.org/2000/svg" viewBox = "0 0 100 100" preserveAspectRatio = "xMidYMid meet" >
< line x1 = "20" y1 = "80" x2 = "80" y2 = "20" stroke = "lightgray" stroke - width = "8" / >
< line x1 = "40" y1 = "80" x2 = "80" y2 = "40" stroke = "lightgray" stroke - width = "8" / >
< / s v g >
`
const enableNearestNeighborTaps = false
/ * *
* A class that collects static methods to maintain the states and parts of
* EyeVisit like cards .
*
* The class is used as a namespace and should never called with new .
* /
2019-07-16 10:04:25 +02:00
export class Card {
2019-07-16 09:21:28 +02:00
static setup ( context , modules = [ ] ) {
console . log ( "Setup Card..." , modules )
context . modules = [ ]
modules . forEach ( module => {
if ( module . apply ( context ) )
context . modules . push ( module . constructor . name )
} )
}
/ * *
*
*
* @ static
* @ param { * } event
* @ memberof Card
* /
static close ( event ) {
let context = this . getContext ( event . target )
if ( context ) {
if ( context . onClose ) {
context . onClose ( event )
} else context . parentNode . removeChild ( context )
} else console . error ( "Could not find context!" , event . target )
}
/ * *
* Replaces a set of attributes using a provided function .
*
* @ static
* @ param { * } html
* @ param { * } attribute
* @ param { * } replaceFunc
* @ memberof Card
* /
static _replaceAttributes ( html , attribute , replaceFunc ) {
let clickables = html . querySelectorAll ( ` [ ${ attribute } ] ` )
clickables . forEach ( element => {
let attributeVal = element . getAttribute ( attribute )
element . removeAttribute ( attribute )
replaceFunc . call ( this , element , attributeVal )
} )
}
/ * *
* Replaces the onClick callback of the element with an
* Interaction mapper event .
*
* @ static
* @ param { * } element
* @ param { * } attributeVal
* @ returns
* @ memberof Card
* /
static _replaceCallback ( element , attributeVal ) {
if ( element . tagName == "A" ) {
element . addEventListener ( "click" , event => { event . preventDefault ( ) } )
}
let callbackParts = attributeVal . split ( "(" )
let funcPart = callbackParts [ 0 ] . trim ( )
let trimmedArgs = callbackParts [ 1 ] . trim ( )
//Remove the closing ')'
trimmedArgs = trimmedArgs . substring ( 0 , trimmedArgs . length - 1 )
let callParts = funcPart . split ( "." )
let argsStrings = trimmedArgs . split ( "," ) . filter ( entry => { return entry . trim ( ) != "" } )
let callStack = window
do {
callStack = callStack [ callParts . shift ( ) . trim ( ) ]
} while ( callParts . length > 0 )
let that = this
let interactionType = this . interactionType
//Remove the events on the circle.
// These are 'hardcoded' inside the convert.js.
if ( element . tagName == "circle" ) return false
InteractionMapper . on ( interactionType , element , ( event ) => {
/ * *
* Replaces the strings from the listener with the cooresponding variables .
* /
let args = [ ]
argsStrings . forEach ( arg => {
arg = arg . trim ( )
if ( arg == "this" )
args . push ( event . target )
else if ( arg == "event" )
args . push ( event )
else {
const firstCharacter = arg [ 0 ]
if ( firstCharacter == "\"" || firstCharacter == "'" ) {
arg = arg . substring ( 1 )
}
const lastCharacter = arg [ arg . length - 1 ]
if ( lastCharacter == "\"" || lastCharacter == "'" ) {
arg = arg . substring ( 0 , arg . length - 1 )
}
args . push ( arg )
}
} )
event . stopPropagation ( )
if ( callStack )
callStack . call ( that , ... args )
else {
console . error ( "Could not call callback function " + attributeVal , ... args )
}
} )
}
/ * *
* 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
* @ memberof 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 .
* /
return html . replace ( /<\s*(a|video|img|image|circle)\s(.*?)(xlink:href|href|src)\s*=\s*["'](\..*?)["']\s*(.*?)>/g , function ( data ) {
let path = that . _getRelativePath ( arguments [ 4 ] )
const tag = ` < ${ arguments [ 1 ] } ${ arguments [ 2 ] } ${ arguments [ 3 ] } =" ${ path } " ${ arguments [ 5 ] } > `
/* if (that.debug) */ console . log ( "Adjusted: " , tag )
return tag
} )
}
/ * *
* Concats the given path with the relative path specified in the Card ( as static variable ) .
* /
static _getRelativePath ( src ) {
let path = ( this . relativePath != "" ) ? this . relativePath + "/" + src : src
return path
}
/ * *
* Loads the card text using an ajax request .
*
* @ static
* @ param { * } path
* @ returns
* @ memberof Card
* /
static loadHTML ( path ) {
return new Promise ( ( resolve , reject ) => {
let request = new XMLHttpRequest ( )
request . onreadystatechange = function ( ) {
if ( this . readyState == 4 ) {
if ( this . status == 200 || Card . _isLocal ( ) ) {
try {
resolve ( this . responseText )
} catch ( e ) {
reject ( e )
}
} else reject ( ` Request failed ' ${ path } '. Returned status ${ this . status } and ready state ${ this . readyState } . ` )
}
}
request . open ( "GET" , path , true )
request . send ( )
} )
}
/ * *
* TODO : Maybe put this in a utility script .
* /
static _isLocal ( ) {
return ( window . location . protocol == "file:" )
}
/ * *
* Returns true if the card belongs to an editable page / context .
*
* @ static
* @ returns
* @ memberof Card
* /
static isEditable ( ) {
return typeof mainController != 'undefined' && mainController . mode
}
/ * *
* Finds the closest element with the given css class name along the parent chain .
*
* @ static
* @ param { any } node - The node to start on
* @ param { any } klass - The css class name
* @ returns { element } - The DOM element with the given class name
* @ memberof Card
* /
static closestWithClass ( node , klass ) {
if ( node && node . classList ) {
if ( node . classList . contains ( klass ) )
return node
return this . closestWithClass ( node . parentNode , klass )
}
return null
}
/ * *
* Returns a computed style property . Similar to the jQuery . css method .
*
* @ param { any } node
* @ param { any } attr
* @ returns { any }
* @ memberof Card
* /
static css ( node , attr ) {
return window . getComputedStyle ( node , null ) . getPropertyValue ( attr )
}
/ * *
* Defines the standard drag behavior of cards . Copies the outer HTML of
* the card to the dataTransfer object of the drag event .
*
* Since dragover does not have the right to see the content we must specify
* additional type information to ensure that only accepted content
* can be dropped . Use the optional type parameter to specify custom types .
*
* The optional effectAllowed is set to 'all' . This is necessary to ensure
* that elements can be copied and moved . Unfortunately this
* adds a plus marker to the mouse that might be misleading . Use
* DataTransfer . dropEffect in dragover events to provide correct feedback instead .
*
* @ static
* @ param { any } event
* @ param { string } [ type = "card" ]
* @ param { string } [ effectAllowed = "all" ]
* @ memberof Card
* /
static dragStart ( event , type = "card" , effectAllowed = 'all' ) {
event . dataTransfer . effectAllowed = effectAllowed
let html = event . target . outerHTML
event . dataTransfer . setData ( "text/html" , html )
// https://stackoverflow.com/questions/11065803/determine-what-is-being-dragged-from-dragenter-dragover-events
event . dataTransfer . setData ( 'iwmbrowser/' + type , '' )
}
/ * *
* Opens a popup by calling the Popup . open method .
*
* @ static
* @ param { DOM element } parent - The DOM node to add the popup
* @ param { any } position - The x , y position of the popup
* @ param { any } content - The content to show within the popup
* @ param { any } callback - A callback that is called on closing the popup
* @ memberof Card
* /
static _openPopup ( context , src , position , content , options = { } ) {
let maxWidth = null
if ( this . debug ) console . log ( "Card._openPopup" , position )
//logging
if ( src ) {
let strparts = src . split ( "/" )
let cardID = strparts [ strparts . length - 2 ]
let cardName = strparts [ strparts . length - 1 ]
//console.log('open popup:',cardID,cardName,context,content)
let msg = 'ShowPopup: ' + cardID + '/' + cardName
console . log ( 'Logging:' , msg )
Logging . log ( msg )
}
let highlight = null
if ( options . highlight ) {
highlight = options . highlight
delete options . highlight
}
this . _createPopup ( context , position , content , options ) . then ( ( popup ) => {
if (
//Test if meanwhile another popup was registered...
this . _getPopup ( context ) ||
// Or if an highlight was loaded, if the highlight already was closed.
highlight !== null && ! this . _isHighlightActive ( context , highlight )
) {
//.. if so remove the create popup instantly.
popup . remove ( )
} else {
// Otherwise set the popup regularly.
let popupParagraphs = popup . element . querySelectorAll ( ".popupContent > *" )
// Remove a design error of naming two adjacent elements popup.
// Todo: fix this properly and remove this code.
// let unnecessaryPopupElement = popup.element.querySelector(".popupContent > .popup")
// unnecessaryPopupElement.classList.remove("popup")
popupParagraphs . forEach ( popupParagraph => {
popupParagraph . setAttribute ( "draggable" , false )
popupParagraph . addEventListener ( "mousedown" , ( event ) => {
event . preventDefault ( )
} )
} )
this . _setPopup ( context , popup , src )
}
} ) . catch ( e => console . error ( e ) )
}
/ * *
* Closes a provided popup and unsets it on the context .
*
* @ static
* @ param { * } context
* @ param { * } popup
* @ memberof Card
* /
static closePopup ( context , popup ) {
if ( popup ) {
if ( this . debug ) console . log ( "Close Popup." , context , popup )
window . popup = popup
popup . close ( )
this . _unsetPopup ( context )
} else {
console . error ( "Requested to close popup, but popup was not found." )
}
}
/ * *
* Creates a new Popup instance .
*
* @ static
* @ param { DomElement } context - The element on which the Popup should be created .
* @ 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 .
* @ memberof Card
* /
static _createPopup ( context , position , content , options = { } ) {
if ( this . debug ) console . log ( "Create Popup." , context , position , content , options )
let popup = new Popup ( Object . assign ( {
parent : context ,
content
} , Object . assign ( {
noStyle : true ,
// TODO: Remove offset when positioning according to element position
// is working.
posOffset : 10
} , options ) ) )
// 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"
let promise = new Promise ( ( resolve ) => {
if ( popup . loaded )
resolve ( popup )
else {
popup . onload = ( ) => {
resolve ( popup )
}
}
} )
promise . then ( ( popup ) => {
popup . element . style . display = "block"
popup . element . style . visibility = "hidden"
popup . element . style . opacity = 0
popup . placeAt ( position )
InteractionMapper . on ( this . interactionType , popup . element , e => {
this . _cleanup ( context )
} )
Object . assign ( popup . element . style , {
zIndex : this . zIndices . popup
} )
TweenMax . to ( popup . element , this . animation . popup , {
autoAlpha : 1 ,
ease : Power2 . easeIn
} )
} )
return promise
}
static _overlayCleanup ( context , overlay ) {
/ * *
* 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 ) {
TweenMax . to ( overlay , 0.2 , {
autoAlpha : 0 , onComplete : ( ) => {
popup . remove ( )
//this._cleanup(context)
//overlay.parentNode.removeChild(overlay)
}
} )
}
}
/ * *
* Opens a popup for a glossary . Typically used as a onlick handler of a link .
*
* @ static
* @ param { any } event - The trigger event , typically a click event
* @ returns { bool } false - Returns false to prevent default click action
* @ memberof Card
* /
static loadPopup ( event , context = null , node = null , local = null ) {
let editable = Card . isEditable ( )
if ( context == null ) {
context = this . getContext ( event . target )
}
if ( node == null ) {
node = event . target
}
if ( local == null ) {
let globalClick = ( event . center ) ? event . center : { x : event . x , y : event . y }
local = Points . fromPageToNode ( context , globalClick )
}
if ( this . debug ) console . log ( "loadPopup" , event )
// Prevents loading the link in the current tab.
// Prevents loading the link in the current tab.
if ( event . type != "Follow" )
event . preventDefault ( )
if ( editable && event . type == 'click' ) {
return false
}
let overlay = document . createElement ( 'div' )
let src = node . getAttribute ( "href" )
let parentArticle = node . closest ( 'article' )
const that = this
let xhr = new XMLHttpRequest ( )
xhr . open ( 'get' , src , true )
xhr . onreadystatechange = ( ) => {
if ( this . debug ) console . log ( "Popup Source: " , src )
if ( xhr . readyState == 4 && ( xhr . status == 200 || xhr . status == 0 ) ) {
if ( editable ) {
if ( this . debug ) console . log ( "Append overlay." , context )
overlay . classList . add ( 'overlay' )
TweenMax . set ( overlay , { autoAlpha : 0 } )
context . appendChild ( overlay )
TweenMax . to ( overlay , 0.5 , { autoAlpha : 0.25 } )
}
// Extract the body from the Popup site.
let parser = new DOMParser ( )
let popupPage = parser . parseFromString ( xhr . responseText , "text/html" )
//Fix the relative path of loaded images in the popup.
popupPage . querySelectorAll ( "img" ) . forEach ( node => {
node . setAttribute ( "src" , that . _getRelativePath ( node . getAttribute ( "src" ) ) )
} )
let html = popupPage . body . innerHTML
/ * *
* 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 ) .
*
* Therefore the selector was changed :
* '.popup' => '.popupHtml'
*
* Which describes the construct even better than the
* '.popup' class .
* /
let selector = Card . popupHtmlSelector
let content = { html , selector }
let isSame = Card . _checkForActiveSource ( context , src )
Card . _cleanup ( context )
if ( ! isSame ) {
Card . _activateCorrespondingHighlights ( context , node , parentArticle )
let callback = ( popup , callback ) => {
if ( this . debug ) console . log ( "Close popup (Editable = " + editable + ")." , popup )
if ( editable ) {
let isDirty = mainController . askSaveNode ( )
if ( isDirty )
mainController . saveNode ( html . innerHTML , url => {
if ( callback ) {
callback ( )
}
saved ( url )
this . _overlayCleanup ( context , overlay )
} )
else {
if ( callback ) {
callback ( )
}
this . _overlayCleanup ( context , overlay )
}
mainController . popController ( )
}
/** This may be in conflice with the cleanup method. */
else {
//this._overlayCleanup(context, overlay)
popup . remove ( )
}
}
Card . _openPopup ( context , src , local , content , {
closeCommand : callback
} )
}
}
overlay . onclick = e => {
if ( editable )
e . preventDefault ( )
}
//console.log("onreadystatechange", editable)
if ( editable ) {
if ( this . debug ) console . log ( "pushController" , src , popup . insertedNode )
mainController . pushController ( popup . insertedNode , src )
}
}
xhr . send ( )
return false
}
/ * *
* When an highlight link is clicked , this method activates all
* corresponding highlights .
*
* @ static
* @ param { DomElement } context - The context of the element .
* @ param { DomElement } node - The node that ( may ) contain a highlightId .
* @ param { DomElement } parent - The parent element that may contain more highlightIds .
* @ memberof Card
* /
static _activateCorrespondingHighlights ( context , node , parent ) {
let highlightId = node . getAttribute ( 'data-highlight-id' )
// console.log("Request Highlight: " + highlightId)
let correspondingHighlights = [ ]
if ( highlightId ) {
let queryString = 'circle[data-highlight-id="' + highlightId + '"]'
if ( parent ) {
correspondingHighlights = parent . querySelectorAll ( queryString )
}
if ( correspondingHighlights . length > 0 ) {
for ( let highlight of correspondingHighlights ) {
if ( highlight . parentNode && highlight . parentNode . nodeName . toLowerCase ( ) == 'g' ) {
Highlight . openHighlight ( highlight , { animation : Card . highlightAnimation } )
this . _addHighlight ( context , highlight )
}
}
}
}
}
/ * *
* Tests if any open item already contains the requested Source .
*
* @ static
* @ param { DomElement } context - Dom context we are in .
* @ param { string } src - Source as dataUrl .
* @ returns { boolean } - True if source is already active , false otherwise .
* @ memberof Card
* /
static _checkForActiveSource ( context , src ) {
let requestedSame = false
let activePopup = Card . _getPopup ( context )
let activeHighlights = Card . _getHighlights ( context )
if ( activePopup ) {
let activeSrc = Card . _getPopupSource ( activePopup )
if ( activeSrc && src == activeSrc ) {
requestedSame = true
}
}
if ( ! requestedSame ) {
for ( let highlight of activeHighlights ) {
let activeSrc = Card . _getHighlightSource ( highlight )
if ( activeSrc == src ) {
requestedSame = true
break
}
}
}
return requestedSame
}
/ * *
* Opens a popup for a highlight . Typically used as a onlick handler of a link .
*
* @ static
* @ param { any } event - The trigger event , typically a click event
* @ returns { bool } false - Returns false to prevent default click action
* @ memberof Card
* /
static loadHighlightPopup ( event ) {
if ( this . debug ) console . log ( "Load Highlight Popup: " , event )
let node
if ( event . firstTarget ) {
node = event . firstTarget
} else {
node = event . target
}
let context = this . getContext ( node )
event . stopPropagation ( )
/ * *
* 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" )
let isSame = this . _checkForActiveSource ( context , src )
this . _cleanup ( context )
if ( ! isSame ) {
this . _addHighlight ( context , node )
Highlight . openHighlight ( node , {
animation : Card . highlightAnimation ,
onExpanded : ( ) => {
// We assume it's always a circle. This may break, when other svg shapes are used.
let x = node . getAttribute ( "cx" )
let y = node . getAttribute ( "cy" )
let position = { x , y }
let radius = parseFloat ( node . getAttribute ( "r" ) )
/ *
As the popup is appended directly to the card . We have to
transform the location from the subcard - / s v g - s p a c e t o t h e
card space .
* /
let svgRoot = node . closest ( "svg" )
let svgPoint = svgRoot . createSVGPoint ( )
svgPoint . x = position . x
svgPoint . y = position . y - radius
let matrix = node . getCTM ( )
let point = svgPoint . matrixTransform ( matrix )
let global = Points . fromNodeToPage ( node . closest ( "div" ) , point )
let local = Points . fromPageToNode ( context , global )
let overlay = document . createElement ( 'div' )
//TODO: Check if this is necessary for the editor.
// overlay.onclick = e =>{}
// This is not really efficient, as
// we could load the data while the circle is animating.
// but for simplicity it's just done here for now.
// TODO: Adjust to load while animating (Problem: Unload when cancelled).
console . log ( src )
this . _loadPopupContent ( src )
. then ( content => {
this . _openPopup ( context , src , local , content , {
highlight : node ,
closeCommand : ( popup , callback ) => {
this . _overlayCleanup ( context , overlay )
popup . remove ( )
}
} )
} )
. catch ( err => { console . error ( err ) } )
}
} )
}
}
/ * *
* Loads the popup from a provided source .
*
* @ static
* @ private
* @ param { string } source - Url to a popup file .
* @ returns { Promise } - Returns a promise , that ' s resolved when the data is loaded .
* @ memberof Card
* /
static _loadPopupContent ( source ) {
return new Promise ( ( resolve , reject ) => {
let xhr = new XMLHttpRequest ( )
xhr . open ( 'get' , source , true )
xhr . onreadystatechange = ( ) => {
if ( xhr . readyState == 4 ) {
if ( ( xhr . status == 200 || xhr . status == 0 ) ) {
let html = this . postProcessResponseText ( xhr . responseText )
let selector = Card . popupHtmlSelector
let content = { html : html . body . innerHTML , selector }
resolve ( content )
} else {
reject ( ` Popup request failed (Code: ${ xhr . status } ): Could not load resource: ${ src } ` )
}
}
}
xhr . send ( )
} )
}
static openZoomable ( event ) {
let node = event . target
//console.log("Open zoomable: ", node, node.classList)
let editable = Card . isEditable ( )
if ( editable ) {
if ( node . classList . contains ( 'zoomable-icon' ) )
this . _openZoomable ( event )
return
}
this . _openZoomable ( event )
}
/ * *
* Retrieve a Rectangle object from the a given zoomable .
*
* @ static
* @ param { DomElement } zoomable - The zoomable , the rectangle should be calculated for .
* @ param { DomElement } wrapper - Specifies the coordinate system of the generated rectangle .
* @ returns { object } - Returns an object of the rectangle in the wrapper ' s coordinate system in the form of { x , y , width , height } .
* @ memberof 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
done using the bounding box of the desired element .
- SO
* /
//The div is cloned and animated, therefore we need it's style!
let actuallyZoomedItem = zoomable . querySelector ( "div" )
let zoomableStyle = window . getComputedStyle ( actuallyZoomedItem )
let svgElement = zoomable . querySelector ( "svg" )
let videoElement = zoomable . querySelector ( "video" )
let curStyle = null
if ( svgElement ) {
curStyle = window . getComputedStyle ( svgElement )
}
if ( videoElement ) {
curStyle = window . getComputedStyle ( videoElement )
}
let imageWidth = parseFloat ( curStyle . width )
let imageHeight = parseFloat ( curStyle . height )
let globalBottomRight = {
x : imageWidth ,
y : imageHeight
}
globalBottomRight = Points . fromNodeToPage ( zoomable , globalBottomRight )
let globalFigurePos = Points . fromNodeToPage ( zoomable , { x : 0 , y : 0 } )
let localFigurePos = Points . fromPageToNode ( wrapper , globalFigurePos )
let relativeBottomRight = Points . fromPageToNode ( zoomable , globalBottomRight )
let width = relativeBottomRight . x + parseFloat ( zoomableStyle . borderLeftWidth ) + parseFloat ( zoomableStyle . borderRightWidth ) /*+ zoomIconRight*/
let height = relativeBottomRight . y + parseFloat ( zoomableStyle . borderTopWidth ) + parseFloat ( zoomableStyle . borderBottomWidth ) /*+ zoomIconRight*/
return { x : localFigurePos . x , y : localFigurePos . y , width , height }
}
/ * *
* Opens a zoomable object , which can be a figure containing an image or video or an image group
*
* @ static
* @ param { any } event - The trigger event , typically a click event
* @ memberof Card
* /
static _openZoomable ( event , editable = false ) {
event . stopPropagation ( )
let node = event . target
let wrapper = this . getContext ( node )
let zoomable = node . closest ( 'figure' )
// load mainimg - if none exists, there is nothing to open
let img = zoomable . querySelector ( '.mainimg' )
if ( img == null ) {
return
}
//close other open elements before opening the zoomable:
//let removeDelay = this.removeActiveZoomable(wrapper, true) ? 0 : 0
this . _cleanup ( wrapper )
// Clone the zoomable and create a clone that is zoomed instead.
let current = this . zoomableCurrentGeometry ( zoomable , wrapper )
let zoomedFig = zoomable . cloneNode ( true )
let caption = zoomedFig . querySelector ( "figcaption.cap" )
caption . parentNode . removeChild ( caption )
this . _toggleNextIcon ( zoomedFig )
this . _setZoomable ( wrapper , zoomable , zoomedFig )
if ( ! editable ) {
/ * *
* I don ' t remember why we need yet another wrapper for the zoomable .
* This should be investigated .
* - SO
* /
let zoomContainer = document . createElement ( 'div' )
zoomContainer . classList . add ( "zoomable-wrapper" )
Object . assign ( zoomContainer . style , {
position : "absolute" ,
top : 0 ,
left : 0 ,
zIndex : 200
} )
wrapper . appendChild ( zoomContainer )
//Note: Remaned ZoomedFigure to zoomed-figure
zoomedFig . className = 'zoomed-figure'
zoomedFig . style . zIndex = this . zIndices . zoomedFigure
let zoomedG = zoomedFig . querySelector ( 'g' )
if ( zoomedG && ! editable )
zoomedG . parentNode . removeChild ( zoomedG )
zoomContainer . appendChild ( zoomedFig )
let zoomedImg = zoomedFig . querySelector ( '.mainimg' )
zoomedImg . removeAttribute ( 'onclick' )
//set the values of corresponding zoomable element (needed when the zoomable is closed indirectly via removeActiveZoomable)
zoomedFig . zoomable = zoomable
zoomedFig . zoomableGeometry = current
//play video
let videoElement = zoomedFig . querySelector ( "video" )
if ( videoElement ) {
videoElement . play ( )
}
//logging
let imgElt = zoomedFig . querySelector ( "image" )
let videoElt = zoomedFig . querySelector ( "video" )
//console.log('open zoomable:',imgElt,videoElt)
if ( imgElt ) {
let imgSrc = imgElt . getAttribute ( 'xlink:href' )
let strparts = imgSrc . split ( "/" )
let cardID = strparts [ strparts . length - 2 ]
let cardName = strparts [ strparts . length - 1 ]
let msg = 'ShowImage: ' + cardID + '/' + cardName
console . log ( 'Logging:' , msg )
Logging . log ( msg )
}
if ( videoElt ) {
let videoSrc = videoElt . getAttribute ( 'src' )
let strparts = videoSrc . split ( "/" )
let cardID = strparts [ strparts . length - 2 ]
let cardName = strparts [ strparts . length - 1 ]
let msg = 'ShowVideo: ' + cardID + '/' + cardName
console . log ( 'Logging:' , msg )
Logging . log ( msg )
}
// (At least in this style) We don't want the user to 'hit' the icon
// It's just an indicator that an action is possible. The click must be
// captured from the whole subcard.
InteractionMapper . on ( this . interactionType , zoomedFig , ( event ) => {
this . _cleanup ( wrapper )
} )
let zoomedFigStyle = window . getComputedStyle ( zoomedFig )
let borderX = parseFloat ( zoomedFigStyle . borderLeftWidth ) + parseFloat ( zoomedFigStyle . borderRightWidth )
let borderY = parseFloat ( zoomedFigStyle . borderBottomWidth ) + parseFloat ( zoomedFigStyle . borderTopWidth )
const scaleFactor = 2
const transformOrigin = 'bottom right'
TweenMax . set ( zoomedFig , {
x : current . x ,
y : current . y ,
width : current . width + borderX ,
height : current . height + borderY ,
transformOrigin
} )
TweenMax . set ( zoomable , { opacity : 0 } )
let icon = zoomedFig . querySelector ( ".icon" )
TweenMax . set ( icon , {
transformOrigin
} )
zoomedFig . style . transformOrigin = "calc(100% - " + parseFloat ( zoomedFigStyle . borderRightWidth ) + "px) calc(100% - " + parseFloat ( zoomedFigStyle . borderBottomWidth ) + "px)"
let tl = new TimelineLite ( )
let zoomCaption = zoomedFig . querySelector ( ".zoomcap" )
tl . to ( zoomedFig , Card . animation . zoomable , {
ease : Power2 . easeIn ,
css : {
scaleX : scaleFactor ,
scaleY : scaleFactor
}
} , 0 ) . set ( zoomCaption , {
css : {
display : "block" ,
opacity : 0 ,
x : - parseFloat ( zoomedFigStyle . borderLeftWidth ) ,
width : current . width + borderX
}
} ) . to ( zoomCaption , this . animation . fade , {
autoAlpha : 1
} )
} else this . _openZoomableEditorBehaviour ( wrapper , img , zoomable , zoomedFig , current )
}
/ * *
* Selects and transforms the zoomicon from a zoomicon to a closeicon
* or the other way around .
*
* @ static
* @ param { DomElement } parent - Parent to be searched for an zoomicon .
* @ memberof Card
* /
static _toggleNextIcon ( parent ) {
let zoomIcon = Card . _findNextIcon ( parent )
const closeClass = "close"
const zoomClass = "zoom"
if ( zoomIcon . classList . contains ( closeClass ) ) {
zoomIcon . classList . remove ( closeClass )
zoomIcon . classList . add ( zoomClass )
} else if ( zoomIcon . classList . contains ( zoomClass ) ) {
zoomIcon . classList . remove ( zoomClass )
zoomIcon . classList . add ( closeClass )
} else {
console . error ( ` Error Toggleing Zoomicon: It did neither contain a class named ${ closeClass } or ${ zoomClass } . ` )
}
}
static _findNextIcon ( parent ) {
return parent . querySelector ( '.icon' )
}
static _openZoomableEditorBehaviour ( wrapper , img , zoomable , zoomedFig , current ) {
let zoomContainer = document . createElement ( 'div' )
let zoomIcon = zoomable . querySelector ( '.zoom-icon' )
zoomContainer . style . position = 'relative'
zoomContainer . style . zIndex = this . zIndices . zoomable
wrapper . appendChild ( zoomContainer )
let zoomParent = zoomable . parentNode
zoomParent . appendChild ( zoomedFig )
zoomedFig . style . opacity = 0.5
zoomContainer . appendChild ( zoomable )
TweenMax . set ( zoomable , { x : current . x , y : current . y , width : current . width , height : current . height } )
let editor = mainController . topController ( ) . ensureEditor ( img )
let savedDisplay = zoomIcon . style . display
let iconClone = zoomIcon . cloneNode ( true )
zoomIcon . style . display = 'none'
let savedOnClick = zoomIcon . getAttribute ( 'onclick' )
zoomIcon . removeAttribute ( 'onclick' )
iconClone . src = iconClone . src . replace ( 'info.svg' , 'close.svg' )
iconClone . style . zIndex = 1000
iconClone . classList . add ( 'zoomed-icon' )
iconClone . onclick = event => {
zoomIcon . style . display = savedDisplay
iconClone . remove ( )
zoomIcon . setAttribute ( 'onclick' , savedOnClick )
zoomedFig . remove ( )
zoomContainer . remove ( )
zoomParent . appendChild ( zoomable )
TweenMax . set ( zoomable , { x : 0 , y : 0 } )
zoomable . onmousedown = null
zoomable . onmousemove = null
zoomable . onmouseup = null
zoomable . onmousewheel = null
}
wrapper . appendChild ( iconClone )
TweenMax . set ( iconClone , { x : current . iconPos . x , y : current . iconPos . y } )
zoomable . onmousedown = event => {
if ( this . debug ) console . log ( 'mousedown' , event . target )
event . preventDefault ( )
zoomable . dragging = true
zoomable . dragStartPos = { x : event . pageX , y : event . pageY }
}
zoomable . onmousemove = event => {
if ( this . debug ) console . log ( 'onmousemove' , event . target )
if ( zoomable . dragging ) {
event . preventDefault ( )
let dx = event . pageX - zoomable . dragStartPos . x
let dy = event . pageY - zoomable . dragStartPos . y
TweenMax . set ( [ zoomable , iconClone ] , { x : '+=' + dx , y : '+=' + dy } )
zoomable . dragStartPos = { x : event . pageX , y : event . pageY }
if ( editor ) {
editor . showControls ( )
}
}
}
zoomable . onmouseup = event => {
zoomable . dragging = false
}
let startZoom = 1
zoomable . onmousewheel = event => {
event . preventDefault ( )
let direction = event . detail < 0 || event . wheelDelta > 0
const zoomFactor = 1.1
let zoom = direction ? zoomFactor : 1 / zoomFactor
startZoom *= zoom
TweenMax . set ( zoomable , { scale : startZoom } )
if ( editor ) {
editor . showControls ( )
}
}
return
}
/ * *
* Closes a zoomable object with animation
*
* @ static
* @ param { any } wrapper - the wrapper containing the index card
* @ param { any } div - the figure containing the relevant elements
* @ param { any } zoomable - the zoomable element , from which the zoomed figure originates
* @ param { any } rect - the target rect for the tween ( typically the top left width height of the zoomable )
* @ memberof Card
* /
static closeZoomable ( context , zoomable , zoomedFig ) {
if ( this . debug ) console . log ( "Close Zoomable" , context , zoomable , zoomedFig )
if ( zoomable ) {
this . _unsetZoomable ( context )
let caption = zoomable . querySelector ( "figcaption.cap" )
zoomable . removeChild ( caption )
zoomable . appendChild ( caption )
let zoomedCaption = zoomedFig . querySelector ( "figcaption.zoomcap" )
TweenMax . to ( zoomedCaption , this . animation . fade , {
autoAlpha : 0 ,
} )
TweenMax . to ( zoomedFig , this . animation . zoomable , {
css : {
scaleX : 1 ,
scaleY : 1
} ,
onComplete : ( ) => {
TweenMax . set ( zoomable , {
opacity : 1
} )
let div = zoomedFig . parentNode
let videoElement = div . querySelector ( 'video' )
if ( videoElement )
videoElement . pause ( )
div . parentNode . removeChild ( div )
}
} )
}
}
/ * *
* Starts and controls the expansion animation of an index card .
* The assumed card structure is as follows :
* < div class = "card" >
* < div class = "card-content content" >
* < h1 class = "has-text-centered" contenteditable = "true" > ... < / h 1 >
* < div class = "wrapper" >
* < div data - innerhtml = "preview" class = "preview" > ... < / d i v >
* < img class = "zoom-icon cardicon" onclick = "Card.openIndexCard(event, './article3.html')" src = "../_theme/icons/info.svg" >
* < / d i v >
* < / d i v >
* < / d i v >
* The basic idea is that the card is cloned . In the animation the clone si expanded while the original is faded out .
*
* @ param { * } card - The card to expand
* @ param { string } html - The original HTML of the card
* @ param { * } tagName - The tagname of the element that is used as exanded element
* @ param { * } src - The src of the expanded element
* @ param { * } callback - A callback that is called when the expanded element is closed
* /
static expandIndexCard ( card , html , tagName = 'article' , src = null , callback = null ) {
let editable = Card . isEditable ( )
let context = this . getContext ( card )
let indexbox = this . closestWithClass ( card , 'mainview' )
this . _cleanup ( context )
let angle = 0 // UO: out of context, to be computed from the scatter
let clone = card . cloneNode ( true )
/ * *
* 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" )
const article = html . body . querySelector ( "article" )
let subcardContent = clone . querySelector ( ".subcard-content" )
subcardContent . appendChild ( article )
cloneWrapper . parentNode . removeChild ( cloneWrapper )
/* Removes the 'default' cleanup on the card */
clone . removeAttribute ( "onclick" )
InteractionMapper . on ( this . interactionType , clone , ( event ) => {
this . _cleanup ( context )
} )
let articleClone = clone . querySelector ( tagName )
let globalPreviewRect = Card . _getGlobalRect ( card )
let globalIndexCardRect = Card . _getGlobalRect ( indexbox )
let localOrigin = Points . fromPageToNode ( indexbox , Rect . getPosition ( globalPreviewRect ) )
let scaleX = globalPreviewRect . width / globalIndexCardRect . width
let scaleY = globalPreviewRect . height / globalIndexCardRect . height
let padding = parseInt ( this . css ( indexbox , 'padding' ) )
let maxWidth = this . css ( card , 'max-width' )
TweenMax . set ( clone , {
css : {
position : 'absolute' ,
width : globalIndexCardRect . width ,
height : globalIndexCardRect . height ,
maxWidth : '100%' ,
margin : 0 ,
zIndex : this . zIndices . article
}
} )
TweenMax . set ( articleClone , {
autoAlpha : 0
} )
TweenMax . set ( card , { css : { maxWidth : '100%' } } )
TweenMax . set ( clone , {
x : localOrigin . x - padding ,
y : localOrigin . y - padding ,
scaleX ,
scaleY ,
transformOrigin : '0% 0%' ,
rotation : angle ,
} )
indexbox . prepend ( clone )
let titlebar = clone . querySelector ( ".titlebar" )
let title = titlebar . querySelector ( "h2" )
let titlebarStyle = window . getComputedStyle ( titlebar )
let start = {
height : parseInt ( titlebarStyle . height )
}
if ( this . dynamicHeight ) {
let targetHeight = subcardContent . offsetHeight ;
console . log ( targetHeight )
subcardContent . classList . add ( "dynamic-height" )
/ * *
* Scale the content from 100 % to it ' s target size .
* /
// TweenMax.set(subcardContent, {
// height: "100%"
// })
// TweenMax.to(subcardContent, Card.animation.articleTransition, {
// height: targetHeight + "px"
// })
}
//jquery hyphenate below
2019-07-16 10:04:25 +02:00
if ( typeof ( $ ) != 'undefined' ) {
$ ( '.column' ) . not ( '.overview' ) . children ( 'p' ) . hyphenate ( 'de' )
}
2019-07-16 09:21:28 +02:00
//logging
if ( src ) {
let strparts = src . split ( "/" )
let cardID = strparts [ strparts . length - 2 ]
let cardName = strparts [ strparts . length - 1 ]
strparts = card . className . split ( " " )
let cardType = strparts [ 1 ]
let msg = 'Card: ' + cardID + ': openTopic: ' + cardType + ', ' + cardName
console . log ( 'Logging:' , msg )
Logging . log ( msg )
}
let desiredBorderBottomWidth = parseInt ( window . getComputedStyle ( titlebar ) . borderBottomWidth )
TweenMax . to ( clone , Card . animation . articleTransition , {
x : - padding ,
y : - padding ,
ease : ExpoScaleEase . config ( scaleX , 1 ) ,
scale : 1 ,
rotation : 0 ,
onComplete : ( ) => {
card . classList . add ( "visited" )
} ,
onUpdateParams : [ '{self}' ] ,
onUpdate : ( self ) => {
let transform = self . target . _gsTransform
TweenMax . set ( title , {
scale : 1 / transform . scaleX
} )
TweenMax . set ( titlebar , {
height : start . height * 1 / transform . scaleY
} )
// Retain the border at same visual thickness.
titlebar . style . borderBottomWidth = desiredBorderBottomWidth / transform . scaleY + "px"
}
} )
TweenMax . to ( [ articleClone ] , this . animation . articleTransition / 2 , {
delay : this . animation . articleTransition / 2 ,
autoAlpha : 1
} )
if ( editable ) {
mainController . pushController ( articleClone , src )
}
Card . _disableCardCloseButton ( context )
const closeAnimation = ( ) => {
//logging
if ( src ) {
let strparts = src . split ( "/" )
let cardID = strparts [ strparts . length - 2 ]
let cardName = strparts [ strparts . length - 1 ]
strparts = card . className . split ( " " )
let cardType = strparts [ 1 ]
let msg = 'Card: ' + cardID + ': closeTopic: ' + cardType + ', ' + cardName
console . log ( 'Logging:' , msg )
Logging . log ( msg )
}
Card . _cleanup ( context )
Card . _unsetSubcard ( context )
this . _enableCardCloseButton ( context )
let previewTitlebar = card . querySelector ( ".titlebar" )
let titlebarStyle = window . getComputedStyle ( previewTitlebar )
let titlebar = clone . querySelector ( ".titlebar" )
TweenMax . to ( titlebar , this . animation . articleTransition , {
height : parseInt ( titlebarStyle . height )
} )
TweenMax . to ( articleClone , this . animation . articleTransition / 2 , {
autoAlpha : 0
} )
let title = titlebar . querySelector ( "h2" )
let original = {
height : parseInt ( titlebarStyle . height )
}
if ( this . dynamicHeight ) {
TweenMax . to ( subcardContent , this . animation . articleTransition , {
height : "100%"
} )
}
TweenMax . set ( card , { autoAlpha : 1 , css : { maxWidth } } )
TweenMax . to ( clone , this . animation . articleTransition , {
x : localOrigin . x - padding ,
y : localOrigin . y - padding ,
scaleX ,
scaleY ,
ease : ExpoScaleEase . config ( 1 , scaleX ) ,
rotation : angle ,
onComplete : ( ) => {
// article.remove()
TweenMax . to ( clone , this . animation . fade ,
{
//delay: 0.2,
autoAlpha : 0 ,
onComplete :
( ) => {
if ( editable ) {
mainController . popController ( )
}
clone . remove ( )
}
} )
} ,
onUpdateParams : [ "{self}" ] ,
onUpdate : function ( self ) {
let transform = self . target . _gsTransform
TweenMax . set ( title , {
scale : 1 / transform . scaleX
} )
TweenMax . set ( titlebar , {
height : original . height * 1 / transform . scaleY
} )
// Retain the border at same visual thickness.
titlebar . style . borderBottomWidth = desiredBorderBottomWidth / transform . scaleY + "px"
}
} )
}
//TODO consider renaming it to something more intuitive.
let iconClone = clone . querySelector ( '.card-icon' )
if ( iconClone == null ) {
iconClone = clone . querySelector ( ".cardicon" )
console . warn ( "Legacy selector. Change it to 'card-icon' and find a more suitable name." )
}
if ( iconClone . tagName == "img" ) {
iconClone . src = iconClone . src . replace ( 'info.svg' , 'close.svg' )
}
//console.log("ICON: ", iconClone)
iconClone . classList . remove ( "info" )
iconClone . classList . add ( "close" , "view-button" , "transparent-background" )
// We append the icon clone to the subcard-content.
// Then it's always on the content and not on the background
// (If there is one).
if ( this . dynamicHeight ) {
article . appendChild ( iconClone )
}
if ( enableNearestNeighborTaps ) {
//look for nearby popups on tap
InteractionMapper . on ( "tap" , indexbox , ( ) => {
console . log ( "Tap handler called" , editable )
if ( ! editable ) {
this . findNearbyPopups ( event , card )
}
} )
}
// Use the 'tap' event for closing.
// Otherwise the subcard cannot be closed,
// when another subcard is touched.
InteractionMapper . on ( "tap" , iconClone , ( ) => {
if ( editable ) {
let isDirty = mainController . askSaveNode ( )
if ( isDirty ) {
mainController . saveNode ( html . innerHTML , url => {
callback ( url )
closeAnimation ( )
} )
}
else {
closeAnimation ( )
}
}
else {
closeAnimation ( )
}
} )
}
/ * *
* Opens the index card . Called by the zoom icon click handler .
* The assumed card structure is as follows :
* < div class = "card" >
* < div class = "card-content content" >
* < h1 class = "has-text-centered" contenteditable = "true" > ... < / h 1 >
* < div class = "wrapper" >
* < div data - innerhtml = "preview" class = "preview" > ... < / d i v >
* < img class = "zoom-icon cardicon" onclick = "Card.openIndexCard(event, './article3.html')" src = "../_theme/icons/info.svg" >
* < / d i v >
* < / d i v >
* < / d i v >
* Loads the DOM element from a file source and adds it before the card - content to ensure that the article content
* can be positioned below the heading .
*
* < div class = "card" >
* < article > INSERTED CONTENT < / a r t i c l e >
* < div class = "card-content content" >
* ...
* < / d i v >
* < / d i v >
*
* @ static
* @ param { any } event - The trigger event , typically a click event
* @ param { string } src - The file source , typically a relative url
* @ memberof Card
* /
static openIndexCard ( event , src ) {
//console.log("openIndexCard", src)
/ *
* Called by the expandIndexCard ( ... )
* /
let target = event . target
const saveCallback = ( url ) => {
let handler = ` Card.openIndexCard(event, ' ${ url } ') `
console . log ( "File has changed" , target , handler )
//TODO If this is required, it should be accessing the interaction type.
target . setAttribute ( "onclick" , handler )
}
let context = this . getContext ( target )
let subcard = this . _getSubcard ( context )
//console.log("openIndexCard", { context, subcard })
//Dont proceeed if a subcard is active
if ( subcard != null )
return
// In edit mode we only accept icon clicks
let editable = Card . isEditable ( )
if ( editable && ! target . classList . contains ( 'icon' ) ) {
return
}
let card = this . closestWithClass ( target , 'subcard' )
this . _setSubcard ( context , card )
const relativeSource = this . _getRelativePath ( src )
let xhr = new XMLHttpRequest ( )
xhr . open ( 'get' , relativeSource , true )
xhr . onreadystatechange = ( ) => {
if ( xhr . readyState == 4 && ( xhr . status == 200 || xhr . status == 0 ) ) {
let html = xhr . responseText
let parsedHTML = this . postProcessResponseText ( html )
// TODO: What is this good for?
// let article = parsedHTML.querySelector('article')
// card.insertAdjacentElement('afterbegin', article)
// TweenMax.set(article, { autoAlpha: 0 })
Card . expandIndexCard ( card , parsedHTML , 'article' , relativeSource , saveCallback )
}
}
xhr . onerror = ( ) => {
alert ( 'Unable to load ' + src )
}
xhr . send ( )
}
static _selectCardCloseButton ( context ) {
return context . querySelector ( ".mainview > .button.close" )
}
static _enableCardCloseButton ( context ) {
//console.log("ENABLE")
let btn = this . _selectCardCloseButton ( context )
//console.log(btn)
btn . classList . remove ( "disabled" )
}
static _disableCardCloseButton ( context ) {
let btn = this . _selectCardCloseButton ( context )
btn . classList . add ( "disabled" )
}
/ * *
* Provides the nearest info - card of the provided item .
*
* @ static
* @ param { DomElement } child - A dom element for which the context shall be retrieved .
* @ returns { DomElement } - The containing context / info - card .
* @ memberof Card
* /
static getContext ( child ) {
let dom = child . closest ( ".info-card" )
if ( ! dom ) {
dom = child . querySelector ( ".wrapper" )
console . warn ( "Change the 'wrapper' class to 'info-card' it's more suitable." )
}
return dom // (dom == null) ? document.body : dom
}
/ * *
* The cleanup method closes all active Popups , Zoomables and Highlights .
*
* @ static
* @ param { DomElement } context - The dom element to inspect .
* @ memberof Card
* /
static _cleanup ( context ) {
let [ zoomable , zoomedFig ] = this . _getZoomable ( context )
if ( zoomable && zoomedFig ) {
this . closeZoomable ( context , zoomable , zoomedFig )
}
let popup = this . _getPopup ( context )
if ( popup ) {
this . closePopup ( context , popup )
}
let highlights = this . _getHighlights ( context )
highlights . forEach ( highlight => {
Highlight . closeHighlight ( highlight , { animation : Card . highlightAnimation } )
} )
this . _unsetHighlights ( context )
}
/ * *
* 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
* @ param { DomElement } context - The element that defines the space , we want the rectangle to be in .
* @ param { DomElement } element - The element , we want to get the rectangle for .
* @ returns { DomRect } - Returns a DomRect representing the element outline in the space of context .
* @ memberof Card
* /
static _getContextRect ( context , element ) {
let global = this . _getGlobalRect ( element )
let localPosition = Points . fromPageToNode ( context , { x : global . x , y : global . y } )
return DOMRectReadOnly . fromRect ( {
x : localPosition . x ,
y : localPosition . y ,
width : global . width ,
height : global . height
} )
}
/ * *
* 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 .
* @ memberof Card
* /
static _getGlobalRect ( element ) {
let cardStyle = window . getComputedStyle ( element )
let cardWidth = parseInt ( cardStyle . width )
let cardHeight = parseInt ( cardStyle . height )
let globalPosition = Points . fromNodeToPage ( element , { x : 0 , y : 0 } )
return DOMRectReadOnly . fromRect ( {
x : globalPosition . x ,
y : globalPosition . y ,
width : cardWidth ,
height : cardHeight
} )
}
/ * *
* Adjusts all links
*
* @ static
* @ param { * } htmlString
* @ returns
* @ memberof Card
* /
static postProcessResponseText ( htmlString ) {
let editable = this . isEditable ( )
htmlString = this . _adjustRelativeLinks ( htmlString )
//console.log(htmlString)
let parser = new DOMParser ( )
let html = parser . parseFromString ( htmlString , "text/html" )
if ( ! editable ) {
this . _replaceAttributes ( html , "onclick" , this . _replaceCallback )
}
let zoomableWrappers = html . querySelectorAll ( ".svg-wrapper" )
zoomableWrappers . forEach ( wrapper => {
let svg = wrapper . querySelector ( "svg" )
Object . assign ( wrapper . style , {
width : svg . getAttribute ( "width" ) + "px" ,
height : svg . getAttribute ( "height" ) + "px"
} )
} )
let zoomableVideoWrappers = html . querySelectorAll ( ".video-wrapper" )
zoomableVideoWrappers . forEach ( wrapper => {
let video = wrapper . querySelector ( "video" )
Object . assign ( wrapper . style , {
width : video . getAttribute ( "width" ) + "px" ,
height : video . getAttribute ( "height" ) + "px"
} )
} )
return html
}
/ * *
* There occured a problem with the tap event , as it was canceled by the event on the svg element
* therefore we restructured the interaction , that only the svg listens for events and if the target is
* a circle ( more are not supported yet ) the target is adjusted using the . firstTarget of the event ,
* that is provided by the Hammer . propergate plugin .
* /
static openPopupOrZoomable ( event ) {
let target = this . _preferFirstTarget ( event )
if ( target . tagName == "circle" ) {
Card . loadHighlightPopup ( event )
event . stopPropagation ( )
} else {
Card . openZoomable ( event )
}
}
static pointRectDist ( p , r ) {
var cx = Math . max ( Math . min ( p . x , r . x + r . width ) , r . x )
var cy = Math . max ( Math . min ( p . y , r . y + r . height ) , r . y )
return Math . sqrt ( ( p . x - cx ) * ( p . x - cx ) + ( p . y - cy ) * ( p . y - cy ) )
}
static findNearbyPopups ( event , card ) {
if ( event . target ) {
//let column = event.target.closest(".column")
let indexbox = this . closestWithClass ( card , 'mainview' )
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 )
let linkRects = links . map ( link => {
let rect = link . getBoundingClientRect ( )
let topLeft = Points . fromPageToNode ( indexbox , rect )
2019-07-16 10:04:25 +02:00
let center = Points . fromPageToNode ( indexbox , { x : rect . x + rect . width / 2 , y : rect . y + rect . height / 2 } )
2019-07-16 09:21:28 +02:00
return { x : topLeft . x , y : topLeft . y , width : rect . width , height : rect . height , center , link }
} )
let distances = [ ]
linkRects . forEach ( rect => {
let distance = Card . pointRectDist ( localClick , rect )
if ( distance == 0.0 ) {
// Distance == 0.0 indicates an inside relation. Since these
// are already handled by the links themselves we put these cases
// to the end of the sorted list below
distance = 100000.0
}
distances . push ( parseInt ( distance ) )
} )
let closestLinkIndex = distances . indexOf ( Math . min ( ... distances ) )
let closestLink = links [ closestLinkIndex ]
console . log ( "finding closest links" , closestLink , closestLink . getClientRects ( ) )
if ( distances [ closestLinkIndex ] < 44 && closestLink . getAttribute ( "href" ) ) {
// Adapt context and local position
let context = this . getContext ( closestLink )
let local = Points . fromPageToNode ( context , globalClick )
let rect = linkRects [ closestLinkIndex ]
local . y = rect . center . y
2019-07-16 10:04:25 +02:00
2019-07-16 09:21:28 +02:00
Card . loadPopup ( event , context , closestLink , local )
}
}
}
}
/ * *
* Evaluates an event , if it has a first target . If so it returns the first target ,
* otherwise it returns the target .
* /
static _preferFirstTarget ( event ) {
return ( event . firstTarget ) ? event . firstTarget : event . target
}
/ * *
* Getter , Setter and 'Unsetter' for the properties hooked onto the context element .
* /
static _getPopup ( context ) {
return context . popup
}
static _setPopup ( context , popup , source ) {
this . _setPopupSource ( popup , source )
context . popup = popup
2019-07-16 10:04:25 +02:00
if ( typeof ( $ ) != 'undefined' ) {
//jquery hyphenate below
console . log ( 'hyphenated popup:' , $ ( 'span' ) . hyphenate ( 'de' ) )
}
2019-07-16 09:21:28 +02:00
}
static _unsetPopup ( context ) {
context . popup = null
}
static _setPopupSource ( popup , source ) {
popup . element . setAttribute ( "data-src" , source )
}
static _getPopupSource ( popup ) {
return popup . element . getAttribute ( "data-src" )
}
static _unsetZoomable ( context ) {
context . zoomable = null
}
static _setZoomable ( context , zoomable , zoomedFig ) {
context . zoomable = [ zoomable , zoomedFig ]
}
static _getZoomable ( context ) {
return ( context . zoomable ) ? context . zoomable : [ ]
}
/ * *
* Helper function to determine if a proided element is still
* an active highlight .
* /
static _isHighlightActive ( context , element ) {
let highlights = this . _getHighlights ( context )
return ( highlights . indexOf ( element ) != - 1 )
}
static _getHighlights ( context ) {
return ( context . highlights ) ? context . highlights : [ ]
}
static _addHighlight ( context , highlight ) {
if ( context . highlights == null ) this . _unsetHighlight ( context )
context . highlights . push ( highlight )
}
static _unsetHighlights ( context ) {
context . highlights = [ ]
}
static _getHighlightSource ( highlight ) {
return highlight . getAttribute ( "xlink:href" )
}
static _getActiveSubcard ( context ) {
return context . querySelector ( ".mainview > .subcard" )
}
static _setSubcard ( context , subcard ) {
//console.trace("_setSubcard", context)
context . subcard = subcard
}
static _getSubcard ( context ) {
return context . subcard
}
static _unsetSubcard ( context ) {
context . subcard = null
}
static incrementZIndex ( context ) {
if ( ! context . zIndex ) context . zIndex = 0
context . zIndex ++
}
static getZIndex ( context ) {
return ( context . zIndex || 0 )
}
/ * *
* 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 .
*
* Not doing this lead to some cases , when Card - / S c a t t e r C a r d . f u n c
* was called and depending on context their were different values
* inside the relativePath variable .
* /
static set relativePath ( value ) {
Card . _relativePath = value
}
static get relativePath ( ) {
return Card . _relativePath
}
}
Card . debug = true
Card . _relativePath = ""
Card . scatterContainer = null
Card . interactionType = "tap"
Card . popupHtmlSelector = ".popupHtml"
Card . dynamicHeight = false
Card . popupYOffset = - 15
Card . zIndices = {
article : 10 ,
popup : 100 ,
zoomable : 101 ,
zoomedFigure : 102
}
Card . animation = {
articleTransition : 0.6 ,
fade : 0.2 ,
popup : 0.1 ,
highlight : 0.4 ,
zoomable : 0.5
}
/ * *
* Extends the card with scatter functionality .
*
* @ class ScatterCard
* /
2019-07-16 10:04:25 +02:00
export class ScatterCard extends Card {
2019-07-16 09:21:28 +02:00
/ * *
* 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 = "../" ,
2019-07-16 10:04:25 +02:00
modules = [ ] ,
2019-07-16 09:21:28 +02:00
onClose = null
} = { } ) {
console . log ( basePath )
return new Promise ( ( resolve , reject ) => {
2019-07-16 10:04:25 +02:00
let url = basePath + "/" + item + "/index.html"
console . log ( "Loading" , url )
this . loadHTML ( url )
2019-07-16 09:21:28 +02:00
. then ( html => {
2019-07-16 10:04:25 +02:00
console . log ( "Received" , html )
2019-07-16 09:21:28 +02:00
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
}
}