diff --git a/app/assets/javascripts/facebook_share_widget/application.js.coffee b/app/assets/javascripts/facebook_share_widget/application.js.coffee index 4276650..951b394 100644 --- a/app/assets/javascripts/facebook_share_widget/application.js.coffee +++ b/app/assets/javascripts/facebook_share_widget/application.js.coffee @@ -4,4 +4,13 @@ # It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the # the compiled file. #= require_tree . -# \ No newline at end of file +# + +window.app = + models: {} + views: {} + +window.facebookShareWidget = + callbacks: + success: (friend) -> + fail: (friend) -> \ No newline at end of file diff --git a/app/assets/javascripts/facebook_share_widget/facebook_friend.js.coffee b/app/assets/javascripts/facebook_share_widget/facebook_friend.js.coffee new file mode 100644 index 0000000..f51426a --- /dev/null +++ b/app/assets/javascripts/facebook_share_widget/facebook_friend.js.coffee @@ -0,0 +1,46 @@ +#= require underscore.js +#= require backbone.js +#= require handlebars-runtime.js + +$ -> + class FacebookFriend extends Backbone.Model + url: '/widget/facebook/share' + + isShared: -> + @get('status') == 'shared' + + shared: -> + @set('status': 'shared') + + isSharing: -> + @get('status') == 'sharing' + + startSharing: -> + @set('status': 'sharing') + + shareFailedBecause: (reason)-> + @set('status': 'failed', 'reason': reason) + + isShareFailed: -> + @get('status') == 'failed' + + share: (template) -> + friend = this + data = $.extend({}, $.parseJSON(template)) + data.facebook_id = @id + data.message = @messageModel.content() + @startSharing() + $.ajax + url: @url + type: "post" + data: { post: data } + success: (resp) => + friend.shared() + error: (jqXHR, textStatus, errorThrown) => + json = $.parseJSON(jqXHR.responseText) + friend.shareFailedBecause(json.message) + + setMessageModel: (model) -> + @messageModel = model + + app.models.FacebookFriend = FacebookFriend \ No newline at end of file diff --git a/app/assets/javascripts/facebook_share_widget/facebook_friend_view.js.coffee b/app/assets/javascripts/facebook_share_widget/facebook_friend_view.js.coffee new file mode 100644 index 0000000..6e6d6c4 --- /dev/null +++ b/app/assets/javascripts/facebook_share_widget/facebook_friend_view.js.coffee @@ -0,0 +1,50 @@ +#= require underscore.js +#= require backbone.js +#= require handlebars-runtime.js + +$ -> + class FacebookFriendView extends Backbone.View + tagName: 'li' + events: + 'click.share-button': 'shareToFriend' + + initialize: -> + @model.on('change', @render, this) + + render: -> + friend = @model.toJSON() + $(@el).html(HandlebarsTemplates['facebook_share_widget/templates/friend'](friend)) + if @model.isShared() + @renderShared() + else if @model.isSharing() + @renderSharing() + else if @model.isShareFailed() + @renderFailed(friend) + else + @renderToShare(friend) + this + + renderShared: -> + $(@el).append("
Shared
") + this + + renderSharing: -> + $(@el).append("
Sharing
") + this + + renderToShare: (friend) -> + $(@el).append("Share") + this + + renderFailed: (friend) -> + $(@el).children('.error-message').text(friend.reason) + this + + shareToFriend: (event) -> + event.preventDefault() + @model.share(@sharingTemplate()) + + sharingTemplate: -> + $('.template').text() + + app.views.FacebookFriendView = FacebookFriendView \ No newline at end of file diff --git a/app/assets/javascripts/facebook_share_widget/facebook_share.js.coffee.erb b/app/assets/javascripts/facebook_share_widget/facebook_share.js.coffee.erb index 03e5bb3..f11f104 100644 --- a/app/assets/javascripts/facebook_share_widget/facebook_share.js.coffee.erb +++ b/app/assets/javascripts/facebook_share_widget/facebook_share.js.coffee.erb @@ -1,52 +1,9 @@ #= require underscore.js #= require backbone.js #= require handlebars-runtime.js +#= require facebook_share_widget/sharing_message $ -> - class FacebookFriend extends Backbone.Model - url: '/widget/facebook/share' - - isShared: -> - @get('status') == 'shared' - - shared: -> - @set('status': 'shared') - - isSharing: -> - @get('status') == 'sharing' - - startSharing: -> - @set('status': 'sharing') - - shareFailedBecause: (reason)-> - @set('status': 'failed', 'reason': reason) - - isShareFailed: -> - @get('status') == 'failed' - - share: (template) -> - friend = this - data = $.extend({}, $.parseJSON(template)) - data.facebook_id = @id - data.message = @messageModel.content() - @startSharing() - $.ajax - url: @url - type: "post" - data: { post: data } - success: (resp) => - friend.shared() - error: (jqXHR, textStatus, errorThrown) => - json = $.parseJSON(jqXHR.responseText) - friend.shareFailedBecause(json.message) - - setMessageModel: (model) -> - @messageModel = model - - class SharingMessage extends Backbone.Model - content: -> - @get('msg') - class NameSearch extends Backbone.Model searchFilter: -> @get('filter') @@ -56,7 +13,7 @@ $ -> class FacebookFriends extends Backbone.Collection url: '/widget/facebook/friends' - model: FacebookFriend + model: app.models.FacebookFriend filterBy: (criteria) -> unless criteria @@ -94,53 +51,9 @@ $ -> filterUpdate: -> @model.setSearchFilter($('input.search-text').val()) - class FacebookFriendView extends Backbone.View - tagName: 'li' - events: - 'click.share-button': 'shareToFriend' - - initialize: -> - @model.on('change', @render, this) - - render: -> - friend = @model.toJSON() - $(@el).html(HandlebarsTemplates['facebook_share_widget/templates/friend'](friend)) - if @model.isShared() - @renderShared() - else if @model.isSharing() - @renderSharing() - else if @model.isShareFailed() - @renderFailed(friend) - else - @renderToShare(friend) - this - - renderShared: -> - $(@el).append("
Shared
") - this - - renderSharing: -> - $(@el).append("
Sharing
") - this - - renderToShare: (friend) -> - $(@el).append("Share") - this - - renderFailed: (friend) -> - $(@el).children('.error-message').text(friend.reason) - this - - shareToFriend: (event) -> - event.preventDefault() - @model.share(@sharingTemplate()) - - sharingTemplate: -> - $('.template').text() - class FacebookFriendsView extends Backbone.View initialize: -> - @sharingMessage = new SharingMessage + @sharingMessage = new app.models.SharingMessage @sharingMessageView = new SharingMessageView(el: $('.message-pane'), model: @sharingMessage) @sharingMessageView.render() @@ -166,8 +79,9 @@ $ -> @appendFriend friend for friend in filteredFriends appendFriend: (friend) -> - friendView = new FacebookFriendView(model: friend) + friendView = new app.views.FacebookFriendView(model: friend) @$('#friends').append(friendView.render().el) if $('#container').length - new FacebookFriendsView(el: $('#container')) \ No newline at end of file + new FacebookFriendsView(el: $('#container')) + diff --git a/app/assets/javascripts/facebook_share_widget/facebook_share_widget.js.coffee.erb b/app/assets/javascripts/facebook_share_widget/facebook_share_widget.js.coffee.erb deleted file mode 100644 index a9c6df0..0000000 --- a/app/assets/javascripts/facebook_share_widget/facebook_share_widget.js.coffee.erb +++ /dev/null @@ -1,111 +0,0 @@ -root = exports ? this - -class @FacebookShareWidget - throttleRate: 250 - friends: [] - base_path: "" - template: {} - - constructor: (@container, @options) -> - @base_path = @options.base_path - @template = @options.template if @options.template? - @callbacks = { - success: @options.callbacks && options.callbacks.success ? facebookShareWidget.callbacks.success - fail: @options.callbacks && options.callbacks.fail ? facebookShareWidget.callbacks.fail - } - - $.ajax - url: @base_path + "/facebook/friends" - type: "get" - dataType: "json" - data: $.extend({}, @template) - - success: (resp) => - @friends = resp - $("ul", @container).html(this.friendsToListItems()) - error: (jqXHR, textStatus, errorThrown) -> - json = $.parseJSON(jqXHR.responseText) - console.debug(json.message) - - $('input.search-text', @container).keydown (event) => - this.filter_list() - #throttle(self, this.filter_list, this.throttleRate) - - $("ul", @container).on "click", "a", (event) => - link = $(event.target) - id = link.data("facebook-id").toString() - friend = this.friendWithId(id) - message = $('textarea.message', @container).val() - - errorMessage = link.siblings(".error-message") - indicator = $("\" />").insertBefore(link) - link.remove() - friend.status = "loading" - - data = $.extend({}, @template) - data.facebook_id = id - data.message = message - - $.ajax - url: @base_path + "/facebook/share" - type: "post" - data: { post: data } - success: (resp) => - indicator.attr src: '<%= asset_path("facebook_share_widget/tick.png") %>' - friend.status = "loaded" - @callbacks.success(friend) - error: (jqXHR, textStatus, errorThrown) => - json = $.parseJSON(jqXHR.responseText) - indicator.attr src: '<%= asset_path("facebook_share_widget/cross.png") %>' - errorMessage.text(json.message) - friend.status = null - @callbacks.fail(friend) - - friendWithId: (id) -> - for friend in @friends - return friend if friend.id == id - - friendsToListItems: -> - html = "" - for friend in @friends - html += this.friendToListItem(friend) - html - - friendToListItem: (item) -> - html = "
  • " - html += "" - html += "#{item.name}" - html += "" - if item.status == "loading" - html += "\" />" - else if item.status == "loaded" - html += "
    \" /> Shared
    " - else - html += "Share" - html += '
  • ' - - filter_list: => - text = $('input.search-text', @container).val() - if !!text - regex = new RegExp(text, "i") - html = "" - for friend in @friends - if regex.test(friend.name) - html += this.friendToListItem(friend) - $("ul", @container).html(html) - else - $("ul", @container).html(this.friendsToListItems()) - -# throttle: (context, fn, delay) -> -# alert('foo') -# timer = null -# -> -# args = arguments -# clearTimeout(timer) -# callback = -> fn.apply(context, args) -# timer = setTimeout(callback, delay) - -root.facebookShareWidget = - callbacks: - success: (friend) -> - fail: (friend) -> diff --git a/app/assets/javascripts/facebook_share_widget/sharing_message.js.coffee b/app/assets/javascripts/facebook_share_widget/sharing_message.js.coffee new file mode 100644 index 0000000..8d367d6 --- /dev/null +++ b/app/assets/javascripts/facebook_share_widget/sharing_message.js.coffee @@ -0,0 +1,5 @@ +$ -> + class SharingMessage extends Backbone.Model + content: -> + @get('msg') + app.models.SharingMessage = SharingMessage \ No newline at end of file diff --git a/app/views/facebook_share_widget/facebook/_widget.html.haml b/app/views/facebook_share_widget/facebook/_widget.html.haml index 37ebe5a..fa755a7 100644 --- a/app/views/facebook_share_widget/facebook/_widget.html.haml +++ b/app/views/facebook_share_widget/facebook/_widget.html.haml @@ -5,6 +5,12 @@ .template.hide = options[:template].to_json + .tick.hide + = image_tag "facebook_share_widget/tick.png" + .loader.hide + = image_tag "facebook_share_widget/loader.gif" + .logo.hide + = image_tag "facebook_share_widget/facebook-logo.png" #container = image_tag 'facebook_share_widget/loader.gif' \ No newline at end of file diff --git a/spec/coffeescripts/facebook_share_spec.js.coffee b/spec/coffeescripts/facebook_share_spec.js.coffee index 4a141a4..290cdde 100644 --- a/spec/coffeescripts/facebook_share_spec.js.coffee +++ b/spec/coffeescripts/facebook_share_spec.js.coffee @@ -1,5 +1,30 @@ describe "FacebookFriend ", -> + + beforeEach -> + window.HandlebarsTemplates = {} + window.HandlebarsTemplates['facebook_share_widget/templates/friend'] = (f) -> + setFixtures('
    ') + + it "should share message to friend", -> -# facebookFriend = new FacebookFriend - expect(1).toBe(1) + + spyOn($, "ajax").andCallFake (options)-> + options.success() + facebookFriend = new app.models.FacebookFriend + facebookFriend.setMessageModel(new app.models.SharingMessage( msg: 'msg')) + facebookFriendView = new app.views.FacebookFriendView(model: facebookFriend, el: $('#sandbox')) + facebookFriendView.render() + + $('.share-button').click() + + expect(facebookFriend.isShared()).toBe(true) + + it "should share message with callback", -> + + + + + + + diff --git a/spec/javascripts/helpers/jasmine-jquery.js b/spec/javascripts/helpers/jasmine-jquery.js new file mode 100644 index 0000000..7e85548 --- /dev/null +++ b/spec/javascripts/helpers/jasmine-jquery.js @@ -0,0 +1,288 @@ +var readFixtures = function() { + return jasmine.getFixtures().proxyCallTo_('read', arguments); +}; + +var preloadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('preload', arguments); +}; + +var loadFixtures = function() { + jasmine.getFixtures().proxyCallTo_('load', arguments); +}; + +var setFixtures = function(html) { + jasmine.getFixtures().set(html); +}; + +var sandbox = function(attributes) { + return jasmine.getFixtures().sandbox(attributes); +}; + +var spyOnEvent = function(selector, eventName) { + jasmine.JQuery.events.spyOn(selector, eventName); +} + +jasmine.getFixtures = function() { + return jasmine.currentFixtures_ = jasmine.currentFixtures_ || new jasmine.Fixtures(); +}; + +jasmine.Fixtures = function() { + this.containerId = 'jasmine-fixtures'; + this.fixturesCache_ = {}; + this.fixturesPath = 'spec/javascripts/fixtures'; +}; + +jasmine.Fixtures.prototype.set = function(html) { + this.cleanUp(); + this.createContainer_(html); +}; + +jasmine.Fixtures.prototype.preload = function() { + this.read.apply(this, arguments); +}; + +jasmine.Fixtures.prototype.load = function() { + this.cleanUp(); + this.createContainer_(this.read.apply(this, arguments)); +}; + +jasmine.Fixtures.prototype.read = function() { + var htmlChunks = []; + + var fixtureUrls = arguments; + for(var urlCount = fixtureUrls.length, urlIndex = 0; urlIndex < urlCount; urlIndex++) { + htmlChunks.push(this.getFixtureHtml_(fixtureUrls[urlIndex])); + } + + return htmlChunks.join(''); +}; + +jasmine.Fixtures.prototype.clearCache = function() { + this.fixturesCache_ = {}; +}; + +jasmine.Fixtures.prototype.cleanUp = function() { + jQuery('#' + this.containerId).remove(); +}; + +jasmine.Fixtures.prototype.sandbox = function(attributes) { + var attributesToSet = attributes || {}; + return jQuery('
    ').attr(attributesToSet); +}; + +jasmine.Fixtures.prototype.createContainer_ = function(html) { + var container; + if(html instanceof jQuery) { + container = jQuery('
    '); + container.html(html); + } else { + container = '
    ' + html + '
    ' + } + jQuery('body').append(container); +}; + +jasmine.Fixtures.prototype.getFixtureHtml_ = function(url) { + if (typeof this.fixturesCache_[url] == 'undefined') { + this.loadFixtureIntoCache_(url); + } + return this.fixturesCache_[url]; +}; + +jasmine.Fixtures.prototype.loadFixtureIntoCache_ = function(relativeUrl) { + var self = this; + var url = this.fixturesPath.match('/$') ? this.fixturesPath + relativeUrl : this.fixturesPath + '/' + relativeUrl; + jQuery.ajax({ + async: false, // must be synchronous to guarantee that no tests are run before fixture is loaded + cache: false, + dataType: 'html', + url: url, + success: function(data) { + self.fixturesCache_[relativeUrl] = data; + }, + error: function(jqXHR, status, errorThrown) { + throw Error('Fixture could not be loaded: ' + url + ' (status: ' + status + ', message: ' + errorThrown.message + ')'); + } + }); +}; + +jasmine.Fixtures.prototype.proxyCallTo_ = function(methodName, passedArguments) { + return this[methodName].apply(this, passedArguments); +}; + + +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); + }, + + toBe: 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.getFixtures().cleanUp(); + jasmine.JQuery.events.cleanUp(); +}); diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml index cff2396..f3da064 100644 --- a/spec/javascripts/support/jasmine.yml +++ b/spec/javascripts/support/jasmine.yml @@ -4,7 +4,6 @@ src_files: - jquery.js - underscore.js - backbone.js - - handlebars-runtime.js - compiled/*.js spec_dir: spec/javascripts