diff options
Diffstat (limited to 'devtools/client/shared/widgets/tooltip/EventTooltipHelper.js')
-rw-r--r-- | devtools/client/shared/widgets/tooltip/EventTooltipHelper.js | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js new file mode 100644 index 000000000..63507bc5e --- /dev/null +++ b/devtools/client/shared/widgets/tooltip/EventTooltipHelper.js @@ -0,0 +1,313 @@ +/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */ +/* vim: set ft=javascript ts=2 et sw=2 tw=80: */ +/* 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 {LocalizationHelper} = require("devtools/shared/l10n"); +const L10N = new LocalizationHelper("devtools/client/locales/inspector.properties"); + +const Editor = require("devtools/client/sourceeditor/editor"); +const beautify = require("devtools/shared/jsbeautify/beautify"); + +const XHTML_NS = "http://www.w3.org/1999/xhtml"; +const CONTAINER_WIDTH = 500; + +/** + * Set the content of a provided HTMLTooltip instance to display a list of event + * listeners, with their event type, capturing argument and a link to the code + * of the event handler. + * + * @param {HTMLTooltip} tooltip + * The tooltip instance on which the event details content should be set + * @param {Array} eventListenerInfos + * A list of event listeners + * @param {Toolbox} toolbox + * Toolbox used to select debugger panel + */ +function setEventTooltip(tooltip, eventListenerInfos, toolbox) { + let eventTooltip = new EventTooltip(tooltip, eventListenerInfos, toolbox); + eventTooltip.init(); +} + +function EventTooltip(tooltip, eventListenerInfos, toolbox) { + this._tooltip = tooltip; + this._eventListenerInfos = eventListenerInfos; + this._toolbox = toolbox; + this._eventEditors = new WeakMap(); + + // Used in tests: add a reference to the EventTooltip instance on the HTMLTooltip. + this._tooltip.eventTooltip = this; + + this._headerClicked = this._headerClicked.bind(this); + this._debugClicked = this._debugClicked.bind(this); + this.destroy = this.destroy.bind(this); +} + +EventTooltip.prototype = { + init: function () { + let config = { + mode: Editor.modes.js, + lineNumbers: false, + lineWrapping: true, + readOnly: true, + styleActiveLine: true, + extraKeys: {}, + theme: "mozilla markup-view" + }; + + let doc = this._tooltip.doc; + this.container = doc.createElementNS(XHTML_NS, "div"); + this.container.className = "devtools-tooltip-events-container"; + + for (let listener of this._eventListenerInfos) { + let phase = listener.capturing ? "Capturing" : "Bubbling"; + let level = listener.DOM0 ? "DOM0" : "DOM2"; + + // Header + let header = doc.createElementNS(XHTML_NS, "div"); + header.className = "event-header devtools-toolbar"; + this.container.appendChild(header); + + if (!listener.hide.debugger) { + let debuggerIcon = doc.createElementNS(XHTML_NS, "img"); + debuggerIcon.className = "event-tooltip-debugger-icon"; + debuggerIcon.setAttribute("src", + "chrome://devtools/skin/images/tool-debugger.svg"); + let openInDebugger = L10N.getStr("eventsTooltip.openInDebugger"); + debuggerIcon.setAttribute("title", openInDebugger); + header.appendChild(debuggerIcon); + } + + if (!listener.hide.type) { + let eventTypeLabel = doc.createElementNS(XHTML_NS, "span"); + eventTypeLabel.className = "event-tooltip-event-type"; + eventTypeLabel.textContent = listener.type; + eventTypeLabel.setAttribute("title", listener.type); + header.appendChild(eventTypeLabel); + } + + if (!listener.hide.filename) { + let filename = doc.createElementNS(XHTML_NS, "span"); + filename.className = "event-tooltip-filename devtools-monospace"; + filename.textContent = listener.origin; + filename.setAttribute("title", listener.origin); + header.appendChild(filename); + } + + let attributesContainer = doc.createElementNS(XHTML_NS, "div"); + attributesContainer.className = "event-tooltip-attributes-container"; + header.appendChild(attributesContainer); + + if (!listener.hide.capturing) { + let attributesBox = doc.createElementNS(XHTML_NS, "div"); + attributesBox.className = "event-tooltip-attributes-box"; + attributesContainer.appendChild(attributesBox); + + let capturing = doc.createElementNS(XHTML_NS, "span"); + capturing.className = "event-tooltip-attributes"; + capturing.textContent = phase; + capturing.setAttribute("title", phase); + attributesBox.appendChild(capturing); + } + + if (listener.tags) { + for (let tag of listener.tags.split(",")) { + let attributesBox = doc.createElementNS(XHTML_NS, "div"); + attributesBox.className = "event-tooltip-attributes-box"; + attributesContainer.appendChild(attributesBox); + + let tagBox = doc.createElementNS(XHTML_NS, "span"); + tagBox.className = "event-tooltip-attributes"; + tagBox.textContent = tag; + tagBox.setAttribute("title", tag); + attributesBox.appendChild(tagBox); + } + } + + if (!listener.hide.dom0) { + let attributesBox = doc.createElementNS(XHTML_NS, "div"); + attributesBox.className = "event-tooltip-attributes-box"; + attributesContainer.appendChild(attributesBox); + + let dom0 = doc.createElementNS(XHTML_NS, "span"); + dom0.className = "event-tooltip-attributes"; + dom0.textContent = level; + dom0.setAttribute("title", level); + attributesBox.appendChild(dom0); + } + + // Content + let content = doc.createElementNS(XHTML_NS, "div"); + let editor = new Editor(config); + this._eventEditors.set(content, { + editor: editor, + handler: listener.handler, + searchString: listener.searchString, + uri: listener.origin, + dom0: listener.DOM0, + appended: false + }); + + content.className = "event-tooltip-content-box"; + this.container.appendChild(content); + + this._addContentListeners(header); + } + + this._tooltip.setContent(this.container, {width: CONTAINER_WIDTH}); + this._tooltip.on("hidden", this.destroy); + }, + + _addContentListeners: function (header) { + header.addEventListener("click", this._headerClicked); + }, + + _headerClicked: function (event) { + if (event.target.classList.contains("event-tooltip-debugger-icon")) { + this._debugClicked(event); + event.stopPropagation(); + return; + } + + let doc = this._tooltip.doc; + let header = event.currentTarget; + let content = header.nextElementSibling; + + if (content.hasAttribute("open")) { + content.removeAttribute("open"); + } else { + let contentNodes = doc.querySelectorAll(".event-tooltip-content-box"); + + for (let node of contentNodes) { + if (node !== content) { + node.removeAttribute("open"); + } + } + + content.setAttribute("open", ""); + + let eventEditor = this._eventEditors.get(content); + + if (eventEditor.appended) { + return; + } + + let {editor, handler} = eventEditor; + + let iframe = doc.createElementNS(XHTML_NS, "iframe"); + iframe.setAttribute("style", "width: 100%; height: 100%; border-style: none;"); + + editor.appendTo(content, iframe).then(() => { + let tidied = beautify.js(handler, { "indent_size": 2 }); + editor.setText(tidied); + + eventEditor.appended = true; + + let container = header.parentElement.getBoundingClientRect(); + if (header.getBoundingClientRect().top < container.top) { + header.scrollIntoView(true); + } else if (content.getBoundingClientRect().bottom > container.bottom) { + content.scrollIntoView(false); + } + + this._tooltip.emit("event-tooltip-ready"); + }); + } + }, + + _debugClicked: function (event) { + let header = event.currentTarget; + let content = header.nextElementSibling; + + let {uri, searchString, dom0} = this._eventEditors.get(content); + + if (uri && uri !== "?") { + // Save a copy of toolbox as it will be set to null when we hide the tooltip. + let toolbox = this._toolbox; + + this._tooltip.hide(); + + uri = uri.replace(/"/g, ""); + + let showSource = ({ DebuggerView }) => { + let matches = uri.match(/(.*):(\d+$)/); + let line = 1; + + if (matches) { + uri = matches[1]; + line = matches[2]; + } + + let item = DebuggerView.Sources.getItemForAttachment(a => a.source.url === uri); + if (item) { + let actor = item.attachment.source.actor; + DebuggerView.setEditorLocation( + actor, line, {noDebug: true} + ).then(() => { + if (dom0) { + let text = DebuggerView.editor.getText(); + let index = text.indexOf(searchString); + let lastIndex = text.lastIndexOf(searchString); + + // To avoid confusion we only search for DOM0 event handlers when + // there is only one possible match in the file. + if (index !== -1 && index === lastIndex) { + text = text.substr(0, index); + let newlineMatches = text.match(/\n/g); + + if (newlineMatches) { + DebuggerView.editor.setCursor({ + line: newlineMatches.length + }); + } + } + } + }); + } + }; + + let debuggerAlreadyOpen = toolbox.getPanel("jsdebugger"); + toolbox.selectTool("jsdebugger").then(({ panelWin: dbg }) => { + if (debuggerAlreadyOpen) { + showSource(dbg); + } else { + dbg.once(dbg.EVENTS.SOURCES_ADDED, () => showSource(dbg)); + } + }); + } + }, + + destroy: function () { + if (this._tooltip) { + this._tooltip.off("hidden", this.destroy); + + let boxes = this.container.querySelectorAll(".event-tooltip-content-box"); + + for (let box of boxes) { + let {editor} = this._eventEditors.get(box); + editor.destroy(); + } + + this._eventEditors = null; + this._tooltip.eventTooltip = null; + } + + let headerNodes = this.container.querySelectorAll(".event-header"); + + for (let node of headerNodes) { + node.removeEventListener("click", this._headerClicked); + } + + let sourceNodes = this.container.querySelectorAll(".event-tooltip-debugger-icon"); + for (let node of sourceNodes) { + node.removeEventListener("click", this._debugClicked); + } + + this._eventListenerInfos = this._toolbox = this._tooltip = null; + } +}; + +module.exports.setEventTooltip = setEventTooltip; |