jquery.mentionsInput wip, almost done

This commit is contained in:
Dan Hansen 2012-01-07 20:15:25 -06:00 committed by Dennis Collinson
parent c073cb078f
commit 168d6cbdbe
10 changed files with 878 additions and 263 deletions

View file

@ -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
@ -87,6 +92,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

View file

@ -0,0 +1,24 @@
var Mentions = {
initialize: function(mentionsInput) {
Mentions.fetchContacts(function(data) {
Mentions.contacts = data;
mentionsInput.mentionsInput(Mentions.options);
});
},
fetchContacts: function(callback) {
$.getJSON($(".selected_contacts_link").attr("href"), callback);
},
options: {
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 %>}")
}
}
};

View file

@ -40,224 +40,6 @@ var Publisher = {
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 "<img src='"+ row.avatar +"' class='avatar'/>" + 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();
}
);
}
},
determineSubmitAvailability: function(){
var onlyWhitespaces = ($.trim(Publisher.input().val()) === ''),
isSubmitDisabled = Publisher.submit().attr('disabled'),
@ -271,7 +53,10 @@ var Publisher = {
},
clear: function(){
this.autocompletion.mentionList.clear();
$("#photodropzone").find('li').remove();
$("#publisher textarea").removeClass("with_attachments")
.css('paddingBottom', '')
.mentionsInput("reset");
},
bindServiceIcons: function(){
@ -385,10 +170,14 @@ var Publisher = {
alert(Diaspora.I18n.t('publisher.at_least_one_aspect'));
return false;
}
Publisher.input().mentionsInput("val", function(value) {
Publisher.hiddenInput().val(value);
});
},
onSubmit: function(data, json, xhr){
$("#photodropzone").find('li').remove();
$("#publisher textarea").removeClass("with_attachments").css('paddingBottom', '');
Publisher.input().removeClass("with_attachments").css('paddingBottom', '');
},
onFailure: function(data, json, xhr){
json = $.parseJSON(json.responseText);
@ -473,16 +262,25 @@ 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.form().find("textarea").bind("focus", function(evt) {
Publisher.open();
});
}
};

View file

@ -0,0 +1,151 @@
/**
* @name Elastic
* @descripton Elastic is jQuery plugin that grow and shrink your textareas automatically
* @version 1.6.10
* @requires jQuery 1.2.6+
*
* @author Jan Jarfalk
* @author-email jan.jarfalk@unwrongest.com
* @author-website http://www.unwrongest.com
*
* @licence MIT License - http://www.opensource.org/licenses/mit-license.php
*/
(function(jQuery) {
jQuery.fn.extend({
elastic: function() {
// We will create a div clone of the textarea
// by copying these attributes from the textarea to the div.
var mimics = [
'paddingTop',
'paddingRight',
'paddingBottom',
'paddingLeft',
'marginTop',
'marginRight',
'marginBottom',
'marginLeft',
'fontSize',
'lineHeight',
'fontFamily',
'width',
'fontWeight',
'border-top-width',
'border-right-width',
'border-bottom-width',
'border-left-width',
'borderTopStyle',
'borderTopColor',
'borderRightStyle',
'borderRightColor',
'borderBottomStyle',
'borderBottomColor',
'borderLeftStyle',
'borderLeftColor',
'box-sizing',
'-moz-box-sizing',
'-webkit-box-sizing'
];
return this.each(function() {
// Elastic only works on textareas
if (this.type !== 'textarea') {
return false;
}
var $textarea = jQuery(this),
$twin = jQuery('<div />').css({'position': 'absolute','display':'none','word-wrap':'break-word'}),
lineHeight = parseInt($textarea.css('line-height'), 10) || parseInt($textarea.css('font-size'), '10'),
minheight = parseInt($textarea.css('height'), 10) || lineHeight * 3,
maxheight = parseInt($textarea.css('max-height'), 10) || Number.MAX_VALUE,
goalheight = 0;
// Opera returns max-height of -1 if not set
if (maxheight < 0) {
maxheight = Number.MAX_VALUE;
}
// Append the twin to the DOM
// We are going to meassure the height of this, not the textarea.
$twin.appendTo($textarea.parent());
// Copy the essential styles (mimics) from the textarea to the twin
var i = mimics.length;
while (i--) {
if (mimics[i].toString() === 'width' && $textarea.css(mimics[i].toString()) === '0px') {
setTwinWidth();
} else {
$twin.css(mimics[i].toString(), $textarea.css(mimics[i].toString()));
}
}
update(true);
// Updates the width of the twin. (solution for textareas with widths in percent)
function setTwinWidth() {
curatedWidth = Math.floor(parseInt($textarea.width(), 10));
if ($twin.width() !== curatedWidth) {
$twin.css({'width': curatedWidth + 'px'});
// Update height of textarea
update(true);
}
}
// Sets a given height and overflow state on the textarea
function setHeightAndOverflow(height, overflow) {
var curratedHeight = Math.floor(parseInt(height, 10));
if ($textarea.height() !== curratedHeight) {
$textarea.css({'height': curratedHeight + 'px','overflow':overflow});
// Fire the custom event resize
$textarea.triggerHandler('resize');
}
}
// This function will update the height of the textarea if necessary
function update(forced) {
// Get curated content from the textarea.
var textareaContent = $textarea.val().replace(/&/g, '&amp;').replace(/ {2}/g, '&nbsp;').replace(/<|>/g, '&gt;').replace(/\n/g, '<br />');
// Compare curated content with curated twin.
var twinContent = $twin.html().replace(/<br>/ig, '<br />');
if (forced || textareaContent + '&nbsp;' !== twinContent) {
// Add an extra white space so new rows are added when you are at the end of a row.
$twin.html(textareaContent + '&nbsp;');
// Change textarea height if twin plus the height of one line differs more than 3 pixel from textarea height
if (Math.abs($twin.outerHeight() + lineHeight - $textarea.outerHeight()) > 3) {
var goalheight = $twin.outerHeight();
if (goalheight >= maxheight) {
setHeightAndOverflow(maxheight, 'auto');
} else if (goalheight <= minheight) {
setHeightAndOverflow(minheight, 'hidden');
} else {
setHeightAndOverflow(goalheight, 'hidden');
}
}
}
}
// Update textarea size on keyup, change, cut and paste
$textarea.bind('input', update);
$textarea.bind('change', update);
$(window).bind('resize', setTwinWidth);
});
}
});
})(jQuery);

132
public/javascripts/vendor/jquery.events.input.js generated vendored Normal file
View file

@ -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);

386
public/javascripts/vendor/jquery.mentionsInput.js generated vendored Normal file
View file

@ -0,0 +1,386 @@
/*
* 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('<div class="mentions-input-box"></div>'),
autocompleteList : _.template('<div class="mentions-autocomplete-list"></div>'),
autocompleteListItem : _.template('<li data-ref-id="<%= id %>" data-ref-type="<%= type %>" data-display="<%= display %>"><%= content %></li>'),
autocompleteListItemAvatar : _.template('<img src="<%= avatar %>" />'),
autocompleteListItemIcon : _.template('<div class="icon <%= icon %>"></div>'),
mentionsOverlay : _.template('<div class="mentions"><div></div></div>'),
mentionItemSyntax : _.template('@[<%= value %>](<%= type %>:<%= id %>)'),
mentionItemHighlight : _.template('<strong><span><%= value %></span></strong>')
}
};
var utils = {
htmlEncode : function (str) {
return _.escape(str);
},
highlightTerm : function (value, term) {
if (!term && !term.length) {
return value;
}
return value.replace(new RegExp("(?![^&;]+;)(?!<[^<>]*)(" + term + ")(?![^<>]*>)(?![^&;]+;)", "gi"), "<b>$1</b>");
},
setCaratPosition : function (domNode, caretPos) {
if (domNode.createTextRange) {
var range = domNode.createTextRange();
range.move('character', caretPos);
range.select();
} else {
if (domNode.selectionStart) {
domNode.focus();
domNode.setSelectionRange(caretPos, caretPos);
} else {
domNode.focus();
}
}
}
};
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 updateValues() {
var syntaxMessage = getInputBoxValue();
_.each(mentionsCollection, function (mention) {
var textSyntax = settings.templates.mentionItemSyntax({ value : mention.value, type : 'contact', id : mention.id });
syntaxMessage = syntaxMessage.replace(mention.value, textSyntax);
});
var mentionText = utils.htmlEncode(syntaxMessage);
_.each(mentionsCollection, function (mention) {
var textSyntax = settings.templates.mentionItemSyntax({ value : utils.htmlEncode(mention.value), type : 'contact', id : mention.id });
var textHighlight = settings.templates.mentionItemHighlight({ value : utils.htmlEncode(mention.value) });
mentionText = mentionText.replace(textSyntax, textHighlight);
});
mentionText = mentionText.replace(/\n/g, '<br />');
mentionText = mentionText.replace(/ {2}/g, '&nbsp; ');
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.value || inputText.indexOf(mention.value) == -1;
});
mentionsCollection = _.compact(mentionsCollection);
}
function addMention(value, id, type) {
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 + value).length;
var updatedMessageText = start + value + end;
mentionsCollection.push({
id : id,
type : type,
value : value
});
// Cleaning before inserting the value, otherwise auto-complete would be triggered with "old" inputbuffer
resetBuffer();
currentDataQuery = '';
hideAutoComplete();
// Mentions & syntax message
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);
addMention(elmTarget.attr('data-display'), elmTarget.attr('data-ref-id'), elmTarget.attr('data-ref-type'));
return false;
}
function onInputBoxClick(e) {
resetBuffer();
}
function onInputBoxInput(e) {
updateValues();
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 mentionValues = _.pluck(mentionsCollection, 'value');
results = _.reject(results, function (item) {
return _.include(mentionValues, item.name);
});
if (!results.length) {
hideAutoComplete();
return;
}
elmAutocompleteList.empty();
var elmDropDownList = $("<ul>").appendTo(elmAutocompleteList).hide();
_.each(results, function (item, index) {
var 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)
}));
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();
},
val : function (callback) {
if (!_.isFunction(callback)) {
return;
}
var value = mentionsCollection.length ? elmInputBox.data('messageText') : getInputBoxValue();
callback.call(this, value);
},
reset : function () {
elmInputBox.val('');
mentionsCollection = [];
updateValues();
},
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, _);

View file

@ -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 {
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);
};
});

View file

@ -935,7 +935,7 @@ label:not(.bootstrapped)
:display none !important
textarea
:height 18px !important
:height 24px !important
.counter
:display none

View file

@ -0,0 +1,96 @@
@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;
left: 1px;
line-height: normal;
overflow: hidden;
padding: 6px 0px 3px;
position: absolute;
right: 0;
top: -1px;
white-space: pre-wrap;
word-wrap: break-word;
> div {
color: white;
white-space: pre-wrap;
width: 100%;
strong { background: #d8dfea; }
em {
}
}
}
}
#publisher .mentions-autocomplete-list ul { width: 483px; }

4
public/stylesheets/vendor/jquery.mentionsInput.css generated vendored Normal file
View file

@ -0,0 +1,4 @@
#publisher .mentions-input-box .mentions-autocomplete-list {
width: 483px;
}