- 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 = "
" + 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);
--------------------------------------------------------------------------------