diff --git a/spec/atom/file-finder-spec.coffee b/spec/atom/file-finder-spec.coffee new file mode 100644 index 000000000..d83242991 --- /dev/null +++ b/spec/atom/file-finder-spec.coffee @@ -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"] + diff --git a/spec/spec-helper.coffee b/spec/spec-helper.coffee index f9aceeab6..94204773f 100644 --- a/spec/spec-helper.coffee +++ b/spec/spec-helper.coffee @@ -1,3 +1,4 @@ +nakedLoad 'jasmine-jquery' $ = require 'jquery' _ = require 'underscore' Native = require 'native' diff --git a/spec/stdlib/template-spec.coffee b/spec/stdlib/template-spec.coffee index 1a139ffd0..c4d9e4824 100644 --- a/spec/stdlib/template-spec.coffee +++ b/spec/stdlib/template-spec.coffee @@ -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() diff --git a/spec/stdlib/template/builder-spec.coffee b/spec/stdlib/template/builder-spec.coffee new file mode 100644 index 000000000..ea055a5f7 --- /dev/null +++ b/spec/stdlib/template/builder-spec.coffee @@ -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 "
" + + builder.reset() + builder.tag 'ol' + expect(builder.toHtml()).toBe "
    " + + it "can generate tags with content", -> + builder.tag 'ol', -> + builder.tag 'li' + builder.tag 'li' + + expect(builder.toHtml()).toBe "
    " + + it "can generate tags with text", -> + builder.tag 'div', "hello" + expect(builder.toHtml()).toBe "
    hello
    " + + builder.reset() + builder.tag 'div', 22 + expect(builder.toHtml()).toBe "
    22
    " + + 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 '
    ' + diff --git a/src/atom/file-finder.coffee b/src/atom/file-finder.coffee new file mode 100644 index 000000000..e615017e0 --- /dev/null +++ b/src/atom/file-finder.coffee @@ -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 $("
  1. #{url}
  2. ") + + 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 + diff --git a/src/atom/layout.coffee b/src/atom/layout.coffee index b59fa7dd5..2496aa567 100644 --- a/src/atom/layout.coffee +++ b/src/atom/layout.coffee @@ -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' diff --git a/src/stdlib/template.coffee b/src/stdlib/template.coffee index 35f358e53..d4585a360 100644 --- a/src/stdlib/template.coffee +++ b/src/stdlib/template.coffee @@ -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)) diff --git a/src/stdlib/template/builder.coffee b/src/stdlib/template/builder.coffee new file mode 100644 index 000000000..819842bc7 --- /dev/null +++ b/src/stdlib/template/builder.coffee @@ -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 = [] + diff --git a/src/stdlib/template/close-tag.coffee b/src/stdlib/template/close-tag.coffee new file mode 100644 index 000000000..3e4874dfc --- /dev/null +++ b/src/stdlib/template/close-tag.coffee @@ -0,0 +1,7 @@ +module.exports = +class CloseTag + constructor: (@name) -> + + toHtml: -> + "" + diff --git a/src/stdlib/template/open-tag.coffee b/src/stdlib/template/open-tag.coffee new file mode 100644 index 000000000..ec37868aa --- /dev/null +++ b/src/stdlib/template/open-tag.coffee @@ -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 diff --git a/src/stdlib/template/text.coffee b/src/stdlib/template/text.coffee new file mode 100644 index 000000000..5f20e1d2e --- /dev/null +++ b/src/stdlib/template/text.coffee @@ -0,0 +1,6 @@ +module.exports = +class Text + constructor: (@string) -> + + toHtml: -> @string + diff --git a/vendor/jasmine-jquery.js b/vendor/jasmine-jquery.js new file mode 100644 index 000000000..e1cafb895 --- /dev/null +++ b/vendor/jasmine-jquery.js @@ -0,0 +1,177 @@ +jQuery = require('jquery'); +jasmine.JQuery = function() {}; + +jasmine.JQuery.browserTagCaseIndependentHtml = function(html) { + return jQuery('
    ').append(html).html(); +}; + +jasmine.JQuery.elementToString = function(element) { + return jQuery('
    ').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(); +}); + diff --git a/vendor/stringscore.js b/vendor/stringscore.js index a734adc9f..7e758b002 100644 --- a/vendor/stringscore.js +++ b/vendor/stringscore.js @@ -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,