diff --git a/Changelog.md b/Changelog.md
index 62269bca2..78f92fbb9 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -90,6 +90,7 @@ Contributions are very welcome, the hard work is done!
* Refactor mobile javascript and add tests [#6394](https://github.com/diaspora/diaspora/pull/6394)
* Dropped `parent_author_signature` from relayables [#6586](https://github.com/diaspora/diaspora/pull/6586)
* Attached ShareVisibilities to the User, not the Contact [#6723](https://github.com/diaspora/diaspora/pull/6723)
+* Refactor mentions input, now based on typeahead.js [#6728](https://github.com/diaspora/diaspora/pull/6728)
## Bug fixes
* Destroy Participation when removing interactions with a post [#5852](https://github.com/diaspora/diaspora/pull/5852)
diff --git a/Gemfile b/Gemfile
index bd634d1ab..79a6eb5a8 100644
--- a/Gemfile
+++ b/Gemfile
@@ -108,11 +108,9 @@ 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"
- 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..6baa27335 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -637,13 +637,9 @@ 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)
- 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)
@@ -989,9 +985,7 @@ 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)!
rails-assets-jquery-placeholder (= 2.3.1)!
rails-assets-jquery-textchange (= 0.2.3)!
diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js
index 52117d495..8f65548aa 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/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
new file mode 100644
index 000000000..f19ec7d1e
--- /dev/null
+++ b/app/assets/javascripts/app/views/publisher/mention_view.js
@@ -0,0 +1,222 @@
+//= require ../search_base_view
+
+app.views.PublisherMention = app.views.SearchBase.extend({
+ triggerChar: "@",
+ invisibleChar: "\u200B", // zero width space
+ mentionRegex: /@([^@\s]+)$/,
+
+ templates: {
+ mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"),
+ mentionItemHighlight: _.template("<%= name %>")
+ },
+
+ events: {
+ "keydown #status_message_fake_text": "onInputBoxKeyDown",
+ "input #status_message_fake_text": "onInputBoxInput",
+ "click #status_message_fake_text": "onInputBoxClick",
+ "blur #status_message_fake_text": "onInputBoxBlur",
+ },
+
+ initialize: function() {
+ this.mentionedPeople = [];
+
+ // 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();
+
+ app.views.SearchBase.prototype.initialize.call(this, {
+ typeaheadInput: this.typeaheadInput,
+ customSearch: true,
+ autoselect: true
+ });
+ },
+
+ bindTypeaheadEvents: function() {
+ var self = this;
+ // Process mention when the user selects a result.
+ this.typeaheadInput.on("typeahead:select", function(evt, person) { self.onSuggestionSelection(person); });
+ },
+
+ 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.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);
+ },
+
+ /**
+ * 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.
+ */
+ updateMessageTexts: function() {
+ var fakeMessageText = this.inputBox.val(),
+ mentionBoxText = fakeMessageText,
+ messageText = fakeMessageText;
+
+ 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);
+
+ this.inputBox.data("messageText", messageText);
+ this.mentionsBox.find(".mentions").html(mentionBoxText);
+ },
+
+ updateTypeaheadInput: function() {
+ var messageText = this.inputBox.val();
+ var caretPosition = this.inputBox[0].selectionStart;
+ var result = this.mentionRegex.exec(messageText.substring(0,caretPosition));
+
+ if(result === null) {
+ this.closeSuggestions();
+ return;
+ }
+
+ // result[1] is the string between the last '@' and the current caret position
+ this.typeaheadInput.typeahead("val", result[1]);
+ this.typeaheadInput.typeahead("open");
+ },
+
+ /**
+ * 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) {
+ persons.forEach(function(person) {
+ this.addPersonToMentions(person);
+ var text = this.invisibleChar + person.name;
+ if(this.inputBox.val().length !== 0) {
+ text = this.inputBox.val() + " " + text;
+ }
+ this.inputBox.val(text);
+ this.updateMessageTexts();
+ }, this);
+ },
+
+ /**
+ * Selects next or previous result when result dropdown is open and
+ * user press up and down arrows.
+ */
+ onArrowKeyDown: function(e) {
+ if(!this.isVisible() || (e.which !== Keycodes.UP && e.which !== Keycodes.DOWN)) {
+ return;
+ }
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ 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.cleanMentionedPeople();
+ this.updateMessageTexts();
+ this.updateTypeaheadInput();
+ },
+
+ 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(_.bind(this.updateTypeaheadInput, this));
+ return;
+ }
+
+ if(!this.isVisible) {
+ return true;
+ }
+
+ switch(e.which) {
+ case Keycodes.ESC:
+ case Keycodes.SPACE:
+ this.closeSuggestions();
+ break;
+ case Keycodes.UP:
+ case Keycodes.DOWN:
+ this.onArrowKeyDown(e);
+ break;
+ case Keycodes.RETURN:
+ case Keycodes.TAB:
+ if(this.$(".tt-cursor").length === 1) {
+ this.$(".tt-cursor").click();
+ return false;
+ }
+ break;
+ }
+ return true;
+ },
+
+ onInputBoxClick: function() {
+ this.updateTypeaheadInput();
+ },
+
+ onInputBoxBlur: function() {
+ this.closeSuggestions();
+ },
+
+ reset: function() {
+ this.inputBox.val("");
+ this.onInputBoxInput();
+ },
+
+ closeSuggestions: function() {
+ this.typeaheadInput.typeahead("val", "");
+ this.typeaheadInput.typeahead("close");
+ },
+
+ isVisible: function() {
+ return this.$(".tt-menu").is(":visible");
+ },
+
+ 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 89e5e38f5..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
@@ -31,6 +33,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 +44,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 +100,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({
@@ -222,8 +227,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();
}
},
@@ -232,7 +237,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;
}
},
@@ -244,8 +249,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() {
@@ -265,32 +271,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 +301,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.mentionedPeople;
var date = (new Date()).toISOString();
var poll = this.getPollData(serializedForm);
var locationCoords = serializedForm["location[coords]"];
@@ -379,7 +359,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;
@@ -387,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("");
@@ -394,9 +377,6 @@ app.views.Publisher = Backbone.View.extend({
.trigger("keydown");
autosize.update(this.inputEl);
- // remove mentions
- this.inputEl.mentionsInput("reset");
-
// remove photos
this.photozoneEl.find("li").remove();
this.$("input[name='photos[]']").remove();
@@ -450,9 +430,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 +498,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..f194af7e1
--- /dev/null
+++ b/app/assets/javascripts/app/views/search_base_view.js
@@ -0,0 +1,124 @@
+app.views.SearchBase = app.views.Base.extend({
+ initialize: function(options) {
+ this.ignoreDiasporaIds = [];
+ this.typeaheadInput = options.typeaheadInput;
+ this.setupBloodhound(options);
+ if(options.customSearch) { this.setupCustomSearch(); }
+ this.setupTypeahead();
+ // TODO: Remove this as soon as corejavascript/typeahead.js has its first release
+ this.setupMouseSelectionEvents();
+ if(options.autoselect) { this.setupAutoselect(); }
+ },
+
+ setupBloodhound: function(options) {
+ var bloodhoundOptions = {
+ 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
+ };
+
+ // 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(bloodhoundOptions);
+ },
+
+ 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;
+ });
+ sync(results);
+ };
+
+ self.bloodhound.search(query, _sync, async);
+ };
+ },
+
+ setupTypeahead: function() {
+ this.typeaheadInput.typeahead({
+ hint: false,
+ highlight: true,
+ minLength: 2
+ },
+ {
+ name: "search",
+ display: "name",
+ limit: 5,
+ source: this.bloodhound.customSearch !== undefined ? this.bloodhound.customSearch : this.bloodhound,
+ 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))
+ };
+ });
+ },
+
+ _deselectAllSuggestions: function() {
+ this.$(".tt-suggestion").removeClass("tt-cursor");
+ },
+
+ _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);
+ });
+ },
+
+ // 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());
+ });
+ },
+
+ ignorePersonForSuggestions: function(person) {
+ if(person.handle) { this.ignoreDiasporaIds.push(person.handle); }
+ },
+});
diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js
index 6e8a1d272..254bcb071 100644
--- a/app/assets/javascripts/app/views/search_view.js
+++ b/app/assets/javascripts/app/views/search_view.js
@@ -1,81 +1,20 @@
// @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");
+ initialize: function() {
this.searchInput = this.$("#q");
-
- // constructs the suggestion engine
- this.setupBloodhound();
- this.setupTypeahead();
+ app.views.SearchBase.prototype.initialize.call(this, {
+ typeaheadInput: this.searchInput,
+ remoteRoute: this.$el.attr("action")
+ });
this.searchInput.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
- });
- },
-
- 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 */
- }
- });
- },
-
- 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) {
// jQuery produces two events for focus/blur (for bubbling)
// don't rely on which event arrives first, by allowing for both variants
@@ -83,14 +22,14 @@ app.views.Search = app.views.Base.extend({
$(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) {
+ if(evt.which === Keycodes.ENTER && $(".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/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/app/views/tags_view.js b/app/assets/javascripts/app/views/tags_view.js
index e420281fb..3913f8798 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/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 78da46496..ba2bb5e3d 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -6,15 +6,12 @@
//= require js-routes
//= require underscore
//= require backbone
-//= require jquery.hotkeys
//= require jquery.remotipart
//= require autosize
//= require jquery.charcount
//= require jquery-placeholder
//= 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
@@ -39,7 +36,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/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/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 32e1e49e3..d2897dfa1 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,
@@ -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;
@@ -83,7 +88,7 @@
}
&.active textarea {
- min-height: 70px;
+ min-height: 90px;
}
.markdownIndications {
@@ -118,22 +123,6 @@
}
}
- &.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;
}
@@ -201,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;
}
@@ -261,3 +208,75 @@
}
}
}
+
+.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; }
+ }
+
+ .twitter-typeahead {
+ left: -1px;
+ position: absolute;
+ }
+}
+
+.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/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss
index 3a46e14b6..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;
@@ -12,10 +15,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/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/publisher/_publisher.html.haml b/app/views/publisher/_publisher.html.haml
index c6ed2f83e..70d386ce5 100644
--- a/app/views/publisher/_publisher.html.haml
+++ b/app/views/publisher/_publisher.html.haml
@@ -9,34 +9,38 @@
= form_for(StatusMessage.new) do |status|
= status.error_messages
%params
- #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"
+ .publisher-textarea-wrapper#publisher_textarea_wrapper
+ .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
%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
#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/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/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/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
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/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
diff --git a/spec/javascripts/app/router_spec.js b/spec/javascripts/app/router_spec.js
index 0dbe5b269..e29a56e58 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();
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_mention_view_spec.js b/spec/javascripts/app/views/publisher_mention_view_spec.js
new file mode 100644
index 000000000..68397e36e
--- /dev/null
+++ b/spec/javascripts/app/views/publisher_mention_view_spec.js
@@ -0,0 +1,472 @@
+describe("app.views.PublisherMention", function() {
+ beforeEach(function() {
+ spec.loadFixture("aspects_index");
+ });
+
+ 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("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 bindTypeaheadEvents", function() {
+ spyOn(app.views.PublisherMention.prototype, "bindTypeaheadEvents");
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ expect(app.views.PublisherMention.prototype.bindTypeaheadEvents).toHaveBeenCalled();
+ });
+ });
+
+ 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"}
+ ]);
+ });
+
+ 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("addPersonToMentions", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ });
+
+ 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"});
+ /* jshint camelcase: true */
+ });
+
+ 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("cleanMentionedPeople", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ });
+
+ 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("onSuggestionSelection", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ this.view.inputBox.val("@user1337 Text before @user1 text after");
+ this.view.inputBox[0].setSelectionRange(28, 28);
+ });
+
+ 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("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("closes the suggestions box", function() {
+ spyOn(this.view, "closeSuggestions");
+ this.view.onSuggestionSelection({name: "user1337", handle: "user1@pod.tld"});
+ expect(this.view.closeSuggestions).toHaveBeenCalled();
+ });
+
+ 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("updateMessageTexts", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ this.view.inputBox.val("@user1 Text before \u200Buser1\ntext after");
+ this.view.mentionedPeople.push({"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 overlay text to HTML", function() {
+ this.view.updateMessageTexts();
+ expect(this.view.mentionsBox.find(".mentions").html())
+ .toBe("@user1 Text before user1\ntext after");
+ });
+ });
+
+ describe("updateTypeaheadInput", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ this.view.inputBox.val("@user1337 Text before @user1 text after");
+ this.view.inputBox[0].setSelectionRange(28, 28);
+ });
+
+ 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("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"}
+ ]);
+
+ 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("onInputBoxKeyDown", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ });
+
+ context("escape key", function() {
+ beforeEach(function() {
+ this.evt = $.Event("keydown", {which: Keycodes.ESC});
+ });
+
+ 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("onInputBoxInput", function() {
+ beforeEach(function() {
+ this.view = new app.views.PublisherMention({ el: "#publisher" });
+ });
+
+ 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.inputBox.val()).toBe("");
+ expect(this.view.onInputBoxInput).toHaveBeenCalled();
+ });
+ });
+
+ 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"}
+ ]);
+ });
+
+ 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);
+ expect(this.view.typeaheadInput.val()).toBe("");
+ });
+ });
+
+ 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 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 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/publisher_view_spec.js b/spec/javascripts/app/views/publisher_view_spec.js
index 3d9ea4559..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();
@@ -430,7 +429,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 = {};
@@ -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/search_base_view_spec.js b/spec/javascripts/app/views/search_base_view_spec.js
new file mode 100644
index 000000000..902176ec1
--- /dev/null
+++ b/spec/javascripts/app/views/search_base_view_spec.js
@@ -0,0 +1,248 @@
+describe("app.views.SearchBase", 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() {
+ spyOn(app.views.SearchBase.prototype, "setupBloodhound").and.callThrough();
+ this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")});
+ expect(app.views.SearchBase.prototype.setupBloodhound).toHaveBeenCalled();
+ });
+
+ 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");
+ this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")});
+ expect(app.views.SearchBase.prototype.setupTypeahead).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 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("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();
+ });
+
+ 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("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);
+ });
+
+ 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", typeaheadInput: $("#q")});
+ });
+
+ 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("setupMouseSelectionEvents", function() {
+ beforeEach(function() {
+ this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")});
+ this.view.bloodhound.add(this.bloodhoundData);
+ });
+
+ 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);
+
+ 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("_deselectAllSuggestions", function() {
+ beforeEach(function() {
+ this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")});
+ this.view.bloodhound.add(this.bloodhoundData);
+ this.search(this.view, "user");
+ });
+
+ it("deselects all suggestions", function() {
+ $(".tt-suggestion").addClass(".tt-cursor");
+ this.view._deselectAllSuggestions();
+ expect($(".tt-suggestion.tt-cursor").length).toBe(0);
+
+ $(".tt-suggestion:eq(1)").addClass(".tt-cursor");
+ this.view._deselectAllSuggestions();
+ expect($(".tt-suggestion.tt-cursor").length).toBe(0);
+ });
+ });
+
+ describe("_selectSuggestion", function() {
+ beforeEach(function() {
+ this.view = new app.views.SearchBase({el: "#search_people_form", typeaheadInput: $("#q")});
+ this.view.bloodhound.add(this.bloodhoundData);
+ this.search(this.view, "user");
+ });
+
+ 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 19fc0ccfb..74e4831eb 100644
--- a/spec/javascripts/app/views/search_view_spec.js
+++ b/spec/javascripts/app/views/search_view_spec.js
@@ -1,21 +1,22 @@
describe("app.views.Search", function() {
- beforeEach(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();
+ 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 setupTypeahead", function() {
- spyOn(app.views.Search.prototype, "setupTypeahead");
- new app.views.Search({ el: "#search_people_form" });
- expect(app.views.Search.prototype.setupTypeahead).toHaveBeenCalled();
+ 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);
});
});
@@ -44,35 +45,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")}
- ]);
- });
- });
- });
});
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;
- }
-}
-};