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 */
40Node.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 */
131Node.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 */
147Node.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 */
164Element.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 */
184Element.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 */
196function 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 */
210function 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 */
230Constraints.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 */
239Constraints.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 */
250Constraints.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 */
261Constraints.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 */
272Constraints.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 */
283Element.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 */
298Element.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 */
309Node.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 */
324Node.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 */
334Node.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 */
347Element.prototype.query = function(query)
348{
349    return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
350}
351
352Element.prototype.removeChildren = function()
353{
354    if (this.firstChild)
355        this.textContent = "";
356}
357
358/**
359 * @return {boolean}
360 */
361Element.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 */
375Document.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 */
388Element.prototype.createChild = function(elementName, className)
389{
390    var element = this.ownerDocument.createElementWithClass(elementName, className);
391    this.appendChild(element);
392    return element;
393}
394
395DocumentFragment.prototype.createChild = Element.prototype.createChild;
396
397/**
398 * @param {string} text
399 * @return {!Text}
400 */
401Element.prototype.createTextChild = function(text)
402{
403    var element = this.ownerDocument.createTextNode(text);
404    this.appendChild(element);
405    return element;
406}
407
408DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild;
409
410/**
411 * @param {...string} var_args
412 */
413Element.prototype.createTextChildren = function(var_args)
414{
415    for (var i = 0, n = arguments.length; i < n; ++i)
416        this.createTextChild(arguments[i]);
417}
418
419DocumentFragment.prototype.createTextChildren = Element.prototype.createTextChildren;
420
421/**
422 * @param {...!Element} var_args
423 */
424Element.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 */
433Element.prototype.totalOffsetLeft = function()
434{
435    return this.totalOffset().left;
436}
437
438/**
439 * @return {number}
440 */
441Element.prototype.totalOffsetTop = function()
442{
443    return this.totalOffset().top;
444}
445
446/**
447 * @return {!{left: number, top: number}}
448 */
449Element.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 */
458Element.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 */
476function 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 */
488AnchorBox.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 */
498AnchorBox.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 */
507AnchorBox.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 */
516Element.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 */
538Element.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 */
552Element.prototype.setTextAndTitle = function(text)
553{
554    this.textContent = text;
555    this.title = text;
556}
557
558KeyboardEvent.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 */
580Event.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 */
593Text.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 */
613Element.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 */
639Node.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 */
657Node.prototype.isDescendant = function(descendant)
658{
659    return !!descendant && descendant.isAncestor(this);
660}
661
662/**
663 * @param {?Node} node
664 * @return {boolean}
665 */
666Node.prototype.isSelfOrAncestor = function(node)
667{
668    return !!node && (node === this || this.isAncestor(node));
669}
670
671/**
672 * @param {?Node} node
673 * @return {boolean}
674 */
675Node.prototype.isSelfOrDescendant = function(node)
676{
677    return !!node && (node === this || this.isDescendant(node));
678}
679
680/**
681 * @param {!Node=} stayWithin
682 * @return {?Node}
683 */
684Node.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 */
710Node.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 */
727Node.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 */
746function isEnterKey(event) {
747    // Check if in IME.
748    return event.keyCode !== 229 && event.keyIdentifier === "Enter";
749}
750
751function consumeEvent(e)
752{
753    e.consume();
754}
755