/* vim: set 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 Heritage = require("sdk/core/heritage"); const XHTML_NS = "http://www.w3.org/1999/xhtml"; const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; // Constants for compatibility with the Web Console output implementation before // bug 778766. // TODO: remove these once bug 778766 is fixed. const COMPAT = { // The various categories of messages. CATEGORIES: { NETWORK: 0, CSS: 1, JS: 2, WEBDEV: 3, INPUT: 4, OUTPUT: 5, SECURITY: 6, }, // The possible message severities. SEVERITIES: { ERROR: 0, WARNING: 1, INFO: 2, LOG: 3, }, }; /** * The ConsoleOutput object is used to manage output of messages in the Web * Console. * * @constructor * @param object owner * The console output owner. This usually the WebConsoleFrame instance. * Any other object can be used, as long as it has the following * properties and methods: * - window * - document * - outputMessage(category, methodOrNode[, methodArguments]) * TODO: this is needed temporarily, until bug 778766 is fixed. */ function ConsoleOutput(owner) { this.owner = owner; this._onFlushOutputMessage = this._onFlushOutputMessage.bind(this); } ConsoleOutput.prototype = { /** * The output container. * @type DOMElement */ get element() { return this.owner.outputNode; }, /** * The document that holds the output. * @type DOMDocument */ get document() { return this.owner.document; }, /** * The DOM window that holds the output. * @type Window */ get window() { return this.owner.window; }, /** * Add a message to output. * * @param object ...args * Any number of Message objects. * @return this */ addMessage: function(...args) { for (let msg of args) { msg.init(this); this.owner.outputMessage(msg._categoryCompat, this._onFlushOutputMessage, [msg]); } return this; }, /** * Message renderer used for compatibility with the current Web Console output * implementation. This method is invoked for every message object that is * flushed to output. The message object is initialized and rendered, then it * is displayed. * * TODO: remove this method once bug 778766 is fixed. * * @private * @param object message * The message object to render. * @return DOMElement * The message DOM element that can be added to the console output. */ _onFlushOutputMessage: function(message) { return message.render().element; }, /** * Get an array of selected messages. This list is based on the text selection * start and end points. * * @param number [limit] * Optional limit of selected messages you want. If no value is given, * all of the selected messages are returned. * @return array * Array of DOM elements for each message that is currently selected. */ getSelectedMessages: function(limit) { let selection = this.window.getSelection(); if (selection.isCollapsed) { return []; } if (selection.containsNode(this.element, true)) { return Array.slice(this.element.children); } let anchor = this.getMessageForElement(selection.anchorNode); let focus = this.getMessageForElement(selection.focusNode); if (!anchor || !focus) { return []; } let start, end; if (anchor.timestamp > focus.timestamp) { start = focus; end = anchor; } else { start = anchor; end = focus; } let result = []; let current = start; while (current) { result.push(current); if (current == end || (limit && result.length == limit)) { break; } current = current.nextSibling; } return result; }, /** * Find the DOM element of a message for any given descendant. * * @param DOMElement elem * The element to start the search from. * @return DOMElement|null * The DOM element of the message, if any. */ getMessageForElement: function(elem) { while (elem && elem.parentNode) { if (elem.classList && elem.classList.contains("message")) { return elem; } elem = elem.parentNode; } return null; }, /** * Select all messages. */ selectAllMessages: function() { let selection = this.window.getSelection(); selection.removeAllRanges(); let range = this.document.createRange(); range.selectNodeContents(this.element); selection.addRange(range); }, /** * Add a message to the selection. * * @param DOMElement elem * The message element to select. */ selectMessage: function(elem) { let selection = this.window.getSelection(); selection.removeAllRanges(); let range = this.document.createRange(); range.selectNodeContents(elem); selection.addRange(range); }, /** * Destroy this ConsoleOutput instance. */ destroy: function() { this.owner = null; }, }; // ConsoleOutput.prototype /** * Message objects container. * @type object */ let Messages = {}; /** * The BaseMessage object is used for all types of messages. Every kind of * message should use this object as its base. * * @constructor */ Messages.BaseMessage = function() { this.widgets = new Set(); }; Messages.BaseMessage.prototype = { /** * Reference to the ConsoleOutput owner. * * @type object|null * This is |null| if the message is not yet initialized. */ output: null, /** * Reference to the parent message object, if this message is in a group or if * it is otherwise owned by another message. * * @type object|null */ parent: null, /** * Message DOM element. * * @type DOMElement|null * This is |null| if the message is not yet rendered. */ element: null, /** * Tells if this message is visible or not. * @type boolean */ get visible() { return this.element && this.element.parentNode; }, /** * Holds the text-only representation of the message. * @type string */ textContent: "", /** * Set of widgets included in this message. * @type Set */ widgets: null, // Properties that allow compatibility with the current Web Console output // implementation. _categoryCompat: null, _severityCompat: null, _categoryNameCompat: null, _severityNameCompat: null, _filterKeyCompat: null, /** * Initialize the message. * * @param object output * The ConsoleOutput owner. * @param object [parent=null] * Optional: a different message object that owns this instance. * @return this */ init: function(output, parent=null) { this.output = output; this.parent = parent; return this; }, /** * Render the message. After this method is invoked the |element| property * will point to the DOM element of this message. * @return this */ render: function() { if (!this.element) { this.element = this._renderCompat(); } return this; }, /** * Prepare the message container for the Web Console, such that it is * compatible with the current implementation. * TODO: remove this once bug 778766. */ _renderCompat: function() { let doc = this.output.document; let container = doc.createElementNS(XHTML_NS, "div"); container.id = "console-msg-" + gSequenceId(); container.className = "message"; container.category = this._categoryCompat; container.severity = this._severityCompat; container.setAttribute("category", this._categoryNameCompat); container.setAttribute("severity", this._severityNameCompat); container.setAttribute("filter", this._filterKeyCompat); container.clipboardText = this.textContent; container.timestamp = this.timestamp; container._messageObject = this; return container; }, }; // Messages.BaseMessage.prototype /** * The NavigationMarker is used to show a page load event. * * @constructor * @extends Messages.BaseMessage * @param string url * The URL to display. * @param number timestamp * The message date and time, milliseconds elapsed since 1 January 1970 * 00:00:00 UTC. */ Messages.NavigationMarker = function(url, timestamp) { Messages.BaseMessage.apply(this, arguments); this._url = url; this.textContent = "------ " + url; this.timestamp = timestamp; }; Messages.NavigationMarker.prototype = Heritage.extend(Messages.BaseMessage.prototype, { /** * Message timestamp. * * @type number * Milliseconds elapsed since 1 January 1970 00:00:00 UTC. */ timestamp: 0, _categoryCompat: COMPAT.CATEGORIES.NETWORK, _severityCompat: COMPAT.SEVERITIES.LOG, _categoryNameCompat: "network", _severityNameCompat: "info", _filterKeyCompat: "networkinfo", /** * Prepare the DOM element for this message. * @return this */ render: function() { if (this.element) { return this; } let url = this._url; let pos = url.indexOf("?"); if (pos > -1) { url = url.substr(0, pos); } let doc = this.output.document; let urlnode = doc.createElementNS(XHTML_NS, "a"); urlnode.className = "url"; urlnode.textContent = url; urlnode.title = this._url; urlnode.href = this._url; urlnode.draggable = false; // This is going into the WebConsoleFrame object instance that owns // the ConsoleOutput object. The WebConsoleFrame owner is the WebConsole // object instance from hudservice.js. // TODO: move _addMessageLinkCallback() into ConsoleOutput once bug 778766 // is fixed. this.output.owner._addMessageLinkCallback(urlnode, () => { this.output.owner.owner.openLink(this._url); }); let render = Messages.BaseMessage.prototype.render.bind(this); render().element.appendChild(urlnode); this.element.classList.add("navigation-marker"); this.element.url = this._url; this.element.appendChild(doc.createTextNode("\n")); return this; }, }); // Messages.NavigationMarker.prototype function gSequenceId() { return gSequenceId.n++; } gSequenceId.n = 0; exports.ConsoleOutput = ConsoleOutput; exports.Messages = Messages;