Commits

Yang Zhang committed 7c3f154

add at.js----> finish @ feature UI part

  • Participants
  • Parent commits ded7429

Comments (0)

Files changed (22)

         g.db.commit()
 
     @staticmethod
-    def get_at_username(startword = ""):
-        return jsonify(users = [dict(nickname="thisistest",id=1),
-                        dict(nickname="thisistest",id=1),
-                        dict(nickname="thisistest",id=1),
-                        dict(nickname="thisistest",id=1),
-                        dict(nickname="thisistest",id=1),]
-        
-        
+    def get_user_startswith(word):
+        return jsonify(users = [dict(nickname="thisistest1",id=1),
+                        dict(nickname="thisistest2",id=1),
+                        dict(nickname="thisistest3",id=1),
+                        dict(nickname="thisistest4",id=1),
+                        dict(nickname="thisistest5",id=1)]
         )
         
     def update_email_nickname(self):
             {'userid':userid,'password':original_pw})
         if(c.fetchone() is None):
             return None
-        c.execute("UPDATE user SET password=:password WHERE id=:userid",\
+        c.execute("UPDATE user SET pas)sword=:password WHERE id=:userid",\
             {'userid':userid,'password':changed_pw})
         g.db.commit()
         return True

static/At.js/.gitignore

+*.swp

static/At.js/MIT-LICENSE.txt

+Copyright (c) 2012 chord.luo@gmail.com
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

static/At.js/README.md

+Add Twitter / Weibo style @ mentions autocomplete to your application.
+
+**Let me know you are using it. So I will work harder on it, Thanks.:)**  
+add your websit on [THIS LIST](https://github.com/ichord/At.js/wiki/Sites) or just tell me if you are using At.js 
+
+###Demo
+[http://ichord.github.com/At.js][1]
+
+###Features
+* Can listen to any character  
+    not just '@', and set up multiple listeners for different characters with different behavior and data.
+* Supports static data and dynamic data(via AJAX) at the same time  
+    static data will be searched first, and then use AJAX to fetch non-existing values.
+* Listener events can be bound to multiple textareas
+* Cacheable
+* Format returned data using templates
+* Keyboard controls in addition to mouse   
+    `Tab` or `Enter` keys select the value, `Up` and `Down` navigate between values
+
+### Requirements
+* jQuery >= 1.7.0.
+
+### Usage
+
+---
+
+#### Settings
+
+Here is the Default setting.
+
+```javascript
+    /*
+     Callback function to dynamically retrieve data based on query.
+     `At` will pass two arguments to the callback: `query` and `callback`.
+     `query` is the keyword that is being autocompleted after the character listener ('@' is the default)
+     `callback` should be run on the data. It accepts a string array or plain object array
+     */
+    'callback': null,
+
+    /*
+     Enable search cache. Set to false if you want to use $.ajax cache.
+     */
+    'cache': true,
+
+    /* 
+     Static data to use before the callback is invoked
+     */
+    'data': [],
+
+    /*
+     How many items to show at a time in the results
+     */
+    'limit': 5,
+
+    /* 
+     Item format template
+     `data-value` contents will be inserted to the textarea on selection
+     */
+    'tpl': "<li id='${index}' data-value='${name}'>${name}</li>",
+
+    /*
+     The name of the data attribute in the item template
+     You can change it into any name defined in attributes of `li` element which is template
+     */
+    'choose': "data-value"
+```
+
+#### Using static data
+
+Bind a textarea to listen to a specific character and pass an array of data in the `data` parameter  
+The first argument is the character you want to listen, and the second one is a map of options:
+
+``` javascript
+    var emoji_list = [
+        "apple", "aquarius", "aries", "arrow_backward", "arrow_down",
+        "arrow_forward", "arrow_left", "arrow_lower_left", "arrow_lower_right",
+        "arrow_right", "arrow_up", "arrow_upper_left", "arrow_upper_right"
+    ];
+    
+    $('textarea').atWho(":", {data:emoji_list});
+```
+
+#### Using dynamic data with AJAX
+
+This time we pass a callback function instead of the static data as the second parameter.  
+You can just set a function as second argument, At.js will determine it and set it to callback option.  
+the data - `names` - would be a string array or a map array which the same as `data` option  
+`query` argument is the string behind the character you are listening as "@" in this example.
+
+``` javascript
+    $('textarea').atWho("@", function(query, callback) {
+        var url = "data.json",
+            param = {'q':query};
+        $.ajax(url, param, function(data) {
+            names = $.parseJSON(data);
+            callback(names);
+        });
+    });
+```
+
+#### Using both static data and dynamic data
+
+We pass a configuration object containing both the `data` and `callback` parameters.  
+It will search the local static data first.
+
+``` javascript
+    var names = ['one', 'two'];
+    $('textarea').atWho("@", {
+        'data': names,
+        'callback': function(query, callback) { 
+            console.log(query, callback);
+        }
+    });
+```
+
+#### Custom templates
+
+**base template**, `li` element and `data-value` attribute are all necessary.  
+We also show how to set up multiple listeners with different characters.
+
+``` html
+    <li data-value='${word}'>anything here</li>
+```
+
+---
+
+we use these static data in all examples below:
+
+``` javascript
+    emojis = $.map(emojis, function(value, i) {
+        return {'id':i, 'key':value+":", 'name':value};
+    });
+
+    data = $.map(data, function(value, i) {
+        return {'id':i, 'name':value, 'email':value+"@email.com"};
+    });
+```
+
+##### Simple
+
+At.js will search by `data-value` and the contents will be inserted to the textarea on selection  
+
+``` javascript
+    $("textarea").atWho("@",{
+        'tpl': "<li id='${id}' data-value='${name}'>${name} <small>${email}</small></li>",
+        'data': data
+    });
+```
+
+``` javascript
+    $("textarea").atWho(":",{
+        tpl: "<li data-value='${key}'>${name} <img src='http://xxx/emoji/${name}.png' height='20' width='20' /></li>",
+        data: emojis
+    });
+```
+
+##### With callback
+
+``` javascript
+    $('textarea').atWho("@",{
+        tpl: "<li id='${id}' data-value='${name}'>${name} <small>${email}</small></li>",
+        callback: function(query, callback) {
+            var url = "data.json",
+                param = {'q':query};
+            $.ajax(url, param, function(data) {
+                names = $.parseJSON(data);
+                callback(names);
+            });
+        }
+    });
+```
+
+##### Insert different value
+
+Alternatively, you can specific which value would be inserted by setting `choose` option.
+
+``` javascript
+    $("textarea").atWho("@", {
+        'tpl': "<li id='${id}' data-value='${name}' data-insert='${email}'>${name} <small>${email}</small></li>",
+        'data': data,
+        'choose': "data-insert"
+    });
+```
+
+---
+
+#### Update Data
+If you want to update data to all binded inputor or specified one. You can do that like this:
+
+``` javascript
+    // for all binded textarea
+    $('textarea').atWho("@", {data:new_data})
+    // for specified one
+    $('textarea#at_mention').atWho("@", {data:new_data})
+```
+
+It won't change others setting which has been setted earlier.
+Actually, It just update the setting. You can use it to change others settings like that.
+
+
+[1]: http://ichord.github.com/At.js

static/At.js/changelog.md

+###v0.1.4 2012-3-23
+* box showing above instead of bottom when it get close to the bottom of window
+* coffeescript here is.
+* every registered character able to have thire own options such as template(`tpl`)
+* every inputor (textarea, input) able to have their own registered character and different behavior
+  even the same character to other inputor
+
+###v0.1.0
+* 可以監聽多個字符  
+    multiple char listening.
+* 顯示缺省列表.  
+    show default list.

static/At.js/coffee/jquery.atwho.coffee

+###
+   Implement Twitter/Weibo @ mentions
+
+   Copyright (c) 2012 chord.luo@gmail.com
+
+   Permission is hereby granted, free of charge, to any person obtaining
+   a copy of this software and associated documentation files (the
+   "Software"), to deal in the Software without restriction, including
+   without limitation the rights to use, copy, modify, merge, publish,
+   distribute, sublicense, and/or sell copies of the Software, and to
+   permit persons to whom the Software is furnished to do so, subject to
+   the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+   LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+   WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+###
+
+(($) ->
+
+    Mirror =
+        $mirror: null
+        css: ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom",'fontFamily', 'borderStyle', 'borderWidth','wordWrap', 'fontSize', 'lineHeight', 'overflowX']
+        init: ($origin) ->
+            $mirror = $('<div></div>')
+            css =
+                position: 'absolute'
+                left: -9999
+                top:0
+                zIndex: -20000
+                'white-space': 'pre-wrap'
+            $.each @.css, (i,p) ->
+                css[p] = $origin.css p
+            $mirror.css(css)
+            @.$mirror = $mirror
+            $origin.after($mirror)
+            this
+        setContent: (html) ->
+            @.$mirror.html(html)
+            this
+        getFlagRect: () ->
+            $flag = @.$mirror.find "span#flag"
+            pos = $flag.position()
+            rect = {left:pos.left, top:pos.top, bottom:$flag.height() + pos.top}
+            @.$mirror.remove()
+            rect
+
+    At = (inputor) ->
+        $inputor = @.$inputor = $(inputor)
+        @options = {}
+        @query =
+            text:""
+            start:0
+            stop:0
+        @_cache = {}
+        @pos = 0
+        @flags = {}
+        @theflag = null
+        @search_word = {}
+
+        @view = AtView
+        @mirror = Mirror
+
+        $inputor
+            .on "keyup.inputor", (e) =>
+                stop = e.keyCode is 40 or e.keyCode is 38
+                lookup = not (stop and @.view.isShowing())
+                @.lookup() if lookup
+        @.init()
+        log "At.new", $inputor[0]
+        return this
+
+    At:: =
+        constructor: At
+
+        init: ->
+            @.$inputor
+                .on 'keyup.inputor', (e) =>
+                    @.onkeyup(e)
+                .on 'keydown.inputor', (e) =>
+                    @.onkeydown(e)
+                .on 'scroll.inputor', (e) =>
+                    @.view.hide()
+                .on 'blur.inputor', (e) =>
+                    @.view.hide(1000)
+            log "At.init", @.$inputor[0]
+
+        reg: (flag, options) ->
+            opt = {}
+            if $.isFunction options
+                opt['callback'] = options
+            else
+                opt = options
+            _default = @.options[flag] or= $.fn.atWho.default
+            @.options[flag] = $.extend {}, _default, opt
+            log "At.reg", @.$inputor[0],flag, options
+
+        dataValue: ->
+            search_word = @.search_word[@.theflag]
+            return search_word if search_word
+            match = /data-value=["']?\$\{(\w+)\}/g.exec(this.getOpt('tpl'))
+            return @.search_word[@.theflag] =  if !_isNil(match) then match[1] else null
+
+        getOpt: (key) ->
+            try
+                return @.options[@.theflag][key]
+            catch error
+                return null
+
+        rect: ->
+            $inputor = @.$inputor
+            if document.selection # for IE full
+                Sel = document.selection.createRange()
+                x = Sel.boundingLeft + $inputor.scrollLeft()
+                y = Sel.boundingTop + $(window).scrollTop() + $inputor.scrollTop()
+                bottom = y + Sel.boundingHeight
+                # -2 : for some font style problem.
+                return {top:y-2, left:x-2, bottom:bottom-2}
+
+            format = (value) ->
+                value.replace(/</g, '&lt')
+                    .replace(/>/g, '&gt')
+                    .replace(/`/g,'&#96')
+                    .replace(/"/g,'&quot')
+                    .replace(/\r\n|\r|\n/g,"<br />")
+
+            ### 克隆完inputor后将原来的文本内容根据
+              @的位置进行分块,以获取@块在inputor(输入框)里的position
+            ###
+            start_range = $inputor.val().slice(0,@pos - 1)
+            html = "<span>"+format(start_range)+"</span>"
+            html += "<span id='flag'>@</span>"
+
+            ###
+              将inputor的 offset(相对于document)
+              和@在inputor里的position相加
+              就得到了@相对于document的offset.
+              当然,还要加上行高和滚动条的偏移量.
+            ###
+            offset = $inputor.offset()
+            at_rect = @mirror.init($inputor).setContent(html).getFlagRect()
+
+            x = offset.left + at_rect.left - $inputor.scrollLeft()
+            y = offset.top - $inputor.scrollTop()
+            bottom = y + at_rect.bottom
+            y += at_rect.top
+
+            # bottom + 2: for some font style problem
+            return {top:y,left:x,bottom:bottom + 2}
+
+        cache: (value) ->
+            key = @.query.text
+            return null if not @.getOpt("cache") or not key
+            return @._cache[key] or= value
+
+        getKeyname: ->
+            $inputor = @.$inputor
+            text = $inputor.val()
+
+            ##获得inputor中插入符的position.
+            caret_pos = $inputor.caretPos()
+
+            ### 向在插入符前的的文本进行正则匹配
+             * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个###
+            subtext = text.slice(0,caret_pos)
+
+            matched = null
+            $.each this.options, (flag) =>
+                regexp = new RegExp flag+'([A-Za-z0-9_\+\-]*)$|'+flag+'([^\\x00-\\xff]*)$','gi'
+                match = regexp.exec subtext
+                if not _isNil(match)
+                    matched = if match[2] then match[2] else match[1]
+                    @.theflag = flag
+                    return no
+
+            if typeof matched is 'string' and matched.length <= 20
+                start = caret_pos - matched.length
+                end = start + matched.length
+                @.pos = start
+                key = {'text':matched.toLowerCase(), 'start':start, 'end':end}
+            else
+                @.view.hide()
+
+            log "At.getKeyname", key
+            @.query = key
+
+        replaceStr: (str) ->
+            $inputor = @.$inputor
+            key = @.query
+            source = $inputor.val()
+            flag_len = if @.getOpt("display_flag") then 0 else @theflag.length
+            start_str = source.slice 0, key.start - flag_len
+            text = start_str + str + source.slice key.end
+
+            $inputor.val text
+            $inputor.caretPos start_str.length + str.length
+            $inputor.change()
+            log "At.replaceStr", text
+
+        onkeyup: (e) ->
+            view = @.view
+            return unless view.isShowing()
+            switch e.keyCode
+                # ESC
+                when 27
+                    e.preventDefault()
+                    view.hide()
+                else
+                    $.noop()
+            e.stopPropagation()
+
+        onkeydown: (e) ->
+            view = @.view
+            return if not view.isShowing()
+            switch e.keyCode
+                # ESC
+                when 27
+                    e.preventDefault()
+                    view.hide()
+                # UP
+                when 38
+                    e.preventDefault()
+                    view.prev()
+                # DOWN
+                when 40
+                    e.preventDefault()
+                    view.next()
+                # TAB or ENTER
+                when 9, 13
+                    return if not view.isShowing()
+                    e.preventDefault()
+                    view.choose()
+                else
+                    $.noop()
+            e.stopPropagation()
+
+        renderView: (datas) ->
+            log "At.renderView", @, datas
+
+            datas = datas.splice(0, @.getOpt('limit'))
+            datas = _unique(datas, @.dataValue())
+            datas = _objectify(datas)
+            datas = _sorter.call(@,datas)
+
+            this.view.render this, datas
+
+        lookup: ->
+            key = this.getKeyname()
+            return if not key
+            log "At.lookup.key", key
+
+            if not _isNil(datas = @.cache())
+                @.renderView datas
+            else if not _isNil(datas = @.lookupWithData key)
+                @.renderView datas
+            else if $.isFunction(callback = @.getOpt 'callback')
+                callback key.text, $.proxy(@.renderView,@)
+            else
+                @.view.hide()
+            $.noop()
+
+        lookupWithData: (key) ->
+            data = @.getOpt "data"
+            if $.isArray(data) and data.length != 0
+                items = $.map data, (item,i) =>
+                    try
+                        name = if $.isPlainObject item then item[@.dataValue()] else item
+                        regexp = new RegExp(key.text.replace("+","\\+"),'i')
+                        match = name.match(regexp)
+                    catch e
+                        return null
+
+                    return if match then item else null
+            items
+
+    AtView =
+        timeout_id: null
+        id: '#at-view'
+        holder: null
+        _jqo: null
+        jqo: ->
+            jqo = @._jqo
+            jqo = if _isNil jqo then (@._jqo = $(@.id)) else jqo
+
+        init: ->
+            return if not _isNil @.jqo()
+            tpl = "<div id='"+this.id.slice(1)+"' class='at-view'><ul id='"+this.id.slice(1)+"-ul'></ul></div>"
+            $("body").append(tpl)
+
+            $menu = @.jqo().find('ul')
+            $menu.on 'mouseenter.view','li', (e) ->
+                    $menu.find('.cur').removeClass 'cur'
+                    $(e.currentTarget).addClass 'cur'
+                .on 'click', (e) =>
+                    e.stopPropagation()
+                    e.preventDefault()
+                    @.choose()
+
+
+        isShowing: () ->
+            @.jqo().is(":visible")
+
+        choose: () ->
+            $li = @.jqo().find ".cur"
+            str = if _isNil($li) then @.holder.query.text+" " else $li.attr(@.holder.getOpt("choose")) + " "
+            @.holder.replaceStr(str)
+            @.hide()
+
+        rePosition: () ->
+            rect = @.holder.rect()
+            if rect.bottom + @.jqo().height() - $(window).scrollTop() > $(window).height()
+                rect.bottom = rect.top - @.jqo().height()
+            log "AtView.rePosition",{left:rect.left, top:rect.bottom}
+            @.jqo().offset {left:rect.left, top:rect.bottom}
+
+        next: () ->
+            cur = @.jqo().find('.cur').removeClass('cur')
+            next = cur.next()
+            next = $(@.jqo().find('li')[0]) if not next.length
+            next.addClass 'cur'
+
+        prev: () ->
+            cur = @.jqo().find('.cur').removeClass('cur')
+            prev = cur.prev()
+            prev = @.jqo().find('li').last() if not prev.length
+            prev.addClass('cur')
+
+        show: () ->
+            @.jqo().show() if not @.isShowing()
+            @.rePosition()
+
+        hide: (time) ->
+            if isNaN time
+                @.jqo().hide() if @.isShowing()
+            else
+                callback = => @.hide()
+                clearTimeout @.timeout_id
+                @.timeout_id = setTimeout callback, 300
+
+        clear: (clear_all) ->
+            @._cache = {} if clear_all is yes
+            @.jqo().find('ul').empty()
+
+        render: (holder, list) ->
+            return no if not $.isArray(list)
+            if list.length <= 0
+                @.hide()
+                return yes
+
+            @.holder = holder
+            holder.cache(list)
+            @.clear()
+
+            $ul = @.jqo().find('ul')
+            tpl = holder.getOpt('tpl')
+
+            $.each list, (i, item) ->
+                tpl or= _DEFAULT_TPL
+                li = _evalTpl tpl, item
+                log "AtView.render", li
+                $ul.append _highlighter li,holder.query.text
+
+            @.show()
+            $ul.find("li:eq(0)").addClass "cur"
+
+
+    _objectify = (list) ->
+        $.map list, (item,k) ->
+            if not $.isPlainObject item
+                item = {id:k, name:item}
+            return item
+
+    _evalTpl = (tpl, map) ->
+        try
+            el = tpl.replace /\$\{([^\}]*)\}/g, (tag,key,pos) ->
+                map[key]
+        catch error
+            ""
+
+    _highlighter = (li,query) ->
+        return li if _isNil(query)
+        li.replace new RegExp(">\\s*(\\w*)(" + query.replace("+","\\+") + ")(\\w*)\\s*<", 'ig'), (str,$1, $2, $3) ->
+            '> '+$1+'<strong>' + $2 + '</strong>'+$3+' <'
+
+    _sorter = (items) ->
+        data_value = @.dataValue()
+        query = @.query.text
+        results = []
+
+        for item in items
+            text = item[data_value]
+            continue if text.toLowerCase().indexOf(query) is -1
+            item.order = text.toLowerCase().indexOf query
+            results.push(item)
+
+        results.sort (a,b) ->
+            a.order - b.order
+        return results
+
+
+    ###
+      maybe we can use $._unique.
+      But i don't know it will delete li element frequently or not.
+      I think we should not change DOM element frequently.
+      more, It seems batter not to call evalTpl function too much times.
+    ###
+    _unique = (list,query) ->
+        record = []
+        $.map list, (v, id) ->
+            value = if $.isPlainObject(v) then v[query] else v
+            if $.inArray(value,record) < 0
+                record.push value
+                return v
+
+    _isNil = (target) ->
+        not target \
+        or ($.isPlainObject(target) and $.isEmptyObject(target)) \
+        or ($.isArray(target) and target.length is 0) \
+        or (target instanceof $ and target.length is 0) \
+        or target is undefined
+
+    _DEFAULT_TPL = "<li id='${id}' data-value='${name}'>${name}</li>"
+
+    log = () ->
+        #console.log(arguments)
+
+    $.fn.atWho = (flag, options) ->
+        AtView.init()
+        @.filter('textarea, input').each () ->
+            $this = $(this)
+            data = $this.data "AtWho"
+
+            $this.data 'AtWho', (data = new At(this)) if not data
+            data.reg flag, options
+
+    $.fn.atWho.default =
+        data: []
+        # Parameter: choose
+        ## specify the attribute on customer tpl,
+        ## so that we could append different value to the input other than the value we searched in
+        choose: "data-value"
+        callback: null
+        cache: yes
+        limit: 5
+        display_flag: yes
+        tpl: _DEFAULT_TPL
+
+)(window.jQuery)

static/At.js/coffee/jquery.caret.coffee

+###
+   Implement Twitter/Weibo @ mentions
+
+   Copyright (c) 2012 chord.luo@gmail.com
+
+   Permission is hereby granted, free of charge, to any person obtaining
+   a copy of this software and associated documentation files (the
+   "Software"), to deal in the Software without restriction, including
+   without limitation the rights to use, copy, modify, merge, publish,
+   distribute, sublicense, and/or sell copies of the Software, and to
+   permit persons to whom the Software is furnished to do so, subject to
+   the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+   LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+   WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 
+###
+
+###
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+###
+(($) ->
+    getCaretPos = (inputor) ->
+        if document.selection #IE
+            # reference: http://tinyurl.com/86pyc4s
+
+            ###
+            #assume we select "HATE" in the inputor such as textarea -> { }.
+             *               start end-point.
+             *              /
+             * <  I really [HATE] IE   > between the brackets is the selection range.
+             *                   \
+             *                    end end-point.
+             ###
+
+            range = document.selection.createRange()
+            pos = 0
+            # selection should in the inputor.
+            if range and range.parentElement() is inputor
+                normalizedValue = inputor.value.replace /\r\n/g, "\n"
+                ### SOMETIME !!! 
+                 "/r/n" is counted as two char.
+                  one line is two, two will be four. balalala.
+                  so we have to using the normalized one's length.;
+                ###
+                len = normalizedValue.length
+                ###
+                   <[  I really HATE IE   ]>:
+                    the whole content in the inputor will be the textInputRange.
+                ###
+                textInputRange = inputor.createTextRange()
+                ###                 _here must be the position of bookmark.
+                                 /
+                   <[  I really [HATE] IE   ]>
+                    [---------->[           ] : this is what moveToBookmark do.
+                   <   I really [[HATE] IE   ]> : here is result.
+                                  \ two brackets in should be in line.
+                ###
+                textInputRange.moveToBookmark range.getBookmark()
+                endRange = inputor.createTextRange()
+                ###  [--------------------->[] : if set false all end-point goto end.
+                  <  I really [[HATE] IE  []]>
+                ###
+                endRange.collapse false
+                ###
+                                ___VS____
+                               /         \
+                 <   I really [[HATE] IE []]>
+                                          \_endRange end-point.
+                
+                " > -1" mean the start end-point will be the same or right to the end end-point
+               * simplelly, all in the end.
+                ####
+                if textInputRange.compareEndPoints("StartToEnd", endRange) > -1
+                    #TextRange object will miss "\r\n". So, we count it ourself.
+                    start = end = len
+                else
+                    ###
+                            I really |HATE] IE   ]> 
+                                   <-|
+                          I really[ [HATE] IE   ]>
+                                <-[
+                        I reall[y  [HATE] IE   ]>
+                     
+                      will return how many unit have moved.
+                    ###
+                    start = -textInputRange.moveStart "character", -len
+                    end = -textInputRange.moveEnd "character", -len
+
+        else
+            start = inputor.selectionStart
+        return start
+
+    setCaretPos = (inputor, pos) ->
+        if document.selection #IE
+            range = inputor.createTextRange()
+            range.move "character", pos
+            range.select()
+        else
+            inputor.setSelectionRange pos, pos
+
+    $.fn.caretPos = (pos) ->
+        inputor = this[0]
+        inputor.focus()
+        if pos
+            setCaretPos(inputor, pos)
+        else
+            getCaretPos(inputor)
+
+)(window.jQuery)

static/At.js/css/jquery.atwho.css

+#at-view {
+    position:absolute;
+    top: 0;
+    left: 0;
+    display: none;
+    margin-top: 18px;
+    background: white;
+    border: 1px solid #DDD;
+    border-radius: 3px;
+    box-shadow: 0 0 5px rgba(0,0,0,0.1);
+    min-width: 120px;
+}
+
+#at-view .cur {
+    background: #3366FF;
+    color: white;
+}
+#at-view .cur small {
+    color: white;
+}
+#at-view strong {
+    color: #3366FF;
+}
+#at-view .cur strong {
+    color: white;
+    font:bold;
+}
+#at-view ul {
+    /* width: 100px; */
+    list-style:none;
+    padding:0;
+    margin:auto;
+}
+#at-view ul li {
+    display: block;
+    padding: 5px 10px;
+    border-bottom: 1px solid #DDD;
+    cursor: pointer;
+    /* border-top: 1px solid #C8C8C8; */
+}
+#at-view small {
+    font-size: smaller;
+    color: #777;
+    font-weight: normal;
+}
+
+

static/At.js/data.json

+[
+    "six_pointed_star", "ski", "skull", "sleepy", "slot_machine", "smile",
+    "smiley", "smirk", "smoking", "snake", "snowman", "sob", "soccer",
+    "space_invader", "spades", "spaghetti", "sparkler", "sparkles",
+    "speaker", "speedboat", "squirrel", "star", "star2", "stars", "station",
+    "statue_of_liberty", "stew", "strawberry", "sunflower", "sunny",
+    "sunrise", "sunrise_over_mountains", "surfer", "sushi", "suspect",
+    "sweat", "sweat_drops", "swimmer", "syringe", "tada", "tangerine",
+    "taurus", "taxi", "tea", "telephone", "tennis", "tent", "thumbsdown",
+    "thumbsup", "ticket", "tiger", "tm", "toilet", "tokyo_tower", "tomato",
+    "tongue", "top", "tophat", "traffic_light", "train", "trident",
+    "trollface", "trophy", "tropical_fish", "truck", "trumpet", "tshirt",
+    "tulip", "tv", "u5272", "u55b6", "u6307", "u6708", "u6709", "u6e80",
+    "-1", "0", "1", "109", "2", "3", "4", "5", "6", "7", "8", "8ball", "9",
+    "a", "ab", "airplane", "alien", "ambulance", "angel", "anger", "angry",
+    "apple", "aquarius", "aries", "arrow_backward", "arrow_down",
+    "arrow_forward", "arrow_left", "arrow_lower_left", "arrow_lower_right",
+    "arrow_right", "arrow_up", "arrow_upper_left", "arrow_upper_right",
+    "art", "astonished", "atm", "b", "baby", "baby_chick", "baby_symbol",
+    "balloon", "bamboo", "bank", "barber", "baseball", "basketball", "bath",
+    "bear", "beer", "beers", "beginner", "bell", "bento", "bike", "bikini",
+    "bird", "birthday", "black_square", "blue_car", "blue_heart", "blush",
+    "boar", "boat", "bomb", "book", "boot", "bouquet", "bow", "bowtie",
+    "boy", "bread", "briefcase", "broken_heart", "bug", "bulb",
+    "bullettrain_front", "bullettrain_side", "bus", "busstop", "cactus",
+    "cake", "calling", "camel", "camera", "cancer", "capricorn", "car",
+    "cat", "cd", "chart", "checkered_flag", "cherry_blossom", "chicken",
+    "christmas_tree", "church", "cinema", "city_sunrise", "city_sunset",
+    "clap", "clapper", "clock1", "clock10", "clock11", "clock12", "clock2",
+    "clock3", "clock4", "clock5", "clock6", "clock7", "clock8", "clock9",
+    "closed_umbrella", "cloud", "clubs", "cn", "cocktail", "coffee",
+    "cold_sweat", "computer", "confounded", "congratulations",
+    "construction", "construction_worker", "convenience_store", "cool",
+    "cop", "copyright", "couple", "couple_with_heart", "couplekiss", "cow",
+    "crossed_flags", "crown", "cry", "cupid", "currency_exchange", "curry",
+    "cyclone", "dancer", "dancers", "dango", "dart", "dash", "de",
+    "department_store", "diamonds", "disappointed", "dog", "dolls",
+    "dolphin", "dress", "dvd", "ear", "ear_of_rice", "egg", "eggplant",
+    "egplant", "eight_pointed_black_star", "eight_spoked_asterisk",
+    "elephant", "email", "es", "european_castle", "exclamation", "eyes",
+    "factory", "fallen_leaf", "fast_forward", "fax", "fearful", "feelsgood",
+    "feet", "ferris_wheel", "finnadie", "fire", "fire_engine", "fireworks",
+    "fish", "fist", "flags", "flushed", "football", "fork_and_knife",
+    "fountain", "four_leaf_clover", "fr", "fries", "frog", "fuelpump", "gb",
+    "gem", "gemini", "ghost", "gift", "gift_heart", "girl", "goberserk",
+    "godmode", "golf", "green_heart", "grey_exclamation", "grey_question",
+    "grin", "guardsman", "guitar", "gun", "haircut", "hamburger", "hammer",
+    "hamster", "hand", "handbag", "hankey", "hash", "headphones", "heart",
+    "heart_decoration", "heart_eyes", "heartbeat", "heartpulse", "hearts",
+    "hibiscus", "high_heel", "horse", "hospital", "hotel", "hotsprings",
+    "house", "hurtrealbad", "icecream", "id", "ideograph_advantage", "imp",
+    "information_desk_person", "iphone", "it", "jack_o_lantern",
+    "japanese_castle", "joy", "jp", "key", "kimono", "kiss", "kissing_face",
+    "kissing_heart", "koala", "koko", "kr", "leaves", "leo", "libra", "lips",
+    "lipstick", "lock", "loop", "loudspeaker", "love_hotel", "mag",
+    "mahjong", "mailbox", "man", "man_with_gua_pi_mao", "man_with_turban",
+    "maple_leaf", "mask", "massage", "mega", "memo", "mens", "metal",
+    "metro", "microphone", "minidisc", "mobile_phone_off", "moneybag",
+    "monkey", "monkey_face", "moon", "mortar_board", "mount_fuji", "mouse",
+    "movie_camera", "muscle", "musical_note", "nail_care", "necktie", "new",
+    "no_good", "no_smoking", "nose", "notes", "o", "o2", "ocean", "octocat",
+    "octopus", "oden", "office", "ok", "ok_hand", "ok_woman", "older_man",
+    "older_woman", "open_hands", "ophiuchus", "palm_tree", "parking",
+    "part_alternation_mark", "pencil", "penguin", "pensive", "persevere",
+    "person_with_blond_hair", "phone", "pig", "pill", "pisces", "plus1",
+    "point_down", "point_left", "point_right", "point_up", "point_up_2",
+    "police_car", "poop", "post_office", "postbox", "pray", "princess",
+    "punch", "purple_heart", "question", "rabbit", "racehorse", "radio",
+    "rage", "rage1", "rage2", "rage3", "rage4", "rainbow", "raised_hands",
+    "ramen", "red_car", "red_circle", "registered", "relaxed", "relieved",
+    "restroom", "rewind", "ribbon", "rice", "rice_ball", "rice_cracker",
+    "rice_scene", "ring", "rocket", "roller_coaster", "rose", "ru", "runner",
+    "sa", "sagittarius", "sailboat", "sake", "sandal", "santa", "satellite",
+    "satisfied", "saxophone", "school", "school_satchel", "scissors",
+    "scorpius", "scream", "seat", "secret", "shaved_ice", "sheep", "shell",
+    "ship", "shipit", "shirt", "shit", "shoe", "signal_strength",
+    "u7121", "u7533", "u7a7a", "umbrella", "unamused", "underage", "unlock",
+    "up", "us", "v", "vhs", "vibration_mode", "virgo", "vs", "walking",
+    "warning", "watermelon", "wave", "wc", "wedding", "whale", "wheelchair",
+    "white_square", "wind_chime", "wink", "wink2", "wolf", "woman",
+    "womans_hat", "womens", "x", "yellow_heart", "zap", "zzz", "+1"
+]

static/At.js/example.html

+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
+<head>
+	<meta http-equiv="Content-Type" content="text/html;charset=UTF-8" />
+	<title></title>
+        <link rel="stylesheet" type="text/css" href="css/jquery.atwho.css"/>
+        <script type="text/javascript" src="https://ajax.googleapis.com/ajax/libs/jquery/1.7.1/jquery.min.js"></script>
+        <script type="text/javascript" src="js/jquery.caret.js" ></script>
+        <script type="text/javascript" src="js/jquery.atwho.js"></script>
+        <script type="text/javascript">
+        $(function(){
+                var data = ["Jacob","Isabella","Ethan","Emma","Michael","Olivia","Alexander","Sophia","William","Ava","Joshua","Emily","Daniel","Madison","Jayden","lepture","Abigail","Noah","Chloe","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","你好","你你你",
+"Jacob","Isabella","Ethan","reorx","Emma","Michael","Olivia","Alexander","Sophia","William","Ava","Joshua","Emily","Daniel","Madison","Jayden","Abigail","Noah","Chloe","aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","你好","你你你"
+                ];
+                data = $.map(data,function(value,i) {
+                    return {'id':i,'name':value,'email':value+"@email.com"};
+                    });
+
+                $("textarea#inputor").atWho("@",{
+                    tpl: "<li id='${id}' data-value='${name}'>${name} <small>${email}</small></li>",
+                    'data':data
+                    })
+                .atWho(":",{
+                    tpl:"<li data-value='${key}'>${name} <img src='http://a248.e.akamai.net/assets.github.com/images/icons/emoji/${name}.png'  height='20' width='20' /></li>"
+                    ,callback:function(query,callback) {
+                        $.ajax({
+                            url:'data.json'
+                            ,type:'GET'
+                            ,success:function(data) {
+                                datas = $.map(data,function(value,i){
+                                    return {'id':i,'key':value+":",'name':value}
+                                    })
+                                callback(datas)
+                            }
+                        })
+                    }
+                    }); 
+                
+                $("button#reflash").click(function(){
+                        $("textarea#inputor").atWho("@",{"data":data.splice(0,3)}) 
+                        })
+
+                emojis = [
+                    "six_pointed_star", "ski", "skull", "sleepy", "slot_machine", "smile",
+                    "smiley", "smirk", "smoking", "snake", "snowman", "sob", "soccer",
+                    "space_invader", "spades", "spaghetti", "sparkler", "sparkles",
+                    "speaker", "speedboat", "squirrel", "star", "star2", "stars", "station",
+                    "statue_of_liberty", "stew", "strawberry", "sunflower", "sunny",
+                    "sunrise", "sunrise_over_mountains", "surfer", "sushi", "suspect",
+                    "sweat", "sweat_drops", "swimmer", "syringe", "tada", "tangerine",
+                    "taurus", "taxi", "tea", "telephone", "tennis", "tent", "thumbsdown", "+1","-1"]
+                emojis = $.map(emojis,function(value,i){
+                    return {'id':i,'key':":"+value+":",'name':value}
+                    })
+                $("textarea#inputor2").atWho("@",{
+                    tpl: "<li id='${id}' data-value='${name}' data-choose='${email}'>${name} <small>${email}</small></li>",
+                    'data':data,
+                    choose:'data-choose'
+                    })
+                .atWho("/:",{
+                    tpl:"<li data-value='${key}'>${name} <img src='http://a248.e.akamai.net/assets.github.com/images/icons/emoji/${name}.png'  height='20' width='20' /></li>"
+                    ,'data':emojis
+                    ,display_flag: false
+                    })
+
+                $('input').atWho("@",{'data':data})
+
+            });
+        </script>
+        <style type="text/css" media="screen">
+            body {
+                font: 14px/1.6 "Lucida Grande", "Helvetica", sans-serif;
+            }
+            .box {
+                background:gray;
+                height:100px;
+                width:100px;
+                margin:10px;
+            }
+            textarea {
+                width: 300px;
+            }
+            #inputor{
+                width:480px;
+                font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
+            }
+        </style>
+</head>
+<body>
+    <div class="box"></div>
+    <div class="box"></div>
+    <button id="reflash">reflash</button>
+    <textarea id="inputor" name="at" rows="8" cols="40">
+        this textarea register "@" with static data 
+        and ":" with ajax.
+        type "@" to try
+    </textarea>
+    <textarea id="inputor2" name="Name" rows="8" cols="40">
+        this register "/:" with static data
+    </textarea>
+<br/>
+<input></input>
+</body>
+</html>

static/At.js/jquery.atwho.css

+#at-view {
+    position:absolute;
+    top: 0;
+    left: 0;
+    display: none;
+    margin-top: 18px;
+    background: white;
+    border: 1px solid #DDD;
+    border-radius: 3px;
+    box-shadow: 0 0 5px rgba(0,0,0,0.1);
+    min-width: 120px;
+}
+
+#at-view .cur {
+    background: #3366FF;
+    color: white;
+}
+#at-view .cur small {
+    color: white;
+}
+#at-view strong {
+    color: #3366FF;
+}
+#at-view .cur strong {
+    color: white;
+    font:bold;
+}
+#at-view ul {
+    /* width: 100px; */
+    list-style:none;
+    padding:0;
+    margin:auto;
+}
+#at-view ul li {
+    display: block;
+    padding: 5px 10px;
+    border-bottom: 1px solid #DDD;
+    cursor: pointer;
+    /* border-top: 1px solid #C8C8C8; */
+}
+#at-view small {
+    font-size: smaller;
+    color: #777;
+    font-weight: normal;
+}
+
+

static/At.js/jquery.atwho.js

+// Generated by CoffeeScript 1.3.3
+
+/*
+   Implement Twitter/Weibo @ mentions
+
+   Copyright (c) 2012 chord.luo@gmail.com
+
+   Permission is hereby granted, free of charge, to any person obtaining
+   a copy of this software and associated documentation files (the
+   "Software"), to deal in the Software without restriction, including
+   without limitation the rights to use, copy, modify, merge, publish,
+   distribute, sublicense, and/or sell copies of the Software, and to
+   permit persons to whom the Software is furnished to do so, subject to
+   the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+   LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+   WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+
+(function() {
+
+  (function($) {
+    var At, AtView, Mirror, log, _DEFAULT_TPL, _evalTpl, _highlighter, _isNil, _objectify, _sorter, _unique;
+    Mirror = {
+      $mirror: null,
+      css: ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom", 'fontFamily', 'borderStyle', 'borderWidth', 'wordWrap', 'fontSize', 'lineHeight', 'overflowX'],
+      init: function($origin) {
+        var $mirror, css;
+        $mirror = $('<div></div>');
+        css = {
+          position: 'absolute',
+          left: -9999,
+          top: 0,
+          zIndex: -20000,
+          'white-space': 'pre-wrap'
+        };
+        $.each(this.css, function(i, p) {
+          return css[p] = $origin.css(p);
+        });
+        $mirror.css(css);
+        this.$mirror = $mirror;
+        $origin.after($mirror);
+        return this;
+      },
+      setContent: function(html) {
+        this.$mirror.html(html);
+        return this;
+      },
+      getFlagRect: function() {
+        var $flag, pos, rect;
+        $flag = this.$mirror.find("span#flag");
+        pos = $flag.position();
+        rect = {
+          left: pos.left,
+          top: pos.top,
+          bottom: $flag.height() + pos.top
+        };
+        this.$mirror.remove();
+        return rect;
+      }
+    };
+    At = function(inputor) {
+      var $inputor,
+        _this = this;
+      $inputor = this.$inputor = $(inputor);
+      this.options = {};
+      this.query = {
+        text: "",
+        start: 0,
+        stop: 0
+      };
+      this._cache = {};
+      this.pos = 0;
+      this.flags = {};
+      this.theflag = null;
+      this.search_word = {};
+      this.view = AtView;
+      this.mirror = Mirror;
+      $inputor.on("keyup.inputor", function(e) {
+        var lookup, stop;
+        stop = e.keyCode === 40 || e.keyCode === 38;
+        lookup = !(stop && _this.view.isShowing());
+        if (lookup) {
+          return _this.lookup();
+        }
+      });
+      this.init();
+      log("At.new", $inputor[0]);
+      return this;
+    };
+    At.prototype = {
+      constructor: At,
+      init: function() {
+        var _this = this;
+        this.$inputor.on('keyup.inputor', function(e) {
+          return _this.onkeyup(e);
+        }).on('keydown.inputor', function(e) {
+          return _this.onkeydown(e);
+        }).on('scroll.inputor', function(e) {
+          return _this.view.hide();
+        }).on('blur.inputor', function(e) {
+          return _this.view.hide(1000);
+        });
+        return log("At.init", this.$inputor[0]);
+      },
+      reg: function(flag, options) {
+        var opt, _base, _default;
+        opt = {};
+        if ($.isFunction(options)) {
+          opt['callback'] = options;
+        } else {
+          opt = options;
+        }
+        _default = (_base = this.options)[flag] || (_base[flag] = $.fn.atWho["default"]);
+        this.options[flag] = $.extend({}, _default, opt);
+        return log("At.reg", this.$inputor[0], flag, options);
+      },
+      dataValue: function() {
+        var match, search_word;
+        search_word = this.search_word[this.theflag];
+        if (search_word) {
+          return search_word;
+        }
+        match = /data-value=["']?\$\{(\w+)\}/g.exec(this.getOpt('tpl'));
+        return this.search_word[this.theflag] = !_isNil(match) ? match[1] : null;
+      },
+      getOpt: function(key) {
+        try {
+          return this.options[this.theflag][key];
+        } catch (error) {
+          return null;
+        }
+      },
+      rect: function() {
+        var $inputor, Sel, at_rect, bottom, format, html, offset, start_range, x, y;
+        $inputor = this.$inputor;
+        if (document.selection) {
+          Sel = document.selection.createRange();
+          x = Sel.boundingLeft + $inputor.scrollLeft();
+          y = Sel.boundingTop + $(window).scrollTop() + $inputor.scrollTop();
+          bottom = y + Sel.boundingHeight;
+          return {
+            top: y - 2,
+            left: x - 2,
+            bottom: bottom - 2
+          };
+        }
+        format = function(value) {
+          return value.replace(/</g, '&lt').replace(/>/g, '&gt').replace(/`/g, '&#96').replace(/"/g, '&quot').replace(/\r\n|\r|\n/g, "<br />");
+        };
+        /* 克隆完inputor后将原来的文本内容根据
+          @的位置进行分块,以获取@块在inputor(输入框)里的position
+        */
+
+        start_range = $inputor.val().slice(0, this.pos - 1);
+        html = "<span>" + format(start_range) + "</span>";
+        html += "<span id='flag'>@</span>";
+        /*
+                      将inputor的 offset(相对于document)
+                      和@在inputor里的position相加
+                      就得到了@相对于document的offset.
+                      当然,还要加上行高和滚动条的偏移量.
+        */
+
+        offset = $inputor.offset();
+        at_rect = this.mirror.init($inputor).setContent(html).getFlagRect();
+        x = offset.left + at_rect.left - $inputor.scrollLeft();
+        y = offset.top - $inputor.scrollTop();
+        bottom = y + at_rect.bottom;
+        y += at_rect.top;
+        return {
+          top: y,
+          left: x,
+          bottom: bottom + 2
+        };
+      },
+      cache: function(value) {
+        var key, _base;
+        key = this.query.text;
+        if (!this.getOpt("cache") || !key) {
+          return null;
+        }
+        return (_base = this._cache)[key] || (_base[key] = value);
+      },
+      getKeyname: function() {
+        var $inputor, caret_pos, end, key, matched, start, subtext, text,
+          _this = this;
+        $inputor = this.$inputor;
+        text = $inputor.val();
+        caret_pos = $inputor.caretPos();
+        /* 向在插入符前的的文本进行正则匹配
+         * 考虑会有多个 @ 的存在, 匹配离插入符最近的一个
+        */
+
+        subtext = text.slice(0, caret_pos);
+        matched = null;
+        $.each(this.options, function(flag) {
+          var match, regexp;
+          regexp = new RegExp(flag + '([A-Za-z0-9_\+\-]*)$|' + flag + '([^\\x00-\\xff]*)$', 'gi');
+          match = regexp.exec(subtext);
+          if (!_isNil(match)) {
+            matched = match[2] ? match[2] : match[1];
+            _this.theflag = flag;
+            return false;
+          }
+        });
+        if (typeof matched === 'string' && matched.length <= 20) {
+          start = caret_pos - matched.length;
+          end = start + matched.length;
+          this.pos = start;
+          key = {
+            'text': matched.toLowerCase(),
+            'start': start,
+            'end': end
+          };
+        } else {
+          this.view.hide();
+        }
+        log("At.getKeyname", key);
+        return this.query = key;
+      },
+      replaceStr: function(str) {
+        var $inputor, flag_len, key, source, start_str, text;
+        $inputor = this.$inputor;
+        key = this.query;
+        source = $inputor.val();
+        flag_len = this.getOpt("display_flag") ? 0 : this.theflag.length;
+        start_str = source.slice(0, key.start - flag_len);
+        text = start_str + str + source.slice(key.end);
+        $inputor.val(text);
+        $inputor.caretPos(start_str.length + str.length);
+        $inputor.change();
+        return log("At.replaceStr", text);
+      },
+      onkeyup: function(e) {
+        var view;
+        view = this.view;
+        if (!view.isShowing()) {
+          return;
+        }
+        switch (e.keyCode) {
+          case 27:
+            e.preventDefault();
+            view.hide();
+            break;
+          default:
+            $.noop();
+        }
+        return e.stopPropagation();
+      },
+      onkeydown: function(e) {
+        var view;
+        view = this.view;
+        if (!view.isShowing()) {
+          return;
+        }
+        switch (e.keyCode) {
+          case 27:
+            e.preventDefault();
+            view.hide();
+            break;
+          case 38:
+            e.preventDefault();
+            view.prev();
+            break;
+          case 40:
+            e.preventDefault();
+            view.next();
+            break;
+          case 9:
+          case 13:
+            if (!view.isShowing()) {
+              return;
+            }
+            e.preventDefault();
+            view.choose();
+            break;
+          default:
+            $.noop();
+        }
+        return e.stopPropagation();
+      },
+      renderView: function(datas) {
+        log("At.renderView", this, datas);
+        datas = datas.splice(0, this.getOpt('limit'));
+        datas = _unique(datas, this.dataValue());
+        datas = _objectify(datas);
+        datas = _sorter.call(this, datas);
+        return this.view.render(this, datas);
+      },
+      lookup: function() {
+        var callback, datas, key;
+        key = this.getKeyname();
+        if (!key) {
+          return;
+        }
+        log("At.lookup.key", key);
+        if (!_isNil(datas = this.cache())) {
+          this.renderView(datas);
+        } else if (!_isNil(datas = this.lookupWithData(key))) {
+          this.renderView(datas);
+        } else if ($.isFunction(callback = this.getOpt('callback'))) {
+          callback(key.text, $.proxy(this.renderView, this));
+        } else {
+          this.view.hide();
+        }
+        return $.noop();
+      },
+      lookupWithData: function(key) {
+        var data, items,
+          _this = this;
+        data = this.getOpt("data");
+        if ($.isArray(data) && data.length !== 0) {
+          items = $.map(data, function(item, i) {
+            var match, name, regexp;
+            try {
+              name = $.isPlainObject(item) ? item[_this.dataValue()] : item;
+              regexp = new RegExp(key.text.replace("+", "\\+"), 'i');
+              match = name.match(regexp);
+            } catch (e) {
+              return null;
+            }
+            if (match) {
+              return item;
+            } else {
+              return null;
+            }
+          });
+        }
+        return items;
+      }
+    };
+    AtView = {
+      timeout_id: null,
+      id: '#at-view',
+      holder: null,
+      _jqo: null,
+      jqo: function() {
+        var jqo;
+        jqo = this._jqo;
+        return jqo = _isNil(jqo) ? (this._jqo = $(this.id)) : jqo;
+      },
+      init: function() {
+        var $menu, tpl,
+          _this = this;
+        if (!_isNil(this.jqo())) {
+          return;
+        }
+        tpl = "<div id='" + this.id.slice(1) + "' class='at-view'><ul id='" + this.id.slice(1) + "-ul'></ul></div>";
+        $("body").append(tpl);
+        $menu = this.jqo().find('ul');
+        return $menu.on('mouseenter.view', 'li', function(e) {
+          $menu.find('.cur').removeClass('cur');
+          return $(e.currentTarget).addClass('cur');
+        }).on('click', function(e) {
+          e.stopPropagation();
+          e.preventDefault();
+          return _this.choose();
+        });
+      },
+      isShowing: function() {
+        return this.jqo().is(":visible");
+      },
+      choose: function() {
+        var $li, str;
+        $li = this.jqo().find(".cur");
+        str = _isNil($li) ? this.holder.query.text + " " : $li.attr(this.holder.getOpt("choose")) + " ";
+        this.holder.replaceStr(str);
+        return this.hide();
+      },
+      rePosition: function() {
+        var rect;
+        rect = this.holder.rect();
+        if (rect.bottom + this.jqo().height() - $(window).scrollTop() > $(window).height()) {
+          rect.bottom = rect.top - this.jqo().height();
+        }
+        log("AtView.rePosition", {
+          left: rect.left,
+          top: rect.bottom
+        });
+        return this.jqo().offset({
+          left: rect.left,
+          top: rect.bottom
+        });
+      },
+      next: function() {
+        var cur, next;
+        cur = this.jqo().find('.cur').removeClass('cur');
+        next = cur.next();
+        if (!next.length) {
+          next = $(this.jqo().find('li')[0]);
+        }
+        return next.addClass('cur');
+      },
+      prev: function() {
+        var cur, prev;
+        cur = this.jqo().find('.cur').removeClass('cur');
+        prev = cur.prev();
+        if (!prev.length) {
+          prev = this.jqo().find('li').last();
+        }
+        return prev.addClass('cur');
+      },
+      show: function() {
+        if (!this.isShowing()) {
+          this.jqo().show();
+        }
+        return this.rePosition();
+      },
+      hide: function(time) {
+        var callback,
+          _this = this;
+        if (isNaN(time)) {
+          if (this.isShowing()) {
+            return this.jqo().hide();
+          }
+        } else {
+          callback = function() {
+            return _this.hide();
+          };
+          clearTimeout(this.timeout_id);
+          return this.timeout_id = setTimeout(callback, 300);
+        }
+      },
+      clear: function(clear_all) {
+        if (clear_all === true) {
+          this._cache = {};
+        }
+        return this.jqo().find('ul').empty();
+      },
+      render: function(holder, list) {
+        var $ul, tpl;
+        if (!$.isArray(list)) {
+          return false;
+        }
+        if (list.length <= 0) {
+          this.hide();
+          return true;
+        }
+        this.holder = holder;
+        holder.cache(list);
+        this.clear();
+        $ul = this.jqo().find('ul');
+        tpl = holder.getOpt('tpl');
+        $.each(list, function(i, item) {
+          var li;
+          tpl || (tpl = _DEFAULT_TPL);
+          li = _evalTpl(tpl, item);
+          log("AtView.render", li);
+          return $ul.append(_highlighter(li, holder.query.text));
+        });
+        this.show();
+        return $ul.find("li:eq(0)").addClass("cur");
+      }
+    };
+    _objectify = function(list) {
+      return $.map(list, function(item, k) {
+        if (!$.isPlainObject(item)) {
+          item = {
+            id: k,
+            name: item
+          };
+        }
+        return item;
+      });
+    };
+    _evalTpl = function(tpl, map) {
+      var el;
+      try {
+        return el = tpl.replace(/\$\{([^\}]*)\}/g, function(tag, key, pos) {
+          return map[key];
+        });
+      } catch (error) {
+        return "";
+      }
+    };
+    _highlighter = function(li, query) {
+      if (_isNil(query)) {
+        return li;
+      }
+      return li.replace(new RegExp(">\\s*(\\w*)(" + query.replace("+", "\\+") + ")(\\w*)\\s*<", 'ig'), function(str, $1, $2, $3) {
+        return '> ' + $1 + '<strong>' + $2 + '</strong>' + $3 + ' <';
+      });
+    };
+    _sorter = function(items) {
+      var data_value, item, query, results, text, _i, _len;
+      data_value = this.dataValue();
+      query = this.query.text;
+      results = [];
+      for (_i = 0, _len = items.length; _i < _len; _i++) {
+        item = items[_i];
+        text = item[data_value];
+        if (text.toLowerCase().indexOf(query) === -1) {
+          continue;
+        }
+        item.order = text.toLowerCase().indexOf(query);
+        results.push(item);
+      }
+      results.sort(function(a, b) {
+        return a.order - b.order;
+      });
+      return results;
+    };
+    /*
+          maybe we can use $._unique.
+          But i don't know it will delete li element frequently or not.
+          I think we should not change DOM element frequently.
+          more, It seems batter not to call evalTpl function too much times.
+    */
+
+    _unique = function(list, query) {
+      var record;
+      record = [];
+      return $.map(list, function(v, id) {
+        var value;
+        value = $.isPlainObject(v) ? v[query] : v;
+        if ($.inArray(value, record) < 0) {
+          record.push(value);
+          return v;
+        }
+      });
+    };
+    _isNil = function(target) {
+      return !target || ($.isPlainObject(target) && $.isEmptyObject(target)) || ($.isArray(target) && target.length === 0) || (target instanceof $ && target.length === 0) || target === void 0;
+    };
+    _DEFAULT_TPL = "<li id='${id}' data-value='${name}'>${name}</li>";
+    log = function() {};
+    $.fn.atWho = function(flag, options) {
+      AtView.init();
+      return this.filter('textarea, input').each(function() {
+        var $this, data;
+        $this = $(this);
+        data = $this.data("AtWho");
+        if (!data) {
+          $this.data('AtWho', (data = new At(this)));
+        }
+        return data.reg(flag, options);
+      });
+    };
+    return $.fn.atWho["default"] = {
+      data: [],
+      choose: "data-value",
+      callback: null,
+      cache: true,
+      limit: 5,
+      display_flag: true,
+      tpl: _DEFAULT_TPL
+    };
+  })(window.jQuery);
+
+}).call(this);

static/At.js/jquery.caret.js

+// Generated by CoffeeScript 1.3.3
+
+/*
+   Implement Twitter/Weibo @ mentions
+
+   Copyright (c) 2012 chord.luo@gmail.com
+
+   Permission is hereby granted, free of charge, to any person obtaining
+   a copy of this software and associated documentation files (the
+   "Software"), to deal in the Software without restriction, including
+   without limitation the rights to use, copy, modify, merge, publish,
+   distribute, sublicense, and/or sell copies of the Software, and to
+   permit persons to whom the Software is furnished to do so, subject to
+   the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+   LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+   WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+
+/*
+本插件操作 textarea 或者 input 内的插入符
+只实现了获得插入符在文本框中的位置,我设置
+插入符的位置.
+*/
+
+
+(function() {
+
+  (function($) {
+    var getCaretPos, setCaretPos;
+    getCaretPos = function(inputor) {
+      var end, endRange, len, normalizedValue, pos, range, start, textInputRange;
+      if (document.selection) {
+        /*
+                    #assume we select "HATE" in the inputor such as textarea -> { }.
+                     *               start end-point.
+                     *              /
+                     * <  I really [HATE] IE   > between the brackets is the selection range.
+                     *                   \
+                     *                    end end-point.
+        */
+
+        range = document.selection.createRange();
+        pos = 0;
+        if (range && range.parentElement() === inputor) {
+          normalizedValue = inputor.value.replace(/\r\n/g, "\n");
+          /* SOMETIME !!! 
+           "/r/n" is counted as two char.
+            one line is two, two will be four. balalala.
+            so we have to using the normalized one's length.;
+          */
+
+          len = normalizedValue.length;
+          /*
+                             <[  I really HATE IE   ]>:
+                              the whole content in the inputor will be the textInputRange.
+          */
+
+          textInputRange = inputor.createTextRange();
+          /*                 _here must be the position of bookmark.
+                           /
+             <[  I really [HATE] IE   ]>
+              [---------->[           ] : this is what moveToBookmark do.
+             <   I really [[HATE] IE   ]> : here is result.
+                            \ two brackets in should be in line.
+          */
+
+          textInputRange.moveToBookmark(range.getBookmark());
+          endRange = inputor.createTextRange();
+          /*  [--------------------->[] : if set false all end-point goto end.
+            <  I really [[HATE] IE  []]>
+          */
+
+          endRange.collapse(false);
+          /*
+                                          ___VS____
+                                         /         \
+                           <   I really [[HATE] IE []]>
+                                                    \_endRange end-point.
+                          
+                          " > -1" mean the start end-point will be the same or right to the end end-point
+                         * simplelly, all in the end.
+          */
+
+          if (textInputRange.compareEndPoints("StartToEnd", endRange) > -1) {
+            start = end = len;
+          } else {
+            /*
+                                        I really |HATE] IE   ]> 
+                                               <-|
+                                      I really[ [HATE] IE   ]>
+                                            <-[
+                                    I reall[y  [HATE] IE   ]>
+                                 
+                                  will return how many unit have moved.
+            */
+
+            start = -textInputRange.moveStart("character", -len);
+            end = -textInputRange.moveEnd("character", -len);
+          }
+        }
+      } else {
+        start = inputor.selectionStart;
+      }
+      return start;
+    };
+    setCaretPos = function(inputor, pos) {
+      var range;
+      if (document.selection) {
+        range = inputor.createTextRange();
+        range.move("character", pos);
+        return range.select();
+      } else {
+        return inputor.setSelectionRange(pos, pos);
+      }
+    };
+    return $.fn.caretPos = function(pos) {
+      var inputor;
+      inputor = this[0];
+      inputor.focus();
+      if (pos) {
+        return setCaretPos(inputor, pos);
+      } else {
+        return getCaretPos(inputor);
+      }
+    };
+  })(window.jQuery);
+
+}).call(this);

static/At.js/js/jquery.atwho.js

+// Generated by CoffeeScript 1.3.3
+
+/*
+   Implement Twitter/Weibo @ mentions
+
+   Copyright (c) 2012 chord.luo@gmail.com
+
+   Permission is hereby granted, free of charge, to any person obtaining
+   a copy of this software and associated documentation files (the
+   "Software"), to deal in the Software without restriction, including
+   without limitation the rights to use, copy, modify, merge, publish,
+   distribute, sublicense, and/or sell copies of the Software, and to
+   permit persons to whom the Software is furnished to do so, subject to
+   the following conditions:
+
+   The above copyright notice and this permission notice shall be
+   included in all copies or substantial portions of the Software.
+
+   THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+   EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+   MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+   NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+   LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+   OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+   WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+
+
+(function() {
+
+  (function($) {
+    var At, AtView, Mirror, log, _DEFAULT_TPL, _evalTpl, _highlighter, _isNil, _objectify, _sorter, _unique;
+    Mirror = {
+      $mirror: null,
+      css: ["overflowY", "height", "width", "paddingTop", "paddingLeft", "paddingRight", "paddingBottom", "marginTop", "marginLeft", "marginRight", "marginBottom", 'fontFamily', 'borderStyle', 'borderWidth', 'wordWrap', 'fontSize', 'lineHeight', 'overflowX'],
+      init: function($origin) {
+        var $mirror, css;
+        $mirror = $('<div></div>');
+        css = {
+          position: 'absolute',
+          left: -9999,
+          top: 0,
+          zIndex: -20000,
+          'white-space': 'pre-wrap'
+        };
+        $.each(this.css, function(i, p) {
+          return css[p] = $origin.css(p);
+        });
+        $mirror.css(css);
+        this.$mirror = $mirror;
+        $origin.after($mirror);
+        return this;
+      },
+      setContent: function(html) {
+        this.$mirror.html(html);
+        return this;
+      },
+      getFlagRect: function() {
+        var $flag, pos, rect;
+        $flag = this.$mirror.find("span#flag");
+        pos = $flag.position();
+        rect = {
+          left: pos.left,
+          top: pos.top,
+          bottom: $flag.height() + pos.top
+        };
+        this.$mirror.remove();
+        return rect;
+      }
+    };
+    At = function(inputor) {
+      var $inputor,
+        _this = this;
+      $inputor = this.$inputor = $(inputor);
+      this.options = {};
+      this.query = {
+        text: "",
+        start: 0,
+        stop: 0
+      };
+      this._cache = {};
+      this.pos = 0;
+      this.flags = {};
+      this.theflag = null;
+      this.search_word = {};
+      this.view = AtView;
+      this.mirror = Mirror;
+      $inputor.on("keyup.inputor", function(e) {
+        var lookup, stop;
+        stop = e.keyCode === 40 || e.keyCode === 38;
+        lookup = !(stop && _this.view.isShowing());
+        if (lookup) {
+          return _this.lookup();
+        }
+      });
+      this.init();
+      log("At.new", $inputor[0]);
+      return this;
+    };
+    At.prototype = {
+      constructor: At,
+      init: function() {
+        var _this = this;
+        this.$inputor.on('keyup.inputor', function(e) {
+          return _this.onkeyup(e);
+        }).on('keydown.inputor', function(e) {
+          return _this.onkeydown(e);
+        }).on('scroll.inputor', function(e) {
+          return _this.view.hide();
+        }).on('blur.inputor', function(e) {
+          return _this.view.hide(1000);
+        });
+        return log("At.init", this.$inputor[0]);
+      },
+      reg: function(flag, options) {
+        var opt, _base, _default;
+        opt = {};
+        if ($.isFunction(options)) {
+          opt['callback'] = options;
+        } else {
+          opt = options;
+        }
+        _default = (_base = this.options)[flag] || (_base[flag] = $.fn.atWho["default"]);
+        this.options[flag] = $.extend({}, _default, opt);
+        return log("At.reg", this.$inputor[0], flag, options);
+      },
+      dataValue: function() {
+        var match, search_word;
+        search_word = this.search_word[this.theflag];
+        if (search_word) {
+          return search_word;
+        }
+        match = /data-value=["']?\$\{(\w+)\}/g.exec(this.getOpt('tpl'));
+        return this.search_word[this.theflag] = !_isNil(match) ? match[1] : null;
+      },
+      getOpt: function(key) {
+        try {
+          return this.options[this.theflag][key];
+        } catch (error) {
+          return null;
+        }
+      },
+      rect: function() {
+        var $inputor, Sel, at_rect, bottom, format, html, offset, start_range, x, y;
+        $inputor = this.$inputor;
+        if (document.selection) {
+          Sel = document.selection.createRange();
+          x = Sel.boundingLeft + $inputor.scrollLeft();
+          y = Sel.boundingTop + $(window).scrollTop() + $inputor.scrollTop();
+          bottom = y + Sel.boundingHeight;
+          return {
+            top: y - 2,
+            left: x - 2,
+            bottom: bottom - 2
+          };
+        }
+        format = function(value) {
+          return value.replace(/</g, '&lt').replace(/>/g, '&gt').replace(/`/g, '&#96').replace(/"/g, '&quot').replace(/\r\n|\r|\n/g, "<br />");
+        };
+        /* 克隆完inputor后将原来的文本内容根据
+          @的位置进行分块,以获取@块在inputor(输入框)里的position
+        */
+
+        start_range = $inputor.val().slice(0, this.pos - 1);