Skip to content

Instantly share code, notes, and snippets.

@dtuite
Created January 29, 2013 10:02
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save dtuite/4663167 to your computer and use it in GitHub Desktop.
Save dtuite/4663167 to your computer and use it in GitHub Desktop.
Someone asked for the JS code from http://domainmasher.com so here it is.
// Service for checking the availability of a given word
Splitter.module('Checkers', function(Checkers, Splitter) {
var LocalChecker = {
comKeyFor: function(compound) {
return 'avail/.com/' + compound;
},
check: function(compound) {
return $.jStorage.get(this.comKeyFor(compound));
},
store: function(compound, availability) {
var key = '';
// Don't want to store error states.
if (availability === 'available' || availability === 'unavailable') {
key = this.comKeyFor(compound);
// Expire the value in 10 mins.
return $.jStorage.set(key, availability, { TTL: 600000 })
};
return false;
}
};
// Class which can check the availability of a word.
var Checker = {
check: function(compound, options) {
options || (options = {});
// console.log("Checking availability of", compound);
// No-op if the word is blank.
if ($.trim(compound).length === 0) { return; };
var localAvailability = LocalChecker.check(compound);
if (localAvailability) {
// console.log("Local availability of", compound + ".com", localAvailability);
options.success(localAvailability);
} else {
$.ajax({
url: '/availabilities/' + encodeURIComponent(compound),
dataType: 'json',
success: function(json) {
var availability = json.status;
// console.log("Remote availability of", compound + ".com", availability);
LocalChecker.store(compound, availability);
options.success(availability);
},
error: options.error
});
}
}
};
Checkers.check = function(word, success) {
Checker.check(word, success);
};
// Create a debounced version of the checking method.
Checkers.debouncedCheck = _.debounce(Checkers.check, 300);
});
// 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);
});
});
// This is the top level namespace for the application and some default settings.
window.Splitter = new Backbone.Marionette.Application();
Splitter.addRegions({
slotsRegion: '#slots-region',
wordRegion: '#word-region'
});
// Override Underscore templating style (because of erb).
_.templateSettings = {
interpolate: /\{\{\=(.+?)\}\}/g,
evaluate: /\{\{(.+?)\}\}/g
};
$(function(){
$("body").tooltip({
selector: '[rel=tooltip]',
placement: 'bottom'
});
});
// A model and view for showing the availability of the compound word chosen by the user.
Splitter.module('Word', function(Word, Splitter) {
var WordModel = Backbone.Model.extend({
defaults: {
components: [],
comAvailability: ''
},
compound: function() {
return this.get('components').join('').replace(/ /g, '');
},
updateAvailability: function() {
// No-op if the same word as last time.
if (this.lastCheckedCompound === this.compound()) { return; }
// Set the com availability to waiting because we are
// in the process of fetching new com availability.
this.set({ comAvailability: 'waiting' });
Splitter.Checkers.debouncedCheck(this.compound(), {
success: _.bind(function(availabilty) {
this.lastCheckedCompound = this.compound();
this.set({ comAvailability: availabilty });
}, this),
error: _.bind(function(xhr) {
// console.log("Availabilty checking error", this, arguments);
this.lastCheckedCompound = this.compound();
this.set({ comAvailability: 'invalid' });
}, this)
});
},
isBlank: function() { return this.compound().length === 0; },
isComAvailable: function() {
return this.get('comAvailability') === 'available';
},
isComUnavailable: function() {
return this.get('comAvailability') === 'unavailable';
},
isComUnknown: function() {
return this.get('comAvailability') === 'unknown';
},
changeComponent: function(index, newWord) {
var newComponents = _.clone(this.get('components'));
newComponents[index] = newWord;
// console.log("Setting new components", newComponents, index, newWord);
this.set({ components: newComponents });
this.updateAvailability();
}
})
var WordView = Backbone.Marionette.ItemView.extend({
template: '#word-template',
className: 'word',
initialize: function() {
this.bindTo(this.model, 'change', this.render, this);
},
serializeData: function() {
return {
compound: this.model.compound(),
comAvailability: this.model.get('comAvailability'),
notBlank: !this.model.isBlank(),
isComAvailable: this.model.isComAvailable(),
isComUnavailable: this.model.isComUnavailable(),
isComUnknown: this.model.isComUnknown(),
host: this.model.compound() + '.com',
// Can't figure out how to make templateHelpers work.
visitLink: function() {
var localDetails = {},
templateName = '#visit-link-template',
mkup;
if (this.isComUnavailable) {
localDetails.href = "http://" + this.host;
mkup = $(templateName).html();
return _.template(mkup, _.extend(this, localDetails));
}
return '';
},
purcahseLink: function() {
var localDetails = {},
templateName = '#purchase-link-template',
mkup;
if (this.isComAvailable) {
localDetails.href =
Splitter.DomainRegistrar.affiliateUrlFor(this.host);
localDetails.registrar = Splitter.domainRegistrar;
mkup = $(templateName).html();
return _.template(mkup, _.extend(this, localDetails));
}
return '';
},
unknownLink: function() {
if (this.isComUnknown) {
var templateName = '#unknown-link-template';
return _.template($(templateName).html(), this);
}
return '';
}
};
}
});
Splitter.addInitializer(function(options) {
var wordAttrs = options.word || {},
wordView;
this.word = new WordModel(wordAttrs);
wordView = new WordView({ model: this.word });
this.vent.on('change:activeWord', function(id, word) {
Splitter.word.changeComponent(id, word);
});
this.wordRegion.show(wordView);
});
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment