Merge pull request #6728 from svbergerem/mentions-typeahead
Rewrite mentions input using typeahead.js
This commit is contained in:
commit
cc5a3997d2
42 changed files with 1411 additions and 961 deletions
|
|
@ -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)
|
||||
|
|
|
|||
2
Gemfile
2
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
|
||||
|
|
|
|||
|
|
@ -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)!
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
222
app/assets/javascripts/app/views/publisher/mention_view.js
Normal file
222
app/assets/javascripts/app/views/publisher/mention_view.js
Normal file
|
|
@ -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("<strong><span><%= name %></span></strong>")
|
||||
},
|
||||
|
||||
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 <strong><span>user1</span></strong>" 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: <diaspora handle>, name: <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();
|
||||
}
|
||||
});
|
||||
|
|
@ -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("<div id=\"location\"></div>");
|
||||
this.wrapperEl.addClass("with_location");
|
||||
this.$(".location-container").append("<div id=\"location\"></div>");
|
||||
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) {
|
||||
|
|
|
|||
124
app/assets/javascripts/app/views/search_base_view.js
Normal file
124
app/assets/javascripts/app/views/search_base_view.js
Normal file
|
|
@ -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); }
|
||||
},
|
||||
});
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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"),
|
||||
|
|
|
|||
|
|
@ -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(){
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -66,6 +66,7 @@
|
|||
|
||||
"app",
|
||||
"Diaspora",
|
||||
"Keycodes",
|
||||
"Mentions",
|
||||
"PosixBracketExpressions"
|
||||
]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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('<div class="mentions-input-box"></div>'),
|
||||
autocompleteList : _.template('<div class="mentions-autocomplete-list"></div>'),
|
||||
autocompleteListItem : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %></li>'),
|
||||
autocompleteListItemAvatar : _.template('<img src="<%= avatar %>" />'),
|
||||
autocompleteListItemIcon : _.template('<div class="icon <%= icon %>"></div>'),
|
||||
mentionsOverlay : _.template('<div class="mentions-box"><div class="mentions"><div></div></div></div>'),
|
||||
mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
|
||||
mentionItemHighlight : _.template('<strong><span><%= value %></span></strong>')
|
||||
}
|
||||
};
|
||||
|
||||
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"), "<b>$1</b>");
|
||||
},
|
||||
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, '<br />');
|
||||
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 = $("<ul>").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
|
||||
117
lib/assets/javascripts/keycodes.js
Normal file
117
lib/assets/javascripts/keycodes.js
Normal file
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
472
spec/javascripts/app/views/publisher_mention_view_spec.js
Normal file
472
spec/javascripts/app/views/publisher_mention_view_spec.js
Normal file
|
|
@ -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 <strong><span>user1</span></strong>\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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -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('<div id="location_container"></div>');
|
||||
setFixtures('<div class="location-container"></div>');
|
||||
|
||||
// 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();
|
||||
|
|
|
|||
248
spec/javascripts/app/views/search_base_view_spec.js
Normal file
248
spec/javascripts/app/views/search_base_view_spec.js
Normal file
|
|
@ -0,0 +1,248 @@
|
|||
describe("app.views.SearchBase", function() {
|
||||
beforeEach(function() {
|
||||
spec.content().html(
|
||||
"<form action='/search' id='search_people_form'><input id='q' name='q' type='search'/></form>"
|
||||
);
|
||||
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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
@ -1,21 +1,22 @@
|
|||
describe("app.views.Search", function() {
|
||||
beforeEach(function(){
|
||||
beforeEach(function() {
|
||||
spec.content().html(
|
||||
"<form action='/search' id='search_people_form'><input id='q' name='q' type='search'></input></form>"
|
||||
"<form action='/search' id='search_people_form'><input id='q' name='q' type='search'/></form>"
|
||||
);
|
||||
});
|
||||
|
||||
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")}
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
13
spec/javascripts/lib/keycodes_spec.js
Normal file
13
spec/javascripts/lib/keycodes_spec.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
117
vendor/assets/javascripts/keycodes.js
vendored
117
vendor/assets/javascripts/keycodes.js
vendored
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
Loading…
Reference in a new issue