• 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 {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
159Element.prototype.removeMatchingStyleClasses = function(classNameRegex)
160{
161    var regex = new RegExp("(^|\\s+)" + classNameRegex + "($|\\s+)");
162    if (regex.test(this.className))
163        this.className = this.className.replace(regex, " ");
164}
165
166/**
167 * @param {number|undefined} x
168 * @param {number|undefined} y
169 * @param {!Element=} relativeTo
170 */
171Element.prototype.positionAt = function(x, y, relativeTo)
172{
173    var shift = {x: 0, y: 0};
174    if (relativeTo)
175       shift = relativeTo.boxInWindow(this.ownerDocument.defaultView);
176
177    if (typeof x === "number")
178        this.style.setProperty("left", (shift.x + x) + "px");
179    else
180        this.style.removeProperty("left");
181
182    if (typeof y === "number")
183        this.style.setProperty("top", (shift.y + y) + "px");
184    else
185        this.style.removeProperty("top");
186}
187
188/**
189 * @return {boolean}
190 */
191Element.prototype.isScrolledToBottom = function()
192{
193    // This code works only for 0-width border.
194    // Both clientHeight and scrollHeight are rounded to integer values, so we tolerate
195    // one pixel error.
196    return Math.abs(this.scrollTop + this.clientHeight - this.scrollHeight) <= 1;
197}
198
199/**
200 * @param {!Node} fromNode
201 * @param {!Node} toNode
202 */
203function removeSubsequentNodes(fromNode, toNode)
204{
205    for (var node = fromNode; node && node !== toNode; ) {
206        var nodeToRemove = node;
207        node = node.nextSibling;
208        nodeToRemove.remove();
209    }
210}
211
212/**
213 * @constructor
214 * @param {!Size} minimum
215 * @param {?Size=} preferred
216 */
217function Constraints(minimum, preferred)
218{
219    /**
220     * @type {!Size}
221     */
222    this.minimum = minimum;
223
224    /**
225     * @type {!Size}
226     */
227    this.preferred = preferred || minimum;
228
229    if (this.minimum.width > this.preferred.width || this.minimum.height > this.preferred.height)
230        throw new Error("Minimum size is greater than preferred.");
231}
232
233/**
234 * @param {?Constraints} constraints
235 * @return {boolean}
236 */
237Constraints.prototype.isEqual = function(constraints)
238{
239    return !!constraints && this.minimum.isEqual(constraints.minimum) && this.preferred.isEqual(constraints.preferred);
240}
241
242/**
243 * @param {!Constraints|number} value
244 * @return {!Constraints}
245 */
246Constraints.prototype.widthToMax = function(value)
247{
248    if (typeof value === "number")
249        return new Constraints(this.minimum.widthToMax(value), this.preferred.widthToMax(value));
250    return new Constraints(this.minimum.widthToMax(value.minimum), this.preferred.widthToMax(value.preferred));
251}
252
253/**
254 * @param {!Constraints|number} value
255 * @return {!Constraints}
256 */
257Constraints.prototype.addWidth = function(value)
258{
259    if (typeof value === "number")
260        return new Constraints(this.minimum.addWidth(value), this.preferred.addWidth(value));
261    return new Constraints(this.minimum.addWidth(value.minimum), this.preferred.addWidth(value.preferred));
262}
263
264/**
265 * @param {!Constraints|number} value
266 * @return {!Constraints}
267 */
268Constraints.prototype.heightToMax = function(value)
269{
270    if (typeof value === "number")
271        return new Constraints(this.minimum.heightToMax(value), this.preferred.heightToMax(value));
272    return new Constraints(this.minimum.heightToMax(value.minimum), this.preferred.heightToMax(value.preferred));
273}
274
275/**
276 * @param {!Constraints|number} value
277 * @return {!Constraints}
278 */
279Constraints.prototype.addHeight = function(value)
280{
281    if (typeof value === "number")
282        return new Constraints(this.minimum.addHeight(value), this.preferred.addHeight(value));
283    return new Constraints(this.minimum.addHeight(value.minimum), this.preferred.addHeight(value.preferred));
284}
285
286/**
287 * @param {?Element=} containerElement
288 * @return {!Size}
289 */
290Element.prototype.measurePreferredSize = function(containerElement)
291{
292    containerElement = containerElement || document.body;
293    containerElement.appendChild(this);
294    this.positionAt(0, 0);
295    var result = new Size(this.offsetWidth, this.offsetHeight);
296    this.positionAt(undefined, undefined);
297    this.remove();
298    return result;
299}
300
301/**
302 * @param {!Event} event
303 * @return {boolean}
304 */
305Element.prototype.containsEventPoint = function(event)
306{
307    var box = this.getBoundingClientRect();
308    return box.left < event.x  && event.x < box.right &&
309           box.top < event.y && event.y < box.bottom;
310}
311
312/**
313 * @param {!Array.<string>} nameArray
314 * @return {?Node}
315 */
316Node.prototype.enclosingNodeOrSelfWithNodeNameInArray = function(nameArray)
317{
318    for (var node = this; node && node !== this.ownerDocument; node = node.parentNode) {
319        for (var i = 0; i < nameArray.length; ++i) {
320            if (node.nodeName.toLowerCase() === nameArray[i].toLowerCase())
321                return node;
322        }
323    }
324    return null;
325}
326
327/**
328 * @param {string} nodeName
329 * @return {?Node}
330 */
331Node.prototype.enclosingNodeOrSelfWithNodeName = function(nodeName)
332{
333    return this.enclosingNodeOrSelfWithNodeNameInArray([nodeName]);
334}
335
336/**
337 * @param {string} className
338 * @param {!Element=} stayWithin
339 * @return {?Element}
340 */
341Node.prototype.enclosingNodeOrSelfWithClass = function(className, stayWithin)
342{
343    for (var node = this; node && node !== stayWithin && node !== this.ownerDocument; node = node.parentNode) {
344        if (node.nodeType === Node.ELEMENT_NODE && node.classList.contains(className))
345            return /** @type {!Element} */ (node);
346    }
347    return null;
348}
349
350/**
351 * @param {string} query
352 * @return {?Node}
353 */
354Element.prototype.query = function(query)
355{
356    return this.ownerDocument.evaluate(query, this, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue;
357}
358
359Element.prototype.removeChildren = function()
360{
361    if (this.firstChild)
362        this.textContent = "";
363}
364
365Element.prototype.appendChildren = function(children)
366{
367    for (var i = 0; i < children.length; ++i)
368        this.appendChild(children[i]);
369}
370
371Element.prototype.setChildren = function(children)
372{
373    this.removeChildren();
374    this.appendChildren(children);
375}
376
377/**
378 * @return {boolean}
379 */
380Element.prototype.isInsertionCaretInside = function()
381{
382    var selection = window.getSelection();
383    if (!selection.rangeCount || !selection.isCollapsed)
384        return false;
385    var selectionRange = selection.getRangeAt(0);
386    return selectionRange.startContainer.isSelfOrDescendant(this);
387}
388
389/**
390 * @param {string} elementName
391 * @param {string=} className
392 * @return {!Element}
393 */
394Document.prototype.createElementWithClass = function(elementName, className)
395{
396    var element = this.createElement(elementName);
397    if (className)
398        element.className = className;
399    return element;
400}
401
402/**
403 * @param {string} elementName
404 * @param {string=} className
405 * @return {!Element}
406 */
407Element.prototype.createChild = function(elementName, className)
408{
409    var element = this.ownerDocument.createElementWithClass(elementName, className);
410    this.appendChild(element);
411    return element;
412}
413
414DocumentFragment.prototype.createChild = Element.prototype.createChild;
415
416/**
417 * @param {string} text
418 * @return {!Text}
419 */
420Element.prototype.createTextChild = function(text)
421{
422    var element = this.ownerDocument.createTextNode(text);
423    this.appendChild(element);
424    return element;
425}
426
427DocumentFragment.prototype.createTextChild = Element.prototype.createTextChild;
428
429/**
430 * @return {number}
431 */
432Element.prototype.totalOffsetLeft = function()
433{
434    return this.totalOffset().left;
435}
436
437/**
438 * @return {number}
439 */
440Element.prototype.totalOffsetTop = function()
441{
442    return this.totalOffset().top;
443
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