| 1 | /* |
|---|
| 2 | DOM Proxy JavaScript Library |
|---|
| 3 | $Id$ |
|---|
| 4 | |
|---|
| 5 | Copyright (c) 2006, Six Apart, Ltd. |
|---|
| 6 | All rights reserved. |
|---|
| 7 | |
|---|
| 8 | Redistribution and use in source and binary forms, with or without |
|---|
| 9 | modification, are permitted provided that the following conditions are |
|---|
| 10 | met: |
|---|
| 11 | |
|---|
| 12 | * Redistributions of source code must retain the above copyright |
|---|
| 13 | notice, this list of conditions and the following disclaimer. |
|---|
| 14 | |
|---|
| 15 | * Redistributions in binary form must reproduce the above |
|---|
| 16 | copyright notice, this list of conditions and the following disclaimer |
|---|
| 17 | in the documentation and/or other materials provided with the |
|---|
| 18 | distribution. |
|---|
| 19 | |
|---|
| 20 | * Neither the name of "Six Apart" nor the names of its |
|---|
| 21 | contributors may be used to endorse or promote products derived from |
|---|
| 22 | this software without specific prior written permission. |
|---|
| 23 | |
|---|
| 24 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS |
|---|
| 25 | "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT |
|---|
| 26 | LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR |
|---|
| 27 | A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT |
|---|
| 28 | OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
|---|
| 29 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT |
|---|
| 30 | LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, |
|---|
| 31 | DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY |
|---|
| 32 | THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT |
|---|
| 33 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE |
|---|
| 34 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
|---|
| 35 | */ |
|---|
| 36 | |
|---|
| 37 | |
|---|
| 38 | DOM.Proxy = new Class( Object, { |
|---|
| 39 | |
|---|
| 40 | LEADING: -1, |
|---|
| 41 | INITIAL: 0, |
|---|
| 42 | TRAILING: 1, |
|---|
| 43 | |
|---|
| 44 | |
|---|
| 45 | /** |
|---|
| 46 | * class <code>DOM.Proxy</code> |
|---|
| 47 | * Initializes a document object model (dom) node, for proxying by this class. |
|---|
| 48 | * @param node <code>Node</code> A dom node. |
|---|
| 49 | */ |
|---|
| 50 | init: function( startNode ) { |
|---|
| 51 | this.startNode = startNode; |
|---|
| 52 | this.update( startNode ); |
|---|
| 53 | }, |
|---|
| 54 | |
|---|
| 55 | |
|---|
| 56 | /** |
|---|
| 57 | * class <code>DOM.Proxy</code> |
|---|
| 58 | * Updates a node with either of the supplied parameters, and adds references onto the proxy |
|---|
| 59 | * that refer to dom node properties. |
|---|
| 60 | * @param node <code>Node</code> OPTIONAL |
|---|
| 61 | * @param edge <code>integer</code> OPTIONAL |
|---|
| 62 | * @return <code>Node</code> The dom node. |
|---|
| 63 | */ |
|---|
| 64 | update: function( node, edge ) { |
|---|
| 65 | if( !node ) |
|---|
| 66 | return; |
|---|
| 67 | |
|---|
| 68 | this.node = node || this.node; |
|---|
| 69 | this.edge = edge || this.INITIAL; |
|---|
| 70 | |
|---|
| 71 | this.nodeType = this.node.nodeType || 0; |
|---|
| 72 | this.nodeName = this.node.nodeName || ""; |
|---|
| 73 | this.nodeValue = this.node.nodeValue || ""; |
|---|
| 74 | this.tagName = this.node.tagName || ""; |
|---|
| 75 | this.attributes = this.node.attributes || []; |
|---|
| 76 | this.parentNode = this.node.parentNode; |
|---|
| 77 | this.previousSibling = this.node.previousSibling; |
|---|
| 78 | this.nextSibling = this.node.nextSibling; |
|---|
| 79 | this.firstChild = this.node.firstChild; |
|---|
| 80 | this.lastChild = this.node.lastChild; |
|---|
| 81 | this.childNodes = this.node.childNodes || []; |
|---|
| 82 | this.empty = false; |
|---|
| 83 | |
|---|
| 84 | return node; |
|---|
| 85 | }, |
|---|
| 86 | |
|---|
| 87 | |
|---|
| 88 | /** |
|---|
| 89 | * class <code>DOM.Proxy</code> |
|---|
| 90 | * Gets the previous node in the current walk of the tree. |
|---|
| 91 | * @return <code>Node</code> A dom node OR <code>undefined</code> |
|---|
| 92 | */ |
|---|
| 93 | getPreviousNode: function() { |
|---|
| 94 | if( !this.node ) |
|---|
| 95 | return undefined; |
|---|
| 96 | |
|---|
| 97 | if( this.lastChild && this.edge != this.LEADING && !this.empty ) |
|---|
| 98 | return this.update( this.lastChild, this.TRAILING ); // down |
|---|
| 99 | |
|---|
| 100 | if( this.previousSibling ) |
|---|
| 101 | return this.update( this.previousSibling, this.TRAILING ); // left |
|---|
| 102 | |
|---|
| 103 | if( this.parentNode ) |
|---|
| 104 | return this.update( this.parentNode, this.LEADING ); // up |
|---|
| 105 | |
|---|
| 106 | return undefined; // borked |
|---|
| 107 | }, |
|---|
| 108 | |
|---|
| 109 | |
|---|
| 110 | /** |
|---|
| 111 | * class <code>DOM.Proxy</code> |
|---|
| 112 | * Gets the previous node in the current walk of the tree. |
|---|
| 113 | * @return <code>Node</code> A dom node OR <code>undefined</code> |
|---|
| 114 | */ |
|---|
| 115 | getNextNode: function() { |
|---|
| 116 | if( !this.node ) |
|---|
| 117 | return undefined; |
|---|
| 118 | |
|---|
| 119 | if( this.firstChild && this.edge != this.TRAILING && !this.empty ) |
|---|
| 120 | return this.update( this.firstChild, this.LEADING ); // down |
|---|
| 121 | |
|---|
| 122 | if( this.nextSibling ) |
|---|
| 123 | return this.update( this.nextSibling, this.LEADING ); // right |
|---|
| 124 | |
|---|
| 125 | if( this.parentNode ) |
|---|
| 126 | return this.update( this.parentNode, this.TRAILING ); // up |
|---|
| 127 | |
|---|
| 128 | return undefined; // borked |
|---|
| 129 | }, |
|---|
| 130 | |
|---|
| 131 | |
|---|
| 132 | /** |
|---|
| 133 | * class <code>DOM.Proxy</code> |
|---|
| 134 | * Generate the markup needed to describe the node (including its subtree, if any). |
|---|
| 135 | * @return <code>string</code> The markup representation of this node. |
|---|
| 136 | */ |
|---|
| 137 | toSource: function() { |
|---|
| 138 | return this.serialize.apply( this, arguments ); |
|---|
| 139 | }, |
|---|
| 140 | |
|---|
| 141 | |
|---|
| 142 | /** |
|---|
| 143 | * class <code>DOM.Proxy</code> |
|---|
| 144 | * Generate the markup needed to describe the node (including its subtree, if any). |
|---|
| 145 | * @return <code>string</code> The markup representation of this node. |
|---|
| 146 | */ |
|---|
| 147 | serialize: function() { |
|---|
| 148 | switch( this.nodeType ) { |
|---|
| 149 | case Node.TEXT_NODE: |
|---|
| 150 | return this.serializeTextNode(); |
|---|
| 151 | |
|---|
| 152 | case Node.COMMENT_NODE: |
|---|
| 153 | return this.serializeComment(); |
|---|
| 154 | |
|---|
| 155 | case Node.ELEMENT_NODE: |
|---|
| 156 | return this.serializeElement(); |
|---|
| 157 | |
|---|
| 158 | default: |
|---|
| 159 | log( "Unknown nodeType: " + this.nodeType ); |
|---|
| 160 | } |
|---|
| 161 | return ""; |
|---|
| 162 | }, |
|---|
| 163 | |
|---|
| 164 | |
|---|
| 165 | /** |
|---|
| 166 | * class <code>DOM.Proxy</code> |
|---|
| 167 | * Serialize the child nodes of this node, if any. |
|---|
| 168 | * @return <code>string</code> The markup representation of the child nodes of this node. |
|---|
| 169 | */ |
|---|
| 170 | serializeChildNodes: function() { |
|---|
| 171 | var source = ""; |
|---|
| 172 | for( var i = 0; i < this.childNodes.length; i++ ) { |
|---|
| 173 | var proxy = new this.constructor( this.childNodes[ i ] ); |
|---|
| 174 | source += proxy.serialize(); |
|---|
| 175 | } |
|---|
| 176 | return source; |
|---|
| 177 | }, |
|---|
| 178 | |
|---|
| 179 | |
|---|
| 180 | /** |
|---|
| 181 | * class <code>DOM.Proxy</code> |
|---|
| 182 | * Serialize this text node. |
|---|
| 183 | * @return <code>string</code> The markup representation of this value of this text node. |
|---|
| 184 | */ |
|---|
| 185 | serializeTextNode: function() { |
|---|
| 186 | return this.nodeValue.encodeHTML(); |
|---|
| 187 | }, |
|---|
| 188 | |
|---|
| 189 | |
|---|
| 190 | /** |
|---|
| 191 | * class <code>DOM.Proxy</code> |
|---|
| 192 | * Serialize this comment node. |
|---|
| 193 | * @return <code>string</code> The markup representation of this value of this comment node |
|---|
| 194 | * (i.e., the value enclosed between '<!' and '>', in addition to the standard xml |
|---|
| 195 | * comment delimeter ('--') ). |
|---|
| 196 | */ |
|---|
| 197 | serializeComment: function() { |
|---|
| 198 | return "<!-- " + this.nodeValue.encodeHTML() + " -->"; |
|---|
| 199 | }, |
|---|
| 200 | |
|---|
| 201 | |
|---|
| 202 | /** |
|---|
| 203 | * class <code>DOM.Proxy</code> |
|---|
| 204 | * Serialize this element node. |
|---|
| 205 | * @return <code>string</code> The markup representation of this value of this element. |
|---|
| 206 | */ |
|---|
| 207 | serializeElement: function() { |
|---|
| 208 | if( !this.tagName ) |
|---|
| 209 | return ""; |
|---|
| 210 | |
|---|
| 211 | var source = "<" + this.tagName; |
|---|
| 212 | source += this.serializeAttributes(); |
|---|
| 213 | |
|---|
| 214 | if( this.empty ) |
|---|
| 215 | source += " />"; |
|---|
| 216 | else { |
|---|
| 217 | source += ">"; |
|---|
| 218 | source += this.serializeChildNodes(); |
|---|
| 219 | source += "</" + this.tagName + ">"; |
|---|
| 220 | } |
|---|
| 221 | |
|---|
| 222 | return source; |
|---|
| 223 | }, |
|---|
| 224 | |
|---|
| 225 | |
|---|
| 226 | /** |
|---|
| 227 | * class <code>DOM.Proxy</code> |
|---|
| 228 | * Serialize the attributes of this node. |
|---|
| 229 | * @return <code>string</code> The markup representation of these attributes. |
|---|
| 230 | */ |
|---|
| 231 | serializeAttributes: function() { |
|---|
| 232 | var source = "", value; |
|---|
| 233 | for( var i = 0; i < this.attributes.length; i++ ) { |
|---|
| 234 | var name = this.attributes[ i ].nodeName; |
|---|
| 235 | value = this.attributes[ i ].nodeValue; |
|---|
| 236 | if ( exists( value ) && typeof value != "boolean" ) |
|---|
| 237 | value = value.toString().encodeHTML(); |
|---|
| 238 | else |
|---|
| 239 | value = ""; |
|---|
| 240 | source += " " + name + "=\"" + value + "\""; |
|---|
| 241 | } |
|---|
| 242 | return source; |
|---|
| 243 | } |
|---|
| 244 | } ); |
|---|
| 245 | |
|---|
| 246 | |
|---|
| 247 | /* static methods */ |
|---|
| 248 | |
|---|
| 249 | extend( DOM.Proxy, { |
|---|
| 250 | /** |
|---|
| 251 | * class <code>DOM.Proxy</code> |
|---|
| 252 | * This is a static method.<br/> |
|---|
| 253 | * It invokes the callback parameter on every previous node to the node parameter until a |
|---|
| 254 | * not-<code>undefined</code> return value is obtained from the callback. |
|---|
| 255 | * @param node <code>Node</code> A dom node. |
|---|
| 256 | * @param callback <code>function</code> A function / bound-method to call on every discovery of |
|---|
| 257 | * a successive, previous node. |
|---|
| 258 | * @return <code>any</code> The result of an invocation of the callback OR <code>undefined</code> |
|---|
| 259 | */ |
|---|
| 260 | forPrevious: function( node, callback ) { |
|---|
| 261 | var proxy = new this( node ); |
|---|
| 262 | while( node = proxy.getPreviousNode() ) { |
|---|
| 263 | var out = callback( node, proxy.startNode ); |
|---|
| 264 | if( defined( out ) ) |
|---|
| 265 | return out; |
|---|
| 266 | } |
|---|
| 267 | return undefined; |
|---|
| 268 | }, |
|---|
| 269 | |
|---|
| 270 | |
|---|
| 271 | /** |
|---|
| 272 | * class <code>DOM.Proxy</code> |
|---|
| 273 | * This is a static method.<br/> |
|---|
| 274 | * It invokes the callback parameter on every next node to the node parameter until a |
|---|
| 275 | * not-<code>undefined</code> return value is obtained from the callback. |
|---|
| 276 | * @param node <code>Node</code> A dom node. |
|---|
| 277 | * @param callback <code>function</code> A function / bound-method to call on completion. |
|---|
| 278 | * @return <code>any</code> The result of an invocation of the callback OR <code>undefined</code> |
|---|
| 279 | */ |
|---|
| 280 | forNext: function( node, callback ) { |
|---|
| 281 | var proxy = new this( node ); |
|---|
| 282 | while( node = proxy.getNextNode() ) { |
|---|
| 283 | var out = callback( node, proxy.startNode ); |
|---|
| 284 | if( defined( out ) ) |
|---|
| 285 | return out; |
|---|
| 286 | } |
|---|
| 287 | return undefined; |
|---|
| 288 | }, |
|---|
| 289 | |
|---|
| 290 | |
|---|
| 291 | /** |
|---|
| 292 | * class <code>DOM.Proxy</code> |
|---|
| 293 | * This is a static method.<br/> |
|---|
| 294 | * It returns the previous text node to the parameter node, if any |
|---|
| 295 | * @param node <code>Node</code> A dom node. |
|---|
| 296 | * @return <code>Node</code> OR <code>undefined</code>. |
|---|
| 297 | */ |
|---|
| 298 | getPreviousTextNode: function( node ) { |
|---|
| 299 | return this.forPrevious( node, DOM.isTextNode ); |
|---|
| 300 | }, |
|---|
| 301 | |
|---|
| 302 | |
|---|
| 303 | /** |
|---|
| 304 | * class <code>DOM.Proxy</code> |
|---|
| 305 | * This is a static method.<br/> |
|---|
| 306 | * It returns the previous inline text node to the parameter node, if any. |
|---|
| 307 | * @param node <code>Node</code> A dom node OR <code>undefined</code>. |
|---|
| 308 | * @return <code>Node</code> OR <code>undefined</code>. |
|---|
| 309 | */ |
|---|
| 310 | getPreviousInlineTextNode: function( node ) { |
|---|
| 311 | return this.forPrevious( node, DOM.isInlineTextNode ); |
|---|
| 312 | }, |
|---|
| 313 | |
|---|
| 314 | |
|---|
| 315 | /** |
|---|
| 316 | * class <code>DOM.Proxy</code> |
|---|
| 317 | * This is a static method.<br/> |
|---|
| 318 | * It returns the text node next to the parameter node, if any. |
|---|
| 319 | * @param node <code>Node</code> A dom node OR <code>undefined</code>. |
|---|
| 320 | * @return <code>Node</code> OR <code>undefined</code>. |
|---|
| 321 | */ |
|---|
| 322 | getNextTextNode: function( node ) { |
|---|
| 323 | return this.forNext( node, DOM.isTextNode ); |
|---|
| 324 | }, |
|---|
| 325 | |
|---|
| 326 | |
|---|
| 327 | /** |
|---|
| 328 | * class <code>DOM.Proxy</code> |
|---|
| 329 | * This is a static method.<br/> |
|---|
| 330 | * It returns the inline text node next to the parameter node, if any. |
|---|
| 331 | * @param node <code>Node</code> A dom node OR <code>undefined</code>. |
|---|
| 332 | * @return <code>Node</code> OR <code>undefined</code>. |
|---|
| 333 | */ |
|---|
| 334 | getNextInlineTextNode: function( node ) { |
|---|
| 335 | return this.forNext( node, DOM.isInlineTextNode ); |
|---|
| 336 | }, |
|---|
| 337 | |
|---|
| 338 | |
|---|
| 339 | /** |
|---|
| 340 | * class <code>DOM.Proxy</code> |
|---|
| 341 | * This is a static method.<br/> |
|---|
| 342 | * It searches a node and its tree, if any, for matching text in a text node value. |
|---|
| 343 | * @param node <code>Node</code> A dom node. |
|---|
| 344 | * @param text <code>string</code> The text to search for. |
|---|
| 345 | * @return <code>Object</code> A hash with property <code>node</code> set to |
|---|
| 346 | * the node where the text was found or to <code>undefined</code>, |
|---|
| 347 | * and a property <code>offset</code> |
|---|
| 348 | * set to either an <code>int</code> (position of the found text in |
|---|
| 349 | * the text node value) or to <code>0</code>. |
|---|
| 350 | */ |
|---|
| 351 | findText: function( node, text ) { |
|---|
| 352 | if( node.nodeType == Node.TEXT_NODE ) { |
|---|
| 353 | var offset = node.nodeValue.indexOf( text ); |
|---|
| 354 | if( offset >= 0 ) |
|---|
| 355 | return { node: node, offset: offset }; |
|---|
| 356 | } else if( node.nodeType == ELEMENT_NODE && node.childNodes ) { |
|---|
| 357 | for( var i = 0; i < node.childNodes.length; i++ ) { |
|---|
| 358 | var found = this.findText( node.childNodes[ i ], text ); |
|---|
| 359 | if( found.node ) |
|---|
| 360 | return found; |
|---|
| 361 | } |
|---|
| 362 | } |
|---|
| 363 | return { node: undefined, offset: 0 }; |
|---|
| 364 | }, |
|---|
| 365 | |
|---|
| 366 | |
|---|
| 367 | /** |
|---|
| 368 | * class <code>DOM.Proxy</code> |
|---|
| 369 | * This is a static method.<br/> |
|---|
| 370 | * It finds the first text node that exists in a series of text nodes that contains as much |
|---|
| 371 | * as or more text characters as specified by the parameter <code>offset</code>. |
|---|
| 372 | * @param node <code>Node</code> A dom node. |
|---|
| 373 | * @param offset <code>int</code> The number of characters to find. |
|---|
| 374 | * @return <code>Object</code> A hash with property <code>node</code> set to |
|---|
| 375 | * the node where the text position was found or to <code>undefined</code>, |
|---|
| 376 | * and a property <code>offset</code> |
|---|
| 377 | * set to either an <code>int</code> (the difference between the length of the |
|---|
| 378 | * <code>node</code> in the hash and the parameter <code>offset</code>) |
|---|
| 379 | * or to <code>0</code>. |
|---|
| 380 | */ |
|---|
| 381 | findTextPosition: function( node, offset ) { |
|---|
| 382 | var position = { node: undefined, offset: 0 }; |
|---|
| 383 | while( node ) { |
|---|
| 384 | if( node.nodeType == Node.TEXT_NODE ) { |
|---|
| 385 | position.offset += node.length; |
|---|
| 386 | var delta = position.offset - offset; |
|---|
| 387 | if( delta >= 0 ) { |
|---|
| 388 | position.node = node; |
|---|
| 389 | position.offset = node.length - delta; |
|---|
| 390 | break; |
|---|
| 391 | } |
|---|
| 392 | } |
|---|
| 393 | node = this.getNextTextNode( node ); |
|---|
| 394 | } |
|---|
| 395 | return position; |
|---|
| 396 | }, |
|---|
| 397 | |
|---|
| 398 | |
|---|
| 399 | /** |
|---|
| 400 | * class <code>DOM.Proxy</code> |
|---|
| 401 | * This is a static method.<br/> |
|---|
| 402 | * Generate the markup needed to describe the parameter <code>node</code> (including its subtree, if any). |
|---|
| 403 | * @param node <code>Node</code> A dom node. |
|---|
| 404 | * @return <code>string</code> The markup representation of this node. |
|---|
| 405 | */ |
|---|
| 406 | serialize: function( node ) { |
|---|
| 407 | var proxy = new this( node ); |
|---|
| 408 | return proxy.serialize(); |
|---|
| 409 | }, |
|---|
| 410 | |
|---|
| 411 | |
|---|
| 412 | /** |
|---|
| 413 | * class <code>DOM.Proxy</code> |
|---|
| 414 | * This is a static method.<br/> |
|---|
| 415 | * Serialize the child nodes of the parameter <code>node</code>, if any. |
|---|
| 416 | * @param node <code>Node</code> A dom node. |
|---|
| 417 | * @return <code>string</code> The markup representation of the child nodes of this node. |
|---|
| 418 | */ |
|---|
| 419 | serializeChildNodes: function( node ) { |
|---|
| 420 | var proxy = new this( node ); |
|---|
| 421 | return proxy.serializeChildNodes(); |
|---|
| 422 | } |
|---|
| 423 | } ); |
|---|