2024-08-01 16:25:05 +02:00
/ *
* v0 . 5.1 generated at Thu Aug 1 04 : 21 : 30 PM CEST 2024
* https : //xrfragment.org
* SPDX - License - Identifier : AGPL - 3.0 - or - later
* /
2024-03-19 10:53:22 +01:00
( function ( ) {
// this orchestrates multiplayer events from the scene graph
window . network = ( opts ) => new Proxy ( {
connected : false ,
pos : '' ,
posName : '' ,
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 ) ] ;
return String ( ` ${ a } - ${ b } - ${ String ( Math . random ( ) ) . substr ( 13 ) } ` ) . toLowerCase ( )
}
} ,
{
// 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 ) )
} )
connectionsComponent = {
html : `
< div id = "connections" >
< 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 >
< / t a b l e >
< / 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 >
< / d i v >
< / d i v >
` ,
init : ( el ) => new Proxy ( {
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' ) } ] ,
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 ( ) {
$chat . visible = ! $chat . visible
} ,
change ( id , e ) {
if ( id . match ( /^(webcam|chatnetwork|scene)$/ ) ) {
this . renderSettings ( ) // trigger this when 'change' event fires on children dom elements
}
} ,
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
} else {
$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' ] } )
}
}
} ,
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 = { }
let select = ( name ) => ( o ) => o . profile . name == name ? plugins [ o . profile . name ] = o : ''
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 = ''
this . forSelectedPluginsDo ( ( plugin ) => $settings . appendChild ( plugin . config ( { ... opts , plugin } ) ) )
this . renderInputs ( )
} ,
renderInputs ( ) {
if ( ! this . selectedWebcam || this . selectedWebcam == 'No thanks' ) {
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 ) ;
} )
} ,
reactToNetwork ( ) { // *TODO* move to network?
document . addEventListener ( 'network.connect' , ( ) => {
this . show ( { hide : true , showChat : true } )
} )
document . addEventListener ( 'network.disconnect' , ( ) => {
this . connected = false
} )
}
} , {
get ( data , k , v ) { return data [ k ] } ,
set ( data , k , v ) {
data [ k ] = v
switch ( k ) {
2024-04-25 17:57:06 +02:00
case "visible" : el . style . display = v ? '' : 'none' ;
if ( ! v && el . parentNode && el . parentNode . parentNode ) el . parentNode . parentNode . remove ( )
break ;
2024-03-19 10:53:22 +01:00
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 ;
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!
2024-04-16 18:44:55 +02:00
document . addEventListener ( '$chat:ready' , ( opts ) => {
2024-03-19 10:53:22 +01:00
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 ;
top : 16 px ;
}
# messages . msg . ui div . tab - frame > div . tab { padding : 25 px 10 px 5 px 10 px ; }
< / s t y l e > `
chatComponent = {
html : `
< div id = "chat" >
< div id = "videos" style = "pointer-events:none" > < / d i v >
2024-04-25 17:57:06 +02:00
< div id = "messages" aria - live = "assertive" role = "log" aria - relevant = "additions" > < / d i v >
2024-03-19 10:53:22 +01:00
< div id = "chatfooter" >
< div id = "chatbar" >
< input id = "chatline" type = "text" placeholder = "chat here" > < / i n p u t >
< / d i v >
2024-04-25 17:57:06 +02:00
< button id = "chatsend" class = "btn" aria - label = "send message" >
< i class = "gg-chevron-right-o" > < / i >
< / b u t t o n >
2024-03-19 10:53:22 +01:00
< / d i v >
< / d i v >
` ,
init : ( el ) => new Proxy ( {
scene : null ,
visible : true ,
messages : [ ] ,
oneMessagePerUser : false ,
username : '' , // configured by 'network.connected' event
$videos : el . querySelector ( "#videos" ) ,
$messages : el . querySelector ( "#messages" ) ,
$chatline : el . querySelector ( "#chatline" ) ,
$chatbar : el . querySelector ( "#chatbar" ) ,
2024-04-25 17:57:06 +02:00
$chatsend : el . querySelector ( "#chatsend" ) ,
2024-03-19 10:53:22 +01:00
install ( opts ) {
this . opts = opts
this . scene = opts . scene
this . $chatbar . style . display = 'none'
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" ] } )
} ,
2024-04-25 17:57:06 +02:00
sendInput ( value ) {
if ( value [ 0 ] == '#' ) return xrf . navigator . to ( value )
let event = value . match ( /^[!\/]/ ) ? "chat.command" : "network.send"
let message = value . replace ( /^[!\/]/ , '' )
let raw = { detail : { message : value , halt : false } }
document . dispatchEvent ( new CustomEvent ( event , { detail : { message } } ) )
document . dispatchEvent ( new CustomEvent ( "chat.input" , raw ) )
if ( event == "network.send" && ! raw . detail . halt ) this . send ( { message : value } )
this . $chatline . lastValue = value
this . $chatline . value = ''
if ( window . innerHeight < 600 ) this . $chatline . blur ( )
} ,
2024-03-19 10:53:22 +01:00
initListeners ( ) {
let { $chatline } = this
$chatline . addEventListener ( 'click' , ( e ) => this . inform ( ) )
$chatline . addEventListener ( 'keydown' , ( e ) => {
if ( e . key == 'Enter' ) {
2024-04-25 17:57:06 +02:00
this . sendInput ( $chatline . value )
}
if ( e . key == 'ArrowUp' ) {
$chatline . value = $chatline . lastValue || ''
2024-03-19 10:53:22 +01: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
} )
2024-04-25 17:57:06 +02:00
document . addEventListener ( 'chat.command' , ( e ) => {
if ( String ( e . detail . message ) . trim ( ) == 'help' ) {
let detail = { message : ` The following commands are available
< br > < br >
< b class = "badge" > / h e l p < / b > s h o w s t h i s h e l p s c r e e n
` }
document . dispatchEvent ( new CustomEvent ( 'chat.command.help' , { detail } ) )
this . send ( { message : detail . message } )
}
} )
this . $chatsend . addEventListener ( 'click' , ( e ) => {
this . sendInput ( $chatline . value )
} )
2024-03-19 10:53:22 +01:00
} ,
inform ( ) {
if ( ! this . inform . informed && ( this . inform . informed = true ) ) {
2024-04-25 17:57:06 +02:00
window . notify ( "You can now type messages in the textfield below." )
2024-03-19 10:53:22 +01:00
}
} ,
toggle ( ) {
this . visible = ! this . visible
if ( this . visible && window . meeting . status == 'offline' ) window . meeting . start ( this . opts )
} ,
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
send ( opts ) {
let { $messages } = this
opts = { linebreak : true , message : "" , class : [ ] , ... opts }
if ( window . frontend && window . frontend . emit ) window . frontend . emit ( '$chat.send' , opts )
opts . pos = opts . pos || network . posName || network . pos
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-04-25 17:57:06 +02:00
if ( ! msg . className . match ( /(info|guide|ui)/ ) && ! opts . from ) {
2024-04-16 15:19:08 +02:00
let frag = xrf . URI . parse ( document . location . hash ) . XRF
2024-03-19 10:53:22 +01:00
opts . from = 'you'
if ( frag . pos ) opts . pos = frag . pos . string
msg . classList . add ( 'self' )
}
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 )
// 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
$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 ;
2024-04-16 18:44:55 +02:00
z - index : 1000 ;
2024-03-19 10:53:22 +01:00
}
# 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 ,
2024-04-25 17:57:06 +02:00
button # chatsend {
2024-03-19 10:53:22 +01:00
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 ;
}
2024-04-25 17:57:06 +02:00
button # chatsend {
line - height : 0 px ;
display : none ;
z - index : 1550 ;
color : white ;
border : 0 ;
height : 35 px ;
background : var ( -- xrf - dark - gray ) ;
font - weight : bold ;
width : 20 px ;
max - width : 20 px ;
border - radius : 20 px 0 px 0 px 20 px ;
overflow : hidden ;
2024-03-19 10:53:22 +01:00
}
# chatbar input {
border : none ;
width : 90 % ;
box - sizing : border - box ;
height : 24 px ;
font - size : var ( -- xrf - font - size - 2 ) ;
max - width : unset ;
}
# messages {
/ *
display : flex ;
flex - direction : column ;
2024-06-15 17:33:08 +02:00
width : 100 % ;
max - width : 40 % ;
2024-03-19 10:53:22 +01:00
* /
width : 100 % ;
2024-04-25 17:57:06 +02:00
box - sizing : border - box ;
2024-03-19 10:53:22 +01:00
align - items : flex - start ;
position : absolute ;
transition : 1 s ;
top : 77 px ;
left : 0 ;
bottom : 49 px ;
padding : 20 px ;
overflow : hidden ;
2024-04-25 17:57:06 +02:00
overflow - y : auto ;
2024-03-19 10:53:22 +01:00
pointer - events : none ;
transition : 1 s ;
z - index : 100 ;
}
body . menu # messages {
top : 50 px ;
}
# messages : hover {
pointer - events : all ;
}
# messages * {
2024-04-25 17:57:06 +02:00
box - sizing : border - box ;
/ *
2024-03-19 10:53:22 +01:00
pointer - events : none ;
- webkit - user - select : none ;
- moz - user - select : - moz - none ;
- ms - user - select : none ;
user - select : none ;
2024-04-25 17:57:06 +02:00
* /
2024-03-19 10:53:22 +01:00
}
# messages . msg {
transition : all 1 s ease ;
background : # fff ;
display : inline - block ;
padding : 1 px 17 px ;
border - radius : 20 px ;
color : # 000 c ;
margin - bottom : 10 px ;
line - height : 23 px ;
line - height : 33 px ;
cursor : grabbing ;
border : 1 px solid # 0002 ;
}
2024-04-25 17:57:06 +02:00
# messages . msg * ,
# messages . user * {
2024-03-19 10:53:22 +01:00
pointer - events : all ;
- webkit - user - select : text ;
- moz - user - select : - moz - text ;
- ms - user - select : text ;
user - select : text ;
}
# messages . msg . self {
border - radius : 20 px ;
2024-04-25 17:57:06 +02:00
background : var ( -- xrf - dark - gray ) ;
2024-03-19 10:53:22 +01:00
}
# messages . msg . self ,
# messages . msg . self div {
color : # FFF ;
}
# messages . msg . info {
2024-04-25 17:57:06 +02:00
background : var ( -- xrf - white ) ;
2024-03-19 10:53:22 +01:00
border - radius : 20 px ;
2024-04-25 17:57:06 +02:00
color : var ( -- xrf - dark - gray ) ;
text - align : left ;
2024-03-19 10:53:22 +01:00
line - height : 19 px ;
}
# messages . msg . info ,
# messages . msg . info * {
font - size : var ( -- xrf - font - size - 0 ) ;
}
# messages . msg a {
text - decoration : underline ;
2024-04-25 17:57:06 +02:00
color : var ( -- xrf - light - xrf - secondary ) ;
2024-03-19 10:53:22 +01:00
font - weight : bold ;
transition : 0.3 s ;
}
# messages . msg . info a ,
# messages a . ruler {
color : # FFF ;
}
# messages . msg a : hover {
color : # 000 ;
}
# 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-04-25 17:57:06 +02:00
. envelope {
margin - right : 15 px ;
2024-06-15 17:33:08 +02:00
width : 100 % ;
max - width : 40 % ;
2024-04-25 17:57:06 +02:00
}
2024-03-19 10:53:22 +01:00
. envelope ,
. envelope * {
transition : 1 s ;
pointer - events : none ;
}
. envelope a ,
. envelope button ,
. envelope input ,
. envelope textarea ,
. envelope msg ,
. envelope msg * {
pointer - events : all ;
}
. user {
margin - left : 13 px ;
font - weight : bold ;
color : var ( -- xrf - dark - gray ) ;
}
. user , . user * {
font - size : var ( -- xrf - font - size - 0 ) ;
}
2024-04-25 17:57:06 +02:00
. gg - chevron - right - o {
color : # FFF ;
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 : 100 px
}
. gg - chevron - right - o : : after {
color : # FFF ;
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute ;
width : 6 px ;
height : 6 px ;
border - bottom : 2 px solid ;
border - right : 2 px solid ;
transform : rotate ( - 45 deg ) ;
left : 5 px ;
top : 6 px
}
2024-03-19 10:53:22 +01:00
< / s t y l e > `
2024-06-04 19:00:48 +02:00
window . debug = ( opts ) => new Proxy ( {
opts ,
enabled : false ,
$console : false ,
toggle ( ) { this . enabled = ! this . enabled } ,
settings ( ) {
this . toggle ( )
} ,
init ( ) {
} ,
setupConsole ( ) {
// add onscreen console
let $console = this . $console = document . createElement ( 'pre' )
$console . style . position = 'fixed'
$console . style . overflow = 'auto'
$console . style . top = $console . style . left = $console . style . bottom = $console . style . right = '0px'
$console . style . height = '98.5vh' ;
$console . style . width = '100%'
$console . style . pointerEvents = 'none'
$console . id = 'console'
document . body . appendChild ( $console )
const wrapper = ( scope , fn , name ) => {
return function ( msg ) {
$console . innerHTML += ` [ ${ name } ] ${ msg } <br> ` ;
if ( name == 'err' ) {
let err = new Error ( )
String ( err . stack ) . split ( "\n" ) . slice ( 2 ) . map ( ( l ) => $console . innerHTML += ` └☑ ${ l } \n ` )
}
$console . scrollTop = $console . scrollHeight ;
fn . call ( scope , msg ) ;
} ;
}
window . console . log = wrapper ( console , console . log , "log" ) ;
window . console . warn = wrapper ( console , console . warn , "wrn" ) ;
window . console . error = wrapper ( console , console . error , "err" ) ;
}
} ,
{
// auto-trigger events on changes
get ( data , k , receiver ) { return data [ k ] } ,
set ( data , k , v ) {
data [ k ] = v
switch ( k ) {
case "enabled" : {
if ( ! data . $console ) data . setupConsole ( )
$ ( '#debug.btn' ) . style . filter = v ? 'brightness(1.0)' : 'brightness(0.5)'
data . $console . style . display = v ? 'block' : 'none'
data . enabled = v
}
}
}
} )
document . addEventListener ( '$menu:ready' , ( e ) => {
try {
debug = debug ( e . detail )
debug . init ( )
document . dispatchEvent ( new CustomEvent ( "debug: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 debug features" id="debug" onclick="debug.enabled = !debug.enabled"><i class="gg-debug"></i>debug</a><br> ` ] )
} catch ( e ) { console . error ( e ) }
} )
document . querySelector ( 'head' ) . innerHTML += `
< style type = "text/css" >
. gg - debug {
box - sizing : border - box ;
position : relative ;
display : block ;
transform : scale ( var ( -- ggs , 1 ) ) ;
width : 12 px ;
height : 18 px ;
border : 2 px solid ;
border - radius : 22 px ;
display : inline - block ;
transform : translate ( 0 px , 4 px ) ;
margin - right : 21 px ;
}
. gg - debug : : after ,
. gg - debug : : before {
content : "" ;
display : block ;
box - sizing : border - box ;
position : absolute
}
. gg - debug : : before {
width : 8 px ;
height : 4 px ;
border : 2 px solid ;
top : - 4 px ;
border - bottom - left - radius : 10 px ;
border - bottom - right - radius : 10 px ;
border - top : 0
}
. gg - debug : : after {
background : currentColor ;
width : 4 px ;
height : 2 px ;
border - radius : 5 px ;
top : 4 px ;
left : 2 px ;
box - shadow :
0 4 px 0 ,
- 6 px - 2 px 0 ,
- 6 px 2 px 0 ,
- 6 px 6 px 0 ,
6 px - 2 px 0 ,
6 px 2 px 0 ,
6 px 6 px 0
}
< / s t y l e >
`
2024-03-19 10:53:22 +01:00
} ) . apply ( { } )