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))
193 if (auto && !this._caretAtEndOfPrompt())
196 // Pass more characters to _backwardsRange so the range will be as short as possible.
197 var wordPrefixRange = this._backwardsRange(" .=:[({;", selectionRange.startContainer, selectionRange.startOffset, this.promptElement);
199 var completions = this.completions(wordPrefixRange, auto);
201 if (!completions || !completions.length)
204 var fullWordRange = document.createRange();
205 fullWordRange.setStart(wordPrefixRange.startContainer, wordPrefixRange.startOffset);
206 fullWordRange.setEnd(selectionRange.endContainer, selectionRange.endOffset);
208 if (completions.length === 1 || selection.isCollapsed || auto) {
209 var completionText = completions[0];
211 var currentText = fullWordRange.toString().trimTrailingWhitespace();
213 var foundIndex = null;
214 for (var i = 0; i < completions.length; ++i) {
215 if (completions[i] === currentText)
219 if (foundIndex === null || (foundIndex + 1) >= completions.length)
220 var completionText = completions[0];
222 var completionText = completions[foundIndex + 1];
225 var wordPrefixLength = wordPrefixRange.toString().length;
227 fullWordRange.deleteContents();
229 var finalSelectionRange = document.createRange();
232 var prefixText = completionText.substring(0, wordPrefixLength);
233 var suffixText = completionText.substring(wordPrefixLength);
235 var prefixTextNode = document.createTextNode(prefixText);
236 fullWordRange.insertNode(prefixTextNode);
238 this.autoCompleteElement = document.createElement("span");
239 this.autoCompleteElement.className = "auto-complete-text";
240 this.autoCompleteElement.textContent = suffixText;
242 prefixTextNode.parentNode.insertBefore(this.autoCompleteElement, prefixTextNode.nextSibling);
244 finalSelectionRange.setStart(prefixTextNode, wordPrefixLength);
245 finalSelectionRange.setEnd(prefixTextNode, wordPrefixLength);
247 var completionTextNode = document.createTextNode(completionText);
248 fullWordRange.insertNode(completionTextNode);
250 if (completions.length > 1)
251 finalSelectionRange.setStart(completionTextNode, wordPrefixLength);
253 finalSelectionRange.setStart(completionTextNode, completionText.length);
255 finalSelectionRange.setEnd(completionTextNode, completionText.length);
258 selection.removeAllRanges();
259 selection.addRange(finalSelectionRange);
262 completions: function(wordRange, bestMatchOnly)
264 // Pass less characters to _backwardsRange so the range will be a more complete expression.
265 var expression = this._backwardsRange(" =:{;", wordRange.startContainer, wordRange.startOffset, this.promptElement);
266 var expressionString = expression.toString();
267 var lastIndex = expressionString.length - 1;
269 var dotNotation = (expressionString[lastIndex] === ".");
270 var bracketNotation = (expressionString[lastIndex] === "[");
272 if (dotNotation || bracketNotation)
273 expressionString = expressionString.substr(0, lastIndex);
275 var prefix = wordRange.toString();
276 if (!expressionString && !prefix)
279 var result = InspectorController.inspectedWindow();
280 if (expressionString) {
282 result = this._evalInInspectedWindow(expressionString);
284 // Do nothing, the prefix will be considered a window property.
288 if (bracketNotation) {
289 if (prefix.length && prefix[0] === "'")
292 var quoteUsed = "\"";
296 var properties = Object.sortedProperties(result);
297 for (var i = 0; i < properties.length; ++i) {
298 var property = properties[i];
300 property = quoteUsed + property.escapeCharacters(quoteUsed + "\\") + quoteUsed + "]";
301 if (property.length < prefix.length)
303 if (property.indexOf(prefix) !== 0)
305 results.push(property);
313 clearButtonClicked: function()
315 this.clearMessages();
318 messagesSelectStart: function(event)
320 if (this._selectionTimeout)
321 clearTimeout(this._selectionTimeout);
323 this.clearAutoComplete();
325 function moveBackIfOutside()
327 delete this._selectionTimeout;
328 if (!this._caretInsidePrompt() && window.getSelection().isCollapsed)
329 this._moveCaretToEndOfPrompt();
330 this.autoCompleteSoon();
333 this._selectionTimeout = setTimeout(moveBackIfOutside.bind(this), 100);
336 messagesClicked: function(event)
338 var link = event.target.enclosingNodeOrSelfWithNodeName("a");
339 if (link && link.representedNode) {
340 WebInspector.updateFocusedNode(link.representedNode);
344 var messageElement = event.target.enclosingNodeOrSelfWithClass("console-message");
348 if (!messageElement.message)
351 var resource = messageElement.message.resource;
355 if (link && link.hasStyleClass("console-message-url")) {
356 WebInspector.navigateToResource(resource);
357 resource.panel.showSourceLine(item.message.line);
360 event.stopPropagation();
361 event.preventDefault();
364 promptKeyDown: function(event)
366 switch (event.keyIdentifier) {
368 this._onEnterPressed(event);
371 this._onUpPressed(event);
374 this._onDownPressed(event);
376 case "U+0009": // Tab
377 this._onTabPressed(event);
380 if (!this.acceptAutoComplete())
381 this.autoCompleteSoon();
384 this.clearAutoComplete();
385 this.autoCompleteSoon();
390 _backwardsRange: function(stopCharacters, endNode, endOffset, stayWithinElement)
397 if (node === stayWithinElement) {
399 startNode = stayWithinElement;
403 if (node.nodeType === Node.TEXT_NODE) {
404 var start = (node === endNode ? endOffset : node.nodeValue.length);
405 for (var i = (start - 1); i >= 0; --i) {
406 var character = node.nodeValue[i];
407 if (stopCharacters.indexOf(character) !== -1) {
418 node = node.traversePreviousNode();
421 var result = document.createRange();
422 result.setStart(startNode, startOffset);
423 result.setEnd(endNode, endOffset);
428 _evalInInspectedWindow: function(expression)
430 // This with block is needed to work around http://bugs.webkit.org/show_bug.cgi?id=11399
431 with (InspectorController.inspectedWindow()) {
432 return eval(expression);
436 _caretInsidePrompt: function()
438 var selection = window.getSelection();
439 if (!selection.rangeCount || !selection.isCollapsed)
441 var selectionRange = selection.getRangeAt(0);
442 return selectionRange.startContainer === this.promptElement || selectionRange.startContainer.isDescendant(this.promptElement);
445 _caretAtEndOfPrompt: function()
447 var selection = window.getSelection();
448 if (!selection.rangeCount || !selection.isCollapsed)
451 var selectionRange = selection.getRangeAt(0);
452 var node = selectionRange.startContainer;
453 if (node !== this.promptElement && !node.isDescendant(this.promptElement))
456 if (node.nodeType === Node.TEXT_NODE && selectionRange.startOffset < node.nodeValue.length)
459 var foundNextText = false;
461 if (node.nodeType === Node.TEXT_NODE && node.nodeValue.length) {
464 foundNextText = true;
467 node = node.traverseNextNode(false, this.promptElement);
473 _moveCaretToEndOfPrompt: function()
475 var selection = window.getSelection();
476 var selectionRange = document.createRange();
478 var offset = this.promptElement.childNodes.length;
479 selectionRange.setStart(this.promptElement, offset);
480 selectionRange.setEnd(this.promptElement, offset);
482 selection.removeAllRanges();
483 selection.addRange(selectionRange);
486 _onTabPressed: function(event)
488 event.preventDefault();
489 event.stopPropagation();
493 _onEnterPressed: function(event)
495 event.preventDefault();
496 event.stopPropagation();
498 this.clearAutoComplete(true);
500 var str = this.promptText;
504 this.commandHistory.push(str);
505 this.commandOffset = 0;
507 this.promptText = "";
510 var exception = false;
512 result = this._evalInInspectedWindow(str);
518 var level = exception ? WebInspector.ConsoleMessage.MessageLevel.Error : WebInspector.ConsoleMessage.MessageLevel.Log;
519 this.addMessage(new WebInspector.ConsoleCommand(str, result, this._format(result), level));
522 _onUpPressed: function(event)
524 event.preventDefault();
525 event.stopPropagation();
527 if (this.commandOffset == this.commandHistory.length)
530 if (this.commandOffset == 0)
531 this.tempSavedCommand = this.promptText;
533 ++this.commandOffset;
534 this.promptText = this.commandHistory[this.commandHistory.length - this.commandOffset];
537 _onDownPressed: function(event)
539 event.preventDefault();
540 event.stopPropagation();
542 if (this.commandOffset == 0)
545 --this.commandOffset;
547 if (this.commandOffset == 0) {
548 this.promptText = this.tempSavedCommand;
549 delete this.tempSavedCommand;
553 this.promptText = this.commandHistory[this.commandHistory.length - this.commandOffset];
556 _format: function(output)
558 var type = Object.type(output);
559 if (type === "object") {
560 if (output instanceof Node)
564 // We don't perform any special formatting on these types, so we just
565 // pass them through the simple _formatvalue function.
566 var undecoratedTypes = {
576 if (type in undecoratedTypes)
577 formatter = "_formatvalue";
579 formatter = "_format" + type;
580 if (!(formatter in this)) {
581 formatter = "_formatobject";
586 var span = document.createElement("span");
587 span.addStyleClass("console-formatted-" + type);
588 this[formatter](output, span);
592 _formatvalue: function(val, elem)
594 elem.appendChild(document.createTextNode(val));
597 _formatstring: function(str, elem)
599 elem.appendChild(document.createTextNode("\"" + str + "\""));
602 _formatregexp: function(re, elem)
604 var formatted = String(re).replace(/([\\\/])/g, "\\$1").replace(/\\(\/[gim]*)$/, "$1").substring(1);
605 elem.appendChild(document.createTextNode(formatted));
608 _formatarray: function(arr, elem)
610 elem.appendChild(document.createTextNode("["));
611 for (var i = 0; i < arr.length; ++i) {
612 elem.appendChild(this._format(arr[i]));
613 if (i < arr.length - 1)
614 elem.appendChild(document.createTextNode(", "));
616 elem.appendChild(document.createTextNode("]"));
619 _formatnode: function(node, elem)
621 var anchor = document.createElement("a");
622 anchor.innerHTML = node.titleInfo().title;
623 anchor.representedNode = node;
624 anchor.addEventListener("mouseover", function() { InspectorController.highlightDOMNode(node) }, false);
625 anchor.addEventListener("mouseout", function() { InspectorController.hideDOMNodeHighlight() }, false);
626 elem.appendChild(anchor);
629 _formatobject: function(obj, elem)
631 elem.appendChild(document.createTextNode(Object.describe(obj)));
635 WebInspector.ConsolePanel.prototype.__proto__ = WebInspector.Panel.prototype;
637 WebInspector.ConsoleMessage = function(source, level, message, line, url)
639 this.source = source;
641 this.message = message;
646 WebInspector.ConsoleMessage.prototype = {
650 return this.resource.displayName;
654 toMessageElement: function()
656 var element = document.createElement("div");
657 element.message = this;
658 element.className = "console-message";
660 switch (this.source) {
661 case WebInspector.ConsoleMessage.MessageSource.HTML:
662 element.addStyleClass("console-html-source");
664 case WebInspector.ConsoleMessage.MessageSource.XML:
665 element.addStyleClass("console-xml-source");
667 case WebInspector.ConsoleMessage.MessageSource.JS:
668 element.addStyleClass("console-js-source");
670 case WebInspector.ConsoleMessage.MessageSource.CSS:
671 element.addStyleClass("console-css-source");
673 case WebInspector.ConsoleMessage.MessageSource.Other:
674 element.addStyleClass("console-other-source");
678 switch (this.level) {
679 case WebInspector.ConsoleMessage.MessageLevel.Tip:
680 element.addStyleClass("console-tip-level");
682 case WebInspector.ConsoleMessage.MessageLevel.Log:
683 element.addStyleClass("console-log-level");
685 case WebInspector.ConsoleMessage.MessageLevel.Warning:
686 element.addStyleClass("console-warning-level");
688 case WebInspector.ConsoleMessage.MessageLevel.Error:
689 element.addStyleClass("console-error-level");
692 var messageTextElement = document.createElement("span");
693 messageTextElement.className = "console-message-text";
694 messageTextElement.textContent = this.message;
695 element.appendChild(messageTextElement);
697 element.appendChild(document.createTextNode(" "));
699 if (this.url && this.url !== "undefined") {
700 var urlElement = document.createElement("a");
701 urlElement.className = "console-message-url";
704 urlElement.textContent = WebInspector.UIString("%s (line %d)", this.url, this.line);
706 urlElement.textContent = this.url;
708 element.appendChild(urlElement);
717 switch (this.source) {
718 case WebInspector.ConsoleMessage.MessageSource.HTML:
719 sourceString = "HTML";
721 case WebInspector.ConsoleMessage.MessageSource.XML:
722 sourceString = "XML";
724 case WebInspector.ConsoleMessage.MessageSource.JS:
727 case WebInspector.ConsoleMessage.MessageSource.CSS:
728 sourceString = "CSS";
730 case WebInspector.ConsoleMessage.MessageSource.Other:
731 sourceString = "Other";
736 switch (this.level) {
737 case WebInspector.ConsoleMessage.MessageLevel.Tip:
740 case WebInspector.ConsoleMessage.MessageLevel.Log:
743 case WebInspector.ConsoleMessage.MessageLevel.Warning:
744 levelString = "Warning";
746 case WebInspector.ConsoleMessage.MessageLevel.Error:
747 levelString = "Error";
751 return sourceString + " " + levelString + ": " + this.message + "\n" + this.url + " line " + this.line;
755 // Note: Keep these constants in sync with the ones in Chrome.h
756 WebInspector.ConsoleMessage.MessageSource = {
764 WebInspector.ConsoleMessage.MessageLevel = {
771 WebInspector.ConsoleCommand = function(command, result, formattedResultElement, level)
773 this.command = command;
774 this.formattedResultElement = formattedResultElement;
778 WebInspector.ConsoleCommand.prototype = {
779 toMessageElement: function()
781 var element = document.createElement("div");
782 element.command = this;
783 element.className = "console-user-command";
785 var commandTextElement = document.createElement("span");
786 commandTextElement.className = "console-message-text";
787 commandTextElement.textContent = this.command;
788 element.appendChild(commandTextElement);
790 var resultElement = document.createElement("div");
791 resultElement.className = "console-message";
792 element.appendChild(resultElement);
794 switch (this.level) {
795 case WebInspector.ConsoleMessage.MessageLevel.Log:
796 resultElement.addStyleClass("console-log-level");
798 case WebInspector.ConsoleMessage.MessageLevel.Warning:
799 resultElement.addStyleClass("console-warning-level");
801 case WebInspector.ConsoleMessage.MessageLevel.Error:
802 resultElement.addStyleClass("console-error-level");
805 var resultTextElement = document.createElement("span");
806 resultTextElement.className = "console-message-text";
807 resultTextElement.appendChild(this.formattedResultElement);
808 resultElement.appendChild(resultTextElement);