diff --git a/Changelog.md b/Changelog.md index 62269bca2..78f92fbb9 100644 --- a/Changelog.md +++ b/Changelog.md @@ -90,6 +90,7 @@ Contributions are very welcome, the hard work is done! * Refactor mobile javascript and add tests [#6394](https://github.com/diaspora/diaspora/pull/6394) * Dropped `parent_author_signature` from relayables [#6586](https://github.com/diaspora/diaspora/pull/6586) * Attached ShareVisibilities to the User, not the Contact [#6723](https://github.com/diaspora/diaspora/pull/6723) +* Refactor mentions input, now based on typeahead.js [#6728](https://github.com/diaspora/diaspora/pull/6728) ## Bug fixes * Destroy Participation when removing interactions with a post [#5852](https://github.com/diaspora/diaspora/pull/5852) diff --git a/Gemfile b/Gemfile index bd634d1ab..79a6eb5a8 100644 --- a/Gemfile +++ b/Gemfile @@ -108,11 +108,9 @@ source "https://rails-assets.org" do # jQuery plugins - gem "rails-assets-jeresig--jquery.hotkeys", "0.2.0" gem "rails-assets-jquery-placeholder", "2.3.1" gem "rails-assets-jquery-textchange", "0.2.3" gem "rails-assets-perfect-scrollbar", "0.6.10" - gem "rails-assets-jakobmattsson--jquery-elastic", "1.6.11" gem "rails-assets-autosize", "3.0.15" gem "rails-assets-blueimp-gallery", "2.17.0" end diff --git a/Gemfile.lock b/Gemfile.lock index 46e679250..6baa27335 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -637,13 +637,9 @@ GEM rails-assets-jquery.ui (~> 1.11.4) rails-assets-favico.js (0.3.10) rails-assets-highlightjs (9.1.0) - rails-assets-jakobmattsson--jquery-elastic (1.6.11) - rails-assets-jquery (>= 1.2.6) rails-assets-jasmine (2.4.1) rails-assets-jasmine-ajax (3.2.0) rails-assets-jasmine (~> 2) - rails-assets-jeresig--jquery.hotkeys (0.2.0) - rails-assets-jquery (>= 1.4.2) rails-assets-jquery (1.12.0) rails-assets-jquery-colorbox (1.6.3) rails-assets-jquery (>= 1.3.2) @@ -989,9 +985,7 @@ DEPENDENCIES rails-assets-blueimp-gallery (= 2.17.0)! rails-assets-diaspora_jsxc (~> 0.1.5.develop)! rails-assets-highlightjs (= 9.1.0)! - rails-assets-jakobmattsson--jquery-elastic (= 1.6.11)! rails-assets-jasmine-ajax (= 3.2.0)! - rails-assets-jeresig--jquery.hotkeys (= 0.2.0)! rails-assets-jquery (= 1.12.0)! rails-assets-jquery-placeholder (= 2.3.1)! rails-assets-jquery-textchange (= 0.2.3)! diff --git a/app/assets/javascripts/app/router.js b/app/assets/javascripts/app/router.js index 52117d495..8f65548aa 100644 --- a/app/assets/javascripts/app/router.js +++ b/app/assets/javascripts/app/router.js @@ -180,8 +180,10 @@ app.Router = Backbone.Router.extend({ } app.page = new app.views.Stream({model : app.stream}); - app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items}); app.shortcuts = app.shortcuts || new app.views.StreamShortcuts({el: $(document)}); + if($("#publisher").length !== 0) { + app.publisher = app.publisher || new app.views.Publisher({collection : app.stream.items}); + } $("#main_stream").html(app.page.render().el); this._hideInactiveStreamLists(); diff --git a/app/assets/javascripts/app/views/aspect_create_view.js b/app/assets/javascripts/app/views/aspect_create_view.js index 325c874bc..ad40616a6 100644 --- a/app/assets/javascripts/app/views/aspect_create_view.js +++ b/app/assets/javascripts/app/views/aspect_create_view.js @@ -32,7 +32,7 @@ app.views.AspectCreate = app.views.Base.extend({ }, inputKeypress: function(evt) { - if(evt.which === 13) { + if(evt.which === Keycodes.ENTER) { evt.preventDefault(); this.createAspect(); } diff --git a/app/assets/javascripts/app/views/comment_stream_view.js b/app/assets/javascripts/app/views/comment_stream_view.js index 87dce7e12..8cc3d238e 100644 --- a/app/assets/javascripts/app/views/comment_stream_view.js +++ b/app/assets/javascripts/app/views/comment_stream_view.js @@ -56,7 +56,7 @@ app.views.CommentStream = app.views.Base.extend({ }, keyDownOnCommentBox: function(evt) { - if(evt.keyCode === 13 && evt.ctrlKey) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { this.$("form").submit(); return false; } diff --git a/app/assets/javascripts/app/views/conversations_form_view.js b/app/assets/javascripts/app/views/conversations_form_view.js index 4f5ac7133..53dbee681 100644 --- a/app/assets/javascripts/app/views/conversations_form_view.js +++ b/app/assets/javascripts/app/views/conversations_form_view.js @@ -43,7 +43,7 @@ app.views.ConversationsForm = Backbone.View.extend({ }, keyDown : function(evt) { - if( evt.keyCode === 13 && evt.ctrlKey ) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { $(evt.target).parents("form").submit(); } } diff --git a/app/assets/javascripts/app/views/conversations_view.js b/app/assets/javascripts/app/views/conversations_view.js index 869471557..d5ed22a3a 100644 --- a/app/assets/javascripts/app/views/conversations_view.js +++ b/app/assets/javascripts/app/views/conversations_view.js @@ -50,7 +50,7 @@ app.views.Conversations = Backbone.View.extend({ }, keyDown : function(evt) { - if( evt.keyCode === 13 && evt.ctrlKey ) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { $(evt.target).parents("form").submit(); } } 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..f19ec7d1e --- /dev/null +++ b/app/assets/javascripts/app/views/publisher/mention_view.js @@ -0,0 +1,222 @@ +//= require ../search_base_view + +app.views.PublisherMention = app.views.SearchBase.extend({ + triggerChar: "@", + invisibleChar: "\u200B", // zero width space + mentionRegex: /@([^@\s]+)$/, + + templates: { + mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}"), + mentionItemHighlight: _.template("<%= name %>") + }, + + events: { + "keydown #status_message_fake_text": "onInputBoxKeyDown", + "input #status_message_fake_text": "onInputBoxInput", + "click #status_message_fake_text": "onInputBoxClick", + "blur #status_message_fake_text": "onInputBoxBlur", + }, + + initialize: function() { + this.mentionedPeople = []; + + // contains the 'fake text' displayed to the user + // also has a data-messageText attribute with the original text + this.inputBox = this.$("#status_message_fake_text"); + // contains the mentions displayed to the user + this.mentionsBox = this.$(".mentions-box"); + this.typeaheadInput = this.$(".typeahead-mention-box"); + this.bindTypeaheadEvents(); + + app.views.SearchBase.prototype.initialize.call(this, { + typeaheadInput: this.typeaheadInput, + customSearch: true, + autoselect: true + }); + }, + + bindTypeaheadEvents: function() { + var self = this; + // Process mention when the user selects a result. + this.typeaheadInput.on("typeahead:select", function(evt, person) { self.onSuggestionSelection(person); }); + }, + + addPersonToMentions: function(person) { + if(!(person && person.name && person.handle)) { return; } + // This is needed for processing preview + /* jshint camelcase: false */ + person.diaspora_id = person.handle; + /* jshint camelcase: true */ + this.mentionedPeople.push(person); + this.ignorePersonForSuggestions(person); + }, + + cleanMentionedPeople: function() { + var inputText = this.inputBox.val(); + this.mentionedPeople = this.mentionedPeople.filter(function(person) { + return person.name && inputText.indexOf(person.name) > -1; + }); + this.ignoreDiasporaIds = this.mentionedPeople.map(function(person) { return person.handle; }); + }, + + onSuggestionSelection: function(person) { + var messageText = this.inputBox.val(); + var caretPosition = this.inputBox[0].selectionStart; + var triggerCharPosition = messageText.lastIndexOf(this.triggerChar, caretPosition); + + if(triggerCharPosition === -1) { return; } + + this.addPersonToMentions(person); + this.closeSuggestions(); + + messageText = messageText.substring(0, triggerCharPosition) + + this.invisibleChar + person.name + messageText.substring(caretPosition); + + this.inputBox.val(messageText); + this.updateMessageTexts(); + + this.inputBox.focus(); + var newCaretPosition = triggerCharPosition + person.name.length + 1; + this.inputBox[0].setSelectionRange(newCaretPosition, newCaretPosition); + }, + + /** + * Replaces every combination of this.invisibleChar + mention.name by the + * correct syntax for both hidden text and visible one. + * + * For instance, the text "Hello \u200Buser1" will be tranformed to + * "Hello @{user1 ; user1@pod.tld}" in the hidden element and + * "Hello user1" in the element visible to the user. + */ + updateMessageTexts: function() { + var fakeMessageText = this.inputBox.val(), + mentionBoxText = fakeMessageText, + messageText = fakeMessageText; + + this.mentionedPeople.forEach(function(person) { + var mentionName = this.invisibleChar + person.name; + messageText = messageText.replace(mentionName, this.templates.mentionItemSyntax(person)); + var textHighlight = this.templates.mentionItemHighlight({name: _.escape(person.name)}); + mentionBoxText = mentionBoxText.replace(mentionName, textHighlight); + }, this); + + this.inputBox.data("messageText", messageText); + this.mentionsBox.find(".mentions").html(mentionBoxText); + }, + + updateTypeaheadInput: function() { + var messageText = this.inputBox.val(); + var caretPosition = this.inputBox[0].selectionStart; + var result = this.mentionRegex.exec(messageText.substring(0,caretPosition)); + + if(result === null) { + this.closeSuggestions(); + return; + } + + // result[1] is the string between the last '@' and the current caret position + this.typeaheadInput.typeahead("val", result[1]); + this.typeaheadInput.typeahead("open"); + }, + + /** + * Let us prefill the publisher with a mention list + * @param persons List of people to mention in a post; + * JSON object of form { handle: , name: , ... } + */ + prefillMention: function(persons) { + persons.forEach(function(person) { + this.addPersonToMentions(person); + var text = this.invisibleChar + person.name; + if(this.inputBox.val().length !== 0) { + text = this.inputBox.val() + " " + text; + } + this.inputBox.val(text); + this.updateMessageTexts(); + }, this); + }, + + /** + * Selects next or previous result when result dropdown is open and + * user press up and down arrows. + */ + onArrowKeyDown: function(e) { + if(!this.isVisible() || (e.which !== Keycodes.UP && e.which !== Keycodes.DOWN)) { + return; + } + + e.preventDefault(); + e.stopPropagation(); + + this.typeaheadInput.typeahead("activate"); + this.typeaheadInput.typeahead("open"); + this.typeaheadInput.trigger($.Event("keydown", {keyCode: e.keyCode, which: e.which})); + }, + + /** + * Listens for user input and opens results dropdown when input contains the trigger char + */ + onInputBoxInput: function() { + this.cleanMentionedPeople(); + this.updateMessageTexts(); + this.updateTypeaheadInput(); + }, + + onInputBoxKeyDown: function(e) { + // This also matches HOME/END on OSX which is CMD+LEFT, CMD+RIGHT + if(e.which === Keycodes.LEFT || e.which === Keycodes.RIGHT || + e.which === Keycodes.HOME || e.which === Keycodes.END) { + _.defer(_.bind(this.updateTypeaheadInput, this)); + return; + } + + if(!this.isVisible) { + return true; + } + + switch(e.which) { + case Keycodes.ESC: + case Keycodes.SPACE: + this.closeSuggestions(); + break; + case Keycodes.UP: + case Keycodes.DOWN: + this.onArrowKeyDown(e); + break; + case Keycodes.RETURN: + case Keycodes.TAB: + if(this.$(".tt-cursor").length === 1) { + this.$(".tt-cursor").click(); + return false; + } + break; + } + return true; + }, + + onInputBoxClick: function() { + this.updateTypeaheadInput(); + }, + + onInputBoxBlur: function() { + this.closeSuggestions(); + }, + + reset: function() { + this.inputBox.val(""); + this.onInputBoxInput(); + }, + + closeSuggestions: function() { + this.typeaheadInput.typeahead("val", ""); + this.typeaheadInput.typeahead("close"); + }, + + isVisible: function() { + return this.$(".tt-menu").is(":visible"); + }, + + getTextForSubmit: function() { + return this.mentionedPeople.length ? this.inputBox.data("messageText") : this.inputBox.val(); + } +}); diff --git a/app/assets/javascripts/app/views/publisher_view.js b/app/assets/javascripts/app/views/publisher_view.js index 89e5e38f5..08c5df881 100644 --- a/app/assets/javascripts/app/views/publisher_view.js +++ b/app/assets/javascripts/app/views/publisher_view.js @@ -5,9 +5,11 @@ * the COPYRIGHT file. */ -//= require ./publisher/services_view //= require ./publisher/aspect_selector_view //= require ./publisher/getting_started_view +//= require ./publisher/mention_view +//= require ./publisher/poll_creator_view +//= require ./publisher/services_view //= require ./publisher/uploader_view //= require jquery-textchange @@ -31,6 +33,7 @@ app.views.Publisher = Backbone.View.extend({ initialize : function(opts){ this.standalone = opts ? opts.standalone : false; + this.prefillMention = opts && opts.prefillMention ? opts.prefillMention : undefined; this.disabled = false; // init shortcut references to the various elements @@ -41,9 +44,6 @@ app.views.Publisher = Backbone.View.extend({ this.previewEl = this.$("button.post_preview_button"); this.photozoneEl = this.$("#photodropzone"); - // init mentions plugin - Mentions.initialize(this.inputEl); - // if there is data in the publisher we ask for a confirmation // before the user is able to leave the page $(window).on("beforeunload", _.bind(this._beforeUnload, this)); @@ -100,6 +100,11 @@ app.views.Publisher = Backbone.View.extend({ }, initSubviews: function() { + this.mention = new app.views.PublisherMention({ el: this.$("#publisher_textarea_wrapper") }); + if(this.prefillMention) { + this.mention.prefillMention([this.prefillMention]); + } + var form = this.$(".content_creation form"); this.view_services = new app.views.PublisherServices({ @@ -222,8 +227,8 @@ app.views.Publisher = Backbone.View.extend({ // creates the location showLocation: function(){ if($("#location").length === 0){ - $("#location_container").append("
"); - this.wrapperEl.addClass("with_location"); + this.$(".location-container").append("
"); + this.wrapperEl.addClass("with-location"); this.view_locator = new app.views.Location(); } }, @@ -232,7 +237,7 @@ app.views.Publisher = Backbone.View.extend({ destroyLocation: function(){ if(this.view_locator){ this.view_locator.remove(); - this.wrapperEl.removeClass("with_location"); + this.wrapperEl.removeClass("with-location"); delete this.view_locator; } }, @@ -244,8 +249,9 @@ app.views.Publisher = Backbone.View.extend({ // avoid submitting form when pressing Enter key avoidEnter: function(evt){ - if(evt.keyCode === 13) + if(evt.which === Keycodes.ENTER) { return false; + } }, getUploadedPhotos: function() { @@ -265,32 +271,6 @@ app.views.Publisher = Backbone.View.extend({ return photos; }, - getMentionedPeople: function(serializedForm) { - var mentionedPeople = [], - regexp = /@{([^;]+); ([^}]+)}/g, - user; - var getMentionedUser = function(handle) { - return Mentions.contacts.filter(function(user) { - return user.handle === handle; - })[0]; - }; - - while( (user = regexp.exec(serializedForm["status_message[text]"])) ) { - // user[1]: name, user[2]: handle - var mentionedUser = getMentionedUser(user[2]); - if(mentionedUser){ - mentionedPeople.push({ - "id": mentionedUser.id, - "guid": mentionedUser.guid, - "name": user[1], - "diaspora_id": user[2], - "avatar": mentionedUser.avatar - }); - } - } - return mentionedPeople; - }, - getPollData: function(serializedForm) { var poll; var pollQuestion = serializedForm.poll_question; @@ -321,7 +301,7 @@ app.views.Publisher = Backbone.View.extend({ var serializedForm = $(evt.target).closest("form").serializeObject(); var photos = this.getUploadedPhotos(); - var mentionedPeople = this.getMentionedPeople(serializedForm); + var mentionedPeople = this.mention.mentionedPeople; var date = (new Date()).toISOString(); var poll = this.getPollData(serializedForm); var locationCoords = serializedForm["location[coords]"]; @@ -379,7 +359,7 @@ app.views.Publisher = Backbone.View.extend({ }, keyDown : function(evt) { - if( evt.keyCode === 13 && evt.ctrlKey ) { + if(evt.which === Keycodes.ENTER && evt.ctrlKey) { this.$("form").submit(); this.open(); return false; @@ -387,6 +367,9 @@ app.views.Publisher = Backbone.View.extend({ }, clear : function() { + // remove mentions + this.mention.reset(); + // clear text(s) this.inputEl.val(""); this.hiddenInputEl.val(""); @@ -394,9 +377,6 @@ app.views.Publisher = Backbone.View.extend({ .trigger("keydown"); autosize.update(this.inputEl); - // remove mentions - this.inputEl.mentionsInput("reset"); - // remove photos this.photozoneEl.find("li").remove(); this.$("input[name='photos[]']").remove(); @@ -450,9 +430,6 @@ app.views.Publisher = Backbone.View.extend({ this.$el.removeClass("closed"); this.wrapperEl.addClass("active"); autosize.update(this.inputEl); - - // fetch contacts for mentioning - Mentions.fetchContacts(); return this; }, @@ -521,9 +498,7 @@ app.views.Publisher = Backbone.View.extend({ var self = this; this.checkSubmitAvailability(); - this.inputEl.mentionsInput("val", function(value){ - self.hiddenInputEl.val(value); - }); + this.hiddenInputEl.val(this.mention.getTextForSubmit()); }, _beforeUnload: function(e) { 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..f194af7e1 --- /dev/null +++ b/app/assets/javascripts/app/views/search_base_view.js @@ -0,0 +1,124 @@ +app.views.SearchBase = app.views.Base.extend({ + initialize: function(options) { + this.ignoreDiasporaIds = []; + this.typeaheadInput = options.typeaheadInput; + this.setupBloodhound(options); + if(options.customSearch) { this.setupCustomSearch(); } + this.setupTypeahead(); + // TODO: Remove this as soon as corejavascript/typeahead.js has its first release + this.setupMouseSelectionEvents(); + if(options.autoselect) { this.setupAutoselect(); } + }, + + setupBloodhound: function(options) { + var bloodhoundOptions = { + datumTokenizer: function(datum) { + var nameTokens = Bloodhound.tokenizers.nonword(datum.name); + var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : []; + return nameTokens.concat(handleTokens); + }, + queryTokenizer: Bloodhound.tokenizers.whitespace, + prefetch: { + url: "/contacts.json", + transform: this.transformBloodhoundResponse, + cache: false + }, + sufficient: 5 + }; + + // Allow bloodhound to look for remote results if there is a route given in the options + if(options.remoteRoute) { + bloodhoundOptions.remote = { + url: options.remoteRoute + ".json?q=%QUERY", + wildcard: "%QUERY", + transform: this.transformBloodhoundResponse + }; + } + + this.bloodhound = new Bloodhound(bloodhoundOptions); + }, + + setupCustomSearch: function() { + var self = this; + this.bloodhound.customSearch = function(query, sync, async) { + var _sync = function(datums) { + var results = datums.filter(function(datum) { + return datum.handle !== undefined && self.ignoreDiasporaIds.indexOf(datum.handle) === -1; + }); + sync(results); + }; + + self.bloodhound.search(query, _sync, async); + }; + }, + + setupTypeahead: function() { + this.typeaheadInput.typeahead({ + hint: false, + highlight: true, + minLength: 2 + }, + { + name: "search", + display: "name", + limit: 5, + source: this.bloodhound.customSearch !== undefined ? this.bloodhound.customSearch : this.bloodhound, + templates: { + /* jshint camelcase: false */ + suggestion: HandlebarsTemplates.search_suggestion_tpl + /* jshint camelcase: true */ + } + }); + }, + + transformBloodhoundResponse: function(response) { + return response.map(function(data) { + // person + if(data.handle) { + data.person = true; + return data; + } + + // hashtag + return { + hashtag: true, + name: data.name, + url: Routes.tag(data.name.substring(1)) + }; + }); + }, + + _deselectAllSuggestions: function() { + this.$(".tt-suggestion").removeClass("tt-cursor"); + }, + + _selectSuggestion: function(suggestion) { + this._deselectAllSuggestions(); + suggestion.addClass("tt-cursor"); + }, + + // TODO: Remove this as soon as corejavascript/typeahead.js has its first release + setupMouseSelectionEvents: function() { + var self = this, + selectSuggestion = function(e) { self._selectSuggestion($(e.target).closest(".tt-suggestion")); }, + deselectAllSuggestions = function() { self._deselectAllSuggestions(); }; + + this.typeaheadInput.on("typeahead:render", function() { + self.$(".tt-menu .tt-suggestion").off("mouseover").on("mouseover", selectSuggestion); + self.$(".tt-menu .tt-suggestion *").off("mouseover").on("mouseover", selectSuggestion); + self.$(".tt-menu .tt-suggestion").off("mouseleave").on("mouseleave", deselectAllSuggestions); + }); + }, + + // Selects the first result when the result dropdown opens + setupAutoselect: function() { + var self = this; + this.typeaheadInput.on("typeahead:render", function() { + self._selectSuggestion(self.$(".tt-menu .tt-suggestion").first()); + }); + }, + + ignorePersonForSuggestions: function(person) { + if(person.handle) { this.ignoreDiasporaIds.push(person.handle); } + }, +}); diff --git a/app/assets/javascripts/app/views/search_view.js b/app/assets/javascripts/app/views/search_view.js index 6e8a1d272..254bcb071 100644 --- a/app/assets/javascripts/app/views/search_view.js +++ b/app/assets/javascripts/app/views/search_view.js @@ -1,81 +1,20 @@ // @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later -app.views.Search = app.views.Base.extend({ +app.views.Search = app.views.SearchBase.extend({ events: { "focusin #q": "toggleSearchActive", "focusout #q": "toggleSearchActive", - "keypress #q": "inputKeypress", + "keypress #q": "inputKeypress" }, - initialize: function(){ - this.searchFormAction = this.$el.attr("action"); + initialize: function() { this.searchInput = this.$("#q"); - - // constructs the suggestion engine - this.setupBloodhound(); - this.setupTypeahead(); + app.views.SearchBase.prototype.initialize.call(this, { + typeaheadInput: this.searchInput, + remoteRoute: this.$el.attr("action") + }); this.searchInput.on("typeahead:select", this.suggestionSelected); }, - setupBloodhound: function() { - this.bloodhound = new Bloodhound({ - datumTokenizer: function(datum) { - var nameTokens = Bloodhound.tokenizers.nonword(datum.name); - var handleTokens = datum.handle ? Bloodhound.tokenizers.nonword(datum.name) : []; - return nameTokens.concat(handleTokens); - }, - queryTokenizer: Bloodhound.tokenizers.whitespace, - remote: { - url: this.searchFormAction + ".json?q=%QUERY", - wildcard: "%QUERY", - transform: this.transformBloodhoundResponse - }, - prefetch: { - url: "/contacts.json", - transform: this.transformBloodhoundResponse, - cache: false - }, - sufficient: 5 - }); - }, - - setupTypeahead: function() { - this.searchInput.typeahead({ - hint: false, - highlight: true, - minLength: 2 - }, - { - name: "search", - display: "name", - limit: 5, - source: this.bloodhound, - templates: { - /* jshint camelcase: false */ - suggestion: HandlebarsTemplates.search_suggestion_tpl - /* jshint camelcase: true */ - } - }); - }, - - transformBloodhoundResponse: function(response) { - var result = response.map(function(data) { - // person - if(data.handle) { - data.person = true; - return data; - } - - // hashtag - return { - hashtag: true, - name: data.name, - url: Routes.tag(data.name.substring(1)) - }; - }); - - return result; - }, - toggleSearchActive: function(evt) { // jQuery produces two events for focus/blur (for bubbling) // don't rely on which event arrives first, by allowing for both variants @@ -83,14 +22,14 @@ app.views.Search = app.views.Base.extend({ $(evt.target).toggleClass("active", isActive); }, - suggestionSelected: function(evt, datum) { - window.location = datum.url; - }, - inputKeypress: function(evt) { - if(evt.which === 13 && $(".tt-suggestion.tt-cursor").length === 0) { + if(evt.which === Keycodes.ENTER && $(".tt-suggestion.tt-cursor").length === 0) { $(evt.target).closest("form").submit(); } + }, + + suggestionSelected: function(evt, datum) { + window.location = datum.url; } }); // @license-ends diff --git a/app/assets/javascripts/app/views/tag_following_list_view.js b/app/assets/javascripts/app/views/tag_following_list_view.js index 3ccdf3a6e..0504ab270 100644 --- a/app/assets/javascripts/app/views/tag_following_list_view.js +++ b/app/assets/javascripts/app/views/tag_following_list_view.js @@ -46,7 +46,7 @@ app.views.TagFollowingList = app.views.Base.extend({ }); this.$("input").bind('keydown', function(evt){ - if(evt.keyCode === 13 || evt.keyCode === 9 || evt.keyCode === 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length === 0 ){ $('li.as-result-item').first().click(); diff --git a/app/assets/javascripts/app/views/tags_view.js b/app/assets/javascripts/app/views/tags_view.js index e420281fb..3913f8798 100644 --- a/app/assets/javascripts/app/views/tags_view.js +++ b/app/assets/javascripts/app/views/tags_view.js @@ -2,7 +2,9 @@ app.views.Tags = Backbone.View.extend({ initialize: function(opts) { - app.publisher.setText("#"+ opts.hashtagName + " "); + if(app.publisher) { + app.publisher.setText("#"+ opts.hashtagName + " "); + } } }); // @license-end diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js index 78da46496..ba2bb5e3d 100644 --- a/app/assets/javascripts/main.js +++ b/app/assets/javascripts/main.js @@ -6,15 +6,12 @@ //= require js-routes //= require underscore //= require backbone -//= require jquery.hotkeys //= require jquery.remotipart //= require autosize //= require jquery.charcount //= require jquery-placeholder //= require rails-timeago //= require jquery.events.input -//= require jakobmattsson-jquery-elastic -//= require jquery.mentionsInput //= require jquery.infinitescroll-custom //= require jquery-ui/core //= require jquery-ui/widget @@ -39,7 +36,6 @@ //= require_tree ./helpers //= require_tree ./pages //= require_tree ./widgets -//= require mentions //= require bootstrap //= require osmlocator //= require bootstrap-switch diff --git a/app/assets/javascripts/mentions.js b/app/assets/javascripts/mentions.js deleted file mode 100644 index f805b97fa..000000000 --- a/app/assets/javascripts/mentions.js +++ /dev/null @@ -1,48 +0,0 @@ -// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-v3-or-Later - -var Mentions = { - initialize: function(mentionsInput) { - return mentionsInput.mentionsInput(Mentions.options); - }, - - // pre-fetch the list of contacts for the current user. - // called by the initializer of the publisher, for faster ('offline') - // execution of the filtering for mentions - fetchContacts : function(){ - Mentions.contacts || $.getJSON("/contacts", function(data) { - Mentions.contacts = Mentions.createList(data); - }); - }, - - // creates a list of mentions out of a list of contacts - // @see _contactToMention - createList: function(contacts) { - return _.map(contacts, Mentions._contactToMention); - }, - - // takes a given contact object and modifies to fit the format - // expected by the jQuery.mentionsInput plugin. - // @see http://podio.github.com/jquery-mentions-input/ - _contactToMention: function(contact) { - contact.value = contact.name; - return contact; - }, - - // default options for jQuery.mentionsInput - // @see http://podio.github.com/jquery-mentions-input/ - options: { - elastic: false, - minChars: 1, - - onDataRequest: function(mode, query, callback) { - var filteredResults = _.filter(Mentions.contacts, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); - - callback.call(this, filteredResults.slice(0,5)); - }, - - templates: { - mentionItemSyntax: _.template("@{<%= name %> ; <%= handle %>}") - } - } -}; -// @license-end diff --git a/app/assets/javascripts/pages/users-getting-started.js b/app/assets/javascripts/pages/users-getting-started.js index fb6184ba6..cb82e03a0 100644 --- a/app/assets/javascripts/pages/users-getting-started.js +++ b/app/assets/javascripts/pages/users-getting-started.js @@ -73,7 +73,7 @@ Diaspora.Pages.UsersGettingStarted = function() { }); autocompleteInput.bind('keydown', function(evt){ - if(evt.keyCode === 13 || evt.keyCode === 9 || evt.keyCode === 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length === 0 ){ $('li.as-result-item').first().click(); diff --git a/app/assets/stylesheets/mentions.scss b/app/assets/stylesheets/mentions.scss index fc25ebf28..33deeb978 100644 --- a/app/assets/stylesheets/mentions.scss +++ b/app/assets/stylesheets/mentions.scss @@ -82,15 +82,9 @@ white-space: pre-wrap; word-wrap: break-word; - > div { - color: white; - white-space: pre-wrap; - width: 100%; - - strong { - background: #d8dfea; - font-weight: normal; - } + > strong { + background: $background-blue; + font-weight: normal; } } } diff --git a/app/assets/stylesheets/publisher.scss b/app/assets/stylesheets/publisher.scss index 32e1e49e3..d2897dfa1 100644 --- a/app/assets/stylesheets/publisher.scss +++ b/app/assets/stylesheets/publisher.scss @@ -5,7 +5,7 @@ &.closed { #button_container, - #location_container, + .location-container, #hide_publisher, #photodropzone_container, .counter, @@ -17,7 +17,12 @@ } .container-fluid{ padding: 0; } - .mentions-autocomplete-list ul { width: 100% !important; } + + .twitter-typeahead { + width: calc(100% + 2px); + + .tt-menu { width: 100%; } + } form { margin: 0; @@ -83,7 +88,7 @@ } &.active textarea { - min-height: 70px; + min-height: 90px; } .markdownIndications { @@ -118,22 +123,6 @@ } } - &.with_location .loader { - height: 20px; - width: 20px; - } - &.with_location #location_container { - height: 30px; - margin-bottom: 0; - border-top: 1px dashed $border-grey; - input[type='text'] { - border: none; - color: $text-grey; - height: 20px; - margin-bottom: 0; - padding: 0; - } - } &.active #button_container { border-top: 1px solid $border-grey; } @@ -201,48 +190,6 @@ text-align: center; } - #publisher-images { - margin-right: 5px; - #file-upload, - #locator, - #poll_creator, - #hide_location { - text-decoration: none !important; - font-size: 16px; - line-height: $line-height-computed; - padding: 4px 2px; - i { - color: $text-grey; - } - &:hover{ - i { color: black; } - } - input[type='file'] { - cursor: pointer; - &::-webkit-file-upload-button { - cursor: pointer; - } - } - } - #hide_location { - display: none; - } - } - &.with_location #publisher-images { - #hide_location { display: inline-block; } - #locator { display: none; } - } - - .counter { - height: 30px; - line-height: 30px; - position: absolute; - right: 10px; - bottom: -25px; - } - &.with_location .counter { - bottom: -62px; - } .warning { color: orange; } @@ -261,3 +208,75 @@ } } } + +.publisher-textarea-wrapper { + &:not(.with-location) .location-container { display: none; } + + &.with-location .loader { + height: 20px; + width: 20px; + } + + &.with-location .location-container { + border-top: 1px dashed $border-grey; + height: 30px; + margin-bottom: 0; + + [type='text'] { + border: 0; + color: $text-grey; + height: 20px; + margin-bottom: 0; + padding: 0; + } + } + + &.with-location .counter { + bottom: -62px; + } + + .counter { + bottom: -25px; + height: 30px; + line-height: 30px; + position: absolute; + right: 10px; + } + + &:not(.with-location) .publisher-buttonbar { + .hide-location { display: none; } + .locator { display: inline-block; } + } + + &.with-location .publisher-buttonbar { + .hide-location { display: inline-block; } + .locator { display: none; } + } + + .twitter-typeahead { + left: -1px; + position: absolute; + } +} + +.publisher-buttonbar { + float: right; + margin-right: 5px; + + .btn.btn-link { + font-size: 16px; + line-height: $line-height-computed; + padding: 4px 2px; + text-decoration: none; + i { color: $text-grey; } + + [type='file'], + [type='file']::-webkit-file-upload-button { + cursor: pointer; + } + } + + .btn.btn-link:hover { + i { color: $black; } + } +} diff --git a/app/assets/stylesheets/typeahead.scss b/app/assets/stylesheets/typeahead.scss index 3a46e14b6..7f427c097 100644 --- a/app/assets/stylesheets/typeahead.scss +++ b/app/assets/stylesheets/typeahead.scss @@ -1,10 +1,13 @@ .tt-menu { - width: 300px; - margin-top: ($navbar-height - $input-height-small) / 2; background-color: $navbar-inverse-bg; box-shadow: 0 5px 10px rgba(0,0,0,.2); } +.navbar.navbar-fixed-top .tt-menu { + margin-top: ($navbar-height - $input-height-small) / 2; + width: 300px; +} + .tt-suggestion { border-top: 1px solid $gray-dark; color: $white; @@ -12,10 +15,9 @@ line-height: 20px; &.tt-cursor { background-color: $brand-primary; + border-top: 1px solid $brand-primary; } - &:hover { background-color: lighten($navbar-inverse-bg, 10%); } - &.search-suggestion-person { padding: 8px; .avatar { 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/profiles/_edit_public.haml b/app/views/profiles/_edit_public.haml index ac45aff8c..c909ca841 100644 --- a/app/views/profiles/_edit_public.haml +++ b/app/views/profiles/_edit_public.haml @@ -21,7 +21,7 @@ }); autocompleteInput.bind('keydown', function(evt){ - if(evt.keyCode == 13 || evt.keyCode == 9 || evt.keyCode == 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length == 0 ){ $('li.as-result-item').first().click(); diff --git a/app/views/publisher/_publisher.html.haml b/app/views/publisher/_publisher.html.haml index c6ed2f83e..70d386ce5 100644 --- a/app/views/publisher/_publisher.html.haml +++ b/app/views/publisher/_publisher.html.haml @@ -9,34 +9,38 @@ = form_for(StatusMessage.new) do |status| = status.error_messages %params - #publisher_textarea_wrapper - - if current_user.getting_started? - = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), - :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", - "data-title" => popover_with_close_html("1. " + t("shared.public_explain.share")), - "data-content" => t("shared.public_explain.new_user_welcome_message"), - "class" => "form-control" - - else - = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), - :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", - "class" => "form-control" + .publisher-textarea-wrapper#publisher_textarea_wrapper + .mentions-input-box + .mentions-box + .mentions + - if current_user.getting_started? + = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), + :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", + "data-title" => popover_with_close_html("1. " + t("shared.public_explain.share")), + "data-content" => t("shared.public_explain.new_user_welcome_message"), + "class" => "form-control" + - else + = status.text_area :fake_text, :rows => 2, :value => h(publisher_formatted_text), + :tabindex => 1, :placeholder => "#{t('contacts.index.start_a_conversation')}...", + "class" => "form-control" + %input.typeahead-mention-box.hidden{type: "text"} = status.hidden_field :text, value: h(publisher_hidden_text), class: "clear_on_submit" .container-fluid#photodropzone_container %ul#photodropzone - #location_container.form-group{ style: "padding: 4px 6px;"} + .location-container.form-group{style: "padding: 4px 6px;"} = hidden_field :location, :coords #poll_creator_container -# handlebars template #button_container - .pull-right#publisher-images - .btn.btn-link#poll_creator{title: t("shared.publisher.poll.add_a_poll")} + .publisher-buttonbar#publisher-images + .btn.btn-link.poll-creator#poll_creator{title: t("shared.publisher.poll.add_a_poll")} %i.entypo-bar-graph - .btn.btn-link#file-upload{title: t("shared.publisher.upload_photos")} + .btn.btn-link.file-upload#file-upload{title: t("shared.publisher.upload_photos")} %i.entypo-camera.publisher_image - .btn.btn-link#locator{title: t("shared.publisher.get_location")} + .btn.btn-link.locator#locator{title: t("shared.publisher.get_location")} %i.entypo-location.publisher_image - .btn.btn-link#hide_location{title: t("shared.publisher.remove_location")} + .btn.btn-link.hide-location#hide_location{title: t("shared.publisher.remove_location")} %i.entypo-cross.publisher_image %span.markdownIndications != t("shared.publisher.formatWithMarkdown", markdown_link: link_to(t("help.markdown"), 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/app/views/users/getting_started.mobile.haml b/app/views/users/getting_started.mobile.haml index af1cb4959..2ab3a3f6c 100644 --- a/app/views/users/getting_started.mobile.haml +++ b/app/views/users/getting_started.mobile.haml @@ -25,7 +25,7 @@ }); autocompleteInput.bind('keydown', function(evt){ - if(evt.keyCode == 13 || evt.keyCode == 9 || evt.keyCode == 32){ + if(evt.which === Keycodes.ENTER || evt.which === Keycodes.TAB || evt.which === Keycodes.SPACE) { evt.preventDefault(); if( $('li.as-result-item.active').length == 0 ){ $('li.as-result-item').first().click(); diff --git a/config/.jshint.json b/config/.jshint.json index cf93d1e6e..ab9e3d704 100644 --- a/config/.jshint.json +++ b/config/.jshint.json @@ -66,6 +66,7 @@ "app", "Diaspora", + "Keycodes", "Mentions", "PosixBracketExpressions" ] 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 = $("