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 = $("").appendTo(elmAutocompleteList).hide();
+
+ _.each(results, function (item, index) {
+ 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)
+ })).data('mention', item);
+
+ 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);
+ });
+ }
+ }
+
+ // Public methods
+ return {
+ init : function (options) {
+ settings = options;
+
+ initTextarea();
+ initAutocomplete();
+ initMentionsOverlay();
+
+ if(options.prefillMention) {
+ addMention(options.prefillMention);
+ }
+ },
+
+ val : function (callback) {
+ if (!_.isFunction(callback)) {
+ return;
+ }
+
+ var value = mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue();
+ callback.call(this, value);
+ },
+
+ reset : function () {
+ elmInputBox.val('');
+ mentionsCollection = [];
+ updateNames();
+ },
+
+ getMentions : function (callback) {
+ if (!_.isFunction(callback)) {
+ return;
+ }
+
+ callback.call(this, mentionsCollection);
+ }
+ };
+ };
+
+ $.fn.mentionsInput = function (method, settings) {
+
+ if (typeof method === 'object' || !method) {
+ settings = $.extend(true, {}, defaultSettings, method);
+ }
+
+ var outerArguments = arguments;
+
+ return this.each(function () {
+ var instance = $.data(this, 'mentionsInput') || $.data(this, 'mentionsInput', new MentionsInput(this));
+
+ 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, settings);
+
+ } else {
+ $.error('Method ' + method + ' does not exist');
+ }
+
+ });
+ };
+
+})(jQuery, _);
diff --git a/public/javascripts/vendor/underscore.js b/public/javascripts/vendor/underscore.js
index 5579c07d3..c8cd1fd0e 100644
--- a/public/javascripts/vendor/underscore.js
+++ b/public/javascripts/vendor/underscore.js
@@ -1,5 +1,5 @@
-// Underscore.js 1.2.2
-// (c) 2011 Jeremy Ashkenas, DocumentCloud Inc.
+// Underscore.js 1.2.4
+// (c) 2009-2012 Jeremy Ashkenas, DocumentCloud Inc.
// Underscore is freely distributable under the MIT license.
// Portions of Underscore are inspired or borrowed from Prototype,
// Oliver Steele's Functional, and John Resig's Micro-Templating.
@@ -67,7 +67,7 @@
}
// Current version.
- _.VERSION = '1.2.2';
+ _.VERSION = '1.2.4';
// Collection Functions
// --------------------
@@ -101,13 +101,14 @@
each(obj, function(value, index, list) {
results[results.length] = iterator.call(context, value, index, list);
});
+ if (obj.length === +obj.length) results.length = obj.length;
return results;
};
// **Reduce** builds up a single result from a list of values, aka `inject`,
// or `foldl`. Delegates to **ECMAScript 5**'s native `reduce` if available.
_.reduce = _.foldl = _.inject = function(obj, iterator, memo, context) {
- var initial = memo !== void 0;
+ var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduce && obj.reduce === nativeReduce) {
if (context) iterator = _.bind(iterator, context);
@@ -121,20 +122,22 @@
memo = iterator.call(context, memo, value, index, list);
}
});
- if (!initial) throw new TypeError("Reduce of empty array with no initial value");
+ if (!initial) throw new TypeError('Reduce of empty array with no initial value');
return memo;
};
// The right-associative version of reduce, also known as `foldr`.
// Delegates to **ECMAScript 5**'s native `reduceRight` if available.
_.reduceRight = _.foldr = function(obj, iterator, memo, context) {
+ var initial = arguments.length > 2;
if (obj == null) obj = [];
if (nativeReduceRight && obj.reduceRight === nativeReduceRight) {
if (context) iterator = _.bind(iterator, context);
- return memo !== void 0 ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
+ return initial ? obj.reduceRight(iterator, memo) : obj.reduceRight(iterator);
}
- var reversed = (_.isArray(obj) ? obj.slice() : _.toArray(obj)).reverse();
- return _.reduce(reversed, iterator, memo, context);
+ var reversed = _.toArray(obj).reverse();
+ if (context && !initial) iterator = _.bind(iterator, context);
+ return initial ? _.reduce(reversed, iterator, memo, context) : _.reduce(reversed, iterator);
};
// Return the first value which passes a truth test. Aliased as `detect`.
@@ -189,7 +192,7 @@
// Delegates to **ECMAScript 5**'s native `some` if available.
// Aliased as `any`.
var any = _.some = _.any = function(obj, iterator, context) {
- iterator = iterator || _.identity;
+ iterator || (iterator = _.identity);
var result = false;
if (obj == null) return result;
if (nativeSome && obj.some === nativeSome) return obj.some(iterator, context);
@@ -215,7 +218,7 @@
_.invoke = function(obj, method) {
var args = slice.call(arguments, 2);
return _.map(obj, function(value) {
- return (method.call ? method || value : value[method]).apply(value, args);
+ return (_.isFunction(method) ? method || value : value[method]).apply(value, args);
});
};
@@ -402,10 +405,11 @@
});
};
- // Take the difference between one array and another.
+ // Take the difference between one array and a number of other arrays.
// Only the elements present in just the first array will remain.
- _.difference = function(array, other) {
- return _.filter(array, function(value){ return !_.include(other, value); });
+ _.difference = function(array) {
+ var rest = _.flatten(slice.call(arguments, 1));
+ return _.filter(array, function(value){ return !_.include(rest, value); });
};
// Zip together multiple lists into a single array -- elements that share
@@ -432,7 +436,7 @@
return array[i] === item ? i : -1;
}
if (nativeIndexOf && array.indexOf === nativeIndexOf) return array.indexOf(item);
- for (i = 0, l = array.length; i < l; i++) if (array[i] === item) return i;
+ for (i = 0, l = array.length; i < l; i++) if (i in array && array[i] === item) return i;
return -1;
};
@@ -441,7 +445,7 @@
if (array == null) return -1;
if (nativeLastIndexOf && array.lastIndexOf === nativeLastIndexOf) return array.lastIndexOf(item);
var i = array.length;
- while (i--) if (array[i] === item) return i;
+ while (i--) if (i in array && array[i] === item) return i;
return -1;
};
@@ -579,7 +583,7 @@
// conditionally execute the original function.
_.wrap = function(func, wrapper) {
return function() {
- var args = [func].concat(slice.call(arguments));
+ var args = [func].concat(slice.call(arguments, 0));
return wrapper.apply(this, args);
};
};
@@ -587,9 +591,9 @@
// Returns a function that is the composition of a list of functions, each
// consuming the return value of the function that follows.
_.compose = function() {
- var funcs = slice.call(arguments);
+ var funcs = arguments;
return function() {
- var args = slice.call(arguments);
+ var args = arguments;
for (var i = funcs.length - 1; i >= 0; i--) {
args = [funcs[i].apply(this, args)];
}
@@ -677,8 +681,8 @@
if (a._chain) a = a._wrapped;
if (b._chain) b = b._wrapped;
// Invoke a custom `isEqual` method if one is provided.
- if (_.isFunction(a.isEqual)) return a.isEqual(b);
- if (_.isFunction(b.isEqual)) return b.isEqual(a);
+ if (a.isEqual && _.isFunction(a.isEqual)) return a.isEqual(b);
+ if (b.isEqual && _.isFunction(b.isEqual)) return b.isEqual(a);
// Compare `[[Class]]` names.
var className = toString.call(a);
if (className != toString.call(b)) return false;
@@ -687,13 +691,11 @@
case '[object String]':
// Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
// equivalent to `new String("5")`.
- return String(a) == String(b);
+ return a == String(b);
case '[object Number]':
- a = +a;
- b = +b;
// `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
// other numeric values.
- return a != a ? b != b : (a == 0 ? 1 / a == 1 / b : a == b);
+ return a != +a ? b != +b : (a == 0 ? 1 / a == 1 / b : a == +b);
case '[object Date]':
case '[object Boolean]':
// Coerce dates and booleans to numeric primitive values. Dates are compared by their
@@ -733,7 +735,7 @@
}
} else {
// Objects with different constructors are not equivalent.
- if ("constructor" in a != "constructor" in b || a.constructor != b.constructor) return false;
+ if ('constructor' in a != 'constructor' in b || a.constructor != b.constructor) return false;
// Deep compare objects.
for (var key in a) {
if (hasOwnProperty.call(a, key)) {
@@ -786,11 +788,10 @@
};
// Is a given variable an arguments object?
- if (toString.call(arguments) == '[object Arguments]') {
- _.isArguments = function(obj) {
- return toString.call(obj) == '[object Arguments]';
- };
- } else {
+ _.isArguments = function(obj) {
+ return toString.call(obj) == '[object Arguments]';
+ };
+ if (!_.isArguments(arguments)) {
_.isArguments = function(obj) {
return !!(obj && hasOwnProperty.call(obj, 'callee'));
};
@@ -891,6 +892,11 @@
escape : /<%-([\s\S]+?)%>/g
};
+ // When customizing `templateSettings`, if you don't want to define an
+ // interpolation, evaluation or escaping regex, we need one that is
+ // guaranteed not to match.
+ var noMatch = /.^/;
+
// JavaScript micro-templating, similar to John Resig's implementation.
// Underscore templating handles arbitrary delimiters, preserves whitespace,
// and correctly escapes quotes within interpolated code.
@@ -900,22 +906,31 @@
'with(obj||{}){__p.push(\'' +
str.replace(/\\/g, '\\\\')
.replace(/'/g, "\\'")
- .replace(c.escape, function(match, code) {
+ .replace(c.escape || noMatch, function(match, code) {
return "',_.escape(" + code.replace(/\\'/g, "'") + "),'";
})
- .replace(c.interpolate, function(match, code) {
+ .replace(c.interpolate || noMatch, function(match, code) {
return "'," + code.replace(/\\'/g, "'") + ",'";
})
- .replace(c.evaluate || null, function(match, code) {
+ .replace(c.evaluate || noMatch, function(match, code) {
return "');" + code.replace(/\\'/g, "'")
- .replace(/[\r\n\t]/g, ' ') + ";__p.push('";
+ .replace(/[\r\n\t]/g, ' ')
+ .replace(/\\\\/g, '\\') + ";__p.push('";
})
.replace(/\r/g, '\\r')
.replace(/\n/g, '\\n')
.replace(/\t/g, '\\t')
+ "');}return __p.join('');";
var func = new Function('obj', '_', tmpl);
- return data ? func(data, _) : function(data) { return func(data, _) };
+ if (data) return func(data, _);
+ return function(data) {
+ return func.call(this, data, _);
+ };
+ };
+
+ // Add a "chain" function, which will delegate to the wrapper.
+ _.chain = function(obj) {
+ return _(obj).chain();
};
// The OOP Wrapper
@@ -950,8 +965,11 @@
each(['pop', 'push', 'reverse', 'shift', 'sort', 'splice', 'unshift'], function(name) {
var method = ArrayProto[name];
wrapper.prototype[name] = function() {
- method.apply(this._wrapped, arguments);
- return result(this._wrapped, this._chain);
+ var wrapped = this._wrapped;
+ method.apply(wrapped, arguments);
+ var length = wrapped.length;
+ if ((name == 'shift' || name == 'splice') && length === 0) delete wrapped[0];
+ return result(wrapped, this._chain);
};
});
diff --git a/public/stylesheets/sass/application.sass b/public/stylesheets/sass/application.sass
index 4c6d18272..8c0429a64 100644
--- a/public/stylesheets/sass/application.sass
+++ b/public/stylesheets/sass/application.sass
@@ -935,7 +935,7 @@ label:not(.bootstrapped)
:display none !important
textarea
- :height 18px !important
+ :height 24px !important
.counter
:display none
diff --git a/public/stylesheets/sass/mentions.scss b/public/stylesheets/sass/mentions.scss
new file mode 100644
index 000000000..368999a64
--- /dev/null
+++ b/public/stylesheets/sass/mentions.scss
@@ -0,0 +1,97 @@
+
+@import 'mixins';
+
+.mentions-input-box {
+ background: #fff;
+ position: relative;
+
+ textarea {
+ display: block;
+ background: transparent;
+ border: 1px solid #dcdcdc;
+ border-radius: 3px;
+ outline: 0;
+ overflow: hidden;
+ position: relative;
+ resize: none;
+ width: 100%;
+
+
+ -webkit-box-sizing: border-box;
+ -moz-box-sizing: border-box;
+ box-sizing: border-box;
+ }
+
+ .mentions-autocomplete-list {
+ background: white;
+ display: none;
+ left: 0;
+ margin-left: -1px;
+ position: absolute;
+ right: 0;
+ z-index: 10000;
+
+
+ ul {
+ border: 1px solid #999;
+ margin: 0;
+ padding: 0;
+
+ @include border-radius(0px, 0px, 5px, 5px);
+
+ li {
+ background: white;
+ border-bottom: 1px solid #ccc;
+ cursor: pointer;
+ font-size: 15px;
+ height: 26px;
+ line-height: 26px;
+ list-style: none;
+ margin: 0;
+ overflow: hidden;
+ padding: 5px;
+ text-decoration: underline;
+ white-space: nowrap;
+
+ &:hover, &.active { background: #eee; }
+ &:last-child { @include border-radius(0px, 0px, 5px, 5px); }
+
+ img, div.icon {
+ float: left;
+ height: 25px;
+ margin-right: 5px;
+ width: 25px;
+ }
+ }
+ }
+ }
+
+ .mentions {
+ bottom: 0;
+ color: white;
+ font-size: 14px;
+ font-family: Arial, Helvetica, sans-serif;
+ left: 4px;
+ line-height: normal;
+ overflow: hidden;
+ padding: 6px 0px 3px;
+ position: absolute;
+ right: 0;
+ top: -2px;
+ white-space: pre-wrap;
+ word-wrap: break-word;
+
+ > div {
+ color: white;
+ white-space: pre-wrap;
+ width: 100%;
+
+ strong {
+ background: #d8dfea;
+ font-weight: normal;
+ }
+ }
+ }
+}
+
+#publisher .mentions-autocomplete-list ul { width: 483px; }
diff --git a/public/stylesheets/vendor/jquery.mentionsInput.css b/public/stylesheets/vendor/jquery.mentionsInput.css
new file mode 100644
index 000000000..1ea96dc74
--- /dev/null
+++ b/public/stylesheets/vendor/jquery.mentionsInput.css
@@ -0,0 +1,4 @@
+
+#publisher .mentions-input-box .mentions-autocomplete-list {
+ width: 483px;
+}
diff --git a/spec/javascripts/publisher-spec.js b/spec/javascripts/publisher-spec.js
index 56bebe5ff..af84eeb39 100644
--- a/spec/javascripts/publisher-spec.js
+++ b/spec/javascripts/publisher-spec.js
@@ -174,259 +174,4 @@ describe("Publisher", function() {
expect(Publisher.input().length).toBe(1);
});
});
-
- describe("autocompletion", function(){
- describe("searchTermFromValue", function(){
- beforeEach(function(){
- this.func = Publisher.autocompletion.searchTermFromValue;
- });
-
- it("returns nothing if the cursor is before the @", function(){
- expect(this.func('not @dan grip', 2)).toBe('');
- });
-
- it("returns everything up to the cursor if the cursor is a word after that @", function(){
- expect(this.func('not @dan grip', 13)).toBe('dan grip');
- });
-
- it("returns up to the cursor if the cursor is after that @", function(){
- expect(this.func('not @dan grip', 7)).toBe('da');
- });
-
- it("returns everything after an @ at the start of the line", function(){
- expect(this.func('@dan grip', 9)).toBe('dan grip');
- });
- it("returns nothing if there is no @", function(){
- expect(this.func('dan', 3)).toBe('');
- });
- it("returns nothing for just an @", function(){
- expect(this.func('@', 1)).toBe('');
- });
- it("returns nothing if there are letters preceding the @", function(){
- expect(this.func('ioj@asdo', 8)).toBe('');
- });
- it("returns everything up to the cursor if there are 2 @s and the cursor is between them", function(){
- expect(this.func('@asdpo aoisdj @asodk', 8)).toBe('asdpo');
- });
- it("returns everything from the 2nd @ up to the cursor if there are 2 @s and the cursor after them", function(){
- expect(this.func('@asod asdo @asd asok', 15)).toBe('asd');
- });
- });
-
- describe("mentionList", function(){
- beforeEach(function(){
- spec.loadFixture('aspects_index');
-
- this.list = Publisher.autocompletion.mentionList;
- this.visibleInput = Publisher.input();
- this.hiddenInput = Publisher.hiddenInput();
- this.mention = { visibleStart : 0,
- visibleEnd : 5,
- mentionString : "@{Danny; dan@pod.org}"
- };
-
- this.list.mentions = [];
- this.list.push(this.mention);
- this.visibleVal = "Danny loves testing javascript";
- this.visibleInput.val(this.visibleVal);
- this.hiddenVal = "@{Danny; dan@pod.org} loves testing javascript";
- this.hiddenInput.val(this.hiddenVal);
- });
-
- describe("selectionDeleted", function(){
- beforeEach(function(){
- this.func = this.list.selectionDeleted;
- this.visibleVal = "Danny Daniel David Darren";
- this.visibleInput.val(this.visibleVal);
- this.list.mentions = [];
- this.danny = {
- visibleStart : 0,
- visibleEnd : 5,
- mentionString : "@{Danny; danny@pod.org}"
- };
- this.daniel = {
- visibleStart : 6,
- visibleEnd : 12,
- mentionString : "@{Daniel; daniel@pod.org}"
- };
- this.david = {
- visibleStart : 13,
- visibleEnd : 18,
- mentionString : "@{David; david@pod.org}"
- };
- this.darren = {
- visibleStart : 19,
- visibleEnd : 25,
- mentionString : "@{Darren; darren@pod.org}"
- };
-
- _.each([this.danny, this.daniel, this.david, this.darren], function(person){
- this.list.push(person);
- }, this);
- });
-
- it("destroys mentions within the selection", function(){
- this.func(4,11);
- expect(this.list.sortedMentions()).toEqual([this.darren, this.david])
- });
-
- it("moves remaining mentions back", function(){
- this.func(7,14);
- var length = 11 - 4;
-
- expect(this.danny.visibleStart).toBe(0);
- expect(this.darren.visibleStart).toBe(19-length);
- });
- });
-
- describe("generateHiddenInput", function(){
- it("replaces mentions in a string", function(){
- expect(this.list.generateHiddenInput(this.visibleVal)).toBe(this.hiddenVal);
- });
- });
-
- describe("push", function(){
- it("adds mention to mentions array", function(){
- expect(this.list.mentions.length).toBe(1);
- expect(this.list.mentions[0]).toBe(this.mention)
- });
- });
-
- describe("mentionAt", function(){
- it("returns the location of the mention at that location in the mentions array", function(){
- expect(this.list.mentions[this.list.mentionAt(3)]).toBe(this.mention);
- });
-
- it("returns null if there is no mention", function(){
- expect(this.list.mentionAt(8)).toBeFalsy();
- });
- });
-
- describe("insertionAt", function(){
- it("does nothing if there is no visible mention at that index", function(){
- this.list.insertionAt(8);
- expect(this.visibleInput.val()).toBe(this.visibleVal);
- expect(this.hiddenInput.val()).toBe(this.hiddenVal);
- });
-
- it("deletes the mention from the hidden field if there is a mention", function(){
- this.list.insertionAt(3);
- expect(this.visibleInput.val()).toBe(this.visibleVal);
- expect(this.list.generateHiddenInput(this.visibleInput.val())).toBe(this.visibleVal);
- });
-
- it("deletes the mention from the list", function(){
- this.list.insertionAt(3);
- expect(this.list.mentionAt(3)).toBeFalsy();
- });
-
- it("calls updateMentionLocations", function(){
- mentionTwo = { visibleStart : 8,
- visibleEnd : 15,
- mentionString : "@{SomeoneElse; other@pod.org}"
- };
- this.list.push(mentionTwo);
-
- spyOn(this.list, 'updateMentionLocations');
- this.list.insertionAt(3,4, 60);
- expect(this.list.updateMentionLocations).toHaveBeenCalled();
- });
- });
-
- describe("updateMentionLocations", function(){
- it("updates the offsets of the remaining mentions in the list", function(){
- mentionTwo = { visibleStart : 8,
- visibleEnd : 15,
- mentionString : "@{SomeoneElse; other@pod.org}"
- };
- this.list.push(mentionTwo);
- this.list.updateMentionLocations(7, 1);
- expect(mentionTwo.visibleStart).toBe(9);
- expect(mentionTwo.visibleEnd).toBe(16);
- });
- });
- });
-
- describe("keyUpHandler", function(){
- beforeEach(function(){
- spec.loadFixture('aspects_index');
- Publisher.initialize();
- this.input = Publisher.input();
- this.submit = Publisher.submit();
- Publisher.open();
- });
-
- it("keep the share button disabled when adding only whitespaces", function(){
- expect(this.submit.attr('disabled')).toBeTruthy();
- this.input.val(' ');
- this.input.keyup();
- expect(this.submit.attr('disabled')).toBeTruthy();
- });
-
- it("enable the share button when adding non-whitespace characters", function(){
- expect(this.submit.attr('disabled')).toBeTruthy();
- this.input.val('some text');
- this.input.keyup();
- expect(this.submit.attr('disabled')).toBeFalsy();
- });
-
- it("should toggle share button disable/enable when playing with input", function(){
- expect(this.submit.attr('disabled')).toBeTruthy();
- this.input.val(' ');
- this.input.keyup();
- expect(this.submit.attr('disabled')).toBeTruthy();
- this.input.val('text');
- this.input.keyup();
- this.expect(this.submit.attr('disabled')).toBeFalsy();
- this.input.val('');
- this.input.keyup();
- expect(this.submit.attr('disabled')).toBeTruthy();
- });
- });
-
- describe("addMentionToInput", function(){
- beforeEach(function(){
- spec.loadFixture('aspects_index');
- this.func = Publisher.autocompletion.addMentionToInput;
- this.input = Publisher.input();
- this.replaceWith = "Replace with this.";
- Publisher.autocompletion.mentionList.mentions = [];
- });
-
- it("replaces everything up to the cursor if the cursor is a word after that @", function(){
- this.input.val('not @dan grip');
- var cursorIndex = 13;
- this.func(this.input, cursorIndex, this.replaceWith);
- expect(this.input.val()).toBe('not ' + this.replaceWith);
- });
-
- it("replaces everything between @ and the cursor if the cursor is after that @", function(){
- this.input.val('not @dan grip');
- var cursorIndex = 7;
- this.func(this.input, cursorIndex, this.replaceWith);
- expect(this.input.val()).toBe('not ' + this.replaceWith + 'n grip');
- });
-
- it("replaces everything up to the cursor from @ at the start of the line", function(){
- this.input.val('@dan grip');
- var cursorIndex = 9;
- this.func(this.input, cursorIndex, this.replaceWith);
- expect(this.input.val()).toBe(this.replaceWith);
- });
-
- it("replaces everything between the first @ and the cursor if there are 2 @s and the cursor is between them", function(){
- this.input.val('@asdpo aoisdj @asodk');
- var cursorIndex = 8;
- this.func(this.input, cursorIndex, this.replaceWith);
- expect(this.input.val()).toBe(this.replaceWith + 'aoisdj @asodk');
- });
-
- it("replaces everything after the 2nd @ if there are 2 @s and the cursor after them", function(){
- this.input.val('@asod asdo @asd asok');
- var cursorIndex = 15;
- this.func(this.input, cursorIndex, this.replaceWith);
- expect(this.input.val()).toBe('@asod asdo ' + this.replaceWith + ' asok');
- });
- });
- });
});
diff --git a/spec/javascripts/support/jasmine.yml b/spec/javascripts/support/jasmine.yml
index d063d9925..958a0fbe3 100644
--- a/spec/javascripts/support/jasmine.yml
+++ b/spec/javascripts/support/jasmine.yml
@@ -11,6 +11,7 @@
# - dist/**/*.js
#
src_files:
+ - public/javascripts/vendor/underscore.js
- public/javascripts/vendor/jquery-1.7.1.min.js
- public/javascripts/vendor/jquery-ui-1.8.9.custom.min.js
- public/javascripts/vendor/bootstrap/bootstrap-popover.js
@@ -20,11 +21,11 @@ src_files:
- public/javascripts/vendor/jquery.autoresize.js
- public/javascripts/vendor/jquery.expander.js
- public/javascripts/vendor/jquery.charcount.js
+ - public/javascripts/vendor/jquery.mentionsInput.js
- public/javascripts/vendor/timeago.js
- public/javascripts/vendor/facebox.js
- public/javascripts/vendor/markdown/*
- public/javascripts/jquery.infieldlabel-custom.js
- - public/javascripts/vendor/underscore.js
- public/javascripts/vendor/backbone.js
- public/javascripts/vendor/handlebars-1.0.0.beta.6.js
- public/javascripts/fileuploader-custom.js
@@ -50,6 +51,7 @@ src_files:
- public/javascripts/mobile.js
- public/javascripts/contact-list.js
- public/javascripts/view.js
+ - public/javascripts/mentions.js
- public/javascripts/publisher.js
- public/javascripts/stream.js
- public/javascripts/validation.js