Merge branch 'view'

This commit is contained in:
Nathan Sobo 2011-12-28 14:22:12 -06:00
commit 0fe85355f8
13 changed files with 461 additions and 18 deletions

View File

@ -0,0 +1,32 @@
FileFinder = require 'file-finder'
describe 'FileFinder', ->
finder = null
beforeEach ->
urls = ['app.coffee', 'buffer.coffee', 'atom/app.coffee', 'atom/buffer.coffee']
finder = FileFinder.build {urls}
describe "when characters are typed into the input element", ->
it "displays matching urls in the ol element", ->
expect(finder.urlList.find('li')).not.toExist()
finder.input.text('ap')
finder.input.keypress()
expect(finder.urlList.children().length).toBe 2
expect(finder.urlList.find('li:contains(app.coffee)').length).toBe 2
expect(finder.urlList.find('li:contains(atom/app.coffee)').length).toBe 1
# we should clear the list before re-populating it
finder.input.text('a/ap')
finder.input.keypress()
expect(finder.urlList.children().length).toBe 1
expect(finder.urlList.find('li:contains(atom/app.coffee)').length).toBe 1
describe "findMatches(queryString)", ->
it "returns urls sorted by score of match against the given query", ->
expect(finder.findMatches('ap')).toEqual ["app.coffee", "atom/app.coffee"]
expect(finder.findMatches('a/ap')).toEqual ["atom/app.coffee"]

View File

@ -1,3 +1,4 @@
nakedLoad 'jasmine-jquery'
$ = require 'jquery'
_ = require 'underscore'
Native = require 'native'

View File

@ -3,18 +3,64 @@ Template = require 'template'
describe "Template", ->
describe "toView", ->
Foo = null
view = null
beforeEach ->
class Foo extends Template
content: ->
div ->
h1 @title
content: (attrs) ->
@div =>
@h1 attrs.title
@list()
list: ->
@ol =>
@li outlet: 'li1', click: 'li1Clicked', class: 'foo', "one"
@li outlet: 'li2', keypress:'li2Keypressed', class: 'bar', "two"
viewProperties:
initialize: (attrs) ->
@initializeCalledWith = attrs
foo: "bar",
li1Clicked: ->,
li2Keypressed: ->
view = Foo.build(title: "Zebra")
afterEach ->
delete window.Foo
it "builds a jquery object based on the content method and extends it with the viewProperties", ->
view = Foo.buildView(title: "Hello World")
expect(view.find('h1').text()).toEqual "Hello World"
describe ".build(attributes)", ->
it "generates markup based on the content method", ->
expect(view).toMatchSelector "div"
expect(view.find("h1:contains(Zebra)")).toExist()
expect(view.find("ol > li.foo:contains(one)")).toExist()
expect(view.find("ol > li.bar:contains(two)")).toExist()
it "extends the view with viewProperties, calling the 'constructor' property if present", ->
expect(view.constructor).toBeDefined()
expect(view.foo).toBe("bar")
expect(view.initializeCalledWith).toEqual title: "Zebra"
it "wires references for elements with 'outlet' attributes", ->
expect(view.li1).toMatchSelector "li.foo:contains(one)"
expect(view.li2).toMatchSelector "li.bar:contains(two)"
it "binds events for elements with event name attributes", ->
spyOn(view, 'li1Clicked').andCallFake (event, elt) ->
expect(event.type).toBe 'click'
expect(elt).toMatchSelector 'li.foo:contains(one)'
spyOn(view, 'li2Keypressed').andCallFake (event, elt) ->
expect(event.type).toBe 'keypress'
expect(elt).toMatchSelector "li.bar:contains(two)"
view.li1.click()
expect(view.li1Clicked).toHaveBeenCalled()
expect(view.li2Keypressed).not.toHaveBeenCalled()
view.li1Clicked.reset()
view.li2.keypress()
expect(view.li2Keypressed).toHaveBeenCalled()
expect(view.li1Clicked).not.toHaveBeenCalled()

View File

@ -0,0 +1,41 @@
Builder = require 'template/builder'
describe "Builder", ->
builder = null
beforeEach -> builder = new Builder
describe ".tag(name, args...)", ->
it "can generate simple tags", ->
builder.tag 'div'
expect(builder.toHtml()).toBe "<div></div>"
builder.reset()
builder.tag 'ol'
expect(builder.toHtml()).toBe "<ol></ol>"
it "can generate tags with content", ->
builder.tag 'ol', ->
builder.tag 'li'
builder.tag 'li'
expect(builder.toHtml()).toBe "<ol><li></li><li></li></ol>"
it "can generate tags with text", ->
builder.tag 'div', "hello"
expect(builder.toHtml()).toBe "<div>hello</div>"
builder.reset()
builder.tag 'div', 22
expect(builder.toHtml()).toBe "<div>22</div>"
it "can generate tags with attributes", ->
builder.tag 'div', id: 'foo', class: 'bar'
fragment = builder.toFragment()
expect(fragment.attr('id')).toBe 'foo'
expect(fragment.attr('class')).toBe 'bar'
it "can generate self-closing tags", ->
builder.tag 'br', id: 'foo'
expect(builder.toHtml()).toBe '<br id="foo">'

View File

@ -0,0 +1,26 @@
$ = require 'jquery'
Template = require 'template'
stringScore = require 'stringscore'
module.exports =
class FileFinder extends Template
content: ->
@div class: 'file-finder', =>
@ol outlet: 'urlList'
@input outlet: 'input', keypress: 'populateUrlList'
viewProperties:
urls: null
initialize: ({@urls}) ->
populateUrlList: ->
@urlList.empty()
for url in @findMatches(@input.text())
@urlList.append $("<li>#{url}</li>")
findMatches: (query) ->
scoredUrls = ({url, score: stringScore(url, query)} for url in @urls)
sortedUrls = scoredUrls.sort (a, b) -> a.score > b.score
urlAndScore.url for urlAndScore in sortedUrls when urlAndScore.score > 0

View File

@ -4,13 +4,13 @@ Template = require 'template'
module.exports =
class Layout extends Template
@attach: ->
view = @buildView()
view = @build()
$('body').append(view)
view
content: ->
link rel: 'stylesheet', href: 'static/atom.css'
div id: 'app-horizontal', ->
div id: 'app-vertical', ->
div id: 'main'
@link rel: 'stylesheet', href: 'static/atom.css'
@div id: 'app-horizontal', =>
@div id: 'app-vertical', =>
@div id: 'main'

View File

@ -1,10 +1,43 @@
$ = require 'jquery'
coffeekup = require 'coffeekup'
_ = require 'underscore'
Builder = require 'template/builder'
module.exports =
class Template
@buildView: (attributes) ->
(new this).buildView(attributes)
@events: 'blur change click dblclick error focus keydown
keypress keyup load mousedown mousemove mouseout mouseover
mouseup resize scroll select submit unload'.split /\s+/
@buildTagMethod: (name) ->
this.prototype[name] = (args...) -> @builder.tag(name, args...)
@buildTagMethod(name) for name in Builder.elements.normal
@buildTagMethod(name) for name in Builder.elements.void
@build: (attributes) ->
(new this).build(attributes)
build: (attributes) ->
@builder = new Builder
@content(attributes)
view = @builder.toFragment()
@wireOutlets(view)
@bindEvents(view)
if @viewProperties
$.extend(view, @viewProperties)
view.initialize?(attributes)
view
wireOutlets: (view) ->
view.find('[outlet]').each ->
elt = $(this)
outletName = elt.attr('outlet')
view[outletName] = elt
bindEvents: (view) ->
for eventName in this.constructor.events
view.find("[#{eventName}]").each ->
elt = $(this)
methodName = elt.attr(eventName)
elt[eventName]((event) -> view[methodName](event, elt))
buildView: (attributes) ->
$(coffeekup.render(@content, attributes))

View File

@ -0,0 +1,60 @@
_ = require 'underscore'
$ = require 'jquery'
OpenTag = require 'template/open-tag'
CloseTag = require 'template/close-tag'
Text = require 'template/text'
module.exports =
class Builder
@elements:
normal: 'a abbr address article aside audio b bdi bdo blockquote body button
canvas caption cite code colgroup datalist dd del details dfn div dl dt em
fieldset figcaption figure footer form h1 h2 h3 h4 h5 h6 head header hgroup
html i iframe ins kbd label legend li map mark menu meter nav noscript object
ol optgroup option output p pre progress q rp rt ruby s samp script section
select small span strong style sub summary sup table tbody td textarea tfoot
th thead time title tr u ul video'.split /\s+/
void: 'area base br col command embed hr img input keygen link meta param
source track wbr'.split /\s+/
constructor: ->
@reset()
toHtml: ->
_.map(@document, (x) -> x.toHtml()).join('')
toFragment: ->
$(@toHtml())
tag: (name, args...) ->
options = @extractOptions(args)
@document.push(new OpenTag(name, options.attributes))
if @elementIsVoid(name)
if (options.text? or options.content?)
throw new Error("Self-closing tag #{tag} cannot have text or content")
else
options.content?()
@text(options.text) if options.text
@document.push(new CloseTag(name))
elementIsVoid: (name) ->
_.contains(this.constructor.elements.void, name)
extractOptions: (args) ->
options = {}
for arg in args
options.content = arg if _.isFunction(arg)
options.text = arg if _.isString(arg)
options.text = arg.toString() if _.isNumber(arg)
options.attributes = arg if _.isObject(arg)
options
text: (string) ->
@document.push(new Text(string))
reset: ->
@document = []

View File

@ -0,0 +1,7 @@
module.exports =
class CloseTag
constructor: (@name) ->
toHtml: ->
"</#{@name}>"

View File

@ -0,0 +1,12 @@
_ = require 'underscore'
module.exports =
class OpenTag
constructor: (@name, @attributes) ->
toHtml: ->
"<#{@name}#{@attributesHtml()}>"
attributesHtml: ->
s = _.map(@attributes, (value, key) -> "#{key}=\"#{value}\"").join(' ')
if s == "" then "" else " " + s

View File

@ -0,0 +1,6 @@
module.exports =
class Text
constructor: (@string) ->
toHtml: -> @string

177
vendor/jasmine-jquery.js vendored Normal file
View File

@ -0,0 +1,177 @@
jQuery = require('jquery');
jasmine.JQuery = function() {};
jasmine.JQuery.browserTagCaseIndependentHtml = function(html) {
return jQuery('<div/>').append(html).html();
};
jasmine.JQuery.elementToString = function(element) {
return jQuery('<div />').append(element.clone()).html();
};
jasmine.JQuery.matchersClass = {};
(function(namespace) {
var data = {
spiedEvents: {},
handlers: []
};
namespace.events = {
spyOn: function(selector, eventName) {
var handler = function(e) {
data.spiedEvents[[selector, eventName]] = e;
};
jQuery(selector).bind(eventName, handler);
data.handlers.push(handler);
},
wasTriggered: function(selector, eventName) {
return !!(data.spiedEvents[[selector, eventName]]);
},
cleanUp: function() {
data.spiedEvents = {};
data.handlers = [];
}
}
})(jasmine.JQuery);
(function(){
var jQueryMatchers = {
toHaveClass: function(className) {
return this.actual.hasClass(className);
},
toBeVisible: function() {
return this.actual.is(':visible');
},
toBeHidden: function() {
return this.actual.is(':hidden');
},
toBeSelected: function() {
return this.actual.is(':selected');
},
toBeChecked: function() {
return this.actual.is(':checked');
},
toBeEmpty: function() {
return this.actual.is(':empty');
},
toExist: function() {
return this.actual.size() > 0;
},
toHaveAttr: function(attributeName, expectedAttributeValue) {
return hasProperty(this.actual.attr(attributeName), expectedAttributeValue);
},
toHaveId: function(id) {
return this.actual.attr('id') == id;
},
toHaveHtml: function(html) {
return this.actual.html() == jasmine.JQuery.browserTagCaseIndependentHtml(html);
},
toHaveText: function(text) {
if (text && jQuery.isFunction(text.test)) {
return text.test(this.actual.text());
} else {
return this.actual.text() == text;
}
},
toHaveValue: function(value) {
return this.actual.val() == value;
},
toHaveData: function(key, expectedValue) {
return hasProperty(this.actual.data(key), expectedValue);
},
toMatchSelector: function(selector) {
return this.actual.is(selector);
},
toContain: function(selector) {
return this.actual.find(selector).size() > 0;
},
toBeDisabled: function(selector){
return this.actual.is(':disabled');
},
// tests the existence of a specific event binding
toHandle: function(eventName) {
var events = this.actual.data("events");
return events && events[eventName].length > 0;
},
// tests the existence of a specific event binding + handler
toHandleWith: function(eventName, eventHandler) {
var stack = this.actual.data("events")[eventName];
var i;
for (i = 0; i < stack.length; i++) {
if (stack[i].handler == eventHandler) {
return true;
}
}
return false;
}
};
var hasProperty = function(actualValue, expectedValue) {
if (expectedValue === undefined) {
return actualValue !== undefined;
}
return actualValue == expectedValue;
};
var bindMatcher = function(methodName) {
var builtInMatcher = jasmine.Matchers.prototype[methodName];
jasmine.JQuery.matchersClass[methodName] = function() {
if (this.actual instanceof jQuery) {
var result = jQueryMatchers[methodName].apply(this, arguments);
this.actual = jasmine.JQuery.elementToString(this.actual);
return result;
}
if (builtInMatcher) {
return builtInMatcher.apply(this, arguments);
}
return false;
};
};
for(var methodName in jQueryMatchers) {
bindMatcher(methodName);
}
})();
beforeEach(function() {
this.addMatchers(jasmine.JQuery.matchersClass);
this.addMatchers({
toHaveBeenTriggeredOn: function(selector) {
this.message = function() {
return [
"Expected event " + this.actual + " to have been triggered on" + selector,
"Expected event " + this.actual + " not to have been triggered on" + selector
];
};
return jasmine.JQuery.events.wasTriggered(selector, this.actual);
}
})
});
afterEach(function() {
jasmine.JQuery.events.cleanUp();
});

View File

@ -1,3 +1,5 @@
// MODIFIED BY NS/CJ - Don't extend the prototype of String
/*!
* string_score.js: String Scoring Algorithm 0.1.9
*
@ -16,10 +18,10 @@
* 'Hello World'.score('he'); //=> 0.5931818181818181
* 'Hello World'.score('Hello'); //=> 0.7318181818181818
*/
String.prototype.score = function(abbreviation, fuzziness) {
module.exports = function(string, abbreviation, fuzziness) {
var total_character_score = 0,
abbreviation_length = abbreviation.length,
string = this,
string_length = string.length,
start_of_string_bonus,
abbreviation_score,