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 0000000000000000000000000000000000000000..270e14161bf091d10c52b6fe20ae9e93bc6f7b6e --- /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("<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(); + } +}); diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index 89e5e38f5b7f128ad8357f3f825b9ae0905bcc29..fd7d85a5e4853ccd22f0a8a8955d598a77b7cf6d 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 0000000000000000000000000000000000000000..af791412291f8b50cb957fb860fc50b5554d7454 --- /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: <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"); + } +}); diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index 6e8a1d272e79c6dccdae31c1af1290b54e39dcb7..65ed775c0dc9a28ec3fca78aaee4a6815c5b6c35 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); - }, - - 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.completeSetup(this.getTypeaheadElement()); + this.bindMoreSelectionEvents(); + this.getTypeaheadElement().on("typeahead:select", this.suggestionSelected); }, - 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 */ - } + /** + * 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); }); }, - 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; + getTypeaheadElement: function(){ + return this.$("#q"); }, - 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 78da464960810ee67c9baa115b8b8f992a50f469..458824bd1e7fe797c56fe284d3c05fb3501309fb 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 32e1e49e3e7811bb8226e7e9565d129460d7af47..9d6b2cd3ff4bbeb38bb67b7aef82ef4668e54946 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 3a46e14b621c847b3f5f1d047257140cca76f644..ed4405c01bc56b55a26b84e4347dab8765c92a03 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 b1e993f790ea14e090f0ef14abb5251ca1e1a8dd..b5edec9b04c6c1bec46a98deda807a6dd0377bda 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 bfb10872ce25aff935cd4b25ed423c002e6ee73b..0deab6f098fce53dc25307d9dba982be588af5c1 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 88e150b9f80c577c3cd175c6a363bb19a56b7109..f1d7f9a8a45f7c57efc4a29592244a113e43d7c1 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 f7939afbe9f604e507624720fe8b70ef1ed86435..a3655756357784e994f427f12a3211f11e9516e8 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 7693caaeef872f2ee1ff7a4e5ca224fc27c3b9dd..0000000000000000000000000000000000000000 --- 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('<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