2025-08-18 16:50:40 -06:00
import { pecBoard , pec } from './pecslib.js' ;
2025-07-15 17:33:12 -06:00
// gui object classes?
// Why am I doing this?
class auxillery _dialog {
constructor ( title ) {
2025-07-21 15:52:20 -06:00
var me = this ;
2025-07-15 17:33:12 -06:00
this . root = document . createElement ( "div" ) ;
2025-07-21 15:52:20 -06:00
this . root . classList . add ( "dialog" ) ;
2025-07-15 17:33:12 -06:00
this . title = "string" == typeof title ? title : "" ;
var title _bar = document . createElement ( "div" ) ;
2025-08-12 16:08:02 -06:00
title _bar . classList . add ( "title_bar" ) ;
var titleText = document . createElement ( "span" ) ;
titleText . innerText = this . title ;
titleText . classList . add ( "title_text" ) ;
title _bar . appendChild ( titleText ) ;
2025-07-15 17:33:12 -06:00
this . root . appendChild ( title _bar ) ;
this . body = document . createElement ( "div" ) ;
2025-07-21 15:52:20 -06:00
this . root . appendChild ( me . body ) ;
var close = document . createElement ( "span" ) ;
close . innerText = "<" ;
close . classList . add ( "close" ) ;
title _bar . appendChild ( close ) ;
close . addEventListener ( "click" , ( ) => {
me . root . classList . remove ( "focused" ) ;
} ) ;
2025-07-15 17:33:12 -06:00
}
}
2025-08-12 16:09:08 -06:00
// Functions
function systemMsg ( message , level = 0 ) { // abstraction?
// Yes, but it allows us to easilly redirect error
// output to where we want, for now, just the console...
switch ( level ) {
case 0 :
console . log ( "INFO: " + message ) ;
break ;
case 1 :
console . error ( "WARNING: " + message ) ;
break ;
case 2 :
console . error ( "ERROR: " + message ) ;
break ;
case 3 :
console . log ( "DEBUG: " + message ) ;
break ;
default :
console . log ( message ) ;
}
// but later we will also have it push an error toast in the ui.
}
2025-08-18 19:54:13 -06:00
function toggleDarkmode ( ) {
var html = document . documentElement ;
if ( html . classList . contains ( "darkmode" ) ) {
html . classList . remove ( "darkmode" ) ;
} else {
html . classList . add ( "darkmode" ) ;
}
}
2025-08-12 16:09:08 -06:00
function inRange ( target , min , max ) {
if ( isNaN ( target ) || isNaN ( min ) || isNaN ( max ) ) { return false ; }
return ( min <= target && target <= max ) ;
}
function invokeLibrary ( e ) {
// Breaking this out of event so its not an
// annon function, so we can swap it in and out
var targetID = ( ( e . currentTarget . getAttribute ( "id" ) ) . split ( "pec" ) ) [ 1 ] ;
if ( isNaN ( targetID ) ) { return systemMsg ( "invokeLibrary targetID not a number" , 2 ) ; }
var targetPec = app . gui [ "pec" + targetID ] ;
if ( undefined == targetPec ) { return systemMsg ( "invokeLibrary targetPec undefined: " + targetID , 2 ) ; }
if ( ! inRange ( targetID , 0 , 15 ) ) { return systemMsg ( "invokeLibrary targetID out of range: " + targetPecIndex , 2 ) ; }
app . targetPec = targetPec ;
app . targetPecIndex = targetID ;
app . gui . library . root . classList . add ( "focused" ) ;
}
function clearPec ( index = null ) {
if ( ! inRange ( index , 0 , 15 ) ) { return systemMsg ( "clearPec was given an out of range index" , 2 ) ; }
var targetPec = app . gui [ "pec" + index ] ;
if ( undefined == targetPec ) { return systemMsg ( "clearPec targetPec undefined" , 2 ) ; }
targetPec . classList . add ( "empty" ) ;
targetPec . innerHTML = "<p>+</p>" ;
targetPec . addEventListener ( "click" , invokeLibrary ) ;
}
function setPec ( pecTemplate = null , targetIndex = null ) {
if ( null == pecTemplate ) { return systemMsg ( "setPec was not provided data" , 2 ) ; }
if ( ! ( pecTemplate instanceof pec ) ) { return systemMsg ( "setPec was not provided a valid template" , 2 ) ; }
if ( isNaN ( targetIndex ) ) { return systemMsg ( "setPec was not provided a valid targetIndex" , 2 ) ; }
var targetPec = app . gui [ "pec" + targetIndex ] ;
targetPec . removeEventListener ( "click" , invokeLibrary ) ;
targetPec . innerHTML = "" ;
targetPec . classList . remove ( "empty" ) ;
2025-09-16 13:32:54 -06:00
var freshPec = new pec ( pecTemplate . word , pecTemplate . image ) ;
targetPec . appendChild ( freshPec . DOM ) ;
2025-08-12 16:09:08 -06:00
}
2025-08-18 15:18:56 -06:00
function repopBoards ( current = 0 ) {
//app.boards;
//app.currentBoards;
//app.menu;
// Normally we only want <, but we want an extra
// iteration here for the "add new board" element
for ( var i = 0 ; i <= app . boards . length ; i ++ ) {
var boardTab = document . getElementById ( "board" + i ) ;
if ( null == boardTab ) {
boardTab = document . createElement ( "span" ) ;
boardTab . setAttribute ( "id" , "board" + i ) ;
boardTab . classList . add ( "board_tab" ) ;
app . gui . menu . appendChild ( boardTab ) ;
}
// Clear event listeners
var oldTab = boardTab ;
boardTab = oldTab . cloneNode ( true ) ;
oldTab . parentNode . replaceChild ( boardTab , oldTab ) ;
// not sure how memory safe the above lines are
var name = "Board " + i ;
if ( i == app . boards . length ) {
name = "+" ;
boardTab . addEventListener ( "click" , ( e ) => {
var current = app . boards . findIndex ( i => i == app . currentBoard ) ;
app . boards . push ( new pecBoard ( ) ) ;
repopBoards ( current ) ;
repopGrid ( app . currentBoard ) ;
} ) ;
} else {
if ( current != i ) {
boardTab . classList . remove ( "active_board" ) ;
boardTab . addEventListener ( "click" , ( e ) => {
var sourceID = ( ( e . currentTarget . getAttribute ( "id" ) ) . split ( "board" ) ) [ 1 ] ;
app . currentBoard = app . boards [ sourceID ] ;
repopBoards ( sourceID ) ;
repopGrid ( app . currentBoard ) ;
} ) ;
} else {
boardTab . classList . add ( "active_board" ) ;
}
if ( undefined != app . boards [ i ] . name ) {
name = app . boards [ i ] . name ;
}
}
boardTab . innerText = name ;
}
}
2025-08-12 16:09:08 -06:00
function repopGrid ( board = null ) {
if ( null == board ) { return systemMsg ( "repopGrid was not provided data" , 2 ) ; }
if ( ! ( board instanceof pecBoard ) ) { return systemMsg ( "repopGrid was not provided data of type pecBoard" , 2 ) ; }
board . pecs . forEach ( ( p , i ) => {
if ( null == p ) { return clearPec ( i ) ; }
if ( ! ( p instanceof pec ) ) {
systemMsg ( "Invalid Pec found, scrubbing and returning to null" , 1 ) ;
board . pecs [ i ] = null ;
return clearPec ( i ) ;
}
setPec ( p , i ) ;
} ) ;
}
function useThisPec ( e ) {
var sourceID = ( ( e . currentTarget . getAttribute ( "id" ) ) . split ( "lPec" ) ) [ 1 ] ;
app . gui . library . root . classList . remove ( "focused" ) ;
2025-09-16 13:32:54 -06:00
app . currentBoard . pecs [ app . targetPecIndex ] = app . library . splice ( sourceID , 1 ) [ 0 ] ;
repopLibrary ( ) ;
2025-08-12 16:09:08 -06:00
repopGrid ( app . currentBoard ) ;
}
2025-09-16 13:32:54 -06:00
function createNewPec ( ) {
var name = app . gui . newpec _name . value ;
var image = app . gui . newpec _image . files [ 0 ] ;
app . library . push ( new pec ( name , URL . createObjectURL ( image ) ) ) ;
repopLibrary ( ) ;
app . gui . newpec . root . classList . remove ( "focused" ) ;
}
function updatePreview ( ) {
var name = app . gui . newpec _name . value ;
var image = app . gui . newpec _image . files [ 0 ] ;
app . gui . newpec _preview . innerHTML = "" ;
app . gui . newpec _preview . removeEventListener ( "click" , createNewPec ) ;
app . gui . newpec _status . innerText = "Create your new pec by naming it and providing an image." ;
var correct = 0 ;
if ( [ "image/png" , "image/jpeg" , "image/svg" ] . includes ( image ? . type ) ) {
var imageBox = document . createElement ( "img" ) ;
imageBox . src = URL . createObjectURL ( image ) ;
imageBox . classList . add ( "pecImage" ) ;
app . gui . newpec _preview . appendChild ( imageBox ) ;
correct ++ ;
}
if ( "" != name ) {
var nameBox = document . createElement ( "div" ) ;
nameBox . innerText = name ;
nameBox . classList . add ( "pecName" ) ;
app . gui . newpec _preview . appendChild ( nameBox ) ;
correct ++ ;
}
if ( 1 < correct ) {
app . gui . newpec _status . innerText = "Click/Tap the preview to save." ;
app . gui . newpec _preview . addEventListener ( "click" , createNewPec ) ;
}
}
function addNewPec ( e ) {
// clear out any previous values in form.
app . gui . newpec _form . reset ( ) ;
updatePreview ( ) ;
// make form visible
app . gui . newpec . root . classList . add ( "focused" ) ;
}
function repopLibrary ( ) {
// clear library
app . gui . library . body . innerHTML = "" ;
// recreate items from app.library
app . library . forEach ( ( p , i ) => {
var newPec = document . createElement ( "span" ) ;
newPec . classList . add ( "libSlot" ) ;
newPec . innerText = p . word ;
newPec . id = "lPec" + i ;
newPec . addEventListener ( "click" , useThisPec ) ;
app . gui . library . body . appendChild ( newPec ) ;
} ) ;
// add new pec button at end of list
var newPec = document . createElement ( "span" ) ;
newPec . classList . add ( "libSlot" ) ;
newPec . innerText = " + " ;
newPec . id = "lPec" + ( app . library . length + 1 ) ;
newPec . addEventListener ( "click" , addNewPec ) ;
app . gui . library . body . appendChild ( newPec ) ;
}
2025-07-07 16:09:29 -06:00
// Initialize gui
window . app = { } ;
app . gui = { } ;
app . gui . main = document . createElement ( "div" ) ;
app . gui . main . id = "root" ;
document . body . appendChild ( app . gui . main ) ;
app . gui . menu = document . createElement ( "nav" ) ;
app . gui . menu . id = "navigation" ;
app . gui . main . appendChild ( app . gui . menu ) ;
2025-08-18 15:21:33 -06:00
app . gui . optionsButton = document . createElement ( "div" ) ;
app . gui . optionsButton . classList . add ( "options_button" ) ;
app . gui . optionsButton . innerHTML = "☰" ;
app . gui . menu . appendChild ( app . gui . optionsButton ) ;
2025-07-07 16:09:29 -06:00
app . gui . view = document . createElement ( "div" ) ;
app . gui . view . id = "view" ;
app . gui . main . appendChild ( app . gui . view ) ;
// Library popup
2025-07-15 17:33:12 -06:00
app . gui . library = new auxillery _dialog ( "Library" ) ;
2025-08-18 19:54:13 -06:00
app . gui . library . body . classList . add ( "library_body" ) ;
2025-08-12 16:08:02 -06:00
app . gui . main . append ( app . gui . library . root ) ;
2025-07-15 17:33:12 -06:00
// Options popup
app . gui . options = new auxillery _dialog ( "Options" ) ;
2025-08-18 19:54:13 -06:00
app . gui . options . body . classList . add ( "options_body" ) ;
2025-08-18 15:21:33 -06:00
app . gui . main . append ( app . gui . options . root ) ;
app . gui . optionsButton . addEventListener ( "click" , ( ) => { app . gui . options . root . classList . add ( "focused" ) } ) ;
2025-09-16 13:32:54 -06:00
// New Pec popup
app . gui . newpec = new auxillery _dialog ( "Create New Pec" ) ;
app . gui . newpec . body . classList . add ( "newpec_body" ) ;
app . gui . main . append ( app . gui . newpec . root ) ;
2025-07-07 16:09:29 -06:00
// pre-populate grid with emptys
for ( var x = 0 ; x < 16 ; x ++ ) {
var name = "pec" + x ;
2025-08-12 16:08:02 -06:00
var thisPec = document . createElement ( "span" ) ;
app . gui [ name ] = thisPec ;
thisPec . classList . add ( "pecSlot" ) ;
thisPec . id = name ;
clearPec ( x ) ;
2025-07-07 16:09:29 -06:00
app . gui . view . appendChild ( thisPec ) ;
}
2025-08-12 16:10:23 -06:00
// Create our initial board
app . boards = [ ] ;
app . boards . push ( new pecBoard ( ) ) ;
app . currentBoard = app . boards [ 0 ] ;
2025-08-18 15:18:56 -06:00
repopBoards ( 0 ) ;
2025-08-12 16:10:23 -06:00
repopGrid ( app . currentBoard ) ;
2025-07-07 16:09:29 -06:00
2025-08-12 16:10:23 -06:00
// Populate Library
2025-08-18 16:50:40 -06:00
import { library as library } from './library.js' ;
app . library = library ;
2025-09-16 13:32:54 -06:00
repopLibrary ( ) ;
2025-08-18 15:21:33 -06:00
// Populate Options
app . options = { } ;
app . options . darkmode = false ; // should always be an option
app . options . audioFeedback = false ; // read word aloude
app . options . feedbackFloodLimit = 3 ;
2025-08-18 19:54:13 -06:00
app . options . feedbackTimeout = 60 ; // 60 seconds
2025-08-18 15:21:33 -06:00
// overload defaults with options stored in localstorage
// build Options UI
app . gui . options _darkmode = document . createElement ( "div" ) ;
app . gui . options . body . appendChild ( app . gui . options _darkmode ) ;
2025-08-18 19:54:13 -06:00
app . gui . options _darkmode . innerHTML = '<label for="darkmode-checkbox">Darkmode</label> <input id="darkmode-checkbox" type="checkbox" class="dm-indicator" />' ;
app . gui . options _darkmode . addEventListener ( "change" , toggleDarkmode ) ;
2025-08-18 15:21:33 -06:00
app . gui . options _audioFeedback = document . createElement ( "div" ) ;
app . gui . options . body . appendChild ( app . gui . options _audioFeedback ) ;
2025-08-18 19:54:13 -06:00
app . gui . options _audioFeedback . innerHTML = '<label for="audio-feedback-toggle">Read Allowed</label> <input id="audio-feedback-toggle" type="checkbox" class="toggle-switch" />' ;
2025-08-18 15:21:33 -06:00
app . gui . options _feedbackFloodLimit = document . createElement ( "div" ) ;
app . gui . options . body . appendChild ( app . gui . options _feedbackFloodLimit ) ;
2025-08-18 19:54:13 -06:00
app . gui . options _feedbackFloodLimit . innerHTML = '<label for="flood-limit">Audio Flood Limit</label> <input id="flood-limit" type="number" min=1 step=2 max=10 />' ;
2025-08-18 15:21:33 -06:00
app . gui . options _feedbackTimeout = document . createElement ( "div" ) ;
app . gui . options . body . appendChild ( app . gui . options _feedbackTimeout ) ;
2025-08-18 19:54:13 -06:00
app . gui . options _feedbackTimeout . innerHTML = '<label for="flood-timeout">Audio Flood Timeout</label> <input id="flood-timeout" type="number" min=0 step=10 />' ;
2025-08-18 21:15:09 -06:00
app . gui . options _about = document . createElement ( "div" ) ;
app . gui . options . body . appendChild ( app . gui . options _about ) ;
2025-09-16 13:32:54 -06:00
app . gui . options _about . innerHTML = 'App licenced <a href="LICENSE.txt" target="_blank"><img src="images/ui/gplv3-or-later.svg" height="30px" /></a><label>-</label> Cards from <a href="https://assistivecards.com" target="_blank"><img src="images/ui/ac.svg" height="30px" alt="Assistive Cards" /></a><label>-</label> Project Source <a href="https://labs.murkfall.net/bluesaxman/librePECS">labs.murkfall.net</a>' ;
app . gui . newpec . body . innerHTML = '<form id="new-pec-form"><label for="new-pec-name">Name</label><input id="new-pec-name" type="text" value="" /><br /><label for="new-pec-image">Picture</label><input id="new-pec-image" type="file" accept="image/png, image/jpeg, image/svg" value="" /><br /><div id="new-pec-preview"></div><br /><div id="new-pec-status">Create your new pec by naming it and providing an image.</div></form>' ;
app . gui . newpec _form = document . getElementById ( "new-pec-form" ) ;
app . gui . newpec _name = document . getElementById ( "new-pec-name" ) ;
app . gui . newpec _image = document . getElementById ( "new-pec-image" ) ;
app . gui . newpec _preview = document . getElementById ( "new-pec-preview" ) ;
app . gui . newpec _status = document . getElementById ( "new-pec-status" ) ;
app . gui . newpec _preview . classList . add ( "pecSlot" ) ;
app . gui . newpec _name . addEventListener ( "change" , updatePreview ) ;
app . gui . newpec _image . addEventListener ( "change" , updatePreview ) ;