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 */ 36Node.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 123Node.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 135Node.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 */ 150Element.prototype.removeStyleClass = function(className) 151{ 152 this.classList.remove(className); 153} 154 155Element.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 */ 165Element.prototype.addStyleClass = function(className) 166{ 167 this.classList.add(className); 168} 169 170/** 171 * @param {string} className 172 * @return {boolean} 173 */ 174Element.prototype.hasStyleClass = function(className) 175{ 176 return this.classList.contains(className); 177} 178 179/** 180 * @param {string} className 181 * @param {*} enable 182 */ 183Element.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 */ 195Element.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 208Element.prototype.isScrolledToBottom = function() 209{ 210 // This code works only for 0-width border 211 return this.scrollTop + this.clientHeight === this.scrollHeight; 212} 213 214Element.prototype.removeSelf = function() 215{ 216 if (this.parentElement) 217 this.parentElement.removeChild(this); 218} 219 220CharacterData.prototype.removeSelf = Element.prototype.removeSelf; 221DocumentType.prototype.removeSelf = Element.prototype.removeSelf; 222 223/** 224 * @param {Node} fromNode 225 * @param {Node} toNode 226 */ 227function removeSubsequentNodes(fromNode, toNode) 228{ 229 for (var node = fromNode; node && node !== toNode; ) { 230 var nodeToRemove = node; 231 node = node.nextSibling; 232 nodeToRemove.removeSelf(); 233 } 234} 235 236/** 237 * @constructor 238 * @param {number} width 239 * @param {number} height 240 */ 241function Size(width, height) 242{ 243 this.width = width; 244 this.height = height; 245} 246 247/** 248 * @param {Element=} containerElement 249 * @return {Size} 250 */ 251Element.prototype.measurePreferredSize = function(containerElement) 252{ 253 containerElement = containerElement || document.body; 254 containerElement.appendChild(this); 255 this.positionAt(0, 0); 256 var result = new Size(this.offsetWidth, this.offsetHeight); 257 this.positionAt(undefined, undefined); 258 this.removeSelf(); 259 return result; 260} 261 262Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray) 263{ 264 for (var node = this; node && node !== this.ownerDocument; node = node.parentNode) 265 for (var i = 0; i < nameArray.length; ++i) 266 if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase()) 267 return node; 268 return null; 269} 270 271Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 272{ 273 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 274} 275 276/** 277 * @param {string} className 278 * @param {Element=} stayWithin 279 */ 280Node.prototype.enclosingNodeOrSelfWithClass = function(className, stayWithin) 281{ 282 for (var node = this; node && node !== stayWithin && node !== this.ownerDocument; node = node.parentNode) 283 if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className)) 284 return node; 285 return null; 286} 287 288Element.prototype.query = function(query) 289{ 290 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 291} 292 293Element.prototype.removeChildren = function() 294{ 295 if (this.firstChild) 296 this.textContent = ""; 297} 298 299Element.prototype.isInsertionCaretInside = function() 300{ 301 var selection = window.getSelection(); 302 if (!selection.rangeCount || !selection.isCollapsed) 303 return false; 304 var selectionRange = selection.getRangeAt(0); 305 return selectionRange.startContainer.isSelfOrDescendant(this); 306} 307 308/** 309 * @param {string=} className 310 */ 311Element.prototype.createChild = function(elementName, className) 312{ 313 var element = this.ownerDocument.createElement(elementName); 314 if (className) 315 element.className = className; 316 this.appendChild(element); 317 return element; 318} 319 320DocumentFragment.prototype.createChild = Element.prototype.createChild; 321 322/** 323 * @param {string} text 324 */ 325Element.prototype.createTextChild = function(text) 326{ 327 var element = this.ownerDocument.createTextNode(text); 328 this.appendChild(element); 329 return element; 330} 331 332DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild; 333 334/** 335 * @return {number} 336 */ 337Element.prototype.totalOffsetLeft = function() 338{ 339 return this.totalOffset().left; 340} 341 342/** 343 * @return {number} 344 */ 345Element.prototype.totalOffsetTop = function() 346{ 347 return this.totalOffset().top; 348 349} 350 351Element.prototype.totalOffset = function() 352{ 353 var totalLeft = 0; 354 var totalTop = 0; 355 356 for (var element = this; element; element = element.offsetParent) { 357 totalLeft += element.offsetLeft; 358 totalTop += element.offsetTop; 359 if (this !== element) { 360 totalLeft += element.clientLeft - element.scrollLeft; 361 totalTop += element.clientTop - element.scrollTop; 362 } 363 } 364 365 return { left: totalLeft, top: totalTop }; 366} 367 368Element.prototype.scrollOffset = function() 369{ 370 var curLeft = 0; 371 var curTop = 0; 372 for (var element = this; element; element = element.scrollParent) { 373 curLeft += element.scrollLeft; 374 curTop += element.scrollTop; 375 } 376 return { left: curLeft, top: curTop }; 377} 378 379/** 380 * @constructor 381 * @param {number=} x 382 * @param {number=} y 383 * @param {number=} width 384 * @param {number=} height 385 */ 386function AnchorBox(x, y, width, height) 387{ 388 this.x = x || 0; 389 this.y = y || 0; 390 this.width = width || 0; 391 this.height = height || 0; 392} 393 394/** 395 * @param {Window} targetWindow 396 * @return {AnchorBox} 397 */ 398Element.prototype.offsetRelativeToWindow = function(targetWindow) 399{ 400 var elementOffset = new AnchorBox(); 401 var curElement = this; 402 var curWindow = this.ownerDocument.defaultView; 403 while (curWindow && curElement) { 404 elementOffset.x += curElement.totalOffsetLeft(); 405 elementOffset.y += curElement.totalOffsetTop(); 406 if (curWindow === targetWindow) 407 break; 408 409 curElement = curWindow.frameElement; 410 curWindow = curWindow.parent; 411 } 412 413 return elementOffset; 414} 415 416/** 417 * @param {Window} targetWindow 418 * @return {AnchorBox} 419 */ 420Element.prototype.boxInWindow = function(targetWindow) 421{ 422 targetWindow = targetWindow || this.ownerDocument.defaultView; 423 424 var anchorBox = this.offsetRelativeToWindow(window); 425 anchorBox.width = Math.min(this.offsetWidth, window.innerWidth - anchorBox.x); 426 anchorBox.height = Math.min(this.offsetHeight, window.innerHeight - anchorBox.y); 427 428 return anchorBox; 429} 430 431/** 432 * @param {string} text 433 */ 434Element.prototype.setTextAndTitle = function(text) 435{ 436 this.textContent = text; 437 this.title = text; 438} 439 440KeyboardEvent.prototype.__defineGetter__("data", function() 441{ 442 // Emulate "data" attribute from DOM 3 TextInput event. 443 // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data 444 switch (this.type) { 445 case "keypress": 446 if (!this.ctrlKey && !this.metaKey) 447 return String.fromCharCode(this.charCode); 448 else 449 return ""; 450 case "keydown": 451 case "keyup": 452 if (!this.ctrlKey && !this.metaKey && !this.altKey) 453 return String.fromCharCode(this.which); 454 else 455 return ""; 456 } 457}); 458 459/** 460 * @param {boolean=} preventDefault 461 */ 462Event.prototype.consume = function(preventDefault) 463{ 464 this.stopImmediatePropagation(); 465 if (preventDefault) 466 this.preventDefault(); 467 this.handled = true; 468} 469 470Text.prototype.select = function(start, end) 471{ 472 start = start || 0; 473 end = end || this.textContent.length; 474 475 if (start < 0) 476 start = end + start; 477 478 var selection = this.ownerDocument.defaultView.getSelection(); 479 selection.removeAllRanges(); 480 var range = this.ownerDocument.createRange(); 481 range.setStart(this, start); 482 range.setEnd(this, end); 483 selection.addRange(range); 484 return this; 485} 486 487Element.prototype.selectionLeftOffset = function() 488{ 489 // Calculate selection offset relative to the current element. 490 491 var selection = window.getSelection(); 492 if (!selection.containsNode(this, true)) 493 return null; 494 495 var leftOffset = selection.anchorOffset; 496 var node = selection.anchorNode; 497 498 while (node !== this) { 499 while (node.previousSibling) { 500 node = node.previousSibling; 501 leftOffset += node.textContent.length; 502 } 503 node = node.parentNode; 504 } 505 506 return leftOffset; 507} 508 509Node.prototype.isAncestor = function(node) 510{ 511 if (!node) 512 return false; 513 514 var currentNode = node.parentNode; 515 while (currentNode) { 516 if (this === currentNode) 517 return true; 518 currentNode = currentNode.parentNode; 519 } 520 return false; 521} 522 523Node.prototype.isDescendant = function(descendant) 524{ 525 return !!descendant && descendant.isAncestor(this); 526} 527 528Node.prototype.isSelfOrAncestor = function(node) 529{ 530 return !!node && (node === this || this.isAncestor(node)); 531} 532 533Node.prototype.isSelfOrDescendant = function(node) 534{ 535 return !!node && (node === this || this.isDescendant(node)); 536} 537 538Node.prototype.traverseNextNode = function(stayWithin) 539{ 540 var node = this.firstChild; 541 if (node) 542 return node; 543 544 if (stayWithin && this === stayWithin) 545 return null; 546 547 node = this.nextSibling; 548 if (node) 549 return node; 550 551 node = this; 552 while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin)) 553 node = node.parentNode; 554 if (!node) 555 return null; 556 557 return node.nextSibling; 558} 559 560Node.prototype.traversePreviousNode = function(stayWithin) 561{ 562 if (stayWithin && this === stayWithin) 563 return null; 564 var node = this.previousSibling; 565 while (node && node.lastChild) 566 node = node.lastChild; 567 if (node) 568 return node; 569 return this.parentNode; 570} 571 572function isEnterKey(event) { 573 // Check if in IME. 574 return event.keyCode !== 229 && event.keyIdentifier === "Enter"; 575} 576 577function consumeEvent(e) 578{ 579 e.consume(); 580} 581 582/** 583 * Mutation observers leak memory. Keep track of them and disconnect 584 * on unload. 585 * @constructor 586 * @param {function(Array.<WebKitMutation>)} handler 587 */ 588function NonLeakingMutationObserver(handler) 589{ 590 this._observer = new WebKitMutationObserver(handler); 591 NonLeakingMutationObserver._instances.push(this); 592 if (!NonLeakingMutationObserver._unloadListener) { 593 NonLeakingMutationObserver._unloadListener = function() { 594 while (NonLeakingMutationObserver._instances.length) 595 NonLeakingMutationObserver._instances[NonLeakingMutationObserver._instances.length - 1].disconnect(); 596 }; 597 window.addEventListener("unload", NonLeakingMutationObserver._unloadListener, false); 598 } 599} 600 601NonLeakingMutationObserver._instances = []; 602 603NonLeakingMutationObserver.prototype = { 604 /** 605 * @param {Element} element 606 * @param {Object} config 607 */ 608 observe: function(element, config) 609 { 610 if (this._observer) 611 this._observer.observe(element, config); 612 }, 613 614 disconnect: function() 615 { 616 if (this._observer) 617 this._observer.disconnect(); 618 NonLeakingMutationObserver._instances.remove(this); 619 delete this._observer; 620 } 621} 622 623