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