diff --git a/app/views/people/show.html.haml b/app/views/people/show.html.haml index 2184906c0..303337bc9 100644 --- a/app/views/people/show.html.haml +++ b/app/views/people/show.html.haml @@ -5,6 +5,8 @@ - content_for :head do = include_javascripts :people + :javascript + Mentions.options.prefillMention = #{@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 f6cb104bc..e1cc91f31 100644 --- a/app/views/status_messages/new.html.haml +++ b/app/views/status_messages/new.html.haml @@ -1,19 +1,13 @@ -# Copyright (c) 2010-2011, Diaspora Inc. This file is -# licensed under the Affero General Public License version 3 or later. See -# the COPYRIGHT file. - - = javascript_include_tag "publisher.js" :javascript - $(document).ready(function() - { - var person = {name: '#{@person.name}', handle: '#{@person.diaspora_handle}' }; - Publisher.autocompletion.onSelect($("#status_message_fake_text"),person,'#{@person.name}'); - $("#publisher #status_message_fake_text").val(function(index, value){ return value + " " }); - $("#publisher").bind('ajax:success', function(){location.reload();}); - Publisher.bookmarklet = true; - }); + $(function() { + $("#publisher").bind('ajax:success', function(){ location.reload(); }); + Publisher.bookmarklet = true; + }); #new_status_message_pane .span-15.last diff --git a/config/assets.yml b/config/assets.yml index 375a1fa4b..72b759643 100644 --- a/config/assets.yml +++ b/config/assets.yml @@ -31,6 +31,10 @@ javascripts: - public/javascripts/vendor/jquery.expander.js - public/javascripts/vendor/timeago.js - public/javascripts/vendor/facebox.js + - public/javascripts/vendor/underscore.js + - public/javascripts/vendor/jquery.events.input.js + - public/javascripts/vendor/jquery.elastic.js + - public/javascripts/vendor/jquery.mentionsInput.js - public/javascripts/jquery.infinitescroll-custom.js - public/javascripts/jquery.autocomplete-custom.js - public/javascripts/jquery.infieldlabel-custom.js @@ -47,6 +51,7 @@ javascripts: - public/javascripts/contact-edit.js - public/javascripts/contact-list.js - public/javascripts/aspect-sorting.js + - public/javascripts/mentions.js - public/javascripts/vendor/bootstrap/bootstrap-twipsy.js - public/javascripts/vendor/bootstrap/bootstrap-popover.js @@ -71,6 +76,10 @@ javascripts: - public/javascripts/aspect-edit-pane.js - public/javascripts/fileuploader-custom.js people: + - public/javascripts/aspect-edit-pane.js + - public/javascripts/fileuploader-custom.js + people: + - public/javascripts/vendor/jquery.autoSuggest.custom.js - public/javascripts/vendor/jquery.autoSuggest.custom.js - public/javascripts/aspect-edit-pane.js photos: @@ -87,6 +96,7 @@ stylesheets: - public/stylesheets/ui.css - public/stylesheets/lightbox.css - public/stylesheets/autocomplete.css + - public/stylesheets/mentions.css - public/stylesheets/tags.css - public/stylesheets/hovercard.css - public/stylesheets/vendor/facebox.css diff --git a/features/step_definitions/custom_web_steps.rb b/features/step_definitions/custom_web_steps.rb index 98f208e47..8bc1cd921 100644 --- a/features/step_definitions/custom_web_steps.rb +++ b/features/step_definitions/custom_web_steps.rb @@ -33,27 +33,10 @@ Then /^the publisher should be expanded$/ do end When /^I append "([^"]*)" to the publisher$/ do |stuff| - # Wait for the publisher to appear and all the elements to lay out - wait_until { evaluate_script("$('#status_message_fake_text').focus().length == 1") } - - # Write to the placeholder field and trigger a keyup to start the copy - page.execute_script <<-JS - $('#status_message_fake_text').val($('#status_message_fake_text').val() + '#{stuff}'); - $('#status_message_fake_text').keyup(); - JS - - # Wait until the text appears in the placeholder + previous_value = page.find("#status_message_fake_text").value + fill_in "status_message_fake_text", :with => previous_value + " " + stuff wait_until do - evaluate_script("$('#status_message_fake_text').val().match(/#{stuff}/) != null") - end - - # WAIT FOR IT!... - - # Wait until the text copy is finished - wait_until do - evaluate_script <<-JS - $('#status_message_text').val() && ($('#status_message_text').val().match(/#{stuff}/) != null) - JS + page.find("#status_message_text").value.match(/#{stuff}/) end end diff --git a/public/javascripts/app/views/publisher_view.js b/public/javascripts/app/views/publisher_view.js index b33f4ea2e..7f8e9d916 100644 --- a/public/javascripts/app/views/publisher_view.js +++ b/public/javascripts/app/views/publisher_view.js @@ -54,8 +54,7 @@ app.views.Publisher = Backbone.View.extend({ // close publishing area (CSS) this.close(); - // clear mentions (TO BE REMOVED!!) - Publisher.autocompletion.mentionList.clear() + Publisher.clear() return this; }, diff --git a/public/javascripts/mentions.js b/public/javascripts/mentions.js new file mode 100644 index 000000000..6436055bb --- /dev/null +++ b/public/javascripts/mentions.js @@ -0,0 +1,26 @@ +var Mentions = { + initialize: function(mentionsInput) { + mentionsInput.mentionsInput(Mentions.options); + Mentions.fetchContacts(); + }, + + fetchContacts : function(){ + $.getJSON($(".selected_contacts_link").attr("href"), function(data) { + Mentions.contacts = data; + }); + }, + + options: { + elastic: false, + + onDataRequest: function(mode, query, callback) { + var filteredResults = _.filter(Mentions.contacts, function(item) { return item.name.toLowerCase().indexOf(query.toLowerCase()) > -1 }); + + callback.call(this, filteredResults); + }, + + templates: { + mentionItemSyntax: _.template("@{<%= mention.name %> ; <%= mention.handle %>}") + } + } +}; diff --git a/public/javascripts/publisher.js b/public/javascripts/publisher.js index 746ca4976..865a0941e 100644 --- a/public/javascripts/publisher.js +++ b/public/javascripts/publisher.js @@ -5,257 +5,22 @@ //TODO: make this a widget var Publisher = { - bookmarklet : false, - cachedForm : false, form: function(){ - if(!Publisher.cachedForm){ - Publisher.cachedForm = $('#publisher'); - } - return Publisher.cachedForm; + return Publisher.cachedForm = Publisher.cachedForm || $('#publisher'); }, - cachedInput : false, input: function(){ - if(!Publisher.cachedInput){ - Publisher.cachedInput = Publisher.form().find('#status_message_fake_text'); - } - return Publisher.cachedInput; + return Publisher.cachedInput = Publisher.cachedInput || Publisher.form().find('#status_message_fake_text'); }, - cachedHiddenInput : false, hiddenInput: function(){ - if(!Publisher.cachedHiddenInput){ - Publisher.cachedHiddenInput = Publisher.form().find('#status_message_text'); - } - return Publisher.cachedHiddenInput; + return Publisher.cachedHiddenInput= Publisher.cachedHiddenInput || Publisher.form().find('#status_message_text'); }, - cachedSubmit : false, submit: function(){ - if(!Publisher.cachedSubmit){ - Publisher.cachedSubmit = Publisher.form().find('#status_message_submit'); - } - return Publisher.cachedSubmit; - }, - - autocompletion: { - options : function(){return { - minChars : 1, - max : 5, - onSelect : Publisher.autocompletion.onSelect, - searchTermFromValue: Publisher.autocompletion.searchTermFromValue, - scroll : false, - formatItem: function(row, i, max) { - return "" + row.name; - }, - formatMatch: function(row, i, max) { - return row.name; - }, - formatResult: function(row) { - return row.name; - }, - disableRightAndLeft : true - };}, - hiddenMentionFromPerson : function(personData){ - return "@{" + personData.name + "; " + personData.handle + "}"; - }, - - onSelect : function(visibleInput, data, formatted) { - var visibleCursorIndex = visibleInput[0].selectionStart; - var visibleLoc = Publisher.autocompletion.addMentionToInput(visibleInput, visibleCursorIndex, formatted); - $.Autocompleter.Selection(visibleInput[0], visibleLoc[1], visibleLoc[1]); - - var mentionString = Publisher.autocompletion.hiddenMentionFromPerson(data); - var mention = { visibleStart: visibleLoc[0], - visibleEnd : visibleLoc[1], - mentionString : mentionString - }; - Publisher.autocompletion.mentionList.push(mention); - Publisher.oldInputContent = visibleInput.val(); - Publisher.hiddenInput().val(Publisher.autocompletion.mentionList.generateHiddenInput(visibleInput.val())); - }, - - mentionList : { - mentions : [], - sortedMentions : function(){ - return this.mentions.sort(function(m1, m2){ - if(m1.visibleStart > m2.visibleStart){ - return -1; - } else if(m1.visibleStart < m2.visibleStart){ - return 1; - } else { - return 0; - } - }); - }, - push : function(mention){ - this.mentions.push(mention); - }, - generateHiddenInput : function(visibleString){ - var resultString = visibleString; - for(var i in this.sortedMentions()){ - var mention = this.mentions[i]; - var start = resultString.slice(0, mention.visibleStart); - var insertion = mention.mentionString; - var end = resultString.slice(mention.visibleEnd); - - resultString = start + insertion + end; - } - return resultString; - }, - - insertionAt : function(insertionStartIndex, selectionEnd, keyCode){ - if(insertionStartIndex != selectionEnd){ - this.selectionDeleted(insertionStartIndex, selectionEnd); - } - this.updateMentionLocations(insertionStartIndex, 1); - this.destroyMentionAt(insertionStartIndex); - }, - deletionAt : function(selectionStart, selectionEnd, keyCode){ - if(selectionStart != selectionEnd){ - this.selectionDeleted(selectionStart, selectionEnd); - return; - } - - var effectiveCursorIndex; - if(keyCode == KEYCODES.DEL){ - effectiveCursorIndex = selectionStart; - }else{ - effectiveCursorIndex = selectionStart - 1; - } - this.updateMentionLocations(effectiveCursorIndex, -1); - this.destroyMentionAt(effectiveCursorIndex); - }, - selectionDeleted : function(selectionStart, selectionEnd){ - Publisher.autocompletion.mentionList.destroyMentionsWithin(selectionStart, selectionEnd); - Publisher.autocompletion.mentionList.updateMentionLocations(selectionStart, selectionStart - selectionEnd); - }, - destroyMentionsWithin : function(start, end){ - for (var i = this.mentions.length - 1; i >= 0; i--){ - var mention = this.mentions[i]; - if(start < mention.visibleEnd && end >= mention.visibleStart){ - this.mentions.splice(i, 1); - } - } - }, - clear: function(){ - this.mentions = []; - }, - destroyMentionAt : function(effectiveCursorIndex){ - - var mentionIndex = this.mentionAt(effectiveCursorIndex); - var mention = this.mentions[mentionIndex]; - if(mention){ - this.mentions.splice(mentionIndex, 1); - } - }, - updateMentionLocations : function(effectiveCursorIndex, offset){ - var changedMentions = this.mentionsAfter(effectiveCursorIndex); - for(var i in changedMentions){ - var mention = changedMentions[i]; - mention.visibleStart += offset; - mention.visibleEnd += offset; - } - }, - mentionAt : function(visibleCursorIndex){ - for(var i in this.mentions){ - var mention = this.mentions[i]; - if(visibleCursorIndex > mention.visibleStart && visibleCursorIndex < mention.visibleEnd){ - return i; - } - } - return false; - }, - mentionsAfter : function(visibleCursorIndex){ - var resultMentions = []; - for(var i in this.mentions){ - var mention = this.mentions[i]; - if(visibleCursorIndex <= mention.visibleStart){ - resultMentions.push(mention); - } - } - return resultMentions; - } - }, - repopulateHiddenInput: function(){ - var newHiddenVal = Publisher.autocompletion.mentionList.generateHiddenInput(Publisher.input().val()); - if(newHiddenVal != Publisher.hiddenInput().val()){ - Publisher.hiddenInput().val(newHiddenVal); - } - }, - - keyUpHandler : function(event){ - Publisher.autocompletion.repopulateHiddenInput(); - Publisher.determineSubmitAvailability(); - }, - - keyDownHandler : function(event){ - var input = Publisher.input(); - var selectionStart = input[0].selectionStart; - var selectionEnd = input[0].selectionEnd; - var isDeletion = (event.keyCode == KEYCODES.DEL && selectionStart < input.val().length) || (event.keyCode == KEYCODES.BACKSPACE && (selectionStart > 0 || selectionStart != selectionEnd)); - var isInsertion = (KEYCODES.isInsertion(event.keyCode) && event.keyCode != KEYCODES.RETURN ); - - if(isDeletion){ - Publisher.autocompletion.mentionList.deletionAt(selectionStart, selectionEnd, event.keyCode); - }else if(isInsertion){ - Publisher.autocompletion.mentionList.insertionAt(selectionStart, selectionEnd, event.keyCode); - } - }, - - addMentionToInput: function(input, cursorIndex, formatted){ - var inputContent = input.val(); - - var stringLoc = Publisher.autocompletion.findStringToReplace(inputContent, cursorIndex); - - var stringStart = inputContent.slice(0, stringLoc[0]); - var stringEnd = inputContent.slice(stringLoc[1]); - - input.val(stringStart + formatted + stringEnd); - var offset = formatted.length - (stringLoc[1] - stringLoc[0]); - Publisher.autocompletion.mentionList.updateMentionLocations(stringStart.length, offset); - return [stringStart.length, stringStart.length + formatted.length]; - }, - - findStringToReplace: function(value, cursorIndex){ - var atLocation = value.lastIndexOf('@', cursorIndex); - if(atLocation == -1){return [0,0];} - var nextAt = cursorIndex; - - if(nextAt == -1){nextAt = value.length;} - return [atLocation, nextAt]; - - }, - - searchTermFromValue: function(value, cursorIndex) { - var stringLoc = Publisher.autocompletion.findStringToReplace(value, cursorIndex); - if(stringLoc[0] <= 2){ - stringLoc[0] = 0; - }else{ - stringLoc[0] -= 2; - } - - var relevantString = value.slice(stringLoc[0], stringLoc[1]).replace(/\s+$/,""); - - var matches = relevantString.match(/(^|\s)@(.+)/); - if(matches){ - return matches[2]; - }else{ - return ''; - } - }, - initialize: function(){ - $.getJSON($("#publisher .selected_contacts_link").attr("href"), undefined , - function(data){ - Publisher.input().autocomplete(data, - Publisher.autocompletion.options()); - Publisher.input().result(Publisher.autocompletion.selectItemCallback); - Publisher.oldInputContent = Publisher.input().val(); - } - ); - } + return Publisher.cachedSubmit = Publisher.cachedSubmit || Publisher.form().find('#status_message_submit'); }, determineSubmitAvailability: function(){ @@ -271,7 +36,11 @@ var Publisher = { }, clear: function(){ - this.autocompletion.mentionList.clear(); + $("#photodropzone").find('li').remove(); + Publisher.input() + .removeClass("with_attachments") + .css('paddingBottom', '') + .mentionsInput("reset"); }, bindServiceIcons: function(){ @@ -380,59 +149,12 @@ var Publisher = { Publisher.toggleAspectIds(li); }); }, - beforeSubmit: function(){ - if($("#publisher .content_creation form #aspect_ids_").length == 0){ - alert(Diaspora.I18n.t('publisher.at_least_one_aspect')); - return false; - } - }, - onSubmit: function(data, json, xhr){ - $("#photodropzone").find('li').remove(); - $("#publisher textarea").removeClass("with_attachments").css('paddingBottom', ''); - }, - onFailure: function(data, json, xhr){ - json = $.parseJSON(json.responseText); - if(json.errors.length !== 0){ - Diaspora.Alert.show(json.errors); - }else{ - Diaspora.Alert.show(Diaspora.I18n.t('failed_to_post_message')); - } - }, - onSuccess: function(data, json, xhr){ - if (Publisher.bookmarklet == false) { - var isPostVisible = Diaspora.page.aspectNavigation.selectedAspects().length == 0; - var postedTo = Publisher.selectedAspectIds(); - - if(Publisher.isPublicPost() || Publisher.isToAllAspects()){ - isPostVisible = true; - - } else { - $.each(Diaspora.page.aspectNavigation.selectedAspects(), function(index, value) { - if (postedTo.indexOf(parseInt(value)) > -1) - isPostVisible = true; - }); - } - - if(isPostVisible) { - Diaspora.page.stream.addPost($("#" + json.post_id)); - } - else { - Diaspora.widgets.flashMessages.render({ - success: true, - message: Diaspora.I18n.t('successfully_posted_message_to_an_aspects_that_is_not_visible') - }); - } - } - - //Stream.setUpImageLinks(); - Stream.setUpAudioLinks(); - }, - bindAjax: function(){ - //Publisher.form().bind('submit', Publisher.beforeSubmit); - Publisher.form().bind('ajax:loading', Publisher.onSubmit); - Publisher.form().bind('ajax:failure', Publisher.onFailure); - Publisher.form().bind('ajax:success', Publisher.onSuccess); + keyUp : function(){ + Publisher.determineSubmitAvailability() + Publisher.input().mentionsInput("val", function(value) { + Publisher.hiddenInput().val(value); + }); }, triggerGettingStarted: function(){ @@ -473,16 +195,22 @@ var Publisher = { Publisher.bindServiceIcons(); Publisher.bindAspectToggles(); - Publisher.autocompletion.initialize(); + /* close text area */ + Publisher.form().delegate("#hide_publisher", "click", function(){ + $.each(Publisher.form().find("textarea"), function(idx, element){ + $(element).val(""); + }); + Publisher.close(); + }); + + Mentions.initialize(Publisher.input()); if(Publisher.hiddenInput().val() === "") { Publisher.hiddenInput().val(Publisher.input().val()); } + Publisher.input().autoResize({'extraSpace' : 10}); - Publisher.input().keydown(Publisher.autocompletion.keyDownHandler); - Publisher.input().keyup(Publisher.autocompletion.keyUpHandler); - Publisher.input().mouseup(Publisher.autocompletion.keyUpHandler); - //Publisher.bindAjax(); + Publisher.input().keyup(Publisher.keyUp) } }; diff --git a/public/javascripts/vendor/jquery.events.input.js b/public/javascripts/vendor/jquery.events.input.js new file mode 100644 index 000000000..9b2bbbfb3 --- /dev/null +++ b/public/javascripts/vendor/jquery.events.input.js @@ -0,0 +1,132 @@ +/* + jQuery `input` special event v1.0 + + http://whattheheadsaid.com/projects/input-special-event + + (c) 2010-2011 Andy Earnshaw + MIT license + www.opensource.org/licenses/mit-license.php + + Modified by Kenneth Auchenberg + * Disabled usage of onPropertyChange event in IE, since its a bit delayed, if you type really fast. +*/ + +(function($) { + // Handler for propertychange events only + function propHandler() { + var $this = $(this); + if (window.event.propertyName == "value" && !$this.data("triggering.inputEvent")) { + $this.data("triggering.inputEvent", true).trigger("input"); + window.setTimeout(function () { + $this.data("triggering.inputEvent", false); + }, 0); + } + } + + $.event.special.input = { + setup: function(data, namespaces) { + var timer, + // Get a reference to the element + elem = this, + // Store the current state of the element + state = elem.value, + // Create a dummy element that we can use for testing event support + tester = document.createElement(this.tagName), + // Check for native oninput + oninput = "oninput" in tester || checkEvent(tester), + // Check for onpropertychange + onprop = "onpropertychange" in tester, + // Generate a random namespace for event bindings + ns = "inputEventNS" + ~~(Math.random() * 10000000), + // Last resort event names + evts = ["focus", "blur", "paste", "cut", "keydown", "drop", ""].join("." + ns + " "); + + function checkState() { + var $this = $(elem); + if (elem.value != state && !$this.data("triggering.inputEvent")) { + state = elem.value; + + $this.data("triggering.inputEvent", true).trigger("input"); + window.setTimeout(function () { + $this.data("triggering.inputEvent", false); + }, 0); + } + } + + // Set up a function to handle the different events that may fire + function handler(e) { + // When focusing, set a timer that polls for changes to the value + if (e.type == "focus") { + checkState(); + clearInterval(timer); + timer = window.setInterval(checkState, 250); + } else if (e.type == "blur") { + // When blurring, cancel the aforeset timer + window.clearInterval(timer); + } else { + // For all other events, queue a timer to check state ASAP + window.setTimeout(checkState, 0); + } + } + + // Bind to native event if available + if (oninput) { + return false; +// } else if (onprop) { +// // Else fall back to propertychange if available +// $(this).find("input, textarea").andSelf().filter("input, textarea").bind("propertychange." + ns, propHandler); + } else { + // Else clutch at straws! + $(this).find("input, textarea").andSelf().filter("input, textarea").bind(evts, handler); + } + $(this).data("inputEventHandlerNS", ns); + }, + teardown: function () { + var elem = $(this); + elem.find("input, textarea").unbind(elem.data("inputEventHandlerNS")); + elem.data("inputEventHandlerNS", ""); + } + }; + + // Setup our jQuery shorthand method + $.fn.input = function (handler) { + return handler ? this.bind("input", handler) : this.trigger("input"); + }; + + /* + The following function tests the element for oninput support in Firefox. Many thanks to + http://blog.danielfriesen.name/2010/02/16/html5-browser-maze-oninput-support/ + */ + function checkEvent(el) { + // First check, for if Firefox fixes its issue with el.oninput = function + el.setAttribute("oninput", "return"); + if (typeof el.oninput == "function") { + return true; + } + // Second check, because Firefox doesn't map oninput attribute to oninput property + try { + + // "* Note * : Disabled focus and dispatch of keypress event due to conflict with DOMready, which resulted in scrolling down to the bottom of the page, possibly because layout wasn't finished rendering. + var e = document.createEvent("KeyboardEvent"), + ok = false, + tester = function(e) { + ok = true; + e.preventDefault(); + e.stopPropagation(); + }; + + // e.initKeyEvent("keypress", true, true, window, false, false, false, false, 0, "e".charCodeAt(0)); + + document.body.appendChild(el); + el.addEventListener("input", tester, false); + // el.focus(); + // el.dispatchEvent(e); + el.removeEventListener("input", tester, false); + document.body.removeChild(el); + return ok; + + } catch(error) { + + } + } +})(jQuery); \ No newline at end of file diff --git a/public/javascripts/vendor/jquery.mentionsInput.js b/public/javascripts/vendor/jquery.mentionsInput.js new file mode 100644 index 000000000..a61746ac6 --- /dev/null +++ b/public/javascripts/vendor/jquery.mentionsInput.js @@ -0,0 +1,387 @@ +/* + * Mentions Input + * Version 1.0 + * Written by: Kenneth Auchenberg (Podio) + * + * Using underscore.js + * + * License: MIT License - http://www.opensource.org/licenses/mit-license.php + */ + +(function ($, _, undefined) { + + // Settings + var KEY = { 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('@[<%= name %>](<%= type %>:<%= id %>)'), + mentionItemHighlight : _.template('<%= name %>') + } + }; + + 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(); + } + } + } + }; + + var MentionsInput = function (input) { + var settings; + var elmInputBox, elmInputWrapper, elmAutocompleteList, elmWrapperBox, elmMentionsOverlay, elmActiveAutoCompleteItem; + var mentionsCollection = []; + var inputBuffer = []; + var currentDataQuery = ''; + + function initTextarea() { + elmInputBox = $(input); + + if (elmInputBox.attr('data-mentions-input') == 'true') { + return; + } + + elmInputWrapper = elmInputBox.parent(); + elmWrapperBox = $(settings.templates.wrapper()); + elmInputBox.wrapAll(elmWrapperBox); + elmWrapperBox = elmInputWrapper.find('> div'); + + elmInputBox.attr('data-mentions-input', 'true'); + elmInputBox.bind('keydown', onInputBoxKeyDown); + elmInputBox.bind('keypress', onInputBoxKeyPress); + elmInputBox.bind('input', onInputBoxInput); + elmInputBox.bind('click', onInputBoxClick); + + if (settings.elastic) { + elmInputBox.elastic(); + } + } + + function initAutocomplete() { + elmAutocompleteList = $(settings.templates.autocompleteList()); + elmAutocompleteList.appendTo(elmWrapperBox); + elmAutocompleteList.delegate('li', 'click', onAutoCompleteItemClick); + } + + function initMentionsOverlay() { + elmMentionsOverlay = $(settings.templates.mentionsOverlay()); + elmMentionsOverlay.prependTo(elmWrapperBox); + } + + function updateNames() { + var syntaxMessage = getInputBoxValue(); + + _.each(mentionsCollection, function (mention) { + var textSyntax = settings.templates.mentionItemSyntax({ name : mention.name, type : 'contact', id : mention.id, mention: mention }); + + syntaxMessage = syntaxMessage.replace(mention.name, textSyntax); + }); + + var mentionText = utils.htmlEncode(syntaxMessage); + + _.each(mentionsCollection, function (mention) { + var textSyntax = settings.templates.mentionItemSyntax({ name : utils.htmlEncode(mention.name), type : 'contact', id : mention.id, mention : mention }); + var textHighlight = settings.templates.mentionItemHighlight({ name : utils.htmlEncode(mention.name), mention : mention }); + + mentionText = mentionText.replace(textSyntax, textHighlight); + }); + + mentionText = mentionText.replace(/\n/g, '
    '); + mentionText = mentionText.replace(/ {2}/g, '  '); + + elmInputBox.data('messageText', syntaxMessage); + elmMentionsOverlay.find('div').html(mentionText); + } + + function resetBuffer() { + inputBuffer = []; + } + + function updateMentionsCollection() { + var inputText = getInputBoxValue(); + + mentionsCollection = _.reject(mentionsCollection, function (mention, index) { + return !mention.name || inputText.indexOf(mention.name) == -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.name).length; + + var updatedMessageText = start + mention.name + end; + + mentionsCollection.push(mention); + + // Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer + resetBuffer(); + currentDataQuery = ''; + hideAutoComplete(); + + // Mentions & syntax message + elmInputBox.val(updatedMessageText); + updateNames(); + + // Set correct focus and selection + elmInputBox.focus(); + utils.setCaratPosition(elmInputBox[0], startEndIndex); + } + + function getInputBoxValue() { + return $.trim(elmInputBox.val()); + } + + function onAutoCompleteItemClick(e) { + var mention = $(this).data("mention"); + + addMention(mention); + + return false; + } + + function onInputBoxClick(e) { + resetBuffer(); + } + + function onInputBoxInput(e) { + updateNames(); + updateMentionsCollection(); + hideAutoComplete(); + + var triggerCharIndex = _.lastIndexOf(inputBuffer, settings.triggerChar); + if (triggerCharIndex > -1) { + currentDataQuery = inputBuffer.slice(triggerCharIndex + 1).join(''); + + _.defer(_.bind(doSearch, this, currentDataQuery)); + } + } + + function onInputBoxKeyPress(e) { + 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); + 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.click(); + 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 mentionedNames = _.pluck(mentionsCollection, 'name'); + results = _.reject(results, function (item) { + return _.include(mentionedNames, item.name); + }); + + if (!results.length) { + hideAutoComplete(); + return; + } + + elmAutocompleteList.empty(); + var elmDropDownList = $("