Merge pull request #6728 from svbergerem/mentions-typeahead

Rewrite mentions input using typeahead.js
This commit is contained in:
Dennis Schubert 2016-03-05 04:54:41 +01:00
commit cc5a3997d2
42 changed files with 1411 additions and 961 deletions

View file

@ -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)

View file

@ -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

View file

@ -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)!

View file

@ -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();

View file

@ -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();
}

View file

@ -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;
}

View file

@ -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();
}
}

View file

@ -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();
}
}

View 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();
}
});

View file

@ -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) {

View 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); }
},
});

View file

@ -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

View file

@ -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();

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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;
}
}
}

View file

@ -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; }
}
}

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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();

View file

@ -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"),

View file

@ -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(){

View file

@ -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();

View file

@ -66,6 +66,7 @@
"app",
"Diaspora",
"Keycodes",
"Mentions",
"PosixBracketExpressions"
]

View file

@ -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

View file

@ -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, '&nbsp; ');
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

View 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;
}
}
};

View file

@ -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

View file

@ -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();

View file

@ -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();
});

View file

@ -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();

View file

@ -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();
});

View 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");
});
});
});

View file

@ -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();

View 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);
});
});
});

View file

@ -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")}
]);
});
});
});
});

View file

@ -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();
});

View 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);
});
});
});

View file

@ -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;
}
}
};