2 * Copyright (C) 2007, 2008 Apple Inc. All rights reserved.
4 * Redistribution and use in source and binary forms, with or without
5 * modification, are permitted provided that the following conditions
8 * 1. Redistributions of source code must retain the above copyright
9 * notice, this list of conditions and the following disclaimer.
10 * 2. Redistributions in binary form must reproduce the above copyright
11 * notice, this list of conditions and the following disclaimer in the
12 * documentation and/or other materials provided with the distribution.
13 * 3. Neither the name of Apple Computer, Inc. ("Apple") nor the names of
14 * its contributors may be used to endorse or promote products derived
15 * from this software without specific prior written permission.
17 * THIS SOFTWARE IS PROVIDED BY APPLE AND ITS CONTRIBUTORS "AS IS" AND ANY
18 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
19 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
20 * DISCLAIMED. IN NO EVENT SHALL APPLE OR ITS CONTRIBUTORS BE LIABLE FOR ANY
21 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
22 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
23 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
24 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF
26 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29 WebInspector.ConsolePanel = function()
31 WebInspector.Panel.call(this);
35 this.commandHistory = [];
36 this.commandOffset = 0;
38 this.messagesElement = document.createElement("div");
39 this.messagesElement.id = "console-messages";
40 this.messagesElement.addEventListener("selectstart", this.messagesSelectStart.bind(this), true);
41 this.messagesElement.addEventListener("click", this.messagesClicked.bind(this), true);
42 this.element.appendChild(this.messagesElement);
44 this.promptElement = document.createElement("div");
45 this.promptElement.id = "console-prompt";
46 this.promptElement.addEventListener("keydown", this.promptKeyDown.bind(this), false);
47 this.promptElement.appendChild(document.createElement("br"));
48 this.messagesElement.appendChild(this.promptElement);
50 this.clearButton = document.createElement("button");
51 this.clearButton.title = WebInspector.UIString("Clear");
52 this.clearButton.textContent = WebInspector.UIString("Clear");
53 this.clearButton.addEventListener("click", this.clearButtonClicked.bind(this), false);
56 WebInspector.ConsolePanel.prototype = {
59 return this.promptElement.textContent;
65 // Append a break element instead of setting textContent to make sure the selection is inside the prompt.
66 this.promptElement.removeChildren();
67 this.promptElement.appendChild(document.createElement("br"));
69 this.promptElement.textContent = x;
71 this._moveCaretToEndOfPrompt();
76 WebInspector.Panel.prototype.show.call(this);
77 WebInspector.consoleListItem.select();
79 this.clearButton.removeStyleClass("hidden");
80 if (!this.clearButton.parentNode)
81 document.getElementById("toolbarButtons").appendChild(this.clearButton);
83 WebInspector.currentFocusElement = document.getElementById("main");
85 function focusPrompt()
87 if (!this._caretInsidePrompt())
88 this._moveCaretToEndOfPrompt();
91 setTimeout(focusPrompt.bind(this), 0);
96 WebInspector.Panel.prototype.hide.call(this);
97 WebInspector.consoleListItem.deselect();
98 this.clearButton.addStyleClass("hidden");
101 addMessage: function(msg)
103 if (msg.url in WebInspector.resourceURLMap) {
104 msg.resource = WebInspector.resourceURLMap[msg.url];
106 case WebInspector.ConsoleMessage.MessageLevel.Warning:
107 ++msg.resource.warnings;
108 msg.resource.panel.addMessageToSource(msg);
110 case WebInspector.ConsoleMessage.MessageLevel.Error:
111 ++msg.resource.errors;
112 msg.resource.panel.addMessageToSource(msg);
117 this.messages.push(msg);
119 var element = msg.toMessageElement();
120 this.messagesElement.insertBefore(element, this.promptElement);
121 this.promptElement.scrollIntoView(false);
124 clearMessages: function()
126 for (var i = 0; i < this.messages.length; ++i) {
127 var resource = this.messages[i].resource;
131 resource.warnings = 0;
136 while (this.messagesElement.firstChild != this.promptElement)
137 this.messagesElement.removeChild(this.messagesElement.firstChild);
140 acceptAutoComplete: function()
142 if (!this.autoCompleteElement || !this.autoCompleteElement.parentNode)
145 var text = this.autoCompleteElement.textContent;
146 var textNode = document.createTextNode(text);
147 this.autoCompleteElement.parentNode.replaceChild(textNode, this.autoCompleteElement);
148 delete this.autoCompleteElement;
150 var finalSelectionRange = document.createRange();
151 finalSelectionRange.setStart(textNode, text.length);
152 finalSelectionRange.setEnd(textNode, text.length);
154 var selection = window.getSelection();
155 selection.removeAllRanges();
156 selection.addRange(finalSelectionRange);
161 clearAutoComplete: function(includeTimeout)
163 if (includeTimeout && "completeTimeout" in this) {
164 clearTimeout(this.completeTimeout);
165 delete this.completeTimeout;
168 if (!this.autoCompleteElement)
171 if (this.autoCompleteElement.parentNode)
172 this.autoCompleteElement.parentNode.removeChild(this.autoCompleteElement);
173 delete this.autoCompleteElement;
176 autoCompleteSoon: function()
178 if (!("completeTimeout" in this))
179 this.completeTimeout = setTimeout(this.complete.bind(this, true), 250);
182 complete: function(auto)
184 this.clearAutoComplete(true);
186 var selection = window.getSelection();
187 if (!selection.rangeCount)
190 var selectionRange = selection.getRangeAt(0);
191 if (!selectionRange.commonAncestorContainer.isDescendant(this.promptElement))
195 if (!selection.isCollapsed)
198 var node = selectionRange.startContainer;
199 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
202 var foundNextText = false;
204 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
207 foundNextText = true;
210 node = node.traverseNextNode(false, this.promptElement);
214 // Pass more characters to _backwardsRange so the range will be as short as possible.
215 var wordPrefixRange = this._backwardsRange(" .=:[({;", selectionRange.startContainer, selectionRange.startOffset, this.promptElement);
217 var completions = this.completions(wordPrefixRange, auto);
219 if (!completions || !completions.length)
222 var fullWordRange = document.createRange();
223 fullWordRange.setStart(wordPrefixRange.startContainer, wordPrefixRange.startOffset);
224 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
226 if (completions.length === 1 || selection.isCollapsed || auto) {
227 var completionText = completions[0];
229 var currentText = fullWordRange.toString().trimTrailingWhitespace();
231 var foundIndex = null;
232 for (var i = 0; i < completions.length; ++i) {
233 if (completions[i] === currentText)
237 if (foundIndex === null || (foundIndex + 1) >= completions.length)
238 var completionText = completions[0];
240 var completionText = completions[foundIndex + 1];
243 var wordPrefixLength = wordPrefixRange.toString().length;
245 fullWordRange.deleteContents();
247 var finalSelectionRange = document.createRange();
250 var prefixText = completionText.substring(0, wordPrefixLength);
251 var suffixText = completionText.substring(wordPrefixLength);
253 var prefixTextNode = document.createTextNode(prefixText);
254 fullWordRange.insertNode(prefixTextNode);
256 this.autoCompleteElement = document.createElement("span");
257 this.autoCompleteElement.className = "auto-complete-text";
258 this.autoCompleteElement.textContent = suffixText;
260 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
262 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
263 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
265 var completionTextNode = document.createTextNode(completionText);
266 fullWordRange.insertNode(completionTextNode);
268 if (completions.length > 1)
269 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
271 finalSelectionRange.setStart(completionTextNode, completionText.length);
273 finalSelectionRange.setEnd(completionTextNode, completionText.length);
276 selection.removeAllRanges();
277 selection.addRange(finalSelectionRange);
280 completions: function(wordRange, bestMatchOnly)
282 // Pass less characters to _backwardsRange so the range will be a more complete expression.
283 var expression = this._backwardsRange(" =:{;", wordRange.startContainer, wordRange.startOffset, this.promptElement);
284 var expressionString = expression.toString();
285 var lastIndex = expressionString.length - 1;
287 var dotNotation = (expressionString[lastIndex] === ".");
288 var bracketNotation = (expressionString[lastIndex] === "[");
290 if (dotNotation || bracketNotation)
291 expressionString = expressionString.substr(0, lastIndex);
293 var prefix = wordRange.toString();
294 if (!expressionString && !prefix)
297 var result = InspectorController.inspectedWindow();
298 if (expressionString) {
300 result = this._evalInInspectedWindow(expressionString);
302 // Do nothing, the prefix will be considered a window property.
306 if (bracketNotation) {
307 if (prefix.length && prefix[0] === "'")
310 var quoteUsed = "\"";
314 var properties = Object.sortedProperties(result);
315 for (var i = 0; i < properties.length; ++i) {
316 var property = properties[i];
318 property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed + "]";
319 if (property.length < prefix.length)
321 if (property.indexOf(prefix) !== 0)
323 results.push(property);
331 clearButtonClicked: function()
333 this.clearMessages();
336 messagesSelectStart: function(event)
338 if (this._selectionTimeout)
339 clearTimeout(this._selectionTimeout);
341 function moveBackIfOutside()
343 delete this._selectionTimeout;
344 if (this._caretInsidePrompt() || !window.getSelection().isCollapsed)
346 this._moveCaretToEndOfPrompt();
349 this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
352 messagesClicked: function(event)
354 var link = event.target.firstParentOrSelfWithNodeName("a");
355 if (link && link.representedNode) {
356 WebInspector.updateFocusedNode(link.representedNode);
360 var messageElement = event.target.firstParentOrSelfWithClass("console-message");
364 if (!messageElement.message)
367 var resource = messageElement.message.resource;
371 if (link && link.hasStyleClass("console-message-url")) {
372 WebInspector.navigateToResource(resource);
373 resource.panel.showSourceLine(item.message.line);
376 event.stopPropagation();
377 event.preventDefault();
380 promptKeyDown: function(event)
382 switch (event.keyIdentifier) {
384 this._onEnterPressed(event);
387 this._onUpPressed(event);
390 this._onDownPressed(event);
392 case "U+0009": // Tab
393 this._onTabPressed(event);
396 if (!this.acceptAutoComplete())
397 this.autoCompleteSoon();
400 this.clearAutoComplete();
401 this.autoCompleteSoon();
406 _backwardsRange: function(stopCharacters, endNode, endOffset, stayWithinElement)
413 if (node === stayWithinElement) {
415 startNode = stayWithinElement;
419 if (node.nodeType === Node.TEXT_NODE) {
420 var start = (node === endNode ? endOffset : node.nodeValue.length);
421 for (var i = (start - 1); i >= 0; --i) {
422 var character = node.nodeValue[i];
423 if (stopCharacters.indexOf(character) !== -1) {
434 node = node.traversePreviousNode();
437 var result = document.createRange();
438 result.setStart(startNode, startOffset);
439 result.setEnd(endNode, endOffset);
444 _evalInInspectedWindow: function(expression)
446 // This with block is needed to work around http://bugs.webkit.org/show_bug.cgi?id=11399
447 with (InspectorController.inspectedWindow()) {
448 return eval(expression);
452 _caretInsidePrompt: function()
454 var selection = window.getSelection();
455 if (!selection.rangeCount || !selection.isCollapsed)
457 var selectionRange = selection.getRangeAt(0);
458 return selectionRange.startContainer === this.promptElement && selectionRange.startContainer.isDescendant(this.promptElement);
461 _moveCaretToEndOfPrompt: function()
463 var selection = window.getSelection();
464 var selectionRange = document.createRange();
466 var offset = this.promptElement.firstChild ? 1 : 0;
467 selectionRange.setStart(this.promptElement, offset);
468 selectionRange.setEnd(this.promptElement, offset);
470 selection.removeAllRanges();
471 selection.addRange(selectionRange);
474 _onTabPressed: function(event)
476 event.preventDefault();
477 event.stopPropagation();
481 _onEnterPressed: function(event)
483 event.preventDefault();
484 event.stopPropagation();
486 this.clearAutoComplete(true);
488 var str = this.promptText;
492 this.commandHistory.push(str);
493 this.commandOffset = 0;
495 this.promptText = "";
498 var exception = false;
500 result = this._evalInInspectedWindow(str);
506 var level = exception ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log;
507 this.addMessage(new WebInspector.ConsoleCommand(str, result, this._format(result), level));
510 _onUpPressed: function(event)
512 event.preventDefault();
513 event.stopPropagation();
515 if (this.commandOffset == this.commandHistory.length)
518 if (this.commandOffset == 0)
519 this.tempSavedCommand = this.promptText;
521 ++this.commandOffset;
522 this.promptText = this.commandHistory[this.commandHistory.length - this.commandOffset];
525 _onDownPressed: function(event)
527 event.preventDefault();
528 event.stopPropagation();
530 if (this.commandOffset == 0)
533 --this.commandOffset;
535 if (this.commandOffset == 0) {
536 this.promptText = this.tempSavedCommand;
537 delete this.tempSavedCommand;
541 this.promptText = this.commandHistory[this.commandHistory.length - this.commandOffset];
544 _format: function(output)
546 var type = Object.type(output);
547 if (type === "object") {
548 if (output instanceof Node)
552 // We don't perform any special formatting on these types, so we just
553 // pass them through the simple _formatvalue function.
554 var undecoratedTypes = {
564 if (type in undecoratedTypes)
565 formatter = "_formatvalue";
567 formatter = "_format" + type;
568 if (!(formatter in this)) {
569 formatter = "_formatobject";
574 var span = document.createElement("span");
575 span.addStyleClass("console-formatted-" + type);
576 this[formatter](output, span);
580 _formatvalue: function(val, elem)
582 elem.appendChild(document.createTextNode(val));
585 _formatstring: function(str, elem)
587 elem.appendChild(document.createTextNode("\"" + str + "\""));
590 _formatregexp: function(re, elem)
592 var formatted = String(re).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
593 elem.appendChild(document.createTextNode(formatted));
596 _formatarray: function(arr, elem)
598 elem.appendChild(document.createTextNode("["));
599 for (var i = 0; i < arr.length; ++i) {
600 elem.appendChild(this._format(arr[i]));
601 if (i < arr.length - 1)
602 elem.appendChild(document.createTextNode(", "));
604 elem.appendChild(document.createTextNode("]"));
607 _formatnode: function(node, elem)
609 var anchor = document.createElement("a");
610 anchor.innerHTML = node.titleInfo().title;
611 anchor.representedNode = node;
612 elem.appendChild(anchor);
615 _formatobject: function(obj, elem)
617 elem.appendChild(document.createTextNode(Object.describe(obj)));
621 WebInspector.ConsolePanel.prototype.__proto__ = WebInspector.Panel.prototype;
623 WebInspector.ConsoleMessage = function(source, level, message, line, url)
625 this.source = source;
627 this.message = message;
632 WebInspector.ConsoleMessage.prototype = {
636 return this.resource.displayName;
640 toMessageElement: function()
642 var element = document.createElement("div");
643 element.message = this;
644 element.className = "console-message";
646 switch (this.source) {
647 case WebInspector.ConsoleMessage.MessageSource.HTML:
648 element.addStyleClass("console-html-source");
650 case WebInspector.ConsoleMessage.MessageSource.XML:
651 element.addStyleClass("console-xml-source");
653 case WebInspector.ConsoleMessage.MessageSource.JS:
654 element.addStyleClass("console-js-source");
656 case WebInspector.ConsoleMessage.MessageSource.CSS:
657 element.addStyleClass("console-css-source");
659 case WebInspector.ConsoleMessage.MessageSource.Other:
660 element.addStyleClass("console-other-source");
664 switch (this.level) {
665 case WebInspector.ConsoleMessage.MessageLevel.Tip:
666 element.addStyleClass("console-tip-level");
668 case WebInspector.ConsoleMessage.MessageLevel.Log:
669 element.addStyleClass("console-log-level");
671 case WebInspector.ConsoleMessage.MessageLevel.Warning:
672 element.addStyleClass("console-warning-level");
674 case WebInspector.ConsoleMessage.MessageLevel.Error:
675 element.addStyleClass("console-error-level");
678 var messageTextElement = document.createElement("span");
679 messageTextElement.className = "console-message-text";
680 messageTextElement.textContent = this.message;
681 element.appendChild(messageTextElement);
683 element.appendChild(document.createTextNode(" "));
685 if (this.url && this.url !== "undefined") {
686 var urlElement = document.createElement("a");
687 urlElement.className = "console-message-url";
690 urlElement.textContent = WebInspector.UIString("%s (line %d)", this.url, this.line);
692 urlElement.textContent = this.url;
694 element.appendChild(urlElement);
703 switch (this.source) {
704 case WebInspector.ConsoleMessage.MessageSource.HTML:
705 sourceString = "HTML";
707 case WebInspector.ConsoleMessage.MessageSource.XML:
708 sourceString = "XML";
710 case WebInspector.ConsoleMessage.MessageSource.JS:
713 case WebInspector.ConsoleMessage.MessageSource.CSS:
714 sourceString = "CSS";
716 case WebInspector.ConsoleMessage.MessageSource.Other:
717 sourceString = "Other";
722 switch (this.level) {
723 case WebInspector.ConsoleMessage.MessageLevel.Tip:
726 case WebInspector.ConsoleMessage.MessageLevel.Log:
729 case WebInspector.ConsoleMessage.MessageLevel.Warning:
730 levelString = "Warning";
732 case WebInspector.ConsoleMessage.MessageLevel.Error:
733 levelString = "Error";
737 return sourceString + " " + levelString + ": " + this.message + "\n" + this.url + " line " + this.line;
741 // Note: Keep these constants in sync with the ones in Chrome.h
742 WebInspector.ConsoleMessage.MessageSource = {
750 WebInspector.ConsoleMessage.MessageLevel = {
757 WebInspector.ConsoleCommand = function(command, result, formattedResultElement, level)
759 this.command = command;
760 this.formattedResultElement = formattedResultElement;
764 WebInspector.ConsoleCommand.prototype = {
765 toMessageElement: function()
767 var element = document.createElement("div");
768 element.command = this;
769 element.className = "console-user-command";
771 var commandTextElement = document.createElement("span");
772 commandTextElement.className = "console-message-text";
773 commandTextElement.textContent = this.command;
774 element.appendChild(commandTextElement);
776 var resultElement = document.createElement("div");
777 resultElement.className = "console-message";
778 element.appendChild(resultElement);
780 switch (this.level) {
781 case WebInspector.ConsoleMessage.MessageLevel.Log:
782 resultElement.addStyleClass("console-log-level");
784 case WebInspector.ConsoleMessage.MessageLevel.Warning:
785 resultElement.addStyleClass("console-warning-level");
787 case WebInspector.ConsoleMessage.MessageLevel.Error:
788 resultElement.addStyleClass("console-error-level");
791 var resultTextElement = document.createElement("span");
792 resultTextElement.className = "console-message-text";
793 resultTextElement.appendChild(this.formattedResultElement);
794 resultElement.appendChild(resultTextElement);