1 /* 2 * Copyright (C) 2007 Apple Inc. All rights reserved. 3 * Copyright (C) 2012 Google Inc. All rights reserved. 4 * 5 * Redistribution and use in source and binary forms, with or without 6 * modification, are permitted provided that the following conditions 7 * are met: 8 * 9 * 1. Redistributions of source code must retain the above copyright 10 * notice, this list of conditions and the following disclaimer. 11 * 2. Redistributions in binary form must reproduce the above copyright 12 * notice, this list of conditions and the following disclaimer in the 13 * documentation and/or other materials provided with the distribution. 14 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of 15 * its contributors may be used to endorse or promote products derived 16 * from this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY 19 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY 22 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 25 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 27 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 * 29 * Contains diff method based on Javascript Diff Algorithm By John Resig 30 * http://ejohn.org/files/jsdiff.js (released under the MIT license). 31 */ 32 33 /** 34 * @param {string=} direction 35 */ 36 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction) 37 { 38 var startNode; 39 var startOffset = 0; 40 var endNode; 41 var endOffset = 0; 42 43 if (!stayWithinNode) 44 stayWithinNode = this; 45 46 if (!direction || direction === "backward" || direction === "both") { 47 var node = this; 48 while (node) { 49 if (node === stayWithinNode) { 50 if (!startNode) 51 startNode = stayWithinNode; 52 break; 53 } 54 55 if (node.nodeType === Node.TEXT_NODE) { 56 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1)); 57 for (var i = start; i >= 0; --i) { 58 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { 59 startNode = node; 60 startOffset = i + 1; 61 break; 62 } 63 } 64 } 65 66 if (startNode) 67 break; 68 69 node = node.traversePreviousNode(stayWithinNode); 70 } 71 72 if (!startNode) { 73 startNode = stayWithinNode; 74 startOffset = 0; 75 } 76 } else { 77 startNode = this; 78 startOffset = offset; 79 } 80 81 if (!direction || direction === "forward" || direction === "both") { 82 node = this; 83 while (node) { 84 if (node === stayWithinNode) { 85 if (!endNode) 86 endNode = stayWithinNode; 87 break; 88 } 89 90 if (node.nodeType === Node.TEXT_NODE) { 91 var start = (node === this ? offset : 0); 92 for (var i = start; i < node.nodeValue.length; ++i) { 93 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { 94 endNode = node; 95 endOffset = i; 96 break; 97 } 98 } 99 } 100 101 if (endNode) 102 break; 103 104 node = node.traverseNextNode(stayWithinNode); 105 } 106 107 if (!endNode) { 108 endNode = stayWithinNode; 109 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length; 110 } 111 } else { 112 endNode = this; 113 endOffset = offset; 114 } 115 116 var result = this.ownerDocument.createRange(); 117 result.setStart(startNode, startOffset); 118 result.setEnd(endNode, endOffset); 119 120 return result; 121 } 122 123 Node.prototype.traverseNextTextNode = function(stayWithin) 124 { 125 var node = this.traverseNextNode(stayWithin); 126 if (!node) 127 return; 128 129 while (node && node.nodeType !== Node.TEXT_NODE) 130 node = node.traverseNextNode(stayWithin); 131 132 return node; 133 } 134 135 Node.prototype.rangeBoundaryForOffset = function(offset) 136 { 137 var node = this.traverseNextTextNode(this); 138 while (node && offset > node.nodeValue.length) { 139 offset -= node.nodeValue.length; 140 node = node.traverseNextTextNode(this); 141 } 142 if (!node) 143 return { container: this, offset: 0 }; 144 return { container: node, offset: offset }; 145 } 146 147 /** 148 * @param {string} className 149 */ 150 Element.prototype.removeStyleClass = function(className) 151 { 152 this.classList.remove(className); 153 } 154 155 Element.prototype.removeMatchingStyleClasses = function(classNameRegex) 156 { 157 var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)"); 158 if (regex.test(this.className)) 159 this.className = this.className.replace(regex, " "); 160 } 161 162 /** 163 * @param {string} className 164 */ 165 Element.prototype.addStyleClass = function(className) 166 { 167 this.classList.add(className); 168 } 169 170 /** 171 * @param {string} className 172 * @return {boolean} 173 */ 174 Element.prototype.hasStyleClass = function(className) 175 { 176 return this.classList.contains(className); 177 } 178 179 /** 180 * @param {string} className 181 * @param {*} enable 182 */ 183 Element.prototype.enableStyleClass = function(className, enable) 184 { 185 if (enable) 186 this.addStyleClass(className); 187 else 188 this.removeStyleClass(className); 189 } 190 191 /** 192 * @param {number|undefined} x 193 * @param {number|undefined} y 194 */ 195 Element.prototype.positionAt = function(x, y) 196 { 197 if (typeof x === "number") 198 this.style.setProperty("left", x + "px"); 199 else 200 this.style.removeProperty("left"); 201 202 if (typeof y === "number") 203 this.style.setProperty("top", y + "px"); 204 else 205 this.style.removeProperty("top"); 206 } 207 208 Element.prototype.isScrolledToBottom = function() 209 { 210 // This code works only for 0-width border 211 return this.scrollTop + this.clientHeight === this.scrollHeight; 212 } 213 214 /** 215 * @param {Node} fromNode 216 * @param {Node} toNode 217 */ 218 function removeSubsequentNodes(fromNode, toNode) 219 { 220 for (var node = fromNode; node && node !== toNode; ) { 221 var nodeToRemove = node; 222 node = node.nextSibling; 223 nodeToRemove.remove(); 224 } 225 } 226 227 /** 228 * @constructor 229 * @param {number} width 230 * @param {number} height 231 */ 232 function Size(width, height) 233 { 234 this.width = width; 235 this.height = height; 236 } 237 238 /** 239 * @param {Element=} containerElement 240 * @return {Size} 241 */ 242 Element.prototype.measurePreferredSize = function(containerElement) 243 { 244 containerElement = containerElement || document.body; 245 containerElement.appendChild(this); 246 this.positionAt(0, 0); 247 var result = new Size(this.offsetWidth, this.offsetHeight); 248 this.positionAt(undefined, undefined); 249 this.remove(); 250 return result; 251 } 252 253 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray) 254 { 255 for (var node = this; node && node !== this.ownerDocument; node = node.parentNode) 256 for (var i = 0; i < nameArray.length; ++i) 257 if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase()) 258 return node; 259 return null; 260 } 261 262 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 263 { 264 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 265 } 266 267 /** 268 * @param {string} className 269 * @param {Element=} stayWithin 270 */ 271 Node.prototype.enclosingNodeOrSelfWithClass = function(className, stayWithin) 272 { 273 for (var node = this; node && node !== stayWithin && node !== this.ownerDocument; node = node.parentNode) 274 if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className)) 275 return node; 276 return null; 277 } 278 279 Element.prototype.query = function(query) 280 { 281 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 282 } 283 284 Element.prototype.removeChildren = function() 285 { 286 if (this.firstChild) 287 this.textContent = ""; 288 } 289 290 Element.prototype.isInsertionCaretInside = function() 291 { 292 var selection = window.getSelection(); 293 if (!selection.rangeCount || !selection.isCollapsed) 294 return false; 295 var selectionRange = selection.getRangeAt(0); 296 return selectionRange.startContainer.isSelfOrDescendant(this); 297 } 298 299 /** 300 * @param {string=} className 301 */ 302 Element.prototype.createChild = function(elementName, className) 303 { 304 var element = this.ownerDocument.createElement(elementName); 305 if (className) 306 element.className = className; 307 this.appendChild(element); 308 return element; 309 } 310 311 DocumentFragment.prototype.createChild = Element.prototype.createChild; 312 313 /** 314 * @param {string} text 315 */ 316 Element.prototype.createTextChild = function(text) 317 { 318 var element = this.ownerDocument.createTextNode(text); 319 this.appendChild(element); 320 return element; 321 } 322 323 DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild; 324 325 /** 326 * @return {number} 327 */ 328 Element.prototype.totalOffsetLeft = function() 329 { 330 return this.totalOffset().left; 331 } 332 333 /** 334 * @return {number} 335 */ 336 Element.prototype.totalOffsetTop = function() 337 { 338 return this.totalOffset().top; 339 340 } 341 342 Element.prototype.totalOffset = function() 343 { 344 var totalLeft = 0; 345 var totalTop = 0; 346 347 for (var element = this; element; element = element.offsetParent) { 348 totalLeft += element.offsetLeft; 349 totalTop += element.offsetTop; 350 if (this !== element) { 351 totalLeft += element.clientLeft - element.scrollLeft; 352 totalTop += element.clientTop - element.scrollTop; 353 } 354 } 355 356 return { left: totalLeft, top: totalTop }; 357 } 358 359 Element.prototype.scrollOffset = function() 360 { 361 var curLeft = 0; 362 var curTop = 0; 363 for (var element = this; element; element = element.scrollParent) { 364 curLeft += element.scrollLeft; 365 curTop += element.scrollTop; 366 } 367 return { left: curLeft, top: curTop }; 368 } 369 370 /** 371 * @constructor 372 * @param {number=} x 373 * @param {number=} y 374 * @param {number=} width 375 * @param {number=} height 376 */ 377 function AnchorBox(x, y, width, height) 378 { 379 this.x = x || 0; 380 this.y = y || 0; 381 this.width = width || 0; 382 this.height = height || 0; 383 } 384 385 /** 386 * @param {Window} targetWindow 387 * @return {AnchorBox} 388 */ 389 Element.prototype.offsetRelativeToWindow = function(targetWindow) 390 { 391 var elementOffset = new AnchorBox(); 392 var curElement = this; 393 var curWindow = this.ownerDocument.defaultView; 394 while (curWindow && curElement) { 395 elementOffset.x += curElement.totalOffsetLeft(); 396 elementOffset.y += curElement.totalOffsetTop(); 397 if (curWindow === targetWindow) 398 break; 399 400 curElement = curWindow.frameElement; 401 curWindow = curWindow.parent; 402 } 403 404 return elementOffset; 405 } 406 407 /** 408 * @param {Window} targetWindow 409 * @return {AnchorBox} 410 */ 411 Element.prototype.boxInWindow = function(targetWindow) 412 { 413 targetWindow = targetWindow || this.ownerDocument.defaultView; 414 415 var anchorBox = this.offsetRelativeToWindow(window); 416 anchorBox.width = Math.min(this.offsetWidth, window.innerWidth - anchorBox.x); 417 anchorBox.height = Math.min(this.offsetHeight, window.innerHeight - anchorBox.y); 418 419 return anchorBox; 420 } 421 422 /** 423 * @param {string} text 424 */ 425 Element.prototype.setTextAndTitle = function(text) 426 { 427 this.textContent = text; 428 this.title = text; 429 } 430 431 KeyboardEvent.prototype.__defineGetter__("data", function() 432 { 433 // Emulate "data" attribute from DOM 3 TextInput event. 434 // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data 435 switch (this.type) { 436 case "keypress": 437 if (!this.ctrlKey && !this.metaKey) 438 return String.fromCharCode(this.charCode); 439 else 440 return ""; 441 case "keydown": 442 case "keyup": 443 if (!this.ctrlKey && !this.metaKey && !this.altKey) 444 return String.fromCharCode(this.which); 445 else 446 return ""; 447 } 448 }); 449 450 /** 451 * @param {boolean=} preventDefault 452 */ 453 Event.prototype.consume = function(preventDefault) 454 { 455 this.stopImmediatePropagation(); 456 if (preventDefault) 457 this.preventDefault(); 458 this.handled = true; 459 } 460 461 Text.prototype.select = function(start, end) 462 { 463 start = start || 0; 464 end = end || this.textContent.length; 465 466 if (start < 0) 467 start = end + start; 468 469 var selection = this.ownerDocument.defaultView.getSelection(); 470 selection.removeAllRanges(); 471 var range = this.ownerDocument.createRange(); 472 range.setStart(this, start); 473 range.setEnd(this, end); 474 selection.addRange(range); 475 return this; 476 } 477 478 Element.prototype.selectionLeftOffset = function() 479 { 480 // Calculate selection offset relative to the current element. 481 482 var selection = window.getSelection(); 483 if (!selection.containsNode(this, true)) 484 return null; 485 486 var leftOffset = selection.anchorOffset; 487 var node = selection.anchorNode; 488 489 while (node !== this) { 490 while (node.previousSibling) { 491 node = node.previousSibling; 492 leftOffset += node.textContent.length; 493 } 494 node = node.parentNode; 495 } 496 497 return leftOffset; 498 } 499 500 Node.prototype.isAncestor = function(node) 501 { 502 if (!node) 503 return false; 504 505 var currentNode = node.parentNode; 506 while (currentNode) { 507 if (this === currentNode) 508 return true; 509 currentNode = currentNode.parentNode; 510 } 511 return false; 512 } 513 514 Node.prototype.isDescendant = function(descendant) 515 { 516 return !!descendant && descendant.isAncestor(this); 517 } 518 519 Node.prototype.isSelfOrAncestor = function(node) 520 { 521 return !!node && (node === this || this.isAncestor(node)); 522 } 523 524 Node.prototype.isSelfOrDescendant = function(node) 525 { 526 return !!node && (node === this || this.isDescendant(node)); 527 } 528 529 Node.prototype.traverseNextNode = function(stayWithin) 530 { 531 var node = this.firstChild; 532 if (node) 533 return node; 534 535 if (stayWithin && this === stayWithin) 536 return null; 537 538 node = this.nextSibling; 539 if (node) 540 return node; 541 542 node = this; 543 while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin)) 544 node = node.parentNode; 545 if (!node) 546 return null; 547 548 return node.nextSibling; 549 } 550 551 Node.prototype.traversePreviousNode = function(stayWithin) 552 { 553 if (stayWithin && this === stayWithin) 554 return null; 555 var node = this.previousSibling; 556 while (node && node.lastChild) 557 node = node.lastChild; 558 if (node) 559 return node; 560 return this.parentNode; 561 } 562 563 function isEnterKey(event) { 564 // Check if in IME. 565 return event.keyCode !== 229 && event.keyIdentifier === "Enter"; 566 } 567 568 function consumeEvent(e) 569 { 570 e.consume(); 571 } 572 573 /** 574 * Mutation observers leak memory. Keep track of them and disconnect 575 * on unload. 576 * @constructor 577 * @param {function(Array.<WebKitMutation>)} handler 578 */ 579 function NonLeakingMutationObserver(handler) 580 { 581 this._observer = new WebKitMutationObserver(handler); 582 NonLeakingMutationObserver._instances.push(this); 583 if (!NonLeakingMutationObserver._unloadListener) { 584 NonLeakingMutationObserver._unloadListener = function() { 585 while (NonLeakingMutationObserver._instances.length) 586 NonLeakingMutationObserver._instances[NonLeakingMutationObserver._instances.length - 1].disconnect(); 587 }; 588 window.addEventListener("unload", NonLeakingMutationObserver._unloadListener, false); 589 } 590 } 591 592 NonLeakingMutationObserver._instances = []; 593 594 NonLeakingMutationObserver.prototype = { 595 /** 596 * @param {Element} element 597 * @param {Object} config 598 */ 599 observe: function(element, config) 600 { 601 if (this._observer) 602 this._observer.observe(element, config); 603 }, 604 605 disconnect: function() 606 { 607 if (this._observer) 608 this._observer.disconnect(); 609 NonLeakingMutationObserver._instances.remove(this); 610 delete this._observer; 611 } 612 } 613 614