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