summaryrefslogtreecommitdiffstats
path: root/devtools/server/actors/highlighters/remote-node-picker-notice.js
blob: 64b131d2a27eba61d07ede69cd0c376bd43a7cea (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
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/* 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 {
  CanvasFrameAnonymousContentHelper,
} = require("resource://devtools/server/actors/highlighters/utils/markup.js");

loader.lazyGetter(this, "HighlightersBundle", () => {
  return new Localization(["devtools/shared/highlighters.ftl"], true);
});

loader.lazyGetter(this, "isAndroid", () => {
  return Services.appinfo.OS === "Android";
});

/**
 * The RemoteNodePickerNotice is a class that displays a notice in a remote debugged page.
 * This is used to signal to users they can click/tap an element to select it in the
 * about:devtools-toolbox toolbox inspector.
 */
class RemoteNodePickerNotice {
  #highlighterEnvironment;
  #previousHoveredElement;

  rootElementId = "node-picker-notice-root";
  hideButtonId = "node-picker-notice-hide-button";
  infoNoticeElementId = "node-picker-notice-info";

  /**
   * @param {highlighterEnvironment} highlighterEnvironment
   */
  constructor(highlighterEnvironment) {
    this.#highlighterEnvironment = highlighterEnvironment;

    this.markup = new CanvasFrameAnonymousContentHelper(
      this.#highlighterEnvironment,
      this.#buildMarkup
    );
    this.isReady = this.markup.initialize();
  }

  #buildMarkup = () => {
    const container = this.markup.createNode({
      attributes: { class: "highlighter-container" },
    });

    // Wrapper element.
    const wrapper = this.markup.createNode({
      parent: container,
      attributes: {
        id: this.rootElementId,
        hidden: "true",
        overlay: "true",
      },
    });

    const toolbar = this.markup.createNode({
      parent: wrapper,
      attributes: {
        id: "node-picker-notice-toolbar",
        class: "toolbar",
      },
    });

    this.markup.createNode({
      parent: toolbar,
      attributes: {
        id: "node-picker-notice-icon",
        class: isAndroid ? "touch" : "",
      },
    });

    const actionStr = HighlightersBundle.formatValueSync(
      isAndroid
        ? "remote-node-picker-notice-action-touch"
        : "remote-node-picker-notice-action-desktop"
    );

    this.markup.createNode({
      nodeType: "span",
      parent: toolbar,
      text: HighlightersBundle.formatValueSync("remote-node-picker-notice", {
        action: actionStr,
      }),
      attributes: {
        id: this.infoNoticeElementId,
      },
    });

    this.markup.createNode({
      nodeType: "button",
      parent: toolbar,
      text: HighlightersBundle.formatValueSync(
        "remote-node-picker-notice-hide-button"
      ),
      attributes: {
        id: this.hideButtonId,
      },
    });

    return container;
  };

  destroy() {
    // hide will nullify take care of this.#abortController.
    this.hide();
    this.markup.destroy();
    this.#highlighterEnvironment = null;
    this.#previousHoveredElement = null;
  }

  /**
   * We can't use event listener directly on the anonymous content because they aren't
   * working while the page is paused.
   * This is called from the NodePicker instance for easier events management.
   *
   * @param {ClickEvent}
   */
  onClick(e) {
    const target = e.originalTarget || e.target;
    const targetId = target?.id;

    if (targetId === this.hideButtonId) {
      this.hide();
    }
  }

  /**
   * Since we can't use :hover in the CSS for the anonymous content as it wouldn't work
   * when the page is paused, we have to roll our own implementation, adding a `.hover`
   * class for the element we want to style on hover (e.g. the close button).
   * This is called from the NodePicker instance for easier events management.
   *
   * @param {MouseMoveEvent}
   */
  handleHoveredElement(e) {
    const hideButton = this.markup.getElement(this.hideButtonId);

    const target = e.originalTarget || e.target;
    const targetId = target?.id;

    // If the user didn't change targets, do nothing
    if (this.#previousHoveredElement?.id === targetId) {
      return;
    }

    if (targetId === this.hideButtonId) {
      hideButton.classList.add("hover");
    } else {
      hideButton.classList.remove("hover");
    }
    this.#previousHoveredElement = target;
  }

  getMarkupRootElement() {
    return this.markup.getElement(this.rootElementId);
  }

  async show() {
    if (this.#highlighterEnvironment.isXUL) {
      return false;
    }
    await this.isReady;

    // Show the highlighter's root element.
    const root = this.getMarkupRootElement();
    root.removeAttribute("hidden");
    root.setAttribute("overlay", "true");

    return true;
  }

  hide() {
    if (this.#highlighterEnvironment.isXUL) {
      return;
    }

    // Hide the overlay.
    this.getMarkupRootElement().setAttribute("hidden", "true");
    // Reset the hover state
    this.markup.getElement(this.hideButtonId).classList.remove("hover");
    this.#previousHoveredElement = null;
  }
}
exports.RemoteNodePickerNotice = RemoteNodePickerNotice;