├── LICENSE ├── README.md ├── example.html ├── jquery.fieldselection.js ├── jquery.scrollTo.js └── jquery.tagmate.js /LICENSE: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any person obtaining 2 | a copy of this software and associated documentation files (the 3 | "Software"), to deal in the Software without restriction, including 4 | without limitation the rights to use, copy, modify, merge, publish, 5 | distribute, sublicense, and/or sell copies of the Software, and to 6 | permit persons to whom the Software is furnished to do so, subject to 7 | the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be 10 | included in all copies or substantial portions of the Software. 11 | 12 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 13 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 14 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 15 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 16 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 17 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 18 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TAGMATE! 2 | ======== 3 | A jQuery plugin for capturing tags within textareas (Facebook / Twitter style). 4 | 5 | 6 | AUTHORS 7 | ------- 8 | * Ryan Probasco 9 | 10 | 11 | FEATURES 12 | -------- 13 | * Supports #hash tags, @name tags, and $price tags out of the box. 14 | * Inline autocomplete similar to jQuery UI autocomplete plugin. 15 | * reate custom tag parsing rules. 16 | * *EXPERIMENTAL* inline tag higlighting mode. 17 | 18 | 19 | REQUIRES 20 | -------- 21 | - jquery.js (http://jquery.com) 22 | - jquery.scrollTo.js (http://demos.flesler.com/jquery/scrollTo/) 23 | - jquery.fieldselection.js (*must* use included version) 24 | 25 | 26 | EXAMPLE 27 | ------- 28 | 29 | 30 | 31 | 32 | 42 |
43 | 44 | 45 |
46 | 47 | 48 | OPTIONS 49 | ------- 50 | * `exprs` - Mapping of tag keys to regular expression rules. 51 | - Example: `{ '#': '\\w+' }` 52 | - Default: `Tagmate.DEFAULT_EXPRS` 53 | * `sources` - Mapping of tag keys to autocomplete suggestions. Value can be an array 54 | or a function. 55 | - Example: `{ '@': [{label:'Mr. Foo',value:'foo'}] }` 56 | * `capture_tag` - Callback fired when tags are captured. 57 | - Example: `function(tag) { alert('Got tag: ' + tag); }` 58 | * `replace_tag` - Callback fired when tag is replaced. 59 | - Example: `function(tag, label) { alert('replaced:' + tag + ' with: ' + label); }` 60 | * `menu_class` - CSS class to add to the menu. 61 | - Default: "tagmate-menu". 62 | * `menu_option_class` - CSS class to add to menu options. 63 | - Default: `"tagmate-menu-option"` 64 | * `menu_option_active_class` - CSS class to use when menu option is active. 65 | - Default: `"tagmate-menu-option-active"` 66 | * `highlight_tags` - *EXPERIMENTAL!* Enable at your own risk! Highlights tags by 67 | placing a div behind the transparent textarea. 68 | - Default: `false` 69 | * `highlight_class` - CSS class to use for highlighted tags. 70 | - Default: `"tagmate-highlight"` 71 | 72 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 74 | 96 | 97 | 98 |
99 |
100 | 101 | 102 | 103 | 104 |
105 | 106 | 107 | -------------------------------------------------------------------------------- /jquery.fieldselection.js: -------------------------------------------------------------------------------- 1 | /* 2 | * jQuery plugin: fieldSelection - v0.1.0 - last change: 2006-12-16 3 | * (c) 2006 Alex Brem - http://blog.0xab.cd 4 | * 5 | * NOTE: This version of the plugin has been heavily modified from its 6 | * original version here: https://github.com/localhost/jquery-fieldselection 7 | * for use with the Tagmate plugin here: https://github.com/pinterest/tagmate. 8 | * Some of the modifications (see setSelection()) were made specifically for 9 | * the Tagmate plugin, while others were made by an unknown author (authors?). 10 | * 11 | * 2011-10-28: Ryan Probasco - added setSelection() 12 | */ 13 | 14 | (function() { 15 | var fieldSelection = { 16 | getSelection: function() { 17 | var e = this.jquery ? this[0] : this; 18 | 19 | return ( 20 | /* mozilla / dom 3.0 */ 21 | ('selectionStart' in e && function() { 22 | var l = e.selectionEnd - e.selectionStart; 23 | return { 24 | start: e.selectionStart, 25 | end: e.selectionEnd, 26 | length: l, 27 | text: e.value.substr(e.selectionStart, l)}; 28 | }) 29 | 30 | /* exploder */ 31 | || (document.selection && function() { 32 | e.focus(); 33 | 34 | var r = document.selection.createRange(); 35 | if (r == null) { 36 | return { 37 | start: 0, 38 | end: e.value.length, 39 | length: 0}; 40 | } 41 | 42 | var re = e.createTextRange(); 43 | var rc = re.duplicate(); 44 | re.moveToBookmark(r.getBookmark()); 45 | rc.setEndPoint('EndToStart', re); 46 | 47 | // IE bug - it counts newline as 2 symbols when getting selection coordinates, 48 | // but counts it as one symbol when setting selection 49 | var rcLen = rc.text.length, 50 | i, 51 | rcLenOut = rcLen; 52 | for (i = 0; i < rcLen; i++) { 53 | if (rc.text.charCodeAt(i) == 13) rcLenOut--; 54 | } 55 | var rLen = r.text.length, 56 | rLenOut = rLen; 57 | for (i = 0; i < rLen; i++) { 58 | if (r.text.charCodeAt(i) == 13) rLenOut--; 59 | } 60 | 61 | return { 62 | start: rcLenOut, 63 | end: rcLenOut + rLenOut, 64 | length: rLenOut, 65 | text: r.text}; 66 | }) 67 | 68 | /* browser not supported */ 69 | || function() { 70 | return { 71 | start: 0, 72 | end: e.value.length, 73 | length: 0}; 74 | } 75 | 76 | )(); 77 | 78 | }, 79 | 80 | // 81 | // Adapted from http://stackoverflow.com/questions/401593/javascript-textarea-selection 82 | // 83 | setSelection: function() 84 | { 85 | var e = this.jquery ? this[0] : this; 86 | var start_pos = arguments[0] || 0; 87 | var end_pos = arguments[1] || 0; 88 | 89 | return ( 90 | //Mozilla and DOM 3.0 91 | ('selectionStart' in e && function() { 92 | e.focus(); 93 | e.selectionStart = start_pos; 94 | e.selectionEnd = end_pos; 95 | return this; 96 | }) 97 | 98 | //IE 99 | || (document.selection && function() { 100 | e.focus(); 101 | var tr = e.createTextRange(); 102 | 103 | //Fix IE from counting the newline characters as two seperate characters 104 | var stop_it = start_pos; 105 | for (i=0; i < stop_it; i++) if( e.value[i].search(/[\r\n]/) != -1 ) start_pos = start_pos - .5; 106 | stop_it = end_pos; 107 | for (i=0; i < stop_it; i++) if( e.value[i].search(/[\r\n]/) != -1 ) end_pos = end_pos - .5; 108 | 109 | tr.moveEnd('textedit',-1); 110 | tr.moveStart('character',start_pos); 111 | tr.moveEnd('character',end_pos - start_pos); 112 | tr.select(); 113 | 114 | return this; 115 | }) 116 | 117 | //Not supported 118 | || function() { 119 | return this; 120 | } 121 | )(); 122 | }, 123 | 124 | replaceSelection: function() { 125 | var e = this.jquery ? this[0] : this; 126 | var text = arguments[0] || ''; 127 | 128 | return ( 129 | /* mozilla / dom 3.0 */ 130 | ('selectionStart' in e && function() { 131 | e.value = e.value.substr(0, e.selectionStart) + text + e.value.substr(e.selectionEnd, e.value.length); 132 | return this; 133 | }) 134 | 135 | /* exploder */ 136 | || (document.selection && function() { 137 | e.focus(); 138 | document.selection.createRange().text = text; 139 | return this; 140 | }) 141 | 142 | /* browser not supported */ 143 | || function() { 144 | e.value += text; 145 | return this; 146 | } 147 | )(); 148 | } 149 | }; 150 | 151 | jQuery.each(fieldSelection, function(i) { jQuery.fn[i] = this; }); 152 | 153 | })(); 154 | -------------------------------------------------------------------------------- /jquery.scrollTo.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jQuery.ScrollTo 3 | * Copyright (c) 2007-2009 Ariel Flesler - aflesler(at)gmail(dot)com | http://flesler.blogspot.com 4 | * Dual licensed under MIT and GPL. 5 | * Date: 5/25/2009 6 | * 7 | * @projectDescription Easy element scrolling using jQuery. 8 | * http://flesler.blogspot.com/2007/10/jqueryscrollto.html 9 | * Works with jQuery +1.2.6. Tested on FF 2/3, IE 6/7/8, Opera 9.5/6, Safari 3, Chrome 1 on WinXP. 10 | * 11 | * @author Ariel Flesler 12 | * @version 1.4.2 13 | * 14 | * @id jQuery.scrollTo 15 | * @id jQuery.fn.scrollTo 16 | * @param {String, Number, DOMElement, jQuery, Object} target Where to scroll the matched elements. 17 | * The different options for target are: 18 | * - A number position (will be applied to all axes). 19 | * - A string position ('44', '100px', '+=90', etc ) will be applied to all axes 20 | * - A jQuery/DOM element ( logically, child of the element to scroll ) 21 | * - A string selector, that will be relative to the element to scroll ( 'li:eq(2)', etc ) 22 | * - A hash { top:x, left:y }, x and y can be any kind of number/string like above. 23 | * - A percentage of the container's dimension/s, for example: 50% to go to the middle. 24 | * - The string 'max' for go-to-end. 25 | * @param {Number} duration The OVERALL length of the animation, this argument can be the settings object instead. 26 | * @param {Object,Function} settings Optional set of settings or the onAfter callback. 27 | * @option {String} axis Which axis must be scrolled, use 'x', 'y', 'xy' or 'yx'. 28 | * @option {Number} duration The OVERALL length of the animation. 29 | * @option {String} easing The easing method for the animation. 30 | * @option {Boolean} margin If true, the margin of the target element will be deducted from the final position. 31 | * @option {Object, Number} offset Add/deduct from the end position. One number for both axes or { top:x, left:y }. 32 | * @option {Object, Number} over Add/deduct the height/width multiplied by 'over', can be { top:x, left:y } when using both axes. 33 | * @option {Boolean} queue If true, and both axis are given, the 2nd axis will only be animated after the first one ends. 34 | * @option {Function} onAfter Function to be called after the scrolling ends. 35 | * @option {Function} onAfterFirst If queuing is activated, this function will be called after the first scrolling ends. 36 | * @return {jQuery} Returns the same jQuery object, for chaining. 37 | * 38 | * @desc Scroll to a fixed position 39 | * @example $('div').scrollTo( 340 ); 40 | * 41 | * @desc Scroll relatively to the actual position 42 | * @example $('div').scrollTo( '+=340px', { axis:'y' } ); 43 | * 44 | * @dec Scroll using a selector (relative to the scrolled element) 45 | * @example $('div').scrollTo( 'p.paragraph:eq(2)', 500, { easing:'swing', queue:true, axis:'xy' } ); 46 | * 47 | * @ Scroll to a DOM element (same for jQuery object) 48 | * @example var second_child = document.getElementById('container').firstChild.nextSibling; 49 | * $('#container').scrollTo( second_child, { duration:500, axis:'x', onAfter:function(){ 50 | * alert('scrolled!!'); 51 | * }}); 52 | * 53 | * @desc Scroll on both axes, to different values 54 | * @example $('div').scrollTo( { top: 300, left:'+=200' }, { axis:'xy', offset:-20 } ); 55 | */ 56 | ;(function( $ ){ 57 | 58 | var $scrollTo = $.scrollTo = function( target, duration, settings ){ 59 | $(window).scrollTo( target, duration, settings ); 60 | }; 61 | 62 | $scrollTo.defaults = { 63 | axis:'xy', 64 | duration: parseFloat($.fn.jquery) >= 1.3 ? 0 : 1 65 | }; 66 | 67 | // Returns the element that needs to be animated to scroll the window. 68 | // Kept for backwards compatibility (specially for localScroll & serialScroll) 69 | $scrollTo.window = function( scope ){ 70 | return $(window)._scrollable(); 71 | }; 72 | 73 | // Hack, hack, hack :) 74 | // Returns the real elements to scroll (supports window/iframes, documents and regular nodes) 75 | $.fn._scrollable = function(){ 76 | return this.map(function(){ 77 | var elem = this, 78 | isWin = !elem.nodeName || $.inArray( elem.nodeName.toLowerCase(), ['iframe','#document','html','body'] ) != -1; 79 | 80 | if( !isWin ) 81 | return elem; 82 | 83 | var doc = (elem.contentWindow || elem).document || elem.ownerDocument || elem; 84 | 85 | return $.browser.safari || doc.compatMode == 'BackCompat' ? 86 | doc.body : 87 | doc.documentElement; 88 | }); 89 | }; 90 | 91 | $.fn.scrollTo = function( target, duration, settings ){ 92 | if( typeof duration == 'object' ){ 93 | settings = duration; 94 | duration = 0; 95 | } 96 | if( typeof settings == 'function' ) 97 | settings = { onAfter:settings }; 98 | 99 | if( target == 'max' ) 100 | target = 9e9; 101 | 102 | settings = $.extend( {}, $scrollTo.defaults, settings ); 103 | // Speed is still recognized for backwards compatibility 104 | duration = duration || settings.speed || settings.duration; 105 | // Make sure the settings are given right 106 | settings.queue = settings.queue && settings.axis.length > 1; 107 | 108 | if( settings.queue ) 109 | // Let's keep the overall duration 110 | duration /= 2; 111 | settings.offset = both( settings.offset ); 112 | settings.over = both( settings.over ); 113 | 114 | return this._scrollable().each(function(){ 115 | var elem = this, 116 | $elem = $(elem), 117 | targ = target, toff, attr = {}, 118 | win = $elem.is('html,body'); 119 | 120 | switch( typeof targ ){ 121 | // A number will pass the regex 122 | case 'number': 123 | case 'string': 124 | if( /^([+-]=)?\d+(\.\d+)?(px|%)?$/.test(targ) ){ 125 | targ = both( targ ); 126 | // We are done 127 | break; 128 | } 129 | // Relative selector, no break! 130 | targ = $(targ,this); 131 | case 'object': 132 | // DOMElement / jQuery 133 | if( targ.is || targ.style ) 134 | // Get the real position of the target 135 | toff = (targ = $(targ)).offset(); 136 | } 137 | $.each( settings.axis.split(''), function( i, axis ){ 138 | var Pos = axis == 'x' ? 'Left' : 'Top', 139 | pos = Pos.toLowerCase(), 140 | key = 'scroll' + Pos, 141 | old = elem[key], 142 | max = $scrollTo.max(elem, axis); 143 | 144 | if( toff ){// jQuery / DOMElement 145 | attr[key] = toff[pos] + ( win ? 0 : old - $elem.offset()[pos] ); 146 | 147 | // If it's a dom element, reduce the margin 148 | if( settings.margin ){ 149 | attr[key] -= parseInt(targ.css('margin'+Pos)) || 0; 150 | attr[key] -= parseInt(targ.css('border'+Pos+'Width')) || 0; 151 | } 152 | 153 | attr[key] += settings.offset[pos] || 0; 154 | 155 | if( settings.over[pos] ) 156 | // Scroll to a fraction of its width/height 157 | attr[key] += targ[axis=='x'?'width':'height']() * settings.over[pos]; 158 | }else{ 159 | var val = targ[pos]; 160 | // Handle percentage values 161 | attr[key] = val.slice && val.slice(-1) == '%' ? 162 | parseFloat(val) / 100 * max 163 | : val; 164 | } 165 | 166 | // Number or 'number' 167 | if( /^\d+$/.test(attr[key]) ) 168 | // Check the limits 169 | attr[key] = attr[key] <= 0 ? 0 : Math.min( attr[key], max ); 170 | 171 | // Queueing axes 172 | if( !i && settings.queue ){ 173 | // Don't waste time animating, if there's no need. 174 | if( old != attr[key] ) 175 | // Intermediate animation 176 | animate( settings.onAfterFirst ); 177 | // Don't animate this axis again in the next iteration. 178 | delete attr[key]; 179 | } 180 | }); 181 | 182 | animate( settings.onAfter ); 183 | 184 | function animate( callback ){ 185 | $elem.animate( attr, duration, settings.easing, callback && function(){ 186 | callback.call(this, target, settings); 187 | }); 188 | }; 189 | 190 | }).end(); 191 | }; 192 | 193 | // Max scrolling position, works on quirks mode 194 | // It only fails (not too badly) on IE, quirks mode. 195 | $scrollTo.max = function( elem, axis ){ 196 | var Dim = axis == 'x' ? 'Width' : 'Height', 197 | scroll = 'scroll'+Dim; 198 | 199 | if( !$(elem).is('html,body') ) 200 | return elem[scroll] - $(elem)[Dim.toLowerCase()](); 201 | 202 | var size = 'client' + Dim, 203 | html = elem.ownerDocument.documentElement, 204 | body = elem.ownerDocument.body; 205 | 206 | return Math.max( html[scroll], body[scroll] ) 207 | - Math.min( html[size] , body[size] ); 208 | 209 | }; 210 | 211 | function both( val ){ 212 | return typeof val == 'object' ? val : { top:val, left:val }; 213 | }; 214 | 215 | })( jQuery ); -------------------------------------------------------------------------------- /jquery.tagmate.js: -------------------------------------------------------------------------------- 1 | /** 2 | * jquery.tagmate.js 3 | * ================= 4 | * Copyright (c) 2011 Cold Brew Labs Inc., http://coldbrewlabs.com 5 | * Licenced under MIT (see included LICENSE) 6 | * 7 | * Requirements 8 | * ------------ 9 | * jquery.js (http://jquery.com) 10 | * jquery.scrollTo.js (http://demos.flesler.com/jquery/scrollTo/) 11 | * jquery.fieldselection.js - (included) 12 | */ 13 | 14 | // 15 | // Global namespace stuff. These are provided as a convenience to plugin users. 16 | // 17 | var Tagmate = (function() { 18 | var HASH_TAG_EXPR = "\\w+"; 19 | var NAME_TAG_EXPR = "\\w+(?: \\w+)*"; // allow spaces 20 | var PRICE_TAG_EXPR = "(?:(?:\\d{1,3}(?:\\,\\d{3})+)|(?:\\d+))(?:\\.\\d{2})?"; 21 | 22 | return { 23 | HASH_TAG_EXPR: HASH_TAG_EXPR, 24 | NAME_TAG_EXPR: NAME_TAG_EXPR, 25 | PRICE_TAG_EXPR: PRICE_TAG_EXPR, 26 | 27 | DEFAULT_EXPRS: { 28 | '@': NAME_TAG_EXPR, 29 | '#': HASH_TAG_EXPR, 30 | '$': PRICE_TAG_EXPR 31 | }, 32 | 33 | // Remove options that don't match the filter. 34 | filterOptions: function(options, term) { 35 | var filtered = []; 36 | for (var i = 0; i < options.length; i++) { 37 | var label_lc = options[i].label.toLowerCase(); 38 | var term_lc = term.toLowerCase(); 39 | if (term_lc.length <= label_lc.length && label_lc.indexOf(term_lc) == 0) 40 | filtered.push(options[i]); 41 | } 42 | return filtered; 43 | } 44 | }; 45 | })(); 46 | 47 | // 48 | // jQuery plugin 49 | // 50 | (function($) { 51 | // Similar to indexOf() but uses RegExp. 52 | function regex_index_of(str, regex, startpos) { 53 | var indexOf = str.substring(startpos || 0).search(regex); 54 | return (indexOf >= 0) ? (indexOf + (startpos || 0)) : indexOf; 55 | } 56 | 57 | // Escape special RegExp chars. 58 | function regex_escape(text) { 59 | return text.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, "\\$&"); 60 | } 61 | 62 | // Parse tags from a textarea (internal). 63 | function parse_tags(textarea, exprs, sources) { 64 | var tags = {}; 65 | for (tok in exprs) { 66 | if (sources && sources[tok]) { 67 | // Favor sources to raw tags if available 68 | var matches = {}, indexes = {}; 69 | for (key in sources[tok]) { 70 | var value = sources[tok][key].value; 71 | var label = sources[tok][key].label; 72 | var tag = regex_escape(tok + label); 73 | // This regexp is insane. \b won't work because we allow spaces. 74 | var e = ["(?:^(",")$|^(",")\\W|\\W(",")\\W|\\W(",")$)"].join(tag); 75 | var i = 0, re = new RegExp(e, "gm"); 76 | while ((i = regex_index_of(textarea.val(), re, i)) > -1) { 77 | var p = indexes[i] ? indexes[i] : null; 78 | // Favor longer matches 79 | if (!p || matches[p].length < label.length) 80 | indexes[i] = value; 81 | matches[value] = label; 82 | i += label.length + 1; 83 | } 84 | } 85 | // Keep only longest matches 86 | for (i in indexes) 87 | tags[tok + indexes[i]] = tok; 88 | } else { 89 | // Check for raw tags 90 | var m = null, re = new RegExp("([" + tok + "]" + exprs[tok] + ")", "gm"); 91 | while (m = re.exec(textarea.val())) 92 | tags[m[1]] = tok; 93 | } 94 | } 95 | 96 | // Keep only uniques 97 | var results = [] 98 | for (tag in tags) 99 | results.push(tag); 100 | return results; 101 | } 102 | 103 | $.fn.extend({ 104 | getTags: function(exprs, sources) { 105 | var textarea = $(this); 106 | exprs = exprs || textarea.data("_tagmate_exprs"); 107 | sources = sources || textarea.data("_tagmate_sources"); 108 | return parse_tags(textarea, exprs, sources); 109 | }, 110 | tagmate: function(options) { 111 | var defaults = { 112 | exprs: Tagmate.DEFAULT_EXPRS, 113 | sources: null, // { '@': [{label:'foo',value:'bar'}] } 114 | capture_tag: null, // Callback fired when tags are captured. function(tag) {} 115 | replace_tag: null, // Callback fired when tag is replaced. function(tag, label) {} 116 | menu_class: "tagmate-menu", 117 | menu_option_class: "tagmate-menu-option", 118 | menu_option_active_class: "tagmate-menu-option-active", 119 | highlight_tags: false, // EXPERIMENTAL: enable at your own risk! 120 | highlight_class: 'tagmate-highlight' 121 | }; 122 | 123 | // Get the previous position of tok starting at pos (or -1) 124 | function prev_tok(str, tok, pos) { 125 | var re = new RegExp("[" + tok + "]"); 126 | for (; pos >= 0 && !re.test(str[pos]); pos--) {}; 127 | return pos; 128 | } 129 | 130 | // Get tag value at current cursor position 131 | function parse_tag(textarea) { 132 | var text = textarea.val(); 133 | var sel = textarea.getSelection(); 134 | 135 | // Search left for closest matching tag token 136 | var m_pos = -1, m_tok = null; 137 | for (tok in defaults.exprs) { 138 | var pos = prev_tok(text, tok, sel.start); 139 | if (pos > m_pos) { 140 | m_pos = pos; 141 | m_tok = tok; 142 | } 143 | } 144 | 145 | // Match from token to cursor 146 | var sub = text.substring(m_pos + 1, sel.start); 147 | 148 | // Look for raw matches 149 | var re = new RegExp("^[" + m_tok + "]" + defaults.exprs[m_tok]); 150 | if (re.exec(m_tok + sub)) 151 | return m_tok + sub 152 | 153 | return null; 154 | } 155 | 156 | // Replace the textarea query text with the suggestion 157 | function replace_tag(textarea, tag, value) { 158 | var text = textarea.val(); 159 | 160 | // Replace occurrence at cursor position 161 | var sel = textarea.getSelection(); 162 | var pos = prev_tok(text, tag[0], sel.start); 163 | var l = text.substr(0, pos); 164 | var r = text.substr(pos + tag.length); 165 | textarea.val(l + tag[0] + value + r); 166 | 167 | // Try to move cursor position at end of tag 168 | var sel_pos = pos + value.length + 1; 169 | textarea.setSelection(sel_pos, sel_pos); 170 | 171 | // Callback for tag replacement 172 | if (defaults.replace_tag) 173 | defaults.replace_tag(tag, value); 174 | } 175 | 176 | // Show the menu of options 177 | function update_menu(menu, options) { 178 | // Sort results alphabetically 179 | options = options.sort(function(a, b) { 180 | var a_lc = a.label.toLowerCase(); 181 | var b_lc = b.label.toLowerCase(); 182 | if (a_lc > b_lc) 183 | return 1; 184 | else if (a_lc < b_lc) 185 | return -1; 186 | return 0; 187 | }); 188 | 189 | // Append results to menu 190 | for (var i = 0; i < options.length; i++) { 191 | var label = options[i].label; 192 | var value = options[i].value; 193 | var image = options[i].image; 194 | if (i == 0) 195 | menu.html(""); 196 | var content = "" + label + ""; 197 | if (image) 198 | content = "" + label + "" + content; 199 | var classes = defaults.menu_option_class; 200 | if (i == 0) 201 | classes += " " + defaults.menu_option_active_class; 202 | menu.append("
" + content + "
"); 203 | } 204 | } 205 | 206 | // Move up or down in the selection menu 207 | function scroll_menu(menu, direction) { 208 | var child_selector = direction == "down" ? ":first-child" : ":last-child"; 209 | var sibling_func = direction == "down" ? "next" : "prev"; 210 | var active = menu.children("." + defaults.menu_option_active_class); 211 | 212 | if (active.length == 0) { 213 | active = menu.children(child_selector); 214 | active.addClass(defaults.menu_option_active_class); 215 | } else { 216 | active.removeClass(defaults.menu_option_active_class); 217 | active = active[sibling_func]().length > 0 ? active[sibling_func]() : active; 218 | active.addClass(defaults.menu_option_active_class); 219 | } 220 | 221 | // Scroll inside menu if necessary 222 | var i, options = menu.children(); 223 | var n = Math.floor($(menu).height() / $(options[0]).height()) - 1; 224 | if ($(menu).height() % $(options[0]).height() > 0) 225 | n -= 1; // don't scroll if bottom row is only partially visible 226 | // Iterate to visible option 227 | for (i = 0; i < options.length && $(options[i]).html() != $(active).html(); i++) {}; 228 | if (i > n && (i - n) >= 0 && (i - n) < options.length) 229 | menu.scrollTo(options[i - n]); 230 | } 231 | 232 | // TODO: Fix this so that it works. 233 | function init_hiliter(textarea) { 234 | textarea.css("background", "transparent"); 235 | 236 | var container = $(textarea).wrap("
"); 237 | 238 | // Set up highlighter div 239 | var hiliter = $("
");
240 |                 hiliter.css("height", textarea.height() + "px");
241 |                 hiliter.css("width", textarea.width() + "px");
242 |                 hiliter.css("border", "1px solid #FFF");
243 |                 //hiliter.css("position", "inherit");
244 |                 //hiliter.css("top", "-" + textarea.outerHeight() + "px");
245 |                 hiliter.css("margin", "0");
246 |                 //hiliter.css("top", "0");
247 |                 //hiliter.css("left", "0");
248 |                 hiliter.css("padding-top", textarea.css("padding-top"));
249 |                 hiliter.css("padding-bottom", textarea.css("padding-bottom"));
250 |                 hiliter.css("padding-left", textarea.css("padding-left"));
251 |                 hiliter.css("padding-right", textarea.css("padding-right"));
252 |                 hiliter.css("color", "#FFF");
253 |                 hiliter.css("z-index", "-1");
254 |                 hiliter.css("background", "#FFF");
255 |                 hiliter.css("font-family", textarea.css("font-family"));
256 |                 hiliter.css("font-size", textarea.css("font-size"));
257 | 
258 |                 // Enable text wrapping in 
259 |                 hiliter.css("white-space", "pre-wrap");
260 |                 hiliter.css("white-space", "-moz-pre-wrap !important");
261 |                 hiliter.css("white-space", "-pre-wrap");
262 |                 hiliter.css("white-space", "-o-pre-wrap");
263 |                 hiliter.css("word-wrap", "break-word");
264 | 
265 |                 textarea.before(hiliter);
266 |                 textarea.css("margin-top", "-" + textarea.outerHeight() + "px");
267 | 
268 |                 return hiliter;
269 |             }
270 | 
271 |             // TODO: Fix this so that it works.
272 |             function update_hiliter(textarea, hiliter) {
273 |                 var html = textarea.val();
274 |                 var sources = textarea.data("_tagmate_sources");
275 |                 var tags = parse_tags(textarea, defaults.exprs, sources);
276 | 
277 |                 for (var i = 0; i < tags.length; i++) {
278 |                     var expr = tags[i], tok = tags[i][0], term = tags[i].substr(1);
279 |                     if (sources && sources[tok]) {
280 |                         for (var j = 0; j < sources[tok].length; j++) {
281 |                             var option = sources[tok][j];
282 |                             if (option.value == term) {
283 |                                 expr = tok + option.label;
284 |                                 break;
285 |                             }
286 |                         }
287 |                     }
288 | 
289 |                     // Wrap tags in highlighter span
290 |                     var re = new RegExp(regex_escape(expr), "g");
291 |                     var span = "" + expr + "";
292 |                     html = html.replace(re, span);
293 |                 }
294 | 
295 |                 hiliter.html(html);
296 |             }
297 | 
298 |             return this.each(function() {
299 |                 if (options)
300 |                     $.extend(defaults, options);
301 | 
302 |                 var textarea = $(this);
303 | 
304 |                 // Optionally enable the hiliter
305 |                 var hiliter = null;
306 |                 if (defaults.highlight_tags)
307 |                     hiliter = init_hiliter(textarea);
308 | 
309 |                 textarea.data("_tagmate_exprs", defaults.exprs);
310 | 
311 |                 // Initialize static lists of sources
312 |                 var sources_holder = {};
313 |                 for (var tok in defaults.sources)
314 |                     sources_holder[tok] = [];
315 |                 textarea.data("_tagmate_sources", sources_holder);
316 | 
317 |                 // Set up the menu
318 |                 var menu = $("
"); 319 | textarea.after(menu); 320 | 321 | var pos = textarea.offset(); 322 | menu.css("position", "absolute"); 323 | menu.hide(); 324 | 325 | // Activate menu and fire callbacks if cursor enters a tag 326 | function tag_check() { 327 | menu.hide(); 328 | 329 | // Check for user tag 330 | var tag = parse_tag(textarea); 331 | if (tag) { 332 | // Make sure cursor is within token 333 | var tok = tag[0], term = tag.substr(1); 334 | var sel = textarea.getSelection(); 335 | var pos = prev_tok(textarea.val(), tok, sel.start); 336 | if ((sel.start - pos) <= tag.length) { 337 | (function(done) { 338 | if (typeof defaults.sources[tok] === 'object') 339 | done(Tagmate.filterOptions(defaults.sources[tok], term)); 340 | else if (typeof defaults.sources[tok] === 'function') 341 | defaults.sources[tok]({term:term}, done); 342 | else if (typeof defaults.sources[tok] === 'string') 343 | $.getJSON(defaults.sources[tok], {term:term}, function(res) { 344 | done(res.options); 345 | }); 346 | else 347 | done(); 348 | })(function(options) { 349 | if (options && options.length > 0) { 350 | // Update and show the menu 351 | update_menu(menu, options); 352 | menu.css("top", (textarea.outerHeight() - 1) + "px"); 353 | menu.show(); 354 | 355 | // Store for parse_tags() 356 | var _sources = textarea.data("_tagmate_sources"); 357 | for (var i = 0; i < options.length; i++) { 358 | var found = false; 359 | for (var j = 0; !found && j < _sources[tok].length; j++) 360 | found = _sources[tok][j].value == options[i].value; 361 | if (!found) 362 | _sources[tok].push(options[i]); 363 | } 364 | } 365 | 366 | // Fire callback if available 367 | if (tag && defaults.capture_tag) 368 | defaults.capture_tag(tag); 369 | }); 370 | } 371 | } 372 | } 373 | 374 | var ignore_keyup = false; 375 | 376 | // Check for tags on keyup, focus and click 377 | $(textarea) 378 | .unbind('.tagmate') 379 | .bind('focus.tagmate', function(e) { 380 | tag_check(); 381 | }) 382 | .bind('blur.tagmate', function(e) { 383 | // blur on textarea fires before mouse menu click 384 | setTimeout(function() { menu.hide(); }, 300); 385 | }) 386 | .bind('click.tagmate', function(e) { 387 | tag_check(); 388 | }) 389 | .bind('keydown.tagmate', function(e) { 390 | if (menu.is(":visible")) { 391 | if (e.keyCode == 40) { // down 392 | scroll_menu(menu, "down"); 393 | ignore_keyup = true; 394 | return false; 395 | } else if (e.keyCode == 38) { // up 396 | scroll_menu(menu, "up"); 397 | ignore_keyup = true; 398 | return false; 399 | } else if (e.keyCode == 13) { // enter 400 | var value = menu.children("." + defaults.menu_option_active_class).text(); 401 | var tag = parse_tag(textarea); 402 | if (tag && value) { 403 | replace_tag(textarea, tag, value); 404 | menu.hide(); 405 | ignore_keyup = true; 406 | return false; 407 | } 408 | } else if (e.keyCode == 27) { // escape 409 | menu.hide(); 410 | ignore_keyup = true; 411 | return false; 412 | } 413 | } 414 | }) 415 | .bind('keyup.tagmate', function(e) { 416 | if (ignore_keyup) { 417 | ignore_keyup = false; 418 | return true; 419 | } 420 | tag_check(); 421 | 422 | if (hiliter) 423 | update_hiliter(textarea, hiliter); 424 | }); 425 | 426 | // Mouse menu activation 427 | //menu.find("." + defaults.menu_option_class) // Doesn't work 428 | $("." + defaults.menu_class + " ." + defaults.menu_option_class) 429 | .die("click.tagmate") 430 | .live("click.tagmate", function() { 431 | var value = $(this).text(); 432 | var tag = parse_tag(textarea); 433 | replace_tag(textarea, tag, value); 434 | textarea.keyup(); 435 | }); 436 | }); 437 | } 438 | }); 439 | })(jQuery); --------------------------------------------------------------------------------