diff --git a/app/assets/javascripts/app/views/publisher/mention_view.js b/app/assets/javascripts/app/views/publisher/mention_view.js
new file mode 100644
index 000000000..270e14161
--- /dev/null
+++ b/app/assets/javascripts/app/views/publisher/mention_view.js
@@ -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("
"),
+ mentionsOverlay: _.template(""),
+ mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"),
+ mentionItemHighlight: _.template("<%= name %>")
+ }
+ },
+
+ 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, "
");
+ 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: , 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("");
+ }
+ return this.$el.find(".typeahead-mention-box");
+ },
+
+ getTextForSubmit: function(){
+ return this.mentionsCollection.length ? this.elmInputBox.data("messageText") : this.getInputBoxValue();
+ }
+});
diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js
index 89e5e38f5..fd7d85a5e 100644
--- a/app/assets/javascripts/app/views/publisher_view.js
+++ b/app/assets/javascripts/app/views/publisher_view.js
@@ -31,6 +31,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 +42,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 +98,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({
@@ -265,32 +268,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 +298,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.mentionsCollection;
var date = (new Date()).toISOString();
var poll = this.getPollData(serializedForm);
var locationCoords = serializedForm["location[coords]"];
@@ -395,7 +372,7 @@ app.views.Publisher = Backbone.View.extend({
autosize.update(this.inputEl);
// remove mentions
- this.inputEl.mentionsInput("reset");
+ this.mention.reset();
// remove photos
this.photozoneEl.find("li").remove();
@@ -450,9 +427,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 +495,7 @@ app.views.Publisher = Backbone.View.extend({
var self = this;
this.checkSubmitAvailability();
- this.inputEl.mentionsInput("val", function(value){
- self.hiddenInputEl.val(value);
- });
+ this.hiddenInputEl.val(this.mention.getTextForSubmit());
},
_beforeUnload: function(e) {
diff --git a/app/assets/javascripts/app/views/search_base_view.js b/app/assets/javascripts/app/views/search_base_view.js
new file mode 100644
index 000000000..af7914122
--- /dev/null
+++ b/app/assets/javascripts/app/views/search_base_view.js
@@ -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: , ... } 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");
+ }
+});
diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js
index 6e8a1d272..65ed775c0 100644
--- a/app/assets/javascripts/app/views/search_view.js
+++ b/app/assets/javascripts/app/views/search_view.js
@@ -1,96 +1,52 @@
// @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");
- this.searchInput = this.$("#q");
-
- // constructs the suggestion engine
- this.setupBloodhound();
- this.setupTypeahead();
- this.searchInput.on("typeahead:select", this.suggestionSelected);
+ this.completeSetup(this.getTypeaheadElement());
+ this.bindMoreSelectionEvents();
+ this.getTypeaheadElement().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
+ /**
+ * This bind events to unselect all results when leaving the menu
+ */
+ bindMoreSelectionEvents: function(){
+ var self = this;
+ var onleave = function(){
+ self.$(".tt-cursor").removeClass("tt-cursor");
+ };
+
+ this.getTypeaheadElement().on("typeahead:render", function(){
+ self.$(".tt-menu").off("mouseleave", onleave);
+ self.$(".tt-menu").on("mouseleave", onleave);
});
},
- 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 */
- }
- });
+ getTypeaheadElement: function(){
+ return this.$("#q");
},
- 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) {
+ toggleSearchActive: function(evt){
// jQuery produces two events for focus/blur (for bubbling)
// don't rely on which event arrives first, by allowing for both variants
var isActive = (_.indexOf(["focus","focusin"], evt.type) !== -1);
$(evt.target).toggleClass("active", isActive);
},
- suggestionSelected: function(evt, datum) {
- window.location = datum.url;
- },
-
- inputKeypress: function(evt) {
- 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();
}
+ },
+
+ suggestionSelected: function(evt, datum){
+ window.location = datum.url;
}
});
// @license-ends
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 78da46496..458824bd1 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -14,7 +14,6 @@
//= 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
diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss
index 32e1e49e3..9d6b2cd3f 100644
--- a/app/assets/stylesheets/publisher.scss
+++ b/app/assets/stylesheets/publisher.scss
@@ -83,7 +83,7 @@
}
&.active textarea {
- min-height: 70px;
+ min-height: 90px;
}
.markdownIndications {
@@ -118,6 +118,8 @@
}
}
+ &:not(.with_location) #location_container { display: none; }
+
&.with_location .loader {
height: 20px;
width: 20px;
diff --git a/app/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss
index 3a46e14b6..ed4405c01 100644
--- a/app/assets/stylesheets/typeahead.scss
+++ b/app/assets/stylesheets/typeahead.scss
@@ -12,10 +12,9 @@
line-height: 20px;
&.tt-cursor {
background-color: $brand-primary;
+ border-top: 1px solid $brand-primary;
}
- &:hover { background-color: lighten($navbar-inverse-bg, 10%); }
-
&.search-suggestion-person {
padding: 8px;
.avatar {
diff --git a/app/views/people/contacts.haml b/app/views/people/contacts.haml
index b1e993f79..b5edec9b0 100644
--- a/app/views/people/contacts.haml
+++ b/app/views/people/contacts.haml
@@ -1,9 +1,3 @@
--# TODO this should happen in the js app
-- content_for :head do
- - if user_signed_in? && @person != current_user.person
- :javascript
- Mentions.options.prefillMention = Mentions._contactToMention(#{j @person.to_json});
-
- content_for :page_title do
= @person.name
diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml
index bfb10872c..0deab6f09 100644
--- a/app/views/people/show.html.haml
+++ b/app/views/people/show.html.haml
@@ -2,12 +2,6 @@
-# licensed under the Affero General Public License version 3 or later. See
-# the COPYRIGHT file.
--# TODO this should happen in the js app
-- content_for :head do
- - if user_signed_in? && @person != current_user.person
- :javascript
- Mentions.options.prefillMention = Mentions._contactToMention(#{j @person.to_json});
-
- content_for :page_title do
= @person.name
diff --git a/app/views/status_messages/new.html.haml b/app/views/status_messages/new.html.haml
index 88e150b9f..f1d7f9a8a 100644
--- a/app/views/status_messages/new.html.haml
+++ b/app/views/status_messages/new.html.haml
@@ -7,7 +7,8 @@
:javascript
$(function() {
app.publisher = new app.views.Publisher({
- standalone: true
+ standalone: true,
+ prefillMention: #{json_escape @person.to_json}
});
app.publisher.open();
$("#publisher").bind('ajax:success', function(){
diff --git a/features/step_definitions/mention_steps.rb b/features/step_definitions/mention_steps.rb
index f7939afbe..a36557563 100644
--- a/features/step_definitions/mention_steps.rb
+++ b/features/step_definitions/mention_steps.rb
@@ -1,24 +1,24 @@
And /^Alice has a post mentioning Bob$/ do
- alice = User.find_by_email 'alice@alice.alice'
- bob = User.find_by_email 'bob@bob.bob'
+ alice = User.find_by_email "alice@alice.alice"
+ bob = User.find_by_email "bob@bob.bob"
aspect = alice.aspects.where(:name => "Besties").first
alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect)
end
And /^Alice has (\d+) posts mentioning Bob$/ do |n|
n.to_i.times do
- alice = User.find_by_email 'alice@alice.alice'
- bob = User.find_by_email 'bob@bob.bob'
+ alice = User.find_by_email "alice@alice.alice"
+ bob = User.find_by_email "bob@bob.bob"
aspect = alice.aspects.where(:name => "Besties").first
alice.post(:status_message, :text => "@{Bob Jones; #{bob.person.diaspora_handle}}", :to => aspect)
end
end
And /^I mention Alice in the publisher$/ do
- alice = User.find_by_email 'alice@alice.alice'
- write_in_publisher("@{Alice Smith ; #{alice.person.diaspora_handle}}")
+ write_in_publisher("@alice")
+ step %(I click on the first user in the mentions dropdown list)
end
And /^I click on the first user in the mentions dropdown list$/ do
- find('.mentions-autocomplete-list li', match: :first).click
+ find(".tt-menu .tt-suggestion", match: :first).click
end
diff --git a/lib/assets/javascripts/jquery.mentionsInput.js b/lib/assets/javascripts/jquery.mentionsInput.js
deleted file mode 100644
index 7693caaee..000000000
--- a/lib/assets/javascripts/jquery.mentionsInput.js
+++ /dev/null
@@ -1,443 +0,0 @@
-// @license magnet:?xt=urn:btih:d3d9a9a6595521f9666a5e94cc830dab83b65699&dn=expat.txt Expat
-/*
- * Mentions Input
- * Version 1.0.2
- * Written by: Kenneth Auchenberg (Podio)
- *
- * Using underscore.js
- *
- * License: MIT License - http://www.opensource.org/licenses/mit-license.php
- *
- * Modifications for Diaspora:
- *
- * Prevent replacing the wrong text by marking the replacement position with a special character
- * Don't add a space after inserting a mention
- * Only use the first div as a wrapperBox
- * Binded paste event on input box to trigger contacts search for autocompletion while adding mention via clipboard
- */
-
-(function ($, _, undefined) {
-
- // Settings
- var KEY = { PASTE : 118, BACKSPACE : 8, TAB : 9, RETURN : 13, ESC : 27, LEFT : 37, UP : 38, RIGHT : 39,
- DOWN : 40, COMMA : 188, SPACE : 32, HOME : 36, END : 35 }; // Keys "enum"
- var defaultSettings = {
- triggerChar : '@',
- onDataRequest : $.noop,
- minChars : 2,
- showAvatars : true,
- elastic : true,
- classes : {
- autoCompleteItemActive : "active"
- },
- templates : {
- wrapper : _.template(''),
- autocompleteList : _.template(''),
- autocompleteListItem : _.template('<%= content %>'),
- autocompleteListItemAvatar : _.template('
'),
- autocompleteListItemIcon : _.template(''),
- mentionsOverlay : _.template(''),
- mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
- mentionItemHighlight : _.template('<%= value %>')
- }
- };
-
- var utils = {
- htmlEncode : function (str) {
- return _.escape(str);
- },
- highlightTerm : function (value, term) {
- if (!term && !term.length) {
- return value;
- }
- return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "$1");
- },
- setCaratPosition : function (domNode, caretPos) {
- if (domNode.createTextRange) {
- var range = domNode.createTextRange();
- range.move('character', caretPos);
- range.select();
- } else {
- if (domNode.selectionStart) {
- domNode.focus();
- domNode.setSelectionRange(caretPos, caretPos);
- } else {
- domNode.focus();
- }
- }
- },
- rtrim: function(string) {
- return string.replace(/\s+$/,"");
- }
- };
-
- var MentionsInput = function (settings) {
-
- var domInput, elmInputBox, elmInputWrapper, elmAutocompleteList, elmWrapperBox, elmMentionsOverlay, elmActiveAutoCompleteItem;
- var mentionsCollection = [];
- var autocompleteItemCollection = {};
- var inputBuffer = [];
- var currentDataQuery = '';
- var mentionChar = "\u200B"; // zero width space
-
- settings = $.extend(true, {}, defaultSettings, settings );
-
- function initTextarea() {
- elmInputBox = $(domInput);
-
- if (elmInputBox.attr('data-mentions-input') == 'true') {
- return;
- }
-
- elmInputWrapper = elmInputBox.parent();
- elmWrapperBox = $(settings.templates.wrapper());
- elmInputBox.wrapAll(elmWrapperBox);
- elmWrapperBox = elmInputWrapper.find('> div').first();
-
- elmInputBox.attr('data-mentions-input', 'true');
- elmInputBox.bind('keydown', onInputBoxKeyDown);
- elmInputBox.bind('keypress', onInputBoxKeyPress);
- elmInputBox.bind('paste',onInputBoxPaste);
- elmInputBox.bind('input', onInputBoxInput);
- elmInputBox.bind('click', onInputBoxClick);
- elmInputBox.bind('blur', onInputBoxBlur);
-
- // Elastic textareas, internal setting for the Dispora guys
- if( settings.elastic ) {
- elmInputBox.elastic();
- }
-
- }
-
- function initAutocomplete() {
- elmAutocompleteList = $(settings.templates.autocompleteList());
- elmAutocompleteList.appendTo(elmWrapperBox);
- elmAutocompleteList.delegate('li', 'mousedown', onAutoCompleteItemClick);
- }
-
- function initMentionsOverlay() {
- elmMentionsOverlay = $(settings.templates.mentionsOverlay());
- elmMentionsOverlay.prependTo(elmWrapperBox);
- }
-
- function updateValues() {
- var syntaxMessage = getInputBoxValue();
-
- _.each(mentionsCollection, function (mention) {
- var textSyntax = settings.templates.mentionItemSyntax(mention);
- syntaxMessage = syntaxMessage.replace(mentionChar + mention.value, textSyntax);
- });
-
- var mentionText = utils.htmlEncode(syntaxMessage);
-
- _.each(mentionsCollection, function (mention) {
- var formattedMention = _.extend({}, mention, {value: mentionChar + utils.htmlEncode(mention.value)});
- var textSyntax = settings.templates.mentionItemSyntax(formattedMention);
- var textHighlight = settings.templates.mentionItemHighlight(formattedMention);
-
- mentionText = mentionText.replace(textSyntax, textHighlight);
- });
-
- mentionText = mentionText.replace(/\n/g, '
');
- mentionText = mentionText.replace(/ {2}/g, ' ');
-
- elmInputBox.data('messageText', syntaxMessage);
- elmMentionsOverlay.find('div > div').html(mentionText);
- }
-
- function resetBuffer() {
- inputBuffer = [];
- }
-
- function updateMentionsCollection() {
- var inputText = getInputBoxValue();
-
- mentionsCollection = _.reject(mentionsCollection, function (mention, index) {
- return !mention.value || inputText.indexOf(mention.value) == -1;
- });
- mentionsCollection = _.compact(mentionsCollection);
- }
-
- function addMention(mention) {
-
- var currentMessage = getInputBoxValue();
-
- // Using a regex to figure out positions
- var regex = new RegExp("\\" + settings.triggerChar + currentDataQuery, "gi");
- regex.exec(currentMessage);
-
- var startCaretPosition = regex.lastIndex - currentDataQuery.length - 1;
- var currentCaretPosition = regex.lastIndex;
-
- var start = currentMessage.substr(0, startCaretPosition);
- var end = currentMessage.substr(currentCaretPosition, currentMessage.length);
- var startEndIndex = (start + mention.value).length + 1;
-
- mentionsCollection.push(mention);
-
- // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
- resetBuffer();
- currentDataQuery = '';
- hideAutoComplete();
-
- // Mentions & syntax message
- var updatedMessageText = start + mentionChar + mention.value + end;
- elmInputBox.val(updatedMessageText);
- updateValues();
-
- // Set correct focus and selection
- elmInputBox.focus();
- utils.setCaratPosition(elmInputBox[0], startEndIndex);
- }
-
- function getInputBoxValue() {
- return $.trim(elmInputBox.val());
- }
-
- function onAutoCompleteItemClick(e) {
- var elmTarget = $(this);
- var mention = autocompleteItemCollection[elmTarget.attr('data-uid')];
-
- addMention(mention);
-
- return false;
- }
-
- function onInputBoxClick(e) {
- resetBuffer();
- }
-
- function onInputBoxBlur(e) {
- hideAutoComplete();
- }
-
- function onInputBoxPaste(e) {
- pastedData = e.originalEvent.clipboardData.getData("text/plain");
- dataArray = pastedData.split("");
- _.each(dataArray, function(value) {
- inputBuffer.push(value);
- });
- }
- function onInputBoxInput(e) {
- updateValues();
- updateMentionsCollection();
- hideAutoComplete();
-
- var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar);
- if (triggerCharIndex > -1) {
- currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join('');
- currentDataQuery = utils.rtrim(currentDataQuery);
-
- _.defer(_.bind(doSearch, this, currentDataQuery));
- }
- }
-
- function onInputBoxKeyPress(e) {
- // Excluding ctrl+v from key press event in firefox
- if (!((e.which === KEY.PASTE && e.ctrlKey) || (e.keyCode === KEY.BACKSPACE))) {
- var typedValue = String.fromCharCode(e.which || e.keyCode);
- inputBuffer.push(typedValue);
- }
- }
-
- function onInputBoxKeyDown(e) {
-
- // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT
- if (e.keyCode == KEY.LEFT || e.keyCode == KEY.RIGHT || e.keyCode == KEY.HOME || e.keyCode == KEY.END) {
- // Defer execution to ensure carat pos has changed after HOME/END keys
- _.defer(resetBuffer);
-
- // IE9 doesn't fire the oninput event when backspace or delete is pressed. This causes the highlighting
- // to stay on the screen whenever backspace is pressed after a highlighed word. This is simply a hack
- // to force updateValues() to fire when backspace/delete is pressed in IE9.
- if (navigator.userAgent.indexOf("MSIE 9") > -1) {
- _.defer(updateValues);
- }
-
- return;
- }
-
- if (e.keyCode == KEY.BACKSPACE) {
- inputBuffer = inputBuffer.slice(0, -1 + inputBuffer.length); // Can't use splice, not available in IE
- return;
- }
-
- if (!elmAutocompleteList.is(':visible')) {
- return true;
- }
-
- switch (e.keyCode) {
- case KEY.UP:
- case KEY.DOWN:
- var elmCurrentAutoCompleteItem = null;
- if (e.keyCode == KEY.DOWN) {
- if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) {
- elmCurrentAutoCompleteItem = elmActiveAutoCompleteItem.next();
- } else {
- elmCurrentAutoCompleteItem = elmAutocompleteList.find('li').first();
- }
- } else {
- elmCurrentAutoCompleteItem = $(elmActiveAutoCompleteItem).prev();
- }
-
- if (elmCurrentAutoCompleteItem.length) {
- selectAutoCompleteItem(elmCurrentAutoCompleteItem);
- }
-
- return false;
-
- case KEY.RETURN:
- case KEY.TAB:
- if (elmActiveAutoCompleteItem && elmActiveAutoCompleteItem.length) {
- elmActiveAutoCompleteItem.trigger('mousedown');
- return false;
- }
-
- break;
- }
-
- return true;
- }
-
- function hideAutoComplete() {
- elmActiveAutoCompleteItem = null;
- elmAutocompleteList.empty().hide();
- }
-
- function selectAutoCompleteItem(elmItem) {
- elmItem.addClass(settings.classes.autoCompleteItemActive);
- elmItem.siblings().removeClass(settings.classes.autoCompleteItemActive);
-
- elmActiveAutoCompleteItem = elmItem;
- }
-
- function populateDropdown(query, results) {
- elmAutocompleteList.show();
-
- // Filter items that has already been mentioned
- var mentionValues = _.pluck(mentionsCollection, 'value');
- results = _.reject(results, function (item) {
- return _.include(mentionValues, item.name);
- });
-
- if (!results.length) {
- hideAutoComplete();
- return;
- }
-
- elmAutocompleteList.empty();
- var elmDropDownList = $("").appendTo(elmAutocompleteList).hide();
-
- _.each(results, function (item, index) {
- var itemUid = _.uniqueId('mention_');
-
- autocompleteItemCollection[itemUid] = _.extend({}, item, {value: item.name});
-
- var elmListItem = $(settings.templates.autocompleteListItem({
- 'id' : utils.htmlEncode(item.id),
- 'display' : utils.htmlEncode(item.name),
- 'type' : utils.htmlEncode(item.type),
- 'content' : utils.highlightTerm(utils.htmlEncode((item.name)), query)
- })).attr('data-uid', itemUid);
-
- if (index === 0) {
- selectAutoCompleteItem(elmListItem);
- }
-
- if (settings.showAvatars) {
- var elmIcon;
-
- if (item.avatar) {
- elmIcon = $(settings.templates.autocompleteListItemAvatar({ avatar : item.avatar }));
- } else {
- elmIcon = $(settings.templates.autocompleteListItemIcon({ icon : item.icon }));
- }
- elmIcon.prependTo(elmListItem);
- }
- elmListItem = elmListItem.appendTo(elmDropDownList);
- });
-
- elmAutocompleteList.show();
- elmDropDownList.show();
- }
-
- function doSearch(query) {
- if (query && query.length && query.length >= settings.minChars) {
- settings.onDataRequest.call(this, 'search', query, function (responseData) {
- populateDropdown(query, responseData);
- });
- }
- }
-
- function resetInput() {
- elmInputBox.val('');
- mentionsCollection = [];
- updateValues();
- }
-
- // Public methods
- return {
- init : function (domTarget) {
-
- domInput = domTarget;
-
- initTextarea();
- initAutocomplete();
- initMentionsOverlay();
- resetInput();
-
- if( settings.prefillMention ) {
- addMention( settings.prefillMention );
- }
-
- },
-
- val : function (callback) {
- if (!_.isFunction(callback)) {
- return;
- }
-
- var value = mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue();
- callback.call(this, value);
- },
-
- reset : function () {
- resetInput();
- },
-
- getMentions : function (callback) {
- if (!_.isFunction(callback)) {
- return;
- }
-
- callback.call(this, mentionsCollection);
- }
- };
- };
-
- $.fn.mentionsInput = function (method, settings) {
-
- var outerArguments = arguments;
-
- if (typeof method === 'object' || !method) {
- settings = method;
- }
-
- return this.each(function () {
- var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(settings));
-
- if (_.isFunction(instance[method])) {
- return instance[method].apply(this, Array.prototype.slice.call(outerArguments, 1));
-
- } else if (typeof method === 'object' || !method) {
- return instance.init.call(this, this);
-
- } else {
- $.error('Method ' + method + ' does not exist');
- }
-
- });
- };
-
-})(jQuery, _);
-// @license-end