From f556a5210eb753d55abfd452f9a0d4b004badc5d Mon Sep 17 00:00:00 2001 From: Steffen van Bergerem Date: Fri, 26 Feb 2016 22:03:50 +0100 Subject: [PATCH] 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); }); });