Hello.
I have developed a system in PHP which processes #hashtags like on Twitter. Now I'm trying to build a system that will suggest tags as I type. When a user starts writing a tag, a drop-down list should appear beneath the textarea with other tags that begin with the same string.
Right now, I have it working where if a user types the hash key (#) the list will show up with the most popular #hashtags. When a tag is clicked, it is inserted at the end of the text in the textarea. I need the tag to be inserted at the cursor location instead. Here's my code; it operates on a textarea with class "facebook_status_text" and a div with class "fbssts_floating_suggestions" that contains an unordered list of links. (Also note that the syntax [#my tag] is used to handle tags with spaces.)
maxlength = 140;
var dest = $('.facebook_status_text:first');
var fbssts_box = $('.fbssts_floating_suggestions');
var fbssts_box_orig = fbssts_box.html();
dest.keyup(function(fbss_key) {
if (fbss_key.which == 51) {
fbssts_box.html(fbssts_box_orig);
$('.fbssts_floating_suggestions .fbssts_suggestions a').click(function() {
var tag = $(this).html();
//This part is not well-optimized.
if (tag.match(/W/)) {
tag = '[#'+ tag +']';
}
else {
tag = '#'+ tag;
}
var orig = dest.val();
orig = orig.substring(0, orig.length - 1);
var last = orig.substring(orig.length - 1);
if (last == '[') {
orig = orig.substring(0, orig.length - 1);
}
//End of particularly poorly optimized code.
dest.val(orig + tag);
fbssts_box.hide();
dest.focus();
return false;
});
fbssts_box.show();
fbssts_box.css('left', dest.offset().left);
fbssts_box.css('top', dest.offset().top + dest.outerHeight() + 1);
}
else if (fbss_key.which != 16) {
fbssts_box.hide();
}
});
dest.blur(function() {
var t = setTimeout(function() { fbssts_box.hide(); }, 250);
});
When the user types, I also need get the 100 characters in the textarea before the cursor, and pass it (presumably via POST) to /fbssts/load/tags. The PHP back-end will process this, figure out what tags to suggest, and print the relevant HTML. Then I need to load that HTML into the .fbssts_floating_suggestions div at the cursor location.
Ideally, I'd like to be able to do this:
var newSuggestions = load('/fbssts/load/tags', {text: dest.getTextBeforeCursor()});
fbssts_box.html(fbssts_box_orig);
$('.fbssts_floating_suggestions .fbssts_suggestions a').click(function() {
var tag = $(this).html();
if (tag.match(/W/)) {
tag = tag +']';
}
dest.insertAtCursor(tag);
fbssts_box.hide();
dest.focus();
return false;
});
And here's the regex I'm using to identify tags (and @mentions) in the PHP back-end, FWIW.
%(\A(#|@)(\w|(\p{L}\p{M}?))+\b)|((?<=\s)(#|@)(\w|(\p{L}\p{M}?))+\b)|(\[(#|@).+?\])%u
Right now, my main hold-up is dealing with the cursor location. I've researched for the last two hours, and just ended up more confused. I would prefer a jQuery solution, but beggars can't be choosers.
Thanks!
EDIT:
With help from people in #jquery on IRC, I arrived at this solution for the JS functions I'd pined for above. This should be enough to get me where I want to go.
(function($){
/**
* Adapted from
* http://alexking.org/blog/2003/06/02/inserting-at-the-cursor-using-javascript
*/
$.fn.insertAtCursor = function (myValue) {
return this.each(function() {
//MSIE
if (document.selection) {
this.focus();
sel = document.selection.createRange();
sel.text = myValue;
this.focus();
}
//Firefox etc.
else if (this.selectionStart || this.selectionStart == '0') {
var startPos = this.selectionStart;
var endPos = this.selectionEnd;
var scrollTop = this.scrollTop;
this.value = this.value.substring(0, startPos)
+ myValue
+ this.value.substring(endPos, this.value.length);
this.focus();
this.selectionStart = startPos + myValue.length;
this.selectionEnd = startPos + myValue.length;
this.scrollTop = scrollTop;
}
else {
this.value += myValue;
this.focus();
}
});
};
/**
* Inspired by http://plugins.jquery.com/project/jCaret
*/
$.fn.textBeforeCursor=function(distanceBefore) {
var t=this[0];
if($.browser.msie){
var range = document.selection.createRange();
var stored_range = range.duplicate();
stored_range.moveToElementText(t);
stored_range.setEndPoint('EndToEnd', range);
var e = stored_range.text.length - range.text.length;
var s = e - distanceBefore;
}
else {
var e = t.selectionStart, s = e - distanceBefore;
}
if (s < 0) {
s = 0;
}
var te = t.value.substring(s, e);
return {start: s, end: e, text: te}
};
})(jQuery);