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 */ 40 Node.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 */ 131 Node.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 */ 147 Node.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 159 Element.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 */ 171 Element.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 */ 191 Element.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 */ 203 function 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 */ 217 function 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 */ 237 Constraints.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 */ 246 Constraints.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 */ 257 Constraints.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 */ 268 Constraints.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 */ 279 Constraints.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 */ 290 Element.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 */ 305 Element.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 */ 316 Node.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 */ 331 Node.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 */ 341 Node.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 */ 354 Element.prototype.query = function(query) 355 { 356 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 357 } 358 359 Element.prototype.removeChildren = function() 360 { 361 if (this.firstChild) 362 this.textContent = ""; 363 } 364 365 Element.prototype.appendChildren = function(children) 366 { 367 for (var i = 0; i < children.length; ++i) 368 this.appendChild(children[i]); 369 } 370 371 Element.prototype.setChildren = function(children) 372 { 373 this.removeChildren(); 374 this.appendChildren(children); 375 } 376 377 /** 378 * @return {boolean} 379 */ 380 Element.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 */ 394 Document.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 */ 407 Element.prototype.createChild = function(elementName, className) 408 { 409 var element = this.ownerDocument.createElementWithClass(elementName, className); 410 this.appendChild(element); 411 return element; 412 } 413 414 DocumentFragment.prototype.createChild = Element.prototype.createChild; 415 416 /** 417 * @param {string} text 418 * @return {!Text} 419 */ 420 Element.prototype.createTextChild = function(text) 421 { 422 var element = this.ownerDocument.createTextNode(text); 423 this.appendChild(element); 424 return element; 425 } 426 427 DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild; 428 429 /** 430 * @return {number} 431 */ 432 Element.prototype.totalOffsetLeft = function() 433 { 434 return this.totalOffset().left; 435 } 436 437 /** 438 * @return {number} 439 */ 440 Element.prototype.totalOffsetTop = function() 441 { 442 return this.totalOffset().top; 443 444 } 445 446 /** 447 * @return {!{left: number, top: number}} 448 */ 449 Element.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 */ 458 Element.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 */ 476 function 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 */ 488 AnchorBox.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 */ 498 AnchorBox.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 */ 507 AnchorBox.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 */ 516 Element.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 */ 538 Element.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 */ 552 Element.prototype.setTextAndTitle = function(text) 553 { 554 this.textContent = text; 555 this.title = text; 556 } 557 558 KeyboardEvent.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 */ 580 Event.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 */ 593 Text.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 */ 613 Element.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 */ 639 Node.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 */ 657 Node.prototype.isDescendant = function(descendant) 658 { 659 return !!descendant && descendant.isAncestor(this); 660 } 661 662 /** 663 * @param {?Node} node 664 * @return {boolean} 665 */ 666 Node.prototype.isSelfOrAncestor = function(node) 667 { 668 return !!node && (node === this || this.isAncestor(node)); 669 } 670 671 /** 672 * @param {?Node} node 673 * @return {boolean} 674 */ 675 Node.prototype.isSelfOrDescendant = function(node) 676 { 677 return !!node && (node === this || this.isDescendant(node)); 678 } 679 680 /** 681 * @param {!Node=} stayWithin 682 * @return {?Node} 683 */ 684 Node.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 */ 710 Node.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 */ 727 Node.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 */ 746 function isEnterKey(event) { 747 // Check if in IME. 748 return event.keyCode !== 229 && event.keyIdentifier === "Enter"; 749 } 750 751 function consumeEvent(e) 752 { 753 e.consume(); 754 } 755