Home | History | Annotate | Download | only in common
      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