views:

350

answers:

0

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