2019-03-22 12:54:57 +01:00
( function ( ) {
'use strict' ;
// In order to test this interface implementation run jsc interface.js
class Interface {
// Abstract interface that should be extended in interface subclasses.
// By convention all interfaces should start with an upper 'I'
static implementationError ( klass ) {
let interfaceKeys = Reflect . ownKeys ( this . prototype ) ;
let classKeys = Reflect . ownKeys ( klass . prototype ) ;
for ( let key of interfaceKeys ) {
let interfaceDesc = this . prototype [ key ] ;
let classDesc = klass . prototype [ key ] ;
if ( typeof ( classDesc ) == 'undefined' )
return 'Missing ' + key
}
return null
}
static implementedBy ( klass ) {
// In the first step only checks whether the methods of this
// interface are all implemented by the given class
let error = this . implementationError ( klass ) ;
return error == null
}
// TODO: Specify optional methods
// static optionalMethods() {
// return [this.onMouseWheel]
// }
}
/ * * B a s i c A p p l i c a t i o n o b j e c t t o b e u s e d a s a s i n g l e t o n .
Provides an interface for automatic testing and common device specific
feature detection .
* /
class IApp extends Interface {
/ * * B u i l d t h e a p p b y r e g i s t e r i n g e v e n t h a n d l e r s ,
* adding DOM elements , instanciating templates , etc ...
* /
setup ( ) { return this }
/ * * R u n t h e a p p l i c a t i o n b y s t a r t i n g a m a i n l o o p , . . .
* /
run ( ) { return this }
}
class App extends Object {
/ * * O v e r r i d e t h i s m e t h o d t o b u i l d y o u r a p p .
* /
setup ( ) {
return this
}
/ * * S t a r t a n d r u n t h e a p p l i c a t i o n . O v e r r i d e t h i s m e t h o d w i t h e v e r y t h i n g
that is needed to maintain your App , main loops , etc .
* /
run ( ) {
return this
}
/ * * D e f i n e s a l l t e s t s u i t e s . O v e r w r i t e t h i s m e t h o d t o e n s u r e t h a t
all testable aspects of your App are evaluated .
* /
allTests ( ) {
console . log ( 'Overwrite App.allTests()' ) ;
}
/ * * R u n a l l t e s t s . S h o u l d r e t u r n ' o k ' a n d t h e a m o u n t o f t i m e n e e d e d t o
run App . allTests ( ) or a failure message with diagnostic error decription .
@ return { array } - array with 'ok' as first element and needed time as
second element or "Tests failed" and an error string
* /
runTests ( ) {
var start = performance . now ( ) ;
try {
this . allTests ( ) ;
var end = performance . now ( ) ;
return [ 'ok' , end - start ]
}
catch ( e ) {
console . trace ( ) ;
return [ 'Tests failed' , e . message ]
}
}
}
IApp . implementedBy ( App ) ;
// Allows browsers to perform doctests.
// Uses the code highlight package from http://highlightjs.readthedocs.io
// if available
var docTestLogMessages = [ ] ;
Array . prototype . equals = function ( array ) {
return this . length == array . length &&
this . every ( function ( this _i , i ) { return this _i == array [ i ] } )
} ;
class Doctest {
static assert ( value ) {
if ( ! value ) {
throw new Error ( 'Assertion violated' )
}
}
static pprint ( obj ) {
if ( obj === null )
return 'null'
let stringified = obj . toString ( ) ;
if ( stringified == '[object Object]' )
return JSON . stringify ( obj )
return stringified
}
static expect ( expr , value ) {
if ( this . pprint ( expr ) != this . pprint ( value ) ) {
//throw new Error("got `" + expr + "` but expected `" + value + "`.")
throw new Error ( 'got `' + this . pprint ( expr ) + '` but expected `' + this . pprint ( value ) + '`.' )
}
}
static expectError ( error , message ) {
let index = error . toString ( ) . indexOf ( message ) ;
if ( index < 0 ) {
throw new Error ( 'got `' + message + '` but expected `' + error + '`.' )
}
}
static expectLog ( ... messages ) {
// if (!docTestLogMessages.equals(messages)) {
docTestLogMessages . forEach ( ( msg , i ) => {
if ( msg != messages [ i ] )
throw new Error ( 'Unexpected log message: `' + messages [ i ] + '`.' )
} ) ;
// throw new Error('Uups')
//}
}
static log ( message ) {
docTestLogMessages . push ( message ) ;
}
static highlight ( code ) {
if ( typeof ( hljs ) == 'undefined' )
return code
return hljs . highlight ( 'javascript' , code )
}
static stripLeadingLines ( code ) {
let result = [ ] ;
let informative = false ;
for ( let line of code . split ( '\n' ) ) {
if ( line . trim ( ) . length > 0 ) {
informative = true ;
}
if ( informative )
result . push ( line ) ;
}
return result . join ( '\n' )
}
static event ( type = 'mouse' , { clientX = 0 , clientY = 0 } = { } ) {
if ( type . startsWith ( 'mouse' ) ) {
return new MouseEvent ( type , { clientX , clientY } )
}
return { type , clientX , clientY }
}
static run ( replaceExpect = false ) {
if ( typeof ( hljs ) != 'undefined' ) {
hljs . initHighlighting ( ) ;
}
let doctests = document . querySelectorAll ( '.doctest' ) ;
for ( let i = 0 ; i < doctests . length ; i ++ ) {
let doctest = doctests [ i ] ;
let code = this . stripLeadingLines ( doctest . innerHTML ) ;
let text = this . highlight ( code ) ;
let pre = document . createElement ( 'pre' ) ;
// See http://stackoverflow.com/questions/1068280/javascript-regex-multiline-flag-doesnt-work
// let re = /Doctest\.expect\(([\s\S]*)[\,\s\S]*([\s\S]*)\)/g
let lines = text . value . split ( '\n' ) ;
let better = [ ] ;
for ( let line of lines ) {
if ( replaceExpect && line . trim ( ) . startsWith ( 'Doctest.expect(' ) ) {
line = line . replace ( /Doctest\.expect\(/ , '>>> ' ) . trim ( ) ;
if ( line . endsWith ( ')' ) || line . endsWith ( ',' ) ) {
line = line . slice ( 0 , - 1 ) ;
}
}
better . push ( line ) ;
}
pre . innerHTML = better . join ( '\n' ) ; // text.value.replace(re, ">>> $1\n$2")
doctest . parentNode . replaceChild ( pre , doctest ) ;
}
}
}
// Needed to make Doctest visible in modules
//window.Doctest = Doctest
var recordedErrors = new Map ( ) ;
class Errors {
static countErrors ( ) {
let total = 0 ;
for ( let error of recordedErrors . keys ( ) ) {
total += recordedErrors . get ( error ) . size ;
}
return total
}
static setStyle ( element , styles ) {
for ( let key in styles ) {
element . style [ key ] = styles [ key ] ;
}
}
static appendError ( error , source ) {
if ( recordedErrors . has ( error ) ) {
let sources = recordedErrors . get ( error ) ;
sources . add ( source ) ;
}
else {
recordedErrors . set ( error , new Set ( [ source ] ) ) ;
}
}
static showErrors ( ) {
if ( this . countErrors ( ) == 0 ) {
return
}
let errors = document . getElementById ( 'runtime-errors' ) ;
if ( errors == null ) {
errors = document . createElement ( 'div' ) ;
errors . setAttribute ( 'id' , 'runtime-errors' ) ;
this . setStyle ( document . body , {
border : '2px solid red'
} ) ;
this . setStyle ( errors , { position : 'absolute' ,
top : '0px' ,
padding : '8px' ,
width : '100%' ,
background : 'red' ,
color : 'white' } ) ;
document . body . appendChild ( errors ) ;
let counter = document . createElement ( 'div' ) ;
counter . setAttribute ( 'id' , 'runtime-errors-counter' ) ;
this . setStyle ( counter , { borderRadius : '50%' ,
width : '32px' ,
height : '32px' ,
background : 'white' ,
color : 'red' ,
fontSize : '18px' ,
textAlign : 'center' ,
lineHeight : '32px' ,
verticalAlign : 'middle' } ) ;
counter . innerHTML = '1' ;
errors . appendChild ( counter ) ;
let header = document . createElement ( 'div' ) ;
this . setStyle ( header , { position : 'absolute' ,
top : '6px' ,
left : '48px' ,
height : '44px' ,
fontSize : '32px' } ) ;
header . innerHTML = 'Runtime Errors' ;
errors . appendChild ( header ) ;
errors . addEventListener ( 'click' , this . toggleErrors . bind ( this ) ) ;
}
let counter = document . getElementById ( 'runtime-errors-counter' ) ;
counter . innerHTML = this . countErrors ( ) ;
}
static expandErrors ( ) {
let errors = document . getElementById ( 'runtime-errors' ) ;
for ( let error of recordedErrors . keys ( ) ) {
for ( var source of recordedErrors . get ( error ) ) {
if ( typeof ( source ) == 'undefined' ) {
source = 'See console for details' ;
return
}
let info = document . createElement ( 'div' ) ;
info . className = 'info' ;
info . style . wordWrap = 'break-word' ;
info . innerHTML = error + ` <br/><small> ${ source } </small> ` ;
errors . appendChild ( info ) ;
}
}
}
static toggleErrors ( ) {
let errors = document . getElementById ( 'runtime-errors' ) ;
let infos = errors . querySelectorAll ( '.info' ) ;
if ( infos . length > 0 ) {
infos . forEach ( ( info ) => errors . removeChild ( info ) ) ;
}
else {
this . expandErrors ( ) ;
}
}
static removeError ( event ) {
console . log ( 'removeError' , event ) ;
if ( recordedErrors . has ( event . error ) ) {
let sources = recordedErrors . get ( event . error ) ;
sources . delete ( event . source ) ;
console . log ( 'sources' , sources ) ;
}
}
static registerGlobalErrorHandler ( ) {
// Register more informative error handler
window . addEventListener ( 'error' , ( event ) => {
// if (typeof(event.error) == 'undefined') {
// console.info("Catched undefined error", event)
// }
this . appendError ( event . error , event . filename ) ;
} , true ) ;
document . addEventListener ( 'DOMContentLoaded' , ( event ) => {
this . showErrors ( ) ;
} ) ;
}
static registerFrameAwaitErrors ( ) {
let iframes = document . getElementsByTagName ( 'iframe' ) ;
for ( let i = 0 ; i < iframes . length ; i ++ ) {
let target = iframes [ i ] ;
target . iframeTimeout = setTimeout (
( ) => {
this . appendError ( 'Cannot load iframe' , target . src ) ; } ,
frameErrorTimeout ) ;
target . onload = ( ) => {
clearTimeout ( target . iframeTimeout ) ;
} ;
}
}
}
Errors . registerGlobalErrorHandler ( ) ;
class Events {
static stop ( event ) {
event . preventDefault ( ) ;
event . stopPropagation ( ) ;
}
static extractPoint ( event ) {
switch ( event . constructor . name ) {
case 'TouchEvent' :
for ( let i = 0 ; i < event . targetTouches . length ; i ++ ) {
let t = event . targetTouches [ i ] ;
return { x : t . clientX , y : t . clientY }
}
break
default :
return { x : event . clientX , y : event . clientY }
}
}
static isCaptured ( event ) {
if ( event . _ _capturedBy )
return true
return false
}
static capturedBy ( event , obj ) {
event . _ _capturedBy = obj ;
}
static isPointerDown ( event ) {
// According to
// https://developer.mozilla.org/en-US/docs/Web/API/Pointer_events
// pointer events use the buttons feature to represent pressed buttons
return event . buttons
}
static isMouseDown ( event ) {
// Attempts to clone the which attribute of events failed in WebKit. May
// be this is a bug or a security feature. Workaround: we introduce
// a mouseDownSubstitute attribute that can be assigned to cloned
// events after instantiation.
if ( Reflect . has ( event , 'mouseDownSubstitute' ) )
return event . mouseDownSubstitute
return event . buttons || event . which
}
static isSimulatedEvent ( event ) {
return Reflect . has ( event , 'mouseDownSubstitute' )
}
static isMouseRightClick ( event ) {
return event . buttons || event . which
}
static extractTouches ( targets ) {
let touches = [ ] ;
for ( let i = 0 ; i < targets . length ; i ++ ) {
let t = targets [ i ] ;
touches . push ( {
targetSelector : this . selector ( t . target ) ,
identifier : t . identifier ,
screenX : t . screenX ,
screenY : t . screenY ,
clientX : t . clientX ,
clientY : t . clientY ,
pageX : t . pageX ,
pageY : t . pageY
} ) ;
}
return touches
}
static createTouchList ( targets ) {
let touches = [ ] ;
for ( let i = 0 ; i < targets . length ; i ++ ) {
let t = targets [ i ] ;
let touchTarget = document . elementFromPoint ( t . pageX , t . pageY ) ;
let touch = new Touch ( undefined , touchTarget , t . identifier ,
t . pageX , t . pageY , t . screenX , t . screenY ) ;
touches . push ( touch ) ;
}
return new TouchList ( ... touches )
}
static extractEvent ( timestamp , event ) {
let targetSelector = this . selector ( event . target ) ;
let infos = {
type : event . type ,
time : timestamp ,
constructor : event . constructor ,
data : {
targetSelector : targetSelector ,
view : event . view ,
mouseDownSubstitute : event . buttons || event . which , // which cannot be cloned directly
bubbles : event . bubbles ,
cancelable : event . cancelable ,
screenX : event . screenX ,
screenY : event . screenY ,
clientX : event . clientX ,
clientY : event . clientY ,
layerX : event . layerX ,
layerY : event . layerY ,
pageX : event . pageX ,
pageY : event . pageY ,
ctrlKey : event . ctrlKey ,
altKey : event . altKey ,
shiftKey : event . shiftKey ,
metaKey : event . metaKey
}
} ;
if ( event . type . startsWith ( 'touch' ) ) {
// On Safari-WebKit the TouchEvent has layerX, layerY coordinates
let data = infos . data ;
data . targetTouches = this . extractTouches ( event . targetTouches ) ;
data . changedTouches = this . extractTouches ( event . changedTouches ) ;
data . touches = this . extractTouches ( event . touches ) ;
}
if ( event . type . startsWith ( 'pointer' ) ) {
let data = infos . data ;
data . pointerId = event . pointerId ;
data . pointerType = event . pointerType ;
}
if ( Events . debug ) {
Events . extracted . push ( this . toLine ( event ) ) ;
}
return infos
}
static cloneEvent ( type , constructor , data ) {
if ( type . startsWith ( 'touch' ) ) {
// We need to find target from layerX, layerY
//var target = document.querySelector(data.targetSelector)
// elementFromPoint(data.layerX, data.layerY)
//data.target = target
data . targetTouches = this . createTouchList ( data . targetTouches ) ;
data . changedTouches = this . createTouchList ( data . changedTouches ) ;
data . touches = this . createTouchList ( data . touches ) ;
}
// We need to find target from pageX, pageY which are only
// available after construction. They seem to getter items.
let clone = Reflect . construct ( constructor , [ type , data ] ) ;
clone . mouseDownSubstitute = data . mouseDownSubstitute ;
return clone
}
static simulateEvent ( type , constructor , data ) {
data . target = document . querySelector ( data . targetSelector ) ;
let clone = this . cloneEvent ( type , constructor , data ) ;
if ( data . target != null ) {
data . target . dispatchEvent ( clone ) ;
}
if ( Events . debug ) {
Events . simulated . push ( this . toLine ( clone ) ) ;
}
}
static toLine ( event ) {
return ` ${ event . type } # ${ event . target . id } ${ event . clientX } ${ event . clientY } `
let result = event . type ;
let selector = this . selector ( event . target ) ;
result += ' selector: ' + selector ;
if ( event . target != document . querySelector ( selector ) )
console . log ( 'Cannot resolve' , selector ) ;
let keys = [ 'layerX' , 'layerY' , 'pageX' , 'pageY' , 'clientX' , 'clientY' ] ;
for ( let key of keys ) {
try {
result += ' ' + key + ':' + event [ key ] ;
}
catch ( e ) {
console . log ( 'Invalid key: ' + key ) ;
}
}
return result
}
static compareExtractedWithSimulated ( ) {
if ( this . extracted . length != this . simulated . length ) {
alert ( 'Unequal length of extracted [' + this . extracted . length +
'] and simulated events [' + this . simulated . length + '].' ) ;
}
else {
for ( let i = 0 ; i < this . extracted . length ; i ++ ) {
var extracted = this . extracted [ i ] ;
var simulated = this . simulated [ i ] ;
if ( extracted != simulated ) {
console . log ( 'Events differ:' + extracted + '|' + simulated ) ;
}
}
}
}
static selector ( context ) {
return OptimalSelect . select ( context )
}
static reset ( ) {
this . extracted = [ ] ;
this . simulated = [ ] ;
}
static resetSimulated ( ) {
this . simulated = [ ] ;
}
static showExtractedEvents ( event ) {
if ( ! event . shiftKey ) {
return
}
if ( this . popup == null ) {
let element = document . createElement ( 'div' ) ;
Elements . setStyle ( element , {
position : 'absolute' ,
width : '480px' ,
height : '640px' ,
overflow : 'auto' ,
backgroundColor : 'lightgray'
} ) ;
document . body . appendChild ( element ) ;
this . popup = element ;
}
this . popup . innerHTML = '' ;
for ( let line of this . extracted ) {
let div = document . createElement ( 'div' ) ;
div . innerHTML = line ;
this . popup . appendChild ( div ) ;
}
let div = document . createElement ( 'div' ) ;
div . innerHTML = '------------ Simulated -----------' ;
this . popup . appendChild ( div ) ;
for ( let line of this . simulated ) {
let div = document . createElement ( 'div' ) ;
div . innerHTML = line ;
this . popup . appendChild ( div ) ;
}
Elements . setStyle ( this . popup ,
{ left : event . clientX + 'px' , top : event . clientY + 'px' } ) ;
}
}
Events . popup = null ;
Events . debug = true ;
Events . extracted = [ ] ;
Events . simulated = [ ] ;
Events . simulationRunning = false ;
class EventRecorder {
constructor ( ) {
this . recording = [ ] ;
this . recorded = [ ] ;
this . step = 0 ;
}
record ( event ) {
let length = this . recording . length ;
if ( length == 0 ) {
this . startTime = event . timeStamp ;
Events . reset ( ) ;
}
else {
let last = this . recording [ length - 1 ] ;
if ( event . timeStamp < last . time ) {
console . log ( 'warning: wrong temporal order' ) ;
}
}
let t = event . timeStamp - this . startTime ;
this . recording . push ( Events . extractEvent ( t , event ) ) ;
}
stopRecording ( ) {
this . recorded = this . recording ;
this . recording = [ ] ;
console . log ( 'Recorded ' + this . recorded . length + ' events' ) ;
}
startReplay ( whileCondition = null , onComplete = null ) {
this . step = 0 ;
Events . resetSimulated ( ) ;
console . log ( 'Start replay' ) ;
Events . simulationRunning = true ;
this . replay ( whileCondition , onComplete ) ;
}
replay ( whileCondition = null , onComplete = null ) {
if ( this . step < this . recorded . length ) {
let { type , time , constructor , data } = this . recorded [ this . step ] ;
Events . simulateEvent ( type , constructor , data ) ;
this . step += 1 ;
let dt = 0 ;
if ( this . step < this . recorded . length ) {
var next = this . recorded [ this . step ] ;
dt = next . time - time ;
if ( dt < 0 ) {
console . log ( 'warning: wrong temporal order' ) ;
}
}
if ( whileCondition == null || whileCondition ( ) ) {
let delta = Math . round ( dt ) ;
setTimeout ( ( ) => this . replay ( whileCondition , onComplete ) , delta ) ;
}
}
else {
console . log ( 'Played ' + this . step + ' events' + onComplete ) ;
Events . simulationRunning = false ;
if ( onComplete != null ) {
onComplete ( ) ;
}
//Events.compareExtractedWithSimulated()
}
}
}
/* globals WebKitPoint */
/ * * T e s t s w h e t h e r a n o b j e c t i s e m p t y
* @ param { Object } obj - the object to be tested
* @ return { boolean }
* /
function isEmpty ( obj ) {
// > isEmpty({})
// true
for ( let i in obj ) {
return false
}
return true
}
function lerp ( start , stop , amt ) {
return amt * ( stop - start ) + start
}
// Returns a function, that, as long as it continues to be invoked, will not
// be triggered. The function will be called after it stops being called for
// N milliseconds. If `immediate` is passed, trigger the function on the
// leading edge, instead of the trailing.
// Taken from: https://davidwalsh.name/essential-javascript-functions
function debounce ( func , wait , immediate ) {
let timeout ;
return function ( ) {
let context = this ,
args = arguments ;
let later = function ( ) {
timeout = null ;
if ( ! immediate ) func . apply ( context , args ) ;
} ;
let callNow = immediate && ! timeout ;
clearTimeout ( timeout ) ;
timeout = setTimeout ( later , wait ) ;
if ( callNow ) func . apply ( context , args ) ;
}
}
/ * * R e t u r n s a n i d t h a t i s g u a r a n t e e d t o b e u n i q u e w i t h i n t h e l i v e t i m e o f t h e
* application
* @ return { string }
* /
let _idGenerator = 0 ;
function getId$1 ( ) {
return 'id' + _idGenerator ++
}
function randomInt ( min = 0 , max = 100 ) {
return Math . floor ( Math . random ( ) * ( max - min + 1 ) + min )
}
function randomFloat ( min = 0.0 , max = 1.0 ) {
return Math . random ( ) * ( max - min ) + min
}
class Dates {
static create ( fullYear , month , day ) {
return new Date ( Date . UTC ( fullYear , month , day ) )
}
static daysInMonth ( date ) {
return new Date ( date . getFullYear ( ) , date . getMonth ( ) + 1 , 0 ) . getDate ( )
}
static startYearRange ( date ) {
return new Date ( Date . UTC ( date . getFullYear ( ) - 1 , 11 , 31 , 23 , 59 , 59 , 999 ) )
}
static endYearRange ( date ) {
return new Date ( Date . UTC ( date . getFullYear ( ) + 1 , 0 , 1 ) )
}
static prevYear ( date , offset = 1 ) {
return this . create ( date . getFullYear ( ) - offset , 0 , 1 )
}
static nextYear ( date , offset = 1 ) {
return this . create ( date . getFullYear ( ) + offset , 0 , 1 )
}
static nextMonth ( date ) {
return this . create ( date . getFullYear ( ) , date . getMonth ( ) + 1 , 1 )
}
static nextDay ( date ) {
return this . create (
date . getFullYear ( ) ,
date . getMonth ( ) ,
date . getDate ( ) + 1
)
}
static nextHour ( date ) {
// See http://stackoverflow.com/questions/1050720/adding-hours-to-javascript-date-object
return new Date ( date . getTime ( ) + 60 * 60 * 1000 )
}
static nextMinute ( date ) {
// See above
return new Date ( date . getTime ( ) + 60 * 1000 )
}
static nextSecond ( date ) {
// See above
return new Date ( date . getTime ( ) + 1000 )
}
static nextMillisecond ( date ) {
// See above
return new Date ( date . getTime ( ) + 1 )
}
static * iterYears ( start , end ) {
let date = this . create ( start . getFullYear ( ) , 0 , 1 ) ;
while ( date <= end ) {
yield date ;
date = this . nextYear ( date ) ;
}
yield date ;
}
static * iterMonths ( year , limit = 12 ) {
let month = 0 ;
while ( month < limit ) {
let date = this . create ( year . getFullYear ( ) , month , 1 ) ;
yield date ;
month += 1 ;
}
}
static * iterMonthsOfYears ( years ) {
for ( let year of years ) {
for ( let month of this . iterMonths ( year ) ) {
yield month ;
}
}
}
static * iterDays ( month ) {
let day = 1 ;
let limit = Dates . daysInMonth ( month ) ;
while ( day <= limit ) {
let date = this . create ( month . getFullYear ( ) , month . getMonth ( ) , day ) ;
yield date ;
day += 1 ;
}
}
static * iterDaysOfMonths ( months ) {
for ( let month of months ) {
for ( let day of this . iterDays ( month ) ) {
yield day ;
}
}
}
}
/* Color conversion functions */
class Colors {
// http://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
static rgb2num ( red , green , blue ) {
let rgb = blue | ( green << 8 ) | ( red << 16 ) ;
return 0x000000 + rgb
}
static rgb2hex ( red , green , blue ) {
let rgb = blue | ( green << 8 ) | ( red << 16 ) ;
return '#' + ( 0x1000000 + rgb ) . toString ( 16 ) . slice ( 1 )
}
static hex2rgb ( hex ) {
// long version
let r = hex . match ( /^#([0-9a-f]{2})([0-9a-f]{2})([0-9a-f]{2})$/i ) ;
if ( r ) {
return r . slice ( 1 , 4 ) . map ( x => {
return parseInt ( x , 16 )
} )
}
// short version
r = hex . match ( /^#([0-9a-f])([0-9a-f])([0-9a-f])$/i ) ;
if ( r ) {
return r . slice ( 1 , 4 ) . map ( x => {
return 0x11 * parseInt ( x , 16 )
} )
}
return null
}
static rgb ( r , g , b ) {
return { r , g , b }
}
static string2hex ( str ) {
return parseInt ( '0x' + str . slice ( 1 ) )
}
static lerp ( rgb1 , rgb2 , amount ) {
return {
r : Math . round ( lerp ( rgb1 . r , rgb2 . r , amount ) ) ,
g : Math . round ( lerp ( rgb1 . g , rgb2 . g , amount ) ) ,
b : Math . round ( lerp ( rgb1 . b , rgb2 . b , amount ) )
}
}
static get violet ( ) {
return Colors . rgb2num ( 89 , 34 , 131 )
}
static get steelblue ( ) {
return Colors . rgb2num ( 0 , 130 , 164 )
}
static get ochre ( ) {
return Colors . rgb2num ( 181 , 157 , 0 )
}
static get turquoise ( ) {
return Colors . rgb2num ( 34 , 164 , 131 )
}
static get eminence ( ) {
return Colors . rgb2num ( 150 , 60 , 134 )
}
static random ( ) {
let r = Math . round ( Math . random ( ) * 255 ) ;
let g = Math . round ( Math . random ( ) * 255 ) ;
let b = Math . round ( Math . random ( ) * 255 ) ;
return Colors . rgb2num ( r , g , b )
}
}
class Cycle extends Array {
constructor ( ... items ) {
super ( ) ;
for ( let item of items ) {
this . push ( item ) ;
}
this . index = 0 ;
}
next ( ) {
if ( this . index == this . length ) {
this . index = 0 ;
}
return this [ this . index ++ ]
}
current ( ) {
if ( this . index === this . length ) {
this . index = 0 ;
}
return this [ this . index ]
}
}
/ * * S t a t i c m e t h o d s t o c o m p u t e 2 D p o i n t s w i t h x a n d y c o o r d i n a t e s .
* /
class Points {
static length ( a ) {
return Math . sqrt ( a . x * a . x + a . y * a . y )
}
static normalize ( p ) {
let len = this . length ( p ) ;
return this . multiplyScalar ( p , 1 / len )
}
static mean ( a , b ) {
return { x : ( a . x + b . x ) / 2 , y : ( a . y + b . y ) / 2 }
}
static subtract ( a , b ) {
return { x : a . x - b . x , y : a . y - b . y }
}
static multiply ( a , b ) {
return { x : a . x * b . x , y : a . y * b . y }
}
static divide ( a , b ) {
return { x : a . x / b . x , y : a . y / b . y }
}
static multiplyScalar ( a , b ) {
return { x : a . x * b , y : a . y * b }
}
static add ( a , b ) {
return { x : a . x + b . x , y : a . y + b . y }
}
static negate ( p ) {
return { x : - p . x , y : - p . y }
}
static angle ( p1 , p2 ) {
return Math . atan2 ( p1 . y - p2 . y , p1 . x - p2 . x )
}
static normalizedAngle ( p1 , p2 ) {
return Angle . normalize ( this . angle ( p1 , p2 ) )
}
static normalized2Angle ( p1 , p2 ) {
return Angle . normalize2 ( this . angle ( p1 , p2 ) )
}
static arc ( p , alpha , radius ) {
return {
x : p . x + radius * Math . cos ( alpha ) ,
y : p . y + radius * Math . sin ( alpha )
}
}
static distance ( a , b ) {
let dx = a . x - b . x ;
let dy = a . y - b . y ;
return Math . sqrt ( dx * dx + dy * dy )
}
static fromPageToNode ( element , p ) {
// if (window.webkitConvertPointFromPageToNode) {
// return window.webkitConvertPointFromPageToNode(element,
// new WebKitPoint(p.x, p.y))
// }
return window . convertPointFromPageToNode ( element , p . x , p . y )
}
static fromNodeToPage ( element , p ) {
// if (window.webkitConvertPointFromNodeToPage) {
// return window.webkitConvertPointFromNodeToPage(element,
// new WebKitPoint(p.x, p.y))
// }
return window . convertPointFromNodeToPage ( element , p . x , p . y )
}
}
/ * *
* A helper class for common set operations .
*
* @ export
* @ class Sets
* /
class Sets {
/ * *
* Returns the intersection of all sets
* https : //stackoverflow.com/questions/31930894/javascript-set-data-structure-intersect
* @ static
* @ param { * } sets
* @ returns
* @ memberof Sets
* /
static intersect ( ... sets ) {
if ( ! sets . length ) return new Set ( )
const i = sets . reduce ( ( m , s , i ) => s . size < sets [ m ] . size ? i : m , 0 ) ;
const [ smallest ] = sets . splice ( i , 1 ) ;
const res = new Set ( ) ;
for ( let val of smallest )
if ( sets . every ( s => s . has ( val ) ) )
res . add ( val ) ;
return res
}
/ * *
* Returns the union of all sets
*
* @ static
* @ param { * } sets
* @ returns
* @ memberof Sets
* /
static union ( ... sets ) {
let result = new Set ( ) ;
for ( let set of sets ) {
for ( let m of set ) {
result . add ( m ) ;
}
}
return result
}
/ * *
* Returns the difference of the given sets . Starts with the first set and removing all elements of the following sets .
*
* @ static
* @ param { * } set
* @ param { * } sets
* @ returns
* @ memberof Sets
* /
static difference ( set , ... sets ) {
let result = new Set ( ) ;
for ( let m of set ) {
result . add ( m ) ;
}
for ( let s of sets ) {
for ( let m of s ) {
result . delete ( m ) ;
}
}
return result
}
}
/ * * S t a t i c m e t h o d s t o c o m p u t e a n g l e s .
* /
class Angle {
static normalize ( angle ) {
let TAU = Math . PI * 2.0 ;
while ( angle > Math . PI ) {
angle -= TAU ;
}
while ( angle < - Math . PI ) {
angle += TAU ;
}
return angle
}
static normalize2 ( angle ) {
let TAU = Math . PI * 2.0 ;
while ( angle > TAU ) {
angle -= TAU ;
}
while ( angle < 0 ) {
angle += TAU ;
}
return angle
}
static normalizeDegree ( angle ) {
let full = 360.0 ;
while ( angle > 180.0 ) {
angle -= full ;
}
while ( angle < - 180.0 ) {
angle += full ;
}
return angle
}
static normalizedDiff ( a , b ) {
return this . normalize ( this . diff ( a , b ) )
}
static normalized2Diff ( a , b ) {
return this . normalize2 ( this . diff ( a , b ) )
}
static diff ( a , b ) {
return Math . atan2 ( Math . sin ( a - b ) , Math . cos ( a - b ) )
}
static degree2radian ( degree ) {
return Math . PI * degree / 180.0
}
static radian2degree ( rad ) {
return 180.0 / Math . PI * rad
}
}
class Elements$1 {
static setStyle ( element , styles ) {
for ( let key in styles ) {
element . style [ key ] = styles [ key ] ;
}
}
static addClass ( element , cssClass ) {
element . classList . add ( cssClass ) ;
}
static removeClass ( element , cssClass ) {
element . classList . remove ( cssClass ) ;
}
static toggleClass ( element , cssClass ) {
element . classList . toggle ( cssClass ) ;
}
static hasClass ( element , cssClass ) {
return element . classList . contains ( cssClass )
}
}
class MapProxy {
/ * T h i s c l a s s i s n e e d e d i f w e w a n t t o u s e t h e i n t e r a c t i o n c l a s s e s
in Firefox 45.8 and modern Browsers .
A workaround for https : //github.com/babel/babel/issues/2334
* /
constructor ( ) {
this . map = new Map ( ) ;
}
get size ( ) {
return this . map . size
}
get ( key ) {
return this . map . get ( key )
}
set ( key , value ) {
return this . map . set ( key , value )
}
delete ( key ) {
return this . map . delete ( key )
}
clear ( ) {
return this . map . clear ( )
}
has ( key ) {
return this . map . has ( key )
}
keys ( ) {
return this . map . keys ( )
}
values ( ) {
return this . map . values ( )
}
entries ( ) {
return this . map . entries ( )
}
forEach ( func ) {
this . map . forEach ( func ) ;
}
}
/* Based om https://gist.github.com/cwleonard/e124d63238bda7a3cbfa */
class Polygon {
/ *
* This is the Polygon constructor . All points are center - relative .
* /
constructor ( center ) {
this . points = new Array ( ) ;
this . center = center ;
}
/ *
* Point x and y values should be relative to the center .
* /
addPoint ( p ) {
this . points . push ( p ) ;
}
/ *
* Point x and y values should be absolute coordinates .
* /
addAbsolutePoint ( p ) {
this . points . push ( { x : p . x - this . center . x , y : p . y - this . center . y } ) ;
}
/ *
* Returns the number of sides . Equal to the number of vertices .
* /
getNumberOfSides ( ) {
return this . points . length
}
/ *
* rotate the polygon by a number of radians
* /
rotate ( rads ) {
for ( let i = 0 ; i < this . points . length ; i ++ ) {
let x = this . points [ i ] . x ;
let y = this . points [ i ] . y ;
this . points [ i ] . x = Math . cos ( rads ) * x - Math . sin ( rads ) * y ;
this . points [ i ] . y = Math . sin ( rads ) * x + Math . cos ( rads ) * y ;
}
}
/ *
* The draw function takes as a parameter a Context object from
* a Canvas element and draws the polygon on it .
* /
draw ( context , { lineWidth = 2 , stroke = '#000000' , fill = null } = { } ) {
context . beginPath ( ) ;
context . moveTo (
this . points [ 0 ] . x + this . center . x ,
this . points [ 0 ] . y + this . center . y
) ;
for ( let i = 1 ; i < this . points . length ; i ++ ) {
context . lineTo (
this . points [ i ] . x + this . center . x ,
this . points [ i ] . y + this . center . y
) ;
}
context . closePath ( ) ;
context . lineWidth = lineWidth ;
if ( stroke ) {
context . strokeStyle = stroke ;
context . stroke ( ) ;
}
if ( fill ) {
context . fillStyle = fill ;
context . fill ( ) ;
}
}
absolutePoints ( ) {
let result = new Array ( ) ;
for ( let p of this . points ) {
result . push ( Points . add ( p , this . center ) ) ;
}
return result
}
flatAbsolutePoints ( ) {
let result = new Array ( ) ;
for ( let p of this . points ) {
let a = Points . add ( p , this . center ) ;
result . push ( a . x ) ;
result . push ( a . y ) ;
}
return result
}
/ *
* This function returns true if the given point is inside the polygon ,
* and false otherwise .
* /
containsPoint ( pnt ) {
let nvert = this . points . length ;
let testx = pnt . x ;
let testy = pnt . y ;
let vertx = new Array ( ) ;
for ( let q = 0 ; q < this . points . length ; q ++ ) {
vertx . push ( this . points [ q ] . x + this . center . x ) ;
}
let verty = new Array ( ) ;
for ( let w = 0 ; w < this . points . length ; w ++ ) {
verty . push ( this . points [ w ] . y + this . center . y ) ;
}
let i ,
j = 0 ;
let c = false ;
for ( i = 0 , j = nvert - 1 ; i < nvert ; j = i ++ ) {
if (
verty [ i ] > testy != verty [ j ] > testy &&
testx <
( vertx [ j ] - vertx [ i ] ) *
( testy - verty [ i ] ) /
( verty [ j ] - verty [ i ] ) +
vertx [ i ]
)
c = ! c ;
}
return c
}
multiplyScalar ( scale ) {
let center = Points . multiplyScalar ( this . center , scale ) ;
let clone = new Polygon ( center ) ;
for ( let p of this . points ) {
clone . addPoint ( Points . multiplyScalar ( p , scale ) ) ;
}
return clone
}
/ *
* To detect intersection with another Polygon object , this
* function uses the Separating Axis Theorem . It returns false
* if there is no intersection , or an object if there is . The object
* contains 2 fields , overlap and axis . Moving the polygon by overlap
* on axis will get the polygons out of intersection .
* /
intersectsWith ( other ) {
let axis = { x : 0 , y : 0 } ;
let tmp , minA , maxA , minB , maxB ;
let side , i ;
let smallest = null ;
let overlap = 99999999 ;
/* test polygon A's sides */
for ( side = 0 ; side < this . getNumberOfSides ( ) ; side ++ ) {
/* get the axis that we will project onto */
if ( side == 0 ) {
axis . x =
this . points [ this . getNumberOfSides ( ) - 1 ] . y -
this . points [ 0 ] . y ;
axis . y =
this . points [ 0 ] . x -
this . points [ this . getNumberOfSides ( ) - 1 ] . x ;
} else {
axis . x = this . points [ side - 1 ] . y - this . points [ side ] . y ;
axis . y = this . points [ side ] . x - this . points [ side - 1 ] . x ;
}
/* normalize the axis */
tmp = Math . sqrt ( axis . x * axis . x + axis . y * axis . y ) ;
axis . x /= tmp ;
axis . y /= tmp ;
/* project polygon A onto axis to determine the min/max */
minA = maxA = this . points [ 0 ] . x * axis . x + this . points [ 0 ] . y * axis . y ;
for ( i = 1 ; i < this . getNumberOfSides ( ) ; i ++ ) {
tmp = this . points [ i ] . x * axis . x + this . points [ i ] . y * axis . y ;
if ( tmp > maxA ) maxA = tmp ;
else if ( tmp < minA ) minA = tmp ;
}
/* correct for offset */
tmp = this . center . x * axis . x + this . center . y * axis . y ;
minA += tmp ;
maxA += tmp ;
/* project polygon B onto axis to determine the min/max */
minB = maxB =
other . points [ 0 ] . x * axis . x + other . points [ 0 ] . y * axis . y ;
for ( i = 1 ; i < other . getNumberOfSides ( ) ; i ++ ) {
tmp = other . points [ i ] . x * axis . x + other . points [ i ] . y * axis . y ;
if ( tmp > maxB ) maxB = tmp ;
else if ( tmp < minB ) minB = tmp ;
}
/* correct for offset */
tmp = other . center . x * axis . x + other . center . y * axis . y ;
minB += tmp ;
maxB += tmp ;
/* test if lines intersect, if not, return false */
if ( maxA < minB || minA > maxB ) {
return false
} else {
let o = maxA > maxB ? maxB - minA : maxA - minB ;
if ( o < overlap ) {
overlap = o ;
smallest = { x : axis . x , y : axis . y } ;
}
}
}
/* test polygon B's sides */
for ( side = 0 ; side < other . getNumberOfSides ( ) ; side ++ ) {
/* get the axis that we will project onto */
if ( side == 0 ) {
axis . x =
other . points [ other . getNumberOfSides ( ) - 1 ] . y -
other . points [ 0 ] . y ;
axis . y =
other . points [ 0 ] . x -
other . points [ other . getNumberOfSides ( ) - 1 ] . x ;
} else {
axis . x = other . points [ side - 1 ] . y - other . points [ side ] . y ;
axis . y = other . points [ side ] . x - other . points [ side - 1 ] . x ;
}
/* normalize the axis */
tmp = Math . sqrt ( axis . x * axis . x + axis . y * axis . y ) ;
axis . x /= tmp ;
axis . y /= tmp ;
/* project polygon A onto axis to determine the min/max */
minA = maxA = this . points [ 0 ] . x * axis . x + this . points [ 0 ] . y * axis . y ;
for ( i = 1 ; i < this . getNumberOfSides ( ) ; i ++ ) {
tmp = this . points [ i ] . x * axis . x + this . points [ i ] . y * axis . y ;
if ( tmp > maxA ) maxA = tmp ;
else if ( tmp < minA ) minA = tmp ;
}
/* correct for offset */
tmp = this . center . x * axis . x + this . center . y * axis . y ;
minA += tmp ;
maxA += tmp ;
/* project polygon B onto axis to determine the min/max */
minB = maxB =
other . points [ 0 ] . x * axis . x + other . points [ 0 ] . y * axis . y ;
for ( i = 1 ; i < other . getNumberOfSides ( ) ; i ++ ) {
tmp = other . points [ i ] . x * axis . x + other . points [ i ] . y * axis . y ;
if ( tmp > maxB ) maxB = tmp ;
else if ( tmp < minB ) minB = tmp ;
}
/* correct for offset */
tmp = other . center . x * axis . x + other . center . y * axis . y ;
minB += tmp ;
maxB += tmp ;
/* test if lines intersect, if not, return false */
if ( maxA < minB || minA > maxB ) {
return false
} else {
let o = maxA > maxB ? maxB - minA : maxA - minB ;
if ( o < overlap ) {
overlap = o ;
smallest = { x : axis . x , y : axis . y } ;
}
}
}
return { overlap : overlap + 0.001 , axis : smallest }
}
static fromPoints ( points ) {
let min = { x : Number . MAX _VALUE , y : Number . MAX _VALUE } ;
let max = { x : Number . MIN _VALUE , y : Number . MIN _VALUE } ;
for ( let p of points ) {
min . x = Math . min ( p . x , min . x ) ;
max . x = Math . max ( p . x , max . x ) ;
min . y = Math . min ( p . y , min . y ) ;
max . y = Math . max ( p . y , max . y ) ;
}
let center = Points . mean ( min , max ) ;
let polygon = new Polygon ( center ) ;
for ( let p of points ) {
polygon . addAbsolutePoint ( p ) ;
}
return polygon
}
}
/ * *
* Util functions to deal with DOMRects .
* /
class Rect {
/ * *
* Test if a given point is contained by the provided Rect .
*
* @ static
* @ param { DOMRect } rect - Rectangle to check the collision with .
* @ param { Point } point - Point that should be tested .
* @ returns { boolean } - True if point is inside of rect , otherwise false .
* @ memberof Rect
* /
static contains ( rect , point ) {
return ( point . x > rect . left &&
point . x < rect . x + rect . right
&& point . y > rect . top && point . y < rect . bottom )
}
/ * *
* Returns the position of an rect as point object .
*
* @ static
* @ param { Rect } rect - The rectangle we want to get the position from .
* @ returns { Point } - Returns the position as Point .
* @ memberof Rect
* /
static getPosition ( rect ) {
return { x : rect . x , y : rect . y }
}
}
/** String utility functions */
class Strings {
static toUpperCaseFirstChar ( str ) {
return str . substr ( 0 , 1 ) . toUpperCase ( ) + str . substr ( 1 )
}
static toLowerCaseFirstChar ( str ) {
return str . substr ( 0 , 1 ) . toLowerCase ( ) + str . substr ( 1 )
}
static toUpperCaseEachWord ( str , delim = ' ' ) {
return str . split ( delim ) . map ( ( v ) => v . toUpperCaseFirstChar ( ) ) . join ( delim )
}
static toLowerCaseEachWord ( str , delim = ' ' ) {
return str . split ( delim ) . map ( ( v ) => v . toLowerCaseFirstChar ( ) ) . join ( delim )
}
}
2019-07-02 09:38:19 +02:00
/* eslint-disable no-undef */
/* eslint-disable no-console */
/* eslint-disable no-unused-vars */
2019-05-24 11:02:44 +02:00
let ipc = null ;
2019-07-02 09:38:19 +02:00
let logMessages = new Set ( ) ;
let logHandlers = {
log : console . log ,
warn : console . warn ,
error : console . error
} ;
2019-05-24 10:29:10 +02:00
try {
ipc = require ( 'electron' ) . ipcRenderer ;
2019-07-02 09:38:19 +02:00
logHandlers . log = message => ipc . send ( 'log' , message ) ;
logHandlers . warn = message => ipc . send ( 'warn' , message ) ;
logHandlers . error = message => ipc . send ( 'error' , message ) ;
} catch ( e ) {
console . log ( 'Cannot use electron logging.' ) ;
}
2019-05-24 10:29:10 +02:00
2019-05-24 09:53:27 +02:00
/ * * B a s i c c l a s s f o r a p p s p e c i f i c l o g g i n g r e q u i r e m e n t s .
* Can be used to implement persistent logging in electron apps .
2019-07-02 09:38:19 +02:00
* Uses a logMessage cache to prevent error overflows . This is
* needed since errors may occur very frequently
* ( e . g . display update loops at 60 fps , programmatic loops , ... ) .
*
* The logging handlers can be overwritten by calling the static
* setup method .
2019-05-24 09:53:27 +02:00
* /
class Logging {
/ * * S t a t i c l o g f u n c t i o n .
* @ param { * } message
* /
2019-05-24 10:29:10 +02:00
static log ( message ) {
2019-07-02 09:38:19 +02:00
logHandlers . log ( message ) ;
}
/ * *
* Static warn function .
* Emits each warning only once per session .
* @ param { * } message
* /
static warn ( message ) {
if ( ! logMessages . has ( message ) ) {
logMessages . add ( message ) ;
logHandlers . warn ( message ) ;
}
}
2019-05-24 10:29:10 +02:00
2019-07-02 09:38:19 +02:00
/ * *
* Static error function .
* Emits each error message only once per session .
* @ param { * } message
* /
static error ( message ) {
if ( ! logMessages . has ( message ) ) {
logMessages . add ( message ) ;
logHandlers . error ( message ) ;
2019-05-24 10:29:10 +02:00
}
2019-05-24 09:53:27 +02:00
}
2019-07-02 09:38:19 +02:00
static setup ( { log = console . log , warn = console . warn , error = console . error } = { } ) {
logHandlers . log = log ;
logHandlers . warn = warn ;
logHandlers . error = error ;
}
2019-05-24 09:53:27 +02:00
}
2019-03-22 12:54:57 +01:00
/* globals Hammer, propagating */
/ * * I n t e r a c t i o n p a t t e r n s
See interaction . html for explanation
* /
class IInteractionTarget extends Interface {
capture ( event ) {
return typeof true
}
onStart ( event , interaction ) { }
onMove ( event , interaction ) { }
onEnd ( event , interaction ) { }
onMouseWheel ( event ) { }
}
class IInteractionMapperTarget extends Interface {
capture ( event ) {
return typeof true
}
findTarget ( event , local , global ) {
return IInteractionTarget
}
}
class PointMap extends MapProxy {
// Collects touch points, mouse coordinates, etc. as key value pairs.
// Keys are pointer and touch ids, the special "mouse" key.
// Values are points, i.e. all objects with numeric x and y properties.
constructor ( points = { } ) {
super ( ) ;
for ( let key in points ) {
this . set ( key , points [ key ] ) ;
}
}
toString ( ) {
let points = [ ] ;
for ( let key of this . keys ( ) ) {
let value = this . get ( key ) ;
points . push ( ` ${ key } :{x: ${ value . x } , y: ${ value . y } } ` ) ;
}
let attrs = points . join ( ', ' ) ;
return ` [PointMap ${ attrs } ] `
}
clone ( ) {
let result = new PointMap ( ) ;
for ( let key of this . keys ( ) ) {
let value = this . get ( key ) ;
result . set ( key , { x : value . x , y : value . y } ) ;
}
return result
}
keyOf ( value ) {
for ( let key of this . keys ( ) ) {
let p = this . get ( key ) ;
if ( p . x == value . x && p . y == value . y ) {
return key
}
}
return null
}
firstKey ( ) {
for ( let key of this . keys ( ) ) {
return key
}
return null
}
first ( ) {
for ( let key of this . keys ( ) ) {
return this . get ( key )
}
return null
}
farthests ( ) {
if ( this . size == 0 ) {
return null
}
let pairs = [ ] ;
for ( let key of this . keys ( ) ) {
let p = this . get ( key ) ;
p . key = key ;
for ( let k of this . keys ( ) ) {
let q = this . get ( k ) ;
q . key = k ;
pairs . push ( [ p , q ] ) ;
}
}
let sorted = pairs . sort ( ( a , b ) => {
return Points . distance ( b [ 0 ] , b [ 1 ] ) - Points . distance ( a [ 0 ] , a [ 1 ] )
} ) ;
return sorted [ 0 ]
}
mean ( ) {
if ( this . size == 0 ) {
return null
}
let x = 0.0 ,
y = 0.0 ;
for ( let p of this . values ( ) ) {
x += p . x ;
y += p . y ;
}
return { x : x / this . size , y : y / this . size }
}
}
class InteractionDelta {
2019-05-31 10:15:50 +02:00
/ * *
* Creates an instance of InteractionDelta .
* @ param { * } x
* @ param { * } y
* @ param { * } zoom
* @ param { * } rotate
* @ param { * } about
* @ param { * } number - number of involved pointer
* @ memberof InteractionDelta
* /
constructor ( x , y , zoom , rotate , about , number ) {
2019-03-22 12:54:57 +01:00
this . x = x ;
this . y = y ;
this . zoom = zoom ;
this . rotate = rotate ;
this . about = about ;
2019-05-31 10:15:50 +02:00
this . number = number ;
2019-03-22 12:54:57 +01:00
}
toString ( ) {
let values = [ ] ;
for ( let key of Object . keys ( this ) ) {
let value = this [ key ] ;
if ( key == 'about' ) {
values . push ( ` ${ key } :{x: ${ value . x } , y: ${ value . y } } ` ) ;
} else {
values . push ( ` ${ key } : ${ value } ` ) ;
}
}
let attrs = values . join ( ', ' ) ;
return ` [InteractionDelta ${ attrs } ] `
}
}
class InteractionPoints {
constructor ( parent = null ) {
this . parent = parent ;
this . current = new PointMap ( ) ;
this . previous = new PointMap ( ) ;
this . start = new PointMap ( ) ;
this . ended = new PointMap ( ) ;
this . timestamps = new Map ( ) ;
}
moved ( key ) {
let current = this . current . get ( key ) ;
let previous = this . previous . get ( key ) ;
return Points . subtract ( current , previous )
}
move ( ) {
let current = this . current . mean ( ) ;
let previous = this . previous . mean ( ) ;
return Points . subtract ( current , previous )
}
/ * *
* Computes the delta between previous and current angles . Corrects
* value that are larger than 45 °
* @ param { * } a
* @ param { * } b
* @ returns delta
* /
diffAngle ( a , b ) {
let alpha = Math . atan2 ( Math . sin ( a - b ) , Math . cos ( a - b ) ) ;
if ( Math . abs ( alpha ) > Math . PI / 4 ) {
alpha -= Math . PI ;
}
return alpha
}
/ * *
* Computes the delta between interaction points at t and t + 1.
*
* @ returns InteractionDelta
* @ memberof InteractionPoints
* /
delta ( ) {
let csize = this . current . size ;
let psize = this . previous . size ;
if ( csize >= 2 && csize == psize ) {
// Reduce to the two farthests points
let current = this . current . farthests ( ) ;
let c1 = current [ 0 ] ;
let c2 = current [ 1 ] ;
let p1 = this . previous . get ( c1 . key ) ;
let p2 = this . previous . get ( c2 . key ) ;
let d1 = Points . subtract ( c1 , p1 ) ;
let d2 = Points . subtract ( c2 , p2 ) ;
let cm = Points . mean ( c1 , c2 ) ;
2019-05-31 10:15:50 +02:00
// Using the mean leads to jumps between time slices with 3 and 2 fingers
2019-03-22 12:54:57 +01:00
// We use the mean of deltas instead
2019-05-31 10:15:50 +02:00
let delta = Points . mean ( d1 , d2 ) ;
2019-03-22 12:54:57 +01:00
let zoom = 1.0 ;
let distance1 = Points . distance ( p1 , p2 ) ;
let distance2 = Points . distance ( c1 , c2 ) ;
if ( distance1 != 0 && distance2 != 0 ) {
zoom = distance2 / distance1 ;
}
let currentAngle = Points . angle ( c1 , c2 ) ;
let previousAngle = Points . angle ( p1 , p2 ) ;
let alpha = this . diffAngle ( currentAngle , previousAngle ) ;
2019-05-31 10:15:50 +02:00
return new InteractionDelta ( delta . x , delta . y , zoom , alpha , cm , csize )
2019-03-22 12:54:57 +01:00
} else if ( csize == 1 && psize == 1 && this . current . firstKey ( ) == this . previous . firstKey ( ) ) {
2019-05-31 10:15:50 +02:00
// We need to ensure that the keys are the same, since single points with different keys
// can jump
2019-03-22 12:54:57 +01:00
let current = this . current . first ( ) ;
let previous = this . previous . first ( ) ;
let delta = Points . subtract ( current , previous ) ;
2019-05-31 10:15:50 +02:00
return new InteractionDelta ( delta . x , delta . y , 1.0 , 0.0 , current , csize )
2019-03-22 12:54:57 +01:00
}
return null
}
started ( key , point ) {
this . current . set ( key , point ) ;
this . start . set ( key , point ) ;
this . previous . set ( key , point ) ;
this . timestamps . set ( key , performance . now ( ) ) ;
}
update ( key , point ) {
// Returns true iff the key is new
this . current . set ( key , point ) ;
if ( ! this . start . has ( key ) ) {
this . start . set ( key , point ) ;
this . previous . set ( key , point ) ;
this . timestamps . set ( key , performance . now ( ) ) ;
return true
}
return false
}
updatePrevious ( ) {
for ( let key of this . current . keys ( ) ) {
this . previous . set ( key , this . current . get ( key ) ) ;
}
}
stop ( key , point ) {
if ( this . current . has ( key ) ) {
this . current . delete ( key ) ;
this . previous . delete ( key ) ;
this . ended . set ( key , point ) ;
}
}
finish ( key , point ) {
this . current . delete ( key ) ;
this . previous . delete ( key ) ;
this . start . delete ( key ) ;
this . timestamps . delete ( key ) ;
this . ended . delete ( key ) ;
}
isFinished ( ) {
return this . current . size == 0
}
isNoLongerTwoFinger ( ) {
return this . previous . size > 1 && this . current . size < 2
}
isTap ( key ) {
return this . parent . isTap ( key )
}
isDoubleTap ( key ) {
return this . parent . isDoubleTap ( key )
}
isLongPress ( key ) {
return this . parent . isLongPress ( key )
}
}
class Interaction extends InteractionPoints {
constructor ( tapDistance = 10 , tapDuration = 250.0 , longPressTime = 500.0 ) {
super ( ) ;
this . tapDistance = tapDistance ;
this . tapCounts = new Map ( ) ;
this . tapPositions = new Map ( ) ;
this . tapTimestamps = new Map ( ) ;
this . tapDuration = tapDuration ;
this . longPressTime = longPressTime ;
this . targets = new Map ( ) ;
this . subInteractions = new Map ( ) ; // target:Object : InteractionPoints
}
stop ( key , point ) {
super . stop ( key , point ) ;
for ( let points of this . subInteractions . values ( ) ) {
points . stop ( key , point ) ;
}
}
addTarget ( key , target ) {
this . targets . set ( key , target ) ;
this . subInteractions . set ( target , new InteractionPoints ( this ) ) ;
}
removeTarget ( key ) {
let target = this . targets . get ( key ) ;
this . targets . delete ( key ) ;
// Only remove target if no keys are refering to the target
let remove = true ;
for ( let t of this . targets . values ( ) ) {
if ( target === t ) {
remove = false ;
}
}
if ( remove ) {
this . subInteractions . delete ( target ) ;
}
}
finish ( key , point ) {
super . finish ( key , point ) ;
this . removeTarget ( key ) ;
}
mapInteraction ( points , aspects , mappingFunc ) {
// Map centrally registered points to target interactions
// Returns an array of [target, updated subInteraction] pairs
let result = new Map ( ) ;
for ( let key in points ) {
if ( this . targets . has ( key ) ) {
let target = this . targets . get ( key ) ;
if ( this . subInteractions . has ( target ) ) {
let interaction = this . subInteractions . get ( target ) ;
for ( let aspect of aspects ) {
let pointMap = this [ aspect ] ;
let point = pointMap . get ( key ) ;
let mapped = mappingFunc ( point , target ) ;
interaction [ aspect ] . set ( key , mapped ) ;
}
result . set ( target , interaction ) ;
}
}
}
return result
}
registerTap ( key , point ) {
if ( this . tapCounts . has ( key ) ) {
let count = this . tapCounts . get ( key ) ;
2019-05-24 11:00:29 +02:00
this . tapCounts . set ( key , count + 1 ) ;
2019-03-22 12:54:57 +01:00
}
else {
this . tapCounts . set ( key , 1 ) ;
}
this . tapPositions . set ( key , point ) ;
this . tapTimestamps . set ( key , performance . now ( ) ) ;
}
unregisterTap ( key ) {
this . tapCounts . delete ( key ) ;
this . tapPositions . delete ( key ) ;
this . tapTimestamps . delete ( key ) ;
}
isTap ( key ) {
let ended = this . ended . get ( key ) ;
let start = this . start . get ( key ) ;
if (
start &&
ended &&
Points . distance ( ended , start ) < this . tapDistance
) {
let t1 = this . timestamps . get ( key ) ;
let tookLong = performance . now ( ) > t1 + this . longPressTime ;
if ( tookLong ) {
return false
}
return true
}
return false
}
isDoubleTap ( key ) {
let ended = this . ended . get ( key ) ;
if ( this . tapCounts . has ( key ) && this . tapCounts . get ( key ) > 2 ) {
this . unregisterTap ( key ) ;
}
if ( this . tapPositions . has ( key ) ) {
let pos = this . tapPositions . get ( key ) ;
if ( Points . distance ( ended , pos ) > this . tapDistance ) {
this . unregisterTap ( key ) ;
}
}
if ( this . tapTimestamps . has ( key ) && performance . now ( ) > this . tapTimestamps . get ( key ) + this . tapDuration ) {
//console.log("tap too long")
this . unregisterTap ( key ) ;
}
let result = false ;
if ( this . isTap ( key ) ) {
2019-05-24 11:00:29 +02:00
2019-03-22 12:54:57 +01:00
this . registerTap ( key , ended ) ;
result = this . tapCounts . get ( key ) == 2 ;
}
else {
this . unregisterTap ( key ) ;
}
//console.log("isDoubleTap", this.tapCounts.get(key), result)
return result
}
isAnyTap ( ) {
for ( let key of this . ended . keys ( ) ) {
if ( this . isTap ( key ) ) return true
}
return false
}
isLongPress ( key ) {
let ended = this . ended . get ( key ) ;
let start = this . start . get ( key ) ;
if (
start &&
ended &&
Points . distance ( ended , start ) < this . tapDistance
) {
let t1 = this . timestamps . get ( key ) ;
let tookLong = performance . now ( ) > t1 + this . longPressTime ;
if ( tookLong ) {
return true
}
return false
}
return false
}
isAnyLongPress ( ) {
for ( let key of this . ended . keys ( ) ) {
if ( this . isLongPress ( key ) ) return true
}
return false
}
isStylus ( key ) {
return key === 'stylus'
}
}
/ * *
* This class implements the main delegate functionality : All necessary event handlers are registered for the
* given element . Uses PointerEvents if available or TouchEvents on iOS . The fallback is on mouse events .
* Collects the events if the interaction target captures the start event ( i . e . declares that
* the target wants the start event as well as all following move and end evcents . )
*
* @ export
* @ class InteractionDelegate
* /
class InteractionDelegate {
// Long press: http://stackoverflow.com/questions/1930895/how-long-is-the-event-onlongpress-in-the-android
// Stylus support: https://w3c.github.io/touch-events/
/ * *
* Creates an instance of InteractionDelegate .
* @ param { any } element
* @ param { any } target
* @ param { any } [ { mouseWheelElement = null , useCapture = true , capturePointerEvents = true , debug = false } = { } ]
* @ memberof InteractionDelegate
* /
constructor (
element ,
target ,
{ mouseWheelElement = null , useCapture = true , capturePointerEvents = true , cancelOnWindowOut = true , debug = false } = { }
) {
this . debug = debug ;
this . interaction = new Interaction ( ) ;
this . element = element ;
this . mouseWheelElement = mouseWheelElement || element ;
this . target = target ;
this . useCapture = useCapture ;
this . capturePointerEvents = capturePointerEvents ;
this . cancelOnWindowOut = cancelOnWindowOut ;
this . setupInteraction ( ) ;
}
setupInteraction ( ) {
if ( this . debug ) {
let error = this . targetInterface . implementationError (
this . target . constructor
) ;
if ( error != null ) {
throw new Error ( 'Expected IInteractionTarget: ' + error )
}
}
this . setupTouchInteraction ( ) ;
this . setupMouseWheelInteraction ( ) ;
}
get targetInterface ( ) {
return IInteractionTarget
}
setupTouchInteraction ( ) {
let element = this . element ;
let useCapture = this . useCapture ;
if ( window . PointerEvent ) {
if ( this . debug ) console . log ( 'Pointer API' + window . PointerEvent ) ;
element . addEventListener (
'pointerdown' ,
e => {
if ( this . debug ) console . log ( 'pointerdown' , e . pointerId ) ;
if ( this . capture ( e ) ) {
if ( this . capturePointerEvents ) {
try {
element . setPointerCapture ( e . pointerId ) ;
} catch ( e ) { }
}
this . onStart ( e ) ;
}
} ,
useCapture
) ;
element . addEventListener (
'pointermove' ,
e => {
if ( this . debug ) console . log ( 'pointermove' , e . pointerId , e . pointerType ) ;
if (
e . pointerType == 'touch' ||
( e . pointerType == 'mouse' && Events . isPointerDown ( e ) )
) {
// this.capture(e) &&
if ( this . debug )
console . log ( 'pointermove captured' , e . pointerId ) ;
this . onMove ( e ) ;
}
} ,
useCapture
) ;
element . addEventListener (
'pointerup' ,
e => {
2019-05-24 11:00:29 +02:00
if ( this . debug ) console . log ( 'pointerup' , e . pointerId , e . pointerType ) ;
2019-03-22 12:54:57 +01:00
this . onEnd ( e ) ;
if ( this . capturePointerEvents ) {
try {
element . releasePointerCapture ( e . pointerId ) ;
} catch ( e ) { }
}
} ,
useCapture
) ;
element . addEventListener (
'pointercancel' ,
e => {
2019-05-24 11:00:29 +02:00
if ( this . debug ) console . log ( 'pointercancel' , e . pointerId , e . pointerType ) ;
2019-03-22 12:54:57 +01:00
this . onEnd ( e ) ;
if ( this . capturePointerEvents )
element . releasePointerCapture ( e . pointerId ) ;
} ,
useCapture
) ;
if ( ! this . capturePointerEvents ) {
element . addEventListener (
'pointerleave' ,
e => {
2019-05-24 11:00:29 +02:00
if ( this . debug ) console . log ( 'pointerleave' , e . pointerId , e . pointerType ) ;
2019-03-22 12:54:57 +01:00
if ( e . target == element ) this . onEnd ( e ) ;
} ,
useCapture
) ;
}
if ( ! this . capturePointerEvents ) {
element . addEventListener (
'pointerout' ,
e => {
2019-05-24 11:00:29 +02:00
if ( this . debug ) console . log ( 'pointerout' , e . pointerId , e . pointerType ) ;
2019-03-22 12:54:57 +01:00
if ( e . target == element ) this . onEnd ( e ) ;
} ,
useCapture
) ;
}
if ( this . cancelOnWindowOut ) {
window . addEventListener (
'pointerout' ,
e => {
2019-05-24 11:00:29 +02:00
if ( this . debug ) console . log ( 'pointerout' , e . pointerId , e . pointerType , e . target ) ;
2019-03-22 12:54:57 +01:00
if ( e . target == element ) {
2019-05-24 11:00:29 +02:00
this . onEnd ( e ) ;
2019-03-22 12:54:57 +01:00
}
} ,
useCapture ) ;
}
} else if ( window . TouchEvent ) {
if ( this . debug ) console . log ( 'Touch API' ) ;
element . addEventListener (
'touchstart' ,
e => {
if ( this . debug )
console . log ( 'touchstart' , this . touchPoints ( e ) ) ;
if ( this . capture ( e ) ) {
for ( let touch of e . changedTouches ) {
this . onStart ( touch ) ;
}
}
} ,
useCapture
) ;
element . addEventListener (
'touchmove' ,
e => {
if ( this . debug )
console . log ( 'touchmove' , this . touchPoints ( e ) , e ) ;
for ( let touch of e . changedTouches ) {
this . onMove ( touch ) ;
}
for ( let touch of e . targetTouches ) {
this . onMove ( touch ) ;
}
} ,
useCapture
) ;
element . addEventListener (
'touchend' ,
e => {
if ( this . debug ) console . log ( 'touchend' , this . touchPoints ( e ) ) ;
for ( let touch of e . changedTouches ) {
this . onEnd ( touch ) ;
}
} ,
useCapture
) ;
element . addEventListener (
'touchcancel' ,
e => {
if ( this . debug )
console . log (
'touchcancel' ,
e . targetTouches . length ,
e . changedTouches . length
) ;
for ( let touch of e . changedTouches ) {
this . onEnd ( touch ) ;
}
} ,
useCapture
) ;
} else {
if ( this . debug ) console . log ( 'Mouse API' ) ;
element . addEventListener (
'mousedown' ,
e => {
if ( this . debug ) console . log ( 'mousedown' , e ) ;
if ( this . capture ( e ) ) {
this . onStart ( e ) ;
}
} ,
useCapture
) ;
element . addEventListener (
'mousemove' ,
e => {
// Dow we only use move events if the mouse is down?
// HOver effects have to be implemented by other means
// && Events.isMouseDown(e))
if ( Events . isMouseDown ( e ) ) {
if ( this . debug )
console . log ( 'mousemove' , e ) ;
this . onMove ( e ) ;
}
} ,
useCapture
) ;
element . addEventListener (
'mouseup' ,
e => {
if ( this . debug ) console . log ( 'mouseup' , e ) ;
this . onEnd ( e ) ;
} ,
true
) ;
if ( ! this . capturePointerEvents ) {
element . addEventListener (
'mouseout' ,
e => {
if ( e . target == element ) {
this . onEnd ( e ) ;
console . warn ( "Shouldn't happen: mouseout ends interaction" ) ;
}
} ,
useCapture
) ;
}
if ( this . cancelOnWindowOut ) {
window . addEventListener (
'mouseout' ,
e => {
if ( e . target == element ) {
this . onEnd ( e ) ;
}
} ,
useCapture ) ;
}
}
}
isDescendant ( parent , child ) {
if ( parent == child ) return true
let node = child . parentNode ;
while ( node != null ) {
if ( node == parent ) {
return true
}
node = node . parentNode ;
}
return false
}
touchPoints ( event ) {
let result = [ ] ;
for ( let touch of event . changedTouches ) {
result . push ( this . extractPoint ( touch ) ) ;
}
return result
}
setupMouseWheelInteraction ( ) {
this . mouseWheelElement . addEventListener (
'mousewheel' ,
this . onMouseWheel . bind ( this ) ,
true
) ;
this . mouseWheelElement . addEventListener (
'DOMMouseScroll' ,
this . onMouseWheel . bind ( this ) ,
true
) ;
}
onMouseWheel ( event ) {
if ( this . capture ( event ) && this . target . onMouseWheel ) {
this . target . onMouseWheel ( event ) ;
}
}
onStart ( event ) {
let extracted = this . extractPoint ( event ) ;
this . startInteraction ( event , extracted ) ;
this . target . onStart ( event , this . interaction ) ;
}
onMove ( event ) {
let extracted = this . extractPoint ( event , 'all' ) ;
this . updateInteraction ( event , extracted ) ;
this . target . onMove ( event , this . interaction ) ;
this . interaction . updatePrevious ( ) ;
}
onEnd ( event ) {
let extracted = this . extractPoint ( event , 'changedTouches' ) ;
this . endInteraction ( event , extracted ) ;
this . target . onEnd ( event , this . interaction ) ;
this . finishInteraction ( event , extracted ) ;
}
/ * *
* Asks the target whether the event should be captured
*
* @ param { any } event
* @ returns { bool }
* @ memberof InteractionDelegate
* /
capture ( event ) {
if ( Events . isCaptured ( event ) ) {
return false
}
let captured = this . target . capture ( event ) ;
return captured
}
getPosition ( event ) {
return { x : event . clientX , y : event . clientY }
}
extractPoint ( event , touchEventKey = 'all' ) {
// 'targetTouches'
let result = { } ;
switch ( event . constructor . name ) {
case 'MouseEvent' :
let buttons = event . buttons || event . which ;
if ( buttons ) result [ 'mouse' ] = this . getPosition ( event ) ;
break
case 'PointerEvent' :
result [ event . pointerId . toString ( ) ] = this . getPosition ( event ) ;
break
case 'Touch' :
let id =
event . touchType === 'stylus'
? 'stylus'
: event . identifier . toString ( ) ;
result [ id ] = this . getPosition ( event ) ;
break
// case 'TouchEvent':
// // Needs to be observed: Perhaps changedTouches are all we need. If so
// // we can remove the touchEventKey default parameter
// if (touchEventKey == 'all') {
// for(let t of event.targetTouches) {
// result[t.identifier.toString()] = this.getPosition(t)
// }
// for(let t of event.changedTouches) {
// result[t.identifier.toString()] = this.getPosition(t)
// }
// }
// else {
// for(let t of event.changedTouches) {
// result[t.identifier.toString()] = this.getPosition(t)
// }
// }
// break
default :
break
}
return result
}
interactionStarted ( event , key , point ) {
// Callback: can be overwritten
}
interactionEnded ( event , key , point ) {
// Callback: can be overwritten
}
interactionFinished ( event , key , point ) { }
startInteraction ( event , extracted ) {
for ( let key in extracted ) {
let point = extracted [ key ] ;
this . interaction . started ( key , point ) ;
this . interactionStarted ( event , key , point ) ;
}
}
updateInteraction ( event , extracted ) {
for ( let key in extracted ) {
let point = extracted [ key ] ;
let updated = this . interaction . update ( key , point ) ;
if ( updated ) {
console . warn ( "new pointer in updateInteraction shouldn't happen" , key ) ;
this . interactionStarted ( event , key , point ) ;
}
}
}
endInteraction ( event , ended ) {
for ( let key in ended ) {
let point = ended [ key ] ;
this . interaction . stop ( key , point ) ;
this . interactionEnded ( event , key , point ) ;
}
}
finishInteraction ( event , ended ) {
for ( let key in ended ) {
let point = ended [ key ] ;
this . interaction . finish ( key , point ) ;
this . interactionFinished ( event , key , point ) ;
}
}
}
/ * *
* A special InteractionDelegate that maps events to specific parts of
* the interaction target . The InteractionTarget must implement a findTarget
* method that returns an object implementing the IInteractionTarget interface .
*
* If the InteractionTarget also implements a mapPositionToPoint method this
* is used to map the points to the local coordinate space of the the target .
*
* This makes it easier to lookup elements and relate events to local
* positions .
*
* @ export
* @ class InteractionMapper
* @ extends { InteractionDelegate }
* /
class InteractionMapper$1 extends InteractionDelegate {
constructor (
element ,
target ,
2019-05-24 09:21:02 +02:00
{ tapDistance = 10 , longPressTime = 500.0 , useCapture = true , mouseWheelElement = null , logInteractionsAbove = 12 } = { }
2019-03-22 12:54:57 +01:00
) {
super ( element , target , { tapDistance , useCapture , longPressTime , mouseWheelElement } ) ;
2019-05-24 09:21:02 +02:00
this . logInteractionsAbove = logInteractionsAbove ;
2019-03-22 12:54:57 +01:00
}
get targetInterface ( ) {
return IInteractionMapperTarget
}
mapPositionToPoint ( point , element = null ) {
if ( this . target . mapPositionToPoint ) {
return this . target . mapPositionToPoint ( point , element )
}
return point
}
interactionStarted ( event , key , point ) {
if ( this . target . findTarget ) {
let local = this . mapPositionToPoint ( point ) ;
let found = this . target . findTarget ( event , local , point ) ;
if ( found != null ) {
this . interaction . addTarget ( key , found ) ;
}
}
2019-05-24 09:21:02 +02:00
let size = this . interaction . current . size ;
let limit = this . logInteractionsAbove ;
if ( size > limit ) {
2019-05-24 09:53:27 +02:00
Logging . log ( ` Number of interactions ${ size } exceeds ${ limit } ` ) ;
2019-05-24 09:21:02 +02:00
}
2019-03-22 12:54:57 +01:00
}
onMouseWheel ( event ) {
if ( this . capture ( event ) ) {
if ( this . target . findTarget ) {
let point = this . getPosition ( event ) ;
let local = this . mapPositionToPoint ( point ) ;
let found = this . target . findTarget ( event , local , point ) ;
if ( found != null && found . onMouseWheel ) {
found . onMouseWheel ( event ) ;
return
}
}
if ( this . target . onMouseWheel ) {
this . target . onMouseWheel ( event ) ;
}
}
}
onStart ( event ) {
let extracted = this . extractPoint ( event ) ;
this . startInteraction ( event , extracted ) ;
let mapped = this . interaction . mapInteraction (
extracted ,
[ 'current' , 'start' ] ,
this . mapPositionToPoint . bind ( this )
) ;
for ( let [ target , interaction ] of mapped . entries ( ) ) {
target . onStart ( event , interaction ) ;
}
}
onMove ( event ) {
let extracted = this . extractPoint ( event , 'all' ) ;
this . updateInteraction ( event , extracted ) ;
let mapped = this . interaction . mapInteraction (
extracted ,
[ 'current' , 'previous' ] ,
this . mapPositionToPoint . bind ( this )
) ;
for ( let [ target , interaction ] of mapped . entries ( ) ) {
target . onMove ( event , interaction ) ;
interaction . updatePrevious ( ) ;
}
this . interaction . updatePrevious ( ) ;
}
onEnd ( event ) {
let extracted = this . extractPoint ( event , 'changedTouches' ) ;
this . endInteraction ( event , extracted ) ;
let mapped = this . interaction . mapInteraction (
extracted ,
[ 'ended' ] ,
this . mapPositionToPoint . bind ( this )
) ;
for ( let [ target , interaction ] of mapped . entries ( ) ) {
target . onEnd ( event , interaction ) ;
}
this . finishInteraction ( event , extracted ) ;
}
/ * *
*
*
* @ static
* @ param { string | array } types - An event type , an array of event types or event types seperated by a space sign . The following
* events are possible :
* pan , panstart , panmove , panend , pancancel , panleft , panright , panup , pandown
* pinch , pinchstart , pinchmove , pinchend , pinchcancel , pinchin , pinchout
* press , pressup
* rotate , rotatestart , rotatemove , rotateend , rotatecancel
* swipe , swipeleft , swiperight , swipeup , swipedown
* tap
* @ param { HTMLElement | HTMLElement [ ] } elements - An HTML element or an array of HTML elements .
* @ param { function } [ cb ] - The callback . A function which is executed after the event occurs . Receives the event object as the
* first paramter
* @ param { object } [ opts ] - An options object . See the hammer documentation for more details .
* /
static on ( types , elements , cb , opts = { } ) {
opts = Object . assign ( { } , {
} , opts ) ;
if ( typeof Hammer === 'undefined' ) {
console . error ( 'Hammer.js not found!' ) ;
return this
}
// convert to array
types = Array . isArray ( types ) ? types : types . split ( /\s/ ) ;
if ( elements instanceof NodeList || elements instanceof HTMLCollection ) {
elements = Array . from ( elements ) ;
}
elements = Array . isArray ( elements ) ? elements : [ elements ] ;
for ( let i = 0 ; i < types . length ; i ++ ) {
const type = types [ i ] . toLowerCase ( ) ;
// list of hammer events
const useHammer = /^(tap|doubletap|press|pan|swipe|pinch|rotate).*$/ . test ( type ) ;
// if it is a hammer event
if ( useHammer ) {
for ( let j = 0 ; j < elements . length ; j ++ ) {
// if(elements[j].tagName == "svg") return false;
let hammer = new Hammer ( elements [ j ] , opts ) ;
if ( window . propagating !== 'undefined' ) {
hammer = propagating ( hammer ) ;
}
// recognizers
if ( type . startsWith ( 'pan' ) ) {
hammer . get ( 'pan' ) . set ( Object . assign ( { direction : Hammer . DIRECTION _ALL } , opts ) ) ;
} else if ( type . startsWith ( 'pinch' ) ) {
hammer . get ( 'pinch' ) . set ( Object . assign ( { enable : true } , opts ) ) ;
} else if ( type . startsWith ( 'press' ) ) {
hammer . get ( 'press' ) . set ( opts ) ;
} else if ( type . startsWith ( 'rotate' ) ) {
hammer . get ( 'rotate' ) . set ( Object . assign ( { enable : true } , opts ) ) ;
} else if ( type . startsWith ( 'swipe' ) ) {
hammer . get ( 'swipe' ) . set ( Object . assign ( { direction : Hammer . DIRECTION _ALL } , opts ) ) ;
} else if ( type . startsWith ( 'tap' ) ) {
hammer . get ( 'tap' ) . set ( opts ) ;
}
hammer . on ( type , event => {
cb ( event ) ;
} ) ;
}
} else {
for ( let j = 0 ; j < elements . length ; j ++ ) {
Hammer . on ( elements [ j ] , type , event => {
cb ( event ) ;
} ) ;
}
}
}
return this
}
}
window . InteractionMapper = InteractionMapper$1 ;
/ * * R e p o r t c a p a b i l i t i e s w i t h g u a r a n t e e d v a l u e s .
* /
class Capabilities {
/ * * R e t u r n s t h e b r o w s e r u s e r A g e n t .
@ return { string }
* /
static get userAgent ( ) {
return navigator . userAgent || 'Unknown Agent'
}
/ * * T e s t s w h e t h e r t h e a p p i s r u n n i n g o n a m o b i l e d e v i c e .
Implemented as a readonly attribute .
@ return { boolean }
* /
static get isMobile ( ) {
return ( /Mobi/ . test ( navigator . userAgent ) )
}
/ * * T e s t s w h e t h e r t h e a p p i s r u n n i n g o n a i O S d e v i c e .
Implemented as a readonly attribute .
@ return { boolean }
* /
static get isIOS ( ) {
return ( /iPad|iPhone|iPod/ . test ( navigator . userAgent ) ) && ! window . MSStream
}
/ * * T e s t s w h e t h e r t h e a p p i s r u n n i n g i n a S a f a r i e n v i r o n m e n t .
See https : //stackoverflow.com/questions/7944460/detect-safari-browser
Implemented as a readonly attribute .
@ return { boolean }
* /
static get isSafari ( ) {
return navigator . vendor && navigator . vendor . indexOf ( 'Apple' ) > - 1 && navigator . userAgent && ! navigator . userAgent . match ( 'CriOS' )
}
/ * *
* Distincts if the app is running inside electron or not .
*
2019-05-24 10:29:10 +02:00
* source : https : //github.com/cheton/is-electron
2019-03-22 12:54:57 +01:00
* /
static get isElectron ( ) {
2019-05-24 10:29:10 +02:00
// Renderer process
if ( typeof window !== 'undefined' && typeof window . process === 'object' && window . process . type === 'renderer' ) {
return true
}
// Main process
if ( typeof process !== 'undefined' && typeof process . versions === 'object' && ! ! process . versions . electron ) {
return true
}
// Detect the user agent when the `nodeIntegration` option is set to true
if ( typeof navigator === 'object' && typeof navigator . userAgent === 'string' && navigator . userAgent . indexOf ( 'Electron' ) >= 0 ) {
return true
}
return false
2019-03-22 12:54:57 +01:00
}
/ * * R e t u r n s t h e d i s p l a y r e s o l u t i o n . N e c e s s a r y f o r r e t i n a d i s p l a y s .
@ return { number }
* /
static get devicePixelRatio ( ) {
return window . devicePixelRatio || 1
}
/ * * R e t u r n s t r u e i f t h e d e v i c e i s a m u l t i - t o u c h t a b l e . T h i s m e t h o d i s c u r r e n t l y n o t u n i v e r s a l u s a b l e a n d n o t s u r e !
@ return { boolean }
* /
static get isMultiTouchTable ( ) {
return Capabilities . devicePixelRatio > 2 && Capabilities . isMobile === false && /Windows/i . test ( Capabilities . userAgent )
}
/ * * R e t u r n s t r u e i f m o u s e e v e n t s a r e s u p p o r t e d
@ return { boolean }
* /
static supportsMouseEvents ( ) {
return typeof ( window . MouseEvent ) != 'undefined'
}
/ * * R e t u r n s t r u e i f t o u c h e v e n t s a r e s u p p o r t e d
@ return { boolean }
* /
static supportsTouchEvents ( ) {
return typeof ( window . TouchEvent ) != 'undefined'
}
/ * * R e t u r n s t r u e i f p o i n t e r e v e n t s a r e s u p p o r t e d
@ return { boolean }
* /
static supportsPointerEvents ( ) {
return typeof ( window . PointerEvent ) != 'undefined'
}
/ * * R e t u r n s t r u e i f D O M t e m p l a t e s a r e s u p p o r t e d
@ return { boolean }
* /
static supportsTemplate ( ) {
return 'content' in document . createElement ( 'template' ) ;
}
}
/ * * B a s i c t e s t s f o r C a p a b i l i t i e s .
* /
class CapabilitiesTests {
static testConfirm ( ) {
let bool = confirm ( 'Please confirm' ) ;
document . getElementById ( 'demo' ) . innerHTML = ( bool ) ? 'Confirmed' : 'Not confirmed' ;
}
static testPrompt ( ) {
let person = prompt ( 'Please enter your name' , 'Harry Potter' ) ;
if ( person != null ) {
demo . innerHTML =
'Hello ' + person + '! How are you today?' ;
}
}
static testUserAgent ( ) {
let agent = 'User-agent: ' + Capabilities . userAgent ;
user _agent . innerHTML = agent ;
}
static testDevicePixelRatio ( ) {
let value = 'Device Pixel Ratio: ' + Capabilities . devicePixelRatio ;
device _pixel _ratio . innerHTML = value ;
}
static testMultiTouchTable ( ) {
let value = 'Is the device a multi-touch table? ' + Capabilities . isMultiTouchTable ;
multi _touch _table . innerHTML = value ;
}
static testSupportedEvents ( ) {
let events = [ ] ;
if ( Capabilities . supportsMouseEvents ( ) ) {
events . push ( 'MouseEvents' ) ;
}
if ( Capabilities . supportsTouchEvents ( ) ) {
events . push ( 'TouchEvents' ) ;
}
if ( Capabilities . supportsPointerEvents ( ) ) {
events . push ( 'PointerEvents' ) ;
}
supported _events . innerHTML = 'Supported Events: ' + events . join ( ', ' ) ;
}
static testAll ( ) {
this . testUserAgent ( ) ;
this . testDevicePixelRatio ( ) ;
this . testMultiTouchTable ( ) ;
this . testSupportedEvents ( ) ;
}
}
/* Optional global variables, needed in DocTests. */
window . Capabilities = Capabilities ;
window . CapabilitiesTests = CapabilitiesTests ;
/ * *
* A base class for scatter specific events .
*
* @ constructor
* @ param { name } String - The name of the event
* @ param { target } Object - The target of the event
* /
class BaseEvent {
constructor ( name , target ) {
this . name = name ;
this . target = target ;
}
}
// Event types
const START = 'onStart' ;
const UPDATE = 'onUpdate' ;
const END = 'onEnd' ;
/ * *
* A scatter event that describes how the scatter has changed .
*
* @ constructor
* @ param { target } Object - The target scatter of the event
* @ param { optional } Object - Optional parameter
* /
class ScatterEvent extends BaseEvent {
constructor (
target ,
{
translate = { x : 0 , y : 0 } ,
scale = null ,
rotate = 0 ,
about = null ,
fast = false ,
type = null
} = { }
) {
super ( 'scatterTransformed' , { target : target } ) ;
this . translate = translate ;
this . scale = scale ;
this . rotate = rotate ;
this . about = about ;
this . fast = fast ;
this . type = type ;
}
toString ( ) {
return (
"Event('scatterTransformed', scale: " +
this . scale +
' about: ' +
this . about . x +
', ' +
this . about . y +
')'
)
}
}
/ * *
* A scatter resize event that describes how the scatter has changed .
*
* @ constructor
* @ param { target } Object - The target scatter of the event
* @ param { optional } Object - Optional parameter
* /
class ResizeEvent extends BaseEvent {
constructor ( target , { width = 0 , height = 0 } = { } ) {
super ( 'scatterResized' , { width : width , height : height } ) ;
this . width = width ;
this . height = height ;
}
toString ( ) {
return (
'Event(scatterResized width: ' +
this . width +
'height: ' +
this . height +
')'
)
}
}
/ * *
* A abstract base class that implements the throwable behavior of a scatter
* object .
*
* @ constructor
* /
class Throwable {
constructor ( {
movableX = true ,
movableY = true ,
throwVisibility = 44 ,
throwDamping = 0.95 ,
autoThrow = true ,
onThrowFinished = null
} = { } ) {
this . movableX = movableX ;
this . movableY = movableY ;
this . throwVisibility = throwVisibility ;
this . throwDamping = throwDamping ;
this . autoThrow = autoThrow ;
this . velocities = [ ] ;
this . velocity = null ;
this . timestamp = null ;
this . onThrowFinished = onThrowFinished ;
//console.log("onThrowFinished", onThrowFinished)
}
observeVelocity ( ) {
this . lastframe = performance . now ( ) ;
}
addVelocity ( delta , buffer = 5 ) {
let t = performance . now ( ) ;
let dt = t - this . lastframe ;
this . lastframe = t ;
if ( dt > 0 ) {
// Avoid division by zero errors later on
2019-05-31 10:15:50 +02:00
// and consider the number of involved pointers sind addVelocity will be called by the
// onMove events
let velocity = { t : t , dt : dt , dx : delta . x / delta . number , dy : delta . y / delta . number } ;
2019-03-22 12:54:57 +01:00
this . velocities . push ( velocity ) ;
while ( this . velocities . length > buffer ) {
this . velocities . shift ( ) ;
}
}
}
meanVelocity ( milliseconds = 30 ) {
2019-05-31 10:15:50 +02:00
this . addVelocity ( { x : 0 , y : 0 , number : 1 } ) ;
2019-03-22 12:54:57 +01:00
let sum = { x : 0 , y : 0 } ;
let count = 0 ;
let t = 0 ;
for ( let i = this . velocities . length - 1 ; i > 0 ; i -- ) {
let v = this . velocities [ i ] ;
t += v . dt ;
let nv = { x : v . dx / v . dt , y : v . dy / v . dt } ;
sum = Points . add ( sum , nv ) ;
count += 1 ;
if ( t > milliseconds ) {
break
}
}
if ( count === 0 ) return sum // empty vector
return Points . multiplyScalar ( sum , 1 / count )
}
killAnimation ( ) {
this . velocity = null ;
this . velocities = [ ] ;
}
startThrow ( ) {
this . velocity = this . meanVelocity ( ) ;
if ( this . velocity != null ) {
// Call next velocity to ansure that specializations
// that use keepOnStage are called
this . velocity = this . nextVelocity ( this . velocity ) ;
if ( this . autoThrow ) this . animateThrow ( performance . now ( ) ) ;
} else {
this . onDragComplete ( ) ;
}
}
2019-06-04 09:34:50 +02:00
_throwDeltaTime ( ) {
let t = performance . now ( ) ;
let dt = t - this . lastframe ;
this . lastframe = t ;
return dt
}
2019-03-22 12:54:57 +01:00
animateThrow ( time ) {
if ( this . velocity != null ) {
2019-06-04 09:34:50 +02:00
let dt = this . _throwDeltaTime ( ) ;
2019-03-22 12:54:57 +01:00
// console.log("animateThrow", dt)
let next = this . nextVelocity ( this . velocity ) ;
let prevLength = Points . length ( this . velocity ) ;
let nextLength = Points . length ( next ) ;
if ( nextLength > prevLength ) {
let factor = nextLength / prevLength ;
next = Points . multiplyScalar ( next , 1 / factor ) ;
console . log ( 'Prevent acceleration' , factor , this . velocity , next ) ;
}
this . velocity = next ;
let d = Points . multiplyScalar ( this . velocity , dt ) ;
this . _move ( d ) ;
this . onDragUpdate ( d ) ;
if ( dt == 0 || this . needsAnimation ( ) ) {
requestAnimationFrame ( this . animateThrow . bind ( this ) ) ;
return
} else {
if ( this . isOutside ( ) ) {
requestAnimationFrame ( this . animateThrow . bind ( this ) ) ;
return
}
}
}
this . onDragComplete ( ) ;
if ( this . onThrowFinished != null ) {
this . onThrowFinished ( ) ;
}
}
needsAnimation ( ) {
if ( this . velocity == null ) {
return false
}
return Points . length ( this . velocity ) > 0.01
}
nextVelocity ( velocity ) {
// Must be overwritten: computes the changed velocity. Implement
// damping, collison detection, etc. here
let next = Points . multiplyScalar ( velocity , this . throwDamping ) ;
return {
x : ( this . movableX ) ? next . x : 0 ,
y : ( this . movableY ) ? next . y : 0
}
}
_move ( delta ) {
// Overwrite if necessary
}
onDragComplete ( ) {
// Overwrite if necessary
}
onDragUpdate ( delta ) {
// Overwrite if necessary
}
}
class AbstractScatter extends Throwable {
constructor ( {
minScale = 0.1 ,
maxScale = 1.0 ,
startScale = 1.0 ,
autoBringToFront = true ,
autoThrow = true ,
translatable = true ,
scalable = true ,
rotatable = true ,
resizable = false ,
movableX = true ,
movableY = true ,
throwVisibility = 44 ,
throwDamping = 0.95 ,
overdoScaling = 1 ,
mouseZoomFactor = 1.1 ,
rotationDegrees = null ,
rotation = null ,
onTransform = null ,
interactive = true ,
onClose = null ,
onThrowFinished = null ,
scaleAutoClose = false ,
scaleCloseThreshold = 0.10 ,
scaleCloseBuffer = 0.05
} = { } ) {
if ( rotationDegrees != null && rotation != null ) {
throw new Error ( 'Use rotationDegrees or rotation but not both' )
} else if ( rotation != null ) {
rotationDegrees = Angle . radian2degree ( rotation ) ;
} else if ( rotationDegrees == null ) {
rotationDegrees = 0 ;
}
super ( {
movableX ,
movableY ,
throwVisibility ,
throwDamping ,
autoThrow ,
onThrowFinished
} ) ;
/ * *
* Closes the card when the minScale is reached and the
* card is released . Card can be saved by scaling it up again .
* /
this . scaleAutoClose = scaleAutoClose ;
this . scaleCloseThreshold = scaleCloseThreshold ;
this . scaleCloseBuffer = scaleCloseBuffer ;
this . scaleAutoCloseTimeout = null ;
this . interactive = interactive ;
this . startRotationDegrees = rotationDegrees ;
this . startScale = startScale ; // Needed to reset object
this . minScale = minScale ;
this . maxScale = maxScale ;
this . overdoScaling = overdoScaling ;
this . translatable = translatable ;
if ( ! translatable ) {
this . movableX = false ;
this . movableY = false ;
}
this . scalable = scalable ;
this . rotatable = rotatable ;
this . resizable = resizable ;
this . mouseZoomFactor = mouseZoomFactor ;
this . autoBringToFront = autoBringToFront ;
this . dragging = false ;
this . onTransform = onTransform != null ? [ onTransform ] : null ;
this . onClose = onClose != null ? [ onClose ] : null ;
}
addCloseEventCallback ( callback ) {
if ( this . onClose == null ) {
this . onClose = [ ] ;
}
this . onClose . push ( callback ) ;
}
addTransformEventCallback ( callback ) {
if ( this . onTransform == null ) {
this . onTransform = [ ] ;
}
this . onTransform . push ( callback ) ;
}
startGesture ( interaction ) {
this . bringToFront ( ) ;
this . killAnimation ( ) ;
this . observeVelocity ( ) ;
return true
}
close ( ) {
if ( this . onClose ) {
this . onClose . forEach ( callback => callback ( this ) ) ;
}
}
gesture ( interaction ) {
let delta = interaction . delta ( ) ;
//console.log("gesture", delta)
if ( delta != null ) {
this . addVelocity ( delta ) ;
this . transform ( delta , delta . zoom , delta . rotate , delta . about ) ;
if ( delta . zoom != 1 ) this . interactionAnchor = delta . about ;
}
}
get polygon ( ) {
let w2 = this . width * this . scale / 2 ;
let h2 = this . height * this . scale / 2 ;
let center = this . center ;
let polygon = new Polygon ( center ) ;
polygon . addPoint ( { x : - w2 , y : - h2 } ) ;
polygon . addPoint ( { x : w2 , y : - h2 } ) ;
polygon . addPoint ( { x : w2 , y : h2 } ) ;
polygon . addPoint ( { x : - w2 , y : h2 } ) ;
polygon . rotate ( this . rotation ) ;
return polygon
}
isOutside ( ) {
let stagePolygon = this . containerPolygon ;
if ( stagePolygon == null )
return false
let polygon = this . polygon ;
if ( polygon == null )
return false
let result = stagePolygon . intersectsWith ( polygon ) ;
return result === false || result . overlap < this . throwVisibility
}
recenter ( ) {
// Return a small vector that guarantees that the scatter is moving
// towards the center of the stage
let center = this . center ;
let target = this . container . center ;
let delta = Points . subtract ( target , center ) ;
return Points . normalize ( delta )
}
nextVelocity ( velocity ) {
return this . keepOnStage ( velocity )
}
bouncing ( ) {
// Implements the bouncing behavior of the scatter. Moves the scatter
// to the center of the stage if the scatter is outside the stage or
// not within the limits of the throwVisibility.
let stagePolygon = this . containerPolygon ;
let polygon = this . polygon ;
let result = stagePolygon . intersectsWith ( polygon ) ;
if ( result === false || result . overlap < this . throwVisibility ) {
let cv = this . recenter ( ) ;
let recentered = false ;
while ( result === false || result . overlap < this . throwVisibility ) {
polygon . center . x += cv . x ;
polygon . center . y += cv . y ;
this . _move ( cv ) ;
result = stagePolygon . intersectsWith ( polygon ) ;
recentered = true ;
}
return recentered
}
return false
}
keepOnStage ( velocity , collision = 0.5 ) {
let stagePolygon = this . containerPolygon ;
2019-05-31 16:10:46 +02:00
// UO: since keepOnStage is called in nextVelocity we need to
// ensure a return value
if ( ! stagePolygon ) return { x : 0 , y : 0 }
2019-03-22 12:54:57 +01:00
let polygon = this . polygon ;
let bounced = this . bouncing ( ) ;
if ( bounced ) {
let stage = this . containerBounds ;
let x = this . center . x ;
let y = this . center . y ;
let dx = this . movableX ? velocity . x : 0 ;
let dy = this . movableY ? velocity . y : 0 ;
let factor = this . throwDamping ;
// if (recentered) {
if ( x < 0 ) {
dx = - dx ;
factor = collision ;
}
if ( x > stage . width ) {
dx = - dx ;
factor = collision ;
}
if ( y < 0 ) {
dy = - dy ;
factor = collision ;
}
if ( y > stage . height ) {
dy = - dy ;
factor = collision ;
}
// }
return Points . multiplyScalar ( { x : dx , y : dy } , factor )
}
return super . nextVelocity ( velocity )
}
endGesture ( interaction ) {
this . startThrow ( ) ;
this . _checkAutoClose ( ) ;
}
_checkAutoClose ( ) {
if ( this . scaleAutoClose )
if ( this . scale < this . minScale + this . scaleCloseThreshold - this . scaleCloseBuffer ) {
this . zoom ( this . minScale , { animate : 0.2 , onComplete : this . close . bind ( this ) } ) ;
} else if ( this . scale < this . minScale + this . scaleCloseThreshold ) {
this . zoom ( this . minScale + this . scaleCloseThreshold , { animate : 0.4 } ) ;
}
}
rotateDegrees ( degrees , anchor ) {
let rad = Angle . degree2radian ( degrees ) ;
this . rotate ( rad , anchor ) ;
}
rotate ( rad , anchor ) {
this . transform ( { x : 0 , y : 0 } , 1.0 , rad , anchor ) ;
}
move ( d , { animate = 0 } = { } ) {
if ( this . translatable ) {
if ( animate > 0 ) {
let startPos = this . position ;
TweenLite . to ( this , animate , {
x : '+=' + d . x ,
y : '+=' + d . y ,
/* scale: scale, uo: not defined, why was this here? */
onUpdate : e => {
let p = this . position ;
let dx = p . x - startPos . x ;
let dy = p . x - startPos . y ;
this . onMoved ( dx , dy ) ;
}
} ) ;
} else {
this . _move ( d ) ;
this . onMoved ( d . x , d . y ) ;
}
}
}
moveTo ( p , { animate = 0 } = { } ) {
let c = this . origin ;
let delta = Points . subtract ( p , c ) ;
this . move ( delta , { animate : animate } ) ;
}
centerAt ( p , { animate = 0 } = { } ) {
let c = this . center ;
let delta = Points . subtract ( p , c ) ;
this . move ( delta , { animate : animate } ) ;
}
zoom (
scale ,
{
animate = 0 ,
about = null ,
delay = 0 ,
x = null ,
y = null ,
onComplete = null
} = { }
) {
let anchor = about || this . center ;
if ( scale != this . scale ) {
if ( animate > 0 ) {
TweenLite . to ( this , animate , {
scale : scale ,
delay : delay ,
onComplete : onComplete ,
onUpdate : this . onZoomed . bind ( this )
} ) ;
} else {
this . scale = scale ;
this . onZoomed ( anchor ) ;
}
}
}
_move ( delta ) {
this . x += this . movableX ? delta . x : 0 ;
this . y += this . movableX ? delta . y : 0 ;
}
transform ( translate , zoom , rotate , anchor ) {
let delta = {
x : this . movableX ? translate . x : 0 ,
y : this . movableY ? translate . y : 0
} ;
if ( this . resizable ) var vzoom = zoom ;
if ( ! this . translatable ) delta = { x : 0 , y : 0 } ;
if ( ! this . rotatable ) rotate = 0 ;
if ( ! this . scalable ) zoom = 1.0 ;
if ( zoom == 1.0 && rotate == 0 ) {
this . _move ( delta ) ;
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
translate : delta ,
scale : this . scale ,
rotate : 0 ,
about : anchor ,
fast : false ,
type : UPDATE
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
return
}
let origin = this . rotationOrigin ;
let beta = Points . angle ( origin , anchor ) ;
let distance = Points . distance ( origin , anchor ) ;
let { scale : newScale , zoom : thresholdedZoom } = this . calculateScale ( zoom ) ;
let newOrigin = Points . arc ( anchor , beta + rotate , distance * thresholdedZoom ) ;
let extra = Points . subtract ( newOrigin , origin ) ;
let offset = Points . subtract ( anchor , origin ) ;
this . _move ( offset ) ;
this . scale = newScale ;
this . rotation += rotate ;
offset = Points . negate ( offset ) ;
offset = Points . add ( offset , extra ) ;
offset = Points . add ( offset , translate ) ;
this . _move ( offset ) ;
delta . x += extra . x ;
delta . y += extra . y ;
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
translate : delta ,
scale : newScale ,
rotate : rotate ,
about : anchor
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
if ( this . resizable ) {
this . resizeAfterTransform ( vzoom ) ;
}
}
/ * *
* For a given zoom , a new scale is calculated , taking
* min and max scale into account .
*
* @ param { number } zoom - The zoom factor , to scale the object with .
* @ returns { object } - Returns an object containing the a value for a valid scale and the corrected zoom factor .
* /
calculateScale ( zoom ) {
let scale = this . scale * zoom ;
let minScale = this . minScale / this . overdoScaling ;
let maxScale = this . maxScale * this . overdoScaling ;
if ( scale < minScale ) {
scale = minScale ;
zoom = scale / this . scale ;
}
if ( scale > maxScale ) {
scale = maxScale ;
zoom = scale / this . scale ;
}
if ( this . scaleAutoClose )
this . _updateTransparency ( ) ;
return { zoom , scale }
}
_updateTransparency ( ) {
if ( this . scale < this . minScale + this . scaleCloseThreshold ) {
let transparency = this . calculateScaleTransparency ( ) ;
this . element . style . opacity = transparency ;
} else this . element . style . opacity = 1 ;
}
calculateScaleTransparency ( ) {
let transparency = ( this . scale - this . minScale ) / this . scaleCloseThreshold ;
transparency = ( transparency > 1 ) ? 1 : ( transparency < 0 ) ? 0 : transparency ;
return transparency
}
resizeAfterTransform ( zoom ) {
// Overwrite this in subclasses.
}
validScale ( scale ) {
scale = Math . max ( scale , this . minScale ) ;
scale = Math . min ( scale , this . maxScale ) ;
return scale
}
animateZoomBounce ( dt = 1 ) {
if ( this . zoomAnchor != null ) {
let zoom = 1 ;
let amount = Math . min ( 0.01 , 0.3 * dt / 100000.0 ) ;
if ( this . scale < this . minScale ) zoom = 1 + amount ;
if ( this . scale > this . maxScale ) zoom = 1 - amount ;
if ( zoom != 1 ) {
this . transform ( { x : 0 , y : 0 } , zoom , 0 , this . zoomAnchor ) ;
requestAnimationFrame ( dt => {
this . animateZoomBounce ( dt ) ;
} ) ;
return
}
this . zoomAnchor = null ;
}
}
checkScaling ( about , delay = 0 ) {
this . zoomAnchor = about ;
clearTimeout ( this . animateZoomBounce . bind ( this ) ) ;
setTimeout ( this . animateZoomBounce . bind ( this ) , delay ) ;
}
onMouseWheel ( event ) {
if ( event . claimedByScatter ) {
if ( event . claimedByScatter != this ) return
}
this . killAnimation ( ) ;
this . targetScale = null ;
let direction = event . detail < 0 || event . wheelDelta > 0 ;
let globalPoint = { x : event . clientX , y : event . clientY } ;
let centerPoint = this . mapPositionToContainerPoint ( globalPoint ) ;
if ( event . shiftKey ) {
let degrees = direction ? 5 : - 5 ;
let rad = Angle . degree2radian ( degrees ) ;
return this . transform ( { x : 0 , y : 0 } , 1.0 , rad , centerPoint )
}
const zoomFactor = this . mouseZoomFactor ;
let zoom = direction ? zoomFactor : 1 / zoomFactor ;
this . transform ( { x : 0 , y : 0 } , zoom , 0 , centerPoint ) ;
this . checkScaling ( centerPoint , 200 ) ;
if ( this . scaleAutoClose ) {
if ( this . scale <= this . minScale + this . scaleCloseThreshold ) {
if ( this . scaleAutoCloseTimeout ) clearTimeout ( this . scaleAutoCloseTimeout ) ;
this . scaleAutoCloseTimeout = setTimeout ( ( ) => {
this . _checkAutoClose ( ) ;
} , 600 ) ;
}
this . _updateTransparency ( ) ;
}
//
// if (this.onTransform != null) {
// let event = new ScatterEvent(this, {
// translate: {x: 0, y: 0},
// scale: this.scale,
// rotate: 0,
// about: null,
// fast: false,
// type: ZOOM
// })
// this.onTransform.forEach(function(f) {
// f(event)
// })
// }
}
onStart ( event , interaction ) {
if ( this . startGesture ( interaction ) ) {
this . dragging = true ;
this . interactionAnchor = null ;
}
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
translate : { x : 0 , y : 0 } ,
scale : this . scale ,
rotate : 0 ,
about : null ,
fast : false ,
type : START
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
onMove ( event , interaction ) {
/ * * A s l o n g a s m o u s e o u t & & m o u s e l e a v e i n t e r r u p t w e c a n n o t b e s u r e t h a t
* dragging remains correct .
* /
if ( this . dragging ) {
this . gesture ( interaction ) ;
}
}
onEnd ( event , interaction ) {
//console.log("Scatter.onEnd", this.dragging)
if ( interaction . isFinished ( ) ) {
this . endGesture ( interaction ) ;
this . dragging = false ;
for ( let key of interaction . ended . keys ( ) ) {
if ( interaction . isTap ( key ) ) {
let point = interaction . ended . get ( key ) ;
this . onTap ( event , interaction , point ) ;
}
}
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
translate : { x : 0 , y : 0 } ,
scale : this . scale ,
rotate : 0 ,
about : null ,
fast : false ,
type : END
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
let about = this . interactionAnchor ;
if ( about != null ) {
this . checkScaling ( about , 100 ) ;
}
}
onTap ( event , interaction , point ) { }
onDragUpdate ( delta ) {
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
fast : true ,
translate : delta ,
scale : this . scale ,
about : this . currentAbout ,
type : null
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
onDragComplete ( ) {
if ( this . onTransform ) {
let event = new ScatterEvent ( this , {
scale : this . scale ,
about : this . currentAbout ,
fast : false ,
type : null
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
onMoved ( dx , dy , about ) {
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
translate : { x : dx , y : dy } ,
about : about ,
fast : true ,
type : null
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
onResizing ( ) {
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
scale : this . scale ,
fast : false ,
type : null
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
onZoomed ( about ) {
if ( this . scaleAutoClose )
this . _updateTransparency ( ) ;
if ( this . onTransform != null ) {
let event = new ScatterEvent ( this , {
scale : this . scale ,
about : about ,
fast : false ,
type : null
} ) ;
this . onTransform . forEach ( function ( f ) {
f ( event ) ;
} ) ;
}
}
}
/ * * A c o n t a i n e r f o r s c a t t e r o b j e c t s , w h i c h u s e s a s i n g l e I n t e r a c t i o n M a p p e r
* for all children . This reduces the number of registered event handlers
* and covers the common use case that multiple objects are scattered
* on the same level .
* /
class DOMScatterContainer {
/ * *
* @ constructor
* @ param { DOM node } element - DOM element that receives events
* @ param { Bool } stopEvents - Whether events should be stopped or propagated
* @ param { Bool } claimEvents - Whether events should be marked as claimed
* if findTarget return as non - null value .
* @ param { String } [ touchAction = none ] - CSS to set touch action style , needed to prevent
* pointer cancel events . Use null if the
* the touch action should not be set .
* /
constructor (
element ,
{ stopEvents = 'auto' , claimEvents = true , useCapture = true , touchAction = 'none' } = { }
) {
this . onCapture = null ;
this . element = element ;
if ( stopEvents === 'auto' ) {
if ( Capabilities . isSafari ) {
document . addEventListener (
'touchmove' ,
event => this . preventPinch ( event ) ,
false
) ;
stopEvents = false ;
} else {
stopEvents = true ;
}
}
this . stopEvents = stopEvents ;
this . claimEvents = claimEvents ;
if ( touchAction !== null ) {
Elements$1 . setStyle ( element , { touchAction } ) ;
}
this . scatter = new Map ( ) ;
this . delegate = new InteractionMapper$1 ( element , this , {
useCapture ,
mouseWheelElement : window
} ) ;
if ( typeof debugCanvas !== 'undefined' ) {
requestAnimationFrame ( dt => {
this . showTouches ( dt ) ;
} ) ;
}
}
showTouches ( dt ) {
let resolution = window . devicePixelRatio ;
let canvas = debugCanvas ;
let current = this . delegate . interaction . current ;
let context = canvas . getContext ( '2d' ) ;
let radius = 20 * resolution ;
context . clearRect ( 0 , 0 , canvas . width , canvas . height ) ;
context . fillStyle = 'rgba(0, 0, 0, 0.3)' ;
context . lineWidth = 2 ;
context . strokeStyle = '#003300' ;
for ( let [ key , point ] of current . entries ( ) ) {
let local = point ;
context . beginPath ( ) ;
context . arc (
local . x * resolution ,
local . y * resolution ,
radius ,
0 ,
2 * Math . PI ,
false
) ;
context . fill ( ) ;
context . stroke ( ) ;
}
requestAnimationFrame ( dt => {
this . showTouches ( dt ) ;
} ) ;
}
preventPinch ( event ) {
event = event . originalEvent || event ;
if ( event . scale !== 1 ) {
event . preventDefault ( ) ;
}
}
add ( scatter ) {
this . scatter . set ( scatter . element , scatter ) ;
}
capture ( event ) {
if ( this . onCapture ) {
return this . onCapture ( event )
}
if ( event . target == this . element && this . stopEvents ) {
Events . stop ( event ) ;
}
return true
}
mapPositionToPoint ( point ) {
return Points . fromPageToNode ( this . element , point )
}
isDescendant ( parent , child , clickable = false ) {
if ( parent == child ) return true
let node = child . parentNode ;
while ( node != null ) {
if ( ! clickable && node . onclick ) {
return false
}
if ( node == parent ) {
return true
}
node = node . parentNode ;
}
return false
}
findTarget ( event , local , global ) {
/ * * * N o t e t h a t e l e m e n t F r o m P o i n t w o r k s w i t h c l i e n t X , c l i e n t Y , n o t p a g e X , p a g e Y
The important point is that event should not be used , since the TouchEvent
points are hidden in sub objects .
* * * /
let found = document . elementFromPoint ( global . x , global . y ) ;
for ( let target of this . scatter . values ( ) ) {
if ( target . interactive && this . isDescendant ( target . element , found ) ) {
if ( this . stopEvents ) Events . stop ( event ) ;
if ( this . claimEvents ) event . claimedByScatter = target ;
return target
}
}
return null
}
get center ( ) {
let r = this . bounds ;
let w2 = r . width / 2 ;
let h2 = r . height / 2 ;
return { x : w2 , y : h2 }
}
get bounds ( ) {
return this . element . getBoundingClientRect ( )
}
get polygon ( ) {
let r = this . bounds ;
let w2 = r . width / 2 ;
let h2 = r . height / 2 ;
let center = { x : w2 , y : h2 } ;
let polygon = new Polygon ( center ) ;
polygon . addPoint ( { x : - w2 , y : - h2 } ) ;
polygon . addPoint ( { x : w2 , y : - h2 } ) ;
polygon . addPoint ( { x : w2 , y : h2 } ) ;
polygon . addPoint ( { x : - w2 , y : h2 } ) ;
return polygon
}
}
class DOMScatter extends AbstractScatter {
constructor (
element ,
container ,
{
startScale = 1.0 ,
minScale = 0.1 ,
maxScale = 1.0 ,
overdoScaling = 1.5 ,
autoBringToFront = true ,
translatable = true ,
scalable = true ,
rotatable = true ,
movableX = true ,
movableY = true ,
rotationDegrees = null ,
rotation = null ,
onTransform = null ,
transformOrigin = 'center center' ,
// extras which are in part needed
x = 0 ,
y = 0 ,
width = null , // required
height = null , // required
resizable = false ,
simulateClick = false ,
verbose = true ,
onResize = null ,
touchAction = 'none' ,
throwVisibility = 44 ,
throwDamping = 0.95 ,
autoThrow = true ,
scaleAutoClose = false ,
onClose = null ,
scaleCloseThreshold = 0.10 ,
scaleCloseBuffer = 0.05
} = { }
) {
super ( {
minScale ,
maxScale ,
startScale ,
overdoScaling ,
autoBringToFront ,
translatable ,
scalable ,
rotatable ,
movableX ,
movableY ,
resizable ,
rotationDegrees ,
rotation ,
onTransform ,
throwVisibility ,
throwDamping ,
autoThrow ,
scaleAutoClose ,
scaleCloseThreshold ,
scaleCloseBuffer ,
onClose
} ) ;
if ( container == null || width == null || height == null ) {
throw new Error ( 'Invalid value: null' )
}
element . scatter = this ;
this . element = element ;
this . x = x ;
this . y = y ;
this . oldX = 0 ;
this . oldY = 0 ;
this . meanX = x ;
this . meanY = y ;
this . width = width ;
this . height = height ;
this . throwVisibility = Math . min ( width , height , throwVisibility ) ;
this . container = container ;
this . simulateClick = simulateClick ;
this . scale = startScale ;
this . rotationDegrees = this . startRotationDegrees ;
this . transformOrigin = transformOrigin ;
this . initialValues = {
x : x ,
y : y ,
width : width ,
height : height ,
scale : startScale ,
rotation : this . startRotationDegrees ,
transformOrigin : transformOrigin
} ;
// For tweenlite we need initial values in _gsTransform
TweenLite . set ( element , this . initialValues ) ;
this . onResize = onResize ;
this . verbose = verbose ;
if ( touchAction !== null ) {
Elements$1 . setStyle ( element , { touchAction } ) ;
}
this . resizeButton = null ;
if ( resizable ) {
let button = document . createElement ( "div" ) ;
button . style . position = "absolute" ;
button . style . right = "0px" ;
button . style . bottom = "0px" ;
button . style . width = "50px" ;
button . style . height = "50px" ;
// button.style.borderRadius = "100% 0px 0px 0px";
// button.style.background = this.element.style.backgroundColor
button . className = "interactiveElement" ;
this . element . appendChild ( button ) ;
button . addEventListener ( 'pointerdown' , ( e ) => {
this . startResize ( e ) ;
} ) ;
button . addEventListener ( 'pointermove' , ( e ) => {
this . resize ( e ) ;
} ) ;
button . addEventListener ( 'pointerup' , ( e ) => {
this . stopResize ( e ) ;
} ) ;
this . resizeButton = button ;
}
container . add ( this ) ;
}
/** Returns geometry data as object. **/
getState ( ) {
return {
scale : this . scale ,
x : this . x ,
y : this . y ,
rotation : this . rotation
}
}
close ( ) {
super . close ( ) ;
let parent = this . element . parentNode ;
if ( parent ) parent . removeChild ( this . element ) ;
}
get rotationOrigin ( ) {
return this . center
}
get x ( ) {
return this . _x
}
get y ( ) {
return this . _y
}
set x ( value ) {
this . _x = value ;
TweenLite . set ( this . element , { x : value } ) ;
}
set y ( value ) {
this . _y = value ;
TweenLite . set ( this . element , { y : value } ) ;
}
get position ( ) {
let transform = this . element . _gsTransform ;
let x = transform . x ;
let y = transform . y ;
return { x , y }
}
get origin ( ) {
let p = this . fromNodeToPage ( 0 , 0 ) ;
return Points . fromPageToNode ( this . container . element , p )
}
get bounds ( ) {
let stage = this . container . element . getBoundingClientRect ( ) ;
let rect = this . element . getBoundingClientRect ( ) ;
return {
top : rect . top - stage . top ,
left : rect . left - stage . left ,
width : rect . width ,
height : rect . height
}
}
get center ( ) {
let r = this . bounds ;
let w2 = r . width / 2 ;
let h2 = r . height / 2 ;
// if (this.resizable) {
// w2 *= this.scale
// h2 *= this.scale
// }
var x = r . left + w2 ;
var y = r . top + h2 ;
return { x , y }
}
set rotation ( radians ) {
let rad = radians ; // Angle.normalize(radians)
let degrees = Angle . radian2degree ( rad ) ;
TweenLite . set ( this . element , { rotation : degrees } ) ;
this . _rotation = rad ;
}
set rotationDegrees ( degrees ) {
let deg = degrees ; // Angle.normalizeDegree(degrees)
TweenLite . set ( this . element , { rotation : deg } ) ;
this . _rotation = Angle . degree2radian ( deg ) ;
}
get rotation ( ) {
return this . _rotation
}
get rotationDegrees ( ) {
return this . _rotation
}
set scale ( scale ) {
TweenLite . set ( this . element , {
scale : scale ,
transformOrigin : this . transformOrigin
} ) ;
this . _scale = scale ;
}
get scale ( ) {
return this . _scale
}
get containerBounds ( ) {
return this . container . bounds
}
get containerPolygon ( ) {
return this . container . polygon
}
mapPositionToContainerPoint ( point ) {
return this . container . mapPositionToPoint ( point )
}
capture ( event ) {
return true
}
reset ( ) {
TweenLite . set ( this . element , this . initialValues ) ;
}
hide ( ) {
TweenLite . to ( this . element , 0.1 , {
display : 'none' ,
onComplete : e => {
this . element . parentNode . removeChild ( this . element ) ;
}
} ) ;
}
show ( ) {
TweenLite . set ( this . element , { display : 'block' } ) ;
}
showAt ( p , rotationDegrees ) {
TweenLite . set ( this . element , {
display : 'block' ,
x : p . x ,
y : p . y ,
rotation : rotationDegrees ,
transformOrigin : this . transformOrigin
} ) ;
}
bringToFront ( ) {
// this.element.parentNode.appendChild(this.element)
// uo: On Chome and Electon appendChild leads to flicker
TweenLite . set ( this . element , { zIndex : DOMScatter . zIndex ++ } ) ;
}
toggleVideo ( element ) {
if ( element . paused ) {
element . play ( ) ;
} else {
element . pause ( ) ;
}
}
onTap ( event , interaction , point ) {
if ( this . simulateClick ) {
let p = Points . fromPageToNode ( this . element , point ) ;
let iframe = this . element . querySelector ( 'iframe' ) ;
if ( iframe ) {
let doc = iframe . contentWindow . document ;
let element = doc . elementFromPoint ( p . x , p . y ) ;
if ( element == null ) {
return
}
switch ( element . tagName ) {
case 'VIDEO' :
console . log ( element . currentSrc ) ;
if ( PopupMenu ) {
PopupMenu . open (
{
Fullscreen : ( ) =>
window . open ( element . currentSrc ) ,
Play : ( ) => element . play ( )
} ,
{ x , y }
) ;
} else {
this . toggleVideo ( element ) ;
}
break
default :
element . click ( ) ;
}
}
}
}
isDescendant ( parent , child ) {
let node = child . parentNode ;
while ( node != null ) {
if ( node == parent ) {
return true
}
node = node . parentNode ;
}
return false
}
fromPageToNode ( x , y ) {
return Points . fromPageToNode ( this . element , { x , y } )
}
fromNodeToPage ( x , y ) {
return Points . fromNodeToPage ( this . element , { x , y } )
}
_move ( delta ) {
// UO: We need to keep TweenLite's _gsTransform and the private
// _x and _y attributes aligned
let x = this . element . _gsTransform . x ;
let y = this . element . _gsTransform . y ;
if ( this . movableX ) {
x += delta . x ;
}
if ( this . movableY ) {
y += delta . y ;
}
this . _x = x ;
this . _y = y ;
TweenLite . set ( this . element , { x : x , y : y } ) ;
}
resizeAfterTransform ( zoom ) {
// let w = this.width * this.scale
// let h = this.height * this.scale
// TweenLite.set(this.element, { width: w, height: h })
if ( this . onResize ) {
let event = new ResizeEvent ( this , { width : w , height : h } ) ;
this . onResize ( event ) ;
}
2019-06-03 11:41:18 +02:00
if ( this . resizeButton != null ) ;
2019-03-22 12:54:57 +01:00
}
startResize ( e ) {
e . preventDefault ( ) ;
let event = new CustomEvent ( 'resizeStarted' ) ;
2019-04-08 12:31:50 +02:00
let oldPostition = { x : this . element . getBoundingClientRect ( ) . left , y : this . element . getBoundingClientRect ( ) . top } ;
2019-03-22 12:54:57 +01:00
this . bringToFront ( ) ;
this . element . style . transformOrigin = "0% 0%" ;
2019-04-08 12:31:50 +02:00
let newPostition = { x : this . element . getBoundingClientRect ( ) . left , y : this . element . getBoundingClientRect ( ) . top } ;
2019-03-22 12:54:57 +01:00
let offset = Points . subtract ( oldPostition , newPostition ) ;
this . oldX = e . clientX ;
this . oldY = e . clientY ;
e . target . setAttribute ( 'resizing' , "true" ) ;
e . target . setPointerCapture ( e . pointerId ) ;
TweenLite . to ( this . element , 0 , { css : { left : "+=" + offset . x + "px" } } ) ;
TweenLite . to ( this . element , 0 , { css : { top : "+=" + offset . y + "px" } } ) ;
this . element . dispatchEvent ( event ) ;
}
resize ( e ) {
e . preventDefault ( ) ;
let rotation = Angle . radian2degree ( this . rotation ) ;
rotation = ( rotation + 360 ) % 360 ;
let event = new CustomEvent ( 'resized' ) ;
if ( e . target . getAttribute ( 'resizing' ) == "true" ) {
let deltaX = ( e . clientX - this . oldX ) ;
let deltaY = ( e . clientY - this . oldY ) ;
let r = Math . sqrt ( Math . pow ( deltaX , 2 ) + Math . pow ( deltaY , 2 ) ) ;
let phi = Angle . radian2degree ( Math . atan2 ( deltaX , deltaY ) ) ;
phi = ( ( phi ) + 630 ) % 360 ;
let rot = ( ( rotation + 90 ) + 630 ) % 360 ;
let diffAngle = ( ( 0 + rot ) + 360 ) % 360 ;
let phiCorrected = ( phi + diffAngle + 360 ) % 360 ;
let resizeW = r * Math . cos ( Angle . degree2radian ( phiCorrected ) ) ;
let resizeH = - r * Math . sin ( Angle . degree2radian ( phiCorrected ) ) ;
2019-06-04 08:43:07 +02:00
if ( ( this . element . offsetWidth + resizeW ) / this . scale > this . width * 0.5 / this . scale && ( this . element . offsetHeight + resizeH ) / this . scale > this . height * 0.3 / this . scale ) TweenLite . to ( this . element , 0 , { width : this . element . offsetWidth + resizeW / this . scale , height : this . element . offsetHeight + resizeH / this . scale } ) ;
2019-03-22 12:54:57 +01:00
this . oldX = e . clientX ;
this . oldY = e . clientY ;
this . onResizing ( ) ;
this . element . dispatchEvent ( event ) ;
}
}
stopResize ( e ) {
e . preventDefault ( ) ;
let event = new CustomEvent ( 'resizeEnded' ) ;
2019-04-08 12:31:50 +02:00
let oldPostition = { x : this . element . getBoundingClientRect ( ) . left , y : this . element . getBoundingClientRect ( ) . top } ;
2019-03-22 12:54:57 +01:00
this . element . style . transformOrigin = "50% 50%" ;
2019-04-08 12:31:50 +02:00
let newPostition = { x : this . element . getBoundingClientRect ( ) . left , y : this . element . getBoundingClientRect ( ) . top } ;
2019-03-22 12:54:57 +01:00
let offset = Points . subtract ( oldPostition , newPostition ) ;
TweenLite . to ( this . element , 0 , { css : { left : "+=" + offset . x + "px" } } ) ;
TweenLite . to ( this . element , 0 , { css : { top : "+=" + offset . y + "px" } } ) ;
e . target . setAttribute ( 'resizing' , "false" ) ;
this . element . dispatchEvent ( event ) ;
}
}
DOMScatter . zIndex = 1000 ;
class CardLoader {
constructor (
src ,
{
x = 0 ,
y = 0 ,
width = 1000 ,
height = 800 ,
maxWidth = null ,
maxHeight = null ,
scale = 1 ,
minScale = 0.5 ,
maxScale = 1.5 ,
rotation = 0
} = { }
) {
this . src = src ;
this . x = x ;
this . y = y ;
this . scale = scale ;
this . rotation = 0 ;
this . maxScale = maxScale ;
this . minScale = minScale ;
this . wantedWidth = width ;
this . wantedHeight = height ;
this . maxWidth = maxWidth != null ? maxWidth : window . innerWidth ;
this . maxHeight = maxHeight != null ? maxHeight : window . innerHeight ;
this . addedNode = null ;
console . log ( {
width ,
height ,
maxWidth ,
maxHeight ,
} ) ;
}
unload ( ) {
if ( this . addedNode ) {
this . addedNode . remove ( ) ;
this . addedNode = null ;
}
}
}
class PDFLoader extends CardLoader {
constructor ( src , { width = 1640 , height = 800 , scale = 1 } = { } ) {
super ( src , { width , height , scale } ) ;
if ( typeof PDFJS == 'undefined' ) {
alert ( 'PDF.js needed' ) ;
}
}
load ( domNode ) {
return new Promise ( ( resolve , reject ) => {
PDFJS . getDocument ( this . src ) . then ( pdf => {
pdf . getPage ( 1 ) . then ( page => {
let scale = this . scale * app . renderer . resolution ;
let invScale = 1 / scale ;
let viewport = page . getViewport ( scale ) ;
// Prepare canvas using PDF page dimensions.
let canvas = document . createElement ( 'canvas' ) ;
let context = canvas . getContext ( '2d' ) ;
canvas . height = viewport . height ;
canvas . width = viewport . width ;
// Render PDF page into canvas context.
let renderContext = {
canvasContext : context ,
viewport : viewport
} ;
page . render ( renderContext ) ;
domNode . appendChild ( canvas ) ;
this . wantedWidth = canvas . width ;
this . wantedHeight = canvas . height ;
this . scale = invScale ;
this . addedNode = canvas ;
resolve ( this ) ;
} ) ;
} ) ;
} )
}
}
class ImageLoader extends CardLoader {
load ( domNode ) {
return new Promise ( ( resolve , reject ) => {
let isImage = domNode instanceof HTMLImageElement ;
let image = isImage ? domNode : document . createElement ( 'img' ) ;
image . onload = e => {
if ( ! isImage ) {
domNode . appendChild ( image ) ;
this . addedNode = image ;
}
this . wantedWidth = image . naturalWidth ;
this . wantedHeight = image . naturalHeight ;
let scaleW = this . maxWidth / image . naturalWidth ;
let scaleH = this . maxHeight / image . naturalHeight ;
this . scale = Math . min ( this . maxScale , Math . min ( scaleW , scaleH ) ) ;
image . setAttribute ( 'draggable' , false ) ;
image . width = image . naturalWidth ;
image . height = image . naturalHeight ;
resolve ( this ) ;
} ;
image . onerror = e => {
reject ( this ) ;
} ;
image . src = this . src ;
} )
}
}
class FrameLoader extends CardLoader {
load ( domNode ) {
return new Promise ( ( resolve , reject ) => {
let isFrame = domNode instanceof HTMLIFrameElement ;
let iframe = isFrame ? domNode : document . createElement ( 'iframe' ) ;
console . log ( 'FrameLoader.load' , isFrame , iframe , this . src ) ;
iframe . frameBorder = 0 ;
iframe . style . scrolling = false ;
iframe . width = this . wantedWidth ;
iframe . height = this . wantedHeight ;
if ( ! isFrame ) {
// Unlike img onload is only triggered if the iframe is part of the DOM tree
domNode . appendChild ( iframe ) ;
this . addedNode = iframe ;
}
iframe . onload = e => {
resolve ( this ) ;
} ;
iframe . onerror = e => {
reject ( this ) ;
} ;
iframe . src = this . src ;
} )
}
}
class HTMLLoader extends CardLoader {
load ( domNode ) {
return new Promise ( ( resolve , reject ) => {
let xhr = new XMLHttpRequest ( ) ;
xhr . open ( 'GET' , this . src , false ) ;
xhr . onload = e => {
domNode . innerHTML = xhr . response ;
this . addedNode = domNode . firstElementChild ;
let { width , height } = this . size ( this . addedNode ) ;
console . log ( "HTMLLoader.load" , { added : this . addedNode , width , height } ) ;
if ( width )
this . wantedWidth = width || this . wantedWidth ;
if ( height )
this . wantedHeight = height || this . wantedHeight ;
resolve ( this ) ;
} ;
xhr . onerror = e => {
reject ( this ) ;
} ;
xhr . send ( ) ;
} )
}
/ * *
* Tries to determine the size of the addedNode .
* Checks for explicit width and height style attributes .
*
* Overwrite this method if you want to extract values from other infos .
*
* @ returns { width : int , height : int }
* @ memberof HTMLLoader
* /
size ( node ) {
let width = parseInt ( node . style . width ) || null ;
let height = parseInt ( node . style . height ) || null ;
return { width , height }
}
}
class DOMFlip {
constructor (
domScatterContainer ,
flipTemplate ,
frontLoader ,
backLoader ,
{
closeOnMinScale = false ,
flipDuration = 1 ,
fadeDuration = 0.2 ,
overdoScaling = 1 ,
autoLoad = false ,
center = null ,
preloadBack = false ,
translatable = true ,
scalable = true ,
rotatable = true ,
onFront = null ,
onBack = null ,
onClose = null ,
onUpdate = null ,
onRemoved = null ,
onLoaded = null
} = { }
) {
this . domScatterContainer = domScatterContainer ;
this . id = getId$1 ( ) ;
this . flipDuration = flipDuration ;
this . fadeDuration = fadeDuration ;
this . closeOnMinScale = closeOnMinScale ;
this . flipTemplate = flipTemplate ;
this . frontLoader = frontLoader ;
this . backLoader = backLoader ;
this . translatable = translatable ;
this . scalable = scalable ;
this . rotatable = rotatable ;
this . onFrontFlipped = onFront ;
this . onBackFlipped = onBack ;
this . onClose = onClose ;
this . onRemoved = onRemoved ;
this . onUpdate = onUpdate ;
this . onLoaded = onLoaded ;
this . center = center ;
this . preloadBack = preloadBack ;
this . overdoScaling = overdoScaling ;
if ( autoLoad ) {
this . load ( ) ;
}
}
load ( ) {
return new Promise ( ( resolve , reject ) => {
let t = this . flipTemplate ;
let dom = this . domScatterContainer . element ;
let wrapper = t . content . querySelector ( '.flipWrapper' ) ;
wrapper . id = this . id ;
let clone = document . importNode ( t . content , true ) ;
dom . appendChild ( clone ) ;
// We cannot use the document fragment itself because it
// is not part of the main dom tree. After the appendChild
// call we can access the new dom element by id
this . cardWrapper = dom . querySelector ( '#' + this . id ) ;
let front = this . cardWrapper . querySelector ( '.front' ) ;
this . frontLoader . load ( front ) . then ( loader => {
this . frontLoaded ( loader ) . then ( ( obj ) => {
if ( this . onLoaded ) this . onLoaded ( ) ;
resolve ( this ) ;
} ) ;
} ) ;
} )
}
frontLoaded ( loader ) {
return new Promise ( ( resolve , reject ) => {
let scatter = new DOMScatter (
this . cardWrapper ,
this . domScatterContainer ,
{
x : loader . x ,
y : loader . y ,
startScale : loader . scale ,
scale : loader . scale ,
maxScale : loader . maxScale ,
minScale : loader . minScale ,
width : loader . wantedWidth ,
height : loader . wantedHeight ,
rotation : loader . rotation ,
translatable : this . translatable ,
scalable : this . scalable ,
rotatable : this . rotatable ,
overdoScaling : this . overdoScaling
}
) ;
if ( this . center ) {
scatter . centerAt ( this . center ) ;
}
if ( this . closeOnMinScale ) {
const removeOnMinScale = function ( ) {
if ( scatter . scale <= scatter . minScale ) {
this . flippable . close ( ) ;
// 'Disable' overdoscaling to avoid weird jumps on close.
scatter . minScale /= scatter . overdoScaling ;
scatter . overdoScaling = 1 ;
//Remove callback
if ( scatter . onTransform ) {
let callbackIdx = scatter . onTransform . indexOf ( removeOnMinScale ) ;
scatter . onTransform . splice ( callbackIdx , 1 ) ;
}
}
} . bind ( this ) ;
scatter . addTransformEventCallback ( removeOnMinScale ) ;
}
let flippable = new DOMFlippable ( this . cardWrapper , scatter , this ) ;
let back = this . cardWrapper . querySelector ( '.back' ) ;
if ( this . preloadBack ) {
this . backLoader . load ( back ) . then ( loader => {
this . setupFlippable ( flippable , loader ) ;
} ) ;
}
this . flippable = flippable ;
resolve ( this ) ;
} )
}
centerAt ( p ) {
this . center = p ;
this . flippable . centerAt ( p ) ;
}
zoom ( scale ) {
this . flippable . zoom ( scale ) ;
}
setupFlippable ( flippable , loader ) {
flippable . wantedWidth = loader . wantedWidth ;
flippable . wantedHeight = loader . wantedHeight ;
flippable . wantedScale = loader . scale ;
flippable . minScale = loader . minScale ;
flippable . maxScale = loader . maxScale ;
flippable . scaleButtons ( ) ;
}
start ( { targetCenter = null } = { } ) {
console . log ( 'DOMFlip.start' , targetCenter ) ;
if ( this . preloadBack ) this . flippable . start ( { duration : this . flipDuration , targetCenter } ) ;
else {
let back = this . cardWrapper . querySelector ( '.back' ) ;
let flippable = this . flippable ;
this . backLoader . load ( back ) . then ( loader => {
this . setupFlippable ( flippable , loader ) ;
flippable . start ( { duration : this . flipDuration , targetCenter } ) ;
} ) ;
}
}
fadeOutAndRemove ( ) {
TweenLite . to ( this . cardWrapper , this . fadeDuration , {
opacity : 0 ,
onComplete : ( ) => {
this . cardWrapper . remove ( ) ;
}
} ) ;
}
closed ( ) {
this . unload ( ) ;
}
unload ( ) {
if ( ! this . preloadBack ) {
this . backLoader . unload ( ) ;
}
}
}
class DOMFlippable {
constructor ( element , scatter , flip ) {
// Set log to console.log or a custom log function
// define data structures to store our touchpoints in
this . element = element ;
this . flip = flip ;
this . card = element . querySelector ( '.flipCard' ) ;
this . front = element . querySelector ( '.front' ) ;
this . back = element . querySelector ( '.back' ) ;
this . flipped = false ;
this . scatter = scatter ;
this . onFrontFlipped = flip . onFrontFlipped ;
this . onBackFlipped = flip . onBackFlipped ;
this . onClose = flip . onClose ;
this . onRemoved = flip . onRemoved ;
this . onUpdate = flip . onUpdate ;
this . flipDuration = flip . flipDuration ;
this . fadeDuration = flip . fadeDuration ;
scatter . addTransformEventCallback ( this . scatterTransformed . bind ( this ) ) ;
console . log ( 'lib.DOMFlippable' , 5000 ) ;
TweenLite . set ( this . element , { perspective : 5000 } ) ;
TweenLite . set ( this . card , { transformStyle : 'preserve-3d' } ) ;
TweenLite . set ( this . back , { rotationY : - 180 } ) ;
TweenLite . set ( [ this . back , this . front ] , {
backfaceVisibility : 'hidden' ,
perspective : 5000
} ) ;
TweenLite . set ( this . front , { visibility : 'visible' } ) ;
this . infoBtn = element . querySelector ( '.infoBtn' ) ;
this . backBtn = element . querySelector ( '.backBtn' ) ;
this . closeBtn = element . querySelector ( '.closeBtn' ) ;
/* Buttons are not guaranteed to exist. */
if ( this . infoBtn ) {
InteractionMapper$1 . on ( 'tap' , this . infoBtn , event => this . flip . start ( ) ) ;
this . enable ( this . infoBtn ) ;
}
if ( this . backBtn ) {
InteractionMapper$1 . on ( 'tap' , this . backBtn , event => this . start ( ) ) ;
}
if ( this . closeBtn ) {
InteractionMapper$1 . on ( 'tap' , this . closeBtn , event => this . close ( ) ) ;
this . enable ( this . closeBtn ) ;
}
this . scaleButtons ( ) ;
this . bringToFront ( ) ;
}
close ( ) {
this . disable ( this . infoBtn ) ;
this . disable ( this . closeBtn ) ;
if ( this . onClose ) {
this . onClose ( this ) ;
this . flip . closed ( ) ;
} else {
this . scatter . zoom ( 0.1 , {
animate : this . fadeDuration ,
onComplete : ( ) => {
this . element . remove ( ) ;
this . flip . closed ( ) ;
if ( this . onRemoved ) {
this . onRemoved . call ( this ) ;
}
}
} ) ;
}
}
showFront ( ) {
TweenLite . set ( this . front , { visibility : 'visible' } ) ;
}
centerAt ( p ) {
this . scatter . centerAt ( p ) ;
}
zoom ( scale ) {
this . scatter . zoom ( scale ) ;
}
get buttonScale ( ) {
let iscale = 1.0 ;
if ( this . scatter != null ) {
let scale = this . scatter . scale || 1 ;
iscale = 1.0 / scale ;
}
return iscale
}
scaleButtons ( ) {
//This also works for svgs.
// if (this.infoBtn)
// this.infoBtn.style.transform = "scale(" + this.buttonScale + ")"
// if (this.backBtn)
// this.backBtn.style.transform = "scale(" + this.buttonScale + ")"
// if (this.closeBtn)
// this.closeBtn.style.transform = "scale(" + this.buttonScale + ")"
console . log ( this . buttonScale ) ;
//// This did not work with svgs!
TweenLite . set ( [ this . infoBtn , this . backBtn , this . closeBtn ] , {
scale : this . buttonScale
} ) ;
}
bringToFront ( ) {
this . scatter . bringToFront ( ) ;
TweenLite . set ( this . element , { zIndex : DOMScatter . zIndex ++ } ) ;
}
clickInfo ( ) {
this . bringToFront ( ) ;
this . infoBtn . click ( ) ;
}
scatterTransformed ( event ) {
this . scaleButtons ( ) ;
}
targetRotation ( alpha ) {
let ortho = 90 ;
let rest = alpha % ortho ;
let delta = 0.0 ;
if ( rest > ortho / 2.0 ) {
delta = ortho - rest ;
} else {
delta = - rest ;
}
return delta
}
infoValues ( info ) {
let startX = this . element . _gsTransform . x ;
let startY = this . element . _gsTransform . y ;
let startAngle = this . element . _gsTransform . rotation ;
let startScale = this . element . _gsTransform . scaleX ;
let w = this . element . style . width ;
let h = this . element . style . height ;
console . log ( info , startX , startY , startAngle , startScale , w , h ) ;
}
show ( element , duration = 0 , alpha = 1 ) {
if ( element ) {
TweenLite . to ( element , duration , { autoAlpha : alpha } ) ; // visibility: 'visible', display: 'initial'})
}
}
hide ( element , duration = 0 , alpha = 0 ) {
if ( element ) {
TweenLite . to ( element , duration , { autoAlpha : alpha } ) ; // {visibility: 'hidden', display: 'none'})
}
}
enable ( button ) {
this . show ( button , this . fadeDuration ) ;
if ( button ) {
TweenLite . set ( button , { pointerEvents : 'auto' } ) ;
}
}
disable ( button ) {
this . hide ( button , this . fadeDuration ) ;
if ( button ) {
TweenLite . set ( button , { pointerEvents : 'none' } ) ;
}
}
start ( { targetCenter = null } = { } ) {
this . bringToFront ( ) ;
if ( ! this . flipped ) {
this . startX = this . element . _gsTransform . x ;
this . startY = this . element . _gsTransform . y ;
this . startAngle = this . element . _gsTransform . rotation ;
this . startScale = this . element . _gsTransform . scaleX ;
this . startWidth = this . element . style . width ;
this . startHeight = this . element . style . height ;
this . scatterStartWidth = this . scatter . width ;
this . scatterStartHeight = this . scatter . height ;
this . show ( this . back ) ;
this . disable ( this . infoBtn ) ;
this . disable ( this . closeBtn ) ;
} else {
this . show ( this . front , this . fadeDuration ) ;
this . disable ( this . backBtn ) ;
}
let { scalable , translatable , rotatable } = this . scatter ;
this . saved = { scalable , translatable , rotatable } ;
this . scatter . scalable = false ;
this . scatter . translatable = false ;
this . scatter . rotatable = false ;
this . scatter . killAnimation ( ) ;
this . flipped = ! this . flipped ;
let targetY = this . flipped ? 180 : 0 ;
let targetZ = this . flipped
? this . startAngle + this . targetRotation ( this . startAngle )
: this . startAngle ;
let targetScale = this . flipped ? this . wantedScale : this . startScale ;
let w = this . flipped ? this . wantedWidth : this . startWidth ;
let h = this . flipped ? this . wantedHeight : this . startHeight ;
let dw = this . wantedWidth - this . scatter . width ;
let dh = this . wantedHeight - this . scatter . height ;
let tc = targetCenter ;
let xx = tc != null ? tc . x - w / 2 : this . startX - dw / 2 ;
let yy = tc != null ? tc . y - h / 2 : this . startY - dh / 2 ;
let x = this . flipped ? xx : this . startX ;
let y = this . flipped ? yy : this . startY ;
console . log ( "DOMFlippable.start" , this . flipped , targetCenter , x , y , this . saved ) ;
let onUpdate = this . onUpdate !== null ? ( ) => this . onUpdate ( this ) : null ;
console . log ( this . flipDuration ) ;
TweenLite . to ( this . card , this . flipDuration , {
rotationY : targetY ,
ease : Power1 . easeOut ,
transformOrigin : '50% 50%' ,
onUpdate ,
onComplete : e => {
if ( this . flipped ) {
//this.hide(this.front)
this . enable ( this . backBtn ) ;
this . show ( this . backBtn ) ;
if ( this . onFrontFlipped ) {
this . onFrontFlipped ( this ) ;
}
} else {
if ( this . onBackFlipped == null ) {
this . enable ( this . infoBtn , this . fadeDuration ) ;
this . enable ( this . closeBtn , this . fadeDuration ) ;
} else {
this . onBackFlipped ( this ) ;
}
this . flip . unload ( ) ;
}
this . scatter . scale = targetScale ;
this . scaleButtons ( ) ;
this . scatter . rotationDegrees = targetZ ;
this . scatter . width = this . flipped ? w : this . scatterStartWidth ;
this . scatter . height = this . flipped ? h : this . scatterStartHeight ;
let { scalable , translatable , rotatable } = this . saved ;
this . scatter . scalable = scalable ;
this . scatter . translatable = translatable ;
this . scatter . rotatable = rotatable ;
} ,
force3D : true
} ) ;
// See https://greensock.com/forums/topic/7997-rotate-the-shortest-way/
TweenLite . to ( this . element , this . flipDuration / 2 , {
scale : targetScale ,
ease : Power1 . easeOut ,
rotationZ : targetZ + '_short' ,
transformOrigin : '50% 50%' ,
width : w ,
height : h ,
x : x ,
y : y ,
onComplete : e => {
if ( this . flipped ) {
this . hide ( this . front ) ;
// this.hide(this.infoBtn)
} else {
this . hide ( this . back ) ;
// this.show(this.infoBtn)
}
}
} ) ;
}
}
class Index {
constructor ( template , pages , notfound = 'thumbnails/notfound.png' ) {
this . template = template ;
this . pages = pages ;
this . notfound = notfound ;
}
setup ( ) {
for ( let pair of this . pages ) {
let [ title , src ] = pair ;
let id = getId ( ) ;
pair . push ( id ) ;
let t = this . template ;
let wrapper = t . content . querySelector ( '.wrapper' ) ;
wrapper . id = id ;
let clone = document . importNode ( t . content , true ) ;
container . appendChild ( clone ) ;
wrapper = container . querySelector ( '#' + id ) ;
let icon = wrapper . querySelector ( '.icon' ) ;
icon . onerror = ( e ) => {
if ( this . notfound )
icon . src = this . notfound ;
} ;
let iconSrc = src . replace ( '.html' , '.png' ) ;
//console.log("iconSrc", iconSrc)
if ( iconSrc . endsWith ( 'index.png' ) ) {
icon . src = iconSrc . replace ( 'index.png' , 'thumbnail.png' ) ;
2019-05-28 13:54:50 +02:00
}
else {
2019-03-22 12:54:57 +01:00
icon . src = 'thumbnails/' + iconSrc ;
}
2019-05-28 13:54:50 +02:00
// icon.src = 'thumbnails/' + iconSrc
// console.log(iconSrc)
2019-03-22 12:54:57 +01:00
wrapper . href = src ;
let titleDiv = wrapper . querySelector ( '.title' ) ;
titleDiv . innerText = title ;
}
}
frames ( ) {
if ( this . pages . length == 0 )
return
let [ title , src , id ] = this . pages . shift ( ) ;
let iframe = document . createElement ( 'iframe' ) ;
iframe . frameborder = 0 ;
let wrapper = document . getElementById ( id ) ;
let icon = wrapper . querySelector ( '.icon' ) ;
icon . parentNode . replaceChild ( iframe , icon ) ;
iframe . onload = ( e ) => {
this . frames ( ) ;
} ;
iframe . src = src + window . location . search ;
}
load ( ) {
this . setup ( ) ;
if ( window . location . search . startsWith ( '?test' ) )
this . frames ( ) ;
}
loadAndTest ( ) {
this . setup ( ) ;
if ( ! Capabilities . isMobile )
this . frames ( ) ;
}
}
/ * * B a s i c c l a s s f o r p o p p a b l e e l e m e n t s t h a t n e e d t o b e c l o s e d a s s o o n a s o n e p o p p a b l e i s
* shown .
* /
class Poppable {
/ * * R e g i s t e r t h e p o p p a b l e e l e m e n t i n a c o n t e x t . C l o s e s p r e v i o u s l y r e g i s t e r e d o n e s .
* @ param { * } context
* /
register ( context ) {
let registered = Poppable . get ( context ) ;
if ( registered != null ) {
registered . close ( ) ;
}
Poppable . set ( context , this ) ;
}
/ * *
* Unregister object from context
* @ param { * } context
* /
unregister ( context ) {
Poppable . delete ( context ) ;
}
/ * *
* Returns the given poppable in a context
* @ static
* @ param { * } context
* @ returns
* @ memberof Poppable
* /
static get ( context ) {
return Poppable . registrations . get ( context )
}
/ * * S e t s t h e p o p p a b l e i n t h e g i v e n c o n t e x t
* @ static
* @ param { * } context
* @ param { * } poppable
* @ returns
* @ memberof Poppable
* /
static set ( context , poppable ) {
return Poppable . registrations . set ( context , poppable )
}
/ * * T e s t w h e t h e r a p o p p a b l e e x i s t s i n t h e g i v e n c o n t e x t
*
* @ param { * } context
* /
static has ( context ) {
return Poppable . registrations . has ( context )
}
/ * *
* Removes the poppable from the given context .
*
* @ static
* @ param { * } context
* @ memberof Poppable
* /
static delete ( context ) {
Poppable . registrations . delete ( context ) ;
}
/** All poppable must implement a close method. */
close ( ) {
console . error ( "Must be implemented" ) ;
}
}
Poppable . registrations = new Map ( ) ;
/ * * A P o p u p t h a t s h o w s t e x t l a b e l s , i m a g e s , o r h t m l
* /
class Popup extends Poppable {
/ * *
* Creates an instance of Popup .
* @ param { any } [ {
* parent = null , - The DOM parent element .
* content = null , - A dict object with type strings ( text , img , html ) as keys
* and corresponding values .
* context = window , - A context object for poppable elements
* fontSize = "1em" , - Describes the font size as CSS value
* fontFamily = "Arial" , - Describes the font family as CSS value
* padding = 16 , - { number || string } padding - Describes the padding as CSS value
* notchSize = 10 , - { number || string } notchSize - Describes the size of the notch ( callout ) as CSS value
* switchPos = false ,
* minWidth = null ,
* maxWidth = 800 ,
* backgroundColor = "#EEE" , - The color of the background as CSS value
* normalColor = "#444" , - normalColor - The color of textitems as CSS value
* notchPosition = "bottomLeft" ,
* zIndex = 0 ,
* keepWithin = null , - Ensure that popup is visible within the bounds of the given container
* autoClose = true , - Autoclose the Popup on tap
* closeIcon = null ,
* resizeIcon = null ,
* closeCommand = null ,
* draggable = false
* noStyle = false - When true , prevents the popup from doing any aesthetic manipulations to the DOM leaving the styling completely to the style sheets .
* } = { } ]
* @ memberof Popup
* /
constructor ( {
parent = null ,
content = null ,
context = window ,
fontSize = "1em" ,
fontFamily = "Arial" ,
padding = 16 ,
notchSize = 10 ,
switchPos = false ,
minWidth = null ,
maxWidth = 800 ,
backgroundColor = "#EEE" ,
normalColor = "#444" ,
notchPosition = "bottomCenter" ,
zIndex = 0 ,
keepWithin = null ,
autoClose = true ,
closeIcon = null ,
resizeIcon = null ,
closeCommand = null ,
draggable = false ,
posOffset = 0 ,
targetBoundingBox = null ,
useEventPosWithBoundingBox = false ,
interactive = false ,
onResize = null ,
onMove = null ,
noStyle = false ,
hideOnUp = true ,
} = { } ) {
super ( ) ;
this . context = context ;
this . noStyle = noStyle ;
this . hideOnUp = hideOnUp ;
this . padding = padding ;
this . notchPosition = notchPosition ;
this . notchSize = notchSize ;
this . switchPos = switchPos ;
this . fontSize = fontSize ;
this . fontFamily = fontFamily ;
this . minWidth = minWidth ;
this . maxWidth = maxWidth ;
this . normalColor = normalColor ;
this . backgroundColor = backgroundColor ;
this . keepWithin = keepWithin ;
this . autoClose = autoClose ;
this . resizeIcon = resizeIcon ;
this . closeIcon = closeIcon ;
this . closeCommand = closeCommand ;
this . zIndex = zIndex ;
this . parent = parent || document . body ;
this . draggable = draggable ;
this . posOffset = posOffset ;
this . targetBoundingBox = targetBoundingBox ;
this . useEventPosWithBoundingBox = useEventPosWithBoundingBox ;
this . currentPos = null ;
this . insertedNode = null ;
this . loaded = false ;
this . interactive = interactive ;
this . onload = null ;
this . onResize = onResize ;
this . onMove = onMove ;
if ( content ) {
this . show ( content ) ;
}
}
/ * * S e t u p p o p u p w i t h a d i c t i o n a r y o f c o n t e n t t y p e s a n d c o n t e n t s .
* @ param { Object } content - A dict object with type strings ( text , img , html ) as keys
* and corresponding values .
* @ return { Popup } this
* /
setup ( content ) {
//console.log("Popup.setup", this.draggable)
this . content = content ;
this . items = { } ;
this . element = document . createElement ( "div" ) ;
this . element . classList . add ( "popup" ) ;
this . setAlpha ( this . element , 0 ) ;
// this.element.style.opacity = 0
Elements$1 . addClass ( this . element , "unselectable" ) ;
this . notch = document . createElement ( "div" ) ;
Elements$1 . setStyle ( this . notch , this . notchStyle ( ) ) ;
this . notch . className = "notch" ;
this . setupDraggable ( ) ;
if ( this . closeIcon ) {
let img = document . createElement ( "img" ) ;
img . setAttribute ( "draggable" , false ) ;
img . src = this . closeIcon ;
img . style . position = "absolute" ;
img . style . right = "0px" ;
img . style . top = "0px" ;
img . style . width = "16px" ;
img . style . height = "16px" ;
img . onclick = e => {
this . close ( ) ;
} ;
this . element . appendChild ( img ) ;
}
if ( this . resizeIcon ) {
let img = document . createElement ( "img" ) ;
img . style . position = "absolute" ;
img . style . right = "0px" ;
img . style . bottom = "0px" ;
img . style . width = "16px" ;
img . style . height = "16px" ;
img . src = this . resizeIcon ;
img . setAttribute ( "draggable" , true ) ;
img . ondragstart = e => {
this . currentPos = { x : e . clientX , y : e . clientY } ;
return true
} ;
img . ondrag = e => {
e . preventDefault ( ) ;
let target = this . element . querySelector ( "iframe" ) || this . element ;
let delta = {
x : e . clientX - this . currentPos . x ,
y : e . clientY - this . currentPos . y
} ;
this . currentPos = { x : e . clientX , y : e . clientY } ;
if ( delta . x == 0 && delta . y == 0 )
return
let rect = target . getBoundingClientRect ( ) ;
let width = rect . width + delta . x ;
let height = rect . height + delta . y ;
target . style . width = width + "px" ;
target . style . height = height + "px" ;
switch ( this . notchPosition ) {
case "bottomLeft" :
case "bottomCenter" :
let bottom = parseFloat ( this . element . style . bottom ) ;
this . element . style . bottom = bottom - delta . y + "px" ;
break
default :
break
}
//console.log("onResize", this.onResize)
if ( this . onResize ) {
this . onResize ( { target , delta , width , height } ) ;
}
} ;
img . ondragend = e => { } ;
this . element . appendChild ( img ) ;
}
for ( let key in content ) {
switch ( key ) {
case "selector" :
break
case "text" :
let text = document . createElement ( "span" ) ;
this . element . appendChild ( text ) ;
text . innerHTML = content [ key ] ;
Elements$1 . setStyle ( text , { color : this . normalColor } ) ;
Elements$1 . addClass ( text , "unselectable" ) ;
Elements$1 . addClass ( text , "PopupContent" ) ;
this . insertedNode = text ;
this . loaded = true ;
break
case "img" :
alert ( "img to be implemented" ) ;
break
case "iframe" :
let iframe = document . createElement ( "iframe" ) ;
iframe . setAttribute ( "frameBorder" , 0 ) ;
iframe . src = content [ key ] ;
iframe . onload = e => {
let body = iframe . contentWindow . document . body ;
let observer = new MutationObserver ( ( ) => {
this . iframeChanged ( iframe ) ;
} ) ;
observer . observe ( iframe . contentWindow . document , {
attributes : true ,
subtree : true ,
childList : true ,
characterData : true
} ) ;
let w = Math . max ( body . scrollWidth , body . offsetWidth ) ;
let h = Math . max ( body . scrollHeight , body . offsetHeight ) ;
iframe . style . width = w + "px" ;
iframe . style . height = h + "px" ;
this . layoutAfterInsert ( ) ;
if ( this . onload != null ) {
this . onload ( ) ;
}
this . loaded = true ;
} ;
this . element . appendChild ( iframe ) ;
Elements$1 . addClass ( iframe , "PopupContent" ) ;
this . insertIntoDOM ( ) ;
return
case "html" :
this . loaded = false ;
let div = document . createElement ( "div" ) ;
Elements$1 . addClass ( div , "PopupContent" ) ;
this . element . appendChild ( div ) ;
div . innerHTML = content . html ;
//console.log("insert", content)
let selector = content . selector ;
if ( selector ) {
this . insertedNode = div . querySelector ( selector ) ;
if ( this . insertedNode == null ) {
div . innerHTML = ` <p style="color:red;">Popup content not found. Missing ${ selector } </p> ` ;
this . insertedNode = div . firstElementChild ;
}
}
else {
this . insertedNode = div . firstElementChild || div ;
}
this . setAlpha ( this . insertedNode , 0 ) ;
let images = this . element . querySelectorAll ( 'img' ) ;
let total = 0 ;
if ( images . length > 0 ) {
let count = 0 ;
for ( let image of images ) {
if ( ! image . complete && ! image . src . startsWith ( 'data:' ) ) {
total += 1 ;
console . log ( "image not complete" , image . src ) ;
image . onload = e => {
count += 1 ;
if ( count == total ) {
this . loaded = true ;
if ( this . onload != null ) {
this . onload ( ) ;
}
}
} ;
}
}
}
if ( total == 0 ) {
this . loaded = true ;
}
break
case "node" :
this . loaded = true ;
Elements$1 . addClass ( content . node , "PopupContent" ) ;
this . element . appendChild ( content . node ) ;
this . insertedNode = content . node ;
this . setAlpha ( this . insertedNode , 0 ) ;
break
default :
alert ( "Unexpected content type: " + key ) ;
break
}
}
this . insertIntoDOM ( ) ;
this . layoutAfterInsert ( ) ;
this . setupEventHandler ( ) ;
return this
}
handleClose ( e ) {
let closing = this . closingEvent ( e ) ;
if ( closing ) {
this . close ( ) ;
}
else {
this . setupCloseHandler ( ) ;
}
}
setupCloseHandler ( ) {
let close = this . handleClose ;
if ( this . hideOnUp ) {
if ( window . PointerEvent )
this . parent . addEventListener ( "pointerup" , close . bind ( this ) , { capture : true , once : true } ) ;
else if ( window . TouchEvent )
this . parent . addEventListener ( "touchend" , close . bind ( this ) , { capture : true , once : true } ) ;
else
this . parent . addEventListener ( "mouseup" , close . bind ( this ) , { capture : true , once : true } ) ;
} else {
if ( window . PointerEvent )
this . parent . addEventListener ( "pointerdown" , close . bind ( this ) , { capture : true , once : true } ) ;
else if ( window . TouchEvent )
this . parent . addEventListener ( "touchstart" , close . bind ( this ) , { capture : true , once : true } ) ;
else
this . parent . addEventListener ( "mousedown" , close . bind ( this ) , { capture : true , once : true } ) ;
}
}
setupEventHandler ( ) {
if ( this . autoClose ) {
this . setupCloseHandler ( ) ;
}
}
closingEvent ( e ) {
if ( this . interactive ) {
let node = e . target . closest ( ".PopupContent" ) ;
return node == null
}
return true
}
iframeChanged ( iframe ) {
let body = iframe . contentWindow . document . body ;
let w = Math . max ( body . scrollWidth , body . offsetWidth ) ;
let h = Math . max ( body . scrollHeight , body . offsetHeight ) ;
iframe . style . width = w + "px" ;
iframe . style . height = h + "px" ;
}
setupDraggable ( ) {
if ( this . draggable ) {
let target = this . element ;
target . setAttribute ( "draggable" , true ) ;
target . ondragstart = e => {
this . currentPos = { x : e . clientX , y : e . clientY } ;
var img = document . createElement ( 'img' ) ;
img . src = 'data:image/gifbase64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' ;
e . dataTransfer . setDragImage ( img , 0 , 0 ) ;
} ;
target . ondrag = e => {
e . preventDefault ( ) ;
let delta = {
x : e . clientX - this . currentPos . x ,
y : e . clientY - this . currentPos . y
} ;
this . currentPos = { x : e . clientX , y : e . clientY } ;
let left = parseFloat ( target . style . left ) ;
let top = parseFloat ( target . style . top ) ;
target . style . left = left + delta . x + 'px' ;
target . style . top = top + delta . y + 'px' ;
//console.log("Popup.ondrag", target, event.target)
if ( this . onMove ) {
this . onMove ( { target , delta } ) ;
}
this . lastDrag = { left , top } ;
} ;
target . ondragend = e => {
target . style . left = this . lastDrag . left + 'px' ;
target . style . top = this . lastDrag . top + 'px' ;
this . currentPos = null ;
} ;
}
}
moveDragged ( target ) {
}
insertIntoDOM ( layout = true ) {
this . setAlpha ( this . insertedNode , 0 ) ;
this . element . appendChild ( this . notch ) ;
this . parent . appendChild ( this . element ) ;
}
layoutAfterInsert ( ) {
Elements$1 . setStyle ( this . element , this . defaultStyle ( ) ) ;
this . layout ( ) ;
//this.element.style.opacity = 1
}
/ * * L a y o u t t h e m e n u i t e m s . N e e d e d o n l y i n t h e s u b c l a s s .
* /
layout ( ) { }
remove ( ) {
if ( this . parent . contains ( this . element ) )
this . parent . removeChild ( this . element ) ;
this . unregister ( this . context ) ;
}
/ * * C l o s e a n d r e m o v e t h e P o p u p f r o m t h e D O M t r e e .
* /
close ( ) {
//console.log("Popup.close", this.closeCommand)
this . unregister ( this . context ) ;
if ( this . closeCommand ) {
this . closeCommand ( this , ( ) => this . remove ( ) ) ;
}
else {
this . remove ( ) ;
}
}
/ * *
* Set the alpha value to show or hide the popup . Uses CSS transitions .
* ( A former dependency on TweenLite has beeen removed . )
*
* @ param { * } targets
* @ param { * } value
* @ memberof Popup
* /
setAlpha ( targets , value ) {
let objs = ( targets instanceof Array ) ? targets : [ targets ] ;
for ( let obj of objs ) {
if ( value ) {
obj . style . transition = "opacity 0.2s ease-in" ;
}
obj . style . opacity = value ;
}
// if (value) {
// TweenLite.to(targets, 0.2, { autoAlpha: value })
// }
// else {
// TweenLite.set(targets, { autoAlpha: 0 })
// }
}
/ * *
* Starts a fade in animation .
*
* @ memberof Popup
* /
fadeIn ( ) {
this . setAlpha ( [ this . element , this . insertedNode ] , 1 ) ;
}
/ * * S h o w s t h e P o p u p w i t h t h e g i v e n c o m m a n d s a t t h e s p e c i f i e d p o i n t .
* @ param { Object } content - A dict object with type strings ( text , img , html ) as keys
* and corresponding values .
* @ param { Point } point - The position as x , y coordinates { px } .
* @ return { Popup } this
* /
showAt ( content , point ) {
this . setup ( content ) ;
console . log ( "showAt" , this . loaded ) ;
if ( this . loaded ) {
this . placeAt ( point ) ;
this . fadeIn ( ) ;
}
else {
this . setAlpha ( [ this . element , this . insertedNode ] , 0 ) ;
this . onload = ( ) => {
this . layoutAfterInsert ( ) ;
this . placeAt ( point ) ;
this . fadeIn ( ) ;
} ;
}
return this
}
/ * *
* Place the origin , i . e . the upper left corner at the given position using CSS styles .
*
* @ param { any } x
* @ param { any } y
* @ memberof Popup
* /
placeOrigin ( x , y ) {
Elements$1 . setStyle ( this . element , { left : x + "px" , top : y + "px" } ) ;
}
/ * *
* Calculate the local coordinates within the keepWithin container .
*
* @ param { any } x
* @ param { any } y
* @ returns
* @ memberof Popup
* /
localPointWithin ( x , y , width , height ) {
let pt = { x , y } ;
return pt
}
withinDimensions ( ) {
return {
width : this . keepWithin . offsetWidth ,
height : this . keepWithin . offsetHeight
}
}
localDimensions ( ) {
return {
width : this . element . offsetWidth ,
height : this . element . offsetHeight
}
}
/ * *
* Returns the notch position depending on the local coordinates within the keepWithin container
* Divides the space vertically into top , center , bottom and horizontally into left , center , right
*
* @ param { any } x
* @ param { any } y
* @ returns
* @ memberof Popup
* /
notchPositionWithin ( x , y ) {
let horizontal = "Center" ;
let vertical = "center" ;
let { width , height } = this . withinDimensions ( ) ;
let local = this . localPointWithin ( x , y , width , height ) ;
if ( local . y < height * 0.33 ) {
vertical = "top" ;
}
if ( local . y > height * 0.66 ) {
vertical = "bottom" ;
}
if ( local . x < width * 0.33 ) {
horizontal = "Left" ;
}
if ( local . x > width * 0.66 ) {
horizontal = "Right" ;
}
let result = vertical + horizontal ;
if ( result == "centerCenter" )
return this . notchPosition
return result
}
placeAt ( point ) {
let x = point . x ;
let y = point . y ;
let notchPosition = this . notchPosition ;
if ( this . keepWithin != null ) {
notchPosition = this . notchPositionWithin ( x , y ) ;
}
Elements$1 . setStyle ( this . notch , this . notchStyle ( notchPosition ) ) ;
this . notch . className = "notch " + notchPosition ;
let { width , height } = this . localDimensions ( ) ;
//if targetBoundingBox is set, popup is placed next to the rectangle
if ( this . targetBoundingBox ) {
let bbTop = this . targetBoundingBox . y ;
let bbBottom = this . targetBoundingBox . y + this . targetBoundingBox . height ;
let bbLeft = this . targetBoundingBox . x ;
let bbRight = this . targetBoundingBox . x + this . targetBoundingBox . width ;
//console.log("place popup with bb set:", x, y, bbTop, bbBottom, bbLeft, bbRight)
switch ( notchPosition ) {
case "bottomLeft" :
case "bottomRight" :
case "bottomCenter" :
y = bbTop ;
if ( ! this . useEventPosWithBoundingBox )
x = ( bbLeft + bbRight ) / 2 ;
break
case "topLeft" :
case "topRight" :
case "topCenter" :
y = bbBottom ;
if ( ! this . useEventPosWithBoundingBox )
x = ( bbLeft + bbRight ) / 2 ;
break
case "centerRight" :
x = bbLeft ;
if ( ! this . useEventPosWithBoundingBox )
y = ( bbTop + bbBottom ) / 2 ;
break
case "centerLeft" :
x = bbRight ;
if ( ! this . useEventPosWithBoundingBox )
y = ( bbTop + bbBottom ) / 2 ;
break
default :
break
}
}
//calculate position depending on several (optional) parameters
switch ( notchPosition ) {
case "bottomLeft" :
x -= this . padding ;
x -= this . notchSize ;
y -= height ;
y -= this . notchSize * 2 ;
y -= this . posOffset ;
break
case "bottomRight" :
x -= width ;
x += this . padding ;
x += this . notchSize ;
y -= height ;
y -= this . notchSize * 2 ;
y -= this . posOffset ;
break
case "bottomCenter" :
x -= width / 2 ;
y -= height ;
y -= this . notchSize * 2 ;
y -= this . posOffset ;
break
case "topLeft" :
x -= this . padding ;
x -= this . notchSize ;
y += this . notchSize * 2 ;
y += this . posOffset ;
break
case "topRight" :
x -= width ;
x += this . padding ;
x += this . notchSize ;
y += this . notchSize * 2 ;
y += this . posOffset ;
break
case "topCenter" :
x -= width / 2 ;
y += this . notchSize * 2 ;
y += this . posOffset ;
break
case "centerRight" :
x -= width + this . notchSize * 2 ;
x -= this . posOffset ;
y -= height / 2 ;
break
case "centerLeft" :
//console.log("height", height)
y -= height / 2 ;
x += this . notchSize * 2 ;
x += this . posOffset ;
break
default :
break
}
this . placeOrigin ( x , y ) ;
}
/ * * S h o w s t h e P o p u p w i t h t h e g i v e n c o m m a n d s a t t h e c u r r e n t p o s i t i o n .
* @ param { Object } content - A dict object with type strings ( text , img , html ) as keys
* and corresponding values .
* @ return { Popup } this
* /
show ( content ) {
this . setup ( content ) ;
this . fadeIn ( ) ;
return this
}
/ * * C o n f i g u r a t i o n o b j e c t . R e t u r n d e f a u l t s t y l e s a s C S S v a l u e s .
* /
defaultStyle ( ) {
let padding = this . padding ;
let style = {
maxWidth : this . maxWidth + "px" ,
zIndex : this . zIndex ,
position : "absolute" ,
} ;
if ( this . minWidth ) {
style . minWidth = this . minWidth + "px" ;
}
if ( ! this . noStyle ) {
Object . assign ( style , {
borderRadius : Math . round ( this . padding / 2 ) + "px" ,
backgroundColor : this . backgroundColor ,
padding : this . padding + "px" ,
boxShadow : "0 10px 15px rgba(0, 0, 0, 0.3)" ,
fontFamily : this . fontFamily ,
fontSize : this . fontSize ,
stroke : "black" ,
fill : "white"
} ) ;
}
return style
}
/ * * C o n f i g u r a t i o n o b j e c t . R e t u r n n o t c h s t y l e s a s C S S v a l u e s .
* /
notchStyle ( notchPosition = null ) {
if ( notchPosition == null ) {
notchPosition = this . notchPosition ;
}
let width = 0 ;
let height = 0 ;
let left = this . padding ;
let size = this . localDimensions ( ) ;
if ( notchPosition . endsWith ( "Right" ) ) {
left = size . width - this . padding - this . notchSize * 2 ;
}
if ( notchPosition . endsWith ( "Center" ) ) {
left = size . width / 2 - this . notchSize ;
}
left = Math . round ( left ) + 'px' ;
if ( notchPosition . startsWith ( "bottom" ) ) {
if ( this . noStyle ) {
return {
width ,
height ,
left ,
bottom : - this . notchSize + "px" ,
position : "absolute" ,
borderStyle : "solid" ,
borderTopWidth : this . notchSize + "px" ,
borderRight : this . notchSize + "px solid transparent" ,
borderLeft : this . notchSize + "px solid transparent" ,
borderBottom : 0
}
} else {
return {
width ,
height ,
left ,
boxShadow : "0 12px 15px rgba(0, 0, 0, 0.1)" ,
bottom : - this . notchSize + "px" ,
position : "absolute" ,
borderTop : this . notchSize + "px solid " + this . backgroundColor ,
borderRight : this . notchSize + "px solid transparent" ,
borderLeft : this . notchSize + "px solid transparent" ,
borderBottom : 0
}
}
}
if ( notchPosition . startsWith ( "top" ) ) {
if ( this . noStyle ) {
return {
width ,
height ,
left ,
top : - this . notchSize + "px" ,
position : "absolute" ,
borderStyle : "solid" ,
borderBottomWidth : this . notchSize + "px" ,
borderRight : this . notchSize + "px solid transparent" ,
borderLeft : this . notchSize + "px solid transparent" ,
borderTop : 0
}
} else {
return {
width ,
height ,
left ,
top : - this . notchSize + "px" ,
position : "absolute" ,
borderBottom : this . notchSize + "px solid " + this . backgroundColor ,
borderRight : this . notchSize + "px solid transparent" ,
borderLeft : this . notchSize + "px solid transparent" ,
borderTop : 0
}
}
}
if ( this . noStyle ) {
if ( notchPosition . endsWith ( "Left" ) ) {
left = - this . notchSize * 2 + "px" ;
}
if ( notchPosition . endsWith ( "Right" ) ) {
left = size . width + "px" ;
}
let top = size . height / 2 - this . notchSize ;
top = Math . round ( top ) + 'px' ;
return {
width ,
height ,
left ,
top ,
borderRightWidth : this . notchSize ,
borderLeftWidth : this . notchSize ,
position : "absolute" ,
borderTop : this . notchSize + "px solid transparent" ,
borderBottom : this . notchSize + "px solid transparent"
}
} else {
let borderRight = this . notchSize + "px solid transparent" ;
let borderLeft = this . notchSize + "px solid transparent" ;
let top = size . height / 2 - this . notchSize ;
if ( notchPosition . endsWith ( "Left" ) ) {
left = - this . notchSize * 2 + "px" ;
borderRight = this . notchSize + "px solid " + this . backgroundColor ;
this . element . style . boxShadow = "15px 10px 15px rgba(0, 0, 0, 0.3)" ;
}
if ( notchPosition . endsWith ( "Right" ) ) {
left = size . width + "px" ;
borderLeft = this . notchSize + "px solid " + this . backgroundColor ;
this . element . style . boxShadow = "15px 5px 15px rgba(0, 0, 0, 0.3)" ;
}
top = Math . round ( top ) + 'px' ;
return {
width ,
height ,
left ,
top ,
borderRight ,
borderLeft ,
// boxShadow,
position : "absolute" ,
borderTop : this . notchSize + "px solid transparent" ,
borderBottom : this . notchSize + "px solid transparent"
}
}
}
/ * * C o n v e n i e n t s t a t i c m e t h o d s t o s h o w a n d r e u s e a P o p u p i m p l e m e n t e d
* as a class variable .
* @ param { Object } content - A dict object with type strings ( text , img , html ) as keys
* and corresponding values .
* @ param { Point } point - The position as x , y coordinates { px } .
* @ param { boolean } autoClose - Autoclose the menu after selecting an item .
* /
static open (
content ,
point ,
{
parent = null ,
context = window ,
fontSize = "1em" ,
fontFamily = "Arial" ,
padding = 16 ,
notchSize = 10 ,
switchPos = false ,
minWidth = null ,
maxWidth = 800 ,
backgroundColor = "#EEE" ,
zIndex = 0 ,
normalColor = "#444" ,
closeIcon = null ,
resizeIcon = null ,
closeCommand = null ,
autoClose = true ,
keepWithin = null ,
draggable = false ,
posOffset = 0 ,
targetBoundingBox = null ,
useEventPosWithBoundingBox = false ,
interactive = false ,
onResize = null ,
onMove = null
} = { }
) {
let notchPosition = ( switchPos && point . y < 50 ) ? "topCenter" : "bottomCenter" ;
let popup = new Popup ( {
parent ,
context ,
fontFamily ,
fontSize ,
padding ,
notchSize ,
switchPos ,
minWidth ,
maxWidth ,
backgroundColor ,
normalColor ,
notchPosition ,
zIndex ,
autoClose ,
keepWithin ,
closeCommand ,
closeIcon ,
resizeIcon ,
draggable ,
posOffset ,
targetBoundingBox ,
useEventPosWithBoundingBox ,
interactive ,
onResize ,
onMove
} ) ;
popup . register ( context ) ;
popup . showAt ( content , point ) ;
return popup
}
/ * * C o n v e n i e n t s t a t i c m e t h o d t o c l o s e t h e P o p u p i m p l e m e n t e d
* as a class variable . Calls the close command .
* /
static closePopup ( context = window ) {
let popup = Poppable . registrations . get ( context ) ;
if ( popup != null ) {
popup . close ( ) ;
}
}
/ * * C o n v e n i e n t s t a t i c m e t h o d s t o r e m o v e t h e P o p u p i m p l e m e n t e d
* as a class variable . Removes the popup without performing the close command .
* /
static remove ( context = window ) {
let popup = Poppable . registrations . get ( context ) ;
if ( popup != null ) {
popup . remove ( ) ;
}
}
/ * *
* Convenient static method to compute the clicked rect of objects that have multiple clients rects .
* Needed to position popups correctly above objects with line breaks , e . g . spans
*
* @ static
* @ param { * } event
* @ returns { * } DOMRect
* @ memberof Popup
* /
static targetRect ( event ) {
let target = event . target ;
let x = event . pageX ;
let y = event . pageY ;
for ( let rect of target . getClientRects ( ) ) {
let withinX = x >= rect . left && x <= rect . left + rect . width ;
let withinY = y >= rect . top && y <= rect . top + rect . height ;
if ( withinX && withinY ) {
return rect
}
}
return null
}
/ * *
* Convenient static method to compute the center of objects that have multiple clients rects .
* Needed to position popups correctly above objects with line breaks , e . g . spans
*
* @ static
* @ param { * } event
* @ returns { * } Point
* @ memberof Popup
* /
static targetCenter ( event ) {
let target = event . target ;
let x = event . pageX ;
let y = event . pageY ;
let rect = Popup . targetRect ( event ) ;
if ( rect != null ) {
x = rect . left + rect . width / 2 ;
y = rect . top + rect . height / 2 ;
}
return { x , y }
}
}
/ * * A P o p u p M e n u t h a t s h o w s t e x t l a b e l s i n a v e r t i c a l r o w .
* /
class PopupMenu$1 extends Popup {
/ * *
* The constructor .
* @ constructor
* @ param { DOM Element } parent - The DOM parent element .
* @ param { Object } commands - A dict object with command label strings as keys
* and command functions as values .
* @ param { string } fontSize - Describes the font size as CSS value
* @ param { number || string } padding - Describes the padding as CSS value
* @ param { number || string } notchSize - Describes the size of the notch ( callout ) as CSS value
* @ param { string } highlightColor - The color of highlighted menu items as CSS value
* @ param { string } backgroundColor - The color of the background as CSS value
* @ param { string } normalColor - The color of normal menu items as CSS value
* @ param { DOM Element } keepWithin - The container to stay within
* @ param { boolean } autoClose - Autoclose the menu after selecting an item
* /
constructor ( { parent = null ,
commands = null ,
fontSize = '1em' ,
fontFamily = 'Arial' ,
padding = 16 ,
zIndex = 1 ,
spacing = '0px' ,
switchPos = false ,
notchSize = 10 ,
maxWidth = 800 ,
backgroundColor = '#EEE' ,
normalColor = '#444' ,
highlightColor = 'black' ,
notchPosition = 'bottomLeft' ,
keepWithin = null ,
autoClose = true } = { } ) {
super ( { parent , fontSize , fontFamily , padding , notchSize , notchPosition , backgroundColor , keepWithin , normalColor , autoClose } ) ;
this . commands = commands ;
this . zIndex = zIndex ;
this . switchPos = switchPos ;
this . spacing = spacing ;
this . highlightColor = highlightColor ;
}
/ * * S e t u p m e n u w i t h a d i c t i o n a r y o f c o m m a n d l a b e l s a n d c o m m a n d f u n c t i o n s .
* @ param { Object } commands - A dict object with command label strings as keys
* and command functions as values .
* @ return { PopupMenu } this
* /
setup ( commands ) {
this . commands = commands ;
this . items = { } ;
this . element = document . createElement ( 'div' ) ;
this . element . style . zIndex = this . zIndex ;
Elements$1 . addClass ( this . element , 'unselectable' ) ;
this . notch = document . createElement ( 'div' ) ;
Elements$1 . setStyle ( this . notch , this . notchStyle ( ) ) ;
for ( let key in commands ) {
let item = document . createElement ( 'div' ) ;
this . element . appendChild ( item ) ;
item . innerHTML = key ;
item . style . paddingBottom = item . style . paddingTop = this . spacing ;
Elements$1 . setStyle ( item , { color : this . normalColor , cursor : 'default' } ) ;
Elements$1 . addClass ( item , 'unselectable' ) ;
Elements$1 . addClass ( item , 'popupMenuItem' ) ;
this . items [ key ] = item ;
item . onclick = ( event ) => { this . perform ( key ) ; } ;
item . ontap = ( event ) => { this . perform ( key ) ; } ;
item . onmouseover = ( event ) => { this . over ( event , key ) ; } ;
item . onmouseout = ( event ) => { this . out ( event , key ) ; } ;
}
this . element . appendChild ( this . notch ) ;
this . parent . appendChild ( this . element ) ;
this . insertedNode = this . element ;
Elements$1 . setStyle ( this . element , this . defaultStyle ( ) ) ;
this . layout ( ) ;
return this
}
/ * * E x e c u t e a m e n u c o m m a n d .
* @ param { string } key - The selected key .
* /
perform ( key ) {
let func = this . commands [ key ] ;
if ( this . autoClose ) {
this . close ( ) ;
}
setTimeout ( ( ) => {
func . call ( ) ;
} , 20 ) ;
}
/ * * U p d a t e t h e m e n u i t e m d e n o t e d b y k e y .
* @ param { string } key - The selected key .
* @ param { boolean } highlight - Show the item highlighted .
* /
update ( key , highlight = false ) {
let text = this . items [ key ] ;
text . style . color = ( highlight ) ? this . highlightColor : this . normalColor ;
}
/ * * M o u s e o v e r h a n d ö e r .
* @ param { Event } event - The mouse event .
* @ param { boolean } key - The selected key .
* /
over ( event , key ) {
for ( let k in this . items ) {
this . update ( k , k == key ) ;
}
}
/ * * M o u s e o u t h a n d ö e r .
* @ param { Event } event - The mouse event .
* @ param { boolean } key - The selected key .
* /
out ( event , key ) {
this . update ( key ) ;
}
/ * * S h o w s t h e P o p u p M e n u w i t h t h e g i v e n c o m m a n d s a t t h e s p e c i f i e d p o i n t .
* @ param { Object } commands - A dict object with command label strings as keys
* and command functions as values .
* @ param { Point } point - The position as x , y coordinates { px } .
* @ return { PopupMenu } this
* /
showAt ( commands , point ) {
this . show ( commands ) ;
this . placeAt ( point ) ;
return this
}
/ * * C o n v e n i e n t s t a t i c m e t h o d s t o s h o w a n d r e u s e a P o p u p M e n u i m p l e m e n t e d
* as a class variable .
* @ param { Object } commands - A dict object with command label strings as keys
* and command functions as values .
* @ param { Point } point - The position as x , y coordinates { px } .
* @ param { string } fontSize - Describes the font size as CSS value
* @ param { number || string } padding - Describes the padding as CSS value
* @ param { number || string } notchSize - Describes the size of the notch ( callout ) as CSS value
* @ param { string } highlightColor - The color of highlighted menu items as CSS value
* @ param { string } backgroundColor - The color of the background as CSS value
* @ param { string } normalColor - The color of normal menu items as CSS value
* @ param { boolean } autoClose - Autoclose the menu after selecting an item
* /
static open ( commands , point , { parent = null ,
context = window ,
fontSize = '1em' ,
fontFamily = 'Arial' ,
padding = 16 ,
zIndex = 1 ,
spacing = '0px' ,
switchPos = false ,
notchSize = 10 ,
maxWidth = 800 ,
keepWithin = null ,
backgroundColor = '#EEE' ,
normalColor = '#444' ,
autoClose = true } = { } ) {
let registered = Poppable . get ( context ) ;
if ( registered ) {
this . closePopup ( ) ;
return
}
console . log ( "open" , point ) ;
let notchPosition = ( point . y < 50 && switchPos ) ? 'topCenter' : 'bottomCenter' ;
let popup = new PopupMenu$1 ( {
parent , fontSize , padding , zIndex , spacing , switchPos , notchSize ,
notchPosition ,
maxWidth , backgroundColor , normalColor ,
notchPosition , keepWithin , autoClose
} ) ;
popup . showAt ( commands , point ) ;
popup . register ( context ) ;
popup . closeEventListener = ( e ) => {
if ( this . eventOutside ( e ) )
this . closePopup ( context ) ;
} ;
if ( autoClose ) {
context . addEventListener ( 'mousedown' , popup . closeEventListener , true ) ;
context . addEventListener ( 'touchstart' , popup . closeEventListener , true ) ;
context . addEventListener ( 'pointerdown' , popup . closeEventListener , true ) ;
}
}
static eventOutside ( e ) {
return ! Elements$1 . hasClass ( e . target , 'popupMenuItem' )
}
/ * * C o n v e n i e n t s t a t i c m e t h o d s t o c l o s e t h e P o p u p M e n u i m p l e m e n t e d
* as a class variable .
* /
static closePopup ( context = window ) {
let registered = Poppable . get ( context ) ;
if ( registered ) {
registered . close ( ) ;
context . removeEventListener ( 'mousedown' , registered . closeEventListener ) ;
context . removeEventListener ( 'touchstart' , registered . closeEventListener ) ;
context . removeEventListener ( 'pointerdown' , registered . closeEventListener ) ;
}
}
}
class FrameContainer {
constructor ( element ) {
this . element = element ;
this . delegate = new InteractionMapper ( element , this ,
{ mouseWheelElement : window } ) ;
}
capture ( event ) {
return true
}
findTarget ( event , local , global ) {
let found = document . elementFromPoint ( global . x , global . y ) ;
let iframe = found . querySelector ( 'iframe' ) ;
if ( iframe ) {
let p = Points . fromPageToNode ( found , global ) ;
let doc = iframe . contentWindow . document ;
let target = doc . elementFromPoint ( p . x , p . y ) ;
if ( target != null ) {
console . log ( 'iframe element' , target ) ;
return new FrameTarget ( iframe , target )
}
}
return null
}
}
class FrameTarget {
constructor ( frame , target , debug = false ) {
this . frame = frame ;
this . target = target ;
this . debug = debug ;
}
capture ( event ) {
return true
}
simulateMouseEvent ( type , point ) {
let p = Points . fromPageToNode ( this . frame , point ) ;
let event = new MouseEvent ( type , {
view : this . frame . contentWindow ,
bubbles : true ,
cancelable : true ,
clientX : p . x ,
clientY : p . y } ) ;
this . target . dispatchEvent ( event ) ;
}
createTouchList ( pointMap ) {
let touches = [ ] ;
let doc = this . frame . contentWindow . document ;
for ( let key of pointMap . keys ( ) ) {
let point = pointMap . get ( key ) ;
let p = Points . fromPageToNode ( this . frame , point ) ;
let touchTarget = doc . elementFromPoint ( p . x , p . y ) ;
let touch = new Touch ( undefined , touchTarget , key ,
p . x , p . y , p . x , p . y ) ;
touches . push ( touch ) ;
}
return new TouchList ( ... touches )
}
simulateTouchEventChrome ( type , point , pointMap ) {
let doc = this . frame . contentWindow . document ;
let p = Points . fromPageToNode ( this . frame , point ) ;
let touchTarget = doc . elementFromPoint ( p . x , p . y ) ;
const touchObj = new Touch ( {
identifier : Date . now ( ) ,
target : touchTarget ,
clientX : p . x ,
clientY : p . y ,
pageX : p . x ,
pageY : p . y ,
radiusX : 2.5 ,
radiusY : 2.5 ,
rotationAngle : 10 ,
force : 0.5 ,
} ) ;
const touchEvent = new TouchEvent ( type , {
cancelable : true ,
bubbles : true ,
touches : [ touchObj ] ,
targetTouches : [ touchObj ] ,
changedTouches : [ touchObj ] ,
shiftKey : false ,
} ) ;
if ( this . debug ) console . log ( "simulateTouchEventChrome" , touchEvent ) ;
this . target . dispatchEvent ( touchEvent ) ;
}
simulateTouchEventSafari ( type , point , pointMap , touchEventKey = 'targetTouches' ) {
let p = Points . fromPageToNode ( this . frame , point ) ;
let data = { view : this . frame . contentWindow ,
bubbles : true ,
cancelable : true ,
clientX : p . x ,
clientY : p . y } ;
data [ touchEventKey ] = this . createTouchList ( pointMap ) ;
let event = new TouchEvent ( type , data ) ;
if ( this . debug ) console . log ( "simulateTouchEventChrome" , touchEvent ) ;
this . target . dispatchEvent ( event ) ;
}
simulateTouchEvent ( type , point , pointMap , touchEventKey = 'targetTouches' ) {
if ( Capabilities . isSafari ) {
this . simulateTouchEventSafari ( type , point , pointMap , touchEventKey ) ;
}
else {
this . simulateTouchEventChrome ( type , point , pointMap ) ;
}
}
isMouseLikeEvent ( event ) {
return event . type . startsWith ( 'mouse' ) || event . type . startsWith ( 'pointer' )
}
onStart ( event , interaction ) {
if ( this . debug ) console . log ( 'onStart' , this . frame . parentNode ) ;
for ( let [ key , point ] of interaction . current . entries ( ) ) {
if ( this . isMouseLikeEvent ( event ) ) {
this . simulateMouseEvent ( 'mousedown' , point ) ;
}
else {
this . simulateTouchEvent ( 'touchstart' , point ,
interaction . current ) ;
return
}
}
}
onMove ( event , interaction ) {
if ( this . debug ) console . log ( 'onMove' ) ;
for ( let [ key , point ] of interaction . current . entries ( ) ) {
if ( this . isMouseLikeEvent ( event ) ) {
this . simulateMouseEvent ( 'mousemove' , point ) ;
}
else {
this . simulateTouchEvent ( 'touchmove' , point ,
interaction . current ) ;
return
}
}
}
onEnd ( event , interaction ) {
if ( this . debug ) console . log ( 'onEnd' ) ;
for ( let [ key , point ] of interaction . current . entries ( ) ) {
if ( this . isMouseLikeEvent ( event ) ) {
this . simulateMouseEvent ( 'mouseend' , point ) ;
}
else {
this . simulateTouchEvent ( 'touchend' , point ,
interaction . ended , 'changedTouches' ) ;
return
}
}
}
}
class Inspect {
// Code inspection functions
static allScriptSources ( )
{
let sources = [ ] ;
let scripts = document . getElementsByTagName ( 'script' ) ;
for ( let i = 0 ; i < scripts . length ; i ++ ) {
console . dir ( scripts [ i ] ) ;
sources . push ( scripts [ i ] ) ;
}
return sources
}
}
/* globals Power0 */
/* eslint no-console: ["error", { allow: ["log", "info", "warn", "error"] }] */
/ * *
* Imports
* /
/ * *
* A class that can be used to perform automated user interface tests .
*
* @ example
* // Create the UITest object
* const test = new UITest ( {
* timeScale : 2
* } )
*
* // Add an action to the test case
* test . tap ( button , { eventType : 'click' } )
*
* // Start the test case
* test . start ( )
*
* @ class
* @ see { @ link https : //www.iwm-tuebingen.de/iwmbrowser/lib/pixi/uitest.html|DocTest}
* /
class UITest {
/ * *
* Creates an instance of an UITest .
*
* In the background , the class UITest uses the Greensock TimelineMax class . The opts object is passed directly to the TimelineMax class , so it can use any key that uses the TimelineMax class .
*
* @ constructor
* @ param { object } [ opts ] - An options object to specify the behaviour of the test case .
* @ param { number } [ opts . timeScale = 1 ] - The speed at which the test should run , see https : //greensock.com/docs/TimelineMax/timeScale().
* @ param { string } [ opts . eventType = auto ] - The type of events which should be used . Possible values : pointer , touch , mouse , auto . If set to auto , the eventType is set depending on the support of the browser used .
* @ param { boolean } [ opts . debug = false ] - If set to true , multiple informations will be print to the console .
* @ param { number } [ opts . defaultInterval ] - The interval used when no action is specified for an action .
* /
constructor ( opts = { } ) {
this . opts = Object . assign ( { } , {
timeScale : 1 ,
eventType : 'auto' ,
debug : false ,
defaultInterval : null
} , opts ) ;
// timeline
//--------------------
this . _timeline = new TimelineMax ( Object . assign ( { } , {
paused : true
} , this . opts ) ) ;
this . _timeline . timeScale ( this . opts . timeScale ) ;
// eventType
//--------------------
if ( this . opts . eventType === 'auto' ) {
if ( window . PointerEvent ) {
this . opts . eventType = 'pointer' ;
} else if ( 'ontouchstart' in window ) {
this . opts . eventType = 'touch' ;
} else {
this . opts . eventType = 'mouse' ;
}
}
if ( this . opts . debug ) {
console . log ( ` Event type: ${ this . opts . eventType } ` ) ;
}
this . _timelinePositions = [ 0 ] ;
this . _actions = 0 ;
// setup
//-----------------
this . setup ( ) ;
}
/ * *
* Generates the required structure .
*
* @ private
* @ return { UITest } A reference to the UITest for chaining .
* /
setup ( ) {
return this
}
/ * *
* Gets the Greensock TimelineMax object , used in the background of UITest .
*
* @ member { TimelineMax }
* /
get timeline ( ) {
return this . _timeline
}
/ * *
* Starts the test case and executes the corresponding statements in the specified order .
*
* @ return { UITest } A reference to the UITest for chaining .
* /
start ( ) {
this . _timeline . play ( ) ;
return this
}
/ * *
* Stops the test case and stops executing any further instructions .
*
* @ return { UITest } A reference to the UITest for chaining .
* /
stop ( ) {
this . _timeline . pause ( ) ;
return this
}
/ * *
* Clears all instructions of the test case .
*
* @ return { UITest } A reference to the UITest for chaining .
* /
clear ( ) {
this . _timeline . clear ( ) ;
return this
}
/ * *
* Restarts the test case .
*
* @ return { UITest } A reference to the UITest for chaining .
* /
restart ( ) {
this . _timeline . restart ( ) ;
return this
}
/ * *
* Executes a tap event ( pointerdown , pointerup ) on a specific element .
*
* @ param { HTMLElement | string } element - The HTML element on which the event is to be executed , e . g . button , document , h2 , canvas , etc . or an selector string . If a selector has been specified , it is evaluated immediately before the event is called using the querySelector method .
* @ param { number [ ] | object | PIXI . DisplayObject } [ position = The center of the element . ] - The local position of the event in the context of the specified HTML element . If no position is specified , the center of the HTML element is used . The position can be specified as an array of numbers , as an object with the two properties x and y , or as a PIXI . Display object .
* @ param { number } [ timelinePosition = One second after the last action . ] - The position in seconds when the event should be triggered , see shttps : //greensock.com/docs/TimelineMax/addCallback().
* @ param { object } [ opts ] - An options object to specify the behaviour of the action .
* @ param { function } [ opts . onStart ] - A function that runs after the first event is fired . Will not be fired if only one event is running ( for example , a click event ) . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { function } [ opts . onComplete ] - A function that runs after the second event is fired . Always fired , even if only one event is running ( for example , a click event ) . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { string [ ] } [ opts . eventTypes = [ 'pointerdown' , 'pointerup' ] ] - The event types to use . If no types are specified , the event types specified in the UITest constructor are used ( or auto if not specified ) .
* @ param { string } [ opts . eventType ] - If you want the tap method to fire only one event ( for example , a click event ) , you can specify the opts . eventType parameter . If eventType is not null , the parameter opts . eventTypes is ignored .
* @ param { Window | Frame } [ opts . context = window ] - The context within which the optionally specified element selector should be executed .
* @ param { boolean } [ opts . bubbles = true ] - The Event property bubbles indicates whether the event bubbles up through the DOM or not .
* @ param { boolean } [ opts . cancelable = true ] - Events ' cancelable property indicates if the event can be canceled , and therefore prevented as if the event never happened . If the event is not cancelable , then its cancelable property will be false and the event listener cannot stop the event from occurring .
* /
tap ( element , position , timelinePosition , opts = { } ) {
// arguments
//--------------------
[ position , timelinePosition , opts ] = this . reorderArguments ( arguments ) ;
this . _timelinePositions . push ( timelinePosition ) ;
// debug
//--------------------
if ( this . opts . debug ) console . log ( 'tap params' , { element , position , timelinePosition , opts } ) ;
// opts
//--------------------
opts = Object . assign ( { } , {
onStart : null ,
onComplete : null ,
eventTypes : this . resolveEvents ( [ 'down' , 'up' ] ) ,
eventType : null ,
context : window ,
bubbles : true ,
cancelable : true
} , opts ) ;
if ( opts . eventType ) {
opts . eventTypes = opts . eventType ;
}
opts . eventTypes = Array . isArray ( opts . eventTypes ) ? opts . eventTypes : [ opts . eventTypes ] ;
// timeline
//--------------------
this . _timeline . addCallback ( position => {
// element
//--------------------
const elem = Util . extractElement ( opts . context , element ) ;
// position
//--------------------
if ( position === null ) {
const rect = elem . getBoundingClientRect ( ) ;
position = [ rect . width / 2 , rect . height / 2 ] ;
}
// coords
//--------------------
const coords = Util . extractPosition ( position ) ;
if ( this . opts . debug ) console . log ( 'local coords' , coords ) ;
// eventTypes
//--------------------
if ( opts . eventTypes . length === 1 ) {
opts . eventTypes . unshift ( null ) ;
}
// event opts
//--------------------
const eventOpts = { bubbles : opts . bubbles , cancelable : opts . cancelable } ;
if ( opts . eventTypes [ 0 ] ) {
// create and dispatch event
//--------------------
const eventStart = Event . create ( elem , coords , opts . eventTypes [ 0 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , eventStart ) ;
elem . dispatchEvent ( eventStart ) ;
// onStart
//--------------------
if ( opts . onStart ) {
opts . onStart . call ( this , eventStart ) ;
}
}
// create and dispatch event
//--------------------
const eventComplete = Event . create ( elem , coords , opts . eventTypes [ 1 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , eventComplete ) ;
elem . dispatchEvent ( eventComplete ) ;
// onComplete
//--------------------
if ( opts . onComplete ) {
opts . onComplete . call ( this , eventComplete ) ;
}
} , timelinePosition , [ position ] ) ;
this . _actions ++ ;
return this
}
/ * *
* Executes a pan event ( pointerdown , pointermove , pointerup ) on a specific element .
*
* @ param { HTMLElement | string } element - The HTML element on which the event is to be executed , e . g . button , document , h2 , canvas , etc . or an selector string . If a selector has been specified , it is evaluated immediately before the event is called using the querySelector method .
* @ param { number [ ] | object | PIXI . DisplayObject } [ position = The center of the element . ] - The local position of the event in the context of the specified HTML element . If no position is specified , the center of the HTML element is used . The position can be specified as an array of numbers , as an object with the two properties x and y , or as a PIXI . Display object .
* @ param { number } [ timelinePosition = One second after the last action . ] - The position in seconds when the event should be triggered , see shttps : //greensock.com/docs/TimelineMax/addCallback().
* @ param { object } [ opts ] - An options object to specify the behaviour of the action .
* @ param { function } [ opts . onStart ] - A function that runs after the first event is fired . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { function } [ opts . onUpdate ] - A function that runs after each execution of the second event . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { function } [ opts . onComplete ] - A function that runs after the third event is fired . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { number [ ] | object | PIXI . DisplayObject } [ opts . to = { x : 0 , y : 0 } ] - The target of the pan process . The position can be specified as an array of numbers , as an object with the two properties x and y , or as a PIXI . Display object .
* @ param { number } [ opts . duration = 1 ] - The duration of the pan animation in seconds , see https : //greensock.com/docs/TweenLite/duration().
* @ param { Ease } [ opts . ease = Power0 . easeNone ] - The easing of the pan animation , see https : //greensock.com/docs/Easing.
* @ param { string [ ] } [ opts . eventTypes = [ 'pointerdown' , 'pointermove' , 'pointerup' ] ] - The event types to use . If no types are specified , the event types specified in the UITest constructor are used ( or auto if not specified ) .
* @ param { Window | Frame } [ opts . context = window ] - The context within which the optionally specified element selector should be executed .
* @ param { boolean } [ opts . bubbles = true ] - The Event property bubbles indicates whether the event bubbles up through the DOM or not .
* @ param { boolean } [ opts . cancelable = true ] - Events ' cancelable property indicates if the event can be canceled , and therefore prevented as if the event never happened . If the event is not cancelable , then its cancelable property will be false and the event listener cannot stop the event from occurring .
* /
pan ( element , position , timelinePosition , opts = { } ) {
// arguments
//--------------------
[ position , timelinePosition , opts ] = this . reorderArguments ( arguments ) ;
this . _timelinePositions . push ( timelinePosition ) ;
// debug
//--------------------
if ( this . opts . debug ) console . log ( 'tap params' , { element , position , timelinePosition , opts } ) ;
// opts
//--------------------
opts = Object . assign ( { } , {
onStart : null ,
onUpdate : null ,
onComplete : null ,
to : { x : 0 , y : 0 } ,
duration : 1 ,
ease : Power0 . easeNone ,
eventTypes : this . resolveEvents ( [ 'down' , 'move' , 'up' ] ) ,
context : window ,
bubbles : true ,
cancelable : true
} , opts ) ;
// timeline
//--------------------
this . _timeline . addCallback ( position => {
// element
//--------------------
const elem = Util . extractElement ( opts . context , element ) ;
// coords
//--------------------
const from = Util . extractPosition ( position ) ;
// event opts
//--------------------
const eventOpts = { bubbles : opts . bubbles , cancelable : opts . cancelable } ;
const gsOpts = {
ease : opts . ease ,
onStart : ( ) => {
// create and dispatch event
//--------------------
const event = Event . create ( elem , from , opts . eventTypes [ 0 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , event ) ;
elem . dispatchEvent ( event ) ;
// onStart
//--------------------
if ( opts . onStart ) {
opts . onStart . call ( this , event ) ;
}
} ,
onUpdate : ( ) => {
// create and dispatch event
//--------------------
const event = Event . create ( elem , from , opts . eventTypes [ 1 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , event ) ;
elem . dispatchEvent ( event ) ;
// onUpdate
//--------------------
if ( opts . onUpdate ) {
opts . onUpdate . call ( this , event ) ;
}
} ,
onComplete : ( ) => {
// create and dispatch event
//--------------------
const event = Event . create ( elem , from , opts . eventTypes [ 2 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , event ) ;
elem . dispatchEvent ( event ) ;
// onComplete
//--------------------
if ( opts . onComplete ) {
opts . onComplete . call ( this , event ) ;
}
}
} ;
// to
//--------------------
const object = Util . extractTo ( opts ) ;
Object . assign ( gsOpts , object ) ;
// drag animation
//--------------------
2019-03-28 09:37:30 +01:00
TweenLite . to ( from , opts . duration , gsOpts ) ;
2019-03-22 12:54:57 +01:00
} , timelinePosition , [ position ] ) ;
this . _actions ++ ;
return this
}
/ * *
* Executes a pinch event ( pointerdown , pointermove , pointerup ) on a specific element with two "fingers" simultaneously .
*
* @ param { HTMLElement | string } element - The HTML element on which the event is to be executed , e . g . button , document , h2 , canvas , etc . or an selector string . If a selector has been specified , it is evaluated immediately before the event is called using the querySelector method .
* @ param { number [ ] | object | PIXI . DisplayObject } [ position = The center of the element . ] - The local position of the event in the context of the specified HTML element . If no position is specified , the center of the HTML element is used . The position can be specified as an array of numbers , as an object with the two properties x and y , or as a PIXI . Display object .
* @ param { number } [ timelinePosition = One second after the last action . ] - The position in seconds when the event should be triggered , see shttps : //greensock.com/docs/TimelineMax/addCallback().
* @ param { object } [ opts ] - An options object to specify the behaviour of the action .
* @ param { function } [ opts . onStart ] - A function that runs after the first events are fired . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { function } [ opts . onUpdate ] - A function that runs after each execution of the second events . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { function } [ opts . onComplete ] - A function that runs after the third events are fired . Receives the fired event object as the first parameter . The test case ( UITest ) is bound to this .
* @ param { boolean } [ opts . doubleCallbacks = false ] - The callbacks onStart , onUpdate and onComplete will be fired only for one finger . If set to true , the events will be fired for both fingers .
* @ param { number } [ opts . distance = 100 ] - The distance in pixels , how far the two "fingers" should move apart . If to or bezier specified , distance is ignored .
* @ param { number [ ] [ ] | object [ ] | PIXI . DisplayObject [ ] } [ opts . to ] - The targets of the pinch process . The position must be an array with two entries . An entry can be specified as an array of numbers , as an object with the two properties x and y , or as a PIXI . Display object . If bezier is specified , to is ignored .
* @ param { number [ ] [ ] | object [ ] | PIXI . DisplayObject [ ] } [ opts . bezier ] - The targets of the pinch process . The position must be an array with two entries . An entry may be an array of positions or a bezier object ( https : //greensock.com/docs/Plugins/BezierPlugin). A position in the array or the values array of the bezier object can be specified as an array of numbers, as an object with the two properties x and y, or as a PIXI.Display object. If bezier is specified, to is ignored.
* @ param { number } [ opts . duration = 1 ] - The duration of the pan animation in seconds , see https : //greensock.com/docs/TweenLite/duration().
* @ param { Ease } [ opts . ease = Power0 . easeNone ] - The easing of the pan animation , see https : //greensock.com/docs/Easing.
* @ param { string [ ] } [ opts . eventTypes = [ 'pointerdown' , 'pointermove' , 'pointerup' ] ] - The event types to use . If no types are specified , the event types specified in the UITest constructor are used ( or auto if not specified ) .
* @ param { Window | Frame } [ opts . context = window ] - The context within which the optionally specified element selector should be executed .
* @ param { boolean } [ opts . bubbles = true ] - The Event property bubbles indicates whether the event bubbles up through the DOM or not .
* @ param { boolean } [ opts . cancelable = true ] - Events ' cancelable property indicates if the event can be canceled , and therefore prevented as if the event never happened . If the event is not cancelable , then its cancelable property will be false and the event listener cannot stop the event from occurring .
* /
pinch ( element , position , timelinePosition , opts = { } ) {
// arguments
//--------------------
[ position , timelinePosition , opts ] = this . reorderArguments ( arguments ) ;
this . _timelinePositions . push ( timelinePosition ) ;
// debug
//--------------------
if ( this . opts . debug ) console . log ( 'tap params' , { element , position , timelinePosition , opts } ) ;
// opts
//--------------------
opts = Object . assign ( { } , {
onStart : null ,
onUpdate : null ,
onComplete : null ,
doubleCallbacks : false ,
duration : 1 ,
distance : 100 ,
to : null ,
bezier : null ,
ease : Power0 . easeNone ,
eventTypes : this . resolveEvents ( [ 'down' , 'move' , 'up' ] ) ,
context : window ,
bubbles : true ,
cancelable : true
} , opts ) ;
// timeline
//--------------------
this . _timeline . addCallback ( position => {
// element
//--------------------
const elem = Util . extractElement ( opts . context , element ) ;
// from
//--------------------
let from1 = null ;
let from2 = null ;
if ( Array . isArray ( position ) && ! Util . isNumber ( position [ 0 ] ) ) {
from1 = Util . extractPosition ( position [ 0 ] ) ;
from2 = Util . extractPosition ( position [ 1 ] ) ;
} else {
from1 = Util . extractPosition ( position ) ;
from2 = { x : from1 . x , y : from1 . y } ;
}
// to
//--------------------
let gsOpts1 = { } ;
let gsOpts2 = { } ;
if ( opts . to || opts . bezier ) {
[ gsOpts1 , gsOpts2 ] = Util . extractMultiTo ( opts ) ;
} else {
const distance = opts . distance != null ? opts . distance : 100 ;
gsOpts1 . x = from1 . x - distance / 2 ;
gsOpts1 . y = from1 . y ;
gsOpts2 . x = from2 . x + distance / 2 ;
gsOpts2 . y = from2 . y ;
}
// pointers
//--------------------
const pointers = new Map ( ) ;
pointers . set ( 0 , { element : from1 , gsOpts : gsOpts1 } ) ;
pointers . set ( 1 , { element : from2 , gsOpts : gsOpts2 } ) ;
// loop
//--------------------
pointers . forEach ( ( value , key ) => {
// from
//--------------------
const from = value . element ;
// event opts
//--------------------
const eventOpts = { bubbles : opts . bubbles , cancelable : opts . cancelable , pointerId : key , isPrimary : key === 0 } ;
const gsOpts = {
ease : opts . ease ,
onStart : ( ) => {
// create and dispatch event
//--------------------
const event = Event . create ( elem , from , opts . eventTypes [ 0 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , event ) ;
elem . dispatchEvent ( event ) ;
// onStart
//--------------------
if ( opts . onStart && ( opts . doubleCallbacks || key === 0 ) ) {
opts . onStart . call ( this , event ) ;
}
} ,
onUpdate : ( ) => {
// create and dispatch event
//--------------------
const event = Event . create ( elem , from , opts . eventTypes [ 1 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , event ) ;
elem . dispatchEvent ( event ) ;
// onUpdate
//--------------------
if ( opts . onUpdate && ( opts . doubleCallbacks || key === 0 ) ) {
opts . onUpdate . call ( this , event ) ;
}
} ,
onComplete : ( ) => {
// create and dispatch event
//--------------------
const event = Event . create ( elem , from , opts . eventTypes [ 2 ] , eventOpts ) ;
if ( this . opts . debug ) console . log ( 'dispatch event' , event ) ;
elem . dispatchEvent ( event ) ;
// onComplete
//--------------------
if ( opts . onComplete && ( opts . doubleCallbacks || key === 0 ) ) {
opts . onComplete . call ( this , event ) ;
}
}
} ;
// to
//--------------------
Object . assign ( gsOpts , value . gsOpts ) ;
// drag animation
//--------------------
2019-03-28 09:37:30 +01:00
TweenLite . to ( from , opts . duration , gsOpts ) ;
2019-03-22 12:54:57 +01:00
} ) ;
} , timelinePosition , [ position ] ) ;
this . _actions ++ ;
return this
}
// /**
// * Adds a tap event to the timeline.
// *
// * @return {UITest} A reference to the uitest for chaining.
// */
// rotate() {
// return this
// }
// /**
// * Adds a tap event to the timeline.
// *
// * @return {UITest} A reference to the uitest for chaining.
// */
// swipe() {
// return this
// }
// /**
// * Adds a tap event to the timeline.
// *
// * @return {UITest} A reference to the uitest for chaining.
// */
// press() {
// return this
// }
// /**
// * Adds a tap event to the timeline.
// *
// * @return {UITest} A reference to the uitest for chaining.
// */
// event() {
// return this
// }
/ * *
* Sorts the parameters so that the second , third , and fourth parameters can be optional ( and possibly slip forward ) .
*
* @ private
* @ param { arguments } params - The arguments which were passed to the function .
* @ returns { array } - Returns an array of the position , the timelinePosition and the opts object .
* /
reorderArguments ( params ) {
// first parameter
//--------------------
const element = params [ 0 ] ;
// other parameter
//--------------------
let position = null ;
let timelinePosition = null ;
let opts = null ;
// second parameter
//--------------------
if ( Util . isNumber ( params [ 1 ] ) ) {
timelinePosition = params [ 1 ] ;
} else if ( Util . isObject ( params [ 1 ] ) && ! Util . isPixiDisplayObject ( params [ 1 ] ) && ( params [ 1 ] . x == null || params [ 1 ] . y == null ) ) {
opts = params [ 1 ] ;
} else if ( params [ 1 ] != null ) {
position = params [ 1 ] ;
}
// third parameter
//--------------------
if ( Util . isNumber ( params [ 2 ] ) ) {
timelinePosition = params [ 2 ] ;
} else if ( Util . isObject ( params [ 2 ] ) ) {
opts = params [ 2 ] ;
}
// fourth parameter
//--------------------
if ( Util . isObject ( params [ 3 ] ) ) {
opts = params [ 3 ] ;
}
if ( timelinePosition === null ) {
if ( this . opts . defaultInterval === null && this . _actions > 1 ) {
throw new Error ( 'No execution time was specified for this action, and a default interval was not set in the class constructor!' )
}
timelinePosition = Math . max ( ... this . _timelinePositions ) + ( this . opts . defaultInterval || 1 ) ;
}
if ( opts === null ) {
opts = { } ;
}
return [ position , timelinePosition , opts ]
}
/ * *
* Converts event type shortcuts to real event names .
*
* @ private
* @ param { string [ ] } events - An array of event types .
* /
resolveEvents ( events ) {
const data = [ ] ;
if ( this . opts . eventType === 'pointer' ) {
events . forEach ( it => {
if ( it === 'down' ) {
data . push ( 'pointerdown' ) ;
} else if ( it === 'move' ) {
data . push ( 'pointermove' ) ;
} else if ( it === 'up' ) {
data . push ( 'pointerup' ) ;
}
} ) ;
} else if ( this . opts . eventType === 'touch' ) {
events . forEach ( it => {
if ( it === 'down' ) {
data . push ( 'touchstart' ) ;
} else if ( it === 'move' ) {
data . push ( 'touchmove' ) ;
} else if ( it === 'up' ) {
data . push ( 'touchend' ) ;
}
} ) ;
} else {
events . forEach ( it => {
if ( it === 'down' ) {
data . push ( 'mousedown' ) ;
} else if ( it === 'move' ) {
data . push ( 'mousemove' ) ;
} else if ( it === 'up' ) {
data . push ( 'mouseup' ) ;
}
} ) ;
}
return data
}
}
/ * *
* Helper class .
*
* @ example
* // Checks if a thing is a number.
* const num = Util . isNumber ( 20 )
*
* @ private
* @ ignore
* @ class
* /
class Util {
/ * *
* Resolves the element from a specific context .
*
* @ static
* @ param { Window | Frame } context - The context within which the optionally specified element selector should be executed .
* @ return { HTMLElement | string } element - The HTML element on which the event is to be executed , e . g . button , document , h2 , canvas , etc . or an selector string . If a selector has been specified , it is evaluated immediately before the event is called using the querySelector method .
* /
static extractElement ( context , element ) {
const cont = Util . isFrame ( context ) ? context . contentDocument : context . document ;
const elem = Util . isString ( element ) ? cont . querySelector ( element ) : element ;
return elem
}
/ * *
* Extracts the position of the second parameter .
*
* @ static
* @ param { object } object - Something were the coords should be extracted .
* @ return { object } - Returns an object with the keys x and y .
* /
static extractPosition ( object ) {
// event coords
//--------------------
const position = { x : 0 , y : 0 } ;
// get the position
//--------------------
if ( ! object ) {
position . x = 0 ;
position . y = 0 ;
} else if ( typeof object . getBounds === 'function' ) {
const bounds = object . getBounds ( ) ;
position . x = bounds . x + bounds . width / 2 ;
position . y = bounds . y + bounds . height / 2 ;
} else if ( Array . isArray ( object ) ) {
position . x = object [ 0 ] ;
position . y = object [ 1 ] ;
} else if ( object . x != null && object . y != null ) {
position . x = object . x ;
position . y = object . y ;
}
return position
}
/ * *
* Extracts the to or bezier key .
*
* @ static
* @ param { object } opts - An options object where to or bezier should be extracted .
* @ return { object } - Returns an object with the to or bezier keys .
* /
static extractTo ( opts ) {
const object = { } ;
if ( opts . bezier ) {
let bezier = null ;
if ( Array . isArray ( opts . bezier ) ) {
bezier = {
values : opts . bezier . map ( it => Util . extractPosition ( it ) ) ,
type : 'thru'
} ;
} else {
opts . bezier . values = opts . bezier . values . map ( it => Util . extractPosition ( it ) ) ;
bezier = opts . bezier ;
}
object . bezier = bezier ;
} else {
const to = Util . extractPosition ( opts . to ) ;
object . x = to . x ;
object . y = to . y ;
}
return object
}
/ * *
* Extracts multiple to or bezier keys .
*
* @ static
* @ param { object } opts - An options object where to or bezier should be extracted .
* @ return { object [ ] } - Returns an array of objects with the keys x and y .
* /
static extractMultiTo ( opts ) {
const objects = [ ] ;
if ( opts . bezier ) {
opts . bezier . forEach ( it => {
let bezier = null ;
if ( Array . isArray ( it ) ) {
bezier = {
values : it . map ( it => Util . extractPosition ( it ) ) ,
type : 'thru'
} ;
} else {
it . values = it . values . map ( it => Util . extractPosition ( it ) ) ;
bezier = it ;
}
objects . push ( {
bezier
} ) ;
} ) ;
} else {
opts . to . forEach ( it => {
const to = Util . extractPosition ( it ) ;
objects . push ( {
x : to . x ,
y : to . y
} ) ;
} ) ;
}
return objects
}
/ * *
* Checks if a thing is a string .
*
* @ static
* @ param { object } object - The object to test for .
* @ return { boolean } - true if the thing is a string , otherwise false .
* /
static isString ( object ) {
return typeof object === 'string'
}
/ * *
* Checks if a thing is a number .
*
* @ static
* @ param { object } object - The object to test for .
* @ return { boolean } - true if the thing is a number , otherwise false .
* /
static isNumber ( object ) {
return typeof object === 'number'
}
/ * *
* Checks if a thing is an object .
*
* @ static
* @ param { object } object - The object to test for .
* @ return { boolean } - true if the thing is an object , otherwise false .
* /
static isObject ( object ) {
return typeof object === 'object' && ! Array . isArray ( object )
}
/ * *
* Checks if a thing is an PIXI . DisplayObject .
*
* @ static
* @ param { object } object - The object to test for .
* @ return { boolean } - true if the thing is a PIXI . DisplayObject , otherwise false .
* /
static isPixiDisplayObject ( object ) {
return typeof object . getBounds === 'function' && typeof object . renderWebGL === 'function' && typeof object . setTransform === 'function'
}
/ * *
* Checks if a thing is a frame .
*
* @ static
* @ param { object } object - The object to test for .
* @ return { boolean } - true if the thing is a frame , otherwise false .
* /
static isFrame ( object ) {
return object . contentDocument != null
}
}
/ * *
* Event helper class .
*
* @ example
* // Creates an event object.
* const event = Event . create ( h2 , { x : 5 , y : 10 } , 'touchstart' )
*
* @ private
* @ ignore
* @ class
* /
class Event {
/ * *
* Creates an event object .
*
* @ static
* @ param { HTMLElement } target - The element on which the event should be executed .
* @ param { object } position - The local position of the event in relation to the target . The object must have the keys x and y .
* @ param { string } type - The type of the event , see https : //developer.mozilla.org/de/docs/Web/Events
* @ param { object } opts - An options object . Every paramter of the event object can be overridden , see e . g . https : //developer.mozilla.org/de/docs/Web/API/MouseEvent for all the properties.
* /
static create ( target , position = { x : 0 , y : 0 } , type = 'pointerup' , opts = { } ) {
const rect = typeof target . getBoundingClientRect === 'function' ? target . getBoundingClientRect ( ) : { x : 0 , y : 0 } ;
// EventInit
const eventOpts = {
bubbles : true ,
cancelable : true ,
composed : false
} ;
// UIEventInit
const uiEventOpts = {
detail : 0 ,
view : window
} ;
// MouseEvent
const mouseEventOpts = {
screenX : window . screenX + target . offsetLeft + position . x ,
screenY : window . screenY + target . offsetTop + position . y ,
clientX : rect . x + position . x ,
clientY : rect . y + position . y ,
ctrlKey : false ,
shiftKey : false ,
altKey : false ,
metaKey : false ,
button : 0 ,
buttons : 1 ,
relatedTarget : null ,
region : null
} ;
// TouchEvent
const touchEventOpts = {
touches : [ ] ,
targetTouches : [ ] ,
changedTouches : [ ] ,
ctrlKey : false ,
shiftKey : false ,
altKey : false ,
metaKey : false
} ;
// PointerEvent
const pointerEventOpts = {
pointerId : 0 ,
width : 1 ,
height : 1 ,
pressure : 0 ,
tangentialPressure : 0 ,
tiltX : 0 ,
tiltY : 0 ,
twist : 0 ,
pointerType : 'touch' ,
isPrimary : true
} ;
if ( type . startsWith ( 'pointer' ) ) {
return new PointerEvent ( type , Object . assign ( { } , eventOpts , uiEventOpts , mouseEventOpts , pointerEventOpts , opts ) )
} else if ( type . startsWith ( 'touch' ) ) {
return new TouchEvent ( type , Object . assign ( { } , eventOpts , uiEventOpts , touchEventOpts , opts ) )
} else {
return new MouseEvent ( type , Object . assign ( { } , eventOpts , uiEventOpts , mouseEventOpts , opts ) )
}
}
}
/ * N e e d e d t o e n s u r e t h a t r o l l u p . j s i n c l u d e s c l a s s d e f i n i t i o n s a n d t h e c l a s s e s
are visible inside doctests .
* /
window . AbstractScatter = AbstractScatter ;
window . Angle = Angle ;
window . App = App ;
window . BaseEvent = BaseEvent ;
window . Capabilities = Capabilities ;
window . CapabilitiesTests = CapabilitiesTests ;
window . Colors = Colors ;
window . Cycle = Cycle ;
window . DOMFlip = DOMFlip ;
window . DOMFlippable = DOMFlippable ;
window . CardLoader = CardLoader ;
window . PDFLoader = PDFLoader ;
window . HTMLLoader = HTMLLoader ;
window . ImageLoader = ImageLoader ;
window . FrameLoader = FrameLoader ;
window . DOMScatter = DOMScatter ;
window . DOMScatterContainer = DOMScatterContainer ;
window . Dates = Dates ;
window . Doctest = Doctest ;
window . Elements = Elements$1 ;
window . Errors = Errors ;
window . EventRecorder = EventRecorder ;
window . Events = Events ;
window . FrameContainer = FrameContainer ;
window . FrameTarget = FrameTarget ;
window . IApp = IApp ;
window . IInteractionMapperTarget = IInteractionMapperTarget ;
window . IInteractionTarget = IInteractionTarget ;
window . Index = Index ;
window . Inspect = Inspect ;
window . Interaction = Interaction ;
window . InteractionDelegate = InteractionDelegate ;
window . InteractionDelta = InteractionDelta ;
window . InteractionMapper = InteractionMapper$1 ;
window . InteractionPoints = InteractionPoints ;
window . Interface = Interface ;
2019-05-24 09:53:27 +02:00
window . Logging = Logging ;
2019-03-22 12:54:57 +01:00
window . PointMap = PointMap ;
window . Rect = Rect ;
window . Points = Points ;
window . Polygon = Polygon ;
window . Poppable = Poppable ;
window . Popup = Popup ;
window . PopupMenu = PopupMenu$1 ;
window . ResizeEvent = ResizeEvent ;
window . ScatterEvent = ScatterEvent ;
window . Sets = Sets ;
window . Strings = Strings ;
window . UITest = UITest ;
window . getId = getId$1 ;
window . isEmpty = isEmpty ;
window . lerp = lerp ;
window . debounce = debounce ;
window . randomInt = randomInt ;
window . randomFloat = randomFloat ;
} ( ) ) ;