2024-01-03 14:23:34 +00:00
( function ( ) {
chatComponent = {
html : `
2024-01-29 20:19:04 +00:00
< div id = "chat" >
2024-01-03 14:23:34 +00:00
< div id = "videos" style = "pointer-events:none" > < / d i v >
< div id = "messages" aria - live = "assertive" aria - relevant > < / d i v >
< div id = "chatfooter" >
< div id = "chatbar" >
2024-01-29 20:19:04 +00:00
< input id = "chatline" type = "text" placeholder = "chat here" > < / i n p u t >
2024-01-03 14:23:34 +00:00
< / d i v >
< button id = "showchat" class = "btn" > show chat < / b u t t o n >
< / d i v >
< / d i v >
` ,
init : ( el ) => new Proxy ( {
2024-01-29 20:19:04 +00:00
scene : null ,
visible : true ,
messages : [ ] ,
oneMessagePerUser : false ,
username : '' , // configured by 'network.connected' event
2024-01-03 14:23:34 +00:00
$videos : el . querySelector ( "#videos" ) ,
$messages : el . querySelector ( "#messages" ) ,
$chatline : el . querySelector ( "#chatline" ) ,
$chatbar : el . querySelector ( "#chatbar" ) ,
install ( opts ) {
this . opts = opts
this . scene = opts . scene
2024-01-29 20:19:04 +00:00
this . $chatbar . style . display = 'none'
2024-01-03 14:23:34 +00:00
el . className = "xrf"
el . style . display = 'none' // start hidden
document . body . appendChild ( el )
document . dispatchEvent ( new CustomEvent ( "$chat:ready" , { detail : opts } ) )
this . send ( { message : ` Welcome to <b> ${ document . location . search . substr ( 1 ) } </b>, a 3D scene(file) which simply links to other ones.<br>You can start a solo offline exploration in XR right away.<br>Type /help below, or use the arrow- or WASD-keys on your keyboard, and mouse-drag to rotate.<br> ` , class : [ "info" , "guide" , "multiline" ] } )
} ,
initListeners ( ) {
let { $chatline } = this
2024-01-29 20:19:04 +00:00
$chatline . addEventListener ( 'click' , ( e ) => this . inform ( ) )
2024-01-03 14:23:34 +00:00
$chatline . addEventListener ( 'keydown' , ( e ) => {
if ( e . key == 'Enter' ) {
if ( $chatline . value [ 0 ] != '/' ) {
document . dispatchEvent ( new CustomEvent ( "network.send" , { detail : { message : $chatline . value } } ) )
}
this . send ( { message : $chatline . value } )
$chatline . value = ''
2024-01-29 20:19:04 +00:00
if ( window . innerHeight < 600 ) $chatline . blur ( )
2024-01-03 14:23:34 +00:00
}
} )
2024-01-29 20:19:04 +00:00
document . addEventListener ( 'network.connect' , ( e ) => {
this . visible = true
this . $chatbar . style . display = '' // show
} )
document . addEventListener ( 'network.connected' , ( e ) => {
if ( e . detail . username ) this . username = e . detail . username
} )
} ,
inform ( ) {
if ( ! this . inform . informed && ( this . inform . informed = true ) ) {
window . notify ( "Connected via P2P. You can now type message which will be visible to others." )
}
2024-01-03 14:23:34 +00:00
} ,
toggle ( ) {
this . visible = ! this . visible
if ( this . visible && window . meeting . status == 'offline' ) window . meeting . start ( this . opts )
} ,
2024-01-29 20:19:04 +00:00
hyphenate ( str ) {
return String ( str ) . replace ( /[^a-zA-Z0-9]/g , '-' )
} ,
// sending messages to the #messages div
// every user can post maximum one msg at a time
// it's more like a 'status' which is more friendly
// for accessibility reasons
// for a fullfledged chat/transcript see matrix clients
2024-01-03 14:23:34 +00:00
send ( opts ) {
let { $messages } = this
opts = { linebreak : true , message : "" , class : [ ] , ... opts }
if ( window . frontend && window . frontend . emit ) window . frontend . emit ( '$chat.send' , opts )
2024-01-29 20:19:04 +00:00
opts . pos = opts . pos || network . posName || network . pos
2024-01-03 14:23:34 +00:00
let div = document . createElement ( 'div' )
let msg = document . createElement ( 'div' )
let br = document . createElement ( 'br' )
let nick = document . createElement ( 'div' )
msg . className = "msg"
let html = ` ${ opts . message || '' } ${ opts . html ? opts . html ( opts ) : '' } `
if ( $messages . last == html ) return
msg . innerHTML = html
if ( opts . el ) msg . appendChild ( opts . el )
opts . id = Math . random ( )
if ( opts . class ) {
msg . classList . add . apply ( msg . classList , opts . class )
br . classList . add . apply ( br . classList , opts . class )
div . classList . add . apply ( div . classList , opts . class . concat ( [ "envelope" ] ) )
}
2024-01-29 20:19:04 +00:00
if ( ! msg . className . match ( /(info|guide|ui)/ ) ) {
let frag = xrf . URI . parse ( document . location . hash )
opts . from = 'you'
if ( frag . pos ) opts . pos = frag . pos . string
msg . classList . add ( 'self' )
}
2024-01-03 14:23:34 +00:00
if ( opts . from ) {
nick . className = "user"
nick . innerText = opts . from + ' '
div . appendChild ( nick )
if ( opts . pos ) {
let a = document . createElement ( "a" )
a . href = a . innerText = ` #pos= ${ opts . pos } `
nick . appendChild ( a )
}
}
div . appendChild ( msg )
2024-01-29 20:19:04 +00:00
// force one message per user
if ( this . oneMessagePerUser && opts . from ) {
div . id = this . hyphenate ( opts . from )
let oldMsg = $messages . querySelector ( ` # ${ div . id } ` )
if ( oldMsg ) oldMsg . remove ( )
}
// remove after timeout
if ( opts . timeout ) setTimeout ( ( div ) => div . remove ( ) , opts . timeout , div )
// finally add the message on top
2024-01-03 14:23:34 +00:00
$messages . appendChild ( div )
if ( opts . linebreak ) div . appendChild ( br )
$messages . scrollTop = $messages . scrollHeight // scroll down
$messages . last = msg . innerHTML
} ,
getChatLog ( ) {
return ( [ ... this . $messages . querySelectorAll ( '.envelope' ) ] )
. filter ( ( d ) => ! d . className . match ( /(info|ui)/ ) )
. map ( ( d ) => d . innerHTML )
. join ( '\n' )
}
} , {
get ( me , k , v ) { return me [ k ] } ,
set ( me , k , v ) {
me [ k ] = v
switch ( k ) {
case "visible" : {
el . style . display = me . visible ? 'block' : 'none'
if ( ! el . inited && ( el . inited = true ) ) me . initListeners ( )
break ;
}
}
}
} )
}
// reactify component!
document . addEventListener ( '$menu:ready' , ( opts ) => {
opts = opts . detail
document . head . innerHTML += chatComponent . css
window . $chat = document . createElement ( 'div' )
$chat . innerHTML = chatComponent . html
$chat = chatComponent . init ( $chat )
$chat . install ( opts )
//$menu.buttons = ([`<a class="btn" aria-label="button" aria-description="toggle text" id="meeting" onclick="$chat.toggle()">📜 toggle text</a><br>`])
// .concat($menu.buttons)
} )
// alpine component for displaying meetings
chatComponent . css = `
< style type = "text/css" >
# videos {
display : grid - auto - columns ;
grid - column - gap : 5 px ;
margin - bottom : 15 px ;
position : fixed ;
top : 0 ;
left : 0 ;
bottom : 0 ;
right : 0 ;
margin : 15 px ;
z - index : 1500 ;
}
# videos > video {
border - radius : 7 px ;
display : inline - block ;
background : black ;
width : 80 px ;
height : 60 px ;
margin - right : 5 px ;
margin - bottom : 5 px ;
vertical - align : top ;
pointer - events : all ;
}
# videos > video : hover {
filter : brightness ( 1.8 ) ;
cursor : pointer ;
}
# chatbar ,
button # showchat {
z - index : 1500 ;
position : fixed ;
bottom : 24 px ;
height : 34 px ;
left : 20 px ;
width : 48 % ;
background : white ;
padding : 0 px 0 px 0 px 15 px ;
border - radius : 30 px ;
max - width : 500 px ;
box - sizing : border - box ;
box - shadow : 0 px 0 px 5 px 5 px # 0002 ;
}
button # showchat {
z - index : 1550 ;
color : white ;
border : 0 ;
display : none ;
height : 44 px ;
background : # 07 F ;
font - weight : bold ;
}
# chatbar input {
border : none ;
width : 90 % ;
box - sizing : border - box ;
height : 24 px ;
font - size : var ( -- xrf - font - size - 2 ) ;
max - width : unset ;
}
# messages {
2024-01-29 20:19:04 +00:00
/ *
display : flex ;
flex - direction : column ;
width : 91 % ;
max - width : 500 px ;
* /
width : 100 % ;
align - items : flex - start ;
2024-01-03 14:23:34 +00:00
position : absolute ;
transition : 1 s ;
2024-01-29 20:19:04 +00:00
top : 77 px ;
2024-01-03 14:23:34 +00:00
left : 0 ;
2024-01-29 20:19:04 +00:00
bottom : 49 px ;
padding : 20 px ;
2024-01-03 14:23:34 +00:00
overflow : hidden ;
2024-01-29 20:19:04 +00:00
overflow - y : scroll ;
2024-01-03 14:23:34 +00:00
pointer - events : none ;
transition : 1 s ;
z - index : 100 ;
}
body . menu # messages {
top : 50 px ;
}
2024-01-29 20:19:04 +00:00
# messages : hover {
2024-01-03 14:23:34 +00:00
pointer - events : all ;
}
2024-01-29 20:19:04 +00:00
# messages * {
pointer - events : none ;
- webkit - user - select : none ;
- moz - user - select : - moz - none ;
- ms - user - select : none ;
user - select : none ;
}
2024-01-03 14:23:34 +00:00
# messages . msg {
transition : all 1 s ease ;
background : # fff ;
display : inline - block ;
padding : 1 px 17 px ;
2024-01-29 20:19:04 +00:00
border - radius : 20 px ;
2024-01-03 14:23:34 +00:00
color : # 000 c ;
margin - bottom : 10 px ;
line - height : 23 px ;
line - height : 33 px ;
cursor : grabbing ;
border : 1 px solid # 0002 ;
}
2024-01-29 20:19:04 +00:00
# messages . msg * {
pointer - events : all ;
- webkit - user - select : text ;
- moz - user - select : - moz - text ;
- ms - user - select : text ;
user - select : text ;
}
2024-01-03 14:23:34 +00:00
# messages . msg . self {
2024-01-29 20:19:04 +00:00
border - radius : 20 px ;
background : var ( -- xrf - box - shadow ) ;
2024-01-03 14:23:34 +00:00
}
# messages . msg . self ,
# messages . msg . self div {
color : # FFF ;
}
# messages . msg . info {
background : # 473 f7f ;
border - radius : 20 px ;
color : # FFF ;
text - align : right ;
line - height : 19 px ;
}
# messages . msg . info ,
# messages . msg . info * {
font - size : var ( -- xrf - font - size - 0 ) ;
}
# messages . msg a {
text - decoration : underline ;
2024-01-29 20:19:04 +00:00
color : var ( -- xrf - primary ) ;
2024-01-03 14:23:34 +00:00
font - weight : bold ;
2024-01-29 20:19:04 +00:00
transition : 0.3 s ;
}
# messages . msg . info a ,
# messages a . ruler {
color : # FFF ;
2024-01-03 14:23:34 +00:00
}
# messages . msg a : hover {
2024-01-29 20:19:04 +00:00
color : # 000 ;
2024-01-03 14:23:34 +00:00
}
# messages . msg . ui ,
# messages . msg . ui div {
background : white ;
border : none ;
color : # 333 ;
border - radius : 20 px ;
margin : 0 ;
padding : 0 px 5 px 5 px 5 px ;
}
# messages . guide , . guide {
display : unset ;
}
# messages . guide , . guide {
display : none ;
}
br . guide {
display : inline - block ;
}
# messages . msg . info a : hover ,
# messages button : hover {
filter : brightness ( 1.4 ) ;
}
# messages . msg . multiline {
padding : 2 px 14 px ;
}
# messages button {
text - decoration : none ;
margin : 0 px 15 px 10 px 0 px ;
background : var ( -- xrf - primary ) ;
font - family : var ( -- xrf - font - sans - serif ) ;
color : # FFF ;
border - radius : 7 px ;
padding : 11 px 15 px ;
border : 0 ;
font - weight : bold ;
box - shadow : 0 px 0 px 5 px 5 px # 0002 ;
pointer - events : all ;
}
# messages , # chatbar , # chatbar * , # messages * {
}
# messages button . emoticon ,
# messages . btn . emoticon {
line - height : 2 px ;
width : 20 px ;
display : inline - block ;
padding : 0 px 0 px ;
margin : 0 ;
vertical - align : middle ;
background : none ;
border : none ;
min - width : 31 px ;
box - shadow : none ;
}
# messages button . emoticon : hover ,
# messages . btn . emoticon : hover {
border : 1 px solid # ccc ! important ;
background : # EEE ;
}
. nomargin {
margin : 0 ;
}
2024-01-29 20:19:04 +00:00
. envelope ,
. envelope * {
2024-01-03 14:23:34 +00:00
overflow : hidden ;
transition : 1 s ;
2024-01-29 20:19:04 +00:00
pointer - events : none ;
}
. envelope a ,
. envelope button ,
. envelope input ,
. envelope textarea ,
. envelope msg ,
. envelope msg * {
pointer - events : all ;
2024-01-03 14:23:34 +00:00
}
. user {
margin - left : 13 px ;
font - weight : bold ;
color : var ( -- xrf - dark - gray ) ;
}
. user , . user * {
font - size : var ( -- xrf - font - size - 0 ) ;
}
< / s t y l e > `
connectionsComponent = {
html : `
< div id = "connections" >
2024-01-29 20:19:04 +00:00
< i class = "gg-close-o" id = "close" onclick = "$connections.visible = false" > < / i >
< br >
< div class = "tab-frame" >
< input type = "radio" name = "tab" id = "login" checked >
< label for = "login" > login < / l a b e l >
< input type = "radio" name = "tab" id = "io" >
< label for = "io" > devices < / l a b e l >
< input type = "radio" name = "tab" id = "networks" >
< label for = "networks" > advanced < / l a b e l >
< div class = "tab" >
< div id = "settings" > < / d i v >
< table >
< tr >
< td > < / t d >
< td >
< button id = "connect" onclick = "network.connect( $connections )" > 📡 Connect ! < / b u t t o n >
< / t d >
< / t r >
2024-01-03 14:23:34 +00:00
< / t a b l e >
2024-01-29 20:19:04 +00:00
< / d i v >
< div class = "tab" >
< div id = "devices" >
< a class = "badge ruler" > Webcam and / or Audio < / a >
< table >
< tr >
< td > Video < / t d >
< td >
< select id = "videoInput" > < / s e l e c t >
< / t d >
< / t r >
< tr >
< td > Mic < / t d >
< td >
< select id = "audioInput" > < / s e l e c t >
< / t d >
< / t r >
< tr style = "display:none" > <!-- not used ( for now ) -- >
< td > Audio < / t d >
< td >
< select id = "audioOutput" > < / s e l e c t >
< / t d >
< / t r >
< / t a b l e >
< / d i v >
< / d i v >
< div class = "tab" >
< div id = "networking" >
Networking a la carte : < br >
< table >
< tr >
< td > Webcam < / t d >
< td >
< select id = "webcam" > < / s e l e c t >
< / t d >
< / t r >
< tr >
< td > Chat < / t d >
< td >
< select id = "chatnetwork" > < / s e l e c t >
< / t d >
< / t r >
< tr >
< td > World sync < / t d >
< td >
< select id = "scene" > < / s e l e c t >
< / t d >
< / t r >
< / t a b l e >
< / d i v >
< / d i v >
2024-01-03 14:23:34 +00:00
< / d i v >
< / d i v >
` ,
init : ( el ) => new Proxy ( {
2024-01-29 20:19:04 +00:00
visible : true ,
webcam : [ { profile : { name : "No thanks" } , config : ( ) => document . createElement ( 'div' ) } ] ,
chatnetwork : [ { profile : { name : "No thanks" } , config : ( ) => document . createElement ( 'div' ) } ] ,
scene : [ { profile : { name : "No thanks" } , config : ( ) => document . createElement ( 'div' ) } ] ,
2024-01-03 14:23:34 +00:00
selectedWebcam : '' ,
selectedChatnetwork : '' ,
selectedScene : '' ,
$webcam : $webcam = el . querySelector ( "#webcam" ) ,
$chatnetwork : $chatnetwork = el . querySelector ( "#chatnetwork" ) ,
$scene : $scene = el . querySelector ( "#scene" ) ,
$settings : $settings = el . querySelector ( "#settings" ) ,
$devices : $devices = el . querySelector ( "#devices" ) ,
$connect : $connect = el . querySelector ( "#connect" ) ,
$networking : $networking = el . querySelector ( "#networking" ) ,
$audioInput : el . querySelector ( 'select#audioInput' ) ,
$audioOutput : el . querySelector ( 'select#audioOutput' ) ,
$videoInput : el . querySelector ( 'select#videoInput' ) ,
install ( opts ) {
this . opts = opts ;
( [ 'change' ] ) . map ( ( e ) => el . addEventListener ( e , ( ev ) => this [ e ] && this [ e ] ( ev . target . id , ev ) ) )
this . reactToNetwork ( )
$menu . buttons = ( [
` <a class="btn" aria-label="button" aria-title="connect button" aria-description="use this to talk or chat with other people" id="meeting" onclick=" $ connections.show()"><i class="gg-user-add"></i> connect</a><br> `
] ) . concat ( $menu . buttons )
if ( document . location . href . match ( /meet=/ ) ) this . show ( )
setTimeout ( ( ) => document . dispatchEvent ( new CustomEvent ( "$connections:ready" , { detail : opts } ) ) , 1 )
} ,
toggle ( ) {
2024-01-29 20:19:04 +00:00
$chat . visible = ! $chat . visible
2024-01-03 14:23:34 +00:00
} ,
change ( id , e ) {
if ( id . match ( /^(webcam|chatnetwork|scene)$/ ) ) {
this . renderSettings ( ) // trigger this when 'change' event fires on children dom elements
}
} ,
2024-01-29 20:19:04 +00:00
show ( opts ) {
opts = opts || { }
if ( opts . hide ) {
if ( el . parentElement ) el . parentElement . parentElement . style . display = 'none' // hide along with wrapper elements
if ( ! opts . showChat ) $chat . visible = false
2024-01-03 14:23:34 +00:00
} else {
2024-01-29 20:19:04 +00:00
$chat . visible = true
this . visible = true
// hide networking settings if entering thru meetinglink
$networking . style . display = document . location . href . match ( /meet=/ ) ? 'none' : 'block'
if ( ! network . connected ) {
document . querySelector ( 'body > .xrf' ) . appendChild ( el )
$chat . send ( { message : "" , el , class : [ 'ui' ] } )
if ( ! network . meetinglink ) { // set default
$webcam . value = opts . webcam || 'Peer2Peer'
$chatnetwork . value = opts . chatnetwork || 'Peer2Peer'
$scene . value = opts . scene || 'Peer2Peer'
}
this . renderSettings ( )
} else {
$chat . send ( { message : "you are already connected, refresh page to create new connection" , class : [ 'info' ] } )
}
2024-01-03 14:23:34 +00:00
}
} ,
update ( ) {
this . selectedWebcam = $webcam . value
this . selectedChatnetwork = $chatnetwork . value
this . selectedScene = $scene . value
} ,
forSelectedPluginsDo ( cb ) {
// this function looks weird but it's handy to prevent the same plugins rendering duplicate configurations
let plugins = { }
2024-01-29 20:19:04 +00:00
let select = ( name ) => ( o ) => o . profile . name == name ? plugins [ o . profile . name ] = o : ''
2024-01-03 14:23:34 +00:00
this . webcam . find ( select ( this . selectedWebcam ) )
this . chatnetwork . find ( select ( this . selectedChatnetwork ) )
this . scene . find ( select ( this . selectedScene ) )
for ( let i in plugins ) {
try { cb ( plugins [ i ] ) } catch ( e ) { console . error ( e ) }
}
} ,
renderSettings ( ) {
let opts = { webcam : $webcam . value , chatnetwork : $chatnetwork . value , scene : $scene . value }
this . update ( )
$settings . innerHTML = ''
2024-01-29 20:19:04 +00:00
this . forSelectedPluginsDo ( ( plugin ) => $settings . appendChild ( plugin . config ( { ... opts , plugin } ) ) )
2024-01-03 14:23:34 +00:00
this . renderInputs ( )
} ,
renderInputs ( ) {
2024-01-29 20:19:04 +00:00
if ( ! this . selectedWebcam || this . selectedWebcam == 'No thanks' ) {
2024-01-03 14:23:34 +00:00
return this . $devices . style . display = 'none'
} else this . $devices . style . display = ''
navigator . mediaDevices . getUserMedia ( {
audio : true ,
video : true
} )
. then ( ( ) => {
const selectors = [ this . $audioInput , this . $audioOutput , this . $videoInput ] ;
const gotDevices = ( deviceInfos ) => {
// Handles being called several times to update labels. Preserve values.
const values = selectors . map ( select => select . value ) ;
selectors . forEach ( select => {
while ( select . firstChild ) {
select . removeChild ( select . firstChild ) ;
}
} ) ;
for ( let i = 0 ; i !== deviceInfos . length ; ++ i ) {
const deviceInfo = deviceInfos [ i ] ;
const option = document . createElement ( 'option' ) ;
option . value = deviceInfo . deviceId ;
if ( deviceInfo . kind === 'audioinput' ) {
option . text = deviceInfo . label || ` microphone ${ this . $audioInput . length + 1 } ` ;
this . $audioInput . appendChild ( option ) ;
} else if ( deviceInfo . kind === 'audiooutput' ) {
option . text = deviceInfo . label || ` speaker ${ this . $audioOutput . length + 1 } ` ;
this . $audioOutput . appendChild ( option ) ;
} else if ( deviceInfo . kind === 'videoinput' ) {
option . text = deviceInfo . label || ` camera this. ${ this . $videoInput . length + 1 } ` ;
this . $videoInput . appendChild ( option ) ;
} else {
console . log ( 'Some other kind of source/device: ' , deviceInfo ) ;
}
}
// hide if there's nothing to choose
let totalDevices = this . $audioInput . options . length + this . $audioOutput . options . length + this . $videoInput . options . length
this . $devices . style . display = totalDevices > 3 ? 'block' : 'none'
selectors . forEach ( ( select , selectorIndex ) => {
if ( Array . prototype . slice . call ( select . childNodes ) . some ( n => n . value === values [ selectorIndex ] ) ) {
select . value = values [ selectorIndex ] ;
}
} ) ;
}
// after getUserMedia we can enumerate
navigator . mediaDevices . enumerateDevices ( ) . then ( gotDevices ) . catch ( console . warn ) ;
} )
} ,
2024-01-29 20:19:04 +00:00
reactToNetwork ( ) { // *TODO* move to network?
2024-01-03 14:23:34 +00:00
document . addEventListener ( 'network.connect' , ( ) => {
2024-01-29 20:19:04 +00:00
this . show ( { hide : true , showChat : true } )
2024-01-03 14:23:34 +00:00
} )
2024-01-29 20:19:04 +00:00
document . addEventListener ( 'network.disconnect' , ( ) => {
this . connected = false
2024-01-03 14:23:34 +00:00
} )
2024-01-29 20:19:04 +00:00
2024-01-03 14:23:34 +00:00
}
} , {
get ( data , k , v ) { return data [ k ] } ,
set ( data , k , v ) {
data [ k ] = v
switch ( k ) {
2024-01-29 20:19:04 +00:00
case "visible" : el . style . display = v ? '' : 'none' ; break ;
case "webcam" : $webcam . innerHTML = ` <option> ${ data [ k ] . map ( ( p ) => p . profile . name ) . join ( '</option><option>' ) } </option> ` ; break ;
case "chatnetwork" : $chatnetwork . innerHTML = ` <option> ${ data [ k ] . map ( ( p ) => p . profile . name ) . join ( '</option><option>' ) } </option> ` ; break ;
case "scene" : $scene . innerHTML = ` <option> ${ data [ k ] . map ( ( p ) => p . profile . name ) . join ( '</option><option>' ) } </option> ` ; break ;
2024-01-03 14:23:34 +00:00
case "selectedScene" : $scene . value = v ; data . renderSettings ( ) ; break ;
case "selectedChatnetwork" : $chatnetwork . value = v ; data . renderSettings ( ) ; break ;
case "selectedWebcam" : {
$webcam . value = v ;
data . renderSettings ( ) ;
$devices . style . display = v ? 'block' : 'none'
break ;
}
}
}
} )
}
// reactify component!
document . addEventListener ( '$menu:ready' , ( opts ) => {
opts = opts . detail
document . head . innerHTML += connectionsComponent . css
window . $connections = document . createElement ( 'div' )
$connections . innerHTML = connectionsComponent . html
$connections = connectionsComponent . init ( $connections )
$connections . install ( opts )
} )
// alpine component for displaying meetings
connectionsComponent . css = `
< style type = "text/css" >
button # connect {
height : 43 px ;
width : 100 % ;
margin : 0 px ;
}
# messages . msg # connections {
position : relative ;
}
. connecthide {
transform : translateY ( - 1000 px ) ;
}
# close {
display : block ;
position : relative ;
float : right ;
2024-01-29 20:19:04 +00:00
top : 16 px ;
2024-01-03 14:23:34 +00:00
}
2024-01-29 20:19:04 +00:00
# messages . msg . ui div . tab - frame > div . tab { padding : 25 px 10 px 5 px 10 px ; }
2024-01-03 14:23:34 +00:00
< / s t y l e > `
// reactive component for displaying the menu
menuComponent = ( el ) => new Proxy ( {
html : `
< div class = "xrf footer" >
< div class = "menu" >
< div id = "buttons" > < / d i v >
< a class = "btn" id = "more" aria - title = "menu button" > < i id = "icon" class = "gg-menu" > < / i > < / a > < b r >
< / d i v >
< / d i v >
` ,
collapsed : false ,
logo : './../../assets/logo.png' ,
buttons : [ ` <a class="btn" aria-label="button" aria-title="share button" aria-description="this allows embedding and sharing of this URL or make a screenshot of it" id="share" onclick="frontend.share()"><i class="gg-link"></i> share</a><br> ` ] ,
$buttons : $buttons = el . querySelector ( '#buttons' ) ,
$btnMore : $btnMore = el . querySelector ( '#more' ) ,
2024-01-29 20:19:04 +00:00
toggle ( state ) {
this . collapsed = state !== undefined ? state : ! this . collapsed
2024-01-03 14:23:34 +00:00
el . querySelector ( "i#icon" ) . className = this . collapsed ? 'gg-close' : 'gg-menu'
document . body . classList [ this . collapsed ? 'add' : 'remove' ] ( [ 'menu' ] )
} ,
init ( opts ) {
el . innerHTML = this . html
document . body . appendChild ( el ) ;
( [ 'click' ] ) . map ( ( e ) => el . addEventListener ( e , ( ev ) => this [ e ] && this [ e ] ( ev . target . id , ev ) ) )
setTimeout ( ( ) => {
document . dispatchEvent ( new CustomEvent ( "$menu:ready" , { detail : { $menu : this , xrf } } ) )
} , 100 )
return this
} ,
click ( id , e ) {
switch ( id ) {
case "icon" :
case "more" : this . toggle ( ) ; break ;
}
}
} ,
{
get ( me , k , v ) { return me [ k ] } ,
set ( me , k , v ) {
me [ k ] = v
switch ( k ) {
case "buttons" : el . querySelector ( "#buttons" ) . innerHTML = this . renderButtons ( me ) ;
document . dispatchEvent ( new CustomEvent ( "$menu:buttons:render" , { detail : el . querySelector ( '.menu' ) } ) )
break ;
case "collapsed" :
el . querySelector ( "#buttons" ) . style . display = me . collapsed ? 'block' : 'none'
frontend . emit ( '$menu:collapse' , v )
break ;
}
} ,
renderButtons : ( data ) => ` ${ data . buttons . join ( '' ) } `
} )
// reactify component!
document . addEventListener ( 'frontend:ready' , ( e ) => {
window . $menu = menuComponent ( document . createElement ( 'div' ) ) . init ( e . detail )
} )
window . accessibility = ( opts ) => new Proxy ( {
opts ,
enabled : false ,
// features
speak _movements : true ,
speak _keyboard : true ,
// audio settings
speak _rate : 1 ,
speak _pitch : 1 ,
speak _volume : 1 ,
speak _voice : - 1 ,
toggle ( ) { this . enabled = ! this . enabled } ,
settings ( ) {
this . toggle ( ) // *TODO* should show settings screen
} ,
speak ( str , opts ) {
opts = opts || { speaksigns : true }
if ( ! this . enabled || ! str ) return
if ( opts . speaksigns ) {
str = str . replace ( /\/\// , ' ' )
. replace ( /:/ , '' )
. replace ( /\// , ' slash ' )
. replace ( /\./ , ' dot ' )
. replace ( /#/ , ' hash ' )
. replace ( /&/ , ' and ' )
. replace ( /=/ , ' is ' )
}
let speech = window . speechSynthesis
let utterance = new SpeechSynthesisUtterance ( str )
if ( this . speak _voice != - 1 ) utterance . voice = speech . getVoices ( ) [ this . speak _voice ] ;
else {
let voices = speech . getVoices ( )
for ( let i = 0 ; i < voices . length ; i ++ ) {
if ( voices [ i ] . lang == navigator . lang ) this . speak _voice = i ;
}
}
utterance . rate = this . speak _rate
utterance . pitch = this . speak _pitch
utterance . volume = this . speak _volume
if ( opts . override ) speech . cancel ( )
speech . speak ( utterance )
} ,
init ( ) {
window . addEventListener ( 'keydown' , ( e ) => {
if ( ! this . speak _keyboard ) return
let k = e . key
switch ( k ) {
case "ArrowUp" : k = "forward" ; break ;
case "ArrowLeft" : k = "left" ; break ;
case "ArrowRight" : k = "right" ; break ;
case "ArrowDown" : k = "backward" ; break ;
}
this . speak ( k , { override : true } )
} )
document . addEventListener ( '$menu:buttons:render' , ( e ) => {
let $ = e . detail
let a = [ ... $ . querySelectorAll ( 'a' ) ]
// make sure anchor buttons are accessible by tabbing to them
a . map ( ( btn ) => {
if ( ! btn . href ) btn . setAttribute ( "href" , "javascript:void(0)" ) // important!
btn . setAttribute ( "aria-label" , "button" )
} )
document . addEventListener ( 'mouseover' , ( e ) => {
if ( e . target . getAttribute ( "aria-title" ) ) {
let lines = [ ]
lines . push ( e . target . getAttribute ( "aria-title" ) )
lines . push ( e . target . getAttribute ( "aria-description" ) )
lines = lines . filter ( ( l ) => l )
this . speak ( lines . join ( "." ) , { override : true , speaksigns : false } )
}
} )
} )
document . addEventListener ( 'network.send' , ( e ) => {
let opts = e . detail
opts . message = opts . message || ''
this . speak ( opts . message )
} )
opts . xrf . addEventListener ( 'pos' , ( opts ) => {
if ( this . enabled ) {
$chat . send ( { message : this . posToMessage ( opts ) } )
2024-01-29 20:19:04 +00:00
network . send ( { message : this . posToMessage ( opts ) , class : [ "info" , "guide" ] } )
}
if ( opts . frag . pos . string . match ( /,/ ) ) {
network . pos = opts . frag . pos . string
} else {
network . posName = opts . frag . pos . string
2024-01-03 14:23:34 +00:00
}
} )
} ,
posToMessage ( opts ) {
let obj
let description
let msg = "teleported to "
let pos = opts . frag . pos
if ( pos . string . match ( ',' ) ) msg += ` coordinates <a href="#pos= ${ pos . string } "> ${ pos . string } </a> `
else {
msg += ` location <a href="#pos= ${ pos . string } "> ${ pos . string } </a> `
obj = opts . scene . getObjectByName ( pos . string )
if ( obj ) {
description = obj . userData [ 'aria-label' ] || ''
} else msg += ", but the teleportation was refused because it cannot be found within this world"
}
return msg
} ,
sanitizeTranscript ( ) {
return $chat . $messages . innerText
. replaceAll ( "<[^>]*>" , "" ) // strip html
. split ( '\n' )
. map ( ( l ) => String ( l + '.' ) . replace ( /(^|:|;|!|\?|\.)\.$/g , '\$1' ) ) // add dot if needed
. join ( '\n' )
}
} ,
{
// auto-trigger events on changes
get ( data , k , receiver ) { return data [ k ] } ,
set ( data , k , v ) {
data [ k ] = v
switch ( k ) {
case "enabled" : {
let message = "accessibility has been" + ( v ? "boosted" : "lowered" )
$ ( '#accessibility.btn' ) . style . filter = v ? 'brightness(1.0)' : 'brightness(0.5)'
if ( v ) $chat . visible = true
$chat . send ( { message , class : [ 'info' , 'guide' ] } )
data . enabled = true
data . speak ( message )
data . enabled = v
$chat . $messages . classList [ v ? 'add' : 'remove' ] ( 'guide' )
if ( ! data . readTranscript && ( data . readTranscript = true ) ) {
data . speak ( data . sanitizeTranscript ( ) )
}
}
}
}
} )
document . addEventListener ( '$menu:ready' , ( e ) => {
try {
accessibility = accessibility ( e . detail )
accessibility . init ( )
document . dispatchEvent ( new CustomEvent ( "accessibility:ready" , e ) )
$menu . buttons = $menu . buttons . concat ( [ ` <a class="btn" style="background:var(--xrf-dark-gray);filter: brightness(0.5);" aria-label="button" aria-description="enable all accessibility features" id="accessibility" onclick="accessibility.settings()"><i class="gg-yinyang"></i>accessibility</a><br> ` ] )
} catch ( e ) { console . error ( e ) }
} )
document . head . innerHTML += `
< style type = "text/css" >
: root {
-- xrf - primary : # 6839 dc ;
-- xrf - primary - fg : # FFF ;
-- xrf - light - primary : # ea23cf ;
-- xrf - secondary : # 872 eff ;
-- xrf - light - xrf - secondary : # ce7df2 ;
-- xrf - topbar - bg : # fffb ;
-- xrf - box - shadow : # 0005 ;
-- xrf - red : red ;
-- xrf - dark - gray : # 343334 ;
-- xrf - gray : # 424280 ;
-- xrf - white : # fdfdfd ;
-- xrf - light - gray : # efefef ;
-- xrf - lighter - gray : # e4e2fb96 ;
-- xrf - font - sans - serif : system - ui , - apple - system , segoe ui , roboto , ubuntu , helvetica , cantarell , noto sans , sans - serif ;
-- xrf - font - monospace : menlo , monaco , lucida console , liberation mono , dejavu sans mono , bitstream vera sans mono , courier new , monospace , serif ;
-- xrf - font - size - 0 : 12 px ;
-- xrf - font - size - 1 : 14 px ;
-- xrf - font - size - 2 : 17 px ;
-- xrf - font - size - 3 : 21 px ;
}
/* CSS reset */
html { line - height : 1.15 ; - webkit - text - size - adjust : 100 % } body { margin : 0 } main { display : block } h1 { font - size : 2 em ; margin : 0.67 em 0 } hr { box - sizing : content - box ; height : 0 ; overflow : visible } pre { font - family : monospace , monospace ; font - size : 1 em } a { background - color : transparent } abbr [ title ] { border - bottom : none ; text - decoration : underline ; text - decoration : underline dotted } b , strong { font - weight : bolder } code , kbd , samp { font - family : monospace , monospace ; font - size : 1 em } small { font - size : 80 % } sub , sup { font - size : 75 % ; line - height : 0 ; position : relative ; vertical - align : baseline } sub { bottom : - 0.25 em } sup { top : - 0.5 em } img { border - style : none } button , input , optgroup , select , textarea { font - family : inherit ; font - size : 100 % ; line - height : 1.15 ; margin : 0 } button , input { overflow : visible } button , select { text - transform : none } [ type = "button" ] , [ type = "reset" ] , [ type = "submit" ] , button { - webkit - appearance : button } [ type = "button" ] : : - moz - focus - inner , [ type = "reset" ] : : - moz - focus - inner , [ type = "submit" ] : : - moz - focus - inner , button : : - moz - focus - inner { border - style : none ; padding : 0 } [ type = "button" ] : - moz - focusring , [ type = "reset" ] : - moz - focusring , [ type = "submit" ] : - moz - focusring , button : - moz - focusring { outline : 1 px dotted ButtonText } fieldset { padding : 0.35 em 0.75 em 0.625 em } legend { box - sizing : border - box ; color : inherit ; display : table ; max - width : 100 % ; padding : 0 ; white - space : normal } progress { vertical - align : baseline } textarea { overflow : auto } [ type = "checkbox" ] , [ type = "radio" ] { box - sizing : border - box ; padding : 0 } [ type = "number" ] : : - webkit - inner - spin - button , [ type = "number" ] : : - webkit - outer - spin - button { height : auto } [ type = "search" ] { - webkit - appearance : textfield ; outline - offset : - 2 px } [ type = "search" ] : : - webkit - search - decoration { - webkit - appearance : none } : : - webkit - file - upload - button { - webkit - appearance : button ; font : inherit } details { display : block } summary { display : list - item } template { display : none } [ hidden ] { display : none }
. xrf table tr td {
vertical - align : top ;
}
. xrf button ,
. xrf input [ type = "submit" ] ,
. xrf . btn {
text - decoration : none ;
background : var ( -- xrf - primary ) ;
border : 0 ;
border - radius : 25 px ;
padding : 11 px 15 px ;
font - weight : bold ;
transition : 0.3 s ;
height : 40 px ;
font - size : var ( -- xrf - font - size - 1 ) ;
color : var ( -- xrf - primary - fg ) ;
line - height : var ( -- xrf - font - size - 1 ) ;
cursor : pointer ;
white - space : pre ;
min - width : 45 px ;
box - shadow : 0 px 0 px 10 px var ( -- xrf - box - shadow ) ;
2024-01-29 20:19:04 +00:00
display : inline - block ;
2024-01-03 14:23:34 +00:00
}
. xrf button : hover ,
. xrf input [ type = "submit" ] : hover ,
. xrf . btn : hover {
background : var ( -- xrf - secondary ) ;
text - decoration : none ;
}
. xrf , . xrf * {
font - family : var ( -- xrf - font - sans - serif ) ;
font - size : var ( -- xrf - font - size - 1 ) ;
line - height : 27 px ;
}
textarea , select , input [ type = "text" ] {
background : transparent ; /* linear-gradient( var(--xrf-lighter-gray), var(--xrf-gray) ) !important; */
}
input [ type = "submit" ] {
color : var ( -- xrf - light - gray ) ;
}
input [ type = text ] {
padding : 7 px 15 px ;
}
input {
border - radius : 7 px ;
margin : 5 px 0 px ;
}
. title {
border - bottom : 2 px solid var ( -- xrf - secondary ) ;
padding - bottom : 20 px ;
}
# topbar {
background : var ( -- xrf - topbar - bg ) ;
position : fixed ;
top : 0 ;
left : 0 ;
width : 100 % ;
height : 48 px ;
box - shadow : 0 px 0 px 10 px var ( -- xrf - box - shadow ) ;
opacity : 0.9 ;
z - index : 2000 ;
display : none ;
}
# topbar . logo {
width : 92 px ;
position : absolute ;
top : 9 px ;
left : 93 px ;
height : 30 px ;
background - size : contain ;
background - repeat : no - repeat ;
}
# topbar > input [ type = "submit" ] {
height : 32 px ;
position : absolute ;
right : 20 px ;
top : 2 px ;
min - width : 135 px ;
}
# topbar > button # navback ,
# topbar > button # navforward {
height : 32 px ;
font - size : 24 px ;
position : absolute ;
left : 9 px ;
padding : 2 px 13 px ;
border - radius : 6 px ;
top : 8 px ;
color : var ( -- xrf - light - gray ) ;
width : 36 px ;
min - width : unset ;
}
# topbar > button # navforward {
left : 49 px ;
}
# topbar > # uri {
height : 18 px ;
font - size : var ( -- xrf - font - size - 3 ) ;
position : absolute ;
left : 200 px ;
top : 9 px ;
max - width : 550 px ;
padding : 5 px 0 px 5 px 5 px ;
width : calc ( 63 % - 200 px ) ;
background : # f0f0f0 ;
border - color : # Ccc ;
border : 2 px solid # CCC ;
border - radius : 7 px ;
color : # 555 ;
}
2024-01-29 20:19:04 +00:00
. footer > . menu . btn {
2024-01-03 14:23:34 +00:00
display : inline - block ;
background : var ( -- xrf - primary ) ;
border - radius : 25 px ;
border : 0 ;
padding : 5 px 19 px ;
font - weight : 1000 ;
font - family : sans - serif ;
font - size : var ( -- xrf - font - size - 2 ) ;
color : var ( -- xrf - primary - fg ) ;
height : 33 px ;
z - index : 2000 ;
cursor : pointer ;
min - width : 145 px ;
text - decoration : none ;
margin - top : 15 px ;
line - height : 36 px ;
margin - right : 10 px ;
text - align : left ;
}
. xrf a . btn # more {
z - index : 3000 ;
width : 19 px ;
min - width : 19 px ;
font - size : 16 px ;
text - align : center ;
background : white ;
color : var ( -- xrf - primary ) ;
}
. xrf a . btn # more i . gg - menu {
margin - top : 15 px ;
}
. xrf a . btn # more i . gg - close ,
. xrf a . btn # more i . gg - menu {
color : # 888 ;
}
. xrf a . btn # meeting i . gg - user - add {
margin - right : 12 px ;
}
. xrf a . btn # share i . gg - link {
margin - right : 24 px ;
}
. xrf a . btn # accessibility i . gg - yinyang {
margin - right : 13 px ;
}
html {
max - width : unset ;
}
. render {
position : absolute ;
top : 0 ;
left : 0 ;
right : 0 ;
bottom : 0 ;
}
. lil - gui . autoPlace {
right : 0 px ! important ;
top : 48 px ! important ;
height : 33 vh ;
}
# VRButton {
margin - bottom : 20 vh ;
}
@ media ( max - width : 450 px ) {
# uri { display : none ; }
}
@ media ( max - width : 640 px ) {
. lil - gui . root {
top : auto ! important ;
left : auto ! important ;
}
. js - snackbar _ _message {
overflow - y : auto ;
max - height : 600 px ;
}
. js - snackbar _ _message h1 , h2 , h3 {
font - size : 22 px ;
}
. xrf table tr td {
}
: root {
-- xrf - font - size - 1 : 13 px ;
-- xrf - font - size - 2 : 17 px ;
-- xrf - font - size - 3 : 20 px ;
}
}
. a - enter - vr - button , . a - enter - ar - button {
height : 41 px ;
}
# qrcode {
background : transparent ;
overflow : hidden ;
height : 121 px ;
display : inline - block ;
position : relative ;
}
input # share {
font - size : var ( -- xrf - font - size - 1 ) ;
font - family : var ( -- xrf - font - monospace ) ;
border : 2 px solid # AAA ;
width : 50 vw ;
max - width : 400 px ;
}
. footer {
z - index : 1000 ;
display : flex ;
flex - direction : column - reverse ; /* This reverses the stacking order of the flex container */
align - items : flex - end ;
height : 100 % ;
position : fixed ;
top : 71 px ;
right : 11 px ;
bottom : 0 ;
padding - bottom : 140 px ;
box - sizing : border - box ;
pointer - events : none ;
}
. footer * {
pointer - events : all ;
}
. footer . menu {
text - align : right ;
}
2024-01-29 20:19:04 +00:00
. badge ,
# messages . msg . ui div . badge {
2024-02-29 13:36:00 +00:00
box - sizing : border - box ;
2024-01-03 14:23:34 +00:00
display : inline - block ;
color : var ( -- xrf - white ) ;
font - weight : bold ;
2024-02-29 13:36:00 +00:00
background : var ( -- xrf - dark - gray ) ;
border - radius : 16 px ;
padding : 0 px 12 px ;
2024-01-03 14:23:34 +00:00
font - size : var ( -- xrf - font - size - 0 ) ;
margin - right : 10 px ;
text - decoration : none ! important ;
}
2024-02-29 13:36:00 +00:00
# messages . msg . ui div . badge a {
color : # FFF ;
}
2024-01-03 14:23:34 +00:00
. ruler {
width : 97 % ;
margin : 7 px 0 px ;
}
a . badge {
text - decoration : none ;
}
. xrf select {
border - inline : none ;
border - inline : none ;
border - block : none ;
border : 3 px solid var ( -- xrf - primary ) ;
border - radius : 5 px ;
background : none ;
border - radius : 30 px ;
}
. xrf select ,
. xrf option {
padding : 0 px 16 px ;
min - width : 150 px ;
max - width : 150 px ;
height : 35 px ;
}
. xrf input {
border - radius : 30 px ;
padding : 7 px 15 px ;
border - block : none ;
border - inline : none ;
border : 1 px solid # 888 ;
background : transparent ;
max - width : 105 px ;
}
. xrf table tr td {
vertical - align : middle ;
text - align : right ;
}
. xrf table tr td : nth - child ( 1 ) {
min - width : 82 px ;
height : 40 px ;
padding - right : 15 px ;
}
. xrf small {
font - size : var ( -- xrf - font - size - 0 ) ;
}
. disabled {
opacity : 0.5
}
body . menu . js - snackbar _ _wrapper {
top : 64 px ;
}
2024-02-29 13:36:00 +00:00
. transcript {
max - height : 105 px ;
max - width : 405 px ;
overflow - y : auto ;
border : 1 px solid var ( -- xrf - gray ) ;
border - radius : 5 px ;
padding : 10 px ;
}
2024-01-29 20:19:04 +00:00
. right { float : right }
. left { float : left }
/ *
* tabs
* /
div . tab - frame > input { display : none ; }
div . tab - frame > label { display : block ; float : left ; padding : 5 px 10 px ; cursor : pointer ; }
div . tab - frame > input : checked + label { cursor : default ; border - bottom : 1 px solid # 888 ; font - weight : bold ; }
div . tab - frame > div . tab { display : none ; padding : 15 px 10 px 5 px 10 px ; clear : left }
div . tab - frame > input : nth - of - type ( 1 ) : checked ~ . tab : nth - of - type ( 1 ) ,
div . tab - frame > input : nth - of - type ( 2 ) : checked ~ . tab : nth - of - type ( 2 ) ,
div . tab - frame > input : nth - of - type ( 3 ) : checked ~ . tab : nth - of - type ( 3 ) { display : block ; }
2024-01-03 14:23:34 +00:00
/ *
* css icons from https : //css.gg
* /
. gg - close - o {
box - sizing : border - box ;
position : relative ;
display : block ;
transform : scale ( var ( -- ggs , 1 ) ) ;
width : 22 px ;
height : 22 px ;
border : 2 px solid ;
border - radius : 40 px
}
. gg - close - o : : after ,
. gg - close - o : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
width : 12 px ;
height : 2 px ;
background : currentColor ;
transform : rotate ( 45 deg ) ;
border - radius : 5 px ;
top : 8 px ;
left : 3 px
}
. gg - close - o : : after {
transform : rotate ( - 45 deg )
}
. gg - user - add {
display : inline - block ;
transform : scale ( var ( -- ggs , 1 ) ) ;
box - sizing : border - box ;
width : 20 px ;
height : 18 px ;
background :
linear - gradient (
to left ,
currentColor 8 px ,
transparent 0 )
no - repeat 14 px 6 px / 6 px 2 px ,
linear - gradient (
to left ,
currentColor 8 px ,
transparent 0 )
no - repeat 16 px 4 px / 2 px 6 px
}
. gg - user - add : : after , . gg - user - add : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
border : 2 px solid
}
. gg - user - add : : before {
width : 8 px ;
height : 8 px ;
border - radius : 30 px ;
top : 0 ;
left : 2 px
}
. gg - user - add : : after {
width : 12 px ;
height : 9 px ;
border - bottom : 0 ;
border - top - left - radius : 3 px ;
border - top - right - radius : 3 px ;
top : 9 px
}
. gg - user {
display : inline - block ;
transform : scale ( var ( -- ggs , 1 ) ) ;
box - sizing : border - box ;
width : 12 px ;
height : 18 px
}
. gg - user : : after ,
. gg - user : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
border : 2 px solid
}
. gg - user : : before {
width : 8 px ;
height : 8 px ;
border - radius : 30 px ;
top : 0 ;
left : 2 px
}
. gg - user : : after {
width : 12 px ;
height : 9 px ;
border - bottom : 0 ;
border - top - left - radius : 3 px ;
border - top - right - radius : 3 px ;
top : 9 px
}
. gg - menu {
transform : scale ( var ( -- ggs , 1 ) )
}
. gg - menu ,
. gg - menu : : after ,
. gg - menu : : before {
box - sizing : border - box ;
position : relative ;
display : block ;
width : 20 px ;
height : 2 px ;
border - radius : 3 px ;
background : currentColor
}
. gg - menu : : after ,
. gg - menu : : before {
content : "" ;
position : absolute ;
top : - 6 px
}
. gg - menu : : after {
top : 6 px
}
. gg - close {
box - sizing : border - box ;
position : relative ;
display : block ;
transform : scale ( var ( -- ggs , 1 ) ) scale ( var ( -- ggs , 1 ) ) translate ( - 2 px , 5 px ) ;
width : 22 px ;
height : 22 px ;
border : 2 px solid transparent ;
border - radius : 40 px
}
. gg - close : : after ,
. gg - close : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
width : 16 px ;
height : 2 px ;
background : currentColor ;
transform : rotate ( 45 deg ) ;
border - radius : 5 px ;
top : 8 px ;
left : 1 px
}
. gg - close : : after {
transform : rotate ( - 45 deg )
}
. gg - link {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
- moz - transform : rotate ( - 45 deg ) scale ( var ( -- ggs , 1 ) ) ;
2024-02-29 13:36:00 +00:00
transform : translate ( 4 px , - 5 px ) rotate ( - 45 deg ) scale ( var ( -- ggs , 1 ) ) ;
2024-01-03 14:23:34 +00:00
width : 8 px ;
height : 2 px ;
background : currentColor ;
line - height : 11 px ;
border - radius : 4 px
}
. gg - link : : after ,
. gg - link : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
border - radius : 3 px ;
width : 8 px ;
height : 10 px ;
border : 2 px solid ;
top : - 4 px
}
. gg - link : : before {
border - right : 0 ;
border - top - left - radius : 40 px ;
border - bottom - left - radius : 40 px ;
left : - 6 px
}
. gg - link : : after {
border - left : 0 ;
border - top - right - radius : 40 px ;
border - bottom - right - radius : 40 px ;
right : - 6 px
}
. gg - info {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
transform : scale ( var ( -- ggs , 1 ) ) translate ( - 3 px , 3 px ) ;
width : 20 px ;
height : 20 px ;
border : 2 px solid ;
border - radius : 40 px
}
. gg - info : : after ,
. gg - info : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
border - radius : 3 px ;
width : 2 px ;
background : currentColor ;
left : 7 px
}
. gg - info : : after {
bottom : 2 px ;
height : 8 px
}
. gg - info : : before {
height : 2 px ;
top : 2 px
}
. gg - yinyang {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
transform : rotate ( 95 deg ) scale ( var ( -- ggs , 1 ) ) translate ( 4 px , 4 px ) ;
width : 20 px ;
height : 20 px ;
border : 2 px solid ;
border - radius : 22 px
}
. gg - yinyang : : after ,
. gg - yinyang : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
width : 8 px ;
height : 8 px ;
border - radius : 10 px ;
top : 4 px
}
. gg - yinyang : : before {
border : 2 px solid ;
left : 0
}
. gg - yinyang : : after {
border : 2 px solid transparent ;
right : 0 ;
box - shadow :
inset 0 0 0 4 px ,
0 - 3 px 0 1 px ,
- 2 px - 4 px 0 1 px ,
- 8 px - 5 px 0 - 1 px ,
- 11 px - 3 px 0 - 2 px ,
- 12 px - 1 px 0 - 3 px ,
- 6 px - 6 px 0 - 1 px
}
. gg - image {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
transform : scale ( var ( -- ggs , 1 ) ) translate ( 1 px , 2 px ) ;
width : 20 px ;
height : 16 px ;
overflow : hidden ;
box - shadow : 0 0 0 2 px ;
border - radius : 2 px
}
. gg - image : : after ,
. gg - image : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
border : 2 px solid
}
. gg - image : : after {
transform : rotate ( 45 deg ) ;
border - radius : 3 px ;
width : 16 px ;
height : 16 px ;
top : 9 px ;
left : 6 px
}
. gg - image : : before {
width : 6 px ;
height : 6 px ;
border - radius : 100 % ;
top : 2 px ;
left : 2 px
}
. gg - serverless {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
transform : scale ( var ( -- ggs , 1 ) ) translate ( 2 px , 1 px ) ;
width : 15 px ;
height : 13 px ;
overflow : hidden
}
. gg - serverless : : after ,
. gg - serverless : : before {
background : currentColor ;
content : "" ;
box - sizing : border - box ;
position : absolute ;
display : block ;
height : 3 px ;
box - shadow : 0 5 px 0 , 0 10 px 0 ;
transform : skew ( - 20 deg )
}
. gg - serverless : : before {
width : 8 px ;
left : - 2 px
}
. gg - serverless : : after {
width : 12 px ;
right : - 5 px
}
. gg - software - download {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
2024-02-29 13:36:00 +00:00
transform : scale ( var ( -- ggs , 1 ) ) translate ( 3 px , 3 px ) ;
2024-01-03 14:23:34 +00:00
width : 16 px ;
height : 6 px ;
border : 2 px solid ;
border - top : 0 ;
border - bottom - left - radius : 2 px ;
border - bottom - right - radius : 2 px ;
2024-01-29 20:19:04 +00:00
line - height : 15 px ;
2024-01-03 14:23:34 +00:00
}
. gg - software - download : : after {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
width : 8 px ;
height : 8 px ;
border - left : 2 px solid ;
border - bottom : 2 px solid ;
transform : rotate ( - 45 deg ) ;
left : 2 px ;
bottom : 4 px
}
. gg - software - download : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
border - radius : 3 px ;
width : 2 px ;
height : 10 px ;
background : currentColor ;
left : 5 px ;
bottom : 5 px
}
. gg - arrow - left - r {
box - sizing : border - box ;
position : relative ;
display : inline - block ;
width : 22 px ;
height : 22 px ;
border : 2 px solid ;
transform : scale ( var ( -- ggs , 1 ) ) ;
border - radius : 4 px
}
. gg - arrow - left - r : : after ,
. gg - arrow - left - r : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
left : 4 px
}
. gg - arrow - left - r : : after {
width : 6 px ;
height : 6 px ;
border - bottom : 2 px solid ;
border - left : 2 px solid ;
transform : rotate ( 45 deg ) ;
bottom : 6 px
}
. gg - arrow - left - r : : before {
width : 10 px ;
height : 2 px ;
bottom : 8 px ;
background : currentColor
}
< / s t y l e >
`
// this has some overlap with $menu.js
// frontend serves as a basis for shared functions (download, share e.g.)
window . frontend = ( opts ) => new Proxy ( {
html : `
< div id = "topbar" class = "xrf" >
< div class = "logo" > < / d i v >
< button id = "navback" onclick = "history.back()" > & # 8249 ; < / b u t t o n >
< button id = "navforward" onclick = "history.forward()" > & # 8250 ; < / b u t t o n >
< input id = "load" type = "submit" value = "load 3D file" > < / i n p u t >
2024-02-29 14:07:20 +00:00
< input type = "text" id = "uri" value = "" onchange = "AFRAME.XRF.navigator.to( $('#uri').value )" style = "display:none" / >
2024-01-03 14:23:34 +00:00
< / d i v >
` ,
el : null ,
plugin : { } ,
xrf ,
// this SUPER-emit forwards custom events to all objects supporting dispatchEvent
// perfect to broadcast events simultaniously to document + 3D scene
emit ( k , v ) {
v = v || { event : k }
for ( let i in opts ) {
if ( opts [ i ] . dispatchEvent ) {
if ( opts . debug ) console . log ( ` ${ i } .emit( ${ k } ,{...}) ` )
opts [ i ] . dispatchEvent ( new CustomEvent ( k , { detail : v } ) )
}
}
} ,
init ( ) {
// setup element and delegate events
this . el = document . createElement ( "div" )
this . el . innerHTML = this . html
document . body . appendChild ( this . el ) ;
( [ 'click' ] ) . map ( ( e ) => this . el . addEventListener ( e , ( ev ) => this [ e ] && this [ e ] ( ev . target . id , ev ) ) )
this
. setupFileLoaders ( )
. setupIframeUrlHandler ( )
. setupCapture ( )
. setupUserHints ( )
2024-01-29 20:19:04 +00:00
. setupNetworkListeners ( )
2024-01-03 14:23:34 +00:00
. hidetopbarWhenMenuCollapse ( )
2024-01-29 20:19:04 +00:00
. hideUIWhenNavigating ( )
2024-01-03 14:23:34 +00:00
window . notify = this . notify
setTimeout ( ( ) => {
document . dispatchEvent ( new CustomEvent ( "frontend:ready" , { detail : opts } ) )
} , 1 )
return this
} ,
click ( id , ev ) {
switch ( id ) {
case "load" : this . fileLoaders ( )
}
} ,
setupFileLoaders ( ) {
// enable user-uploaded asset files (activated by load button)
this . fileLoaders = this . loadFile ( {
".gltf" : ( file ) => file . arrayBuffer ( ) . then ( ( data ) => xrf . navigator . to ( file . name , null , ( new xrf . loaders . gltf ( ) ) , data ) ) ,
".glb" : ( file ) => file . arrayBuffer ( ) . then ( ( data ) => xrf . navigator . to ( file . name , null , ( new xrf . loaders . gltf ( ) ) , data ) )
} )
return this
} ,
setupIframeUrlHandler ( ) {
// allow iframe to open url
window . addEventListener ( 'message' , ( event ) => {
if ( event . data && event . data . url ) {
window . open ( event . data . url , '_blank' ) ;
}
} ) ;
return this
} ,
setupCapture ( ) {
// add screenshot component with camera to capture bigger size equirects
// document.querySelector('a-scene').components.screenshot.capture('perspective')
$ ( 'a-scene' ) . setAttribute ( "screenshot" , { camera : "[camera]" , width : 4096 * 2 , height : 2048 * 2 } )
return this
} ,
setupUserHints ( ) {
// notify navigation + href mouseovers to user
setTimeout ( ( ) => {
window . notify ( 'loading ' + document . location . search . substr ( 1 ) )
2024-02-29 13:36:00 +00:00
setTimeout ( ( ) => {
window . notify ( "use WASD-keys and mouse-drag to move around" , { timeout : false } )
xrf . addEventListener ( 'navigate' , ( ) => SnackBar ( ) ) // close dialogs when url changes
} , 2000 )
xrf . addEventListener ( 'href' , ( data ) => {
2024-02-29 14:07:20 +00:00
if ( ! data . selected ) return
2024-02-29 13:36:00 +00:00
let html = ` <b class="badge"> ${ data . mesh . isSRC && ! data . mesh . portal ? 'src' : 'href' } </b> ${ data . xrf ? data . xrf . string : data . mesh . userData . src } <br> `
2024-02-29 14:07:20 +00:00
let metadata = data . mesh . userData
2024-02-29 13:36:00 +00:00
let meta = xrf . Parser . getMetaData ( )
let hasMeta = false
for ( let label in meta ) {
let fields = meta [ label ]
for ( let i = 0 ; i < fields . length ; i ++ ) {
let field = fields [ i ]
if ( metadata [ field ] ) {
hasMeta = true
html += ` <br><b style="min-width:110px;display:inline-block"> ${ label } :</b> ${ metadata [ field ] } \n `
break
}
}
}
let transcript = ''
let root = data . mesh . portal ? data . mesh . portal . stencilObject : data . mesh
root . traverse ( ( n ) => {
if ( n . userData [ 'aria-description' ] && n . uuid != data . mesh . uuid ) {
transcript += ` <b># ${ n . name } </b> ${ n . userData [ 'aria-description' ] } . `
}
} )
if ( transcript . length ) html += ` <br><b>transcript:</b><br><div class="transcript"> ${ transcript } </div> `
if ( hasMeta && ! data . mesh . portal ) html += ` <br><br><a class="btn" style="float:right" onclick="xrf.navigator.to(' ${ data . mesh . userData . href } ')">Visit embedded scene</a> `
window . notify ( html , { timeout : 7000 * ( hasMeta ? 1.5 : 1 ) } )
} )
2024-01-03 14:23:34 +00:00
} , 100 )
return this
} ,
2024-01-29 20:19:04 +00:00
setupNetworkListeners ( ) {
document . addEventListener ( 'network.connect' , ( e ) => {
console . log ( "network.connect" )
2024-02-29 14:07:20 +00:00
window . notify ( "🪐 connecting to awesomeness.." )
2024-01-29 20:19:04 +00:00
$chat . send ( { message : ` 🪐 connecting to awesomeness.. ` , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.connected' , ( e ) => {
2024-02-29 14:07:20 +00:00
window . notify ( "🪐 connected to awesomeness.." )
2024-01-29 20:19:04 +00:00
$chat . visibleChatbar = true
$chat . send ( { message : ` 🎉 ${ e . detail . plugin . profile . name || '' } connected! ` , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.disconnect' , ( ) => {
2024-02-29 14:07:20 +00:00
window . notify ( "🪐 disconnecting.." )
2024-01-29 20:19:04 +00:00
} )
document . addEventListener ( 'network.info' , ( e ) => {
window . notify ( e . detail . message )
$chat . send ( { ... e . detail , class : [ 'info' ] , timeout : 5000 } )
} )
document . addEventListener ( 'network.error' , ( e ) => {
window . notify ( e . detail . message )
$chat . send ( { ... e . detail , class : [ 'info' ] , timeout : 5000 } )
} )
return this
} ,
2024-01-03 14:23:34 +00:00
hidetopbarWhenMenuCollapse ( ) {
// hide topbar when menu collapse button is pressed
document . addEventListener ( '$menu:collapse' , ( e ) => this . el . querySelector ( "#topbar" ) . style . display = e . detail === true ? 'block' : 'none' )
return this
} ,
2024-01-29 20:19:04 +00:00
hideUIWhenNavigating ( ) {
// hide ui when user is navigating the scene using mouse/touch
let showUI = ( show ) => ( e ) => {
let isChatMsg = e . target . closest ( '.msg' )
let isChatLine = e . target . id == 'chatline'
let isChatEmptySpace = e . target . id == 'messages'
let isUI = e . target . closest ( '.ui' )
//console.dir({class: e.target.className, id: e.target.id, isChatMsg,isChatLine,isChatEmptySpace,isUI, tagName: e.target.tagName})
if ( isUI || e . target . tagName . match ( /^(BUTTON|TEXTAREA|INPUT|A)/ ) || e . target . className . match ( /(btn)/ ) ) return
if ( show ) {
$chat . visible = true
} else {
2024-02-29 14:07:20 +00:00
$chat . visible = false
2024-01-29 20:19:04 +00:00
$menu . toggle ( false )
}
return true
}
document . addEventListener ( 'mousedown' , showUI ( false ) )
document . addEventListener ( 'mouseup' , showUI ( true ) )
document . addEventListener ( 'touchstart' , showUI ( false ) )
document . addEventListener ( 'touchend' , showUI ( true ) )
} ,
2024-01-03 14:23:34 +00:00
loadFile ( contentLoaders , multiple ) {
return ( ) => {
window . notify ( "if you're on Meta browser, file-uploads might be disabled" )
let input = document . createElement ( 'input' ) ;
input . type = 'file' ;
input . multiple = multiple ;
input . accept = Object . keys ( contentLoaders ) . join ( "," ) ;
input . onchange = ( ) => {
let files = Array . from ( input . files ) ;
2024-02-29 14:07:20 +00:00
let file = files . slice ? files [ 0 ] : files
2024-01-03 14:23:34 +00:00
for ( var i in contentLoaders ) {
let r = new RegExp ( '\\' + i + '$' )
if ( file . name . match ( r ) ) return contentLoaders [ i ] ( file )
}
alert ( file . name + " is not supported" )
} ;
input . click ( ) ;
}
} ,
notify ( _str , opts ) {
if ( window . outerWidth < 800 ) return
if ( window . accessibility && window . accessibility . enabled ) return $chat . send ( { message : _str , class : [ 'info' ] } )
2024-02-29 14:07:20 +00:00
opts = opts || { status : 'info' }
2024-01-03 14:23:34 +00:00
opts = Object . assign ( { status , timeout : 4000 } , opts )
opts . message = _str
if ( typeof str == 'string' ) {
2024-02-29 14:07:20 +00:00
str = _str . replace ( /(^\w+):/ , "<div class='badge'>\$1</div>" )
if ( ! opts . status ) {
2024-01-03 14:23:34 +00:00
if ( str . match ( /error/g ) ) opts . status = "danger"
if ( str . match ( /warning/g ) ) opts . status = "warning"
}
opts . message = str
}
window . SnackBar ( opts )
opts . message = typeof _str == 'string' ? _str : _str . innerText
window . frontend . emit ( "notify" , opts )
} ,
download ( ) {
function fetchAndDownload ( dataurl , filename ) {
var a = document . createElement ( "a" ) ;
a . href = dataurl ;
a . setAttribute ( "download" , filename ) ;
a . click ( ) ;
return false ;
}
let file = document . location . search . replace ( /\?/ , '' )
fetchAndDownload ( file , file )
} ,
updateHashPosition ( randomize ) {
// *TODO* this should be part of the XRF Threejs framework
2024-02-29 14:07:20 +00:00
if ( typeof THREE == 'undefined' ) THREE = xrf . THREE
2024-01-03 14:23:34 +00:00
let radToDeg = THREE . MathUtils . radToDeg
let toDeg = ( x ) => x / ( Math . PI / 180 )
let camera = document . querySelector ( '[camera]' ) . object3D . parent // *TODO* fix for threejs
camera . position . x += Math . random ( ) / 10
camera . position . z += Math . random ( ) / 10
// *TODO* add camera direction
let direction = new xrf . THREE . Vector3 ( )
camera . getWorldDirection ( direction )
const pitch = Math . asin ( direction . y ) ;
const yaw = Math . atan2 ( direction . x , direction . z ) ;
const pitchInDegrees = pitch * 180 / Math . PI ;
const yawInDegrees = yaw * 180 / Math . PI ;
let lastPos = ` pos= ${ camera . position . x . toFixed ( 2 ) } , ${ camera . position . y . toFixed ( 2 ) } , ${ camera . position . z . toFixed ( 2 ) } `
let newHash = document . location . hash . replace ( /[&]?(pos|rot)=[0-9\.-]+,[0-9\.-]+,[0-9\.-]+/ , '' )
newHash += ` & ${ lastPos } `
document . location . hash = newHash . replace ( /&&/ , '&' )
. replace ( /#&/ , '' )
this . copyToClipboard ( window . location . href ) ;
} ,
copyToClipboard ( text ) {
2024-02-29 14:07:20 +00:00
// copy url to clipboard
2024-01-03 14:23:34 +00:00
var dummy = document . createElement ( 'input' )
document . body . appendChild ( dummy ) ;
dummy . value = text ;
dummy . select ( ) ;
document . execCommand ( 'copy' ) ;
2024-02-29 14:07:20 +00:00
document . body . removeChild ( dummy ) ;
2024-01-03 14:23:34 +00:00
} ,
share ( opts ) {
2024-01-29 20:19:04 +00:00
opts = opts || { notify : true , qr : true , share : true , linkonly : false }
if ( network . meetingLink && ! document . location . hash . match ( /meet=/ ) ) {
document . location . hash += ` &meet= ${ network . meetingLink } `
}
if ( ! document . location . hash . match ( /pos=/ ) ) {
document . location . hash += ` &pos= ${ network . posName || network . pos } `
2024-01-03 14:23:34 +00:00
}
let url = window . location . href
2024-02-29 14:07:20 +00:00
if ( opts . linkonly ) return url
2024-01-03 14:23:34 +00:00
this . copyToClipboard ( url )
2024-02-29 14:07:20 +00:00
// End of *TODO*
2024-01-03 14:23:34 +00:00
if ( opts . notify ) {
2024-01-29 20:19:04 +00:00
window . notify ( ` <h2> ${ network . connected ? 'Meeting link ' : 'Link' } copied to clipboard!</h2>
Now share it with your friends ❤ ️ < br >
2024-01-03 14:23:34 +00:00
< canvas id = "qrcode" width = "121" height = "121" > < / c a n v a s > < b r >
< button onclick = "frontend.download()" > < i class = "gg-software-download" > < / i > & n b s p ; & n b s p ; & n b s p ; d o w n l o a d s c e n e f i l e < / b u t t o n > < b r >
< button onclick = "alert('this might take a while'); $('a-scene').components.screenshot.capture('equirectangular')" > < i class = "gg-image" > < / i > & n b s p ; & n b s p ; d o w n l o a d 3 6 0 s c r e e n s h o t < / b u t t o n > < b r >
< a class = "btn" target = "_blank" href = "https://github.com/coderofsalvation/xrfragment-helloworld" > < i class = "gg-serverless" > < / i > & n b s p ; & n b s p ; & n b s p ; c l o n e & s e l f h o s t t h i s e x p e r i e n c e < / a > < b r >
To embed this experience in your blog , < br >
copy / paste the following into your HTML : < br > < input type = "text" value = "<iframe src='${document.location.href}'></iframe>" id = "share" / >
< br >
2024-01-29 20:19:04 +00:00
< br >
2024-01-03 14:23:34 +00:00
` ,{timeout:false})
}
// draw QR code
if ( opts . qr ) {
setTimeout ( ( ) => {
let QR = window . QR
QR . canvas = document . getElementById ( 'qrcode' )
QR . draw ( url , QR . canvas )
} , 1 )
}
// mobile share
if ( opts . share && typeof navigator . share != 'undefined' ) {
navigator . share ( {
url ,
title : 'your meeting link'
} )
}
2024-02-29 14:07:20 +00:00
$menu . collapse = true
2024-01-03 14:23:34 +00:00
}
} ,
2024-02-29 14:07:20 +00:00
{
// auto-trigger events on changes
2024-01-03 14:23:34 +00:00
get ( me , k , receiver ) { return me [ k ] } ,
set ( me , k , v ) {
let from = me [ k ]
me [ k ] = v
switch ( k ) {
case "logo" : $logo . style . backgroundImage = ` url( ${ v } ) ` ; break ;
default : me . emit ( ` me. ${ k } .change ` , { from , to : v } ) ; break ;
}
}
} )
2024-02-29 14:07:20 +00:00
2024-01-03 14:23:34 +00:00
frontend = frontend ( { xrf , document } ) . init ( )
// this orchestrates multiplayer events from the scene graph
window . network = ( opts ) => new Proxy ( {
connected : false ,
pos : '' ,
2024-01-29 20:19:04 +00:00
posName : '' ,
2024-01-03 14:23:34 +00:00
meetinglink : "" ,
peers : { } ,
plugin : { } ,
opts ,
init ( ) {
document . addEventListener ( 'network.disconnect' , ( ) => this . connected = false )
document . addEventListener ( 'network.connected' , ( ) => this . connected = true )
setTimeout ( ( ) => window . frontend . emit ( 'network.init' ) , 100 )
return this
} ,
connect ( opts ) {
window . frontend . emit ( ` network. ${ this . connected ? 'disconnect' : 'connect' } ` , opts )
} ,
add ( peerid , data ) {
data = { lastUpdated : new Date ( ) . getTime ( ) , id : peerid , ... data }
this . peers [ peerid ] = data
window . frontend . emit ( ` network.peer.add ` , { peer } )
} ,
remove ( peerid , data ) {
delete this . peers [ peerid ]
window . frontend . emit ( ` network.peer.remove ` , { peer } )
} ,
send ( opts ) {
window . frontend . emit ( 'network.send' , opts )
} ,
receive ( opts ) {
} ,
getMeetingFromUrl ( url ) {
let hash = url . replace ( /.*#/ , '' )
let parts = hash . split ( "&" )
let meeting = ''
parts . map ( ( p ) => {
if ( p . split ( "=" ) [ 0 ] == 'meet' ) meeting = p . split ( "=" ) [ 1 ]
} )
return meeting
} ,
randomRoom ( ) {
var names = [ ]
let add = ( s ) => s . length < 6 && ! s . match ( /[0-9$]/ ) && ! s . match ( /_/ ) ? names . push ( s ) : false
for ( var i in window ) add ( i )
for ( var i in Object . prototype ) add ( i )
for ( var i in Function . prototype ) add ( i )
for ( var i in Array . prototype ) add ( i )
for ( var i in String . prototype ) add ( i )
var a = names [ Math . floor ( Math . random ( ) * names . length ) ] ;
var b = names [ Math . floor ( Math . random ( ) * names . length ) ] ;
2024-01-29 20:19:04 +00:00
return String ( ` ${ a } - ${ b } - ${ String ( Math . random ( ) ) . substr ( 13 ) } ` ) . toLowerCase ( )
2024-01-03 14:23:34 +00:00
}
} ,
{
// auto-trigger events on changes
get ( data , k , receiver ) { return data [ k ] } ,
set ( data , k , v ) {
let from = data [ k ]
data [ k ] = v
}
} )
document . addEventListener ( 'frontend:ready' , ( e ) => {
window . network = network ( e . detail ) . init ( )
document . dispatchEvent ( new CustomEvent ( "network:ready" , e ) )
} )
// a portable snackbar
window . SnackBar = function ( userOptions ) {
var snackbar = this || ( window . snackbar = { } ) ;
var _Interval ;
var _Message ;
var _Element ;
var _Container ;
var _OptionDefaults = {
message : "Operation performed successfully." ,
dismissible : true ,
timeout : 7000 ,
status : ""
}
var _Options = _OptionDefaults ;
function _Create ( ) {
_Container = document . querySelector ( ".js-snackbar-container" )
if ( _Container ) {
_Container . remove ( )
}
_Container = null
if ( ! _Container ) {
// need to create a new container for notifications
_Container = document . createElement ( "div" ) ;
_Container . classList . add ( "js-snackbar-container" ) ;
document . body . appendChild ( _Container ) ;
}
_Container . opts = _Options
_Container . innerHTML = ''
_Element = document . createElement ( "div" ) ;
_Element . classList . add ( "js-snackbar__wrapper" , "xrf" ) ;
let innerSnack = document . createElement ( "div" ) ;
innerSnack . classList . add ( "js-snackbar" , "js-snackbar--show" ) ;
if ( _Options . status ) {
_Options . status = _Options . status . toLowerCase ( ) . trim ( ) ;
let status = document . createElement ( "span" ) ;
status . classList . add ( "js-snackbar__status" ) ;
if ( _Options . status === "success" || _Options . status === "green" ) {
status . classList . add ( "js-snackbar--success" ) ;
}
else if ( _Options . status === "warning" || _Options . status === "alert" || _Options . status === "orange" ) {
status . classList . add ( "js-snackbar--warning" ) ;
}
else if ( _Options . status === "danger" || _Options . status === "error" || _Options . status === "red" ) {
status . classList . add ( "js-snackbar--danger" ) ;
}
else {
status . classList . add ( "js-snackbar--info" ) ;
}
innerSnack . appendChild ( status ) ;
}
_Message = document . createElement ( "span" ) ;
_Message . classList . add ( "js-snackbar__message" ) ;
if ( typeof _Options . message == 'string' ) {
_Message . innerHTML = _Options . message ;
} else _Message . appendChild ( _Options . message )
innerSnack . appendChild ( _Message ) ;
if ( _Options . dismissible ) {
let closeBtn = document . createElement ( "span" ) ;
closeBtn . classList . add ( "js-snackbar__close" ) ;
closeBtn . innerText = "\u00D7" ;
closeBtn . onclick = snackbar . Close ;
innerSnack . appendChild ( closeBtn ) ;
}
_Element . style . height = "0px" ;
_Element . style . opacity = "0" ;
_Element . style . marginTop = "0px" ;
_Element . style . marginBottom = "0px" ;
_Element . appendChild ( innerSnack ) ;
_Container . appendChild ( _Element ) ;
if ( _Options . timeout !== false ) {
_Interval = setTimeout ( snackbar . Close , _Options . timeout ) ;
}
}
snackbar . Open = function ( ) {
let contentHeight = _Element . firstElementChild . scrollHeight ; // get the height of the content
_Element . style . height = contentHeight + "px" ;
_Element . style . opacity = 1 ;
_Element . style . marginTop = "5px" ;
_Element . style . marginBottom = "5px" ;
_Element . addEventListener ( "transitioned" , function ( ) {
_Element . removeEventListener ( "transitioned" , arguments . callee ) ;
_Element . style . height = null ;
} )
}
snackbar . Close = function ( ) {
if ( _Interval )
clearInterval ( _Interval ) ;
let snackbarHeight = _Element . scrollHeight ; // get the auto height as a px value
let snackbarTransitions = _Element . style . transition ;
_Element . style . transition = "" ;
requestAnimationFrame ( function ( ) {
_Element . style . height = snackbarHeight + "px" ; // set the auto height to the px height
_Element . style . opacity = 1 ;
_Element . style . marginTop = "0px" ;
_Element . style . marginBottom = "0px" ;
_Element . style . transition = snackbarTransitions
requestAnimationFrame ( function ( ) {
_Element . style . height = "0px" ;
_Element . style . opacity = 0 ;
} )
} ) ;
setTimeout ( function ( ) {
try {
_Container . removeChild ( _Element ) ;
} catch ( e ) { }
} , 1000 ) ;
} ;
_Options = { ... _OptionDefaults , ... userOptions }
_Create ( ) ;
2024-02-29 13:36:00 +00:00
if ( userOptions ) snackbar . Open ( ) ;
2024-01-03 14:23:34 +00:00
}
document . head . innerHTML += `
< style type = "text/css" >
. js - snackbar - container . btn ,
. js - snackbar - container input [ type = submit ] ,
. js - snackbar - container button {
margin - bottom : 15 px ;
}
. js - snackbar - container {
position : absolute ;
top : 10 px ;
left : 0 px ;
display : flex ;
align - items : center ;
width : 100 % ;
max - width : 100 % ;
padding : 10 px ;
z - index : 1001 ;
justify - content : center ;
overflow : hidden ;
}
. js - snackbar - container * {
box - sizing : border - box ;
}
. js - snackbar _ _wrapper {
-- color - c : # 555 ;
-- color - a : # FFF ;
}
. js - snackbar _ _wrapper {
transition : 1 s ;
overflow : hidden ;
height : auto ;
margin : 5 px 0 ;
transition : all ease . 5 s ;
2024-01-29 20:19:04 +00:00
border - radius : 15 px ;
2024-01-03 14:23:34 +00:00
box - shadow : 0 0 4 px 0 var ( -- xrf - box - shadow ) ;
right : 20 px ;
position : fixed ;
top : 18 px ;
}
. js - snackbar {
display : inline - flex ;
box - sizing : border - box ;
border - radius : 3 px ;
color : var ( -- color - c ) ;
background - color : var ( -- color - a ) ;
vertical - align : bottom ;
}
. js - snackbar _ _close ,
. js - snackbar _ _status ,
. js - snackbar _ _message {
position : relative ;
}
. js - snackbar _ _message {
margin : 12 px ;
}
. js - snackbar _ _status {
display : none ;
width : 15 px ;
margin - right : 5 px ;
border - radius : 3 px 0 0 3 px ;
background - color : transparent ;
}
. js - snackbar _ _status . js - snackbar -- success ,
. js - snackbar _ _status . js - snackbar -- warning ,
. js - snackbar _ _status . js - snackbar -- danger ,
. js - snackbar _ _status . js - snackbar -- info {
display : block ;
}
. js - snackbar _ _status . js - snackbar -- success {
background - color : # 4 caf50 ;
}
. js - snackbar _ _status . js - snackbar -- warning {
background - color : # ff9800 ;
}
. js - snackbar _ _status . js - snackbar -- danger {
background - color : # ff6060 ;
}
. js - snackbar _ _status . js - snackbar -- info {
background - color : # CCC ;
}
. js - snackbar _ _close {
cursor : pointer ;
display : flex ;
2024-01-29 20:19:04 +00:00
align - items : top ;
padding : 8 px 13 px 0 px 0 px ;
2024-01-03 14:23:34 +00:00
user - select : none ;
}
. js - snackbar _ _close : hover {
background - color : # 4443 ;
}
< / s t y l e >
`
} ) . apply ( { } )