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 {number} offset 35 * @param {string} stopCharacters 36 * @param {!Node} stayWithinNode 37 * @param {string=} direction 38 * @return {!Range} 39 */ 40Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction) 41{ 42 var startNode; 43 var startOffset = 0; 44 var endNode; 45 var endOffset = 0; 46 47 if (!stayWithinNode) 48 stayWithinNode = this; 49 50 if (!direction || direction === "backward" || direction === "both") { 51 var node = this; 52 while (node) { 53 if (node === stayWithinNode) { 54 if (!startNode) 55 startNode = stayWithinNode; 56 break; 57 } 58 59 if (node.nodeType === Node.TEXT_NODE) { 60 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1)); 61 for (var i = start; i >= 0; --i) { 62 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { 63 startNode = node; 64 startOffset = i + 1; 65 break; 66 } 67 } 68 } 69 70 if (startNode) 71 break; 72 73 node = node.traversePreviousNode(stayWithinNode); 74 } 75 76 if (!startNode) { 77 startNode = stayWithinNode; 78 startOffset = 0; 79 } 80 } else { 81 startNode = this; 82 startOffset = offset; 83 } 84 85 if (!direction || direction === "forward" || direction === "both") { 86 node = this; 87 while (node) { 88 if (node === stayWithinNode) { 89 if (!endNode) 90 endNode = stayWithinNode; 91 break; 92 } 93 94 if (node.nodeType === Node.TEXT_NODE) { 95 var start = (node === this ? offset : 0); 96 for (var i = start; i < node.nodeValue.length; ++i) { 97 if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) { 98 endNode = node; 99 endOffset = i; 100 break; 101 } 102 } 103 } 104 105 if (endNode) 106 break; 107 108 node = node.traverseNextNode(stayWithinNode); 109 } 110 111 if (!endNode) { 112 endNode = stayWithinNode; 113 endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length; 114 } 115 } else { 116 endNode = this; 117 endOffset = offset; 118 } 119 120 var result = this.ownerDocument.createRange(); 121 result.setStart(startNode, startOffset); 122 result.setEnd(endNode, endOffset); 123 124 return result; 125} 126 127/** 128 * @param {!Node=} stayWithin 129 * @return {?Node} 130 */ 131Node.prototype.traverseNextTextNode = function(stayWithin) 132{ 133 var node = this.traverseNextNode(stayWithin); 134 if (!node) 135 return null; 136 137 while (node && node.nodeType !== Node.TEXT_NODE) 138 node = node.traverseNextNode(stayWithin); 139 140 return node; 141} 142 143/** 144 * @param {number} offset 145 * @return {!{container: !Node, offset: number}} 146 */ 147Node.prototype.rangeBoundaryForOffset = function(offset) 148{ 149 var node = this.traverseNextTextNode(this); 150 while (node && offset > node.nodeValue.length) { 151 offset -= node.nodeValue.length; 152 node = node.traverseNextTextNode(this); 153 } 154 if (!node) 155 return { container: this, offset: 0 }; 156 return { container: node, offset: offset }; 157} 158 159Element.prototype.removeMatchingStyleClasses = function(classNameRegex) 160{ 161 var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)"); 162 if (regex.test(this.className)) 163 this.className = this.className.replace(regex, " "); 164} 165 166/** 167 * @param {number|undefined} x 168 * @param {number|undefined} y 169 * @param {!Element=} relativeTo 170 */ 171Element.prototype.positionAt = function(x, y, relativeTo) 172{ 173 var shift = {x: 0, y: 0}; 174 if (relativeTo) 175 shift = relativeTo.boxInWindow(this.ownerDocument.defaultView); 176 177 if (typeof x === "number") 178 this.style.setProperty("left", (shift.x + x) + "px"); 179 else 180 this.style.removeProperty("left"); 181 182 if (typeof y === "number") 183 this.style.setProperty("top", (shift.y + y) + "px"); 184 else 185 this.style.removeProperty("top"); 186} 187 188/** 189 * @return {boolean} 190 */ 191Element.prototype.isScrolledToBottom = function() 192{ 193 // This code works only for 0-width border. 194 // Both clientHeight and scrollHeight are rounded to integer values, so we tolerate 195 // one pixel error. 196 return Math.abs(this.scrollTop + this.clientHeight - this.scrollHeight) <= 1; 197} 198 199/** 200 * @param {!Node} fromNode 201 * @param {!Node} toNode 202 */ 203function removeSubsequentNodes(fromNode, toNode) 204{ 205 for (var node = fromNode; node && node !== toNode; ) { 206 var nodeToRemove = node; 207 node = node.nextSibling; 208 nodeToRemove.remove(); 209 } 210} 211 212/** 213 * @constructor 214 * @param {!Size} minimum 215 * @param {?Size=} preferred 216 */ 217function Constraints(minimum, preferred) 218{ 219 /** 220 * @type {!Size} 221 */ 222 this.minimum = minimum; 223 224 /** 225 * @type {!Size} 226 */ 227 this.preferred = preferred || minimum; 228 229 if (this.minimum.width > this.preferred.width || this.minimum.height > this.preferred.height) 230 throw new Error("Minimum size is greater than preferred."); 231} 232 233/** 234 * @param {?Constraints} constraints 235 * @return {boolean} 236 */ 237Constraints.prototype.isEqual = function(constraints) 238{ 239 return !!constraints && this.minimum.isEqual(constraints.minimum) && this.preferred.isEqual(constraints.preferred); 240} 241 242/** 243 * @param {!Constraints|number} value 244 * @return {!Constraints} 245 */ 246Constraints.prototype.widthToMax = function(value) 247{ 248 if (typeof value === "number") 249 return new Constraints(this.minimum.widthToMax(value), this.preferred.widthToMax(value)); 250 return new Constraints(this.minimum.widthToMax(value.minimum), this.preferred.widthToMax(value.preferred)); 251} 252 253/** 254 * @param {!Constraints|number} value 255 * @return {!Constraints} 256 */ 257Constraints.prototype.addWidth = function(value) 258{ 259 if (typeof value === "number") 260 return new Constraints(this.minimum.addWidth(value), this.preferred.addWidth(value)); 261 return new Constraints(this.minimum.addWidth(value.minimum), this.preferred.addWidth(value.preferred)); 262} 263 264/** 265 * @param {!Constraints|number} value 266 * @return {!Constraints} 267 */ 268Constraints.prototype.heightToMax = function(value) 269{ 270 if (typeof value === "number") 271 return new Constraints(this.minimum.heightToMax(value), this.preferred.heightToMax(value)); 272 return new Constraints(this.minimum.heightToMax(value.minimum), this.preferred.heightToMax(value.preferred)); 273} 274 275/** 276 * @param {!Constraints|number} value 277 * @return {!Constraints} 278 */ 279Constraints.prototype.addHeight = function(value) 280{ 281 if (typeof value === "number") 282 return new Constraints(this.minimum.addHeight(value), this.preferred.addHeight(value)); 283 return new Constraints(this.minimum.addHeight(value.minimum), this.preferred.addHeight(value.preferred)); 284} 285 286/** 287 * @param {?Element=} containerElement 288 * @return {!Size} 289 */ 290Element.prototype.measurePreferredSize = function(containerElement) 291{ 292 containerElement = containerElement || document.body; 293 containerElement.appendChild(this); 294 this.positionAt(0, 0); 295 var result = new Size(this.offsetWidth, this.offsetHeight); 296 this.positionAt(undefined, undefined); 297 this.remove(); 298 return result; 299} 300 301/** 302 * @param {!Event} event 303 * @return {boolean} 304 */ 305Element.prototype.containsEventPoint = function(event) 306{ 307 var box = this.getBoundingClientRect(); 308 return box.left < event.x && event.x < box.right && 309 box.top < event.y && event.y < box.bottom; 310} 311 312/** 313 * @param {!Array.<string>} nameArray 314 * @return {?Node} 315 */ 316Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray) 317{ 318 for (var node = this; node && node !== this.ownerDocument; node = node.parentNode) { 319 for (var i = 0; i < nameArray.length; ++i) { 320 if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase()) 321 return node; 322 } 323 } 324 return null; 325} 326 327/** 328 * @param {string} nodeName 329 * @return {?Node} 330 */ 331Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 332{ 333 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 334} 335 336/** 337 * @param {string} className 338 * @param {!Element=} stayWithin 339 * @return {?Element} 340 */ 341Node.prototype.enclosingNodeOrSelfWithClass = function(className, stayWithin) 342{ 343 for (var node = this; node && node !== stayWithin && node !== this.ownerDocument; node = node.parentNode) { 344 if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(className)) 345 return /** @type {!Element} */ (node); 346 } 347 return null; 348} 349 350/** 351 * @param {string} query 352 * @return {?Node} 353 */ 354Element.prototype.query = function(query) 355{ 356 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 357} 358 359Element.prototype.removeChildren = function() 360{ 361 if (this.firstChild) 362 this.textContent = ""; 363} 364 365Element.prototype.appendChildren = function(children) 366{ 367 for (var i = 0; i < children.length; ++i) 368 this.appendChild(children[i]); 369} 370 371Element.prototype.setChildren = function(children) 372{ 373 this.removeChildren(); 374 this.appendChildren(children); 375} 376 377/** 378 * @return {boolean} 379 */ 380Element.prototype.isInsertionCaretInside = function() 381{ 382 var selection = window.getSelection(); 383 if (!selection.rangeCount || !selection.isCollapsed) 384 return false; 385 var selectionRange = selection.getRangeAt(0); 386 return selectionRange.startContainer.isSelfOrDescendant(this); 387} 388 389/** 390 * @param {string} elementName 391 * @param {string=} className 392 * @return {!Element} 393 */ 394Document.prototype.createElementWithClass = function(elementName, className) 395{ 396 var element = this.createElement(elementName); 397 if (className) 398 element.className = className; 399 return element; 400} 401 402/** 403 * @param {string} elementName 404 * @param {string=} className 405 * @return {!Element} 406 */ 407Element.prototype.createChild = function(elementName, className) 408{ 409 var element = this.ownerDocument.createElementWithClass(elementName, className); 410 this.appendChild(element); 411 return element; 412} 413 414DocumentFragment.prototype.createChild = Element.prototype.createChild; 415 416/** 417 * @param {string} text 418 * @return {!Text} 419 */ 420Element.prototype.createTextChild = function(text) 421{ 422 var element = this.ownerDocument.createTextNode(text); 423 this.appendChild(element); 424 return element; 425} 426 427DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild; 428 429/** 430 * @return {number} 431 */ 432Element.prototype.totalOffsetLeft = function() 433{ 434 return this.totalOffset().left; 435} 436 437/** 438 * @return {number} 439 */ 440Element.prototype.totalOffsetTop = function() 441{ 442 return this.totalOffset().top; 443 444} 445 446/** 447 * @return {!{left: number, top: number}} 448 */ 449Element.prototype.totalOffset = function() 450{ 451 var rect = this.getBoundingClientRect(); 452 return { left: rect.left, top: rect.top }; 453} 454 455/** 456 * @return {!{left: number, top: number}} 457 */ 458Element.prototype.scrollOffset = function() 459{ 460 var curLeft = 0; 461 var curTop = 0; 462 for (var element = this; element; element = element.scrollParent) { 463 curLeft += element.scrollLeft; 464 curTop += element.scrollTop; 465 } 466 return { left: curLeft, top: curTop }; 467} 468 469/** 470 * @constructor 471 * @param {number=} x 472 * @param {number=} y 473 * @param {number=} width 474 * @param {number=} height 475 */ 476function AnchorBox(x, y, width, height) 477{ 478 this.x = x || 0; 479 this.y = y || 0; 480 this.width = width || 0; 481 this.height = height || 0; 482} 483 484/** 485 * @param {!AnchorBox} box 486 * @return {!AnchorBox} 487 */ 488AnchorBox.prototype.relativeTo = function(box) 489{ 490 return new AnchorBox( 491 this.x - box.x, this.y - box.y, this.width, this.height); 492} 493 494/** 495 * @param {!Element} element 496 * @return {!AnchorBox} 497 */ 498AnchorBox.prototype.relativeToElement = function(element) 499{ 500 return this.relativeTo(element.boxInWindow(element.ownerDocument.defaultView)); 501} 502 503/** 504 * @param {?AnchorBox} anchorBox 505 * @return {boolean} 506 */ 507AnchorBox.prototype.equals = function(anchorBox) 508{ 509 return !!anchorBox && this.x === anchorBox.x && this.y === anchorBox.y && this.width === anchorBox.width && this.height === anchorBox.height; 510} 511 512/** 513 * @param {!Window} targetWindow 514 * @return {!AnchorBox} 515 */ 516Element.prototype.offsetRelativeToWindow = function(targetWindow) 517{ 518 var elementOffset = new AnchorBox(); 519 var curElement = this; 520 var curWindow = this.ownerDocument.defaultView; 521 while (curWindow && curElement) { 522 elementOffset.x += curElement.totalOffsetLeft(); 523 elementOffset.y += curElement.totalOffsetTop(); 524 if (curWindow === targetWindow) 525 break; 526 527 curElement = curWindow.frameElement; 528 curWindow = curWindow.parent; 529 } 530 531 return elementOffset; 532} 533 534/** 535 * @param {!Window=} targetWindow 536 * @return {!AnchorBox} 537 */ 538Element.prototype.boxInWindow = function(targetWindow) 539{ 540 targetWindow = targetWindow || this.ownerDocument.defaultView; 541 542 var anchorBox = this.offsetRelativeToWindow(window); 543 anchorBox.width = Math.min(this.offsetWidth, window.innerWidth - anchorBox.x); 544 anchorBox.height = Math.min(this.offsetHeight, window.innerHeight - anchorBox.y); 545 546 return anchorBox; 547} 548 549/** 550 * @param {string} text 551 */ 552Element.prototype.setTextAndTitle = function(text) 553{ 554 this.textContent = text; 555 this.title = text; 556} 557 558KeyboardEvent.prototype.__defineGetter__("data", function() 559{ 560 // Emulate "data" attribute from DOM 3 TextInput event. 561 // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data 562 switch (this.type) { 563 case "keypress": 564 if (!this.ctrlKey && !this.metaKey) 565 return String.fromCharCode(this.charCode); 566 else 567 return ""; 568 case "keydown": 569 case "keyup": 570 if (!this.ctrlKey && !this.metaKey && !this.altKey) 571 return String.fromCharCode(this.which); 572 else 573 return ""; 574 } 575}); 576 577/** 578 * @param {boolean=} preventDefault 579 */ 580Event.prototype.consume = function(preventDefault) 581{ 582 this.stopImmediatePropagation(); 583 if (preventDefault) 584 this.preventDefault(); 585 this.handled = true; 586} 587 588/** 589 * @param {number=} start 590 * @param {number=} end 591 * @return {!Text} 592 */ 593Text.prototype.select = function(start, end) 594{ 595 start = start || 0; 596 end = end || this.textContent.length; 597 598 if (start < 0) 599 start = end + start; 600 601 var selection = this.ownerDocument.defaultView.getSelection(); 602 selection.removeAllRanges(); 603 var range = this.ownerDocument.createRange(); 604 range.setStart(this, start); 605 range.setEnd(this, end); 606 selection.addRange(range); 607 return this; 608} 609 610/** 611 * @return {?number} 612 */ 613Element.prototype.selectionLeftOffset = function() 614{ 615 // Calculate selection offset relative to the current element. 616 617 var selection = window.getSelection(); 618 if (!selection.containsNode(this, true)) 619 return null; 620 621 var leftOffset = selection.anchorOffset; 622 var node = selection.anchorNode; 623 624 while (node !== this) { 625 while (node.previousSibling) { 626 node = node.previousSibling; 627 leftOffset += node.textContent.length; 628 } 629 node = node.parentNode; 630 } 631 632 return leftOffset; 633} 634 635/** 636 * @param {?Node} node 637 * @return {boolean} 638 */ 639Node.prototype.isAncestor = function(node) 640{ 641 if (!node) 642 return false; 643 644 var currentNode = node.parentNode; 645 while (currentNode) { 646 if (this === currentNode) 647 return true; 648 currentNode = currentNode.parentNode; 649 } 650 return false; 651} 652 653/** 654 * @param {?Node} descendant 655 * @return {boolean} 656 */ 657Node.prototype.isDescendant = function(descendant) 658{ 659 return !!descendant && descendant.isAncestor(this); 660} 661 662/** 663 * @param {?Node} node 664 * @return {boolean} 665 */ 666Node.prototype.isSelfOrAncestor = function(node) 667{ 668 return !!node && (node === this || this.isAncestor(node)); 669} 670 671/** 672 * @param {?Node} node 673 * @return {boolean} 674 */ 675Node.prototype.isSelfOrDescendant = function(node) 676{ 677 return !!node && (node === this || this.isDescendant(node)); 678} 679 680/** 681 * @param {!Node=} stayWithin 682 * @return {?Node} 683 */ 684Node.prototype.traverseNextNode = function(stayWithin) 685{ 686 var node = this.firstChild; 687 if (node) 688 return node; 689 690 if (stayWithin && this === stayWithin) 691 return null; 692 693 node = this.nextSibling; 694 if (node) 695 return node; 696 697 node = this; 698 while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin)) 699 node = node.parentNode; 700 if (!node) 701 return null; 702 703 return node.nextSibling; 704} 705 706/** 707 * @param {!Node=} stayWithin 708 * @return {?Node} 709 */ 710Node.prototype.traversePreviousNode = function(stayWithin) 711{ 712 if (stayWithin && this === stayWithin) 713 return null; 714 var node = this.previousSibling; 715 while (node && node.lastChild) 716 node = node.lastChild; 717 if (node) 718 return node; 719 return this.parentNode; 720} 721 722/** 723 * @param {*} text 724 * @param {string=} placeholder 725 * @return {boolean} true if was truncated 726 */ 727Node.prototype.setTextContentTruncatedIfNeeded = function(text, placeholder) 728{ 729 // Huge texts in the UI reduce rendering performance drastically. 730 // Moreover, Blink/WebKit uses <unsigned short> internally for storing text content 731 // length, so texts longer than 65535 are inherently displayed incorrectly. 732 const maxTextContentLength = 65535; 733 734 if (typeof text === "string" && text.length > maxTextContentLength) { 735 this.textContent = typeof placeholder === "string" ? placeholder : text.trimEnd(maxTextContentLength); 736 return true; 737 } 738 739 this.textContent = text; 740 return false; 741} 742 743/** 744 * @return {boolean} 745 */ 746function isEnterKey(event) { 747 // Check if in IME. 748 return event.keyCode !== 229 && event.keyIdentifier === "Enter"; 749} 750 751function consumeEvent(e) 752{ 753 e.consume(); 754} 755