/*
* v0.5.1 generated at Fri Aug 2 10:04:04 AM UTC 2024
* https://xrfragment.org
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
(function(){
// 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);
if( _Options.onclose ) _Options.onclose()
};
_Options = { ..._OptionDefaults, ...userOptions }
_Create();
if( userOptions ) snackbar.Open();
}
document.head.innerHTML += `
`
window.accessibility = (opts) => new Proxy({
opts,
enabled: false,
// features
speak_teleports: true,
speak_keyboard: false,
// audio settings
speak_rate: 1,
speak_pitch: 1,
speak_volume: 1,
speak_voice: -1,
speak_voices: 0,
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 ')
}
if( str == this.speak.lastStr ) return // no duplicates
this.speak.lastStr = str
let speech = window.speechSynthesis
let utterance = new SpeechSynthesisUtterance( str )
this.speak_voices = speech.getVoices().length
if( this.speak_voice != -1 && this.speak_voice < this.speak_voices ){
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(){
this
.speakArrowKeys()
.setupListeners()
.setupPersistance()
.setupHrefCycling()
.setupSpeechKillOnEscape()
setTimeout( () => this.initCommands(), 200 )
},
speakArrowKeys(){
// speak arrow keys
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})
})
return this
},
setupSpeechKillOnEscape(){
window.addEventListener('keydown', (e) => {
if( e.key == "Escape" ){
this.speak("stop",{override:true})
}
})
},
setupHrefCycling(){
// speak arrow keys
window.addEventListener('keydown', (e) => {
if( e.key != "Tab" && e.key != "Enter" ) return
let subScene = xrf.scene.getObjectByName( xrf.frag.pos.last )
if( !subScene ) subScene = xrf.scene
let cache = this.setupHrefCycling.cache = this.setupHrefCycling.cache || {current: -1}
let objects = []
subScene.traverse( (n) => (n.userData.href || n.userData['aria-description']) && objects.push(n) )
const highlight = (n) => {
if( this.helper){
if( this.helper.selected == n.uuid ) return // already selected
xrf.scene.remove(this.helper)
}
this.selected = n
this.helper = new THREE.BoxHelper( n, 0xFF00FF )
this.helper.computeLineDistances()
this.helper.material.linewidth = 8
this.helper.material.color = xrf.focusLine.material.color
this.helper.material.dashSize = xrf.focusLine.material.dashSize
this.helper.material.gapSize = xrf.focusLine.material.gapSize
this.helper.selected = n.uuid
xrf.scene.add(this.helper)
notify(`${n.userData['aria-description']||''}` + (n.userData.href ? `
name: ${n.name}
href: ${n.userData['href']}` :'') )
}
if( e.key == 'Enter' && objects[cache.current].userData.href ){
xrf.navigator.to( objects[cache.current].userData.href )
}
// increment to next
cache.current = (cache.current + 1) % objects.length
if( e.key == 'Tab'){
highlight( objects[cache.current] )
}
e.preventDefault()
return false
})
return this
},
setupListeners(){
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('$chat.send', (opts) => {
if( opts.detail.message ) this.speak( opts.detail.message)
})
})
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 && this.speak_teleports ){
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
}
})
return this
},
setupPersistance(){
// auto-enable if previously enabled
if( window.localStorage.getItem("accessibility") === 'true' || xrf.navigator.URI.XRF.accessible ){
setTimeout( () => {
this.enabled = true
this.setFontSize()
}, 100 )
}
return this
},
initCommands(){
document.addEventListener('chat.command.help', (e) => {
e.detail.message += `
<Escape> silence TTS `
e.detail.message += `
<Tab> cycle [href] buttons / silence TTS `
e.detail.message += `
/fontsize <number> set fontsize (default=14) `
})
document.addEventListener('chat.command', (e) => {
if( e.detail.message.match(/^fontsize/) ){
try{
let fontsize = parseInt( e.detail.message.replace(/^fontsize /,'').trim() )
if( fontsize == NaN ) throw 'not a number'
this.setFontSize(fontsize)
$chat.send({message:'fontsize set to '+fontsize})
}catch(e){
console.error(e)
$chat.send({message:'example usage: /fontsize 20'})
}
}
})
},
setFontSize(size){
if( size ){
window.localStorage.setItem("fontsize",size)
}else size = window.localStorage.getItem("fontsize")
if( !size ) return
document.head.innerHTML += `
`
$messages = document.querySelector('#messages')
setTimeout( () => $messages.scrollTop = $messages.scrollHeight, 1000 )
},
posToMessage(opts){
let obj
let description
let msg = "teleported to "
let pos = opts.frag.pos
if( pos.string.match(',') ) msg += `coordinates ${pos.string}`
else{
msg += `location ${pos.string}`
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 mode has been "+(v?"activated":"disabled")+".
Type /help for help."
$('#accessibility.btn').style.filter= v ? 'brightness(1.0)' : 'brightness(0.5)'
if( v ) $chat.visible = true
$chat.send({message,class:['info']})
data.enabled = true
data.speak(message)
data.enabled = v
window.localStorage.setItem("accessibility", v)
$chat.$messages.classList[ v ? 'add' : 'remove' ]('guide')
document.body.classList.toggle(['accessibility'])
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([`accessibility
`])
}catch(e){console.error(e)}
})
document.querySelector('head').innerHTML += `
`
// reactive component for displaying the menu
menuComponent = (el) => new Proxy({
html: `