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 Element.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 */ 158 Element.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 */ 170 Element.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 183 Element.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 */ 193 function 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 */ 207 function Size(width, height) 208 { 209 this.width = width; 210 this.height = height; 211 } 212 213 /** 214 * @param {?Element=} containerElement 215 * @return {!Size} 216 */ 217 Element.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 */ 232 Element.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 239 Node.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 248 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 249 { 250 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 251 } 252 253 /** 254 * @param {string} className 255 * @param {!Element=} stayWithin 256 */ 257 Node.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 265 Element.prototype.query = function(query) 266 { 267 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 268 } 269 270 Element.prototype.removeChildren = function() 271 { 272 if (this.firstChild) 273 this.textContent = ""; 274 } 275 276 Element.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 */ 289 Document.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 */ 300 Element.prototype.createChild = function(elementName, className) 301 { 302 var element = this.ownerDocument.createElementWithClass(elementName, className); 303 this.appendChild(element); 304 return element; 305 } 306 307 DocumentFragment.prototype.createChild = Element.prototype.createChild; 308 309 /** 310 * @param {string} text 311 */ 312 Element.prototype.createTextChild = function(text) 313 { 314 var element = this.ownerDocument.createTextNode(text); 315 this.appendChild(element); 316 return element; 317 } 318 319 DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild; 320 321 /** 322 * @return {number} 323 */ 324 Element.prototype.totalOffsetLeft = function() 325 { 326 return this.totalOffset().left; 327 } 328 329 /** 330 * @return {number} 331 */ 332 Element.prototype.totalOffsetTop = function() 333 { 334 return this.totalOffset().top; 335 336 } 337 338 Element.prototype.totalOffset = function() 339 { 340 var rect = this.getBoundingClientRect(); 341 return { left: rect.left, top: rect.top }; 342 } 343 344 Element.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 */ 362 function 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 */ 374 Element.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 */ 396 Element.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 */ 410 Element.prototype.setTextAndTitle = function(text) 411 { 412 this.textContent = text; 413 this.title = text; 414 } 415 416 KeyboardEvent.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 */ 438 Event.prototype.consume = function(preventDefault) 439 { 440 this.stopImmediatePropagation(); 441 if (preventDefault) 442 this.preventDefault(); 443 this.handled = true; 444 } 445 446 Text.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 463 Element.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 485 Node.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 499 Node.prototype.isDescendant = function(descendant) 500 { 501 return !!descendant && descendant.isAncestor(this); 502 } 503 504 Node.prototype.isSelfOrAncestor = function(node) 505 { 506 return !!node && (node === this || this.isAncestor(node)); 507 } 508 509 Node.prototype.isSelfOrDescendant = function(node) 510 { 511 return !!node && (node === this || this.isDescendant(node)); 512 } 513 514 Node.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 536 Node.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 548 function isEnterKey(event) { 549 // Check if in IME. 550 return event.keyCode !== 229 && event.keyIdentifier === "Enter"; 551 } 552 553 function 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 */ 564 function 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 577 NonLeakingMutationObserver._instances = []; 578 579 NonLeakingMutationObserver.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