• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 */
36Node.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
123Node.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
135Node.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
147Element.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 */
158Element.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 */
170Element.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
183Element.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 */
193function 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 */
207function Size(width, height)
208{
209    this.width = width;
210    this.height = height;
211}
212
213/**
214 * @param {?Element=} containerElement
215 * @return {!Size}
216 */
217Element.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 */
232Element.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
239Node.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
248Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
249{
250    return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
251}
252
253/**
254 * @param {string} className
255 * @param {!Element=} stayWithin
256 */
257Node.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
265Element.prototype.query = function(query)
266{
267    return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
268}
269
270Element.prototype.removeChildren = function()
271{
272    if (this.firstChild)
273        this.textContent = "";
274}
275
276Element.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 */
289Document.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 */
300Element.prototype.createChild = function(elementName, className)
301{
302    var element = this.ownerDocument.createElementWithClass(elementName, className);
303    this.appendChild(element);
304    return element;
305}
306
307DocumentFragment.prototype.createChild = Element.prototype.createChild;
308
309/**
310 * @param {string} text
311 */
312Element.prototype.createTextChild = function(text)
313{
314    var element = this.ownerDocument.createTextNode(text);
315    this.appendChild(element);
316    return element;
317}
318
319DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild;
320
321/**
322 * @return {number}
323 */
324Element.prototype.totalOffsetLeft = function()
325{
326    return this.totalOffset().left;
327}
328
329/**
330 * @return {number}
331 */
332Element.prototype.totalOffsetTop = function()
333{
334    return this.totalOffset().top;
335
336}
337
338Element.prototype.totalOffset = function()
339{
340    var rect = this.getBoundingClientRect();
341    return { left: rect.left, top: rect.top };
342}
343
344Element.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 */
362function 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 */
374Element.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 */
396Element.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 */
410Element.prototype.setTextAndTitle = function(text)
411{
412    this.textContent = text;
413    this.title = text;
414}
415
416KeyboardEvent.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 */
438Event.prototype.consume = function(preventDefault)
439{
440    this.stopImmediatePropagation();
441    if (preventDefault)
442        this.preventDefault();
443    this.handled = true;
444}
445
446Text.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
463Element.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
485Node.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
499Node.prototype.isDescendant = function(descendant)
500{
501    return !!descendant && descendant.isAncestor(this);
502}
503
504Node.prototype.isSelfOrAncestor = function(node)
505{
506    return !!node && (node === this || this.isAncestor(node));
507}
508
509Node.prototype.isSelfOrDescendant = function(node)
510{
511    return !!node && (node === this || this.isDescendant(node));
512}
513
514Node.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
536Node.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
548function isEnterKey(event) {
549    // Check if in IME.
550    return event.keyCode !== 229 && event.keyIdentifier === "Enter";
551}
552
553function 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 */
564function 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
577NonLeakingMutationObserver._instances = [];
578
579NonLeakingMutationObserver.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