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