/** * Plugin Name: areacomplete * Author: Amir Harel, Sean Templeton, Nathan Broadbent * Copyright: amir harel (harel.amir1@gmail.com) * Twitter: @amir_harel * Version 1.4.1 * Published at : http://www.amirharel.com/2011/03/07/implementing-autocomplete-jquery-plugin-for-textarea/ */ (function($){ /** * @param obj * @attr wordCount {Number} the number of words the user want to for matching it with the dictionary * @attr mode {String} set "outter" for using an autocomplete that is being displayed in the outter layout of the textarea, as opposed to inner display * @attr on {Object} containing the followings: * @attr query {Function} will be called to query if there is any match for the user input */ $.fn.areacomplete = function(obj){ if( typeof $.browser.msie != 'undefined' ) obj.mode = 'outter'; this.each(function(index,element){ if( element.nodeName == 'TEXTAREA' ){ makeAutoComplete(element,obj); } }); } var browser = {isChrome: $.browser.webkit }; function getTextAreaSelectionEnd(ta) { var textArea = ta;//document.getElementById('textarea1'); if (document.selection) { //IE var bm = document.selection.createRange().getBookmark(); var sel = textArea.createTextRange(); sel.moveToBookmark(bm); var sleft = textArea.createTextRange(); sleft.collapse(true); sleft.setEndPoint("EndToStart", sel); return sleft.text.length + sel.text.length; } return textArea.selectionEnd; //ff & chrome } function getDefaultCharArray(){ return { '`':0, '~':0, '1':0, '!':0, '2':0, '@':0, '3':0, '#':0, '4':0, '$':0, '5':0, '%':0, '6':0, '^':0, '7':0, '&':0, '8':0, '*':0, '9':0, '(':0, '0':0, ')':0, '-':0, '_':0, '=':0, '+':0, 'q':0, 'Q':0, 'w':0, 'W':0, 'e':0, 'E':0, 'r':0, 'R':0, 't':0, 'T':0, 'y':0, 'Y':0, 'u':0, 'U':0, 'i':0, 'I':0, 'o':0, 'O':0, 'p':0, 'P':0, '[':0, '{':0, ']':0, '}':0, 'a':0, 'A':0, 's':0, 'S':0, 'd':0, 'D':0, 'f':0, 'F':0, 'g':0, 'G':0, 'h':0, 'H':0, 'j':0, 'J':0, 'k':0, 'K':0, 'l':0, 'L':0, ';':0, ':':0, '\'':0, '"':0, '\\':0, '|':0, 'z':0, 'Z':0, 'x':0, 'X':0, 'c':0, 'C':0, 'v':0, 'V':0, 'b':0, 'B':0, 'n':0, 'N':0, 'm':0, 'M':0, ',':0, '<':0, '.':0, '>':0, '/':0, '?':0, ' ':0 }; } function setCharSize(data){ for( var ch in data.chars ){ if( ch == ' ' ) $(data.clone).html(" "); else $(data.clone).html(""+ch+""); var testWidth = $("#test-width_"+data.id).width(); data.chars[ch] = testWidth; } } var _data = {}; var _count = 0; function makeAutoComplete(ta,obj){ _count++; _data[_count] = { id:"auto_"+_count, ta:ta, wordCount:obj.wordCount, on:obj.on, clone:null, lineHeight:0, list:null, charInLines:{}, mode:obj.mode, chars:getDefaultCharArray()}; var clone = createClone(_count); _data[_count].clone = clone; setCharSize(_data[_count]); //_data[_count].lineHeight = $(ta).css("font-size"); var ta_id = $(ta).attr('id') $(ta).after("
"); _data[_count].list = $('.auto-list').first() registerEvents(_data[_count]); } function createClone(id){ var data = _data[id]; var div = document.createElement("div"); var offset = $(data.ta).offset(); offset.top = offset.top - parseInt($(data.ta).css("margin-top")); offset.left = offset.left - parseInt($(data.ta).css("margin-left")); //console.log("createClone: offset.top=",offset.top," offset.left=",offset.left); $(div).css({ position:"absolute", top: offset.top, left: offset.left, "border-collapse" : $(data.ta).css("border-collapse"), "border-bottom-style" : $(data.ta).css("border-bottom-style"), "border-bottom-width" : $(data.ta).css("border-bottom-width"), "border-left-style" : $(data.ta).css("border-left-style"), "border-left-width" : $(data.ta).css("border-left-width"), "border-right-style" : $(data.ta).css("border-right-style"), "border-right-width" : $(data.ta).css("border-right-width"), "border-spacing" : $(data.ta).css("border-spacing"), "border-top-style" : $(data.ta).css("border-top-style"), "border-top-width" : $(data.ta).css("border-top-width"), "direction" : $(data.ta).css("direction"), "font-size-adjust" : $(data.ta).css("font-size-adjust"), "font-size" : $(data.ta).css("font-size"), "font-stretch" : $(data.ta).css("font-stretch"), "font-style" : $(data.ta).css("font-style"), "font-family" : $(data.ta).css("font-family"), "font-variant" : $(data.ta).css("font-variant"), "font-weight" : $(data.ta).css("font-weight"), "width" : $(data.ta).css("width"), "height" : $(data.ta).css("height"), "letter-spacing" : $(data.ta).css("letter-spacing"), "margin-bottom" : $(data.ta).css("margin-bottom"), "margin-top" : $(data.ta).css("margin-top"), "margin-right" : $(data.ta).css("margin-right"), "margin-left" : $(data.ta).css("margin-left"), "padding-bottom" : $(data.ta).css("padding-bottom"), "padding-top" : $(data.ta).css("padding-top"), "padding-right" : $(data.ta).css("padding-right"), "padding-left" : $(data.ta).css("padding-left"), "overflow-x" : "hidden", "line-height" : $(data.ta).css("line-height"), "overflow-y" : "hidden", "z-index" : -10 }); //console.log("createClone: ta width=",$(data.ta).css("width")," ta clientWidth=",data.ta.clientWidth, "scrollWidth=",data.ta.scrollWidth," offsetWidth=",data.ta.offsetWidth," jquery.width=",$(data.ta).width()); //i don't know why by chrome adds some pixels to the clientWidth... data.chromeWidthFix = (data.ta.clientWidth - $(data.ta).width()); data.lineHeight = $(data.ta).css("line-height"); if( isNaN(parseInt(data.lineHeight)) ) data.lineHeight = parseInt($(data.ta).css("font-size"))+2; document.body.appendChild(div); return div; } function getWords(data){ var selectionEnd = getTextAreaSelectionEnd(data.ta);//.selectionEnd; var text = data.ta.value; text = text.substr(0,selectionEnd); if( text.charAt(text.length-1) == ' ' || text.charAt(text.length-1) == '\n' ) return ""; var ret = []; var wordsFound = 0; var pos = text.length-1; while( wordsFound < data.wordCount && pos >= 0 && text.charAt(pos) != '\n'){ ret.unshift(text.charAt(pos)); pos--; if( text.charAt(pos) == ' ' || pos < 0 ){ wordsFound++; } } return ret.join(""); } function showList(data, text, list){ if( !data.listVisible ){ data.listVisible = true; var pos = getCursorPosition(data); $(data.list).css({ left: pos.left+"px", top: pos.top+"px", display: "block" }); } var attrs, value, d; var regEx = new RegExp("("+text+")", "i"); var taWidth = $(data.ta).width()-5; var width = data.mode == "outter" ? taWidth - 3 : ""; $(data.list).empty(); for( var i=0; i< list.length; i++ ) { d = {}; if(typeof list[i] == "string") value = list[i]; else { value = list[i].value; d = list[i].data; } $(data.list).append($('
  • ').css('width', width + 'px').attr({'data-value': value}).html(value.replace(regEx,"$1")).data(d)); } } function breakLines(text,data){ var lines = []; var width = $(data.clone).width(); var line1 = ""; var line1Width = 0; var line2Width = 0; var line2 = ""; var chSize = data.chars; var len = text.length; for( var i=0; i"+lines[i]+""); } miror.append(""+lines[lines.length-1]+""); miror.append(""+restText.replace(/\n/g,"
    ")+" 
    "); miror.get(0).scrollTop = ta.scrollTop; var span = miror.children("#"+data.id); var offset = span.offset(); return {top:offset.top+span.height(),left:offset.left+span.width()}; } function getOuterPosition(data){ var offset = $(data.ta).offset(); return {top:offset.top+$(data.ta).height()+8,left:offset.left}; } function hideList(data){ if( data.listVisible ){ $(data.list).css("display","none"); data.listVisible = false; } } // Selects the first element if none are selected function ensureSelected(data) { var selected = $(data.list).find("[data-selected=true]"); if( selected.length != 1 ){ var new_selected = $(data.list).find("li:first-child") new_selected.attr("data-selected","true"); } if (new_selected[0]) { scrollIntoView(new_selected[0]); } } function setSelected(dir, data){ var selected = $(data.list).find("[data-selected=true]"); // Don't allow selection to wrap from bottom if( selected.length != 1 && dir > 0 ){ var new_selected = $(data.list).find("li:first-child") new_selected.attr("data-selected","true"); } else { var new_selected = dir > 0 ? selected.next() : selected.prev(); // Don't unselect the last element when pressing down arrow, // but DO unselect it if pressing UP from the first element if (new_selected[0] || dir < 0) { selected.attr("data-selected","false"); new_selected.attr("data-selected","true"); } } // Set scroll position on ul if (new_selected[0]) { scrollIntoView(new_selected[0]); } } function scrollIntoView(element) { var container = element.offsetParent; var containerTop = $(container).scrollTop(); var containerBottom = containerTop + $(container).height(); var elemTop = element.offsetTop; var elemBottom = elemTop + $(element).height() + 4; if (elemTop < containerTop) { $(container).scrollTop(elemTop); } else if (elemBottom > containerBottom) { $(container).scrollTop(elemBottom + 4 - $(container).height()); } } function getCurrentSelected(data){ var selected = $(data.list).find("[data-selected=true]"); if( selected.length == 1) return selected.get(0); return null; } function onUserSelected(li,data){ var selectedText = $(li).attr("data-value"); var selectionEnd = getTextAreaSelectionEnd(data.ta);//.selectionEnd; var text = data.ta.value; text = text.substr(0,selectionEnd); //if( text.charAt(text.length-1) == ' ' || text.charAt(text.length-1) == '\n' ) return ""; //var ret = []; var wordsFound = 0; var pos = text.length-1; while( wordsFound < data.wordCount && pos >= 0 && text.charAt(pos) != '\n'){ pos--; if( text.charAt(pos) == ' ' || pos < 0 ){ wordsFound++; } } var a = data.ta.value.substr(0, pos + 1); var c = data.ta.value.substr(selectionEnd, data.ta.value.length); var scrollTop = data.ta.scrollTop; if(data.on && data.on.selected) var retText = data.on.selected(selectedText, $(li).data()); if(retText) selectedText = retText; data.ta.value = a + selectedText + ' ' + c; data.ta.scrollTop = scrollTop; data.ta.selectionEnd = pos + 2 + selectedText.length; hideList(data); $(data.ta).focus(); } function registerEvents(data){ $(data.list).delegate("li","click",function(e){ var li = this; onUserSelected(li,data); e.stopPropagation(); e.preventDefault(); return false; }); $(data.ta).blur(function(e){ setTimeout(function(){ hideList(data); }, 400); }); $(data.ta).click(function(e){ hideList(data); }); $(data.ta).keydown(function(e){ //console.log("keydown keycode="+e.keyCode); if( data.listVisible ){ switch(e.keyCode){ case 27: //esc hideList(data); return false; case 40: // up setSelected(+1,data); e.stopImmediatePropagation(); e.preventDefault(); return false; case 38: // down setSelected(-1,data); e.stopImmediatePropagation(); e.preventDefault(); return false; } if( e.keyCode == 13 ){//enter key var li = getCurrentSelected(data); if( li ){ e.stopImmediatePropagation(); e.preventDefault(); hideList(data); onUserSelected(li,data); return false; } hideList(data); } } }); $(data.ta).keyup(function(e){ if( data.listVisible ){ switch(e.keyCode){ // Ignore arrow key / delete / end / home events case 37: case 38: case 39: case 40: e.stopImmediatePropagation(); e.preventDefault(); return false; } } else { switch(e.keyCode){ // Ignore arrow key / delete / end / home events case 13: case 35: case 36: case 37: case 38: case 39: case 40: case 46: e.stopImmediatePropagation(); e.preventDefault(); return true; } } if (e.keyCode == 27) { return true }; // escape var text = getWords(data); //console.log("getWords return ",text); if( text != "" ){ data.on.query(text,function(list, matching_text){ //console.log("got list = ",list); if (typeof matching_text === "undefined") var matching_text = text; if( list.length ){ showList(data, matching_text, list); ensureSelected(data); } else{ hideList(data); } }); } else{ hideList(data); } }); // Don't lose focus when scrolling the c with the mouse $(data.list).mousedown(function(e){ e.stopImmediatePropagation(); e.preventDefault(); return false; }); $(data.ta).scroll(function(e){ var ta = e.target; var miror = $(data.clone); miror.get(0).scrollTop = ta.scrollTop; }); } })(jQuery);