├── README.md ├── index.js ├── lib └── nodestream │ └── index.js └── public └── nodestream.js /README.md: -------------------------------------------------------------------------------- 1 | Nodestream 2 | ========== 3 | 4 | Nodestream aims to make the most common use cases for realtime web applications easy to implement, by connecting event listeners to template rendering. 5 | 6 | ## Example 7 | 8 | ### Use-case 9 | 10 | Consider the following example, written in Express. The following two routes show and save items respectively 11 | 12 | app.get('/', function(req, res){ 13 | database.getItems(function(items){ 14 | res.render('my.items.jade', {locals: { items: items }}) 15 | }) 16 | }) 17 | 18 | app.post('/save/item', function(req, res){ 19 | database.saveItem(req.body, function(){ 20 | res.redirect('/'); 21 | }) 22 | }) 23 | 24 | And the following template displays them 25 | 26 | - each item in items 27 | div.item 28 | p This is an item titled #{item.title} 29 | 30 | When a user loads the page, he'll see all the items that are in the database at that particular moment. When he adds one, he'll be redirected and the page will be refreshed entirely. 31 | 32 | ### The need for realtime 33 | 34 | Even though this model works fine for really simple applications, reloading the page after a save is often undesirable. Developers usually turn to ajax, which means after a save a chunk of html is taken and appended it to the list. 35 | 36 | This approach has drawbacks: 37 | 38 | - If a user has multiple tabs open with your applicaiton, only one tab will be updated. 39 | 40 | - If multiple users are editing concurrently, only your own changes are shown, but not changes from others. 41 | 42 | ### The nodestream solution 43 | 44 | Building on top of the powerful jade template engine filters, nodestream solves the problems listed above by making only two changes to your applications: 45 | 46 | 1) Firing events 47 | 48 | database.saveItem(item){ 49 | ... 50 | nodestream.emit('newitem', item) 51 | } 52 | 53 | 2) Adding a line of code to your template (`:realtime`) 54 | 55 | :realtime(append: 'newitem', local: 'items', obj: 'item') 56 | div.item 57 | p This is an item titled #{item.title} 58 | 59 | ### How to use 60 | 61 | 1. Include [`socket.io`](http://github.com/learnboost/socket.io-node) and `nodestream`: 62 | 63 | var io = require('socket.io'); 64 | require('nodestream'); 65 | 66 | 2. Attach `socket.io` to your `http.Server` and load nodestream 67 | 68 | var nodestream = io.listen(httpserver).nodestream(); 69 | 70 | 3. In the client side, load `public/nodestream.js` and attach it to your `io.Socket` 71 | 72 | var mySocket = new io.Socket(): 73 | mySocket.connect(); 74 | Nodestream(mySocket); 75 | 76 | 4. In your code, fire events and edit your templates! 77 | 78 | ### API 79 | 80 | The `:realtime` filter takes the following options 81 | 82 | `local`: The local variable passed to your template that your piece of html interacts with 83 | 84 | `obj`: Required for `append`, the name of the local used when the collection is looped through. 85 | 86 | `append`: Append event 87 | 88 | `repaint`: Repaint event name. If it finished on `.`, the `id` property is appended to it for every item on a collection (dynamic event name) 89 | 90 | `remove`: Remove event name. If it finished on `.`, the `id` property is appended to it for every item on a collection (dynamic event name) 91 | 92 | `id`: The name of the key to look for an unique identifier to append to events finished in `.` 93 | 94 | ### Example 95 | 96 | #### Showing how many users are browsing your website 97 | 98 | On the server side: 99 | 100 | var connections = 0; 101 | var nodestream = io.listen(app).nodestream() 102 | .on('connect', function(){ 103 | connections++; 104 | this.emit('connections', connections); 105 | }) 106 | .on('disconnect', function(){ 107 | connections--; 108 | this.emit('connections', connections); 109 | }); 110 | 111 | On the client side: 112 | 113 | :realtime(repaint: 'connections', local: 'connections') 114 | .connections 115 | - if (connections > 1) 116 | p #{connections} people are editing right now 117 | - else 118 | p You're all alone, loser 119 | 120 | ### Credits 121 | 122 | Guillermo Rauch <guillermo@learnboost.com> ([guille](http://github.com/guille)) 123 | 124 | ### License 125 | 126 | (The MIT License) 127 | 128 | Copyright (c) 2010 LearnBoost <dev@learnboost.com> 129 | 130 | Permission is hereby granted, free of charge, to any person obtaining 131 | a copy of this software and associated documentation files (the 132 | 'Software'), to deal in the Software without restriction, including 133 | without limitation the rights to use, copy, modify, merge, publish, 134 | distribute, sublicense, and/or sell copies of the Software, and to 135 | permit persons to whom the Software is furnished to do so, subject to 136 | the following conditions: 137 | 138 | The above copyright notice and this permission notice shall be 139 | included in all copies or substantial portions of the Software. 140 | 141 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 142 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 143 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 144 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 145 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 146 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 147 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/nodestream'); -------------------------------------------------------------------------------- /lib/nodestream/index.js: -------------------------------------------------------------------------------- 1 | // pending subscriptions maps a hash of the session id, filename and block id 2 | // to 3 | var pendingSubscriptions = {}, 4 | subscriptions = {}, 5 | fileBlocks = {}; 6 | 7 | var sys = require('sys'); 8 | 9 | // socket.io additions 10 | 11 | function subscribe(client, nodeid, ev, type, file, blockIndex, local, sessionId){ 12 | if (!(ev in subscriptions)) subscriptions[ev] = []; 13 | if (!(ev in client._nodestreamCleanup)) client._nodestreamCleanup[ev] = []; 14 | client._nodestreamCleanup[ev].push(subscriptions.length); 15 | subscriptions[ev].push({ 16 | nodeId: nodeid 17 | , type: type 18 | , clientId: client.sessionId 19 | , file: file 20 | , blockIndex: blockIndex 21 | , local: local 22 | , sessionId: sessionId 23 | }); 24 | }; 25 | 26 | function paint(type, filename, index, local, obj, sessionId){ 27 | var locals = {}; 28 | locals[local] = obj; 29 | locals._ = {}; 30 | locals.attrs = attrs; 31 | locals.escape = escape 32 | // fake a request 33 | var req = new Request(); 34 | req.sessionId = sessionId; 35 | var oldSubscribe = req.subscribe; 36 | // do not resubscribe to append 37 | req.subscribe = function(file, blockIndex, append, repaint, remove, local){ 38 | return oldSubscribe.call(req, file, blockIndex, '', repaint, remove, local); 39 | }; 40 | return fileBlocks[filename][index][type].call(req, locals); 41 | }; 42 | 43 | var Listener = require('socket.io').Listener; 44 | 45 | Listener.prototype.nodestream = function(){ 46 | if (!this._nodestream){ 47 | // prevent double-attachment 48 | this._nodestream = new Nodestream(this); 49 | } 50 | return this._nodestream; 51 | }; 52 | 53 | var Nodestream = function(socket){ 54 | this.socket = socket; 55 | var self = this; 56 | socket.on('connection', function(c){ 57 | c.on('message', function(msg){ 58 | if (typeof msg == 'object' && 'nodestream' in msg){ 59 | self._handle(c, msg); 60 | if (!c._nodestream){ 61 | self._emit('connect', c); 62 | c._nodestream = true; 63 | } 64 | } 65 | }); 66 | c.on('disconnect', function(){ 67 | if (c._nodestream){ 68 | self._end(c); 69 | } 70 | }); 71 | }); 72 | }; 73 | 74 | sys.inherits(Nodestream, process.EventEmitter); 75 | 76 | Nodestream.prototype._emit = Nodestream.prototype.emit; 77 | 78 | Nodestream.prototype._handle = function(client, message){ 79 | if (!('_nodestreamCleanup' in client)) client._nodestreamCleanup = {}; 80 | // we receive a hash that confirms the suscription of the client 81 | // to the events stored by that hash 82 | if (message.subscribe){ 83 | // lookup pending subscriptions by hash 84 | var subscription = pendingSubscriptions[message.subscribe]; 85 | 86 | if (subscription){ 87 | if (subscription[2] && subscription[2].length > 1) 88 | subscribe(client, message.subscribe, subscription[2], 'append', subscription[0], subscription[1], subscription[5], subscription[6]); 89 | if (subscription[3] && subscription[3].length > 1) 90 | subscribe(client, message.subscribe, subscription[3], 'repaint', subscription[0], subscription[1], subscription[5], subscription[6]); 91 | if (subscription[4] && subscription[4].length > 1) 92 | subscribe(client, message.subscribe, subscription[4], 'remove', subscription[0], subscription[1], null, subscription[6]); 93 | 94 | delete pendingSubscriptions[message.subscribe]; 95 | } else { 96 | console.error('cant find subscription by encoded id ' + message.subscribe); 97 | } 98 | } 99 | }; 100 | 101 | Nodestream.prototype._end = function(client){ 102 | for (var ev in client._nodestreamCleanup){ 103 | (function(ev){ 104 | client._nodestreamCleanup[ev].forEach(function(i){ 105 | subscriptions[ev][i] = null; 106 | }); 107 | })(ev); 108 | } 109 | this._emit('disconnect', client); 110 | }; 111 | 112 | Nodestream.prototype.emit = function(ev, obj){ 113 | // notify suscriptors 114 | var socket = this.socket; 115 | if (ev in subscriptions){ 116 | subscriptions[ev].forEach(function(s){ 117 | if (!s) return; 118 | if (socket.clients[s.clientId]){ 119 | var args = {id: s.nodeId}; 120 | 121 | if (s.type == 'repaint' || s.type == 'append'){ 122 | args.html = paint(s.type, s.file, s.blockIndex, s.local, obj, s.sessionId); 123 | } 124 | 125 | socket.clients[s.clientId].send({ 126 | nodestream: 1, 127 | type: s.type, 128 | args: args 129 | }) 130 | } 131 | }); 132 | } 133 | }; 134 | 135 | // express request 136 | 137 | var crypto = require('crypto') 138 | , Request = require('http').IncomingMessage; 139 | 140 | function md5(str) { 141 | return crypto.createHash('md5').update(str).digest('hex'); 142 | } 143 | 144 | Request.prototype.subscribe = function(file, blockIndex, append, repaint, remove, local){ 145 | if (!('_pendingSubscriptions' in this)){ 146 | this._pendingSubscriptions = {}; 147 | } 148 | 149 | var hash = md5(file + blockIndex + this.sessionId + append + repaint + remove) 150 | , random = md5(hash + Math.random()); 151 | 152 | if (hash in this._pendingSubscriptions){ 153 | return this._pendingSubscriptions[hash]; 154 | } 155 | 156 | pendingSubscriptions[random] = Array.prototype.slice.call(arguments).concat(this.sessionId); 157 | this._pendingSubscriptions[hash] = random; 158 | return random; 159 | }; 160 | 161 | // filters 162 | var jade = require('jade') 163 | , Compiler = jade.Compiler 164 | , filters = jade.filters 165 | , nodes = jade.nodes; 166 | 167 | filters.realtime = function(block, compiler, attrs){ 168 | var placeholder, filename = compiler.options.filename; 169 | 170 | if (!(filename in fileBlocks)) fileBlocks[filename] = []; 171 | 172 | block.forEach(function(node, i){ 173 | if (node.name == 'placeholder'){ 174 | placeholder = new nodes.Tag('div', node.block); 175 | placeholder.setAttribute('class', '"placeholder"') 176 | block.splice(i, 1); 177 | } 178 | }); 179 | 180 | if (!attrs.local){ 181 | throw new Error('Please pass the `local` to the :realtime filter options'); 182 | } 183 | 184 | if (!attrs.append && !attrs.obj){ 185 | attrs.obj = attrs.local; 186 | } 187 | 188 | var events = ['undefined', attrs.repaint || 'undefined', attrs.remove || 'undefined']; 189 | 190 | events.forEach(function(name, i){ 191 | // append '+ obj.id' to events finishing in a dot 192 | if (/\.'$/.test(name)){ 193 | events[i] = name + (' + ' + (eval(attrs.obj) || 'obj') + '.' + (eval(attrs.id) || 'id')); 194 | } 195 | }); 196 | 197 | // wrapper tag 198 | // actually: there's no need to wrap! we could inspect the `class` of the main node, or we could make it an option 199 | var subscribeString = 'this.subscribe("'+ filename +'", '+ fileBlocks[filename].length + ', ' + events.join(',') + ', '+ (attrs.obj || '"obj"') +')'; 200 | 201 | block[0].setAttribute('class', block[0].getAttribute('class') + ' + "nodestream nodestream_" + ' + subscribeString); 202 | block.push(new nodes.Filter('javascript', new nodes.Text('Nodestream.register(\'#{'+ subscribeString +'}\');'))); 203 | 204 | var compiled = {}; 205 | 206 | if (attrs.append){ 207 | var bl = new nodes.Block() 208 | , ifNode = new nodes.Code('if ('+ eval(attrs.local) +'.length)') 209 | , forEachBlock = new nodes.Block() 210 | , forEach = new nodes.Each(eval(attrs.local), eval(attrs.obj) || 'obj', undefined, block) 211 | , elseNode = new nodes.Code('else') 212 | , elseBlock = new nodes.Block(); 213 | 214 | forEachBlock.push(forEach); 215 | ifNode.block = forEachBlock; 216 | 217 | if (placeholder){ 218 | elseBlock.push(placeholder); 219 | elseNode.block = elseBlock; 220 | } 221 | 222 | bl.push(ifNode); 223 | bl.push(elseNode); 224 | 225 | var cc = new Compiler(block); 226 | compiled['append'] = new Function('locals', 'with (locals) {' + cc.compile() + '}'); 227 | 228 | // create a fake invisible element that serves as an indicator of where the parent container is 229 | events = [attrs.append, 'undefined', 'undefined']; 230 | subscribeString = 'this.subscribe("'+ filename +'", '+ fileBlocks[filename].length + ', ' + events.join(',') + ', '+ (attrs.obj || '"obj"') +')'; 231 | var appendPlaceholder = new nodes.Tag('div'); 232 | appendPlaceholder.setAttribute('class', '"nodestream nodestream_" + ' + subscribeString); 233 | bl.push(appendPlaceholder); 234 | bl.push(new nodes.Filter('javascript', new nodes.Text('Nodestream.register(\'#{'+ subscribeString +'}\');'))); 235 | } 236 | 237 | if (attrs.repaint){ 238 | var cc = new Compiler(block[0].block); 239 | compiled['repaint'] = new Function('locals', 'with (locals) {' + cc.compile() + '}'); 240 | } 241 | 242 | fileBlocks[filename].push(compiled); 243 | 244 | if (attrs.append){ 245 | compiler.visit(bl); 246 | } else { 247 | compiler.visit(block); 248 | } 249 | }; 250 | 251 | function attrs(obj){ 252 | var buf = [], 253 | terse = obj.terse; 254 | delete obj.terse; 255 | var keys = Object.keys(obj), 256 | len = keys.length; 257 | if (len) { 258 | buf.push(''); 259 | for (var i = 0; i < len; ++i) { 260 | var key = keys[i], 261 | val = obj[key]; 262 | if (typeof val === 'boolean' || val === '' || val == null) { 263 | if (val) { 264 | terse 265 | ? buf.push(key) 266 | : buf.push(key + '="' + key + '"'); 267 | } 268 | } else { 269 | buf.push(key + '="' + escape(val) + '"'); 270 | } 271 | } 272 | } 273 | return buf.join(' '); 274 | } 275 | 276 | /** 277 | * Escape the given string of `html`. 278 | * 279 | * @param {String} html 280 | * @return {String} 281 | * @api private 282 | */ 283 | 284 | function escape(html){ 285 | return String(html) 286 | .replace(/&(?!\w+;)/g, '&') 287 | .replace(//g, '>') 289 | .replace(/"/g, '"'); 290 | } -------------------------------------------------------------------------------- /public/nodestream.js: -------------------------------------------------------------------------------- 1 | (function($){ 2 | 3 | var socket, 4 | subscriptions = []; 5 | 6 | var oldMessage = io.Socket.prototype._onMessage; 7 | 8 | io.Socket.prototype._onMessage = function(msg){ 9 | if (typeof msg == 'object' && 'nodestream' in msg){ 10 | Nodestream.handle(msg.type, msg.args); 11 | } else { 12 | oldMessage.apply(this, arguments); 13 | } 14 | }; 15 | 16 | Nodestream = function(s){ 17 | socket = s; 18 | socket.on('connect', function(){ 19 | $(function(){ 20 | var name; 21 | while(name = subscriptions.shift()){ 22 | socket.send({ 23 | nodestream: 1, 24 | subscribe: name 25 | }); 26 | } 27 | }); 28 | }) 29 | }; 30 | 31 | Nodestream.register = function(name){ 32 | if (socket){ 33 | socket.send({ 34 | nodestream: 1, 35 | subscribe: name 36 | }); 37 | } else { 38 | subscriptions.push(name); 39 | } 40 | }; 41 | 42 | Nodestream.handle = function(type, args){ 43 | Nodestream.actions[type].call(this, args); 44 | }; 45 | 46 | Nodestream.subscribe = function(name, type){ 47 | subscriptions.push([name, type]); 48 | }; 49 | 50 | Nodestream.actions = { 51 | 52 | remove: function(args){ 53 | $('.nodestream_' + args.id).highlightFade({ complete: function(){ 54 | $('.nodestream_' + args.id).remove(); 55 | }}); 56 | var parent = $('.nodestream_' + args.id).parent() 57 | , siblings = parent.children(); 58 | if (siblings.length == 0){ 59 | parent.find('.placeholder').css('display', 'block'); 60 | } 61 | }, 62 | 63 | repaint: function(args){ 64 | $('.nodestream_' + args.id).html(args.html).highlightFade(); 65 | }, 66 | 67 | append: function(args){ 68 | $('.nodestream_' + args.id).parent().find('.placeholder').css('display', 'none'); 69 | if (args.bottom){ 70 | $('.nodestream_' + args.id).parent().append(args.html).children(':last').highlightFade(); 71 | } else { 72 | $('.nodestream_' + args.id).parent().prepend(args.html).children(':first').highlightFade(); 73 | } 74 | } 75 | 76 | } 77 | 78 | })(jQuery); 79 | 80 | /** 81 | * jQuery Plugin highlightFade (jquery.offput.ca/highlightFade) 82 | * (c) 2006 Blair Mitchelmore (offput.ca) blair@offput.ca 83 | */ 84 | /** 85 | * This is version 0.7 of my highlightFade plugin. It follows the yellow fade technique of Web 2.0 fame 86 | * but expands it to allow any starting colour and allows you to specify the end colour as well. 87 | * 88 | * For the moment, I'm done with this plug-in. Unless I come upon a really cool feature it should have 89 | * this plug-in will only receive updates to ensure future compatibility with jQuery. 90 | * 91 | * As of now (Aug. 16, 2006) the plugin has been written with the 1.0.1 release of jQuery (rev 249) which 92 | * is available from http://jquery.com/src/jquery-1.0.1.js 93 | * 94 | * A note regarding rgb() syntax: I noticed that most browsers implement rgb syntax as either an integer 95 | * (0-255) or percentage (0-100%) value for each field, that is, rgb(i/p,i/p,i/p); however, the W3C 96 | * standard clearly defines it as "either three integer values or three percentage values" [http://www.w3.org/TR/CSS21/syndata.html] 97 | * which I choose to follow despite the error redundancy of the typical behaviour browsers employ. 98 | * 99 | * Changelog: 100 | * 101 | * 0.7: 102 | * - Added the awesome custom attribute support written by George Adamson (slightly modified) 103 | * - Removed bgColor plugin dependency seeing as attr is customizable now... 104 | * 0.6: 105 | * - Abstracted getBGColor into its own plugin with optional test and data retrieval functions 106 | * - Converted all $ references to jQuery references as John's code seems to be shifting away 107 | * from that and I don't want to have to update this for a long time. 108 | * 0.5: 109 | * - Added simple argument syntax for only specifying start colour of event 110 | * - Removed old style argument syntax 111 | * - Added 'interval', 'final, and 'end' properties 112 | * - Renamed 'color' property to 'start' 113 | * - Added second argument to $.highlightFade.getBGColor to bypass the e.highlighting check 114 | * 0.4: 115 | * - Added rgb(%,%,%) color syntax 116 | * 0.3: 117 | * - Fixed bug when event was called while parent was also running event corrupting the 118 | * the background colour of the child 119 | * 0.2: 120 | * - Fixed bug where an unspecified onComplete function made the page throw continuous errors 121 | * - Fixed bug where multiple events on the same element would speed each subsequent event 122 | * 0.1: 123 | * - Initial Release 124 | * 125 | * @author Blair Mitchelmore (blair@offput.ca) 126 | * @version 0.5 127 | */ 128 | jQuery.fn.highlightFade = function(settings) { 129 | var o = (settings && settings.constructor == String) ? {start: settings} : settings || {}; 130 | var d = jQuery.highlightFade.defaults; 131 | var i = o['interval'] || d['interval']; 132 | var a = o['attr'] || d['attr']; 133 | var ts = { 134 | 'linear': function(s,e,t,c) { return parseInt(s+(c/t)*(e-s)); }, 135 | 'sinusoidal': function(s,e,t,c) { return parseInt(s+Math.sin(((c/t)*90)*(Math.PI/180))*(e-s)); }, 136 | 'exponential': function(s,e,t,c) { return parseInt(s+(Math.pow(c/t,2))*(e-s)); } 137 | }; 138 | var t = (o['iterator'] && o['iterator'].constructor == Function) ? o['iterator'] : ts[o['iterator']] || ts[d['iterator']] || ts['linear']; 139 | if (d['iterator'] && d['iterator'].constructor == Function) t = d['iterator']; 140 | return this.each(function() { 141 | if (!this.highlighting) this.highlighting = {}; 142 | var e = (this.highlighting[a]) ? this.highlighting[a].end : jQuery.highlightFade.getBaseValue(this,a) || [255,255,255]; 143 | var c = jQuery.highlightFade.getRGB(o['start'] || o['colour'] || o['color'] || d['start'] || [255,255,128]); 144 | var s = jQuery.speed(o['speed'] || d['speed']); 145 | var r = o['final'] || (this.highlighting[a] && this.highlighting[a].orig) ? this.highlighting[a].orig : jQuery.curCSS(this,a); 146 | if (o['end'] || d['end']) r = jQuery.highlightFade.asRGBString(e = jQuery.highlightFade.getRGB(o['end'] || d['end'])); 147 | if (typeof o['final'] != 'undefined') r = o['final']; 148 | if (this.highlighting[a] && this.highlighting[a].timer) window.clearInterval(this.highlighting[a].timer); 149 | this.highlighting[a] = { steps: ((s.duration) / i), interval: i, currentStep: 0, start: c, end: e, orig: r, attr: a }; 150 | jQuery.highlightFade(this,a,o['complete'],t); 151 | }); 152 | }; 153 | 154 | jQuery.highlightFade = function(e,a,o,t) { 155 | e.highlighting[a].timer = window.setInterval(function() { 156 | var newR = t(e.highlighting[a].start[0],e.highlighting[a].end[0],e.highlighting[a].steps,e.highlighting[a].currentStep); 157 | var newG = t(e.highlighting[a].start[1],e.highlighting[a].end[1],e.highlighting[a].steps,e.highlighting[a].currentStep); 158 | var newB = t(e.highlighting[a].start[2],e.highlighting[a].end[2],e.highlighting[a].steps,e.highlighting[a].currentStep); 159 | jQuery(e).css(a,jQuery.highlightFade.asRGBString([newR,newG,newB])); 160 | if (e.highlighting[a].currentStep++ >= e.highlighting[a].steps) { 161 | jQuery(e).css(a,e.highlighting[a].orig || ''); 162 | window.clearInterval(e.highlighting[a].timer); 163 | e.highlighting[a] = null; 164 | if (o && o.constructor == Function) o.call(e); 165 | } 166 | },e.highlighting[a].interval); 167 | }; 168 | 169 | jQuery.highlightFade.defaults = { 170 | start: [255,255,128], 171 | interval: 50, 172 | speed: 400, 173 | attr: 'backgroundColor' 174 | }; 175 | 176 | jQuery.highlightFade.getRGB = function(c,d) { 177 | var result; 178 | if (c && c.constructor == Array && c.length == 3) return c; 179 | if (result = /rgb\(\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*,\s*([0-9]{1,3})\s*\)/.exec(c)) 180 | return [parseInt(result[1]),parseInt(result[2]),parseInt(result[3])]; 181 | else if (result = /rgb\(\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*,\s*([0-9]+(?:\.[0-9]+)?)\%\s*\)/.exec(c)) 182 | return [parseFloat(result[1])*2.55,parseFloat(result[2])*2.55,parseFloat(result[3])*2.55]; 183 | else if (result = /#([a-fA-F0-9]{2})([a-fA-F0-9]{2})([a-fA-F0-9]{2})/.exec(c)) 184 | return [parseInt("0x" + result[1]),parseInt("0x" + result[2]),parseInt("0x" + result[3])]; 185 | else if (result = /#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/.exec(c)) 186 | return [parseInt("0x"+ result[1] + result[1]),parseInt("0x" + result[2] + result[2]),parseInt("0x" + result[3] + result[3])]; 187 | else 188 | return jQuery.highlightFade.checkColorName(c) || d || null; 189 | }; 190 | 191 | jQuery.highlightFade.asRGBString = function(a) { 192 | return "rgb(" + a.join(",") + ")"; 193 | }; 194 | 195 | jQuery.highlightFade.getBaseValue = function(e,a,b) { 196 | var s, t; 197 | b = b || false; 198 | t = a = a || jQuery.highlightFade.defaults['attr']; 199 | do { 200 | s = jQuery(e).css(t || 'backgroundColor'); 201 | if ((s != '' && s != 'transparent') || (e.tagName.toLowerCase() == "body") || (!b && e.highlighting && e.highlighting[a] && e.highlighting[a].end)) break; 202 | t = false; 203 | } while (e = e.parentNode); 204 | if (!b && e.highlighting && e.highlighting[a] && e.highlighting[a].end) s = e.highlighting[a].end; 205 | if (s == undefined || s == '' || s == 'transparent') s = [255,255,255]; 206 | return jQuery.highlightFade.getRGB(s); 207 | }; 208 | 209 | jQuery.highlightFade.checkColorName = function(c) { 210 | if (!c) return null; 211 | switch(c.replace(/^\s*|\s*$/g,'').toLowerCase()) { 212 | case 'aqua': return [0,255,255]; 213 | case 'black': return [0,0,0]; 214 | case 'blue': return [0,0,255]; 215 | case 'fuchsia': return [255,0,255]; 216 | case 'gray': return [128,128,128]; 217 | case 'green': return [0,128,0]; 218 | case 'lime': return [0,255,0]; 219 | case 'maroon': return [128,0,0]; 220 | case 'navy': return [0,0,128]; 221 | case 'olive': return [128,128,0]; 222 | case 'purple': return [128,0,128]; 223 | case 'red': return [255,0,0]; 224 | case 'silver': return [192,192,192]; 225 | case 'teal': return [0,128,128]; 226 | case 'white': return [255,255,255]; 227 | case 'yellow': return [255,255,0]; 228 | } 229 | }; --------------------------------------------------------------------------------