@@ -0,0 +1,397 @@
// The widgets for selecting words. This comprises the main part of the UI.
Splitter . module ( 'Slot' , function ( Slot , Splitter , Backbone , M ) {
var Word = Backbone . Model . extend ( {
defaults : { text : '' , active : false } ,
activate : function ( options ) {
options || ( options = { } ) ;
this . collection . activateWord ( this , options ) ;
} ,
deactivate : function ( options ) {
options || ( options = { } ) ;
return this . set ( { active : false } , options ) ;
} ,
} ) ;
var SynList = Backbone . Collection . extend ( { model : Word } ) ;
var WordList = Backbone . Collection . extend ( {
model : Word ,
url : '/words' ,
activeWord : function ( ) {
var word = this . where ( { active : true } ) [ 0 ] ;
if ( ! word ) {
word = this . add ( { active : true , text : '' } ) ;
} ;
return word ;
} ,
activeIndex : function ( ) {
return this . indexOf ( this . activeWord ( ) ) ;
} ,
preActives : function ( ) {
var number = this . length - ( this . activeIndex ( ) + 1 ) ;
return this . last ( number ) ;
} ,
postActives : function ( ) {
return this . first ( this . activeIndex ( ) ) ;
} ,
resetActive : function ( text ) {
var newActiveWord ;
text = $ . trim ( text ) ;
if ( text !== this . lastFetchedText ) {
newActiveWord = new this . model ( ) ;
// Don't do anything if the text is invalid.
if ( newActiveWord . set ( { text : text , active : true } ) ) {
this . reset ( newActiveWord , { silent : true } ) ;
this . fetchSynonyms ( ) ;
}
}
} ,
fetchSynonyms : function ( ) {
var text = this . activeWord ( ) . get ( 'text' ) ;
// Don't actually send a request if the text is blank.
if ( text . length === 0 ) {
this . lastFetchedText = text ;
this . reset ( ) ;
return ;
}
$ . ajax ( {
url : '/synonyms/' + encodeURIComponent ( text ) ,
dataType : 'json' ,
success : _ . bind ( function ( json ) {
var wordAttrs ;
// console.log("Fetched synonyms of", text, json.synonyms);
wordAttrs = _ . map ( json . synonyms , function ( syn ) {
return { text : syn , active : false } ;
} )
// The problem with using add is that it causes the
// syn lists to re-render loads of times. THus, we must add
// silently before triggering the event once.
this . add ( wordAttrs , { silent : true } ) ;
} , this ) ,
error : _ . bind ( function ( json ) {
this . trigger ( 'error' , text , json . error ) ;
} , this ) ,
// This needs to happen on both success and error.
complete : _ . bind ( function ( json ) {
this . lastFetchedText = text ;
// console.log("Last fetched text set to", this.lastFetchedText);
// To clear the synonyms.
this . trigger ( 'reset' ) ;
} , this )
} ) ;
} ,
deactivateAll : function ( ) {
this . each ( function ( word ) { word . deactivate ( { silent : true } ) } ) ;
} ,
activateWord : function ( word , options ) {
options || ( options = { } ) ;
if ( word ) {
this . deactivateAll ( ) ;
return word . set ( { active : true } , options ) ;
} ;
} ,
promoteActive : function ( ) { this . changeActiveBy ( 1 ) ; } ,
demoteActive : function ( ) { this . changeActiveBy ( - 1 ) ; } ,
changeActiveBy : function ( places ) {
var currentActiveIndex = this . activeIndex ( ) ,
wordToActivate = this . at ( currentActiveIndex + places ) ;
// console.log("Changing active by", places, currentActiveIndex, wordToActivate);
this . activateWord ( wordToActivate ) ;
}
} ) ;
var SlotView = M . Layout . extend ( {
template : '#slot-layout-template' ,
className : 'slot span4' ,
regions : {
postActiveRegion : '.post-active-region' ,
preActiveRegion : '.pre-active-region' ,
activeWordRegion : '.active-word-region'
} ,
events : {
'click a.demote' : 'demoteClicked' ,
'click a.promote' : 'promoteClicked'
} ,
initialize : function ( ) {
var modelId = this . model . id ;
// Grab the focus when the slot tells us to.
this . bindTo ( this . model , 'focus' , this . takeFocus ) ;
// Attach the sub views.
this . activeWordView = new ActiveWordView ( {
collection : this . model . get ( 'words' ) ,
} ) ;
this . activeWordView . on ( 'show' , function ( ) {
// Add an arbitrary integer to the tabindex to leave
// room for in-between tab indexes later.
// NOTE: I can't move this into the sub view bacause I don't
// have access to the slot id there.
this . setTabIndex ( modelId + 11 ) ;
} ) ;
} ,
renderInactives : function ( ) {
var preActives = new SynonymListView ( {
collection : this . model . get ( 'preActives' )
} ) ;
this . preActiveRegion . show ( preActives ) ;
var postActives = new SynonymListView ( {
collection : this . model . get ( 'postActives' )
} ) ;
this . postActiveRegion . show ( postActives ) ;
} ,
renderActive : function ( ) {
this . activeWordRegion . show ( this . activeWordView ) ;
} ,
onRender : function ( ) {
// Add the slot id to the root element.
this . $el . attr ( { id : 'slot_' + this . model . id } ) ;
this . renderActive ( ) ;
this . renderInactives ( ) ;
} ,
takeFocus : function ( ) {
this . activeWordView . takeFocus ( ) ;
} ,
demoteClicked : function ( e ) {
e . preventDefault ( ) ;
this . model . demoteActive ( ) ;
} ,
promoteClicked : function ( e ) {
e . preventDefault ( ) ;
this . model . promoteActive ( ) ;
}
} ) ;
var Slot = Backbone . Model . extend ( {
defaults : function ( ) {
return {
words : new WordList ( ) ,
preActives : new SynList ( ) ,
postActives : new SynList ( )
} ;
} ,
initialize : function ( ) {
this . activeWord ( ) . bind ( 'fetch:synonyms' , this . fetchSynonyms , this ) ;
this . get ( 'words' ) . bind ( 'reset add change:active' , this . resetSynLists , this ) ;
this . get ( 'words' ) . bind ( 'reset change:active' , function ( word ) {
// console.log("Acive component", this.id, this.activeText());
Splitter . vent . trigger ( 'change:activeWord' , this . id , this . activeText ( ) ) ;
} , this ) ;
} ,
resetSynLists : function ( ) {
this . get ( 'preActives' ) . reset ( this . get ( 'words' ) . preActives ( ) ) ;
this . get ( 'postActives' ) . reset ( this . get ( 'words' ) . postActives ( ) ) ;
} ,
activeText : function ( ) { return this . activeWord ( ) . get ( 'text' ) ; } ,
activeWord : function ( ) { return this . get ( 'words' ) . activeWord ( ) ; } ,
promoteActive : function ( ) { this . get ( 'words' ) . promoteActive ( ) ; } ,
demoteActive : function ( ) { this . get ( 'words' ) . demoteActive ( ) ; } ,
} ) ;
var SynonymView = M . ItemView . extend ( {
template : '#synonym-template' ,
tagName : 'li' ,
events : {
'click a' : 'linkClicked'
} ,
serializeData : function ( ) {
return {
text : this . model . get ( 'text' )
} ;
} ,
linkClicked : function ( e ) {
e . preventDefault ( ) ;
this . model . activate ( ) ;
}
} ) ;
var SynonymListView = M . CollectionView . extend ( {
itemView : SynonymView ,
tagName : 'ul' ,
} ) ;
var ActiveWordView = M . ItemView . extend ( {
template : '#active-word-template' ,
tagName : 'form' ,
events : {
'keyup input' : 'keyPressed' ,
'submit' : 'formSubmitted'
} ,
// Override the initial events method to prevent the
// view from re-rendering whenever the collection resets.
initialEvents : function ( ) { } ,
keyPressed : function ( e ) {
switch ( e . which ) {
case 40 :
this . downPressed ( e ) ;
break ;
case 38 :
this . upPressed ( e ) ;
break ;
case 37 :
this . leftPressed ( e ) ;
break ;
case 39 :
this . rightPressed ( e ) ;
break ;
default :
this . typingFinished ( e ) ;
return ;
}
} ,
ui : { input : 'input' } ,
cursorPosition : function ( ) {
return this . $el . find ( this . ui . input ) . cursorPosition ( ) ;
} ,
getText : function ( ) {
return this . $el . find ( this . ui . input ) . val ( ) ;
} ,
setTabIndex : function ( index ) {
this . $el . find ( this . ui . input ) . attr ( { tabIndex : index } ) ;
} ,
leftPressed : function ( e ) {
if ( this . cursorPosition ( ) === 0 ) {
this . removeErrors ( ) ;
Splitter . slotsList . focusLeft ( ) ;
}
} ,
rightPressed : function ( e ) {
if ( this . cursorPosition ( ) === this . getText ( ) . length ) {
this . removeErrors ( ) ;
Splitter . slotsList . focusRight ( ) ;
}
} ,
takeFocus : function ( ) { this . $el . find ( this . ui . input ) . select ( ) ; } ,
downPressed : function ( ) {
this . collection . demoteActive ( ) ;
this . takeFocus ( ) ;
} ,
upPressed : function ( e ) {
this . collection . promoteActive ( ) ;
this . takeFocus ( ) ;
} ,
initialize : function ( ) {
this . typingFinished = _ . debounce ( this . typingFinished , 500 ) ;
// Re-render when the user changes the active word.
this . bindTo ( this . collection , 'change' , this . render ) ;
// Display invalid when the user enters an invalid word.
this . bindTo ( this . collection , 'error' , this . displayErrors ) ;
} ,
serializeData : function ( ) {
return {
text : this . collection . activeWord ( ) . get ( 'text' )
} ;
} ,
displayErrors : function ( ) {
this . $el . find ( this . ui . input )
. closest ( '.control-group' ) . addClass ( 'error' ) ;
} ,
removeErrors : function ( ) {
this . $el . find ( this . ui . input )
. closest ( '.control-group' ) . removeClass ( 'error' ) ;
} ,
typingFinished : function ( e ) {
this . removeErrors ( ) ;
this . collection . resetActive ( this . getText ( ) ) ;
} ,
formSubmitted : function ( e ) {
e . preventDefault ( ) ;
this . typingFinished ( ) ;
}
} ) ;
var SlotsList = Backbone . Collection . extend ( {
url : '/words' ,
model : Slot ,
nextSlotId : function ( ) { return this . models . length ; } ,
focusLeft : function ( ) {
// HACK: Hard-coded slot id.
var leftSlot = this . at ( 0 ) ;
if ( leftSlot ) { leftSlot . trigger ( 'focus' ) ; } ;
} ,
focusRight : function ( ) {
// HACK: Hard-coded slot id.
var rightSlot = this . at ( 1 ) ;
if ( rightSlot ) { rightSlot . trigger ( 'focus' ) ; } ;
} ,
// Add a slot to the collection.
addSlot : function ( component ) {
var newModel = new this . model ( { id : this . nextSlotId ( ) } ) ;
this . add ( newModel ) ;
if ( component ) { newModel . get ( 'words' ) . resetActive ( component ) ; }
}
} ) ;
var SlotsListView = M . CollectionView . extend ( {
itemView : SlotView ,
} ) ;
Splitter . addInitializer ( function ( options ) {
var wordAttrs = options . word || { } ,
slotsListView ;
this . slotsList = new SlotsList ( ) ;
_ . each ( wordAttrs . components , _ . bind ( function ( component ) {
this . slotsList . addSlot ( component ) ;
} , this ) ) ;
slotsListView = new SlotsListView ( { collection : this . slotsList } ) ;
Splitter . slotsRegion . show ( slotsListView ) ;
} ) ;
} ) ;