Home | History | Annotate | Download | only in front_end
      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 {string=} direction
     35  */
     36 Node.prototype.rangeOfWord = function(offset, stopCharacters, stayWithinNode, direction)
     37 {
     38     var startNode;
     39     var startOffset = 0;
     40     var endNode;
     41     var endOffset = 0;
     42 
     43     if (!stayWithinNode)
     44         stayWithinNode = this;
     45 
     46     if (!direction || direction === "backward" || direction === "both") {
     47         var node = this;
     48         while (node) {
     49             if (node === stayWithinNode) {
     50                 if (!startNode)
     51                     startNode = stayWithinNode;
     52                 break;
     53             }
     54 
     55             if (node.nodeType === Node.TEXT_NODE) {
     56                 var start = (node === this ? (offset - 1) : (node.nodeValue.length - 1));
     57                 for (var i = start; i >= 0; --i) {
     58                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
     59                         startNode = node;
     60                         startOffset = i + 1;
     61                         break;
     62                     }
     63                 }
     64             }
     65 
     66             if (startNode)
     67                 break;
     68 
     69             node = node.traversePreviousNode(stayWithinNode);
     70         }
     71 
     72         if (!startNode) {
     73             startNode = stayWithinNode;
     74             startOffset = 0;
     75         }
     76     } else {
     77         startNode = this;
     78         startOffset = offset;
     79     }
     80 
     81     if (!direction || direction === "forward" || direction === "both") {
     82         node = this;
     83         while (node) {
     84             if (node === stayWithinNode) {
     85                 if (!endNode)
     86                     endNode = stayWithinNode;
     87                 break;
     88             }
     89 
     90             if (node.nodeType === Node.TEXT_NODE) {
     91                 var start = (node === this ? offset : 0);
     92                 for (var i = start; i < node.nodeValue.length; ++i) {
     93                     if (stopCharacters.indexOf(node.nodeValue[i]) !== -1) {
     94                         endNode = node;
     95                         endOffset = i;
     96                         break;
     97                     }
     98                 }
     99             }
    100 
    101             if (endNode)
    102                 break;
    103 
    104             node = node.traverseNextNode(stayWithinNode);
    105         }
    106 
    107         if (!endNode) {
    108             endNode = stayWithinNode;
    109             endOffset = stayWithinNode.nodeType === Node.TEXT_NODE ? stayWithinNode.nodeValue.length : stayWithinNode.childNodes.length;
    110         }
    111     } else {
    112         endNode = this;
    113         endOffset = offset;
    114     }
    115 
    116     var result = this.ownerDocument.createRange();
    117     result.setStart(startNode, startOffset);
    118     result.setEnd(endNode, endOffset);
    119 
    120     return result;
    121 }
    122 
    123 Node.prototype.traverseNextTextNode = function(stayWithin)
    124 {
    125     var node = this.traverseNextNode(stayWithin);
    126     if (!node)
    127         return;
    128 
    129     while (node && node.nodeType !== Node.TEXT_NODE)
    130         node = node.traverseNextNode(stayWithin);
    131 
    132     return node;
    133 }
    134 
    135 Node.prototype.rangeBoundaryForOffset = function(offset)
    136 {
    137     var node = this.traverseNextTextNode(this);
    138     while (node && offset > node.nodeValue.length) {
    139         offset -= node.nodeValue.length;
    140         node = node.traverseNextTextNode(this);
    141     }
    142     if (!node)
    143         return { container: this, offset: 0 };
    144     return { container: node, offset: offset };
    145 }
    146 
    147 /**
    148  * @param {string} className
    149  */
    150 Element.prototype.removeStyleClass = function(className)
    151 {
    152     this.classList.remove(className);
    153 }
    154 
    155 Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
    156 {
    157     var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
    158     if (regex.test(this.className))
    159         this.className = this.className.replace(regex, " ");
    160 }
    161 
    162 /**
    163  * @param {string} className
    164  */
    165 Element.prototype.addStyleClass = function(className)
    166 {
    167     this.classList.add(className);
    168 }
    169 
    170 /**
    171  * @param {string} className
    172  * @return {boolean}
    173  */
    174 Element.prototype.hasStyleClass = function(className)
    175 {
    176     return this.classList.contains(className);
    177 }
    178 
    179 /**
    180  * @param {string} className
    181  * @param {*} enable
    182  */
    183 Element.prototype.enableStyleClass = function(className, enable)
    184 {
    185     if (enable)
    186         this.addStyleClass(className);
    187     else
    188         this.removeStyleClass(className);
    189 }
    190 
    191 /**
    192  * @param {number|undefined} x
    193  * @param {number|undefined} y
    194  */
    195 Element.prototype.positionAt = function(x, y)
    196 {
    197     if (typeof x === "number")
    198         this.style.setProperty("left", x + "px");
    199     else
    200         this.style.removeProperty("left");
    201 
    202     if (typeof y === "number")
    203         this.style.setProperty("top", y + "px");
    204     else
    205         this.style.removeProperty("top");
    206 }
    207 
    208 Element.prototype.isScrolledToBottom = function()
    209 {
    210     // This code works only for 0-width border
    211     return this.scrollTop + this.clientHeight === this.scrollHeight;
    212 }
    213 
    214 /**
    215  * @param {Node} fromNode
    216  * @param {Node} toNode
    217  */
    218 function removeSubsequentNodes(fromNode, toNode)
    219 {
    220     for (var node = fromNode; node && node !== toNode; ) {
    221         var nodeToRemove = node;
    222         node = node.nextSibling;
    223         nodeToRemove.remove();
    224     }
    225 }
    226 
    227 /**
    228  * @constructor
    229  * @param {number} width
    230  * @param {number} height
    231  */
    232 function Size(width, height)
    233 {
    234     this.width = width;
    235     this.height = height;
    236 }
    237 
    238 /**
    239  * @param {Element=} containerElement
    240  * @return {Size}
    241  */
    242 Element.prototype.measurePreferredSize = function(containerElement)
    243 {
    244     containerElement = containerElement || document.body;
    245     containerElement.appendChild(this);
    246     this.positionAt(0, 0);
    247     var result = new Size(this.offsetWidth, this.offsetHeight);
    248     this.positionAt(undefined, undefined);
    249     this.remove();
    250     return result;
    251 }
    252 
    253 Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
    254 {
    255     for (var node = this; node && node !== this.ownerDocument; node = node.parentNode)
    256         for (var i = 0; i < nameArray.length; ++i)
    257             if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
    258                 return node;
    259     return null;
    260 }
    261 
    262 Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
    263 {
    264     return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
    265 }
    266 
    267 /**
    268  * @param {string} className
    269  * @param {Element=} stayWithin
    270  */
    271 Node.prototype.enclosingNodeOrSelfWithClass = function(className, stayWithin)
    272 {
    273     for (var node = this; node && node !== stayWithin && node !== this.ownerDocument; node = node.parentNode)
    274         if (node.nodeType === Node.ELEMENT_NODE && node.hasStyleClass(className))
    275             return node;
    276     return null;
    277 }
    278 
    279 Element.prototype.query = function(query)
    280 {
    281     return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
    282 }
    283 
    284 Element.prototype.removeChildren = function()
    285 {
    286     if (this.firstChild)
    287         this.textContent = "";
    288 }
    289 
    290 Element.prototype.isInsertionCaretInside = function()
    291 {
    292     var selection = window.getSelection();
    293     if (!selection.rangeCount || !selection.isCollapsed)
    294         return false;
    295     var selectionRange = selection.getRangeAt(0);
    296     return selectionRange.startContainer.isSelfOrDescendant(this);
    297 }
    298 
    299 /**
    300  * @param {string=} className
    301  */
    302 Element.prototype.createChild = function(elementName, className)
    303 {
    304     var element = this.ownerDocument.createElement(elementName);
    305     if (className)
    306         element.className = className;
    307     this.appendChild(element);
    308     return element;
    309 }
    310 
    311 DocumentFragment.prototype.createChild = Element.prototype.createChild;
    312 
    313 /**
    314  * @param {string} text
    315  */
    316 Element.prototype.createTextChild = function(text)
    317 {
    318     var element = this.ownerDocument.createTextNode(text);
    319     this.appendChild(element);
    320     return element;
    321 }
    322 
    323 DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild;
    324 
    325 /**
    326  * @return {number}
    327  */
    328 Element.prototype.totalOffsetLeft = function()
    329 {
    330     return this.totalOffset().left;
    331 }
    332 
    333 /**
    334  * @return {number}
    335  */
    336 Element.prototype.totalOffsetTop = function()
    337 {
    338     return this.totalOffset().top;
    339 
    340 }
    341 
    342 Element.prototype.totalOffset = function()
    343 {
    344     var totalLeft = 0;
    345     var totalTop = 0;
    346 
    347     for (var element = this; element; element = element.offsetParent) {
    348         totalLeft += element.offsetLeft;
    349         totalTop += element.offsetTop;
    350         if (this !== element) {
    351             totalLeft += element.clientLeft - element.scrollLeft;
    352             totalTop += element.clientTop - element.scrollTop;
    353         }
    354     }
    355 
    356     return { left: totalLeft, top: totalTop };
    357 }
    358 
    359 Element.prototype.scrollOffset = function()
    360 {
    361     var curLeft = 0;
    362     var curTop = 0;
    363     for (var element = this; element; element = element.scrollParent) {
    364         curLeft += element.scrollLeft;
    365         curTop += element.scrollTop;
    366     }
    367     return { left: curLeft, top: curTop };
    368 }
    369 
    370 /**
    371  * @constructor
    372  * @param {number=} x
    373  * @param {number=} y
    374  * @param {number=} width
    375  * @param {number=} height
    376  */
    377 function AnchorBox(x, y, width, height)
    378 {
    379     this.x = x || 0;
    380     this.y = y || 0;
    381     this.width = width || 0;
    382     this.height = height || 0;
    383 }
    384 
    385 /**
    386  * @param {Window} targetWindow
    387  * @return {AnchorBox}
    388  */
    389 Element.prototype.offsetRelativeToWindow = function(targetWindow)
    390 {
    391     var elementOffset = new AnchorBox();
    392     var curElement = this;
    393     var curWindow = this.ownerDocument.defaultView;
    394     while (curWindow && curElement) {
    395         elementOffset.x += curElement.totalOffsetLeft();
    396         elementOffset.y += curElement.totalOffsetTop();
    397         if (curWindow === targetWindow)
    398             break;
    399 
    400         curElement = curWindow.frameElement;
    401         curWindow = curWindow.parent;
    402     }
    403 
    404     return elementOffset;
    405 }
    406 
    407 /**
    408  * @param {Window} targetWindow
    409  * @return {AnchorBox}
    410  */
    411 Element.prototype.boxInWindow = function(targetWindow)
    412 {
    413     targetWindow = targetWindow || this.ownerDocument.defaultView;
    414 
    415     var anchorBox = this.offsetRelativeToWindow(window);
    416     anchorBox.width = Math.min(this.offsetWidth, window.innerWidth - anchorBox.x);
    417     anchorBox.height = Math.min(this.offsetHeight, window.innerHeight - anchorBox.y);
    418 
    419     return anchorBox;
    420 }
    421 
    422 /**
    423  * @param {string} text
    424  */
    425 Element.prototype.setTextAndTitle = function(text)
    426 {
    427     this.textContent = text;
    428     this.title = text;
    429 }
    430 
    431 KeyboardEvent.prototype.__defineGetter__("data", function()
    432 {
    433     // Emulate "data" attribute from DOM 3 TextInput event.
    434     // See http://www.w3.org/TR/DOM-Level-3-Events/#events-Events-TextEvent-data
    435     switch (this.type) {
    436         case "keypress":
    437             if (!this.ctrlKey && !this.metaKey)
    438                 return String.fromCharCode(this.charCode);
    439             else
    440                 return "";
    441         case "keydown":
    442         case "keyup":
    443             if (!this.ctrlKey && !this.metaKey && !this.altKey)
    444                 return String.fromCharCode(this.which);
    445             else
    446                 return "";
    447     }
    448 });
    449 
    450 /**
    451  * @param {boolean=} preventDefault
    452  */
    453 Event.prototype.consume = function(preventDefault)
    454 {
    455     this.stopImmediatePropagation();
    456     if (preventDefault)
    457         this.preventDefault();
    458     this.handled = true;
    459 }
    460 
    461 Text.prototype.select = function(start, end)
    462 {
    463     start = start || 0;
    464     end = end || this.textContent.length;
    465 
    466     if (start < 0)
    467         start = end + start;
    468 
    469     var selection = this.ownerDocument.defaultView.getSelection();
    470     selection.removeAllRanges();
    471     var range = this.ownerDocument.createRange();
    472     range.setStart(this, start);
    473     range.setEnd(this, end);
    474     selection.addRange(range);
    475     return this;
    476 }
    477 
    478 Element.prototype.selectionLeftOffset = function()
    479 {
    480     // Calculate selection offset relative to the current element.
    481 
    482     var selection = window.getSelection();
    483     if (!selection.containsNode(this, true))
    484         return null;
    485 
    486     var leftOffset = selection.anchorOffset;
    487     var node = selection.anchorNode;
    488 
    489     while (node !== this) {
    490         while (node.previousSibling) {
    491             node = node.previousSibling;
    492             leftOffset += node.textContent.length;
    493         }
    494         node = node.parentNode;
    495     }
    496 
    497     return leftOffset;
    498 }
    499 
    500 Node.prototype.isAncestor = function(node)
    501 {
    502     if (!node)
    503         return false;
    504 
    505     var currentNode = node.parentNode;
    506     while (currentNode) {
    507         if (this === currentNode)
    508             return true;
    509         currentNode = currentNode.parentNode;
    510     }
    511     return false;
    512 }
    513 
    514 Node.prototype.isDescendant = function(descendant)
    515 {
    516     return !!descendant && descendant.isAncestor(this);
    517 }
    518 
    519 Node.prototype.isSelfOrAncestor = function(node)
    520 {
    521     return !!node && (node === this || this.isAncestor(node));
    522 }
    523 
    524 Node.prototype.isSelfOrDescendant = function(node)
    525 {
    526     return !!node && (node === this || this.isDescendant(node));
    527 }
    528 
    529 Node.prototype.traverseNextNode = function(stayWithin)
    530 {
    531     var node = this.firstChild;
    532     if (node)
    533         return node;
    534 
    535     if (stayWithin && this === stayWithin)
    536         return null;
    537 
    538     node = this.nextSibling;
    539     if (node)
    540         return node;
    541 
    542     node = this;
    543     while (node && !node.nextSibling && (!stayWithin || !node.parentNode || node.parentNode !== stayWithin))
    544         node = node.parentNode;
    545     if (!node)
    546         return null;
    547 
    548     return node.nextSibling;
    549 }
    550 
    551 Node.prototype.traversePreviousNode = function(stayWithin)
    552 {
    553     if (stayWithin && this === stayWithin)
    554         return null;
    555     var node = this.previousSibling;
    556     while (node && node.lastChild)
    557         node = node.lastChild;
    558     if (node)
    559         return node;
    560     return this.parentNode;
    561 }
    562 
    563 function isEnterKey(event) {
    564     // Check if in IME.
    565     return event.keyCode !== 229 && event.keyIdentifier === "Enter";
    566 }
    567 
    568 function consumeEvent(e)
    569 {
    570     e.consume();
    571 }
    572 
    573 /**
    574  * Mutation observers leak memory. Keep track of them and disconnect
    575  * on unload.
    576  * @constructor
    577  * @param {function(Array.<WebKitMutation>)} handler
    578  */
    579 function NonLeakingMutationObserver(handler)
    580 {
    581     this._observer = new WebKitMutationObserver(handler);
    582     NonLeakingMutationObserver._instances.push(this);
    583     if (!NonLeakingMutationObserver._unloadListener) {
    584         NonLeakingMutationObserver._unloadListener = function() {
    585             while (NonLeakingMutationObserver._instances.length)
    586                 NonLeakingMutationObserver._instances[NonLeakingMutationObserver._instances.length - 1].disconnect();
    587         };
    588         window.addEventListener("unload", NonLeakingMutationObserver._unloadListener, false);
    589     }
    590 }
    591 
    592 NonLeakingMutationObserver._instances = [];
    593 
    594 NonLeakingMutationObserver.prototype = {
    595     /**
    596      * @param {Element} element
    597      * @param {Object} config
    598      */
    599     observe: function(element, config)
    600     {
    601         if (this._observer)
    602             this._observer.observe(element, config);
    603     },
    604 
    605     disconnect: function()
    606     {
    607         if (this._observer)
    608             this._observer.disconnect();
    609         NonLeakingMutationObserver._instances.remove(this);
    610         delete this._observer;
    611     }
    612 }
    613 
    614