/* -*- 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;