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 /** 160 * @param {number|undefined} x 161 * @param {number|undefined} y 162 * @param {!Element=} relativeTo 163 */ 164 Element.prototype.positionAt = function(x, y, relativeTo) 165 { 166 var shift = {x: 0, y: 0}; 167 if (relativeTo) 168 shift = relativeTo.boxInWindow(this.ownerDocument.defaultView); 169 170 if (typeof x === "number") 171 this.style.setProperty("left", (shift.x + x) + "px"); 172 else 173 this.style.removeProperty("left"); 174 175 if (typeof y === "number") 176 this.style.setProperty("top", (shift.y + y) + "px"); 177 else 178 this.style.removeProperty("top"); 179 } 180 181 /** 182 * @return {boolean} 183 */ 184 Element.prototype.isScrolledToBottom = function() 185 { 186 // This code works only for 0-width border. 187 // Both clientHeight and scrollHeight are rounded to integer values, so we tolerate 188 // one pixel error. 189 return Math.abs(this.scrollTop + this.clientHeight - this.scrollHeight) <= 1; 190 } 191 192 /** 193 * @param {!Node} fromNode 194 * @param {!Node} toNode 195 */ 196 function removeSubsequentNodes(fromNode, toNode) 197 { 198 for (var node = fromNode; node && node !== toNode; ) { 199 var nodeToRemove = node; 200 node = node.nextSibling; 201 nodeToRemove.remove(); 202 } 203 } 204 205 /** 206 * @constructor 207 * @param {!Size} minimum 208 * @param {?Size=} preferred 209 */ 210 function Constraints(minimum, preferred) 211 { 212 /** 213 * @type {!Size} 214 */ 215 this.minimum = minimum; 216 217 /** 218 * @type {!Size} 219 */ 220 this.preferred = preferred || minimum; 221 222 if (this.minimum.width > this.preferred.width || this.minimum.height > this.preferred.height) 223 throw new Error("Minimum size is greater than preferred."); 224 } 225 226 /** 227 * @param {?Constraints} constraints 228 * @return {boolean} 229 */ 230 Constraints.prototype.isEqual = function(constraints) 231 { 232 return !!constraints && this.minimum.isEqual(constraints.minimum) && this.preferred.isEqual(constraints.preferred); 233 } 234 235 /** 236 * @param {!Constraints|number} value 237 * @return {!Constraints} 238 */ 239 Constraints.prototype.widthToMax = function(value) 240 { 241 if (typeof value === "number") 242 return new Constraints(this.minimum.widthToMax(value), this.preferred.widthToMax(value)); 243 return new Constraints(this.minimum.widthToMax(value.minimum), this.preferred.widthToMax(value.preferred)); 244 } 245 246 /** 247 * @param {!Constraints|number} value 248 * @return {!Constraints} 249 */ 250 Constraints.prototype.addWidth = function(value) 251 { 252 if (typeof value === "number") 253 return new Constraints(this.minimum.addWidth(value), this.preferred.addWidth(value)); 254 return new Constraints(this.minimum.addWidth(value.minimum), this.preferred.addWidth(value.preferred)); 255 } 256 257 /** 258 * @param {!Constraints|number} value 259 * @return {!Constraints} 260 */ 261 Constraints.prototype.heightToMax = function(value) 262 { 263 if (typeof value === "number") 264 return new Constraints(this.minimum.heightToMax(value), this.preferred.heightToMax(value)); 265 return new Constraints(this.minimum.heightToMax(value.minimum), this.preferred.heightToMax(value.preferred)); 266 } 267 268 /** 269 * @param {!Constraints|number} value 270 * @return {!Constraints} 271 */ 272 Constraints.prototype.addHeight = function(value) 273 { 274 if (typeof value === "number") 275 return new Constraints(this.minimum.addHeight(value), this.preferred.addHeight(value)); 276 return new Constraints(this.minimum.addHeight(value.minimum), this.preferred.addHeight(value.preferred)); 277 } 278 279 /** 280 * @param {?Element=} containerElement 281 * @return {!Size} 282 */ 283 Element.prototype.measurePreferredSize = function(containerElement) 284 { 285 containerElement = containerElement || document.body; 286 containerElement.appendChild(this); 287 this.positionAt(0, 0); 288 var result = new Size(this.offsetWidth, this.offsetHeight); 289 this.positionAt(undefined, undefined); 290 this.remove(); 291 return result; 292 } 293 294 /** 295 * @param {!Event} event 296 * @return {boolean} 297 */ 298 Element.prototype.containsEventPoint = function(event) 299 { 300 var box = this.getBoundingClientRect(); 301 return box.left < event.x && event.x < box.right && 302 box.top < event.y && event.y < box.bottom; 303 } 304 305 /** 306 * @param {!Array.<string>} nameArray 307 * @return {?Node} 308 */ 309 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray) 310 { 311 for (var node = this; node && node !== this.ownerDocument; node = node.parentNode) { 312 for (var i = 0; i < nameArray.length; ++i) { 313 if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase()) 314 return node; 315 } 316 } 317 return null; 318 } 319 320 /** 321 * @param {string} nodeName 322 * @return {?Node} 323 */ 324 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName) 325 { 326 return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]); 327 } 328 329 /** 330 * @param {string} className 331 * @param {!Element=} stayWithin 332 * @return {?Element} 333 */ 334 Node.prototype.enclosingNodeOrSelfWithClass = function(className, stayWithin) 335 { 336 for (var node = this; node && node !== stayWithin && node !== this.ownerDocument; node = node.parentNode) { 337 if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(className)) 338 return /** @type {!Element} */ (node); 339 } 340 return null; 341 } 342 343 /** 344 * @param {string} query 345 * @return {?Node} 346 */ 347 Element.prototype.query = function(query) 348 { 349 return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue; 350 } 351 352 Element.prototype.removeChildren = function() 353 { 354 if (this.firstChild) 355 this.textContent = ""; 356 } 357 358 /** 359 * @return {boolean} 360 */ 361 Element.prototype.isInsertionCaretInside = function() 362 { 363 var selection = window.getSelection(); 364 if (!selection.rangeCount || !selection.isCollapsed) 365 return false; 366 var selectionRange = selection.getRangeAt(0); 367 return selectionRange.startContainer.isSelfOrDescendant(this); 368 } 369 370 /** 371 * @param {string} elementName 372 * @param {string=} className 373 * @return {!Element} 374 */ 375 Document.prototype.createElementWithClass = function(elementName, className) 376 { 377 var element = this.createElement(elementName); 378 if (className) 379 element.className = className; 380 return element; 381 } 382 383 /** 384 * @param {string} elementName 385 * @param {string=} className 386 * @return {!Element} 387 */ 388 Element.prototype.createChild = function(elementName, className) 389 { 390 var element = this.ownerDocument.createElementWithClass(elementName, className); 391 this.appendChild(element); 392 return element; 393 } 394 395 DocumentFragment.prototype.createChild = Element.prototype.createChild; 396 397 /** 398 * @param {string} text 399 * @return {!Text} 400 */ 401 Element.prototype.createTextChild = function(text) 402 { 403 var element = this.ownerDocument.createTextNode(text); 404 this.appendChild(element); 405 return element; 406 } 407 408 DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild; 409 410 /** 411 * @param {...string} var_args 412 */ 413 Element.prototype.createTextChildren = function(var_args) 414 { 415 for (var i = 0, n = arguments.length; i < n; ++i) 416 this.createTextChild(arguments[i]); 417 } 418 419 DocumentFragment.prototype.createTextChildren = Element.prototype.createTextChildren; 420 421 /** 422 * @param {...!Element} var_args 423 */ 424 Element.prototype.appendChildren = function(var_args) 425 { 426 for (var i = 0, n = arguments.length; i < n; ++i) 427 this.appendChild(arguments[i]); 428 } 429 430 /** 431 * @return {number} 432 */ 433 Element.prototype.totalOffsetLeft = function() 434 { 435 return this.totalOffset().left; 436 } 437 438 /** 439 * @return {number} 440 */ 441 Element.prototype.totalOffsetTop = function() 442 { 443 return this.totalOffset().top; 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