Using Typeahead for mentionning box
This commit is contained in:
parent
e0d6da7ad7
commit
c9f87796cc
12 changed files with 510 additions and 574 deletions
322
app/assets/javascripts/app/views/publisher/mention_view.js
Normal file
322
app/assets/javascripts/app/views/publisher/mention_view.js
Normal file
|
|
@ -0,0 +1,322 @@
|
||||||
|
//= require ../search_base_view
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is based on jQuery.mentionsInput by Kenneth Auchenberg
|
||||||
|
* licensed under MIT License - http://www.opensource.org/licenses/mit-license.php
|
||||||
|
* Website: https://podio.github.io/jquery-mentions-input/
|
||||||
|
*/
|
||||||
|
|
||||||
|
app.views.PublisherMention = app.views.SearchBase.extend({
|
||||||
|
KEYS: {
|
||||||
|
BACKSPACE: 8, TAB: 9, RETURN: 13, ESC: 27, LEFT: 37, UP: 38,
|
||||||
|
RIGHT: 39, DOWN: 40, COMMA: 188, SPACE: 32, HOME: 36, END: 35
|
||||||
|
},
|
||||||
|
|
||||||
|
settings: {
|
||||||
|
triggerChar: "@",
|
||||||
|
minChars: 2,
|
||||||
|
templates: {
|
||||||
|
wrapper: _.template("<div class='mentions-input-box'></div>"),
|
||||||
|
mentionsOverlay: _.template("<div class='mentions-box'><div class='mentions'><div></div></div></div>"),
|
||||||
|
mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"),
|
||||||
|
mentionItemHighlight: _.template("<strong><span><%= name %></span></strong>")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
utils: {
|
||||||
|
setCaretPosition: function(domNode, caretPos){
|
||||||
|
if(domNode.createTextRange){
|
||||||
|
var range = domNode.createTextRange();
|
||||||
|
range.move("character", caretPos);
|
||||||
|
range.select();
|
||||||
|
} else{
|
||||||
|
if(domNode.selectionStart){
|
||||||
|
domNode.focus();
|
||||||
|
domNode.setSelectionRange(caretPos, caretPos);
|
||||||
|
} else{
|
||||||
|
domNode.focus();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
rtrim: function(string){
|
||||||
|
return string.replace(/\s+$/, "");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
events: {
|
||||||
|
"keydown #status_message_fake_text": "onInputBoxKeyDown",
|
||||||
|
"keypress #status_message_fake_text": "onInputBoxKeyPress",
|
||||||
|
"input #status_message_fake_text": "onInputBoxInput",
|
||||||
|
"click #status_message_fake_text": "onInputBoxClick",
|
||||||
|
"blur #status_message_fake_text": "onInputBoxBlur"
|
||||||
|
},
|
||||||
|
|
||||||
|
initialize: function(){
|
||||||
|
this.mentionsCollection = [];
|
||||||
|
this.inputBuffer = [];
|
||||||
|
this.currentDataQuery = "";
|
||||||
|
this.mentionChar = "\u200B";
|
||||||
|
|
||||||
|
this.elmInputBox = this.$el.find("#status_message_fake_text");
|
||||||
|
this.elmInputWrapper = this.elmInputBox.parent();
|
||||||
|
this.elmWrapperBox = $(this.settings.templates.wrapper());
|
||||||
|
this.elmInputBox.wrapAll(this.elmWrapperBox);
|
||||||
|
this.elmWrapperBox = this.elmInputWrapper.find("> div").first();
|
||||||
|
this.elmMentionsOverlay = $(this.settings.templates.mentionsOverlay());
|
||||||
|
this.elmMentionsOverlay.prependTo(this.elmWrapperBox);
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
this.getSearchInput().on("typeahead:select", function(evt, datum){
|
||||||
|
self.processMention(datum);
|
||||||
|
self.resetMentionBox();
|
||||||
|
self.addToFilteredResults(datum);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.getSearchInput().on("typeahead:render", function(){
|
||||||
|
self.select(self.$(".tt-menu .tt-suggestion").first());
|
||||||
|
});
|
||||||
|
|
||||||
|
this.completeSetup(this.getSearchInput());
|
||||||
|
|
||||||
|
this.$el.find(".twitter-typeahead").css({position: "absolute", left: "-1px"});
|
||||||
|
this.$el.find(".twitter-typeahead .tt-menu").css("margin-top", 0);
|
||||||
|
},
|
||||||
|
|
||||||
|
clearBuffer: function(){
|
||||||
|
this.inputBuffer.length = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
updateMentionsCollection: function(){
|
||||||
|
var inputText = this.getInputBoxValue();
|
||||||
|
|
||||||
|
this.mentionsCollection = _.reject(this.mentionsCollection, function(mention){
|
||||||
|
return !mention.name || inputText.indexOf(mention.name) === -1;
|
||||||
|
});
|
||||||
|
this.mentionsCollection = _.compact(this.mentionsCollection);
|
||||||
|
},
|
||||||
|
|
||||||
|
addMention: function(person){
|
||||||
|
if(!person || !person.name || !person.handle){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// This is needed for processing preview
|
||||||
|
/* jshint camelcase: false */
|
||||||
|
person.diaspora_id = person.handle;
|
||||||
|
/* jshint camelcase: true */
|
||||||
|
this.mentionsCollection.push(person);
|
||||||
|
},
|
||||||
|
|
||||||
|
processMention: function(mention){
|
||||||
|
var currentMessage = this.getInputBoxValue();
|
||||||
|
|
||||||
|
// Using a regex to figure out positions
|
||||||
|
var regex = new RegExp("\\" + this.settings.triggerChar + this.currentDataQuery, "gi");
|
||||||
|
regex.exec(currentMessage);
|
||||||
|
|
||||||
|
var startCaretPosition = regex.lastIndex - this.currentDataQuery.length - 1;
|
||||||
|
var currentCaretPosition = regex.lastIndex;
|
||||||
|
|
||||||
|
var start = currentMessage.substr(0, startCaretPosition);
|
||||||
|
var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
|
||||||
|
var startEndIndex = (start + mention.name).length + 1;
|
||||||
|
|
||||||
|
this.addMention(mention);
|
||||||
|
|
||||||
|
// Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
|
||||||
|
this.clearBuffer();
|
||||||
|
this.currentDataQuery = "";
|
||||||
|
this.resetMentionBox();
|
||||||
|
|
||||||
|
// Mentions & syntax message
|
||||||
|
var updatedMessageText = start + this.mentionChar + mention.name + end;
|
||||||
|
this.elmInputBox.val(updatedMessageText);
|
||||||
|
this.updateValues();
|
||||||
|
|
||||||
|
// Set correct focus and selection
|
||||||
|
this.elmInputBox.focus();
|
||||||
|
this.utils.setCaretPosition(this.elmInputBox[0], startEndIndex);
|
||||||
|
},
|
||||||
|
|
||||||
|
updateValues: function(){
|
||||||
|
var syntaxMessage = this.getInputBoxValue();
|
||||||
|
var mentionText = this.getInputBoxValue();
|
||||||
|
this.clearFilteredResults();
|
||||||
|
|
||||||
|
var self = this;
|
||||||
|
|
||||||
|
_.each(this.mentionsCollection, function(mention){
|
||||||
|
self.addToFilteredResults(mention);
|
||||||
|
|
||||||
|
var mentionVal = self.mentionChar + mention.name;
|
||||||
|
|
||||||
|
var textSyntax = self.settings.templates.mentionItemSyntax(mention);
|
||||||
|
syntaxMessage = syntaxMessage.replace(mentionVal, textSyntax);
|
||||||
|
|
||||||
|
var textHighlight = self.settings.templates.mentionItemHighlight({ name: _.escape(mention.name) });
|
||||||
|
mentionText = mentionText.replace(mentionVal, textHighlight);
|
||||||
|
});
|
||||||
|
|
||||||
|
mentionText = mentionText.replace(/\n/g, "<br/>");
|
||||||
|
mentionText = mentionText.replace(/ {2}/g, " ");
|
||||||
|
|
||||||
|
this.elmInputBox.data("messageText", syntaxMessage);
|
||||||
|
this.elmMentionsOverlay.find("div > div").html(mentionText);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Let us prefill the publisher with a mention list
|
||||||
|
* @param persons List of people to mention in a post;
|
||||||
|
* JSON object of form { handle: <diaspora handle>, name: <name>, ... }
|
||||||
|
*/
|
||||||
|
prefillMention: function(persons){
|
||||||
|
var self = this;
|
||||||
|
_.each(persons, function(person){
|
||||||
|
self.addMention(person);
|
||||||
|
self.addToFilteredResults(person);
|
||||||
|
self.elmInputBox.val(self.mentionChar + person.name);
|
||||||
|
self.updateValues();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
selectNextResult: function(evt){
|
||||||
|
if(this.isVisible()){
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.getSelected().size() !== 1 || this.getSelected().next().size() !== 1){
|
||||||
|
this.getSelected().removeClass("tt-cursor");
|
||||||
|
this.$el.find(".tt-suggestion").first().addClass("tt-cursor");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.getSelected().removeClass("tt-cursor").next().addClass("tt-cursor");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
selectPreviousResult: function(evt){
|
||||||
|
if(this.isVisible()){
|
||||||
|
evt.preventDefault();
|
||||||
|
evt.stopPropagation();
|
||||||
|
}
|
||||||
|
|
||||||
|
if(this.getSelected().size() !== 1 || this.getSelected().prev().size() !== 1){
|
||||||
|
this.getSelected().removeClass("tt-cursor");
|
||||||
|
this.$el.find(".tt-suggestion").last().addClass("tt-cursor");
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
this.getSelected().removeClass("tt-cursor").prev().addClass("tt-cursor");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputBoxKeyPress: function(e){
|
||||||
|
if(e.keyCode !== this.KEYS.BACKSPACE){
|
||||||
|
var typedValue = String.fromCharCode(e.which || e.keyCode);
|
||||||
|
this.inputBuffer.push(typedValue);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputBoxInput: function(){
|
||||||
|
this.updateValues();
|
||||||
|
this.updateMentionsCollection();
|
||||||
|
|
||||||
|
var triggerCharIndex = _.lastIndexOf(this.inputBuffer, this.settings.triggerChar);
|
||||||
|
if(triggerCharIndex > -1){
|
||||||
|
this.currentDataQuery = this.inputBuffer.slice(triggerCharIndex + 1).join("");
|
||||||
|
this.currentDataQuery = this.utils.rtrim(this.currentDataQuery);
|
||||||
|
|
||||||
|
this.showMentionBox();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputBoxKeyDown: function(e){
|
||||||
|
// This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
|
||||||
|
if(e.keyCode === this.KEYS.LEFT || e.keyCode === this.KEYS.RIGHT ||
|
||||||
|
e.keyCode === this.KEYS.HOME || e.keyCode === this.KEYS.END){
|
||||||
|
_.defer(this.clearBuffer);
|
||||||
|
|
||||||
|
// IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
|
||||||
|
// to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack
|
||||||
|
// to force updateValues() to fire when backspace/delete is pressed in IE9.
|
||||||
|
if(navigator.userAgent.indexOf("MSIE 9") > -1){
|
||||||
|
_.defer(this.updateValues);
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(e.keyCode === this.KEYS.BACKSPACE){
|
||||||
|
this.inputBuffer = this.inputBuffer.slice(0, this.inputBuffer.length - 1);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(!this.isVisible){
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(e.keyCode){
|
||||||
|
case this.KEYS.ESC:
|
||||||
|
case this.KEYS.SPACE:
|
||||||
|
this.resetMentionBox();
|
||||||
|
break;
|
||||||
|
case this.KEYS.UP:
|
||||||
|
this.selectPreviousResult(e);
|
||||||
|
break;
|
||||||
|
case this.KEYS.DOWN:
|
||||||
|
this.selectNextResult(e);
|
||||||
|
break;
|
||||||
|
case this.KEYS.RETURN:
|
||||||
|
case this.KEYS.TAB:
|
||||||
|
if(this.getSelected().size() === 1){
|
||||||
|
this.getSelected().click();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputBoxClick: function(){
|
||||||
|
this.resetMentionBox();
|
||||||
|
},
|
||||||
|
|
||||||
|
onInputBoxBlur: function(){
|
||||||
|
this.resetMentionBox();
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: function(){
|
||||||
|
this.elmInputBox.val("");
|
||||||
|
this.mentionsCollection.length = 0;
|
||||||
|
this.clearFilteredResults();
|
||||||
|
this.updateValues();
|
||||||
|
},
|
||||||
|
|
||||||
|
showMentionBox: function(){
|
||||||
|
this.getSearchInput().typeahead("val", this.currentDataQuery);
|
||||||
|
this.getSearchInput().typeahead("open");
|
||||||
|
},
|
||||||
|
|
||||||
|
resetMentionBox: function(){
|
||||||
|
this.getSearchInput().typeahead("val", "");
|
||||||
|
this.getSearchInput().typeahead("close");
|
||||||
|
},
|
||||||
|
|
||||||
|
getInputBoxValue: function(){
|
||||||
|
return $.trim(this.elmInputBox.val());
|
||||||
|
},
|
||||||
|
|
||||||
|
isVisible: function(){
|
||||||
|
return this.$el.find(".tt-menu").is(":visible");
|
||||||
|
},
|
||||||
|
|
||||||
|
getSearchInput: function(){
|
||||||
|
if(this.$el.find(".typeahead-mention-box").length === 0){
|
||||||
|
this.elmInputBox.after("<input class='typeahead-mention-box hidden' type='text'/>");
|
||||||
|
}
|
||||||
|
return this.$el.find(".typeahead-mention-box");
|
||||||
|
},
|
||||||
|
|
||||||
|
getTextForSubmit: function(){
|
||||||
|
return this.mentionsCollection.length ? this.elmInputBox.data("messageText") : this.getInputBoxValue();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -31,6 +31,7 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
|
|
||||||
initialize : function(opts){
|
initialize : function(opts){
|
||||||
this.standalone = opts ? opts.standalone : false;
|
this.standalone = opts ? opts.standalone : false;
|
||||||
|
this.prefillMention = opts && opts.prefillMention ? opts.prefillMention : undefined;
|
||||||
this.disabled = false;
|
this.disabled = false;
|
||||||
|
|
||||||
// init shortcut references to the various elements
|
// init shortcut references to the various elements
|
||||||
|
|
@ -41,9 +42,6 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
this.previewEl = this.$("button.post_preview_button");
|
this.previewEl = this.$("button.post_preview_button");
|
||||||
this.photozoneEl = this.$("#photodropzone");
|
this.photozoneEl = this.$("#photodropzone");
|
||||||
|
|
||||||
// init mentions plugin
|
|
||||||
Mentions.initialize(this.inputEl);
|
|
||||||
|
|
||||||
// if there is data in the publisher we ask for a confirmation
|
// if there is data in the publisher we ask for a confirmation
|
||||||
// before the user is able to leave the page
|
// before the user is able to leave the page
|
||||||
$(window).on("beforeunload", _.bind(this._beforeUnload, this));
|
$(window).on("beforeunload", _.bind(this._beforeUnload, this));
|
||||||
|
|
@ -100,6 +98,11 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
},
|
},
|
||||||
|
|
||||||
initSubviews: function() {
|
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");
|
var form = this.$(".content_creation form");
|
||||||
|
|
||||||
this.view_services = new app.views.PublisherServices({
|
this.view_services = new app.views.PublisherServices({
|
||||||
|
|
@ -265,32 +268,6 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
return photos;
|
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) {
|
getPollData: function(serializedForm) {
|
||||||
var poll;
|
var poll;
|
||||||
var pollQuestion = serializedForm.poll_question;
|
var pollQuestion = serializedForm.poll_question;
|
||||||
|
|
@ -321,7 +298,7 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
|
|
||||||
var serializedForm = $(evt.target).closest("form").serializeObject();
|
var serializedForm = $(evt.target).closest("form").serializeObject();
|
||||||
var photos = this.getUploadedPhotos();
|
var photos = this.getUploadedPhotos();
|
||||||
var mentionedPeople = this.getMentionedPeople(serializedForm);
|
var mentionedPeople = this.mention.mentionsCollection;
|
||||||
var date = (new Date()).toISOString();
|
var date = (new Date()).toISOString();
|
||||||
var poll = this.getPollData(serializedForm);
|
var poll = this.getPollData(serializedForm);
|
||||||
var locationCoords = serializedForm["location[coords]"];
|
var locationCoords = serializedForm["location[coords]"];
|
||||||
|
|
@ -395,7 +372,7 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
autosize.update(this.inputEl);
|
autosize.update(this.inputEl);
|
||||||
|
|
||||||
// remove mentions
|
// remove mentions
|
||||||
this.inputEl.mentionsInput("reset");
|
this.mention.reset();
|
||||||
|
|
||||||
// remove photos
|
// remove photos
|
||||||
this.photozoneEl.find("li").remove();
|
this.photozoneEl.find("li").remove();
|
||||||
|
|
@ -450,9 +427,6 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
this.$el.removeClass("closed");
|
this.$el.removeClass("closed");
|
||||||
this.wrapperEl.addClass("active");
|
this.wrapperEl.addClass("active");
|
||||||
autosize.update(this.inputEl);
|
autosize.update(this.inputEl);
|
||||||
|
|
||||||
// fetch contacts for mentioning
|
|
||||||
Mentions.fetchContacts();
|
|
||||||
return this;
|
return this;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
@ -521,9 +495,7 @@ app.views.Publisher = Backbone.View.extend({
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
this.checkSubmitAvailability();
|
this.checkSubmitAvailability();
|
||||||
this.inputEl.mentionsInput("val", function(value){
|
this.hiddenInputEl.val(this.mention.getTextForSubmit());
|
||||||
self.hiddenInputEl.val(value);
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
|
|
||||||
_beforeUnload: function(e) {
|
_beforeUnload: function(e) {
|
||||||
|
|
|
||||||
140
app/assets/javascripts/app/views/search_base_view.js
Normal file
140
app/assets/javascripts/app/views/search_base_view.js
Normal file
|
|
@ -0,0 +1,140 @@
|
||||||
|
app.views.SearchBase = app.views.Base.extend({
|
||||||
|
completeSetup: function(typeaheadElement){
|
||||||
|
this.typeaheadElement = $(typeaheadElement);
|
||||||
|
this.setupBloodhound();
|
||||||
|
this.setupTypeahead();
|
||||||
|
this.bindSelectionEvents();
|
||||||
|
this.resultsTofilter = [];
|
||||||
|
},
|
||||||
|
|
||||||
|
setupBloodhound: function() {
|
||||||
|
var self = this;
|
||||||
|
var bloodhoundConf = {
|
||||||
|
datumTokenizer: function(datum) {
|
||||||
|
var nameTokens = Bloodhound.tokenizers.nonword(datum.name);
|
||||||
|
var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : [];
|
||||||
|
return nameTokens.concat(handleTokens);
|
||||||
|
},
|
||||||
|
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
||||||
|
prefetch: {
|
||||||
|
url: "/contacts.json",
|
||||||
|
transform: this.transformBloodhoundResponse,
|
||||||
|
cache: false
|
||||||
|
},
|
||||||
|
sufficient: 5
|
||||||
|
};
|
||||||
|
|
||||||
|
// The publisher does not define an additionnal source for searchin
|
||||||
|
// This prevents tests from failing when this additionnal source isn't set
|
||||||
|
if(this.searchFormAction !== undefined){
|
||||||
|
bloodhoundConf.remote = {
|
||||||
|
url: this.searchFormAction + ".json?q=%QUERY",
|
||||||
|
wildcard: "%QUERY",
|
||||||
|
transform: this.transformBloodhoundResponse
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bloodhound = new Bloodhound(bloodhoundConf);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Custom searching function that let us filter contacts from prefetched Bloodhound results.
|
||||||
|
*/
|
||||||
|
this.bloodhound.customSearch = function(query, sync, async){
|
||||||
|
var filterResults = function(datums){
|
||||||
|
return _.filter(datums, function(result){
|
||||||
|
if(result.handle){
|
||||||
|
return !_.contains(self.resultsTofilter, result.handle);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
var _sync = function(datums){
|
||||||
|
var results = filterResults(datums);
|
||||||
|
sync(results);
|
||||||
|
};
|
||||||
|
|
||||||
|
self.bloodhound.search(query, _sync, async);
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
setupTypeahead: function() {
|
||||||
|
this.typeaheadElement.typeahead({
|
||||||
|
hint: false,
|
||||||
|
highlight: true,
|
||||||
|
minLength: 2
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "search",
|
||||||
|
display: "name",
|
||||||
|
limit: 5,
|
||||||
|
source: this.bloodhound.customSearch,
|
||||||
|
templates: {
|
||||||
|
/* jshint camelcase: false */
|
||||||
|
suggestion: HandlebarsTemplates.search_suggestion_tpl
|
||||||
|
/* jshint camelcase: true */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
transformBloodhoundResponse: function(response) {
|
||||||
|
return response.map(function(data){
|
||||||
|
// person
|
||||||
|
if(data.handle){
|
||||||
|
data.person = true;
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashtag
|
||||||
|
return {
|
||||||
|
hashtag: true,
|
||||||
|
name: data.name,
|
||||||
|
url: Routes.tag(data.name.substring(1))
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This bind events to highlight a result when overing it
|
||||||
|
*/
|
||||||
|
bindSelectionEvents: function(){
|
||||||
|
var self = this;
|
||||||
|
var onover = function(evt){
|
||||||
|
var isSuggestion = $(evt.target).is(".tt-suggestion");
|
||||||
|
var suggestion = isSuggestion ? $(evt.target) : $(evt.target).parent(".tt-suggestion");
|
||||||
|
if(suggestion){
|
||||||
|
self.select(suggestion);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
this.typeaheadElement.on("typeahead:render", function(){
|
||||||
|
self.$(".tt-menu *").off("mouseover", onover);
|
||||||
|
self.$(".tt-menu .tt-suggestion").on("mouseover", onover);
|
||||||
|
self.$(".tt-menu .tt-suggestion *").on("mouseover", onover);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This function lets us filter contacts from Bloodhound's responses
|
||||||
|
* It is used by app.views.PublisherMention to filter already mentionned
|
||||||
|
* people in post. Does not filter tags from results.
|
||||||
|
* @param person a JSON object of form { handle: <diaspora handle>, ... } representing the filtered contact
|
||||||
|
*/
|
||||||
|
addToFilteredResults: function(person){
|
||||||
|
if(person.handle){
|
||||||
|
this.resultsTofilter.push(person.handle);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
clearFilteredResults: function(){
|
||||||
|
this.resultsTofilter.length = 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
getSelected: function(){
|
||||||
|
return this.$el.find(".tt-cursor");
|
||||||
|
},
|
||||||
|
|
||||||
|
select: function(el){
|
||||||
|
this.getSelected().removeClass("tt-cursor");
|
||||||
|
$(el).addClass("tt-cursor");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,96 +1,52 @@
|
||||||
// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later
|
// @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: {
|
events: {
|
||||||
"focusin #q": "toggleSearchActive",
|
"focusin #q": "toggleSearchActive",
|
||||||
"focusout #q": "toggleSearchActive",
|
"focusout #q": "toggleSearchActive",
|
||||||
"keypress #q": "inputKeypress",
|
"keypress #q": "inputKeypress"
|
||||||
},
|
},
|
||||||
|
|
||||||
initialize: function(){
|
initialize: function(){
|
||||||
this.searchFormAction = this.$el.attr("action");
|
this.searchFormAction = this.$el.attr("action");
|
||||||
this.searchInput = this.$("#q");
|
this.completeSetup(this.getTypeaheadElement());
|
||||||
|
this.bindMoreSelectionEvents();
|
||||||
// constructs the suggestion engine
|
this.getTypeaheadElement().on("typeahead:select", this.suggestionSelected);
|
||||||
this.setupBloodhound();
|
|
||||||
this.setupTypeahead();
|
|
||||||
this.searchInput.on("typeahead:select", this.suggestionSelected);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
setupBloodhound: function() {
|
/**
|
||||||
this.bloodhound = new Bloodhound({
|
* This bind events to unselect all results when leaving the menu
|
||||||
datumTokenizer: function(datum) {
|
*/
|
||||||
var nameTokens = Bloodhound.tokenizers.nonword(datum.name);
|
bindMoreSelectionEvents: function(){
|
||||||
var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : [];
|
var self = this;
|
||||||
return nameTokens.concat(handleTokens);
|
var onleave = function(){
|
||||||
},
|
self.$(".tt-cursor").removeClass("tt-cursor");
|
||||||
queryTokenizer: Bloodhound.tokenizers.whitespace,
|
};
|
||||||
remote: {
|
|
||||||
url: this.searchFormAction + ".json?q=%QUERY",
|
this.getTypeaheadElement().on("typeahead:render", function(){
|
||||||
wildcard: "%QUERY",
|
self.$(".tt-menu").off("mouseleave", onleave);
|
||||||
transform: this.transformBloodhoundResponse
|
self.$(".tt-menu").on("mouseleave", onleave);
|
||||||
},
|
|
||||||
prefetch: {
|
|
||||||
url: "/contacts.json",
|
|
||||||
transform: this.transformBloodhoundResponse,
|
|
||||||
cache: false
|
|
||||||
},
|
|
||||||
sufficient: 5
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
|
||||||
setupTypeahead: function() {
|
getTypeaheadElement: function(){
|
||||||
this.searchInput.typeahead({
|
return this.$("#q");
|
||||||
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) {
|
toggleSearchActive: function(evt){
|
||||||
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)
|
// jQuery produces two events for focus/blur (for bubbling)
|
||||||
// don't rely on which event arrives first, by allowing for both variants
|
// don't rely on which event arrives first, by allowing for both variants
|
||||||
var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1);
|
var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1);
|
||||||
$(evt.target).toggleClass("active", isActive);
|
$(evt.target).toggleClass("active", isActive);
|
||||||
},
|
},
|
||||||
|
|
||||||
suggestionSelected: function(evt, datum) {
|
inputKeypress: function(evt){
|
||||||
window.location = datum.url;
|
if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0){
|
||||||
},
|
|
||||||
|
|
||||||
inputKeypress: function(evt) {
|
|
||||||
if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0) {
|
|
||||||
$(evt.target).closest("form").submit();
|
$(evt.target).closest("form").submit();
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
suggestionSelected: function(evt, datum){
|
||||||
|
window.location = datum.url;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// @license-ends
|
// @license-ends
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,6 @@
|
||||||
//= require rails-timeago
|
//= require rails-timeago
|
||||||
//= require jquery.events.input
|
//= require jquery.events.input
|
||||||
//= require jakobmattsson-jquery-elastic
|
//= require jakobmattsson-jquery-elastic
|
||||||
//= require jquery.mentionsInput
|
|
||||||
//= require jquery.infinitescroll-custom
|
//= require jquery.infinitescroll-custom
|
||||||
//= require jquery-ui/core
|
//= require jquery-ui/core
|
||||||
//= require jquery-ui/widget
|
//= require jquery-ui/widget
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
&.active textarea {
|
&.active textarea {
|
||||||
min-height: 70px;
|
min-height: 90px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.markdownIndications {
|
.markdownIndications {
|
||||||
|
|
@ -118,6 +118,8 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:not(.with_location) #location_container { display: none; }
|
||||||
|
|
||||||
&.with_location .loader {
|
&.with_location .loader {
|
||||||
height: 20px;
|
height: 20px;
|
||||||
width: 20px;
|
width: 20px;
|
||||||
|
|
|
||||||
|
|
@ -12,10 +12,9 @@
|
||||||
line-height: 20px;
|
line-height: 20px;
|
||||||
&.tt-cursor {
|
&.tt-cursor {
|
||||||
background-color: $brand-primary;
|
background-color: $brand-primary;
|
||||||
|
border-top: 1px solid $brand-primary;
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover { background-color: lighten($navbar-inverse-bg, 10%); }
|
|
||||||
|
|
||||||
&.search-suggestion-person {
|
&.search-suggestion-person {
|
||||||
padding: 8px;
|
padding: 8px;
|
||||||
.avatar {
|
.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
|
- content_for :page_title do
|
||||||
= @person.name
|
= @person.name
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,12 +2,6 @@
|
||||||
-# licensed under the Affero General Public License version 3 or later. See
|
-# licensed under the Affero General Public License version 3 or later. See
|
||||||
-# the COPYRIGHT file.
|
-# 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
|
- content_for :page_title do
|
||||||
= @person.name
|
= @person.name
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,8 @@
|
||||||
:javascript
|
:javascript
|
||||||
$(function() {
|
$(function() {
|
||||||
app.publisher = new app.views.Publisher({
|
app.publisher = new app.views.Publisher({
|
||||||
standalone: true
|
standalone: true,
|
||||||
|
prefillMention: #{json_escape @person.to_json}
|
||||||
});
|
});
|
||||||
app.publisher.open();
|
app.publisher.open();
|
||||||
$("#publisher").bind('ajax:success', function(){
|
$("#publisher").bind('ajax:success', function(){
|
||||||
|
|
|
||||||
|
|
@ -1,24 +1,24 @@
|
||||||
And /^Alice has a post mentioning Bob$/ do
|
And /^Alice has a post mentioning Bob$/ do
|
||||||
alice = User.find_by_email 'alice@alice.alice'
|
alice = User.find_by_email "alice@alice.alice"
|
||||||
bob = User.find_by_email 'bob@bob.bob'
|
bob = User.find_by_email "bob@bob.bob"
|
||||||
aspect = alice.aspects.where(:name => "Besties").first
|
aspect = alice.aspects.where(:name => "Besties").first
|
||||||
alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect)
|
alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect)
|
||||||
end
|
end
|
||||||
|
|
||||||
And /^Alice has (\d+) posts mentioning Bob$/ do |n|
|
And /^Alice has (\d+) posts mentioning Bob$/ do |n|
|
||||||
n.to_i.times do
|
n.to_i.times do
|
||||||
alice = User.find_by_email 'alice@alice.alice'
|
alice = User.find_by_email "alice@alice.alice"
|
||||||
bob = User.find_by_email 'bob@bob.bob'
|
bob = User.find_by_email "bob@bob.bob"
|
||||||
aspect = alice.aspects.where(:name => "Besties").first
|
aspect = alice.aspects.where(:name => "Besties").first
|
||||||
alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect)
|
alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
And /^I mention Alice in the publisher$/ do
|
And /^I mention Alice in the publisher$/ do
|
||||||
alice = User.find_by_email 'alice@alice.alice'
|
write_in_publisher("@alice")
|
||||||
write_in_publisher("@{Alice Smith ; #{alice.person.diaspora_handle}}")
|
step %(I click on the first user in the mentions dropdown list)
|
||||||
end
|
end
|
||||||
|
|
||||||
And /^I click on the first user in the mentions dropdown list$/ do
|
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
|
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
|
|
||||||
Loading…
Reference in a new issue