summaryrefslogtreecommitdiff
path: root/devtools/client/shared/DOMHelpers.jsm
blob: 9c861006e99d6750ab9b5ae6480113c07e012a3b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

"use strict";

const Ci = Components.interfaces;
const Cu = Components.utils;
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { require } = Cu.import("resource://devtools/shared/Loader.jsm", {});
const nodeFilterConstants = require("devtools/shared/dom-node-filter-constants");

this.EXPORTED_SYMBOLS = ["DOMHelpers"];

/**
 * DOMHelpers
 * Makes DOM traversal easier. Goes through iframes.
 *
 * @constructor
 * @param nsIDOMWindow aWindow
 *        The content window, owning the document to traverse.
 */
this.DOMHelpers = function DOMHelpers(aWindow) {
  if (!aWindow) {
    throw new Error("window can't be null or undefined");
  }
  this.window = aWindow;
};

DOMHelpers.prototype = {
  getParentObject: function Helpers_getParentObject(node)
  {
    let parentNode = node ? node.parentNode : null;

    if (!parentNode) {
      // Documents have no parentNode; Attr, Document, DocumentFragment, Entity,
      // and Notation. top level windows have no parentNode
      if (node && node == this.window.Node.DOCUMENT_NODE) {
        // document type
        if (node.defaultView) {
          let embeddingFrame = node.defaultView.frameElement;
          if (embeddingFrame)
            return embeddingFrame.parentNode;
        }
      }
      // a Document object without a parentNode or window
      return null;  // top level has no parent
    }

    if (parentNode.nodeType == this.window.Node.DOCUMENT_NODE) {
      if (parentNode.defaultView) {
        return parentNode.defaultView.frameElement;
      }
      // parent is document element, but no window at defaultView.
      return null;
    }

    if (!parentNode.localName)
      return null;

    return parentNode;
  },

  getChildObject: function Helpers_getChildObject(node, index, previousSibling,
                                                showTextNodesWithWhitespace)
  {
    if (!node)
      return null;

    if (node.contentDocument) {
      // then the node is a frame
      if (index == 0) {
        return node.contentDocument.documentElement;  // the node's HTMLElement
      }
      return null;
    }

    if (node.getSVGDocument) {
      let svgDocument = node.getSVGDocument();
      if (svgDocument) {
        // then the node is a frame
        if (index == 0) {
          return svgDocument.documentElement;  // the node's SVGElement
        }
        return null;
      }
    }

    let child = null;
    if (previousSibling)  // then we are walking
      child = this.getNextSibling(previousSibling);
    else
      child = this.getFirstChild(node);

    if (showTextNodesWithWhitespace)
      return child;

    for (; child; child = this.getNextSibling(child)) {
      if (!this.isWhitespaceText(child))
        return child;
    }

    return null;  // we have no children worth showing.
  },

  getFirstChild: function Helpers_getFirstChild(node)
  {
    let SHOW_ALL = nodeFilterConstants.SHOW_ALL;
    this.treeWalker = node.ownerDocument.createTreeWalker(node,
      SHOW_ALL, null);
    return this.treeWalker.firstChild();
  },

  getNextSibling: function Helpers_getNextSibling(node)
  {
    let next = this.treeWalker.nextSibling();

    if (!next)
      delete this.treeWalker;

    return next;
  },

  isWhitespaceText: function Helpers_isWhitespaceText(node)
  {
    return node.nodeType == this.window.Node.TEXT_NODE &&
                            !/[^\s]/.exec(node.nodeValue);
  },

  destroy: function Helpers_destroy()
  {
    delete this.window;
    delete this.treeWalker;
  },

  /**
   * A simple way to be notified (once) when a window becomes
   * interactive (DOMContentLoaded).
   *
   * It is based on the chromeEventHandler. This is useful when
   * chrome iframes are loaded in content docshells (in Firefox
   * tabs for example).
   */
  onceDOMReady: function Helpers_onLocationChange(callback, targetURL) {
    let window = this.window;
    let docShell = window.QueryInterface(Ci.nsIInterfaceRequestor)
                         .getInterface(Ci.nsIWebNavigation)
                         .QueryInterface(Ci.nsIDocShell);
    let onReady = function (event) {
      if (event.target == window.document) {
        docShell.chromeEventHandler.removeEventListener("DOMContentLoaded", onReady, false);
        // If in `callback` the URL of the window is changed and a listener to DOMContentLoaded
        // is attached, the event we just received will be also be caught by the new listener.
        // We want to avoid that so we execute the callback in the next queue.
        Services.tm.mainThread.dispatch(callback, 0);
      }
    };
    if ((window.document.readyState == "complete" ||
         window.document.readyState == "interactive") &&
         window.location.href == targetURL) {
      Services.tm.mainThread.dispatch(callback, 0);
    } else {
      docShell.chromeEventHandler.addEventListener("DOMContentLoaded", onReady, false);
    }
  }
};