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