From c9f87796cc6d9f0a6104b49461872afec62c8814 Mon Sep 17 00:00:00 2001 From: augier Date: Sun, 8 Nov 2015 17:17:26 +0100 Subject: [PATCH 01/10] Using Typeahead for mentionning box --- .../app/views/publisher/mention_view.js | 322 +++++++++++++ .../javascripts/app/views/publisher_view.js | 46 +- .../javascripts/app/views/search_base_view.js | 140 ++++++ .../javascripts/app/views/search_view.js | 96 +--- app/assets/javascripts/main.js | 1 - app/assets/stylesheets/publisher.scss | 4 +- app/assets/stylesheets/typeahead.scss | 3 +- app/views/people/contacts.haml | 6 - app/views/people/show.html.haml | 6 - app/views/status_messages/new.html.haml | 3 +- features/step_definitions/mention_steps.rb | 14 +- .../javascripts/jquery.mentionsInput.js | 443 ------------------ 12 files changed, 510 insertions(+), 574 deletions(-) create mode 100644 app/assets/javascripts/app/views/publisher/mention_view.js create mode 100644 app/assets/javascripts/app/views/search_base_view.js delete mode 100644 lib/assets/javascripts/jquery.mentionsInput.js diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js new file mode 100644 index 000000000..270e14161 --- /dev/null +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -0,0 +1,322 @@ +//= require ../search_base_view + +/* + * This file is based on jQuery.mentionsInput by Kenneth Auchenberg + * licensed under MIT License - http://www.opensource.org/licenses/mit-license.php + * Website: https://podio.github.io/jquery-mentions-input/ + */ + +app.views.PublisherMention = app.views.SearchBase.extend({ + KEYS: { + BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37, UP: 38, + RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35 + }, + + settings: { + triggerChar: "@", + minChars: 2, + templates: { + wrapper: _.template("
"), + mentionsOverlay: _.template("
"), + mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"), + mentionItemHighlight: _.template("<%= name %>") + } + }, + + utils: { + setCaretPosition: function(domNode, caretPos){ + if(domNode.createTextRange){ + var range = domNode.createTextRange(); + range.move("character", caretPos); + range.select(); + } else{ + if(domNode.selectionStart){ + domNode.focus(); + domNode.setSelectionRange(caretPos, caretPos); + } else{ + domNode.focus(); + } + } + }, + + rtrim: function(string){ + return string.replace(/\s+$/, ""); + } + }, + + events: { + "keydown #status_message_fake_text": "onInputBoxKeyDown", + "keypress #status_message_fake_text": "onInputBoxKeyPress", + "input #status_message_fake_text": "onInputBoxInput", + "click #status_message_fake_text": "onInputBoxClick", + "blur #status_message_fake_text": "onInputBoxBlur" + }, + + initialize: function(){ + this.mentionsCollection = []; + this.inputBuffer = []; + this.currentDataQuery = ""; + this.mentionChar = "\u200B"; + + this.elmInputBox = this.$el.find("#status_message_fake_text"); + this.elmInputWrapper = this.elmInputBox.parent(); + this.elmWrapperBox = $(this.settings.templates.wrapper()); + this.elmInputBox.wrapAll(this.elmWrapperBox); + this.elmWrapperBox = this.elmInputWrapper.find("> div").first(); + this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay()); + this.elmMentionsOverlay.prependTo(this.elmWrapperBox); + + var self = this; + this.getSearchInput().on("typeahead:select", function(evt, datum){ + self.processMention(datum); + self.resetMentionBox(); + self.addToFilteredResults(datum); + }); + + this.getSearchInput().on("typeahead:render", function(){ + self.select(self.$(".tt-menu .tt-suggestion").first()); + }); + + this.completeSetup(this.getSearchInput()); + + this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"}); + this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0); + }, + + clearBuffer: function(){ + this.inputBuffer.length = 0; + }, + + updateMentionsCollection: function(){ + var inputText = this.getInputBoxValue(); + + this.mentionsCollection = _.reject(this.mentionsCollection, function(mention){ + return !mention.name || inputText.indexOf(mention.name) === -1; + }); + this.mentionsCollection = _.compact(this.mentionsCollection); + }, + + addMention: function(person){ + if(!person || !person.name || !person.handle){ + return; + } + // This is needed for processing preview + /* jshint camelcase: false */ + person.diaspora_id = person.handle; + /* jshint camelcase: true */ + this.mentionsCollection.push(person); + }, + + processMention: function(mention){ + var currentMessage = this.getInputBoxValue(); + + // Using a regex to figure out positions + var regex = new RegExp("\\" + this.settings.triggerChar + this.currentDataQuery, "gi"); + regex.exec(currentMessage); + + var startCaretPosition = regex.lastIndex - this.currentDataQuery.length - 1; + var currentCaretPosition = regex.lastIndex; + + var start = currentMessage.substr(0, startCaretPosition); + var end = currentMessage.substr(currentCaretPosition, currentMessage.length); + var startEndIndex = (start + mention.name).length + 1; + + this.addMention(mention); + + // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer + this.clearBuffer(); + this.currentDataQuery = ""; + this.resetMentionBox(); + + // Mentions & syntax message + var updatedMessageText = start + this.mentionChar + mention.name + end; + this.elmInputBox.val(updatedMessageText); + this.updateValues(); + + // Set correct focus and selection + this.elmInputBox.focus(); + this.utils.setCaretPosition(this.elmInputBox[0], startEndIndex); + }, + + updateValues: function(){ + var syntaxMessage = this.getInputBoxValue(); + var mentionText = this.getInputBoxValue(); + this.clearFilteredResults(); + + var self = this; + + _.each(this.mentionsCollection, function(mention){ + self.addToFilteredResults(mention); + + var mentionVal = self.mentionChar + mention.name; + + var textSyntax = self.settings.templates.mentionItemSyntax(mention); + syntaxMessage = syntaxMessage.replace(mentionVal, textSyntax); + + var textHighlight = self.settings.templates.mentionItemHighlight({ name: _.escape(mention.name) }); + mentionText = mentionText.replace(mentionVal, textHighlight); + }); + + mentionText = mentionText.replace(/\n/g, "
"); + mentionText = mentionText.replace(/ {2}/g, "  "); + + this.elmInputBox.data("messageText", syntaxMessage); + this.elmMentionsOverlay.find("div > div").html(mentionText); + }, + + /** + * Let us prefill the publisher with a mention list + * @param persons List of people to mention in a post; + * JSON object of form { handle: , name: , ... } + */ + prefillMention: function(persons){ + var self = this; + _.each(persons, function(person){ + self.addMention(person); + self.addToFilteredResults(person); + self.elmInputBox.val(self.mentionChar + person.name); + self.updateValues(); + }); + }, + + selectNextResult: function(evt){ + if(this.isVisible()){ + evt.preventDefault(); + evt.stopPropagation(); + } + + if(this.getSelected().size() !== 1 || this.getSelected().next().size() !== 1){ + this.getSelected().removeClass("tt-cursor"); + this.$el.find(".tt-suggestion").first().addClass("tt-cursor"); + } + else{ + this.getSelected().removeClass("tt-cursor").next().addClass("tt-cursor"); + } + }, + + selectPreviousResult: function(evt){ + if(this.isVisible()){ + evt.preventDefault(); + evt.stopPropagation(); + } + + if(this.getSelected().size() !== 1 || this.getSelected().prev().size() !== 1){ + this.getSelected().removeClass("tt-cursor"); + this.$el.find(".tt-suggestion").last().addClass("tt-cursor"); + } + else{ + this.getSelected().removeClass("tt-cursor").prev().addClass("tt-cursor"); + } + }, + + onInputBoxKeyPress: function(e){ + if(e.keyCode !== this.KEYS.BACKSPACE){ + var typedValue = String.fromCharCode(e.which || e.keyCode); + this.inputBuffer.push(typedValue); + } + }, + + onInputBoxInput: function(){ + this.updateValues(); + this.updateMentionsCollection(); + + var triggerCharIndex = _.lastIndexOf(this.inputBuffer, this.settings.triggerChar); + if(triggerCharIndex > -1){ + this.currentDataQuery = this.inputBuffer.slice(triggerCharIndex + 1).join(""); + this.currentDataQuery = this.utils.rtrim(this.currentDataQuery); + + this.showMentionBox(); + } + }, + + onInputBoxKeyDown: function(e){ + // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT + if(e.keyCode === this.KEYS.LEFT || e.keyCode === this.KEYS.RIGHT || + e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){ + _.defer(this.clearBuffer); + + // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting + // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack + // to force updateValues() to fire when backspace/delete is pressed in IE9. + if(navigator.userAgent.indexOf("MSIE 9") > -1){ + _.defer(this.updateValues); + } + + return; + } + + if(e.keyCode === this.KEYS.BACKSPACE){ + this.inputBuffer = this.inputBuffer.slice(0, this.inputBuffer.length - 1); + return; + } + + if(!this.isVisible){ + return true; + } + + switch(e.keyCode){ + case this.KEYS.ESC: + case this.KEYS.SPACE: + this.resetMentionBox(); + break; + case this.KEYS.UP: + this.selectPreviousResult(e); + break; + case this.KEYS.DOWN: + this.selectNextResult(e); + break; + case this.KEYS.RETURN: + case this.KEYS.TAB: + if(this.getSelected().size() === 1){ + this.getSelected().click(); + return false; + } + break; + } + return true; + }, + + onInputBoxClick: function(){ + this.resetMentionBox(); + }, + + onInputBoxBlur: function(){ + this.resetMentionBox(); + }, + + reset: function(){ + this.elmInputBox.val(""); + this.mentionsCollection.length = 0; + this.clearFilteredResults(); + this.updateValues(); + }, + + showMentionBox: function(){ + this.getSearchInput().typeahead("val", this.currentDataQuery); + this.getSearchInput().typeahead("open"); + }, + + resetMentionBox: function(){ + this.getSearchInput().typeahead("val", ""); + this.getSearchInput().typeahead("close"); + }, + + getInputBoxValue: function(){ + return $.trim(this.elmInputBox.val()); + }, + + isVisible: function(){ + return this.$el.find(".tt-menu").is(":visible"); + }, + + getSearchInput: function(){ + if(this.$el.find(".typeahead-mention-box").length === 0){ + this.elmInputBox.after(""); + } + return this.$el.find(".typeahead-mention-box"); + }, + + getTextForSubmit: function(){ + return this.mentionsCollection.length ? this.elmInputBox.data("messageText") : this.getInputBoxValue(); + } +}); diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index 89e5e38f5..fd7d85a5e 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -31,6 +31,7 @@ app.views.Publisher = Backbone.View.extend({ initialize : function(opts){ this.standalone = opts ? opts.standalone : false; + this.prefillMention = opts && opts.prefillMention ? opts.prefillMention : undefined; this.disabled = false; // init shortcut references to the various elements @@ -41,9 +42,6 @@ app.views.Publisher = Backbone.View.extend({ this.previewEl = this.$("button.post_preview_button"); this.photozoneEl = this.$("#photodropzone"); - // init mentions plugin - Mentions.initialize(this.inputEl); - // if there is data in the publisher we ask for a confirmation // before the user is able to leave the page $(window).on("beforeunload", _.bind(this._beforeUnload, this)); @@ -100,6 +98,11 @@ app.views.Publisher = Backbone.View.extend({ }, initSubviews: function() { + this.mention = new app.views.PublisherMention({ el: this.$("#publisher_textarea_wrapper") }); + if(this.prefillMention){ + this.mention.prefillMention([this.prefillMention]); + } + var form = this.$(".content_creation form"); this.view_services = new app.views.PublisherServices({ @@ -265,32 +268,6 @@ app.views.Publisher = Backbone.View.extend({ return photos; }, - getMentionedPeople: function(serializedForm) { - var mentionedPeople = [], - regexp = /@{([^;]+); ([^}]+)}/g, - user; - var getMentionedUser = function(handle) { - return Mentions.contacts.filter(function(user) { - return user.handle === handle; - })[0]; - }; - - while( (user = regexp.exec(serializedForm["status_message[text]"])) ) { - // user[1]: name, user[2]: handle - var mentionedUser = getMentionedUser(user[2]); - if(mentionedUser){ - mentionedPeople.push({ - "id": mentionedUser.id, - "guid": mentionedUser.guid, - "name": user[1], - "diaspora_id": user[2], - "avatar": mentionedUser.avatar - }); - } - } - return mentionedPeople; - }, - getPollData: function(serializedForm) { var poll; var pollQuestion = serializedForm.poll_question; @@ -321,7 +298,7 @@ app.views.Publisher = Backbone.View.extend({ var serializedForm = $(evt.target).closest("form").serializeObject(); var photos = this.getUploadedPhotos(); - var mentionedPeople = this.getMentionedPeople(serializedForm); + var mentionedPeople = this.mention.mentionsCollection; var date = (new Date()).toISOString(); var poll = this.getPollData(serializedForm); var locationCoords = serializedForm["location[coords]"]; @@ -395,7 +372,7 @@ app.views.Publisher = Backbone.View.extend({ autosize.update(this.inputEl); // remove mentions - this.inputEl.mentionsInput("reset"); + this.mention.reset(); // remove photos this.photozoneEl.find("li").remove(); @@ -450,9 +427,6 @@ app.views.Publisher = Backbone.View.extend({ this.$el.removeClass("closed"); this.wrapperEl.addClass("active"); autosize.update(this.inputEl); - - // fetch contacts for mentioning - Mentions.fetchContacts(); return this; }, @@ -521,9 +495,7 @@ app.views.Publisher = Backbone.View.extend({ var self = this; this.checkSubmitAvailability(); - this.inputEl.mentionsInput("val", function(value){ - self.hiddenInputEl.val(value); - }); + this.hiddenInputEl.val(this.mention.getTextForSubmit()); }, _beforeUnload: function(e) { diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js new file mode 100644 index 000000000..af7914122 --- /dev/null +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -0,0 +1,140 @@ +app.views.SearchBase = app.views.Base.extend({ + completeSetup: function(typeaheadElement){ + this.typeaheadElement = $(typeaheadElement); + this.setupBloodhound(); + this.setupTypeahead(); + this.bindSelectionEvents(); + this.resultsTofilter = []; + }, + + setupBloodhound: function() { + var self = this; + var bloodhoundConf = { + datumTokenizer: function(datum) { + var nameTokens = Bloodhound.tokenizers.nonword(datum.name); + var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : []; + return nameTokens.concat(handleTokens); + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + prefetch: { + url: "/contacts.json", + transform: this.transformBloodhoundResponse, + cache: false + }, + sufficient: 5 + }; + + // The publisher does not define an additionnal source for searchin + // This prevents tests from failing when this additionnal source isn't set + if(this.searchFormAction !== undefined){ + bloodhoundConf.remote = { + url: this.searchFormAction + ".json?q=%QUERY", + wildcard: "%QUERY", + transform: this.transformBloodhoundResponse + }; + } + + this.bloodhound = new Bloodhound(bloodhoundConf); + + /** + * Custom searching function that let us filter contacts from prefetched Bloodhound results. + */ + this.bloodhound.customSearch = function(query, sync, async){ + var filterResults = function(datums){ + return _.filter(datums, function(result){ + if(result.handle){ + return !_.contains(self.resultsTofilter, result.handle); + } + }); + }; + + var _sync = function(datums){ + var results = filterResults(datums); + sync(results); + }; + + self.bloodhound.search(query, _sync, async); + }; + }, + + setupTypeahead: function() { + this.typeaheadElement.typeahead({ + hint: false, + highlight: true, + minLength: 2 + }, + { + name: "search", + display: "name", + limit: 5, + source: this.bloodhound.customSearch, + templates: { + /* jshint camelcase: false */ + suggestion: HandlebarsTemplates.search_suggestion_tpl + /* jshint camelcase: true */ + } + }); + }, + + transformBloodhoundResponse: function(response) { + return response.map(function(data){ + // person + if(data.handle){ + data.person = true; + return data; + } + + // hashtag + return { + hashtag: true, + name: data.name, + url: Routes.tag(data.name.substring(1)) + }; + }); + }, + + /** + * This bind events to highlight a result when overing it + */ + bindSelectionEvents: function(){ + var self = this; + var onover = function(evt){ + var isSuggestion = $(evt.target).is(".tt-suggestion"); + var suggestion = isSuggestion ? $(evt.target) : $(evt.target).parent(".tt-suggestion"); + if(suggestion){ + self.select(suggestion); + } + }; + + this.typeaheadElement.on("typeahead:render", function(){ + self.$(".tt-menu *").off("mouseover", onover); + self.$(".tt-menu .tt-suggestion").on("mouseover", onover); + self.$(".tt-menu .tt-suggestion *").on("mouseover", onover); + }); + }, + + /** + * This function lets us filter contacts from Bloodhound's responses + * It is used by app.views.PublisherMention to filter already mentionned + * people in post. Does not filter tags from results. + * @param person a JSON object of form { handle: , ... } representing the filtered contact + */ + addToFilteredResults: function(person){ + if(person.handle){ + this.resultsTofilter.push(person.handle); + } + }, + + clearFilteredResults: function(){ + this.resultsTofilter.length = 0; + }, + + getSelected: function(){ + return this.$el.find(".tt-cursor"); + }, + + select: function(el){ + this.getSelected().removeClass("tt-cursor"); + $(el).addClass("tt-cursor"); + } +}); diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index 6e8a1d272..65ed775c0 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -1,96 +1,52 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later -app.views.Search = app.views.Base.extend({ +app.views.Search = app.views.SearchBase.extend({ events: { "focusin #q": "toggleSearchActive", "focusout #q": "toggleSearchActive", - "keypress #q": "inputKeypress", + "keypress #q": "inputKeypress" }, initialize: function(){ this.searchFormAction = this.$el.attr("action"); - this.searchInput = this.$("#q"); - - // constructs the suggestion engine - this.setupBloodhound(); - this.setupTypeahead(); - this.searchInput.on("typeahead:select", this.suggestionSelected); + this.completeSetup(this.getTypeaheadElement()); + this.bindMoreSelectionEvents(); + this.getTypeaheadElement().on("typeahead:select", this.suggestionSelected); }, - setupBloodhound: function() { - this.bloodhound = new Bloodhound({ - datumTokenizer: function(datum) { - var nameTokens = Bloodhound.tokenizers.nonword(datum.name); - var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : []; - return nameTokens.concat(handleTokens); - }, - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: this.searchFormAction + ".json?q=%QUERY", - wildcard: "%QUERY", - transform: this.transformBloodhoundResponse - }, - prefetch: { - url: "/contacts.json", - transform: this.transformBloodhoundResponse, - cache: false - }, - sufficient: 5 + /** + * This bind events to unselect all results when leaving the menu + */ + bindMoreSelectionEvents: function(){ + var self = this; + var onleave = function(){ + self.$(".tt-cursor").removeClass("tt-cursor"); + }; + + this.getTypeaheadElement().on("typeahead:render", function(){ + self.$(".tt-menu").off("mouseleave", onleave); + self.$(".tt-menu").on("mouseleave", onleave); }); }, - setupTypeahead: function() { - this.searchInput.typeahead({ - hint: false, - highlight: true, - minLength: 2 - }, - { - name: "search", - display: "name", - limit: 5, - source: this.bloodhound, - templates: { - /* jshint camelcase: false */ - suggestion: HandlebarsTemplates.search_suggestion_tpl - /* jshint camelcase: true */ - } - }); + getTypeaheadElement: function(){ + return this.$("#q"); }, - transformBloodhoundResponse: function(response) { - var result = response.map(function(data) { - // person - if(data.handle) { - data.person = true; - return data; - } - - // hashtag - return { - hashtag: true, - name: data.name, - url: Routes.tag(data.name.substring(1)) - }; - }); - - return result; - }, - - toggleSearchActive: function(evt) { + toggleSearchActive: function(evt){ // jQuery produces two events for focus/blur (for bubbling) // don't rely on which event arrives first, by allowing for both variants var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1); $(evt.target).toggleClass("active", isActive); }, - suggestionSelected: function(evt, datum) { - window.location = datum.url; - }, - - inputKeypress: function(evt) { - if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0) { + inputKeypress: function(evt){ + if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0){ $(evt.target).closest("form").submit(); } + }, + + suggestionSelected: function(evt, datum){ + window.location = datum.url; } }); // @license-ends diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 78da46496..458824bd1 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -14,7 +14,6 @@ //= require rails-timeago //= require jquery.events.input //= require jakobmattsson-jquery-elastic -//= require jquery.mentionsInput //= require jquery.infinitescroll-custom //= require jquery-ui/core //= require jquery-ui/widget diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss index 32e1e49e3..9d6b2cd3f 100644 --- a/app/assets/stylesheets/publisher.scss +++ b/app/assets/stylesheets/publisher.scss @@ -83,7 +83,7 @@ } &.active textarea { - min-height: 70px; + min-height: 90px; } .markdownIndications { @@ -118,6 +118,8 @@ } } + &:not(.with_location) #location_container { display: none; } + &.with_location .loader { height: 20px; width: 20px; diff --git a/app/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss index 3a46e14b6..ed4405c01 100644 --- a/app/assets/stylesheets/typeahead.scss +++ b/app/assets/stylesheets/typeahead.scss @@ -12,10 +12,9 @@ line-height: 20px; &.tt-cursor { background-color: $brand-primary; + border-top: 1px solid $brand-primary; } - &:hover { background-color: lighten($navbar-inverse-bg, 10%); } - &.search-suggestion-person { padding: 8px; .avatar { diff --git a/app/views/people/contacts.haml b/app/views/people/contacts.haml index b1e993f79..b5edec9b0 100644 --- a/app/views/people/contacts.haml +++ b/app/views/people/contacts.haml @@ -1,9 +1,3 @@ --# TODO this should happen in the js app -- content_for :head do - - if user_signed_in? && @person != current_user.person - :javascript - Mentions.options.prefillMention = Mentions._contactToMention(#{j @person.to_json}); - - content_for :page_title do = @person.name diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index bfb10872c..0deab6f09 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -2,12 +2,6 @@ -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. --# TODO this should happen in the js app -- content_for :head do - - if user_signed_in? && @person != current_user.person - :javascript - Mentions.options.prefillMention = Mentions._contactToMention(#{j @person.to_json}); - - content_for :page_title do = @person.name diff --git a/app/views/status_messages/new.html.haml b/app/views/status_messages/new.html.haml index 88e150b9f..f1d7f9a8a 100644 --- a/app/views/status_messages/new.html.haml +++ b/app/views/status_messages/new.html.haml @@ -7,7 +7,8 @@ :javascript $(function() { app.publisher = new app.views.Publisher({ - standalone: true + standalone: true, + prefillMention: #{json_escape @person.to_json} }); app.publisher.open(); $("#publisher").bind('ajax:success', function(){ diff --git a/features/step_definitions/mention_steps.rb b/features/step_definitions/mention_steps.rb index f7939afbe..a36557563 100644 --- a/features/step_definitions/mention_steps.rb +++ b/features/step_definitions/mention_steps.rb @@ -1,24 +1,24 @@ And /^Alice has a post mentioning Bob$/ do - alice = User.find_by_email 'alice@alice.alice' - bob = User.find_by_email 'bob@bob.bob' + alice = User.find_by_email "alice@alice.alice" + bob = User.find_by_email "bob@bob.bob" aspect = alice.aspects.where(:name => "Besties").first alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect) end And /^Alice has (\d+) posts mentioning Bob$/ do |n| n.to_i.times do - alice = User.find_by_email 'alice@alice.alice' - bob = User.find_by_email 'bob@bob.bob' + alice = User.find_by_email "alice@alice.alice" + bob = User.find_by_email "bob@bob.bob" aspect = alice.aspects.where(:name => "Besties").first alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect) end end And /^I mention Alice in the publisher$/ do - alice = User.find_by_email 'alice@alice.alice' - write_in_publisher("@{Alice Smith ; #{alice.person.diaspora_handle}}") + write_in_publisher("@alice") + step %(I click on the first user in the mentions dropdown list) end And /^I click on the first user in the mentions dropdown list$/ do - find('.mentions-autocomplete-list li', match: :first).click + find(".tt-menu .tt-suggestion", match: :first).click end diff --git a/lib/assets/javascripts/jquery.mentionsInput.js b/lib/assets/javascripts/jquery.mentionsInput.js deleted file mode 100644 index 7693caaee..000000000 --- a/lib/assets/javascripts/jquery.mentionsInput.js +++ /dev/null @@ -1,443 +0,0 @@ -// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat -/* - * Mentions Input - * Version 1.0.2 - * Written by: Kenneth Auchenberg (Podio) - * - * Using underscore.js - * - * License: MIT License - http://www.opensource.org/licenses/mit-license.php - * - * Modifications for Diaspora: - * - * Prevent replacing the wrong text by marking the replacement position with a special character - * Don't add a space after inserting a mention - * Only use the first div as a wrapperBox - * Binded paste event on input box to trigger contacts search for autocompletion while adding mention via clipboard - */ - -(function ($, _, undefined) { - - // Settings - var KEY = { PASTE : 118, BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39, - DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35 }; // Keys "enum" - var defaultSettings = { - triggerChar : '@', - onDataRequest : $.noop, - minChars : 2, - showAvatars : true, - elastic : true, - classes : { - autoCompleteItemActive : "active" - }, - templates : { - wrapper : _.template('
'), - autocompleteList : _.template('
'), - autocompleteListItem : _.template('
  • <%= content %>
  • '), - autocompleteListItemAvatar : _.template(''), - autocompleteListItemIcon : _.template('
    '), - mentionsOverlay : _.template('
    '), - mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'), - mentionItemHighlight : _.template('<%= value %>') - } - }; - - var utils = { - htmlEncode : function (str) { - return _.escape(str); - }, - highlightTerm : function (value, term) { - if (!term && !term.length) { - return value; - } - return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1"); - }, - setCaratPosition : function (domNode, caretPos) { - if (domNode.createTextRange) { - var range = domNode.createTextRange(); - range.move('character', caretPos); - range.select(); - } else { - if (domNode.selectionStart) { - domNode.focus(); - domNode.setSelectionRange(caretPos, caretPos); - } else { - domNode.focus(); - } - } - }, - rtrim: function(string) { - return string.replace(/\s+$/,""); - } - }; - - var MentionsInput = function (settings) { - - var domInput, elmInputBox, elmInputWrapper, elmAutocompleteList, elmWrapperBox, elmMentionsOverlay, elmActiveAutoCompleteItem; - var mentionsCollection = []; - var autocompleteItemCollection = {}; - var inputBuffer = []; - var currentDataQuery = ''; - var mentionChar = "\u200B"; // zero width space - - settings = $.extend(true, {}, defaultSettings, settings ); - - function initTextarea() { - elmInputBox = $(domInput); - - if (elmInputBox.attr('data-mentions-input') == 'true') { - return; - } - - elmInputWrapper = elmInputBox.parent(); - elmWrapperBox = $(settings.templates.wrapper()); - elmInputBox.wrapAll(elmWrapperBox); - elmWrapperBox = elmInputWrapper.find('> div').first(); - - elmInputBox.attr('data-mentions-input', 'true'); - elmInputBox.bind('keydown', onInputBoxKeyDown); - elmInputBox.bind('keypress', onInputBoxKeyPress); - elmInputBox.bind('paste',onInputBoxPaste); - elmInputBox.bind('input', onInputBoxInput); - elmInputBox.bind('click', onInputBoxClick); - elmInputBox.bind('blur', onInputBoxBlur); - - // Elastic textareas, internal setting for the Dispora guys - if( settings.elastic ) { - elmInputBox.elastic(); - } - - } - - function initAutocomplete() { - elmAutocompleteList = $(settings.templates.autocompleteList()); - elmAutocompleteList.appendTo(elmWrapperBox); - elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick); - } - - function initMentionsOverlay() { - elmMentionsOverlay = $(settings.templates.mentionsOverlay()); - elmMentionsOverlay.prependTo(elmWrapperBox); - } - - function updateValues() { - var syntaxMessage = getInputBoxValue(); - - _.each(mentionsCollection, function (mention) { - var textSyntax = settings.templates.mentionItemSyntax(mention); - syntaxMessage = syntaxMessage.replace(mentionChar + mention.value, textSyntax); - }); - - var mentionText = utils.htmlEncode(syntaxMessage); - - _.each(mentionsCollection, function (mention) { - var formattedMention = _.extend({}, mention, {value: mentionChar + utils.htmlEncode(mention.value)}); - var textSyntax = settings.templates.mentionItemSyntax(formattedMention); - var textHighlight = settings.templates.mentionItemHighlight(formattedMention); - - mentionText = mentionText.replace(textSyntax, textHighlight); - }); - - mentionText = mentionText.replace(/\n/g, '
    '); - mentionText = mentionText.replace(/ {2}/g, '  '); - - elmInputBox.data('messageText', syntaxMessage); - elmMentionsOverlay.find('div > div').html(mentionText); - } - - function resetBuffer() { - inputBuffer = []; - } - - function updateMentionsCollection() { - var inputText = getInputBoxValue(); - - mentionsCollection = _.reject(mentionsCollection, function (mention, index) { - return !mention.value || inputText.indexOf(mention.value) == -1; - }); - mentionsCollection = _.compact(mentionsCollection); - } - - function addMention(mention) { - - var currentMessage = getInputBoxValue(); - - // Using a regex to figure out positions - var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi"); - regex.exec(currentMessage); - - var startCaretPosition = regex.lastIndex - currentDataQuery.length - 1; - var currentCaretPosition = regex.lastIndex; - - var start = currentMessage.substr(0, startCaretPosition); - var end = currentMessage.substr(currentCaretPosition, currentMessage.length); - var startEndIndex = (start + mention.value).length + 1; - - mentionsCollection.push(mention); - - // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer - resetBuffer(); - currentDataQuery = ''; - hideAutoComplete(); - - // Mentions & syntax message - var updatedMessageText = start + mentionChar + mention.value + end; - elmInputBox.val(updatedMessageText); - updateValues(); - - // Set correct focus and selection - elmInputBox.focus(); - utils.setCaratPosition(elmInputBox[0], startEndIndex); - } - - function getInputBoxValue() { - return $.trim(elmInputBox.val()); - } - - function onAutoCompleteItemClick(e) { - var elmTarget = $(this); - var mention = autocompleteItemCollection[elmTarget.attr('data-uid')]; - - addMention(mention); - - return false; - } - - function onInputBoxClick(e) { - resetBuffer(); - } - - function onInputBoxBlur(e) { - hideAutoComplete(); - } - - function onInputBoxPaste(e) { - pastedData = e.originalEvent.clipboardData.getData("text/plain"); - dataArray = pastedData.split(""); - _.each(dataArray, function(value) { - inputBuffer.push(value); - }); - } - function onInputBoxInput(e) { - updateValues(); - updateMentionsCollection(); - hideAutoComplete(); - - var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar); - if (triggerCharIndex > -1) { - currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join(''); - currentDataQuery = utils.rtrim(currentDataQuery); - - _.defer(_.bind(doSearch, this, currentDataQuery)); - } - } - - function onInputBoxKeyPress(e) { - // Excluding ctrl+v from key press event in firefox - if (!((e.which === KEY.PASTE && e.ctrlKey) || (e.keyCode === KEY.BACKSPACE))) { - var typedValue = String.fromCharCode(e.which || e.keyCode); - inputBuffer.push(typedValue); - } - } - - function onInputBoxKeyDown(e) { - - // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT - if (e.keyCode == KEY.LEFT || e.keyCode == KEY.RIGHT || e.keyCode == KEY.HOME || e.keyCode == KEY.END) { - // Defer execution to ensure carat pos has changed after HOME/END keys - _.defer(resetBuffer); - - // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting - // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack - // to force updateValues() to fire when backspace/delete is pressed in IE9. - if (navigator.userAgent.indexOf("MSIE 9") > -1) { - _.defer(updateValues); - } - - return; - } - - if (e.keyCode == KEY.BACKSPACE) { - inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE - return; - } - - if (!elmAutocompleteList.is(':visible')) { - return true; - } - - switch (e.keyCode) { - case KEY.UP: - case KEY.DOWN: - var elmCurrentAutoCompleteItem = null; - if (e.keyCode == KEY.DOWN) { - if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { - elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next(); - } else { - elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first(); - } - } else { - elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev(); - } - - if (elmCurrentAutoCompleteItem.length) { - selectAutoCompleteItem(elmCurrentAutoCompleteItem); - } - - return false; - - case KEY.RETURN: - case KEY.TAB: - if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) { - elmActiveAutoCompleteItem.trigger('mousedown'); - return false; - } - - break; - } - - return true; - } - - function hideAutoComplete() { - elmActiveAutoCompleteItem = null; - elmAutocompleteList.empty().hide(); - } - - function selectAutoCompleteItem(elmItem) { - elmItem.addClass(settings.classes.autoCompleteItemActive); - elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive); - - elmActiveAutoCompleteItem = elmItem; - } - - function populateDropdown(query, results) { - elmAutocompleteList.show(); - - // Filter items that has already been mentioned - var mentionValues = _.pluck(mentionsCollection, 'value'); - results = _.reject(results, function (item) { - return _.include(mentionValues, item.name); - }); - - if (!results.length) { - hideAutoComplete(); - return; - } - - elmAutocompleteList.empty(); - var elmDropDownList = $("
      ").appendTo(elmAutocompleteList).hide(); - - _.each(results, function (item, index) { - var itemUid = _.uniqueId('mention_'); - - autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name}); - - var elmListItem = $(settings.templates.autocompleteListItem({ - 'id' : utils.htmlEncode(item.id), - 'display' : utils.htmlEncode(item.name), - 'type' : utils.htmlEncode(item.type), - 'content' : utils.highlightTerm(utils.htmlEncode((item.name)), query) - })).attr('data-uid', itemUid); - - if (index === 0) { - selectAutoCompleteItem(elmListItem); - } - - if (settings.showAvatars) { - var elmIcon; - - if (item.avatar) { - elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar })); - } else { - elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon })); - } - elmIcon.prependTo(elmListItem); - } - elmListItem = elmListItem.appendTo(elmDropDownList); - }); - - elmAutocompleteList.show(); - elmDropDownList.show(); - } - - function doSearch(query) { - if (query && query.length && query.length >= settings.minChars) { - settings.onDataRequest.call(this, 'search', query, function (responseData) { - populateDropdown(query, responseData); - }); - } - } - - function resetInput() { - elmInputBox.val(''); - mentionsCollection = []; - updateValues(); - } - - // Public methods - return { - init : function (domTarget) { - - domInput = domTarget; - - initTextarea(); - initAutocomplete(); - initMentionsOverlay(); - resetInput(); - - if( settings.prefillMention ) { - addMention( settings.prefillMention ); - } - - }, - - val : function (callback) { - if (!_.isFunction(callback)) { - return; - } - - var value = mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue(); - callback.call(this, value); - }, - - reset : function () { - resetInput(); - }, - - getMentions : function (callback) { - if (!_.isFunction(callback)) { - return; - } - - callback.call(this, mentionsCollection); - } - }; - }; - - $.fn.mentionsInput = function (method, settings) { - - var outerArguments = arguments; - - if (typeof method === 'object' || !method) { - settings = method; - } - - return this.each(function () { - var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings)); - - if (_.isFunction(instance[method])) { - return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1)); - - } else if (typeof method === 'object' || !method) { - return instance.init.call(this, this); - - } else { - $.error('Method ' + method + ' does not exist'); - } - - }); - }; - -})(jQuery, _); -// @license-end From f1e8c57c260408e77836c431c6ad0faab1c62901 Mon Sep 17 00:00:00 2001 From: augier Date: Fri, 20 Nov 2015 14:26:57 +0100 Subject: [PATCH 02/10] CSS guidestyle --- app/assets/javascripts/app/views/publisher_view.js | 6 +++--- app/assets/stylesheets/publisher.scss | 12 ++++++------ app/views/publisher/_publisher.html.haml | 2 +- spec/javascripts/app/views/publisher_view_spec.js | 2 +- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index fd7d85a5e..05081d0c3 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -225,8 +225,8 @@ app.views.Publisher = Backbone.View.extend({ // creates the location showLocation: function(){ if($("#location").length === 0){ - $("#location_container").append("
      "); - this.wrapperEl.addClass("with_location"); + this.$(".location-container").append("
      "); + this.wrapperEl.addClass("with-location"); this.view_locator = new app.views.Location(); } }, @@ -235,7 +235,7 @@ app.views.Publisher = Backbone.View.extend({ destroyLocation: function(){ if(this.view_locator){ this.view_locator.remove(); - this.wrapperEl.removeClass("with_location"); + this.wrapperEl.removeClass("with-location"); delete this.view_locator; } }, diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss index 9d6b2cd3f..1a2ddaec1 100644 --- a/app/assets/stylesheets/publisher.scss +++ b/app/assets/stylesheets/publisher.scss @@ -5,7 +5,7 @@ &.closed { #button_container, - #location_container, + .location-container, #hide_publisher, #photodropzone_container, .counter, @@ -118,13 +118,13 @@ } } - &:not(.with_location) #location_container { display: none; } + &:not(.with-location) .location-container { display: none; } - &.with_location .loader { + &.with-location .loader { height: 20px; width: 20px; } - &.with_location #location_container { + &.with-location .location-container { height: 30px; margin-bottom: 0; border-top: 1px dashed $border-grey; @@ -230,7 +230,7 @@ display: none; } } - &.with_location #publisher-images { + &.with-location #publisher-images { #hide_location { display: inline-block; } #locator { display: none; } } @@ -242,7 +242,7 @@ right: 10px; bottom: -25px; } - &.with_location .counter { + &.with-location .counter { bottom: -62px; } .warning { diff --git a/app/views/publisher/_publisher.html.haml b/app/views/publisher/_publisher.html.haml index c6ed2f83e..b8ccdb6c1 100644 --- a/app/views/publisher/_publisher.html.haml +++ b/app/views/publisher/_publisher.html.haml @@ -24,7 +24,7 @@ .container-fluid#photodropzone_container %ul#photodropzone - #location_container.form-group{ style: "padding: 4px 6px;"} + .location-container.form-group{style: "padding: 4px 6px;"} = hidden_field :location, :coords #poll_creator_container -# handlebars template diff --git a/spec/javascripts/app/views/publisher_view_spec.js b/spec/javascripts/app/views/publisher_view_spec.js index 3d9ea4559..d89e9a219 100644 --- a/spec/javascripts/app/views/publisher_view_spec.js +++ b/spec/javascripts/app/views/publisher_view_spec.js @@ -430,7 +430,7 @@ describe("app.views.Publisher", function() { it("Show location", function(){ // inserts location to the DOM; it is the location's view element - setFixtures('
      '); + setFixtures('
      '); // creates a fake Locator OSM = {}; From b1d60d7c9a890c664fa80f2abeb7cc5aa828e3d1 Mon Sep 17 00:00:00 2001 From: augier Date: Mon, 23 Nov 2015 19:58:51 +0100 Subject: [PATCH 03/10] Jasmine tests --- .../app/views/publisher/mention_view.js | 104 +++--- .../javascripts/app/views/search_base_view.js | 17 +- app/assets/stylesheets/publisher.scss | 7 +- .../app/views/publisher_mention_view_spec.js | 322 ++++++++++++++++++ .../app/views/search_base_view_spec.js | 183 ++++++++++ .../javascripts/app/views/search_view_spec.js | 95 +++--- 6 files changed, 628 insertions(+), 100 deletions(-) create mode 100644 spec/javascripts/app/views/publisher_mention_view_spec.js create mode 100644 spec/javascripts/app/views/search_base_view_spec.js diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js index 270e14161..4383da759 100644 --- a/app/assets/javascripts/app/views/publisher/mention_view.js +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -8,8 +8,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({ KEYS: { - BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37, UP: 38, - RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35 + PASTE: 118, BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37, + UP: 38, RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35 }, settings: { @@ -23,33 +23,13 @@ app.views.PublisherMention = app.views.SearchBase.extend({ } }, - utils: { - setCaretPosition: function(domNode, caretPos){ - if(domNode.createTextRange){ - var range = domNode.createTextRange(); - range.move("character", caretPos); - range.select(); - } else{ - if(domNode.selectionStart){ - domNode.focus(); - domNode.setSelectionRange(caretPos, caretPos); - } else{ - domNode.focus(); - } - } - }, - - rtrim: function(string){ - return string.replace(/\s+$/, ""); - } - }, - events: { "keydown #status_message_fake_text": "onInputBoxKeyDown", "keypress #status_message_fake_text": "onInputBoxKeyPress", "input #status_message_fake_text": "onInputBoxInput", "click #status_message_fake_text": "onInputBoxClick", - "blur #status_message_fake_text": "onInputBoxBlur" + "blur #status_message_fake_text": "onInputBoxBlur", + "paste #status_message_fake_text": "onInputBoxPaste" }, initialize: function(){ @@ -66,21 +46,24 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay()); this.elmMentionsOverlay.prependTo(this.elmWrapperBox); + this.bindMentionningEvents(); + this.completeSetup(this.getTypeaheadInput()); + + this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"}); + this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0); + }, + + bindMentionningEvents: function(){ var self = this; - this.getSearchInput().on("typeahead:select", function(evt, datum){ + this.getTypeaheadInput().on("typeahead:select", function(evt, datum){ self.processMention(datum); self.resetMentionBox(); self.addToFilteredResults(datum); }); - this.getSearchInput().on("typeahead:render", function(){ + this.getTypeaheadInput().on("typeahead:render", function(){ self.select(self.$(".tt-menu .tt-suggestion").first()); }); - - this.completeSetup(this.getSearchInput()); - - this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"}); - this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0); }, clearBuffer: function(){ @@ -110,12 +93,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({ processMention: function(mention){ var currentMessage = this.getInputBoxValue(); - // Using a regex to figure out positions - var regex = new RegExp("\\" + this.settings.triggerChar + this.currentDataQuery, "gi"); - regex.exec(currentMessage); - - var startCaretPosition = regex.lastIndex - this.currentDataQuery.length - 1; - var currentCaretPosition = regex.lastIndex; + var currentCaretPosition = this.getCaretPosition(); + var startCaretPosition = currentCaretPosition - (this.currentDataQuery.length + 1); var start = currentMessage.substr(0, startCaretPosition); var end = currentMessage.substr(currentCaretPosition, currentMessage.length); @@ -135,7 +114,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ // Set correct focus and selection this.elmInputBox.focus(); - this.utils.setCaretPosition(this.elmInputBox[0], startEndIndex); + this.setCaretPosition(startEndIndex); }, updateValues: function(){ @@ -153,7 +132,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ var textSyntax = self.settings.templates.mentionItemSyntax(mention); syntaxMessage = syntaxMessage.replace(mentionVal, textSyntax); - var textHighlight = self.settings.templates.mentionItemHighlight({ name: _.escape(mention.name) }); + var textHighlight = self.settings.templates.mentionItemHighlight({name: _.escape(mention.name)}); mentionText = mentionText.replace(mentionVal, textHighlight); }); @@ -174,7 +153,11 @@ app.views.PublisherMention = app.views.SearchBase.extend({ _.each(persons, function(person){ self.addMention(person); self.addToFilteredResults(person); - self.elmInputBox.val(self.mentionChar + person.name); + var text = self.mentionChar + person.name; + if(self.elmInputBox.val().length !== 0){ + text = self.elmInputBox.val() + " " + text; + } + self.elmInputBox.val(text); self.updateValues(); }); }, @@ -210,7 +193,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({ }, onInputBoxKeyPress: function(e){ - if(e.keyCode !== this.KEYS.BACKSPACE){ + // Excluding ctrl+v from key press event in firefox + if(!((e.which === this.KEYS.PASTE && e.ctrlKey) || (e.keyCode === this.KEYS.BACKSPACE))){ var typedValue = String.fromCharCode(e.which || e.keyCode); this.inputBuffer.push(typedValue); } @@ -223,7 +207,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ var triggerCharIndex = _.lastIndexOf(this.inputBuffer, this.settings.triggerChar); if(triggerCharIndex > -1){ this.currentDataQuery = this.inputBuffer.slice(triggerCharIndex + 1).join(""); - this.currentDataQuery = this.utils.rtrim(this.currentDataQuery); + this.currentDataQuery = this.rtrim(this.currentDataQuery); this.showMentionBox(); } @@ -232,7 +216,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ onInputBoxKeyDown: function(e){ // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT if(e.keyCode === this.KEYS.LEFT || e.keyCode === this.KEYS.RIGHT || - e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){ + e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){ _.defer(this.clearBuffer); // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting @@ -268,7 +252,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ case this.KEYS.RETURN: case this.KEYS.TAB: if(this.getSelected().size() === 1){ - this.getSelected().click(); + this.getSelected().click(); return false; } break; @@ -284,6 +268,15 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.resetMentionBox(); }, + onInputBoxPaste: function(evt){ + var pastedData = evt.originalEvent.clipboardData.getData("text/plain"); + var dataArray = pastedData.split(""); + var self = this; + _.each(dataArray, function(value){ + self.inputBuffer.push(value); + }); + }, + reset: function(){ this.elmInputBox.val(""); this.mentionsCollection.length = 0; @@ -292,13 +285,13 @@ app.views.PublisherMention = app.views.SearchBase.extend({ }, showMentionBox: function(){ - this.getSearchInput().typeahead("val", this.currentDataQuery); - this.getSearchInput().typeahead("open"); + this.getTypeaheadInput().typeahead("val", this.currentDataQuery); + this.getTypeaheadInput().typeahead("open"); }, resetMentionBox: function(){ - this.getSearchInput().typeahead("val", ""); - this.getSearchInput().typeahead("close"); + this.getTypeaheadInput().typeahead("val", ""); + this.getTypeaheadInput().typeahead("close"); }, getInputBoxValue: function(){ @@ -309,7 +302,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ return this.$el.find(".tt-menu").is(":visible"); }, - getSearchInput: function(){ + getTypeaheadInput: function(){ if(this.$el.find(".typeahead-mention-box").length === 0){ this.elmInputBox.after(""); } @@ -318,5 +311,18 @@ app.views.PublisherMention = app.views.SearchBase.extend({ getTextForSubmit: function(){ return this.mentionsCollection.length ? this.elmInputBox.data("messageText") : this.getInputBoxValue(); + }, + + setCaretPosition: function(caretPos){ + this.elmInputBox[0].focus(); + this.elmInputBox[0].setSelectionRange(caretPos, caretPos); + }, + + getCaretPosition: function(){ + return this.elmInputBox[0].selectionStart; + }, + + rtrim: function(string){ + return string.replace(/\s+$/, ""); } }); diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js index af7914122..65bf83fcd 100644 --- a/app/assets/javascripts/app/views/search_base_view.js +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -98,18 +98,19 @@ app.views.SearchBase = app.views.Base.extend({ */ bindSelectionEvents: function(){ var self = this; - var onover = function(evt){ - var isSuggestion = $(evt.target).is(".tt-suggestion"); - var suggestion = isSuggestion ? $(evt.target) : $(evt.target).parent(".tt-suggestion"); - if(suggestion){ + var onover = function(suggestion){ + return function(){ self.select(suggestion); - } + }; }; this.typeaheadElement.on("typeahead:render", function(){ - self.$(".tt-menu *").off("mouseover", onover); - self.$(".tt-menu .tt-suggestion").on("mouseover", onover); - self.$(".tt-menu .tt-suggestion *").on("mouseover", onover); + self.$(".tt-menu *").off("mouseover"); + self.$(".tt-menu .tt-suggestion").each(function(){ + var $suggestion = $(this); + $suggestion.on("mouseover", onover($suggestion)); + $suggestion.find("*").on("mouseover", onover($suggestion)); + }); }); }, diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss index 1a2ddaec1..f5b874445 100644 --- a/app/assets/stylesheets/publisher.scss +++ b/app/assets/stylesheets/publisher.scss @@ -17,7 +17,12 @@ } .container-fluid{ padding: 0; } - .mentions-autocomplete-list ul { width: 100% !important; } + + .twitter-typeahead { + width: calc(100% + 2px); + + .tt-menu { width: 100%; } + } form { margin: 0; diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js new file mode 100644 index 000000000..f10470494 --- /dev/null +++ b/spec/javascripts/app/views/publisher_mention_view_spec.js @@ -0,0 +1,322 @@ +describe("app.views.PublisherMention", function(){ + beforeEach(function(){ + spec.content().html( + "
      " + + "" + + "
      "); + }); + + describe("initialize", function(){ + beforeEach(function(){ + spyOn(app.views.PublisherMention.prototype, "completeSetup").and.callThrough(); + spyOn(app.views.PublisherMention.prototype, "bindMentionningEvents").and.callThrough(); + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("initializes object properties", function(){ + expect(this.view.mentionsCollection).toEqual([]); + expect(this.view.inputBuffer).toEqual([]); + expect(this.view.currentDataQuery).toBe(""); + expect(this.view.mentionChar).toBe("\u200B"); + }); + + it("calls completeSetup", function(){ + expect(app.views.PublisherMention.prototype.completeSetup).toHaveBeenCalledWith(this.view.getTypeaheadInput()); + expect(app.views.PublisherMention.prototype.bindMentionningEvents).toHaveBeenCalled(); + }); + + it("initializes html elements", function(){ + expect(this.view.$(".typeahead-mention-box").length).toBe(1); + expect(this.view.$(".mentions-input-box").length).toBe(1); + expect(this.view.$(".mentions-box").length).toBe(1); + expect(this.view.$(".mentions").length).toBe(1); + }); + }); + + describe("bindMentionningEvents", function(){ + beforeEach(function(){ + spyOn(app.views.PublisherMention.prototype, "processMention"); + spyOn(app.views.PublisherMention.prototype, "resetMentionBox"); + spyOn(app.views.PublisherMention.prototype, "addToFilteredResults"); + this.view = new app.views.PublisherMention({ el: "#publisher" }); + this.view.bloodhound.add([ + {"person": true, "name":"user1", "handle":"user1@pod.tld"}, + {"person": true, "name":"user2", "handle":"user2@pod.tld"} + ]); + }); + + it("highlights the first item when rendering results", function(){ + this.view.getTypeaheadInput().typeahead("val", "user"); + this.view.getTypeaheadInput().typeahead("open"); + expect(this.view.$(".tt-suggestion").first()).toHaveClass("tt-cursor"); + }); + + it("process mention when clicking a result", function(){ + this.view.getTypeaheadInput().typeahead("val", "user"); + this.view.getTypeaheadInput().typeahead("open"); + this.view.$(".tt-suggestion").first().click(); + expect(app.views.PublisherMention.prototype.processMention).toHaveBeenCalled(); + expect(app.views.PublisherMention.prototype.resetMentionBox).toHaveBeenCalled(); + expect(app.views.PublisherMention.prototype.addToFilteredResults).toHaveBeenCalled(); + }); + }); + + describe("updateMentionsCollection", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("removes person from mention collection if not mentionned anymore", function(){ + this.view.mentionsCollection.push({name: "user1"}); + expect(this.view.mentionsCollection.length).toBe(1); + this.view.updateMentionsCollection(); + expect(this.view.mentionsCollection.length).toBe(0); + }); + + it("removes item from mention collection if not a person", function(){ + this.view.mentionsCollection.push({}); + expect(this.view.mentionsCollection.length).toBe(1); + this.view.updateMentionsCollection(); + expect(this.view.mentionsCollection.length).toBe(0); + }); + }); + + describe("addMention", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("add person to mentionned people", function(){ + expect(this.view.mentionsCollection.length).toBe(0); + this.view.addMention({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.mentionsCollection.length).toBe(1); + expect(this.view.mentionsCollection[0]).toEqual({ + /* jshint camelcase: false */ + "name":"user1", "handle":"user1@pod.tld", diaspora_id: "user1@pod.tld"}); + /* jshint camelcase: true */ + }); + + it("does not add mention if not a person", function(){ + expect(this.view.mentionsCollection.length).toBe(0); + this.view.addMention(); + expect(this.view.mentionsCollection.length).toBe(0); + this.view.addMention({}); + expect(this.view.mentionsCollection.length).toBe(0); + this.view.addMention({"name": "user1"}); + expect(this.view.mentionsCollection.length).toBe(0); + this.view.addMention({"handle":"user1@pod.tld"}); + expect(this.view.mentionsCollection.length).toBe(0); + }); + }); + + describe("getTypeaheadInput", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("inserts typeahead input if it does not already exist", function(){ + this.view.getTypeaheadInput().remove(); + expect(this.view.$(".typeahead-mention-box").length).toBe(0); + this.view.getTypeaheadInput(); + expect(this.view.$(".typeahead-mention-box").length).toBe(1); + }); + }); + + describe("processMention", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + this.view.elmInputBox.val("@user1 Text before @user1 text after"); + this.view.currentDataQuery = "user1"; + this.view.elmInputBox[0].setSelectionRange(25, 25); + }); + + it("add person to mentionned people", function(){ + spyOn(this.view, "addMention"); + this.view.processMention({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + }); + + it("cleans buffers", function(){ + spyOn(this.view, "clearBuffer"); + spyOn(this.view, "resetMentionBox"); + this.view.processMention({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.clearBuffer).toHaveBeenCalled(); + expect(this.view.resetMentionBox).toHaveBeenCalled(); + expect(this.view.currentDataQuery).toBe(""); + }); + + it("correctly formats the text", function(){ + spyOn(this.view, "updateValues"); + this.view.processMention({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.updateValues).toHaveBeenCalled(); + expect(this.view.getInputBoxValue()).toBe("@user1 Text before " + this.view.mentionChar + "user1 text after"); + }); + + it("places the caret at the right position", function(){ + this.view.processMention({"name":"user1WithLongName", "handle":"user1@pod.tld"}); + var expectedCaretPosition = ("@user1 Text before " + this.view.mentionChar + "user1WithLongName").length; + expect(this.view.elmInputBox[0].selectionStart).toBe(expectedCaretPosition); + }); + }); + + describe("updateValues", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + this.view.elmInputBox.val("@user1 Text before " + this.view.mentionChar + "user1\ntext after"); + this.view.mentionsCollection.push({"name":"user1", "handle":"user1@pod.tld"}); + }); + + it("filters mention from future results", function(){ + spyOn(this.view, "clearFilteredResults"); + spyOn(this.view, "addToFilteredResults"); + this.view.updateValues(); + expect(this.view.clearFilteredResults).toHaveBeenCalled(); + expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + }); + + it("formats message text data with correct mentionning syntax", function(){ + this.view.updateValues(); + expect(this.view.elmInputBox.data("messageText")).toBe("@user1 Text before @{user1 ; user1@pod.tld}\ntext after"); + }); + + it("formats overlay text to HTML", function(){ + this.view.updateValues(); + expect(this.view.elmMentionsOverlay.find("div > div").html()) + .toBe("@user1 Text before user1
      text after"); + }); + }); + + describe("prefillMention", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + spyOn(this.view, "addMention"); + spyOn(this.view, "addToFilteredResults"); + spyOn(this.view, "updateValues"); + }); + + it("prefills one mention", function(){ + this.view.prefillMention([{"name":"user1", "handle":"user1@pod.tld"}]); + + expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.addToFilteredResults) + .toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.updateValues).toHaveBeenCalled(); + expect(this.view.getInputBoxValue()).toBe(this.view.mentionChar + "user1"); + }); + + it("prefills multiple mentions", function(){ + this.view.prefillMention([ + {"name":"user1", "handle":"user1@pod.tld"}, + {"name":"user2", "handle":"user2@pod.tld"} + ]); + + expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.addMention).toHaveBeenCalledWith({"name":"user2", "handle":"user2@pod.tld"}); + expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user2", "handle":"user2@pod.tld"}); + expect(this.view.updateValues).toHaveBeenCalled(); + expect(this.view.getInputBoxValue()).toBe(this.view.mentionChar + "user1 " + this.view.mentionChar + "user2"); + }); + }); + + describe("onInputBoxPaste", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("add person to mentionned people", function(){ + var pasteEvent = {originalEvent: {clipboardData: {getData: function(){ + return "Pasted text"; + }}}}; + + this.view.onInputBoxPaste(pasteEvent); + expect(this.view.inputBuffer).toEqual(["P", "a", "s", "t", "e", "d", " ", "t", "e", "x", "t"]); + }); + }); + + describe("reset", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + spyOn(this.view, "clearFilteredResults"); + spyOn(this.view, "updateValues"); + }); + + it("resets the mention box", function(){ + this.view.reset(); + expect(this.view.elmInputBox.val()).toBe(""); + expect(this.view.mentionsCollection.length).toBe(0); + expect(this.view.clearFilteredResults).toHaveBeenCalled(); + expect(this.view.updateValues).toHaveBeenCalled(); + }); + }); + + describe("showMentionBox", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + this.view.bloodhound.add([ + {"person": true, "name":"user1", "handle":"user1@pod.tld"} + ]); + this.view.currentDataQuery = "user1"; + }); + + it("shows the mention box", function(){ + expect(this.view.$(".tt-menu").is(":visible")).toBe(false); + expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(0); + this.view.showMentionBox(); + expect(this.view.$(".tt-menu").is(":visible")).toBe(true); + expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(1); + }); + }); + + describe("resetMentionBox", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + this.view.bloodhound.add([ + {"person": true, "name":"user1", "handle":"user1@pod.tld"} + ]); + }); + + it("resets results and closes mention box", function(){ + this.view.getTypeaheadInput().typeahead("val", "user"); + this.view.getTypeaheadInput().typeahead("open"); + expect(this.view.$(".tt-menu").is(":visible")).toBe(true); + expect(this.view.$(".tt-menu .tt-suggestion").length >= 1).toBe(true); + this.view.resetMentionBox(); + expect(this.view.$(".tt-menu").is(":visible")).toBe(false); + expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(0); + }); + }); + + describe("getInputBoxValue", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("returns trimmed text", function(){ + this.view.elmInputBox.val("Text with trailing spaces "); + expect(this.view.getInputBoxValue()).toBe("Text with trailing spaces"); + }); + }); + + describe("getTextForSubmit", function(){ + beforeEach(function(){ + this.view = new app.views.PublisherMention({ el: "#publisher" }); + this.view.bloodhound.add([ + {"person": true, "name":"user1", "handle":"user1@pod.tld"} + ]); + }); + + it("returns text with mention syntax if someone is mentionned", function(){ + this.view.getTypeaheadInput().typeahead("val", "user"); + this.view.getTypeaheadInput().typeahead("open"); + this.view.$(".tt-suggestion").first().click(); + expect(this.view.getTextForSubmit()).toBe("@{user1 ; user1@pod.tld}"); + }); + + it("returns normal text if nobody is mentionned", function(){ + this.view.elmInputBox.data("messageText", "Bad text"); + this.view.elmInputBox.val("Good text"); + expect(this.view.getTextForSubmit()).toBe("Good text"); + }); + }); +}); diff --git a/spec/javascripts/app/views/search_base_view_spec.js b/spec/javascripts/app/views/search_base_view_spec.js new file mode 100644 index 000000000..36bd65912 --- /dev/null +++ b/spec/javascripts/app/views/search_base_view_spec.js @@ -0,0 +1,183 @@ +describe("app.views.SearchBase", function() { + beforeEach(function(){ + spec.content().html( + "
      " + ); + }); + + describe("completeSetup", function(){ + it("calls setupBloodhound", function(){ + spyOn(app.views.SearchBase.prototype, "setupBloodhound").and.callThrough(); + var view = new app.views.SearchBase({el: "#search_people_form"}); + view.completeSetup(); + expect(app.views.SearchBase.prototype.setupBloodhound).toHaveBeenCalled(); + }); + + it("calls setupTypeahead", function(){ + spyOn(app.views.SearchBase.prototype, "setupTypeahead"); + var view = new app.views.SearchBase({el: "#search_people_form"}); + view.completeSetup(); + expect(app.views.SearchBase.prototype.setupTypeahead).toHaveBeenCalled(); + }); + + it("calls bindSelectionEvents", function(){ + spyOn(app.views.SearchBase.prototype, "bindSelectionEvents"); + var view = new app.views.SearchBase({el: "#search_people_form"}); + view.completeSetup(); + expect(app.views.SearchBase.prototype.bindSelectionEvents).toHaveBeenCalled(); + }); + + it("initializes the results to filter", function(){ + spyOn(app.views.SearchBase.prototype, "bindSelectionEvents"); + var view = new app.views.SearchBase({el: "#search_people_form"}); + view.completeSetup(); + expect(view.resultsTofilter.length).toBe(0); + }); + }); + + describe("setupBloodhound", function(){ + beforeEach(function(){ + this.view = new app.views.SearchBase({el: "#search_people_form"}); + this.syncCallback = function(){}; + this.asyncCallback = function(){}; + }); + + context("when performing a local search with 1 filtered result", function(){ + beforeEach(function(){ + this.view.completeSetup(this.view.$("#q")); + this.view.bloodhound.add([ + {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}, + {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"} + ]); + }); + + it("should not return the filtered result", function(){ + spyOn(this, "syncCallback"); + spyOn(this, "asyncCallback"); + + this.view.bloodhound.customSearch("user", this.syncCallback, this.asyncCallback); + expect(this.syncCallback).toHaveBeenCalledWith([ + {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}, + {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"} + ]); + expect(this.asyncCallback).not.toHaveBeenCalled(); + + this.view.addToFilteredResults({"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}); + this.view.bloodhound.customSearch("user", this.syncCallback, this.asyncCallback); + expect(this.syncCallback).toHaveBeenCalledWith( + [{"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"}]); + expect(this.asyncCallback).not.toHaveBeenCalled(); + }); + }); + }); + + describe("transformBloodhoundResponse", function() { + beforeEach(function() { + this.view = new app.views.SearchBase({ el: "#search_people_form" }); + }); + + context("with persons", function() { + beforeEach(function() { + this.response = [{name: "Person", handle: "person@pod.tld"},{name: "User", handle: "user@pod.tld"}]; + }); + + it("sets data.person to true", function() { + expect(this.view.transformBloodhoundResponse(this.response)).toEqual([ + {name: "Person", handle: "person@pod.tld", person: true}, + {name: "User", handle: "user@pod.tld", person: true} + ]); + }); + }); + + context("with hashtags", function() { + beforeEach(function() { + this.response = [{name: "#tag"}, {name: "#hashTag"}]; + }); + + it("sets data.hashtag to true and adds the correct URL", function() { + expect(this.view.transformBloodhoundResponse(this.response)).toEqual([ + {name: "#tag", hashtag: true, url: Routes.tag("tag")}, + {name: "#hashTag", hashtag: true, url: Routes.tag("hashTag")} + ]); + }); + }); + }); + + describe("bindSelectionEvents", function(){ + beforeEach(function() { + this.view = new app.views.SearchBase({ el: "#search_people_form" }); + this.view.completeSetup(this.view.$("#q")); + this.view.bloodhound.add([ + {"person": true, "name":"user1", "handle":"user1@pod.tld"}, + {"person": true, "name":"user2", "handle":"user2@pod.tld"} + ]); + }); + + context("bind over events", function(){ + it("binds over event only once", function(){ + this.view.$("#q").trigger("focusin"); + this.view.$("#q").val("user"); + this.view.$("#q").trigger("keypress"); + this.view.$("#q").trigger("input"); + this.view.$("#q").trigger("focus"); + var numBindedEvents = $._data(this.view.$(".tt-menu .tt-suggestion")[0], "events").mouseover.length; + expect(numBindedEvents).toBe(1); + this.view.$("#q").trigger("focusout"); + this.view.$("#q").trigger("focusin"); + this.view.$("#q").val("user"); + this.view.$("#q").trigger("keypress"); + this.view.$("#q").trigger("input"); + this.view.$("#q").trigger("focus"); + numBindedEvents = $._data(this.view.$(".tt-menu .tt-suggestion")[0], "events").mouseover.length; + expect(numBindedEvents).toBe(1); + }); + + it("highlights the result when overing it", function(){ + this.view.$("#q").trigger("focusin"); + this.view.$("#q").val("user"); + this.view.$("#q").trigger("keypress"); + this.view.$("#q").trigger("input"); + this.view.$("#q").trigger("focus"); + this.view.$(".tt-menu .tt-suggestion").first().trigger("mouseover"); + expect(this.view.$(".tt-menu .tt-suggestion").first()).toHaveClass("tt-cursor"); + }); + }); + }); + + describe("addToFilteredResults", function(){ + beforeEach(function() { + this.view = new app.views.SearchBase({ el: "#search_people_form" }); + this.view.completeSetup(this.view.$("#q")); + }); + + context("when item is a person", function(){ + it("add the item to filtered results", function(){ + this.view.addToFilteredResults({handle: "user@pod.tld"}); + expect(this.view.resultsTofilter.length).toBe(1); + }); + }); + + context("when item is not a person", function(){ + it("does not add the item to filtered results", function(){ + this.view.addToFilteredResults({}); + expect(this.view.resultsTofilter.length).toBe(0); + }); + }); + }); + + describe("clearFilteredResults", function(){ + beforeEach(function() { + this.view = new app.views.SearchBase({ el: "#search_people_form" }); + this.view.completeSetup(this.view.$("#q")); + }); + + context("clear filtered results", function(){ + it("clears the filtered results list", function(){ + this.view.addToFilteredResults({handle: "user@pod.tld"}); + expect(this.view.resultsTofilter.length).toBe(1); + this.view.clearFilteredResults(); + expect(this.view.resultsTofilter.length).toBe(0); + }); + }); + }); +}); diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js index 19fc0ccfb..70525ae3b 100644 --- a/spec/javascripts/app/views/search_view_spec.js +++ b/spec/javascripts/app/views/search_view_spec.js @@ -1,21 +1,63 @@ -describe("app.views.Search", function() { +describe("app.views.Search", function(){ beforeEach(function(){ spec.content().html( - "
      " + "
      " ); }); - describe("initialize", function() { - it("calls setupBloodhound", function() { - spyOn(app.views.Search.prototype, "setupBloodhound").and.callThrough(); - new app.views.Search({ el: "#search_people_form" }); - expect(app.views.Search.prototype.setupBloodhound).toHaveBeenCalled(); + describe("initialize", function(){ + it("calls completeSetup", function(){ + spyOn(app.views.Search.prototype, "completeSetup").and.callThrough(); + var view = new app.views.Search({el: "#search_people_form"}); + expect(app.views.Search.prototype.completeSetup).toHaveBeenCalledWith(view.getTypeaheadElement()); }); - it("calls setupTypeahead", function() { - spyOn(app.views.Search.prototype, "setupTypeahead"); - new app.views.Search({ el: "#search_people_form" }); - expect(app.views.Search.prototype.setupTypeahead).toHaveBeenCalled(); + it("calls bindMoreSelectionEvents", function(){ + spyOn(app.views.Search.prototype, "bindMoreSelectionEvents").and.callThrough(); + new app.views.Search({el: "#search_people_form"}); + expect(app.views.Search.prototype.bindMoreSelectionEvents).toHaveBeenCalled(); + }); + }); + + describe("bindMoreSelectionEvents", function(){ + beforeEach(function() { + this.view = new app.views.Search({ el: "#search_people_form" }); + this.view.bloodhound.add([ + {"person": true, "name":"user1", "handle":"user1@pod.tld"}, + {"person": true, "name":"user2", "handle":"user2@pod.tld"} + ]); + }); + + context("bind mouseleave event", function(){ + it("binds mouseleave event only once", function(){ + this.view.$("#q").trigger("focusin"); + this.view.$("#q").val("user"); + this.view.$("#q").trigger("keypress"); + this.view.$("#q").trigger("input"); + this.view.$("#q").trigger("focus"); + var numBindedEvents = $._data(this.view.$(".tt-menu")[0], "events").mouseout.length; + expect(numBindedEvents).toBe(1); + this.view.$("#q").trigger("focusout"); + this.view.$("#q").trigger("focusin"); + this.view.$("#q").val("user"); + this.view.$("#q").trigger("keypress"); + this.view.$("#q").trigger("input"); + this.view.$("#q").trigger("focus"); + numBindedEvents = $._data(this.view.$(".tt-menu")[0], "events").mouseout.length; + expect(numBindedEvents).toBe(1); + }); + + it("remove result highlight when leaving results list", function(){ + this.view.$("#q").trigger("focusin"); + this.view.$("#q").val("user"); + this.view.$("#q").trigger("keypress"); + this.view.$("#q").trigger("input"); + this.view.$("#q").trigger("focus"); + this.view.$(".tt-menu .tt-suggestion").first().trigger("mouseover"); + expect(this.view.$(".tt-menu .tt-suggestion").first()).toHaveClass("tt-cursor"); + this.view.$(".tt-menu").first().trigger("mouseleave"); + expect(this.view.$(".tt-menu .tt-cursor").length).toBe(0); + }); }); }); @@ -44,35 +86,4 @@ describe("app.views.Search", function() { }); }); }); - - describe("transformBloodhoundResponse" , function() { - beforeEach(function() { - this.view = new app.views.Search({ el: "#search_people_form" }); - }); - context("with persons", function() { - beforeEach(function() { - this.response = [{name: "Person", handle: "person@pod.tld"},{name: "User", handle: "user@pod.tld"}]; - }); - - it("sets data.person to true", function() { - expect(this.view.transformBloodhoundResponse(this.response)).toEqual([ - {name: "Person", handle: "person@pod.tld", person: true}, - {name: "User", handle: "user@pod.tld", person: true} - ]); - }); - }); - - context("with hashtags", function() { - beforeEach(function() { - this.response = [{name: "#tag"}, {name: "#hashTag"}]; - }); - - it("sets data.hashtag to true and adds the correct URL", function() { - expect(this.view.transformBloodhoundResponse(this.response)).toEqual([ - {name: "#tag", hashtag: true, url: Routes.tag("tag")}, - {name: "#hashTag", hashtag: true, url: Routes.tag("hashTag")} - ]); - }); - }); - }); }); From 8f021be20b05a0b68368b8b353f5753cccb9827e Mon Sep 17 00:00:00 2001 From: Augier Date: Tue, 16 Feb 2016 12:49:56 +0100 Subject: [PATCH 04/10] Do not instanciate publisher if not publisher element is present --- app/assets/javascripts/app/router.js | 4 +++- app/assets/javascripts/app/views/tags_view.js | 4 +++- spec/javascripts/app/router_spec.js | 8 ++++++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 52117d495..312afbdae 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -180,8 +180,10 @@ app.Router = Backbone.Router.extend({ } app.page = new app.views.Stream({model : app.stream}); - app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items}); app.shortcuts = app.shortcuts || new app.views.StreamShortcuts({el: $(document)}); + if($("#publisher").length !== 0){ + app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items}); + } $("#main_stream").html(app.page.render().el); this._hideInactiveStreamLists(); diff --git a/app/assets/javascripts/app/views/tags_view.js b/app/assets/javascripts/app/views/tags_view.js index e420281fb..563050b3c 100644 --- a/app/assets/javascripts/app/views/tags_view.js +++ b/app/assets/javascripts/app/views/tags_view.js @@ -2,7 +2,9 @@ app.views.Tags = Backbone.View.extend({ initialize: function(opts) { - app.publisher.setText("#"+ opts.hashtagName + " "); + if(app.publisher){ + app.publisher.setText("#"+ opts.hashtagName + " "); + } } }); // @license-end diff --git a/spec/javascripts/app/router_spec.js b/spec/javascripts/app/router_spec.js index 0dbe5b269..a356bedb5 100644 --- a/spec/javascripts/app/router_spec.js +++ b/spec/javascripts/app/router_spec.js @@ -2,6 +2,7 @@ describe('app.Router', function () { describe('followed_tags', function() { beforeEach(function() { factory.preloads({tagFollowings: []}); + spec.loadFixture("aspects_index"); }); it('decodes name before passing it into TagFollowingAction', function () { @@ -92,6 +93,7 @@ describe('app.Router', function () { delete app.page; delete app.publisher; delete app.shortcuts; + spec.loadFixture("aspects_index"); }); it("sets app.page", function() { @@ -112,6 +114,12 @@ describe('app.Router', function () { expect(app.publisher.jasmineTestValue).toEqual(42); }); + it("doesn't set app.publisher if there is no publisher element in page", function(){ + $("#publisher").remove(); + app.router._initializeStreamView(); + expect(app.publisher).toBeUndefined(); + }); + it("sets app.shortcuts", function() { expect(app.shortcuts).toBeUndefined(); app.router._initializeStreamView(); From e34960392cf93d5f71888980c6a04ee28d9ea9b2 Mon Sep 17 00:00:00 2001 From: Augier Date: Sat, 20 Feb 2016 17:13:22 +0100 Subject: [PATCH 05/10] Code simplifications and typo --- .../app/views/publisher/mention_view.js | 119 ++++++++++++------ .../javascripts/app/views/search_base_view.js | 34 ++--- .../javascripts/app/views/search_view.js | 2 +- app/assets/javascripts/main.js | 1 - app/assets/javascripts/mentions.js | 48 ------- .../app/views/publisher_mention_view_spec.js | 11 +- .../app/views/search_base_view_spec.js | 22 ++-- .../javascripts/app/views/search_view_spec.js | 7 +- 8 files changed, 118 insertions(+), 126 deletions(-) delete mode 100644 app/assets/javascripts/mentions.js diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js index 4383da759..27bb92990 100644 --- a/app/assets/javascripts/app/views/publisher/mention_view.js +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -32,6 +32,27 @@ app.views.PublisherMention = app.views.SearchBase.extend({ "paste #status_message_fake_text": "onInputBoxPaste" }, + /** + * Performs setup of the setup of the plugin. + * + * this.mentionsCollection: used to keep track of the people mentionned in the post + * this.inputBuffer: buffer to keep track of the text currently being typed. It is cleared + * each time a mention has been processed. + * See this#onInputBoxKeyPress + * this.currentDataQuery: contains the query for the search engine + * + * The plugin initilizes two different elements that will contain the text of the post: + * + * this.elmInputBox: hidden element which keeps track of typed text formatted following + * the mentioning syntax given by this.settings.templates#mentionItemSyntax + * For instance, if the user writes the text "Hello @user1", the resulting hidden + * text will be: "Hello @{user1 ; user1@pod.tld}. This is the text that is submitted + * to the pod when the user posts. + * this.elmMentionsOverlay: contains the text that will be displayed to the user + * + * this.mentionChar is a invisible caracter used to mark the name of the mentionned person + * during the process. See this#processMention + */ initialize: function(){ this.mentionsCollection = []; this.inputBuffer = []; @@ -39,28 +60,32 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.mentionChar = "\u200B"; this.elmInputBox = this.$el.find("#status_message_fake_text"); - this.elmInputWrapper = this.elmInputBox.parent(); - this.elmWrapperBox = $(this.settings.templates.wrapper()); - this.elmInputBox.wrapAll(this.elmWrapperBox); - this.elmWrapperBox = this.elmInputWrapper.find("> div").first(); + var elmInputWrapper = this.elmInputBox.parent(); + this.elmInputBox.wrapAll($(this.settings.templates.wrapper())); + var elmWrapperBox = elmInputWrapper.find("> div").first(); this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay()); - this.elmMentionsOverlay.prependTo(this.elmWrapperBox); + this.elmMentionsOverlay.prependTo(elmWrapperBox); - this.bindMentionningEvents(); - this.completeSetup(this.getTypeaheadInput()); + this.bindMentioningEvents(); + app.views.SearchBase.prototype.initialize.call(this, {typeaheadElement: this.getTypeaheadInput()}); this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"}); this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0); }, - bindMentionningEvents: function(){ + /** + * Attach events to Typeahead. + */ + bindMentioningEvents: function(){ var self = this; + // Process mention when the user selects a result. this.getTypeaheadInput().on("typeahead:select", function(evt, datum){ self.processMention(datum); self.resetMentionBox(); self.addToFilteredResults(datum); }); + // Highlight the first result when the results dropdown opens this.getTypeaheadInput().on("typeahead:render", function(){ self.select(self.$(".tt-menu .tt-suggestion").first()); }); @@ -70,6 +95,10 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.inputBuffer.length = 0; }, + /** + * Cleans the collection of mentionned people. Rejects every item who's name + * is not present in the post an falsy values (false, null, "", etc.) + */ updateMentionsCollection: function(){ var inputText = this.getInputBoxValue(); @@ -79,6 +108,11 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.mentionsCollection = _.compact(this.mentionsCollection); }, + /** + * Adds mention to the mention collection + * @param person Mentionned person. + * JSON object of form { handle: , name: , ... } + */ addMention: function(person){ if(!person || !person.name || !person.handle){ return; @@ -90,12 +124,25 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.mentionsCollection.push(person); }, + /** + * Process the text to add mention to the post. Every @mention in the text + * will be replaced by this.mentionChar + mention.name. This temporary text + * will then be replaced by final syntax in this#updateValues + * + * For instance if the user types text "Hello @use" and selects result user1, + * The text will be transformed to "Hello \u200Buser1" before calling this#updateValues + * + * @param mention Mentionned person. + * JSON object of form { handle: , name: , ... } + */ processMention: function(mention){ var currentMessage = this.getInputBoxValue(); var currentCaretPosition = this.getCaretPosition(); var startCaretPosition = currentCaretPosition - (this.currentDataQuery.length + 1); + // Extracts the text before the mention and the text after it. + // startEndIndex is the position where to place the caret at the en of the process var start = currentMessage.substr(0, startCaretPosition); var end = currentMessage.substr(currentCaretPosition, currentMessage.length); var startEndIndex = (start + mention.name).length + 1; @@ -107,16 +154,24 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.currentDataQuery = ""; this.resetMentionBox(); - // Mentions & syntax message + // Autocompletes mention and updates message text var updatedMessageText = start + this.mentionChar + mention.name + end; this.elmInputBox.val(updatedMessageText); this.updateValues(); - // Set correct focus and selection + // Set correct focus and caret position this.elmInputBox.focus(); this.setCaretPosition(startEndIndex); }, + /** + * Replaces every combination of this.mentionChar + mention.name by the + * correct syntax for both hidden text and visible one. + * + * For instance, the text "Hello \u200Buser1" will be tranformed to + * "Hello @{user1 ; user1@pod.tld}" in the hidden element and + * "Hello user1" in the element visible to the user. + */ updateValues: function(){ var syntaxMessage = this.getInputBoxValue(); var mentionText = this.getInputBoxValue(); @@ -162,34 +217,21 @@ app.views.PublisherMention = app.views.SearchBase.extend({ }); }, - selectNextResult: function(evt){ - if(this.isVisible()){ - evt.preventDefault(); - evt.stopPropagation(); + /** + * Selects next or previous result when result dropdown is open and + * user press up and down arrows. + */ + onArrowKeysPress: function(e){ + if(!this.isVisible() || (e.keyCode !== this.KEYS.UP && e.keyCode !== this.KEYS.DOWN)){ + return; } - if(this.getSelected().size() !== 1 || this.getSelected().next().size() !== 1){ - this.getSelected().removeClass("tt-cursor"); - this.$el.find(".tt-suggestion").first().addClass("tt-cursor"); - } - else{ - this.getSelected().removeClass("tt-cursor").next().addClass("tt-cursor"); - } - }, + e.preventDefault(); + e.stopPropagation(); - selectPreviousResult: function(evt){ - if(this.isVisible()){ - evt.preventDefault(); - evt.stopPropagation(); - } - - if(this.getSelected().size() !== 1 || this.getSelected().prev().size() !== 1){ - this.getSelected().removeClass("tt-cursor"); - this.$el.find(".tt-suggestion").last().addClass("tt-cursor"); - } - else{ - this.getSelected().removeClass("tt-cursor").prev().addClass("tt-cursor"); - } + this.getTypeaheadInput().typeahead("activate"); + this.getTypeaheadInput().typeahead("open"); + this.getTypeaheadInput().trigger($.Event("keydown", {keyCode: e.keyCode})); }, onInputBoxKeyPress: function(e){ @@ -200,6 +242,9 @@ app.views.PublisherMention = app.views.SearchBase.extend({ } }, + /** + * Listens for user input and opens results dropdown when input contains the trigger char + */ onInputBoxInput: function(){ this.updateValues(); this.updateMentionsCollection(); @@ -244,10 +289,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.resetMentionBox(); break; case this.KEYS.UP: - this.selectPreviousResult(e); - break; case this.KEYS.DOWN: - this.selectNextResult(e); + this.onArrowKeysPress(e); break; case this.KEYS.RETURN: case this.KEYS.TAB: diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js index 65bf83fcd..6b2dfae29 100644 --- a/app/assets/javascripts/app/views/search_base_view.js +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -1,6 +1,6 @@ app.views.SearchBase = app.views.Base.extend({ - completeSetup: function(typeaheadElement){ - this.typeaheadElement = $(typeaheadElement); + initialize: function(options){ + this.typeaheadElement = $(options.typeaheadElement); this.setupBloodhound(); this.setupTypeahead(); this.bindSelectionEvents(); @@ -59,21 +59,21 @@ app.views.SearchBase = app.views.Base.extend({ setupTypeahead: function() { this.typeaheadElement.typeahead({ - hint: false, - highlight: true, - minLength: 2 - }, - { - name: "search", - display: "name", - limit: 5, - source: this.bloodhound.customSearch, - templates: { - /* jshint camelcase: false */ - suggestion: HandlebarsTemplates.search_suggestion_tpl - /* jshint camelcase: true */ - } - }); + hint: false, + highlight: true, + minLength: 2 + }, + { + name: "search", + display: "name", + limit: 5, + source: this.searchFormAction !== undefined ? this.bloodhound : this.bloodhound.customSearch, + templates: { + /* jshint camelcase: false */ + suggestion: HandlebarsTemplates.search_suggestion_tpl + /* jshint camelcase: true */ + } + }); }, transformBloodhoundResponse: function(response) { diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index 65ed775c0..ec183b094 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -8,7 +8,7 @@ app.views.Search = app.views.SearchBase.extend({ initialize: function(){ this.searchFormAction = this.$el.attr("action"); - this.completeSetup(this.getTypeaheadElement()); + app.views.SearchBase.prototype.initialize.call(this, {typeaheadElement: this.getTypeaheadElement()}); this.bindMoreSelectionEvents(); this.getTypeaheadElement().on("typeahead:select", this.suggestionSelected); }, diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 458824bd1..2a2ef7c1d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -38,7 +38,6 @@ //= require_tree ./helpers //= require_tree ./pages //= require_tree ./widgets -//= require mentions //= require bootstrap //= require osmlocator //= require bootstrap-switch diff --git a/app/assets/javascripts/mentions.js b/app/assets/javascripts/mentions.js deleted file mode 100644 index f805b97fa..000000000 --- a/app/assets/javascripts/mentions.js +++ /dev/null @@ -1,48 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later - -var Mentions = { - initialize: function(mentionsInput) { - return mentionsInput.mentionsInput(Mentions.options); - }, - - // pre-fetch the list of contacts for the current user. - // called by the initializer of the publisher, for faster ('offline') - // execution of the filtering for mentions - fetchContacts : function(){ - Mentions.contacts || $.getJSON("/contacts", function(data) { - Mentions.contacts = Mentions.createList(data); - }); - }, - - // creates a list of mentions out of a list of contacts - // @see _contactToMention - createList: function(contacts) { - return _.map(contacts, Mentions._contactToMention); - }, - - // takes a given contact object and modifies to fit the format - // expected by the jQuery.mentionsInput plugin. - // @see http://podio.github.com/jquery-mentions-input/ - _contactToMention: function(contact) { - contact.value = contact.name; - return contact; - }, - - // default options for jQuery.mentionsInput - // @see http://podio.github.com/jquery-mentions-input/ - options: { - elastic: false, - minChars: 1, - - onDataRequest: function(mode, query, callback) { - var filteredResults = _.filter(Mentions.contacts, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); - - callback.call(this, filteredResults.slice(0,5)); - }, - - templates: { - mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}") - } - } -}; -// @license-end diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js index f10470494..f67aad215 100644 --- a/spec/javascripts/app/views/publisher_mention_view_spec.js +++ b/spec/javascripts/app/views/publisher_mention_view_spec.js @@ -8,8 +8,8 @@ describe("app.views.PublisherMention", function(){ describe("initialize", function(){ beforeEach(function(){ - spyOn(app.views.PublisherMention.prototype, "completeSetup").and.callThrough(); - spyOn(app.views.PublisherMention.prototype, "bindMentionningEvents").and.callThrough(); + spyOn(app.views.SearchBase.prototype, "initialize").and.callThrough(); + spyOn(app.views.PublisherMention.prototype, "bindMentioningEvents").and.callThrough(); this.view = new app.views.PublisherMention({ el: "#publisher" }); }); @@ -21,8 +21,9 @@ describe("app.views.PublisherMention", function(){ }); it("calls completeSetup", function(){ - expect(app.views.PublisherMention.prototype.completeSetup).toHaveBeenCalledWith(this.view.getTypeaheadInput()); - expect(app.views.PublisherMention.prototype.bindMentionningEvents).toHaveBeenCalled(); + expect(app.views.SearchBase.prototype.initialize) + .toHaveBeenCalledWith({typeaheadElement: this.view.getTypeaheadInput()}); + expect(app.views.PublisherMention.prototype.bindMentioningEvents).toHaveBeenCalled(); }); it("initializes html elements", function(){ @@ -33,7 +34,7 @@ describe("app.views.PublisherMention", function(){ }); }); - describe("bindMentionningEvents", function(){ + describe("bindMentioningEvents", function(){ beforeEach(function(){ spyOn(app.views.PublisherMention.prototype, "processMention"); spyOn(app.views.PublisherMention.prototype, "resetMentionBox"); diff --git a/spec/javascripts/app/views/search_base_view_spec.js b/spec/javascripts/app/views/search_base_view_spec.js index 36bd65912..d0e633305 100644 --- a/spec/javascripts/app/views/search_base_view_spec.js +++ b/spec/javascripts/app/views/search_base_view_spec.js @@ -1,36 +1,32 @@ describe("app.views.SearchBase", function() { beforeEach(function(){ spec.content().html( - "
      " + "
      " ); }); - describe("completeSetup", function(){ + describe("initialize", function(){ it("calls setupBloodhound", function(){ spyOn(app.views.SearchBase.prototype, "setupBloodhound").and.callThrough(); - var view = new app.views.SearchBase({el: "#search_people_form"}); - view.completeSetup(); + new app.views.SearchBase({el: "#search_people_form"}); expect(app.views.SearchBase.prototype.setupBloodhound).toHaveBeenCalled(); }); it("calls setupTypeahead", function(){ spyOn(app.views.SearchBase.prototype, "setupTypeahead"); - var view = new app.views.SearchBase({el: "#search_people_form"}); - view.completeSetup(); + new app.views.SearchBase({el: "#search_people_form"}); expect(app.views.SearchBase.prototype.setupTypeahead).toHaveBeenCalled(); }); it("calls bindSelectionEvents", function(){ spyOn(app.views.SearchBase.prototype, "bindSelectionEvents"); - var view = new app.views.SearchBase({el: "#search_people_form"}); - view.completeSetup(); + new app.views.SearchBase({el: "#search_people_form"}); expect(app.views.SearchBase.prototype.bindSelectionEvents).toHaveBeenCalled(); }); it("initializes the results to filter", function(){ spyOn(app.views.SearchBase.prototype, "bindSelectionEvents"); var view = new app.views.SearchBase({el: "#search_people_form"}); - view.completeSetup(); expect(view.resultsTofilter.length).toBe(0); }); }); @@ -44,7 +40,7 @@ describe("app.views.SearchBase", function() { context("when performing a local search with 1 filtered result", function(){ beforeEach(function(){ - this.view.completeSetup(this.view.$("#q")); + this.view.initialize({typeaheadElement: this.view.$("#q")}); this.view.bloodhound.add([ {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}, {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"} @@ -106,7 +102,7 @@ describe("app.views.SearchBase", function() { describe("bindSelectionEvents", function(){ beforeEach(function() { this.view = new app.views.SearchBase({ el: "#search_people_form" }); - this.view.completeSetup(this.view.$("#q")); + this.view.initialize({typeaheadElement: this.view.$("#q")}); this.view.bloodhound.add([ {"person": true, "name":"user1", "handle":"user1@pod.tld"}, {"person": true, "name":"user2", "handle":"user2@pod.tld"} @@ -147,7 +143,7 @@ describe("app.views.SearchBase", function() { describe("addToFilteredResults", function(){ beforeEach(function() { this.view = new app.views.SearchBase({ el: "#search_people_form" }); - this.view.completeSetup(this.view.$("#q")); + this.view.initialize({typeaheadElement: this.view.$("#q")}); }); context("when item is a person", function(){ @@ -168,7 +164,7 @@ describe("app.views.SearchBase", function() { describe("clearFilteredResults", function(){ beforeEach(function() { this.view = new app.views.SearchBase({ el: "#search_people_form" }); - this.view.completeSetup(this.view.$("#q")); + this.view.initialize({typeaheadElement: this.view.$("#q")}); }); context("clear filtered results", function(){ diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js index 70525ae3b..001fabddd 100644 --- a/spec/javascripts/app/views/search_view_spec.js +++ b/spec/javascripts/app/views/search_view_spec.js @@ -6,10 +6,11 @@ describe("app.views.Search", function(){ }); describe("initialize", function(){ - it("calls completeSetup", function(){ - spyOn(app.views.Search.prototype, "completeSetup").and.callThrough(); + it("calls app.views.SearchBase.prototype.initialize", function(){ + spyOn(app.views.SearchBase.prototype, "initialize").and.callThrough(); var view = new app.views.Search({el: "#search_people_form"}); - expect(app.views.Search.prototype.completeSetup).toHaveBeenCalledWith(view.getTypeaheadElement()); + expect(app.views.SearchBase.prototype.initialize) + .toHaveBeenCalledWith({typeaheadElement: view.getTypeaheadElement()}); }); it("calls bindMoreSelectionEvents", function(){ From f948120ba6bf4f084738a1cd3eda18f3b3bedbd0 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 26 Feb 2016 09:03:33 +0100 Subject: [PATCH 06/10] Refactor keycodes --- .../app/views/aspect_create_view.js | 2 +- .../app/views/comment_stream_view.js | 2 +- .../app/views/conversations_form_view.js | 2 +- .../app/views/conversations_view.js | 2 +- .../app/views/publisher/mention_view.js | 31 ++--- .../javascripts/app/views/publisher_view.js | 5 +- .../javascripts/app/views/search_view.js | 2 +- .../app/views/tag_following_list_view.js | 2 +- .../pages/users-getting-started.js | 2 +- app/views/profiles/_edit_public.haml | 2 +- app/views/users/getting_started.mobile.haml | 2 +- config/.jshint.json | 1 + lib/assets/javascripts/keycodes.js | 117 ++++++++++++++++++ .../app/views/aspect_create_view_spec.js | 4 +- .../app/views/comment_stream_view_spec.js | 6 +- .../app/views/conversations_view_spec.js | 4 +- .../app/views/publisher_view_spec.js | 6 +- .../app/views/stream/shortcuts_spec.js | 56 +++------ spec/javascripts/lib/keycodes_spec.js | 13 ++ vendor/assets/javascripts/keycodes.js | 117 ------------------ 20 files changed, 178 insertions(+), 200 deletions(-) create mode 100644 lib/assets/javascripts/keycodes.js create mode 100644 spec/javascripts/lib/keycodes_spec.js delete mode 100644 vendor/assets/javascripts/keycodes.js diff --git a/app/assets/javascripts/app/views/aspect_create_view.js b/app/assets/javascripts/app/views/aspect_create_view.js index 325c874bc..ad40616a6 100644 --- a/app/assets/javascripts/app/views/aspect_create_view.js +++ b/app/assets/javascripts/app/views/aspect_create_view.js @@ -32,7 +32,7 @@ app.views.AspectCreate = app.views.Base.extend({ }, inputKeypress: function(evt) { - if(evt.which === 13) { + if(evt.which === Keycodes.ENTER) { evt.preventDefault(); this.createAspect(); } diff --git a/app/assets/javascripts/app/views/comment_stream_view.js b/app/assets/javascripts/app/views/comment_stream_view.js index 87dce7e12..8cc3d238e 100644 --- a/app/assets/javascripts/app/views/comment_stream_view.js +++ b/app/assets/javascripts/app/views/comment_stream_view.js @@ -56,7 +56,7 @@ app.views.CommentStream = app.views.Base.extend({ }, keyDownOnCommentBox: function(evt) { - if(evt.keyCode === 13 && evt.ctrlKey) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { this.$("form").submit(); return false; } diff --git a/app/assets/javascripts/app/views/conversations_form_view.js b/app/assets/javascripts/app/views/conversations_form_view.js index 4f5ac7133..53dbee681 100644 --- a/app/assets/javascripts/app/views/conversations_form_view.js +++ b/app/assets/javascripts/app/views/conversations_form_view.js @@ -43,7 +43,7 @@ app.views.ConversationsForm = Backbone.View.extend({ }, keyDown : function(evt) { - if( evt.keyCode === 13 && evt.ctrlKey ) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { $(evt.target).parents("form").submit(); } } diff --git a/app/assets/javascripts/app/views/conversations_view.js b/app/assets/javascripts/app/views/conversations_view.js index 869471557..d5ed22a3a 100644 --- a/app/assets/javascripts/app/views/conversations_view.js +++ b/app/assets/javascripts/app/views/conversations_view.js @@ -50,7 +50,7 @@ app.views.Conversations = Backbone.View.extend({ }, keyDown : function(evt) { - if( evt.keyCode === 13 && evt.ctrlKey ) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { $(evt.target).parents("form").submit(); } } diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js index 27bb92990..9f3859b67 100644 --- a/app/assets/javascripts/app/views/publisher/mention_view.js +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -7,11 +7,6 @@ */ app.views.PublisherMention = app.views.SearchBase.extend({ - KEYS: { - PASTE: 118, BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37, - UP: 38, RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35 - }, - settings: { triggerChar: "@", minChars: 2, @@ -222,7 +217,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ * user press up and down arrows. */ onArrowKeysPress: function(e){ - if(!this.isVisible() || (e.keyCode !== this.KEYS.UP && e.keyCode !== this.KEYS.DOWN)){ + if(!this.isVisible() || (e.which !== Keycodes.UP && e.which !== Keycodes.DOWN)){ return; } @@ -231,12 +226,12 @@ app.views.PublisherMention = app.views.SearchBase.extend({ this.getTypeaheadInput().typeahead("activate"); this.getTypeaheadInput().typeahead("open"); - this.getTypeaheadInput().trigger($.Event("keydown", {keyCode: e.keyCode})); + this.getTypeaheadInput().trigger($.Event("keydown", {keyCode: e.keyCode, which: e.which})); }, onInputBoxKeyPress: function(e){ // Excluding ctrl+v from key press event in firefox - if(!((e.which === this.KEYS.PASTE && e.ctrlKey) || (e.keyCode === this.KEYS.BACKSPACE))){ + if(!((String.fromCharCode(e.which).toLowerCase() === "v" && e.ctrlKey) || (e.which === Keycodes.BACKSPACE))){ var typedValue = String.fromCharCode(e.which || e.keyCode); this.inputBuffer.push(typedValue); } @@ -260,8 +255,8 @@ app.views.PublisherMention = app.views.SearchBase.extend({ onInputBoxKeyDown: function(e){ // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT - if(e.keyCode === this.KEYS.LEFT || e.keyCode === this.KEYS.RIGHT || - e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){ + if(e.which === Keycodes.LEFT || e.which === Keycodes.RIGHT || + e.which === Keycodes.HOME || e.which === Keycodes.END){ _.defer(this.clearBuffer); // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting @@ -274,7 +269,7 @@ app.views.PublisherMention = app.views.SearchBase.extend({ return; } - if(e.keyCode === this.KEYS.BACKSPACE){ + if(e.which === Keycodes.BACKSPACE){ this.inputBuffer = this.inputBuffer.slice(0, this.inputBuffer.length - 1); return; } @@ -283,17 +278,17 @@ app.views.PublisherMention = app.views.SearchBase.extend({ return true; } - switch(e.keyCode){ - case this.KEYS.ESC: - case this.KEYS.SPACE: + switch(e.which){ + case Keycodes.ESC: + case Keycodes.SPACE: this.resetMentionBox(); break; - case this.KEYS.UP: - case this.KEYS.DOWN: + case Keycodes.UP: + case Keycodes.DOWN: this.onArrowKeysPress(e); break; - case this.KEYS.RETURN: - case this.KEYS.TAB: + case Keycodes.RETURN: + case Keycodes.TAB: if(this.getSelected().size() === 1){ this.getSelected().click(); return false; diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index 05081d0c3..d5b562f36 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -247,8 +247,9 @@ app.views.Publisher = Backbone.View.extend({ // avoid submitting form when pressing Enter key avoidEnter: function(evt){ - if(evt.keyCode === 13) + if(evt.which === Keycodes.ENTER) { return false; + } }, getUploadedPhotos: function() { @@ -356,7 +357,7 @@ app.views.Publisher = Backbone.View.extend({ }, keyDown : function(evt) { - if( evt.keyCode === 13 && evt.ctrlKey ) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { this.$("form").submit(); this.open(); return false; diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index ec183b094..b45faeb96 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -40,7 +40,7 @@ app.views.Search = app.views.SearchBase.extend({ }, inputKeypress: function(evt){ - if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0){ + if(evt.which === Keycodes.ENTER && $(".tt-suggestion.tt-cursor").length === 0){ $(evt.target).closest("form").submit(); } }, diff --git a/app/assets/javascripts/app/views/tag_following_list_view.js b/app/assets/javascripts/app/views/tag_following_list_view.js index 3ccdf3a6e..0504ab270 100644 --- a/app/assets/javascripts/app/views/tag_following_list_view.js +++ b/app/assets/javascripts/app/views/tag_following_list_view.js @@ -46,7 +46,7 @@ app.views.TagFollowingList = app.views.Base.extend({ }); this.$("input").bind('keydown', function(evt){ - if(evt.keyCode === 13 || evt.keyCode === 9 || evt.keyCode === 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length === 0 ){ $('li.as-result-item').first().click(); diff --git a/app/assets/javascripts/pages/users-getting-started.js b/app/assets/javascripts/pages/users-getting-started.js index fb6184ba6..cb82e03a0 100644 --- a/app/assets/javascripts/pages/users-getting-started.js +++ b/app/assets/javascripts/pages/users-getting-started.js @@ -73,7 +73,7 @@ Diaspora.Pages.UsersGettingStarted = function() { }); autocompleteInput.bind('keydown', function(evt){ - if(evt.keyCode === 13 || evt.keyCode === 9 || evt.keyCode === 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length === 0 ){ $('li.as-result-item').first().click(); diff --git a/app/views/profiles/_edit_public.haml b/app/views/profiles/_edit_public.haml index ac45aff8c..c909ca841 100644 --- a/app/views/profiles/_edit_public.haml +++ b/app/views/profiles/_edit_public.haml @@ -21,7 +21,7 @@ }); autocompleteInput.bind('keydown', function(evt){ - if(evt.keyCode == 13 || evt.keyCode == 9 || evt.keyCode == 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length == 0 ){ $('li.as-result-item').first().click(); diff --git a/app/views/users/getting_started.mobile.haml b/app/views/users/getting_started.mobile.haml index af1cb4959..2ab3a3f6c 100644 --- a/app/views/users/getting_started.mobile.haml +++ b/app/views/users/getting_started.mobile.haml @@ -25,7 +25,7 @@ }); autocompleteInput.bind('keydown', function(evt){ - if(evt.keyCode == 13 || evt.keyCode == 9 || evt.keyCode == 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length == 0 ){ $('li.as-result-item').first().click(); diff --git a/config/.jshint.json b/config/.jshint.json index cf93d1e6e..ab9e3d704 100644 --- a/config/.jshint.json +++ b/config/.jshint.json @@ -66,6 +66,7 @@ "app", "Diaspora", + "Keycodes", "Mentions", "PosixBracketExpressions" ] diff --git a/lib/assets/javascripts/keycodes.js b/lib/assets/javascripts/keycodes.js new file mode 100644 index 000000000..c0bcb3af5 --- /dev/null +++ b/lib/assets/javascripts/keycodes.js @@ -0,0 +1,117 @@ +window.Keycodes = { + BACKSPACE: 8, + TAB: 9, + ENTER: 13, + RETURN: 13, + SHIFT: 16, + CTRL: 17, + ALT: 18, + PAUSE: 19, + BREAK: 19, + CAPSLOCK: 20, + ESCAPE: 27, + ESC: 27, + SPACEBAR: 32, + SPACE: 32, + PAGEUP: 33, + PAGEDOWN: 34, + END: 35, + HOME: 36, + LEFT: 37, + UP: 38, + RIGHT: 39, + DOWN: 40, + INSERT: 45, + DEL: 46, + DELETE: 46, + 0: 48, + 1: 49, + 2: 50, + 3: 51, + 4: 52, + 5: 53, + 6: 54, + 7: 55, + 8: 56, + 9: 57, + A: 65, + B: 66, + C: 67, + D: 68, + E: 69, + F: 70, + G: 71, + H: 72, + I: 73, + J: 74, + K: 75, + L: 76, + M: 77, + N: 78, + O: 79, + P: 80, + Q: 81, + R: 82, + S: 83, + T: 84, + U: 85, + V: 86, + W: 87, + X: 88, + Y: 89, + Z: 90, + LEFTWINDOW: 91, + RIGHTWINDOW: 92, + SELECT: 93, + NUMPAD0: 96, + NUMPAD1: 97, + NUMPAD2: 98, + NUMPAD3: 99, + NUMPAD4: 100, + NUMPAD5: 101, + NUMPAD6: 102, + NUMPAD7: 103, + NUMPAD8: 104, + NUMPAD9: 105, + MULTIPLY: 106, + ADD: 107, + SUBTRACT: 109, + DECIMALPOINT: 110, + DIVIDE: 111, + F1: 112, + F2: 113, + F3: 114, + F4: 115, + F5: 116, + F6: 117, + F7: 118, + F8: 119, + F9: 120, + F10: 121, + F11: 122, + F12: 123, + NUMLOCK: 144, + SCROLLLOCK: 145, + SEMICOLON: 186, + EQUALSIGN: 187, + COMMA: 188, + DASH: 189, + PERIOD: 190, + FORWARDSLASH: 191, + ACCENTGRAVE: 192, + OPENBRACKET: 219, + BACKSLASH: 220, + CLOSEBRACKET: 221, + SINGLEQUOTE: 222, + isInsertion: function(keyCode) { + if(keyCode <= 46 && keyCode !== this.RETURN && keyCode !== this.SPACEBAR) { + return false; + } else if(keyCode > 90 && keyCode < 96) { + return false; + } else if(keyCode >= 112 && keyCode <= 145) { + return false; + } else { + return true; + } + } +}; diff --git a/spec/javascripts/app/views/aspect_create_view_spec.js b/spec/javascripts/app/views/aspect_create_view_spec.js index a99e4d195..780a5c9cd 100644 --- a/spec/javascripts/app/views/aspect_create_view_spec.js +++ b/spec/javascripts/app/views/aspect_create_view_spec.js @@ -47,13 +47,13 @@ describe("app.views.AspectCreate", function() { }); it("should call createAspect if the enter key was pressed", function() { - var e = $.Event("keypress", { which: 13 }); + var e = $.Event("keypress", { which: Keycodes.ENTER }); this.view.inputKeypress(e); expect(this.view.createAspect).toHaveBeenCalled(); }); it("shouldn't call createAspect if another key was pressed", function() { - var e = $.Event("keypress", { which: 42 }); + var e = $.Event("keypress", { which: Keycodes.TAB }); this.view.inputKeypress(e); expect(this.view.createAspect).not.toHaveBeenCalled(); }); diff --git a/spec/javascripts/app/views/comment_stream_view_spec.js b/spec/javascripts/app/views/comment_stream_view_spec.js index 58fa00323..e45f82033 100644 --- a/spec/javascripts/app/views/comment_stream_view_spec.js +++ b/spec/javascripts/app/views/comment_stream_view_spec.js @@ -107,8 +107,7 @@ describe("app.views.CommentStream", function(){ var form = this.view.$("form"); form.submit(submitCallback); - var e = $.Event("keydown", { keyCode: 13 }); - e.ctrlKey = false; + var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: false }); this.view.keyDownOnCommentBox(e); expect(submitCallback).not.toHaveBeenCalled(); @@ -119,8 +118,7 @@ describe("app.views.CommentStream", function(){ var form = this.view.$("form"); form.submit(submitCallback); - var e = $.Event("keydown", { keyCode: 13 }); - e.ctrlKey = true; + var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: true }); this.view.keyDownOnCommentBox(e); expect(submitCallback).toHaveBeenCalled(); diff --git a/spec/javascripts/app/views/conversations_view_spec.js b/spec/javascripts/app/views/conversations_view_spec.js index d48b88047..ae5d62e10 100644 --- a/spec/javascripts/app/views/conversations_view_spec.js +++ b/spec/javascripts/app/views/conversations_view_spec.js @@ -64,14 +64,14 @@ describe("app.views.Conversations", function(){ it("should submit the form with ctrl+enter", function(){ $("form#new_message").submit(this.submitCallback); - var e = $.Event("keydown", { keyCode: 13, ctrlKey: true }); + var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: true }); $("textarea#message_text").trigger(e); expect(this.submitCallback).toHaveBeenCalled(); }); it("shouldn't submit the form without the ctrl key", function(){ $("form#new_message").submit(this.submitCallback); - var e = $.Event("keydown", { keyCode: 13, ctrlKey: false }); + var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: false }); $("textarea#message_text").trigger(e); expect(this.submitCallback).not.toHaveBeenCalled(); }); diff --git a/spec/javascripts/app/views/publisher_view_spec.js b/spec/javascripts/app/views/publisher_view_spec.js index d89e9a219..b9be5089d 100644 --- a/spec/javascripts/app/views/publisher_view_spec.js +++ b/spec/javascripts/app/views/publisher_view_spec.js @@ -225,8 +225,7 @@ describe("app.views.Publisher", function() { var submitCallback = jasmine.createSpy().and.returnValue(false); form.submit(submitCallback); - var e = $.Event("keydown", { keyCode: 13 }); - e.ctrlKey = true; + var e = $.Event("keydown", { which: Keycodes.ENTER, ctrlKey: true }); this.view.keyDown(e); expect(submitCallback).toHaveBeenCalled(); @@ -460,8 +459,7 @@ describe("app.views.Publisher", function() { describe('#avoidEnter', function(){ it("Avoid submitting the form when pressing enter", function(){ // simulates the event object - var evt = {}; - evt.keyCode = 13; + var evt = $.Event("keydown", { which: Keycodes.ENTER }); // should return false in order to avoid the form submition expect(this.view.avoidEnter(evt)).toBeFalsy(); diff --git a/spec/javascripts/app/views/stream/shortcuts_spec.js b/spec/javascripts/app/views/stream/shortcuts_spec.js index a90b8ddd5..071a0605a 100644 --- a/spec/javascripts/app/views/stream/shortcuts_spec.js +++ b/spec/javascripts/app/views/stream/shortcuts_spec.js @@ -16,9 +16,7 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'j'", function(){ it("should call 'gotoNext' if not pressed in an input field", function(){ spyOn(this.view, 'gotoNext'); - var e = $.Event("keydown", { which: 74, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('j'); + var e = $.Event("keydown", { which: Keycodes.J, target: {type: "div"} }); this.view._onHotkeyDown(e); expect(this.view.gotoNext).toHaveBeenCalled(); }); @@ -32,9 +30,7 @@ describe("app.views.StreamShortcuts", function () { it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'gotoNext'); spyOn(this.view, 'selectPost'); - var e = $.Event("keydown", { which: 74, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('j'); + var e = $.Event("keydown", { which: Keycodes.J, target: {type: "textarea"} }); this.view._onHotkeyDown(e); expect(this.view.gotoNext).not.toHaveBeenCalled(); expect(this.view.selectPost).not.toHaveBeenCalled(); @@ -44,9 +40,7 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'k'", function(){ it("should call 'gotoPrev' if not pressed in an input field", function(){ spyOn(this.view, 'gotoPrev'); - var e = $.Event("keydown", { which: 75, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('k'); + var e = $.Event("keydown", { which: Keycodes.K, target: {type: "div"} }); this.view._onHotkeyDown(e); expect(this.view.gotoPrev).toHaveBeenCalled(); }); @@ -60,9 +54,7 @@ describe("app.views.StreamShortcuts", function () { it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'gotoPrev'); spyOn(this.view, 'selectPost'); - var e = $.Event("keydown", { which: 75, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('k'); + var e = $.Event("keydown", { which: Keycodes.K, target: {type: "textarea"} }); this.view._onHotkeyDown(e); expect(this.view.gotoPrev).not.toHaveBeenCalled(); expect(this.view.selectPost).not.toHaveBeenCalled(); @@ -72,18 +64,14 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'c'", function(){ it("should click on the comment-button if not pressed in an input field", function(){ spyOn(this.view, 'commentSelected'); - var e = $.Event("keyup", { which: 67, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('c'); + var e = $.Event("keyup", { which: Keycodes.C, target: {type: "div"} }); this.view._onHotkeyUp(e); expect(this.view.commentSelected).toHaveBeenCalled(); }); it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'commentSelected'); - var e = $.Event("keyup", { which: 67, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('c'); + var e = $.Event("keyup", { which: Keycodes.C, target: {type: "textarea"} }); this.view._onHotkeyUp(e); expect(this.view.commentSelected).not.toHaveBeenCalled(); }); @@ -92,18 +80,14 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'l'", function(){ it("should click on the like-button if not pressed in an input field", function(){ spyOn(this.view, 'likeSelected'); - var e = $.Event("keyup", { which: 76, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('l'); + var e = $.Event("keyup", { which: Keycodes.L, target: {type: "div"} }); this.view._onHotkeyUp(e); expect(this.view.likeSelected).toHaveBeenCalled(); }); it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'likeSelected'); - var e = $.Event("keyup", { which: 76, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('l'); + var e = $.Event("keyup", { which: Keycodes.L, target: {type: "textarea"} }); this.view._onHotkeyUp(e); expect(this.view.likeSelected).not.toHaveBeenCalled(); }); @@ -112,18 +96,14 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'r'", function(){ it("should click on the reshare-button if not pressed in an input field", function(){ spyOn(this.view, 'reshareSelected'); - var e = $.Event("keyup", { which: 82, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('r'); + var e = $.Event("keyup", { which: Keycodes.R, target: {type: "div"} }); this.view._onHotkeyUp(e); expect(this.view.reshareSelected).toHaveBeenCalled(); }); it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'reshareSelected'); - var e = $.Event("keyup", { which: 82, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('r'); + var e = $.Event("keyup", { which: Keycodes.R, target: {type: "textarea"} }); this.view._onHotkeyUp(e); expect(this.view.reshareSelected).not.toHaveBeenCalled(); }); @@ -132,18 +112,14 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'm'", function(){ it("should click on the more-button if not pressed in an input field", function(){ spyOn(this.view, 'expandSelected'); - var e = $.Event("keyup", { which: 77, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('m'); + var e = $.Event("keyup", { which: Keycodes.M, target: {type: "div"} }); this.view._onHotkeyUp(e); expect(this.view.expandSelected).toHaveBeenCalled(); }); it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'expandSelected'); - var e = $.Event("keyup", { which: 77, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('m'); + var e = $.Event("keyup", { which: Keycodes.M, target: {type: "textarea"} }); this.view._onHotkeyUp(e); expect(this.view.expandSelected).not.toHaveBeenCalled(); }); @@ -152,18 +128,14 @@ describe("app.views.StreamShortcuts", function () { describe("pressing 'o'", function(){ it("should click on the more-button if not pressed in an input field", function(){ spyOn(this.view, 'openFirstLinkSelected'); - var e = $.Event("keyup", { which: 79, target: {type: "div"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('o'); + var e = $.Event("keyup", { which: Keycodes.O, target: {type: "div"} }); this.view._onHotkeyUp(e); expect(this.view.openFirstLinkSelected).toHaveBeenCalled(); }); it("shouldn't do anything if the user types in an input field", function(){ spyOn(this.view, 'openFirstLinkSelected'); - var e = $.Event("keyup", { which: 79, target: {type: "textarea"} }); - //verify that the test is correct - expect(String.fromCharCode( e.which ).toLowerCase()).toBe('o'); + var e = $.Event("keyup", { which: Keycodes.O, target: {type: "textarea"} }); this.view._onHotkeyUp(e); expect(this.view.openFirstLinkSelected).not.toHaveBeenCalled(); }); diff --git a/spec/javascripts/lib/keycodes_spec.js b/spec/javascripts/lib/keycodes_spec.js new file mode 100644 index 000000000..f3a0258ae --- /dev/null +++ b/spec/javascripts/lib/keycodes_spec.js @@ -0,0 +1,13 @@ +describe("Keycodes", function() { + it("sets the correct keycode for letters", function() { + "ABCDEFGHIJKLMNOPQRSTUVWXYZ".split("").forEach(function(c) { + expect(String.fromCharCode(Keycodes[c])).toBe(c); + }); + }); + + it("sets the correct keycode for digits", function() { + "0123456789".split("").forEach(function(c) { + expect(String.fromCharCode(Keycodes[c])).toBe(c); + }); + }); +}); diff --git a/vendor/assets/javascripts/keycodes.js b/vendor/assets/javascripts/keycodes.js deleted file mode 100644 index 10f081726..000000000 --- a/vendor/assets/javascripts/keycodes.js +++ /dev/null @@ -1,117 +0,0 @@ -var KEYCODES = { -BACKSPACE : 8, -TAB : 9, -ENTER : 13, -RETURN : 13, -SHIFT : 16, -CTRL : 17, -ALT : 18, -PAUSE : 19, -BREAK : 19, -CAPSLOCK : 20, -ESCAPE : 27, -ESC : 27, -SPACEBAR : 32, -SPACE: 32, -PAGEUP : 33, -PAGEDOWN : 34, -END : 35, -HOME : 36, -LEFT : 37, -UP : 38, -RIGHT : 39, -DOWN : 40, -INSERT : 45, -DEL : 46, -DELETE : 46, -0 : 48, -1 : 49, -2 : 50, -3 : 51, -4 : 52, -5 : 53, -6 : 54, -7 : 55, -8 : 56, -9 : 57, -A : 65, -B : 66, -C : 67, -D : 68, -E : 69, -F : 70, -G : 71, -H : 72, -I : 73, -J : 74, -K : 75, -L : 76, -M : 77, -N : 78, -O : 79, -P : 80, -Q : 81, -R : 82, -S : 83, -T : 84, -U : 85, -V : 86, -W : 87, -X : 88, -Y : 89, -Z : 90, -LEFTWINDOW : 91, -RIGHTWINDOW : 92, -SELECT : 93, -NUMPAD0 : 96, -NUMPAD1 : 97, -NUMPAD2 : 98, -NUMPAD3 : 99, -NUMPAD4 : 100, -NUMPAD5 : 101, -NUMPAD6 : 102, -NUMPAD7 : 103, -NUMPAD8 : 104, -NUMPAD9 : 105, -MULTIPLY : 106, -ADD : 107, -SUBTRACT : 109, -DECIMALPOINT : 110, -DIVIDE : 111, -F1 : 112, -F2 : 113, -F3 : 114, -F4 : 115, -F5 : 116, -F6 : 117, -F7 : 118, -F8 : 119, -F9 : 120, -F10 : 121, -F11 : 122, -F12 : 123, -NUMLOCK : 144, -SCROLLLOCK : 145, -SEMICOLON : 186, -EQUALSIGN : 187, -COMMA : 188, -DASH : 189, -PERIOD : 190, -FORWARDSLASH : 191, -ACCENTGRAVE : 192, -OPENBRACKET : 219, -BACKSLASH : 220, -CLOSEBRACKET : 221, -SINGLEQUOTE : 222, -isInsertion : function(keyCode){ - if(keyCode <= 46 && keyCode != this.RETURN && keyCode != this.SPACEBAR){ - return false; - }else if(keyCode > 90 && keyCode < 96){ - return false; - }else if(keyCode >= 112 && keyCode <= 145){ - return false; - }else { - return true; - } -} -}; From bee08daca671b4c5e4d9e3802921859ef0e629e8 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 26 Feb 2016 09:35:20 +0100 Subject: [PATCH 07/10] Remove jquery-elastic --- Gemfile | 1 - Gemfile.lock | 3 --- app/assets/javascripts/main.js | 1 - 3 files changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index bd634d1ab..2d65b08be 100644 --- a/Gemfile +++ b/Gemfile @@ -112,7 +112,6 @@ source "https://rails-assets.org" do gem "rails-assets-jquery-placeholder", "2.3.1" gem "rails-assets-jquery-textchange", "0.2.3" gem "rails-assets-perfect-scrollbar", "0.6.10" - gem "rails-assets-jakobmattsson--jquery-elastic", "1.6.11" gem "rails-assets-autosize", "3.0.15" gem "rails-assets-blueimp-gallery", "2.17.0" end diff --git a/Gemfile.lock b/Gemfile.lock index 46e679250..d702ad0e2 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -637,8 +637,6 @@ GEM rails-assets-jquery.ui (~> 1.11.4) rails-assets-favico.js (0.3.10) rails-assets-highlightjs (9.1.0) - rails-assets-jakobmattsson--jquery-elastic (1.6.11) - rails-assets-jquery (>= 1.2.6) rails-assets-jasmine (2.4.1) rails-assets-jasmine-ajax (3.2.0) rails-assets-jasmine (~> 2) @@ -989,7 +987,6 @@ DEPENDENCIES rails-assets-blueimp-gallery (= 2.17.0)! rails-assets-diaspora_jsxc (~> 0.1.5.develop)! rails-assets-highlightjs (= 9.1.0)! - rails-assets-jakobmattsson--jquery-elastic (= 1.6.11)! rails-assets-jasmine-ajax (= 3.2.0)! rails-assets-jeresig--jquery.hotkeys (= 0.2.0)! rails-assets-jquery (= 1.12.0)! diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 2a2ef7c1d..47618526d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -13,7 +13,6 @@ //= require jquery-placeholder //= require rails-timeago //= require jquery.events.input -//= require jakobmattsson-jquery-elastic //= require jquery.infinitescroll-custom //= require jquery-ui/core //= require jquery-ui/widget From f06ff01c9e5e30e2554707c011253bbb8fc53871 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 26 Feb 2016 09:39:23 +0100 Subject: [PATCH 08/10] Remove jquery.hotkeys --- Gemfile | 1 - Gemfile.lock | 3 --- app/assets/javascripts/main.js | 1 - 3 files changed, 5 deletions(-) diff --git a/Gemfile b/Gemfile index 2d65b08be..79a6eb5a8 100644 --- a/Gemfile +++ b/Gemfile @@ -108,7 +108,6 @@ source "https://rails-assets.org" do # jQuery plugins - gem "rails-assets-jeresig--jquery.hotkeys", "0.2.0" gem "rails-assets-jquery-placeholder", "2.3.1" gem "rails-assets-jquery-textchange", "0.2.3" gem "rails-assets-perfect-scrollbar", "0.6.10" diff --git a/Gemfile.lock b/Gemfile.lock index d702ad0e2..6baa27335 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -640,8 +640,6 @@ GEM rails-assets-jasmine (2.4.1) rails-assets-jasmine-ajax (3.2.0) rails-assets-jasmine (~> 2) - rails-assets-jeresig--jquery.hotkeys (0.2.0) - rails-assets-jquery (>= 1.4.2) rails-assets-jquery (1.12.0) rails-assets-jquery-colorbox (1.6.3) rails-assets-jquery (>= 1.3.2) @@ -988,7 +986,6 @@ DEPENDENCIES rails-assets-diaspora_jsxc (~> 0.1.5.develop)! rails-assets-highlightjs (= 9.1.0)! rails-assets-jasmine-ajax (= 3.2.0)! - rails-assets-jeresig--jquery.hotkeys (= 0.2.0)! rails-assets-jquery (= 1.12.0)! rails-assets-jquery-placeholder (= 2.3.1)! rails-assets-jquery-textchange (= 0.2.3)! diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 47618526d..ba2bb5e3d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -6,7 +6,6 @@ //= require js-routes //= require underscore //= require backbone -//= require jquery.hotkeys //= require jquery.remotipart //= require autosize //= require jquery.charcount From bf17205c1742ab95c51ef7ed59d142863f4b97f4 Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 26 Feb 2016 10:31:28 +0100 Subject: [PATCH 09/10] Fix some publisher.scss scss-lint errors --- app/assets/stylesheets/publisher.scss | 127 ++++++++++++----------- app/views/publisher/_publisher.html.haml | 12 +-- spec/integration/profile_spec.rb | 12 +-- 3 files changed, 79 insertions(+), 72 deletions(-) diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss index f5b874445..663d45d38 100644 --- a/app/assets/stylesheets/publisher.scss +++ b/app/assets/stylesheets/publisher.scss @@ -123,24 +123,6 @@ } } - &:not(.with-location) .location-container { display: none; } - - &.with-location .loader { - height: 20px; - width: 20px; - } - &.with-location .location-container { - height: 30px; - margin-bottom: 0; - border-top: 1px dashed $border-grey; - input[type='text'] { - border: none; - color: $text-grey; - height: 20px; - margin-bottom: 0; - padding: 0; - } - } &.active #button_container { border-top: 1px solid $border-grey; } @@ -208,48 +190,6 @@ text-align: center; } - #publisher-images { - margin-right: 5px; - #file-upload, - #locator, - #poll_creator, - #hide_location { - text-decoration: none !important; - font-size: 16px; - line-height: $line-height-computed; - padding: 4px 2px; - i { - color: $text-grey; - } - &:hover{ - i { color: black; } - } - input[type='file'] { - cursor: pointer; - &::-webkit-file-upload-button { - cursor: pointer; - } - } - } - #hide_location { - display: none; - } - } - &.with-location #publisher-images { - #hide_location { display: inline-block; } - #locator { display: none; } - } - - .counter { - height: 30px; - line-height: 30px; - position: absolute; - right: 10px; - bottom: -25px; - } - &.with-location .counter { - bottom: -62px; - } .warning { color: orange; } @@ -268,3 +208,70 @@ } } } + +.publisher-textarea-wrapper { + &:not(.with-location) .location-container { display: none; } + + &.with-location .loader { + height: 20px; + width: 20px; + } + + &.with-location .location-container { + border-top: 1px dashed $border-grey; + height: 30px; + margin-bottom: 0; + + [type='text'] { + border: 0; + color: $text-grey; + height: 20px; + margin-bottom: 0; + padding: 0; + } + } + + &.with-location .counter { + bottom: -62px; + } + + .counter { + bottom: -25px; + height: 30px; + line-height: 30px; + position: absolute; + right: 10px; + } + + &:not(.with-location) .publisher-buttonbar { + .hide-location { display: none; } + .locator { display: inline-block; } + } + + &.with-location .publisher-buttonbar { + .hide-location { display: inline-block; } + .locator { display: none; } + } +} + +.publisher-buttonbar { + float: right; + margin-right: 5px; + + .btn.btn-link { + font-size: 16px; + line-height: $line-height-computed; + padding: 4px 2px; + text-decoration: none; + i { color: $text-grey; } + + [type='file'], + [type='file']::-webkit-file-upload-button { + cursor: pointer; + } + } + + .btn.btn-link:hover { + i { color: $black; } + } +} diff --git a/app/views/publisher/_publisher.html.haml b/app/views/publisher/_publisher.html.haml index b8ccdb6c1..3c4339e75 100644 --- a/app/views/publisher/_publisher.html.haml +++ b/app/views/publisher/_publisher.html.haml @@ -9,7 +9,7 @@ = form_for(StatusMessage.new) do |status| = status.error_messages %params - #publisher_textarea_wrapper + .publisher-textarea-wrapper#publisher_textarea_wrapper - if current_user.getting_started? = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", @@ -29,14 +29,14 @@ #poll_creator_container -# handlebars template #button_container - .pull-right#publisher-images - .btn.btn-link#poll_creator{title: t("shared.publisher.poll.add_a_poll")} + .publisher-buttonbar#publisher-images + .btn.btn-link.poll-creator#poll_creator{title: t("shared.publisher.poll.add_a_poll")} %i.entypo-bar-graph - .btn.btn-link#file-upload{title: t("shared.publisher.upload_photos")} + .btn.btn-link.file-upload#file-upload{title: t("shared.publisher.upload_photos")} %i.entypo-camera.publisher_image - .btn.btn-link#locator{title: t("shared.publisher.get_location")} + .btn.btn-link.locator#locator{title: t("shared.publisher.get_location")} %i.entypo-location.publisher_image - .btn.btn-link#hide_location{title: t("shared.publisher.remove_location")} + .btn.btn-link.hide-location#hide_location{title: t("shared.publisher.remove_location")} %i.entypo-cross.publisher_image %span.markdownIndications != t("shared.publisher.formatWithMarkdown", markdown_link: link_to(t("help.markdown"), diff --git a/spec/integration/profile_spec.rb b/spec/integration/profile_spec.rb index 3428c11d1..4884e530a 100644 --- a/spec/integration/profile_spec.rb +++ b/spec/integration/profile_spec.rb @@ -13,7 +13,7 @@ describe PeopleController, type: :request do expect(response.status).to eq(200) # make sure we are signed in expect(response.body).not_to match(/a class="login"/) - expect(response.body).to match(/div id='publisher_textarea_wrapper'/) + expect(response.body).to match(/div class='publisher-textarea-wrapper' id='publisher_textarea_wrapper'/) end it "displays the publisher for people path" do @@ -22,7 +22,7 @@ describe PeopleController, type: :request do expect(response.status).to eq(200) # make sure we are signed in expect(response.body).not_to match(/a class="login"/) - expect(response.body).to match(/div id='publisher_textarea_wrapper'/) + expect(response.body).to match(/div class='publisher-textarea-wrapper' id='publisher_textarea_wrapper'/) end end @@ -37,7 +37,7 @@ describe PeopleController, type: :request do expect(response.status).to eq(200) # make sure we are signed in expect(response.body).not_to match(/a class="login"/) - expect(response.body).not_to match(/div id='publisher_textarea_wrapper'/) + expect(response.body).not_to match(/div class='publisher-textarea-wrapper' id='publisher_textarea_wrapper'/) end it "doesn't display the publisher for people path" do @@ -46,7 +46,7 @@ describe PeopleController, type: :request do expect(response.status).to eq(200) # make sure we are signed in expect(response.body).not_to match(/a class="login"/) - expect(response.body).not_to match(/div id='publisher_textarea_wrapper'/) + expect(response.body).not_to match(/div class='publisher-textarea-wrapper' id='publisher_textarea_wrapper'/) end end @@ -57,7 +57,7 @@ describe PeopleController, type: :request do expect(response.status).to eq(200) # make sure we aren't signed in expect(response.body).to match(/a class="login"/) - expect(response.body).not_to match(/div id='publisher_textarea_wrapper'/) + expect(response.body).not_to match(/div class='publisher-textarea-wrapper' id='publisher_textarea_wrapper'/) end it "doesn't display the publisher for people path" do @@ -66,7 +66,7 @@ describe PeopleController, type: :request do expect(response.status).to eq(200) # make sure we aren't signed in expect(response.body).to match(/a class="login"/) - expect(response.body).not_to match(/div id='publisher_textarea_wrapper'/) + expect(response.body).not_to match(/div class='publisher-textarea-wrapper' id='publisher_textarea_wrapper'/) end end end From f556a5210eb753d55abfd452f9a0d4b004badc5d Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 26 Feb 2016 22:03:50 +0100 Subject: [PATCH 10/10] Refactor js search views --- app/assets/javascripts/app/router.js | 2 +- .../app/views/publisher/mention_view.js | 388 ++++-------- .../javascripts/app/views/publisher_view.js | 14 +- .../javascripts/app/views/search_base_view.js | 121 ++-- .../javascripts/app/views/search_view.js | 37 +- app/assets/javascripts/app/views/tags_view.js | 2 +- app/assets/stylesheets/mentions.scss | 12 +- app/assets/stylesheets/publisher.scss | 5 + app/assets/stylesheets/typeahead.scss | 7 +- app/views/publisher/_publisher.html.haml | 24 +- spec/javascripts/app/router_spec.js | 2 +- .../app/views/publisher_mention_view_spec.js | 591 +++++++++++------- .../app/views/search_base_view_spec.js | 267 +++++--- .../javascripts/app/views/search_view_spec.js | 66 +- 14 files changed, 772 insertions(+), 766 deletions(-) diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 312afbdae..8f65548aa 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -181,7 +181,7 @@ app.Router = Backbone.Router.extend({ app.page = new app.views.Stream({model : app.stream}); app.shortcuts = app.shortcuts || new app.views.StreamShortcuts({el: $(document)}); - if($("#publisher").length !== 0){ + if($("#publisher").length !== 0) { app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items}); } diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js index 9f3859b67..f19ec7d1e 100644 --- a/app/assets/javascripts/app/views/publisher/mention_view.js +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -1,196 +1,122 @@ //= require ../search_base_view -/* - * This file is based on jQuery.mentionsInput by Kenneth Auchenberg - * licensed under MIT License - http://www.opensource.org/licenses/mit-license.php - * Website: https://podio.github.io/jquery-mentions-input/ - */ - app.views.PublisherMention = app.views.SearchBase.extend({ - settings: { - triggerChar: "@", - minChars: 2, - templates: { - wrapper: _.template("
      "), - mentionsOverlay: _.template("
      "), - mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"), - mentionItemHighlight: _.template("<%= name %>") - } + triggerChar: "@", + invisibleChar: "\u200B", // zero width space + mentionRegex: /@([^@\s]+)$/, + + templates: { + mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"), + mentionItemHighlight: _.template("<%= name %>") }, events: { "keydown #status_message_fake_text": "onInputBoxKeyDown", - "keypress #status_message_fake_text": "onInputBoxKeyPress", "input #status_message_fake_text": "onInputBoxInput", "click #status_message_fake_text": "onInputBoxClick", "blur #status_message_fake_text": "onInputBoxBlur", - "paste #status_message_fake_text": "onInputBoxPaste" }, - /** - * Performs setup of the setup of the plugin. - * - * this.mentionsCollection: used to keep track of the people mentionned in the post - * this.inputBuffer: buffer to keep track of the text currently being typed. It is cleared - * each time a mention has been processed. - * See this#onInputBoxKeyPress - * this.currentDataQuery: contains the query for the search engine - * - * The plugin initilizes two different elements that will contain the text of the post: - * - * this.elmInputBox: hidden element which keeps track of typed text formatted following - * the mentioning syntax given by this.settings.templates#mentionItemSyntax - * For instance, if the user writes the text "Hello @user1", the resulting hidden - * text will be: "Hello @{user1 ; user1@pod.tld}. This is the text that is submitted - * to the pod when the user posts. - * this.elmMentionsOverlay: contains the text that will be displayed to the user - * - * this.mentionChar is a invisible caracter used to mark the name of the mentionned person - * during the process. See this#processMention - */ - initialize: function(){ - this.mentionsCollection = []; - this.inputBuffer = []; - this.currentDataQuery = ""; - this.mentionChar = "\u200B"; + initialize: function() { + this.mentionedPeople = []; - this.elmInputBox = this.$el.find("#status_message_fake_text"); - var elmInputWrapper = this.elmInputBox.parent(); - this.elmInputBox.wrapAll($(this.settings.templates.wrapper())); - var elmWrapperBox = elmInputWrapper.find("> div").first(); - this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay()); - this.elmMentionsOverlay.prependTo(elmWrapperBox); + // contains the 'fake text' displayed to the user + // also has a data-messageText attribute with the original text + this.inputBox = this.$("#status_message_fake_text"); + // contains the mentions displayed to the user + this.mentionsBox = this.$(".mentions-box"); + this.typeaheadInput = this.$(".typeahead-mention-box"); + this.bindTypeaheadEvents(); - this.bindMentioningEvents(); - app.views.SearchBase.prototype.initialize.call(this, {typeaheadElement: this.getTypeaheadInput()}); - - this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"}); - this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0); + app.views.SearchBase.prototype.initialize.call(this, { + typeaheadInput: this.typeaheadInput, + customSearch: true, + autoselect: true + }); }, - /** - * Attach events to Typeahead. - */ - bindMentioningEvents: function(){ + bindTypeaheadEvents: function() { var self = this; // Process mention when the user selects a result. - this.getTypeaheadInput().on("typeahead:select", function(evt, datum){ - self.processMention(datum); - self.resetMentionBox(); - self.addToFilteredResults(datum); - }); - - // Highlight the first result when the results dropdown opens - this.getTypeaheadInput().on("typeahead:render", function(){ - self.select(self.$(".tt-menu .tt-suggestion").first()); - }); + this.typeaheadInput.on("typeahead:select", function(evt, person) { self.onSuggestionSelection(person); }); }, - clearBuffer: function(){ - this.inputBuffer.length = 0; - }, - - /** - * Cleans the collection of mentionned people. Rejects every item who's name - * is not present in the post an falsy values (false, null, "", etc.) - */ - updateMentionsCollection: function(){ - var inputText = this.getInputBoxValue(); - - this.mentionsCollection = _.reject(this.mentionsCollection, function(mention){ - return !mention.name || inputText.indexOf(mention.name) === -1; - }); - this.mentionsCollection = _.compact(this.mentionsCollection); - }, - - /** - * Adds mention to the mention collection - * @param person Mentionned person. - * JSON object of form { handle: , name: , ... } - */ - addMention: function(person){ - if(!person || !person.name || !person.handle){ - return; - } + addPersonToMentions: function(person) { + if(!(person && person.name && person.handle)) { return; } // This is needed for processing preview /* jshint camelcase: false */ person.diaspora_id = person.handle; /* jshint camelcase: true */ - this.mentionsCollection.push(person); + this.mentionedPeople.push(person); + this.ignorePersonForSuggestions(person); + }, + + cleanMentionedPeople: function() { + var inputText = this.inputBox.val(); + this.mentionedPeople = this.mentionedPeople.filter(function(person) { + return person.name && inputText.indexOf(person.name) > -1; + }); + this.ignoreDiasporaIds = this.mentionedPeople.map(function(person) { return person.handle; }); + }, + + onSuggestionSelection: function(person) { + var messageText = this.inputBox.val(); + var caretPosition = this.inputBox[0].selectionStart; + var triggerCharPosition = messageText.lastIndexOf(this.triggerChar, caretPosition); + + if(triggerCharPosition === -1) { return; } + + this.addPersonToMentions(person); + this.closeSuggestions(); + + messageText = messageText.substring(0, triggerCharPosition) + + this.invisibleChar + person.name + messageText.substring(caretPosition); + + this.inputBox.val(messageText); + this.updateMessageTexts(); + + this.inputBox.focus(); + var newCaretPosition = triggerCharPosition + person.name.length + 1; + this.inputBox[0].setSelectionRange(newCaretPosition, newCaretPosition); }, /** - * Process the text to add mention to the post. Every @mention in the text - * will be replaced by this.mentionChar + mention.name. This temporary text - * will then be replaced by final syntax in this#updateValues - * - * For instance if the user types text "Hello @use" and selects result user1, - * The text will be transformed to "Hello \u200Buser1" before calling this#updateValues - * - * @param mention Mentionned person. - * JSON object of form { handle: , name: , ... } - */ - processMention: function(mention){ - var currentMessage = this.getInputBoxValue(); - - var currentCaretPosition = this.getCaretPosition(); - var startCaretPosition = currentCaretPosition - (this.currentDataQuery.length + 1); - - // Extracts the text before the mention and the text after it. - // startEndIndex is the position where to place the caret at the en of the process - var start = currentMessage.substr(0, startCaretPosition); - var end = currentMessage.substr(currentCaretPosition, currentMessage.length); - var startEndIndex = (start + mention.name).length + 1; - - this.addMention(mention); - - // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer - this.clearBuffer(); - this.currentDataQuery = ""; - this.resetMentionBox(); - - // Autocompletes mention and updates message text - var updatedMessageText = start + this.mentionChar + mention.name + end; - this.elmInputBox.val(updatedMessageText); - this.updateValues(); - - // Set correct focus and caret position - this.elmInputBox.focus(); - this.setCaretPosition(startEndIndex); - }, - - /** - * Replaces every combination of this.mentionChar + mention.name by the + * Replaces every combination of this.invisibleChar + mention.name by the * correct syntax for both hidden text and visible one. * * For instance, the text "Hello \u200Buser1" will be tranformed to * "Hello @{user1 ; user1@pod.tld}" in the hidden element and * "Hello user1" in the element visible to the user. */ - updateValues: function(){ - var syntaxMessage = this.getInputBoxValue(); - var mentionText = this.getInputBoxValue(); - this.clearFilteredResults(); + updateMessageTexts: function() { + var fakeMessageText = this.inputBox.val(), + mentionBoxText = fakeMessageText, + messageText = fakeMessageText; - var self = this; + this.mentionedPeople.forEach(function(person) { + var mentionName = this.invisibleChar + person.name; + messageText = messageText.replace(mentionName, this.templates.mentionItemSyntax(person)); + var textHighlight = this.templates.mentionItemHighlight({name: _.escape(person.name)}); + mentionBoxText = mentionBoxText.replace(mentionName, textHighlight); + }, this); - _.each(this.mentionsCollection, function(mention){ - self.addToFilteredResults(mention); + this.inputBox.data("messageText", messageText); + this.mentionsBox.find(".mentions").html(mentionBoxText); + }, - var mentionVal = self.mentionChar + mention.name; + updateTypeaheadInput: function() { + var messageText = this.inputBox.val(); + var caretPosition = this.inputBox[0].selectionStart; + var result = this.mentionRegex.exec(messageText.substring(0,caretPosition)); - var textSyntax = self.settings.templates.mentionItemSyntax(mention); - syntaxMessage = syntaxMessage.replace(mentionVal, textSyntax); + if(result === null) { + this.closeSuggestions(); + return; + } - var textHighlight = self.settings.templates.mentionItemHighlight({name: _.escape(mention.name)}); - mentionText = mentionText.replace(mentionVal, textHighlight); - }); - - mentionText = mentionText.replace(/\n/g, "
      "); - mentionText = mentionText.replace(/ {2}/g, "  "); - - this.elmInputBox.data("messageText", syntaxMessage); - this.elmMentionsOverlay.find("div > div").html(mentionText); + // result[1] is the string between the last '@' and the current caret position + this.typeaheadInput.typeahead("val", result[1]); + this.typeaheadInput.typeahead("open"); }, /** @@ -198,99 +124,69 @@ app.views.PublisherMention = app.views.SearchBase.extend({ * @param persons List of people to mention in a post; * JSON object of form { handle: , name: , ... } */ - prefillMention: function(persons){ - var self = this; - _.each(persons, function(person){ - self.addMention(person); - self.addToFilteredResults(person); - var text = self.mentionChar + person.name; - if(self.elmInputBox.val().length !== 0){ - text = self.elmInputBox.val() + " " + text; + prefillMention: function(persons) { + persons.forEach(function(person) { + this.addPersonToMentions(person); + var text = this.invisibleChar + person.name; + if(this.inputBox.val().length !== 0) { + text = this.inputBox.val() + " " + text; } - self.elmInputBox.val(text); - self.updateValues(); - }); + this.inputBox.val(text); + this.updateMessageTexts(); + }, this); }, /** * Selects next or previous result when result dropdown is open and * user press up and down arrows. */ - onArrowKeysPress: function(e){ - if(!this.isVisible() || (e.which !== Keycodes.UP && e.which !== Keycodes.DOWN)){ + onArrowKeyDown: function(e) { + if(!this.isVisible() || (e.which !== Keycodes.UP && e.which !== Keycodes.DOWN)) { return; } e.preventDefault(); e.stopPropagation(); - this.getTypeaheadInput().typeahead("activate"); - this.getTypeaheadInput().typeahead("open"); - this.getTypeaheadInput().trigger($.Event("keydown", {keyCode: e.keyCode, which: e.which})); - }, - - onInputBoxKeyPress: function(e){ - // Excluding ctrl+v from key press event in firefox - if(!((String.fromCharCode(e.which).toLowerCase() === "v" && e.ctrlKey) || (e.which === Keycodes.BACKSPACE))){ - var typedValue = String.fromCharCode(e.which || e.keyCode); - this.inputBuffer.push(typedValue); - } + this.typeaheadInput.typeahead("activate"); + this.typeaheadInput.typeahead("open"); + this.typeaheadInput.trigger($.Event("keydown", {keyCode: e.keyCode, which: e.which})); }, /** * Listens for user input and opens results dropdown when input contains the trigger char */ - onInputBoxInput: function(){ - this.updateValues(); - this.updateMentionsCollection(); - - var triggerCharIndex = _.lastIndexOf(this.inputBuffer, this.settings.triggerChar); - if(triggerCharIndex > -1){ - this.currentDataQuery = this.inputBuffer.slice(triggerCharIndex + 1).join(""); - this.currentDataQuery = this.rtrim(this.currentDataQuery); - - this.showMentionBox(); - } + onInputBoxInput: function() { + this.cleanMentionedPeople(); + this.updateMessageTexts(); + this.updateTypeaheadInput(); }, - onInputBoxKeyDown: function(e){ + onInputBoxKeyDown: function(e) { // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT if(e.which === Keycodes.LEFT || e.which === Keycodes.RIGHT || - e.which === Keycodes.HOME || e.which === Keycodes.END){ - _.defer(this.clearBuffer); - - // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting - // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack - // to force updateValues() to fire when backspace/delete is pressed in IE9. - if(navigator.userAgent.indexOf("MSIE 9") > -1){ - _.defer(this.updateValues); - } - + e.which === Keycodes.HOME || e.which === Keycodes.END) { + _.defer(_.bind(this.updateTypeaheadInput, this)); return; } - if(e.which === Keycodes.BACKSPACE){ - this.inputBuffer = this.inputBuffer.slice(0, this.inputBuffer.length - 1); - return; - } - - if(!this.isVisible){ + if(!this.isVisible) { return true; } - switch(e.which){ + switch(e.which) { case Keycodes.ESC: case Keycodes.SPACE: - this.resetMentionBox(); + this.closeSuggestions(); break; case Keycodes.UP: case Keycodes.DOWN: - this.onArrowKeysPress(e); + this.onArrowKeyDown(e); break; case Keycodes.RETURN: case Keycodes.TAB: - if(this.getSelected().size() === 1){ - this.getSelected().click(); + if(this.$(".tt-cursor").length === 1) { + this.$(".tt-cursor").click(); return false; } break; @@ -298,69 +194,29 @@ app.views.PublisherMention = app.views.SearchBase.extend({ return true; }, - onInputBoxClick: function(){ - this.resetMentionBox(); + onInputBoxClick: function() { + this.updateTypeaheadInput(); }, - onInputBoxBlur: function(){ - this.resetMentionBox(); + onInputBoxBlur: function() { + this.closeSuggestions(); }, - onInputBoxPaste: function(evt){ - var pastedData = evt.originalEvent.clipboardData.getData("text/plain"); - var dataArray = pastedData.split(""); - var self = this; - _.each(dataArray, function(value){ - self.inputBuffer.push(value); - }); + reset: function() { + this.inputBox.val(""); + this.onInputBoxInput(); }, - reset: function(){ - this.elmInputBox.val(""); - this.mentionsCollection.length = 0; - this.clearFilteredResults(); - this.updateValues(); + closeSuggestions: function() { + this.typeaheadInput.typeahead("val", ""); + this.typeaheadInput.typeahead("close"); }, - showMentionBox: function(){ - this.getTypeaheadInput().typeahead("val", this.currentDataQuery); - this.getTypeaheadInput().typeahead("open"); + isVisible: function() { + return this.$(".tt-menu").is(":visible"); }, - resetMentionBox: function(){ - this.getTypeaheadInput().typeahead("val", ""); - this.getTypeaheadInput().typeahead("close"); - }, - - getInputBoxValue: function(){ - return $.trim(this.elmInputBox.val()); - }, - - isVisible: function(){ - return this.$el.find(".tt-menu").is(":visible"); - }, - - getTypeaheadInput: function(){ - if(this.$el.find(".typeahead-mention-box").length === 0){ - this.elmInputBox.after(""); - } - return this.$el.find(".typeahead-mention-box"); - }, - - getTextForSubmit: function(){ - return this.mentionsCollection.length ? this.elmInputBox.data("messageText") : this.getInputBoxValue(); - }, - - setCaretPosition: function(caretPos){ - this.elmInputBox[0].focus(); - this.elmInputBox[0].setSelectionRange(caretPos, caretPos); - }, - - getCaretPosition: function(){ - return this.elmInputBox[0].selectionStart; - }, - - rtrim: function(string){ - return string.replace(/\s+$/, ""); + getTextForSubmit: function() { + return this.mentionedPeople.length ? this.inputBox.data("messageText") : this.inputBox.val(); } }); diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index d5b562f36..08c5df881 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -5,9 +5,11 @@ * the COPYRIGHT file. */ -//= require ./publisher/services_view //= require ./publisher/aspect_selector_view //= require ./publisher/getting_started_view +//= require ./publisher/mention_view +//= require ./publisher/poll_creator_view +//= require ./publisher/services_view //= require ./publisher/uploader_view //= require jquery-textchange @@ -99,7 +101,7 @@ app.views.Publisher = Backbone.View.extend({ initSubviews: function() { this.mention = new app.views.PublisherMention({ el: this.$("#publisher_textarea_wrapper") }); - if(this.prefillMention){ + if(this.prefillMention) { this.mention.prefillMention([this.prefillMention]); } @@ -299,7 +301,7 @@ app.views.Publisher = Backbone.View.extend({ var serializedForm = $(evt.target).closest("form").serializeObject(); var photos = this.getUploadedPhotos(); - var mentionedPeople = this.mention.mentionsCollection; + var mentionedPeople = this.mention.mentionedPeople; var date = (new Date()).toISOString(); var poll = this.getPollData(serializedForm); var locationCoords = serializedForm["location[coords]"]; @@ -365,6 +367,9 @@ app.views.Publisher = Backbone.View.extend({ }, clear : function() { + // remove mentions + this.mention.reset(); + // clear text(s) this.inputEl.val(""); this.hiddenInputEl.val(""); @@ -372,9 +377,6 @@ app.views.Publisher = Backbone.View.extend({ .trigger("keydown"); autosize.update(this.inputEl); - // remove mentions - this.mention.reset(); - // remove photos this.photozoneEl.find("li").remove(); this.$("input[name='photos[]']").remove(); diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js index 6b2dfae29..f194af7e1 100644 --- a/app/assets/javascripts/app/views/search_base_view.js +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -1,15 +1,17 @@ app.views.SearchBase = app.views.Base.extend({ - initialize: function(options){ - this.typeaheadElement = $(options.typeaheadElement); - this.setupBloodhound(); + initialize: function(options) { + this.ignoreDiasporaIds = []; + this.typeaheadInput = options.typeaheadInput; + this.setupBloodhound(options); + if(options.customSearch) { this.setupCustomSearch(); } this.setupTypeahead(); - this.bindSelectionEvents(); - this.resultsTofilter = []; + // TODO: Remove this as soon as corejavascript/typeahead.js has its first release + this.setupMouseSelectionEvents(); + if(options.autoselect) { this.setupAutoselect(); } }, - setupBloodhound: function() { - var self = this; - var bloodhoundConf = { + setupBloodhound: function(options) { + var bloodhoundOptions = { datumTokenizer: function(datum) { var nameTokens = Bloodhound.tokenizers.nonword(datum.name); var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : []; @@ -24,32 +26,25 @@ app.views.SearchBase = app.views.Base.extend({ sufficient: 5 }; - // The publisher does not define an additionnal source for searchin - // This prevents tests from failing when this additionnal source isn't set - if(this.searchFormAction !== undefined){ - bloodhoundConf.remote = { - url: this.searchFormAction + ".json?q=%QUERY", + // Allow bloodhound to look for remote results if there is a route given in the options + if(options.remoteRoute) { + bloodhoundOptions.remote = { + url: options.remoteRoute + ".json?q=%QUERY", wildcard: "%QUERY", transform: this.transformBloodhoundResponse }; } - this.bloodhound = new Bloodhound(bloodhoundConf); + this.bloodhound = new Bloodhound(bloodhoundOptions); + }, - /** - * Custom searching function that let us filter contacts from prefetched Bloodhound results. - */ - this.bloodhound.customSearch = function(query, sync, async){ - var filterResults = function(datums){ - return _.filter(datums, function(result){ - if(result.handle){ - return !_.contains(self.resultsTofilter, result.handle); - } + setupCustomSearch: function() { + var self = this; + this.bloodhound.customSearch = function(query, sync, async) { + var _sync = function(datums) { + var results = datums.filter(function(datum) { + return datum.handle !== undefined && self.ignoreDiasporaIds.indexOf(datum.handle) === -1; }); - }; - - var _sync = function(datums){ - var results = filterResults(datums); sync(results); }; @@ -58,7 +53,7 @@ app.views.SearchBase = app.views.Base.extend({ }, setupTypeahead: function() { - this.typeaheadElement.typeahead({ + this.typeaheadInput.typeahead({ hint: false, highlight: true, minLength: 2 @@ -67,7 +62,7 @@ app.views.SearchBase = app.views.Base.extend({ name: "search", display: "name", limit: 5, - source: this.searchFormAction !== undefined ? this.bloodhound : this.bloodhound.customSearch, + source: this.bloodhound.customSearch !== undefined ? this.bloodhound.customSearch : this.bloodhound, templates: { /* jshint camelcase: false */ suggestion: HandlebarsTemplates.search_suggestion_tpl @@ -77,9 +72,9 @@ app.views.SearchBase = app.views.Base.extend({ }, transformBloodhoundResponse: function(response) { - return response.map(function(data){ + return response.map(function(data) { // person - if(data.handle){ + if(data.handle) { data.person = true; return data; } @@ -93,49 +88,37 @@ app.views.SearchBase = app.views.Base.extend({ }); }, - /** - * This bind events to highlight a result when overing it - */ - bindSelectionEvents: function(){ - var self = this; - var onover = function(suggestion){ - return function(){ - self.select(suggestion); - }; - }; + _deselectAllSuggestions: function() { + this.$(".tt-suggestion").removeClass("tt-cursor"); + }, - this.typeaheadElement.on("typeahead:render", function(){ - self.$(".tt-menu *").off("mouseover"); - self.$(".tt-menu .tt-suggestion").each(function(){ - var $suggestion = $(this); - $suggestion.on("mouseover", onover($suggestion)); - $suggestion.find("*").on("mouseover", onover($suggestion)); - }); + _selectSuggestion: function(suggestion) { + this._deselectAllSuggestions(); + suggestion.addClass("tt-cursor"); + }, + + // TODO: Remove this as soon as corejavascript/typeahead.js has its first release + setupMouseSelectionEvents: function() { + var self = this, + selectSuggestion = function(e) { self._selectSuggestion($(e.target).closest(".tt-suggestion")); }, + deselectAllSuggestions = function() { self._deselectAllSuggestions(); }; + + this.typeaheadInput.on("typeahead:render", function() { + self.$(".tt-menu .tt-suggestion").off("mouseover").on("mouseover", selectSuggestion); + self.$(".tt-menu .tt-suggestion *").off("mouseover").on("mouseover", selectSuggestion); + self.$(".tt-menu .tt-suggestion").off("mouseleave").on("mouseleave", deselectAllSuggestions); }); }, - /** - * This function lets us filter contacts from Bloodhound's responses - * It is used by app.views.PublisherMention to filter already mentionned - * people in post. Does not filter tags from results. - * @param person a JSON object of form { handle: , ... } representing the filtered contact - */ - addToFilteredResults: function(person){ - if(person.handle){ - this.resultsTofilter.push(person.handle); - } + // Selects the first result when the result dropdown opens + setupAutoselect: function() { + var self = this; + this.typeaheadInput.on("typeahead:render", function() { + self._selectSuggestion(self.$(".tt-menu .tt-suggestion").first()); + }); }, - clearFilteredResults: function(){ - this.resultsTofilter.length = 0; + ignorePersonForSuggestions: function(person) { + if(person.handle) { this.ignoreDiasporaIds.push(person.handle); } }, - - getSelected: function(){ - return this.$el.find(".tt-cursor"); - }, - - select: function(el){ - this.getSelected().removeClass("tt-cursor"); - $(el).addClass("tt-cursor"); - } }); diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index b45faeb96..254bcb071 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -6,46 +6,29 @@ app.views.Search = app.views.SearchBase.extend({ "keypress #q": "inputKeypress" }, - initialize: function(){ - this.searchFormAction = this.$el.attr("action"); - app.views.SearchBase.prototype.initialize.call(this, {typeaheadElement: this.getTypeaheadElement()}); - this.bindMoreSelectionEvents(); - this.getTypeaheadElement().on("typeahead:select", this.suggestionSelected); - }, - - /** - * This bind events to unselect all results when leaving the menu - */ - bindMoreSelectionEvents: function(){ - var self = this; - var onleave = function(){ - self.$(".tt-cursor").removeClass("tt-cursor"); - }; - - this.getTypeaheadElement().on("typeahead:render", function(){ - self.$(".tt-menu").off("mouseleave", onleave); - self.$(".tt-menu").on("mouseleave", onleave); + initialize: function() { + this.searchInput = this.$("#q"); + app.views.SearchBase.prototype.initialize.call(this, { + typeaheadInput: this.searchInput, + remoteRoute: this.$el.attr("action") }); + this.searchInput.on("typeahead:select", this.suggestionSelected); }, - getTypeaheadElement: function(){ - return this.$("#q"); - }, - - toggleSearchActive: function(evt){ + toggleSearchActive: function(evt) { // jQuery produces two events for focus/blur (for bubbling) // don't rely on which event arrives first, by allowing for both variants var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1); $(evt.target).toggleClass("active", isActive); }, - inputKeypress: function(evt){ - if(evt.which === Keycodes.ENTER && $(".tt-suggestion.tt-cursor").length === 0){ + inputKeypress: function(evt) { + if(evt.which === Keycodes.ENTER && $(".tt-suggestion.tt-cursor").length === 0) { $(evt.target).closest("form").submit(); } }, - suggestionSelected: function(evt, datum){ + suggestionSelected: function(evt, datum) { window.location = datum.url; } }); diff --git a/app/assets/javascripts/app/views/tags_view.js b/app/assets/javascripts/app/views/tags_view.js index 563050b3c..3913f8798 100644 --- a/app/assets/javascripts/app/views/tags_view.js +++ b/app/assets/javascripts/app/views/tags_view.js @@ -2,7 +2,7 @@ app.views.Tags = Backbone.View.extend({ initialize: function(opts) { - if(app.publisher){ + if(app.publisher) { app.publisher.setText("#"+ opts.hashtagName + " "); } } diff --git a/app/assets/stylesheets/mentions.scss b/app/assets/stylesheets/mentions.scss index fc25ebf28..33deeb978 100644 --- a/app/assets/stylesheets/mentions.scss +++ b/app/assets/stylesheets/mentions.scss @@ -82,15 +82,9 @@ white-space: pre-wrap; word-wrap: break-word; - > div { - color: white; - white-space: pre-wrap; - width: 100%; - - strong { - background: #d8dfea; - font-weight: normal; - } + > strong { + background: $background-blue; + font-weight: normal; } } } diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss index 663d45d38..d2897dfa1 100644 --- a/app/assets/stylesheets/publisher.scss +++ b/app/assets/stylesheets/publisher.scss @@ -252,6 +252,11 @@ .hide-location { display: inline-block; } .locator { display: none; } } + + .twitter-typeahead { + left: -1px; + position: absolute; + } } .publisher-buttonbar { diff --git a/app/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss index ed4405c01..7f427c097 100644 --- a/app/assets/stylesheets/typeahead.scss +++ b/app/assets/stylesheets/typeahead.scss @@ -1,10 +1,13 @@ .tt-menu { - width: 300px; - margin-top: ($navbar-height - $input-height-small) / 2; background-color: $navbar-inverse-bg; box-shadow: 0 5px 10px rgba(0,0,0,.2); } +.navbar.navbar-fixed-top .tt-menu { + margin-top: ($navbar-height - $input-height-small) / 2; + width: 300px; +} + .tt-suggestion { border-top: 1px solid $gray-dark; color: $white; diff --git a/app/views/publisher/_publisher.html.haml b/app/views/publisher/_publisher.html.haml index 3c4339e75..70d386ce5 100644 --- a/app/views/publisher/_publisher.html.haml +++ b/app/views/publisher/_publisher.html.haml @@ -10,16 +10,20 @@ = status.error_messages %params .publisher-textarea-wrapper#publisher_textarea_wrapper - - if current_user.getting_started? - = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), - :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", - "data-title" => popover_with_close_html("1. " + t("shared.public_explain.share")), - "data-content" => t("shared.public_explain.new_user_welcome_message"), - "class" => "form-control" - - else - = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), - :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", - "class" => "form-control" + .mentions-input-box + .mentions-box + .mentions + - if current_user.getting_started? + = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), + :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", + "data-title" => popover_with_close_html("1. " + t("shared.public_explain.share")), + "data-content" => t("shared.public_explain.new_user_welcome_message"), + "class" => "form-control" + - else + = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), + :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", + "class" => "form-control" + %input.typeahead-mention-box.hidden{type: "text"} = status.hidden_field :text, value: h(publisher_hidden_text), class: "clear_on_submit" .container-fluid#photodropzone_container diff --git a/spec/javascripts/app/router_spec.js b/spec/javascripts/app/router_spec.js index a356bedb5..e29a56e58 100644 --- a/spec/javascripts/app/router_spec.js +++ b/spec/javascripts/app/router_spec.js @@ -114,7 +114,7 @@ describe('app.Router', function () { expect(app.publisher.jasmineTestValue).toEqual(42); }); - it("doesn't set app.publisher if there is no publisher element in page", function(){ + it("doesn't set app.publisher if there is no publisher element in page", function() { $("#publisher").remove(); app.router._initializeStreamView(); expect(app.publisher).toBeUndefined(); diff --git a/spec/javascripts/app/views/publisher_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js index f67aad215..68397e36e 100644 --- a/spec/javascripts/app/views/publisher_mention_view_spec.js +++ b/spec/javascripts/app/views/publisher_mention_view_spec.js @@ -1,322 +1,471 @@ -describe("app.views.PublisherMention", function(){ - beforeEach(function(){ - spec.content().html( - "
      " + - "" + - "
      "); +describe("app.views.PublisherMention", function() { + beforeEach(function() { + spec.loadFixture("aspects_index"); }); - describe("initialize", function(){ - beforeEach(function(){ - spyOn(app.views.SearchBase.prototype, "initialize").and.callThrough(); - spyOn(app.views.PublisherMention.prototype, "bindMentioningEvents").and.callThrough(); + describe("initialize", function() { + it("initializes object properties", function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); + expect(this.view.mentionedPeople).toEqual([]); + expect(this.view.invisibleChar).toBe("\u200B"); + expect(this.view.triggerChar).toBe("@"); }); - it("initializes object properties", function(){ - expect(this.view.mentionsCollection).toEqual([]); - expect(this.view.inputBuffer).toEqual([]); - expect(this.view.currentDataQuery).toBe(""); - expect(this.view.mentionChar).toBe("\u200B"); + it("calls app.views.SearchBase.initialize", function() { + spyOn(app.views.SearchBase.prototype, "initialize"); + this.view = new app.views.PublisherMention({ el: "#publisher" }); + expect(app.views.SearchBase.prototype.initialize).toHaveBeenCalled(); + var call = app.views.SearchBase.prototype.initialize.calls.mostRecent(); + expect(call.args[0].typeaheadInput.selector).toBe("#publisher .typeahead-mention-box"); + expect(call.args[0].customSearch).toBeTruthy(); + expect(call.args[0].autoselect).toBeTruthy(); }); - it("calls completeSetup", function(){ - expect(app.views.SearchBase.prototype.initialize) - .toHaveBeenCalledWith({typeaheadElement: this.view.getTypeaheadInput()}); - expect(app.views.PublisherMention.prototype.bindMentioningEvents).toHaveBeenCalled(); - }); - - it("initializes html elements", function(){ - expect(this.view.$(".typeahead-mention-box").length).toBe(1); - expect(this.view.$(".mentions-input-box").length).toBe(1); - expect(this.view.$(".mentions-box").length).toBe(1); - expect(this.view.$(".mentions").length).toBe(1); + it("calls bindTypeaheadEvents", function() { + spyOn(app.views.PublisherMention.prototype, "bindTypeaheadEvents"); + this.view = new app.views.PublisherMention({ el: "#publisher" }); + expect(app.views.PublisherMention.prototype.bindTypeaheadEvents).toHaveBeenCalled(); }); }); - describe("bindMentioningEvents", function(){ - beforeEach(function(){ - spyOn(app.views.PublisherMention.prototype, "processMention"); - spyOn(app.views.PublisherMention.prototype, "resetMentionBox"); - spyOn(app.views.PublisherMention.prototype, "addToFilteredResults"); + describe("bindTypeaheadEvents", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); this.view.bloodhound.add([ - {"person": true, "name":"user1", "handle":"user1@pod.tld"}, - {"person": true, "name":"user2", "handle":"user2@pod.tld"} + {person: true, name: "user1", handle: "user1@pod.tld"}, + {person: true, name: "user2", handle: "user2@pod.tld"} ]); }); - it("highlights the first item when rendering results", function(){ - this.view.getTypeaheadInput().typeahead("val", "user"); - this.view.getTypeaheadInput().typeahead("open"); - expect(this.view.$(".tt-suggestion").first()).toHaveClass("tt-cursor"); - }); - - it("process mention when clicking a result", function(){ - this.view.getTypeaheadInput().typeahead("val", "user"); - this.view.getTypeaheadInput().typeahead("open"); - this.view.$(".tt-suggestion").first().click(); - expect(app.views.PublisherMention.prototype.processMention).toHaveBeenCalled(); - expect(app.views.PublisherMention.prototype.resetMentionBox).toHaveBeenCalled(); - expect(app.views.PublisherMention.prototype.addToFilteredResults).toHaveBeenCalled(); + it("process mention when clicking a result", function() { + spyOn(this.view, "onSuggestionSelection"); + this.view.typeaheadInput.typeahead("val", "user"); + this.view.typeaheadInput.typeahead("open"); + $(".tt-suggestion").first().click(); + expect(this.view.onSuggestionSelection).toHaveBeenCalledWith( + {person: true, name: "user1", handle: "user1@pod.tld"} + ); }); }); - describe("updateMentionsCollection", function(){ - beforeEach(function(){ + describe("addPersonToMentions", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); }); - it("removes person from mention collection if not mentionned anymore", function(){ - this.view.mentionsCollection.push({name: "user1"}); - expect(this.view.mentionsCollection.length).toBe(1); - this.view.updateMentionsCollection(); - expect(this.view.mentionsCollection.length).toBe(0); - }); - - it("removes item from mention collection if not a person", function(){ - this.view.mentionsCollection.push({}); - expect(this.view.mentionsCollection.length).toBe(1); - this.view.updateMentionsCollection(); - expect(this.view.mentionsCollection.length).toBe(0); - }); - }); - - describe("addMention", function(){ - beforeEach(function(){ - this.view = new app.views.PublisherMention({ el: "#publisher" }); - }); - - it("add person to mentionned people", function(){ - expect(this.view.mentionsCollection.length).toBe(0); - this.view.addMention({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.mentionsCollection.length).toBe(1); - expect(this.view.mentionsCollection[0]).toEqual({ + it("adds a person to mentioned people", function() { + expect(this.view.mentionedPeople.length).toBe(0); + this.view.addPersonToMentions({name: "user1", handle: "user1@pod.tld"}); + expect(this.view.mentionedPeople.length).toBe(1); + expect(this.view.mentionedPeople[0]).toEqual({ /* jshint camelcase: false */ - "name":"user1", "handle":"user1@pod.tld", diaspora_id: "user1@pod.tld"}); + name: "user1", handle: "user1@pod.tld", diaspora_id: "user1@pod.tld"}); /* jshint camelcase: true */ }); - it("does not add mention if not a person", function(){ - expect(this.view.mentionsCollection.length).toBe(0); - this.view.addMention(); - expect(this.view.mentionsCollection.length).toBe(0); - this.view.addMention({}); - expect(this.view.mentionsCollection.length).toBe(0); - this.view.addMention({"name": "user1"}); - expect(this.view.mentionsCollection.length).toBe(0); - this.view.addMention({"handle":"user1@pod.tld"}); - expect(this.view.mentionsCollection.length).toBe(0); + it("adds a person to the ignored diaspora ids", function() { + spyOn(this.view, "ignorePersonForSuggestions"); + this.view.addPersonToMentions({name: "user1", handle: "user1@pod.tld"}); + expect(this.view.ignorePersonForSuggestions).toHaveBeenCalledWith({ + /* jshint camelcase: false */ + name: "user1", handle: "user1@pod.tld", diaspora_id: "user1@pod.tld"}); + /* jshint camelcase: true */ + }); + + it("doesn't add mention if not a person", function() { + expect(this.view.mentionedPeople.length).toBe(0); + this.view.addPersonToMentions(); + expect(this.view.mentionedPeople.length).toBe(0); + this.view.addPersonToMentions({}); + expect(this.view.mentionedPeople.length).toBe(0); + this.view.addPersonToMentions({name: "user1"}); + expect(this.view.mentionedPeople.length).toBe(0); + this.view.addPersonToMentions({handle: "user1@pod.tld"}); + expect(this.view.mentionedPeople.length).toBe(0); }); }); - describe("getTypeaheadInput", function(){ - beforeEach(function(){ + describe("cleanMentionedPeople", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); }); - it("inserts typeahead input if it does not already exist", function(){ - this.view.getTypeaheadInput().remove(); - expect(this.view.$(".typeahead-mention-box").length).toBe(0); - this.view.getTypeaheadInput(); - expect(this.view.$(".typeahead-mention-box").length).toBe(1); + it("removes person from mentioned people if not mentioned anymore", function() { + this.view.addPersonToMentions({name: "user1", handle: "user1@pod.tld"}); + expect(this.view.mentionedPeople.length).toBe(1); + this.view.cleanMentionedPeople(); + expect(this.view.mentionedPeople.length).toBe(0); + }); + + it("removes person from ignored people if not mentioned anymore", function() { + this.view.addPersonToMentions({name: "user1", handle: "user1@pod.tld"}); + expect(this.view.ignoreDiasporaIds.length).toBe(1); + this.view.cleanMentionedPeople(); + expect(this.view.ignoreDiasporaIds.length).toBe(0); + }); + + it("keeps mentioned persons", function() { + this.view.addPersonToMentions({name: "user1", handle: "user1@pod.tld"}); + this.view.inputBox.val("user1"); + expect(this.view.mentionedPeople.length).toBe(1); + this.view.cleanMentionedPeople(); + expect(this.view.mentionedPeople.length).toBe(1); + }); + + it("keeps mentioned persons for ignored diaspora ids", function() { + this.view.addPersonToMentions({name: "user1", handle: "user1@pod.tld"}); + this.view.inputBox.val("user1"); + expect(this.view.ignoreDiasporaIds.length).toBe(1); + this.view.cleanMentionedPeople(); + expect(this.view.ignoreDiasporaIds.length).toBe(1); }); }); - describe("processMention", function(){ - beforeEach(function(){ + describe("onSuggestionSelection", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); - this.view.elmInputBox.val("@user1 Text before @user1 text after"); - this.view.currentDataQuery = "user1"; - this.view.elmInputBox[0].setSelectionRange(25, 25); + this.view.inputBox.val("@user1337 Text before @user1 text after"); + this.view.inputBox[0].setSelectionRange(28, 28); }); - it("add person to mentionned people", function(){ - spyOn(this.view, "addMention"); - this.view.processMention({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + it("doesn't do anything if there is no '@' in front of the caret", function() { + spyOn(this.view, "addPersonToMentions"); + this.view.inputBox.val("user1337 Text before @user1 text after"); + this.view.inputBox[0].setSelectionRange(9, 9); + this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"}); + expect(this.view.addPersonToMentions).not.toHaveBeenCalled(); }); - it("cleans buffers", function(){ - spyOn(this.view, "clearBuffer"); - spyOn(this.view, "resetMentionBox"); - this.view.processMention({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.clearBuffer).toHaveBeenCalled(); - expect(this.view.resetMentionBox).toHaveBeenCalled(); - expect(this.view.currentDataQuery).toBe(""); + it("adds a person to mentioned people", function() { + spyOn(this.view, "addPersonToMentions"); + this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"}); + expect(this.view.addPersonToMentions).toHaveBeenCalledWith({name: "user1337", handle: "user1@pod.tld"}); }); - it("correctly formats the text", function(){ - spyOn(this.view, "updateValues"); - this.view.processMention({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.updateValues).toHaveBeenCalled(); - expect(this.view.getInputBoxValue()).toBe("@user1 Text before " + this.view.mentionChar + "user1 text after"); + it("closes the suggestions box", function() { + spyOn(this.view, "closeSuggestions"); + this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"}); + expect(this.view.closeSuggestions).toHaveBeenCalled(); }); - it("places the caret at the right position", function(){ - this.view.processMention({"name":"user1WithLongName", "handle":"user1@pod.tld"}); - var expectedCaretPosition = ("@user1 Text before " + this.view.mentionChar + "user1WithLongName").length; - expect(this.view.elmInputBox[0].selectionStart).toBe(expectedCaretPosition); + it("correctly formats the text", function() { + this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"}); + expect(this.view.inputBox.val()).toBe("@user1337 Text before \u200Buser1337 text after"); + }); + + it("replaces the correct mention", function() { + this.view.inputBox.val("@user1337 123 user2 @user2 456 @user3 789"); + this.view.inputBox[0].setSelectionRange(26, 26); + this.view.onSuggestionSelection({name: "user23", handle: "user2@pod.tld"}); + expect(this.view.inputBox.val()).toBe("@user1337 123 user2 \u200Buser23 456 @user3 789"); + this.view.inputBox[0].setSelectionRange(9, 9); + this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"}); + expect(this.view.inputBox.val()).toBe("\u200Buser1337 123 user2 \u200Buser23 456 @user3 789"); + this.view.inputBox[0].setSelectionRange(38, 38); + this.view.onSuggestionSelection({name: "user32", handle: "user3@pod.tld"}); + expect(this.view.inputBox.val()).toBe("\u200Buser1337 123 user2 \u200Buser23 456 \u200Buser32 789"); + }); + + it("calls updateMessageTexts", function() { + spyOn(this.view, "updateMessageTexts"); + this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"}); + expect(this.view.updateMessageTexts).toHaveBeenCalled(); + }); + + it("places the caret at the right position", function() { + this.view.onSuggestionSelection({"name": "user1WithLongName", "handle": "user1@pod.tld"}); + var expectedCaretPosition = ("@user1337 Text before \u200Buser1WithLongName").length; + expect(this.view.inputBox[0].selectionStart).toBe(expectedCaretPosition); }); }); - describe("updateValues", function(){ - beforeEach(function(){ + describe("updateMessageTexts", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); - this.view.elmInputBox.val("@user1 Text before " + this.view.mentionChar + "user1\ntext after"); - this.view.mentionsCollection.push({"name":"user1", "handle":"user1@pod.tld"}); + this.view.inputBox.val("@user1 Text before \u200Buser1\ntext after"); + this.view.mentionedPeople.push({"name": "user1", "handle": "user1@pod.tld"}); }); - it("filters mention from future results", function(){ - spyOn(this.view, "clearFilteredResults"); - spyOn(this.view, "addToFilteredResults"); - this.view.updateValues(); - expect(this.view.clearFilteredResults).toHaveBeenCalled(); - expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); + it("sets the correct messageText", function() { + this.view.updateMessageTexts(); + expect(this.view.inputBox.data("messageText")).toBe("@user1 Text before @{user1 ; user1@pod.tld}\ntext after"); }); - it("formats message text data with correct mentionning syntax", function(){ - this.view.updateValues(); - expect(this.view.elmInputBox.data("messageText")).toBe("@user1 Text before @{user1 ; user1@pod.tld}\ntext after"); - }); - - it("formats overlay text to HTML", function(){ - this.view.updateValues(); - expect(this.view.elmMentionsOverlay.find("div > div").html()) - .toBe("@user1 Text before user1
      text after"); + it("formats overlay text to HTML", function() { + this.view.updateMessageTexts(); + expect(this.view.mentionsBox.find(".mentions").html()) + .toBe("@user1 Text before user1\ntext after"); }); }); - describe("prefillMention", function(){ - beforeEach(function(){ + describe("updateTypeaheadInput", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); - spyOn(this.view, "addMention"); - spyOn(this.view, "addToFilteredResults"); - spyOn(this.view, "updateValues"); + this.view.inputBox.val("@user1337 Text before @user1 text after"); + this.view.inputBox[0].setSelectionRange(28, 28); }); - it("prefills one mention", function(){ - this.view.prefillMention([{"name":"user1", "handle":"user1@pod.tld"}]); - - expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.addToFilteredResults) - .toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.updateValues).toHaveBeenCalled(); - expect(this.view.getInputBoxValue()).toBe(this.view.mentionChar + "user1"); + it("calls 'closeSuggestions' if there is no '@' in front of the caret", function() { + spyOn(this.view, "closeSuggestions"); + this.view.inputBox.val("user1337 Text before @user1 text after"); + this.view.inputBox[0].setSelectionRange(9, 9); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions).toHaveBeenCalled(); }); - it("prefills multiple mentions", function(){ + it("calls 'closeSuggestions' if there is a whitespace between the '@' and the caret", function() { + spyOn(this.view, "closeSuggestions"); + this.view.inputBox.val("@user1337 Text before @user1 text after"); + this.view.inputBox[0].setSelectionRange(9, 9); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions.calls.count()).toEqual(0); + this.view.inputBox[0].setSelectionRange(10, 10); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions.calls.count()).toEqual(1); + this.view.inputBox[0].setSelectionRange(11, 11); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions.calls.count()).toEqual(2); + }); + + it("fills the typeahead input with the correct text", function() { + spyOn(this.view, "closeSuggestions"); + this.view.inputBox.val("@user1337 Text before @user1 text after"); + this.view.inputBox[0].setSelectionRange(2, 2); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions).not.toHaveBeenCalled(); + expect(this.view.typeaheadInput.val()).toBe("u"); + this.view.inputBox[0].setSelectionRange(9, 9); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions).not.toHaveBeenCalled(); + expect(this.view.typeaheadInput.val()).toBe("user1337"); + this.view.inputBox[0].setSelectionRange(27, 27); + this.view.updateTypeaheadInput(); + expect(this.view.closeSuggestions).not.toHaveBeenCalled(); + expect(this.view.typeaheadInput.val()).toBe("user"); + }); + }); + + describe("prefillMention", function() { + beforeEach(function() { + this.view = new app.views.PublisherMention({ el: "#publisher" }); + spyOn(this.view, "addPersonToMentions"); + spyOn(this.view, "updateMessageTexts"); + }); + + it("prefills one mention", function() { + this.view.prefillMention([{"name": "user1", "handle": "user1@pod.tld"}]); + expect(this.view.addPersonToMentions).toHaveBeenCalledWith({"name": "user1", "handle": "user1@pod.tld"}); + expect(this.view.updateMessageTexts).toHaveBeenCalled(); + expect(this.view.inputBox.val()).toBe("\u200Buser1"); + }); + + it("prefills multiple mentions", function() { this.view.prefillMention([ - {"name":"user1", "handle":"user1@pod.tld"}, - {"name":"user2", "handle":"user2@pod.tld"} + {"name": "user1", "handle": "user1@pod.tld"}, + {"name": "user2", "handle": "user2@pod.tld"} ]); - expect(this.view.addMention).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.addMention).toHaveBeenCalledWith({"name":"user2", "handle":"user2@pod.tld"}); - expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user1", "handle":"user1@pod.tld"}); - expect(this.view.addToFilteredResults).toHaveBeenCalledWith({"name":"user2", "handle":"user2@pod.tld"}); - expect(this.view.updateValues).toHaveBeenCalled(); - expect(this.view.getInputBoxValue()).toBe(this.view.mentionChar + "user1 " + this.view.mentionChar + "user2"); + expect(this.view.addPersonToMentions).toHaveBeenCalledWith({"name": "user1", "handle": "user1@pod.tld"}); + expect(this.view.addPersonToMentions).toHaveBeenCalledWith({"name": "user2", "handle": "user2@pod.tld"}); + expect(this.view.updateMessageTexts).toHaveBeenCalled(); + expect(this.view.inputBox.val()).toBe("\u200Buser1 \u200Buser2"); }); }); - describe("onInputBoxPaste", function(){ - beforeEach(function(){ + describe("onInputBoxKeyDown", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); }); - it("add person to mentionned people", function(){ - var pasteEvent = {originalEvent: {clipboardData: {getData: function(){ - return "Pasted text"; - }}}}; + context("escape key", function() { + beforeEach(function() { + this.evt = $.Event("keydown", {which: Keycodes.ESC}); + }); - this.view.onInputBoxPaste(pasteEvent); - expect(this.view.inputBuffer).toEqual(["P", "a", "s", "t", "e", "d", " ", "t", "e", "x", "t"]); + it("calls 'closeSuggestions'", function() { + spyOn(this.view, "closeSuggestions"); + this.view.onInputBoxKeyDown(this.evt); + expect(this.view.closeSuggestions).toHaveBeenCalled(); + }); + }); + + context("space key", function() { + beforeEach(function() { + this.evt = $.Event("keydown", {which: Keycodes.SPACE}); + }); + + it("calls 'closeSuggestions'", function() { + spyOn(this.view, "closeSuggestions"); + this.view.onInputBoxKeyDown(this.evt); + expect(this.view.closeSuggestions).toHaveBeenCalled(); + }); + }); + + context("up key", function() { + beforeEach(function() { + this.evt = $.Event("keydown", {which: Keycodes.UP}); + }); + + it("calls 'onArrowKeyDown'", function() { + spyOn(this.view, "onArrowKeyDown"); + this.view.onInputBoxKeyDown(this.evt); + expect(this.view.onArrowKeyDown).toHaveBeenCalled(); + }); + }); + + context("down key", function() { + beforeEach(function() { + this.evt = $.Event("keydown", {which: Keycodes.DOWN}); + }); + + it("calls 'onArrowKeyDown'", function() { + spyOn(this.view, "onArrowKeyDown"); + this.view.onInputBoxKeyDown(this.evt); + expect(this.view.onArrowKeyDown).toHaveBeenCalled(); + }); + }); + + context("return key", function() { + beforeEach(function() { + this.evt = $.Event("keydown", {which: Keycodes.RETURN}); + this.view.bloodhound.add([ + {person: true, name: "user1", handle: "user1@pod.tld"}, + {person: true, name: "user2", handle: "user2@pod.tld"} + ]); + this.view.typeaheadInput.typeahead("val", "user"); + this.view.typeaheadInput.typeahead("open"); + $(".tt-suggestion").first().addClass(".tt-cursor"); + }); + + it("calls 'onSuggestionSelection'", function() { + spyOn(this.view, "onSuggestionSelection"); + this.view.onInputBoxKeyDown(this.evt); + expect(this.view.onSuggestionSelection).toHaveBeenCalled(); + }); + }); + + context("tab key", function() { + beforeEach(function() { + this.evt = $.Event("keydown", {which: Keycodes.TAB}); + this.view.bloodhound.add([ + {person: true, name: "user1", handle: "user1@pod.tld"}, + {person: true, name: "user2", handle: "user2@pod.tld"} + ]); + this.view.typeaheadInput.typeahead("val", "user"); + this.view.typeaheadInput.typeahead("open"); + $(".tt-suggestion").first().addClass(".tt-cursor"); + }); + + it("calls 'onSuggestionSelection'", function() { + spyOn(this.view, "onSuggestionSelection"); + this.view.onInputBoxKeyDown(this.evt); + expect(this.view.onSuggestionSelection).toHaveBeenCalled(); + }); }); }); - describe("reset", function(){ - beforeEach(function(){ + describe("onInputBoxInput", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); - spyOn(this.view, "clearFilteredResults"); - spyOn(this.view, "updateValues"); }); - it("resets the mention box", function(){ + it("calls 'cleanMentionedPeople'", function() { + spyOn(this.view, "cleanMentionedPeople"); + this.view.onInputBoxInput(); + expect(this.view.cleanMentionedPeople).toHaveBeenCalled(); + }); + + it("calls 'updateMessageTexts'", function() { + spyOn(this.view, "updateMessageTexts"); + this.view.onInputBoxInput(); + expect(this.view.updateMessageTexts).toHaveBeenCalled(); + }); + + it("calls 'updateTypeaheadInput'", function() { + spyOn(this.view, "updateTypeaheadInput"); + this.view.onInputBoxInput(); + expect(this.view.updateTypeaheadInput).toHaveBeenCalled(); + }); + }); + + describe("onInputBoxClick", function() { + beforeEach(function() { + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("calls 'updateTypeaheadInput'", function() { + spyOn(this.view, "updateTypeaheadInput"); + this.view.onInputBoxClick(); + expect(this.view.updateTypeaheadInput).toHaveBeenCalled(); + }); + }); + + describe("onInputBoxBlur", function() { + beforeEach(function() { + this.view = new app.views.PublisherMention({ el: "#publisher" }); + }); + + it("calls 'closeSuggestions'", function() { + spyOn(this.view, "closeSuggestions"); + this.view.onInputBoxBlur(); + expect(this.view.closeSuggestions).toHaveBeenCalled(); + }); + }); + + describe("reset", function() { + beforeEach(function() { + this.view = new app.views.PublisherMention({ el: "#publisher" }); + spyOn(this.view, "onInputBoxInput"); + }); + + it("resets the mention box", function() { this.view.reset(); - expect(this.view.elmInputBox.val()).toBe(""); - expect(this.view.mentionsCollection.length).toBe(0); - expect(this.view.clearFilteredResults).toHaveBeenCalled(); - expect(this.view.updateValues).toHaveBeenCalled(); + expect(this.view.inputBox.val()).toBe(""); + expect(this.view.onInputBoxInput).toHaveBeenCalled(); }); }); - describe("showMentionBox", function(){ - beforeEach(function(){ + describe("closeSuggestions", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); this.view.bloodhound.add([ - {"person": true, "name":"user1", "handle":"user1@pod.tld"} + {"person": true, "name": "user1", "handle": "user1@pod.tld"} ]); - this.view.currentDataQuery = "user1"; }); - it("shows the mention box", function(){ + it("resets results and closes mention box", function() { + this.view.typeaheadInput.typeahead("val", "user"); + this.view.typeaheadInput.typeahead("open"); + expect(this.view.$(".tt-menu").is(":visible")).toBe(true); + expect(this.view.$(".tt-menu .tt-suggestion").length).toBeGreaterThan(0); + expect(this.view.typeaheadInput.val()).toBe("user"); + this.view.closeSuggestions(); expect(this.view.$(".tt-menu").is(":visible")).toBe(false); expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(0); - this.view.showMentionBox(); - expect(this.view.$(".tt-menu").is(":visible")).toBe(true); - expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(1); + expect(this.view.typeaheadInput.val()).toBe(""); }); }); - describe("resetMentionBox", function(){ - beforeEach(function(){ + describe("getTextForSubmit", function() { + beforeEach(function() { this.view = new app.views.PublisherMention({ el: "#publisher" }); this.view.bloodhound.add([ - {"person": true, "name":"user1", "handle":"user1@pod.tld"} + {person: true, name: "user1", handle: "user1@pod.tld"} ]); }); - it("resets results and closes mention box", function(){ - this.view.getTypeaheadInput().typeahead("val", "user"); - this.view.getTypeaheadInput().typeahead("open"); - expect(this.view.$(".tt-menu").is(":visible")).toBe(true); - expect(this.view.$(".tt-menu .tt-suggestion").length >= 1).toBe(true); - this.view.resetMentionBox(); - expect(this.view.$(".tt-menu").is(":visible")).toBe(false); - expect(this.view.$(".tt-menu .tt-suggestion").length).toBe(0); - }); - }); - - describe("getInputBoxValue", function(){ - beforeEach(function(){ - this.view = new app.views.PublisherMention({ el: "#publisher" }); - }); - - it("returns trimmed text", function(){ - this.view.elmInputBox.val("Text with trailing spaces "); - expect(this.view.getInputBoxValue()).toBe("Text with trailing spaces"); - }); - }); - - describe("getTextForSubmit", function(){ - beforeEach(function(){ - this.view = new app.views.PublisherMention({ el: "#publisher" }); - this.view.bloodhound.add([ - {"person": true, "name":"user1", "handle":"user1@pod.tld"} - ]); - }); - - it("returns text with mention syntax if someone is mentionned", function(){ - this.view.getTypeaheadInput().typeahead("val", "user"); - this.view.getTypeaheadInput().typeahead("open"); + it("returns text with mention if someone has been mentioned", function() { + this.view.inputBox.val("@user"); + this.view.inputBox[0].setSelectionRange(5, 5); + this.view.typeaheadInput.typeahead("val", "user"); + this.view.typeaheadInput.typeahead("open"); this.view.$(".tt-suggestion").first().click(); expect(this.view.getTextForSubmit()).toBe("@{user1 ; user1@pod.tld}"); }); - it("returns normal text if nobody is mentionned", function(){ - this.view.elmInputBox.data("messageText", "Bad text"); - this.view.elmInputBox.val("Good text"); + it("returns normal text if nobody has been mentioned", function() { + this.view.inputBox.data("messageText", "Bad text"); + this.view.inputBox.val("Good text"); expect(this.view.getTextForSubmit()).toBe("Good text"); }); }); diff --git a/spec/javascripts/app/views/search_base_view_spec.js b/spec/javascripts/app/views/search_base_view_spec.js index d0e633305..902176ec1 100644 --- a/spec/javascripts/app/views/search_base_view_spec.js +++ b/spec/javascripts/app/views/search_base_view_spec.js @@ -1,75 +1,106 @@ describe("app.views.SearchBase", function() { - beforeEach(function(){ + beforeEach(function() { spec.content().html( "
      " ); + this.search = function(view, name) { + view.$("#q").trigger("focusin"); + view.$("#q").val(name); + view.$("#q").trigger("keypress"); + view.$("#q").trigger("input"); + view.$("#q").trigger("focus"); + }; + this.bloodhoundData = [ + {"person": true, "name": "user1", "handle": "user1@pod.tld"}, + {"person": true, "name": "user2", "handle": "user2@pod.tld"} + ]; }); - describe("initialize", function(){ - it("calls setupBloodhound", function(){ + describe("initialize", function() { + it("calls setupBloodhound", function() { spyOn(app.views.SearchBase.prototype, "setupBloodhound").and.callThrough(); - new app.views.SearchBase({el: "#search_people_form"}); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); expect(app.views.SearchBase.prototype.setupBloodhound).toHaveBeenCalled(); }); - it("calls setupTypeahead", function(){ + it("doesn't call setupCustomSearch if customSearch hasn't been enabled", function() { + spyOn(app.views.SearchBase.prototype, "setupCustomSearch"); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + expect(app.views.SearchBase.prototype.setupCustomSearch).not.toHaveBeenCalled(); + }); + + it("calls setupCustomSearch if customSearch has been enabled", function() { + spyOn(app.views.SearchBase.prototype, "setupCustomSearch"); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q"), customSearch: true}); + expect(app.views.SearchBase.prototype.setupCustomSearch).toHaveBeenCalled(); + }); + + it("calls setupTypeahead", function() { spyOn(app.views.SearchBase.prototype, "setupTypeahead"); - new app.views.SearchBase({el: "#search_people_form"}); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); expect(app.views.SearchBase.prototype.setupTypeahead).toHaveBeenCalled(); }); - it("calls bindSelectionEvents", function(){ - spyOn(app.views.SearchBase.prototype, "bindSelectionEvents"); - new app.views.SearchBase({el: "#search_people_form"}); - expect(app.views.SearchBase.prototype.bindSelectionEvents).toHaveBeenCalled(); + it("calls setupMouseSelectionEvents", function() { + spyOn(app.views.SearchBase.prototype, "setupMouseSelectionEvents"); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + expect(app.views.SearchBase.prototype.setupMouseSelectionEvents).toHaveBeenCalled(); }); - it("initializes the results to filter", function(){ - spyOn(app.views.SearchBase.prototype, "bindSelectionEvents"); - var view = new app.views.SearchBase({el: "#search_people_form"}); - expect(view.resultsTofilter.length).toBe(0); + it("initializes the array of diaspora ids that should be excluded from the search results", function() { + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + expect(this.view.ignoreDiasporaIds.length).toBe(0); + }); + + it("doesn't call setupAutoselect if autoselect hasn't been enabled", function() { + spyOn(app.views.SearchBase.prototype, "setupAutoselect"); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + expect(app.views.SearchBase.prototype.setupAutoselect).not.toHaveBeenCalled(); + }); + + it("calls setupAutoselect if autoselect has been enabled", function() { + spyOn(app.views.SearchBase.prototype, "setupAutoselect"); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q"), autoselect: true}); + expect(app.views.SearchBase.prototype.setupAutoselect).toHaveBeenCalled(); }); }); - describe("setupBloodhound", function(){ - beforeEach(function(){ - this.view = new app.views.SearchBase({el: "#search_people_form"}); - this.syncCallback = function(){}; - this.asyncCallback = function(){}; + describe("setupCustomSearch", function() { + it("sets bloodhound.customSearch", function() { + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + expect(this.view.bloodhound.customSearch).toBeUndefined(); + this.view.setupCustomSearch(); + expect(this.view.bloodhound.customSearch).toBeDefined(); }); - context("when performing a local search with 1 filtered result", function(){ - beforeEach(function(){ - this.view.initialize({typeaheadElement: this.view.$("#q")}); - this.view.bloodhound.add([ - {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}, - {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"} - ]); + describe("customSearch", function() { + beforeEach(function() { + this.view = new app.views.SearchBase({ + el: "#search_people_form", + typeaheadInput: $("#q"), + customSearch: true + }); + this.view.bloodhound.add(this.bloodhoundData); }); - it("should not return the filtered result", function(){ - spyOn(this, "syncCallback"); - spyOn(this, "asyncCallback"); + it("returns all results if none of them should be ignored", function() { + var spy = jasmine.createSpyObj("callbacks", ["syncCallback", "asyncCallback"]); + this.view.bloodhound.customSearch("user", spy.syncCallback, spy.asyncCallback); + expect(spy.syncCallback).toHaveBeenCalledWith(this.bloodhoundData); + }); - this.view.bloodhound.customSearch("user", this.syncCallback, this.asyncCallback); - expect(this.syncCallback).toHaveBeenCalledWith([ - {"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}, - {"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"} - ]); - expect(this.asyncCallback).not.toHaveBeenCalled(); - - this.view.addToFilteredResults({"id":1,"guid":"1","name":"user1","handle":"user1@pod.tld","url":"/people/1"}); - this.view.bloodhound.customSearch("user", this.syncCallback, this.asyncCallback); - expect(this.syncCallback).toHaveBeenCalledWith( - [{"id":2,"guid":"2","name":"user2","handle":"user2@pod.tld","url":"/people/2"}]); - expect(this.asyncCallback).not.toHaveBeenCalled(); + it("doesn't return results that should be ignored", function() { + var spy = jasmine.createSpyObj("callbacks", ["syncCallback", "asyncCallback"]); + this.view.ignorePersonForSuggestions({handle: "user1@pod.tld"}); + this.view.bloodhound.customSearch("user", spy.syncCallback, spy.asyncCallback); + expect(spy.syncCallback).toHaveBeenCalledWith([this.bloodhoundData[1]]); }); }); }); describe("transformBloodhoundResponse", function() { beforeEach(function() { - this.view = new app.views.SearchBase({ el: "#search_people_form" }); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); }); context("with persons", function() { @@ -99,81 +130,119 @@ describe("app.views.SearchBase", function() { }); }); - describe("bindSelectionEvents", function(){ + describe("setupMouseSelectionEvents", function() { beforeEach(function() { - this.view = new app.views.SearchBase({ el: "#search_people_form" }); - this.view.initialize({typeaheadElement: this.view.$("#q")}); - this.view.bloodhound.add([ - {"person": true, "name":"user1", "handle":"user1@pod.tld"}, - {"person": true, "name":"user2", "handle":"user2@pod.tld"} - ]); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + this.view.bloodhound.add(this.bloodhoundData); }); - context("bind over events", function(){ - it("binds over event only once", function(){ - this.view.$("#q").trigger("focusin"); - this.view.$("#q").val("user"); - this.view.$("#q").trigger("keypress"); - this.view.$("#q").trigger("input"); - this.view.$("#q").trigger("focus"); - var numBindedEvents = $._data(this.view.$(".tt-menu .tt-suggestion")[0], "events").mouseover.length; - expect(numBindedEvents).toBe(1); - this.view.$("#q").trigger("focusout"); - this.view.$("#q").trigger("focusin"); - this.view.$("#q").val("user"); - this.view.$("#q").trigger("keypress"); - this.view.$("#q").trigger("input"); - this.view.$("#q").trigger("focus"); - numBindedEvents = $._data(this.view.$(".tt-menu .tt-suggestion")[0], "events").mouseover.length; - expect(numBindedEvents).toBe(1); - }); + it("binds mouseover and mouseleave events only once", function() { + this.search(this.view, "user"); + $("#q").trigger("focusout"); + expect($._data($(".tt-menu .tt-suggestion")[0], "events").mouseover.length).toBe(1); + expect($._data($(".tt-menu .tt-suggestion")[0], "events").mouseout.length).toBe(1); - it("highlights the result when overing it", function(){ - this.view.$("#q").trigger("focusin"); - this.view.$("#q").val("user"); - this.view.$("#q").trigger("keypress"); - this.view.$("#q").trigger("input"); - this.view.$("#q").trigger("focus"); - this.view.$(".tt-menu .tt-suggestion").first().trigger("mouseover"); - expect(this.view.$(".tt-menu .tt-suggestion").first()).toHaveClass("tt-cursor"); - }); + this.search(this.view, "user"); + $("#q").trigger("focusout"); + expect($._data($(".tt-menu .tt-suggestion")[0], "events").mouseover.length).toBe(1); + expect($._data($(".tt-menu .tt-suggestion")[0], "events").mouseout.length).toBe(1); + }); + + it("allows selecting results with the mouse", function() { + this.search(this.view, "user"); + this.view.$(".tt-menu .tt-suggestion:eq(0)").trigger("mouseover"); + expect(this.view.$(".tt-menu .tt-suggestion:eq(0)")).toHaveClass("tt-cursor"); + expect(this.view.$(".tt-cursor").length).toBe(1); + + this.view.$(".tt-menu .tt-suggestion:eq(1)").trigger("mouseover"); + expect(this.view.$(".tt-menu .tt-suggestion:eq(1)")).toHaveClass("tt-cursor"); + expect(this.view.$(".tt-cursor").length).toBe(1); + + this.view.$(".tt-menu .tt-suggestion:eq(1)").trigger("mouseleave"); + expect(this.view.$(".tt-cursor").length).toBe(0); + + this.view.$(".tt-menu .tt-suggestion:eq(0)").trigger("mouseover"); + expect(this.view.$(".tt-menu .tt-suggestion:eq(0)")).toHaveClass("tt-cursor"); + expect(this.view.$(".tt-cursor").length).toBe(1); }); }); - describe("addToFilteredResults", function(){ + describe("_deselectAllSuggestions", function() { beforeEach(function() { - this.view = new app.views.SearchBase({ el: "#search_people_form" }); - this.view.initialize({typeaheadElement: this.view.$("#q")}); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + this.view.bloodhound.add(this.bloodhoundData); + this.search(this.view, "user"); }); - context("when item is a person", function(){ - it("add the item to filtered results", function(){ - this.view.addToFilteredResults({handle: "user@pod.tld"}); - expect(this.view.resultsTofilter.length).toBe(1); - }); - }); + it("deselects all suggestions", function() { + $(".tt-suggestion").addClass(".tt-cursor"); + this.view._deselectAllSuggestions(); + expect($(".tt-suggestion.tt-cursor").length).toBe(0); - context("when item is not a person", function(){ - it("does not add the item to filtered results", function(){ - this.view.addToFilteredResults({}); - expect(this.view.resultsTofilter.length).toBe(0); - }); + $(".tt-suggestion:eq(1)").addClass(".tt-cursor"); + this.view._deselectAllSuggestions(); + expect($(".tt-suggestion.tt-cursor").length).toBe(0); }); }); - describe("clearFilteredResults", function(){ + describe("_selectSuggestion", function() { beforeEach(function() { - this.view = new app.views.SearchBase({ el: "#search_people_form" }); - this.view.initialize({typeaheadElement: this.view.$("#q")}); + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + this.view.bloodhound.add(this.bloodhoundData); + this.search(this.view, "user"); }); - context("clear filtered results", function(){ - it("clears the filtered results list", function(){ - this.view.addToFilteredResults({handle: "user@pod.tld"}); - expect(this.view.resultsTofilter.length).toBe(1); - this.view.clearFilteredResults(); - expect(this.view.resultsTofilter.length).toBe(0); + it("selects a suggestion", function() { + this.view._selectSuggestion($(".tt-suggestion:eq(1)")); + expect($(".tt-suggestion.tt-cursor").length).toBe(1); + expect($(".tt-suggestion:eq(1)")).toHaveClass("tt-cursor"); + }); + + it("deselects all other suggestions", function() { + spyOn(this.view, "_deselectAllSuggestions").and.callThrough(); + $(".tt-suggestion:eq(0)").addClass(".tt-cursor"); + this.view._selectSuggestion($(".tt-suggestion:eq(1)")); + expect(this.view._deselectAllSuggestions).toHaveBeenCalled(); + expect($(".tt-suggestion.tt-cursor").length).toBe(1); + expect($(".tt-suggestion:eq(1)")).toHaveClass("tt-cursor"); + }); + }); + + describe("setupAutoSelect", function() { + beforeEach(function() { + this.view = new app.views.SearchBase({ + el: "#search_people_form", + typeaheadInput: $("#q"), + autoselect: true }); + this.view.bloodhound.add(this.bloodhoundData); + }); + + it("selects the first suggestion when showing the results", function() { + this.search(this.view, "user"); + expect($(".tt-suggestion:eq(0)")).toHaveClass("tt-cursor"); + expect($(".tt-suggestion:eq(1)")).not.toHaveClass("tt-cursor"); + }); + }); + + describe("ignorePersonForSuggestions", function() { + beforeEach(function() { + this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")}); + }); + + it("adds the diaspora ids to the ignore list", function() { + expect(this.view.ignoreDiasporaIds.length).toBe(0); + this.view.ignorePersonForSuggestions({handle: "user1@pod.tld"}); + expect(this.view.ignoreDiasporaIds.length).toBe(1); + this.view.ignorePersonForSuggestions({handle: "user2@pod.tld", someData: true}); + expect(this.view.ignoreDiasporaIds.length).toBe(2); + expect(this.view.ignoreDiasporaIds).toEqual(["user1@pod.tld", "user2@pod.tld"]); + }); + + it("doesn't fail when the diaspora id is missing", function() { + expect(this.view.ignoreDiasporaIds.length).toBe(0); + this.view.ignorePersonForSuggestions({data: "user1@pod.tld"}); + expect(this.view.ignoreDiasporaIds.length).toBe(0); }); }); }); diff --git a/spec/javascripts/app/views/search_view_spec.js b/spec/javascripts/app/views/search_view_spec.js index 001fabddd..74e4831eb 100644 --- a/spec/javascripts/app/views/search_view_spec.js +++ b/spec/javascripts/app/views/search_view_spec.js @@ -1,64 +1,22 @@ -describe("app.views.Search", function(){ - beforeEach(function(){ +describe("app.views.Search", function() { + beforeEach(function() { spec.content().html( "
      " ); }); - describe("initialize", function(){ - it("calls app.views.SearchBase.prototype.initialize", function(){ - spyOn(app.views.SearchBase.prototype, "initialize").and.callThrough(); - var view = new app.views.Search({el: "#search_people_form"}); - expect(app.views.SearchBase.prototype.initialize) - .toHaveBeenCalledWith({typeaheadElement: view.getTypeaheadElement()}); + describe("initialize", function() { + it("calls app.views.SearchBase.prototype.initialize", function() { + spyOn(app.views.SearchBase.prototype, "initialize"); + this.view = new app.views.Search({el: "#search_people_form"}); + var call = app.views.SearchBase.prototype.initialize.calls.mostRecent(); + expect(call.args[0].typeaheadInput.selector).toBe("#search_people_form #q"); + expect(call.args[0].remoteRoute).toBe("/search"); }); - it("calls bindMoreSelectionEvents", function(){ - spyOn(app.views.Search.prototype, "bindMoreSelectionEvents").and.callThrough(); - new app.views.Search({el: "#search_people_form"}); - expect(app.views.Search.prototype.bindMoreSelectionEvents).toHaveBeenCalled(); - }); - }); - - describe("bindMoreSelectionEvents", function(){ - beforeEach(function() { - this.view = new app.views.Search({ el: "#search_people_form" }); - this.view.bloodhound.add([ - {"person": true, "name":"user1", "handle":"user1@pod.tld"}, - {"person": true, "name":"user2", "handle":"user2@pod.tld"} - ]); - }); - - context("bind mouseleave event", function(){ - it("binds mouseleave event only once", function(){ - this.view.$("#q").trigger("focusin"); - this.view.$("#q").val("user"); - this.view.$("#q").trigger("keypress"); - this.view.$("#q").trigger("input"); - this.view.$("#q").trigger("focus"); - var numBindedEvents = $._data(this.view.$(".tt-menu")[0], "events").mouseout.length; - expect(numBindedEvents).toBe(1); - this.view.$("#q").trigger("focusout"); - this.view.$("#q").trigger("focusin"); - this.view.$("#q").val("user"); - this.view.$("#q").trigger("keypress"); - this.view.$("#q").trigger("input"); - this.view.$("#q").trigger("focus"); - numBindedEvents = $._data(this.view.$(".tt-menu")[0], "events").mouseout.length; - expect(numBindedEvents).toBe(1); - }); - - it("remove result highlight when leaving results list", function(){ - this.view.$("#q").trigger("focusin"); - this.view.$("#q").val("user"); - this.view.$("#q").trigger("keypress"); - this.view.$("#q").trigger("input"); - this.view.$("#q").trigger("focus"); - this.view.$(".tt-menu .tt-suggestion").first().trigger("mouseover"); - expect(this.view.$(".tt-menu .tt-suggestion").first()).toHaveClass("tt-cursor"); - this.view.$(".tt-menu").first().trigger("mouseleave"); - expect(this.view.$(".tt-menu .tt-cursor").length).toBe(0); - }); + it("binds typeahead:select", function() { + this.view = new app.views.Search({el: "#search_people_form"}); + expect($._data($("#q")[0], "events")["typeahead:select"].length).toBe(1); }); });