diff --git a/browser/components/tabview/test/Makefile.in b/browser/components/tabview/test/Makefile.in index 03411c594ab..614936794f5 100644 --- a/browser/components/tabview/test/Makefile.in +++ b/browser/components/tabview/test/Makefile.in @@ -169,6 +169,7 @@ _BROWSER_FILES = \ browser_tabview_bug705621.js \ browser_tabview_bug706430.js \ browser_tabview_bug706736.js \ + browser_tabview_bug707466.js \ browser_tabview_click_group.js \ browser_tabview_dragdrop.js \ browser_tabview_exit_button.js \ diff --git a/browser/components/tabview/test/browser_tabview_bug707466.js b/browser/components/tabview/test/browser_tabview_bug707466.js new file mode 100644 index 00000000000..7b7259f4ecc --- /dev/null +++ b/browser/components/tabview/test/browser_tabview_bug707466.js @@ -0,0 +1,60 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +function test() { + waitForExplicitFinish(); + + // create two groups and each group has one tab item + let newState = { + windows: [{ + tabs: [{ + entries: [{ url: "about:blank" }], + hidden: true, + attributes: {}, + extData: { + "tabview-tab": + '{"bounds":{"left":21,"top":29,"width":204,"height":153},' + + '"userSize":null,"url":"about:blank","groupID":1,' + + '"imageData":null,"title":null}' + } + },{ + entries: [{ url: "about:blank" }], + hidden: false, + attributes: {}, + extData: { + "tabview-tab": + '{"bounds":{"left":315,"top":29,"width":111,"height":84},' + + '"userSize":null,"url":"about:blank","groupID":2,' + + '"imageData":null,"title":null}' + }, + }], + selected:2, + _closedTabs: [], + extData: { + "tabview-groups": '{"nextID":3,"activeGroupId":2}', + "tabview-group": + '{"1":{"bounds":{"left":15,"top":5,"width":280,"height":232},' + + '"userSize":null,"title":"","id":1},' + + '"2":{"bounds":{"left":309,"top":5,"width":267,"height":226},' + + '"userSize":null,"title":"","id":2}}', + "tabview-ui": '{"pageBounds":{"left":0,"top":0,"width":788,"height":548}}' + }, sizemode:"normal" + }] + }; + + newWindowWithState(newState, function(win) { + registerCleanupFunction(function () win.close()); + + whenTabViewIsShown(function() { + let cw = win.TabView.getContentWindow(); + + is(cw.GroupItems.groupItems.length, 2, "There are still two groups"); + is(win.gBrowser.tabs.length, 1, "There is only one tab"); + is(cw.UI.getActiveTab(), win.gBrowser.selectedTab._tabViewTabItem, "The last tab is selected"); + + finish(); + }, win); + win.gBrowser.removeTab(win.gBrowser.selectedTab); + }); +} + diff --git a/browser/components/tabview/ui.js b/browser/components/tabview/ui.js index 9306f6488c7..2cb9ea06551 100644 --- a/browser/components/tabview/ui.js +++ b/browser/components/tabview/ui.js @@ -475,7 +475,7 @@ let UI = { } else { GroupItems.setActiveGroupItem(item); if (!options || !options.dontSetActiveTabInGroup) { - let activeTab = item.getActiveTab() + let activeTab = item.getActiveTab(); if (activeTab) this._setActiveTab(activeTab); } @@ -574,7 +574,8 @@ let UI = { TabItems.resumePainting(); }); } else { - self.clearActiveTab(); + if (!currentTab || !currentTab._tabViewTabItem) + self.clearActiveTab(); self._isChangingVisibility = false; dispatchEvent(event); diff --git a/browser/devtools/shared/Templater.jsm b/browser/devtools/shared/Templater.jsm index e27fce789d9..0dcad782b36 100644 --- a/browser/devtools/shared/Templater.jsm +++ b/browser/devtools/shared/Templater.jsm @@ -37,7 +37,7 @@ * ***** END LICENSE BLOCK ***** */ -var EXPORTED_SYMBOLS = [ "Templater" ]; +var EXPORTED_SYMBOLS = [ "Templater", "template" ]; Components.utils.import("resource://gre/modules/Services.jsm"); const Node = Components.interfaces.nsIDOMNode; @@ -45,12 +45,53 @@ const Node = Components.interfaces.nsIDOMNode; // WARNING: do not 'use_strict' without reading the notes in _envEval(); /** - * A templater that allows one to quickly template DOM nodes. + * Begin a new templating process. + * @param node A DOM element or string referring to an element's id + * @param data Data to use in filling out the template + * @param options Options to customize the template processing. One of: + * - allowEval: boolean (default false) Basic template interpolations are + * either property paths (e.g. ${a.b.c.d}), however if allowEval=true then we + * allow arbitrary JavaScript */ -function Templater() { +function template(node, data, options) { + var template = new Templater(options || {}); + template.processNode(node, data); + return template; +} + +/** + * Construct a Templater object. Use template() in preference to this ctor. + * @deprecated Use template(node, data, options); + */ +function Templater(options) { + if (options == null) { + options = { allowEval: true }; + } + this.options = options; this.stack = []; } +/** + * Cached regex used to find ${...} sections in some text. + * Performance note: This regex uses ( and ) to capture the 'script' for + * further processing. Not all of the uses of this regex use this feature so + * if use of the capturing group is a performance drain then we should split + * this regex in two. + */ +Templater.prototype._templateRegion = /\$\{([^}]*)\}/g; + +/** + * Cached regex used to split a string using the unicode chars F001 and F002. + * See Templater._processTextNode() for details. + */ +Templater.prototype._splitSpecial = /\uF001|\uF002/; + +/** + * Cached regex used to detect if a script is capable of being interpreted + * using Template._property() or if we need to use Template._envEval() + */ +Templater.prototype._isPropertyScript = /^[a-zA-Z0-9.]*$/; + /** * Recursive function to walk the tree processing the attributes as it goes. * @param node the node to process. If you pass a string in instead of a DOM @@ -111,7 +152,7 @@ Templater.prototype.processNode = function(node, data) { } } else { // Replace references in all other attributes - var newValue = value.replace(/\$\{[^}]*\}/g, function(path) { + var newValue = value.replace(this._templateRegion, function(path) { return this._envEval(path.slice(2, -1), data, value); }.bind(this)); // Remove '_' prefix of attribute names so the DOM won't try @@ -295,8 +336,8 @@ Templater.prototype._processTextNode = function(node, data) { // We can then split using \uF001 or \uF002 to get an array of strings // where scripts are prefixed with $. // \uF001 and \uF002 are just unicode chars reserved for private use. - value = value.replace(/\$\{([^}]*)\}/g, '\uF001$$$1\uF002'); - var parts = value.split(/\uF001|\uF002/); + value = value.replace(this._templateRegion, '\uF001$$$1\uF002'); + var parts = value.split(this._splitSpecial); if (parts.length > 1) { parts.forEach(function(part) { if (part === null || part === undefined || part === '') { @@ -363,7 +404,7 @@ Templater.prototype._handleAsync = function(thing, siblingNode, inserter) { * @return The string stripped of ${ and }, or untouched if it does not match */ Templater.prototype._stripBraces = function(str) { - if (!str.match(/\$\{.*\}/g)) { + if (!str.match(this._templateRegion)) { this._handleError('Expected ' + str + ' to match ${...}'); return str; } @@ -427,17 +468,26 @@ Templater.prototype._property = function(path, data, newValue) { * execution failed. */ Templater.prototype._envEval = function(script, data, frame) { - with (data) { - try { - this.stack.push(frame); - return eval(script); - } catch (ex) { - this._handleError('Template error evaluating \'' + script + '\'' + - ' environment=' + Object.keys(data).join(', '), ex); - return script; - } finally { - this.stack.pop(); + try { + this.stack.push(frame); + if (this._isPropertyScript.test(script)) { + return this._property(script, data); + } else { + if (!this.options.allowEval) { + this._handleError('allowEval is not set, however \'' + script + '\'' + + ' can not be resolved using a simple property path.'); + return '${' + script + '}'; + } + with (data) { + return eval(script); + } } + } catch (ex) { + this._handleError('Template error evaluating \'' + script + '\'' + + ' environment=' + Object.keys(data).join(', '), ex); + return '${' + script + '}'; + } finally { + this.stack.pop(); } }; diff --git a/browser/devtools/shared/test/browser_templater_basic.js b/browser/devtools/shared/test/browser_templater_basic.js index 12ba0076b56..9812fcd8149 100644 --- a/browser/devtools/shared/test/browser_templater_basic.js +++ b/browser/devtools/shared/test/browser_templater_basic.js @@ -22,7 +22,7 @@ function runTest(index) { holder.innerHTML = options.template; info('Running ' + options.name); - new Templater().processNode(holder, options.data); + template(holder, options.data, options.options); if (typeof options.result == 'string') { is(holder.innerHTML, options.result, options.name); @@ -88,6 +88,7 @@ var tests = [ function() { return { name: 'returnDom', template: '
${__element.ownerDocument.createTextNode(\'pass 2\')}
', + options: { allowEval: true }, data: {}, result: '
pass 2
' };}, @@ -102,6 +103,7 @@ var tests = [ function() { return { name: 'ifTrue', template: '

hello ${name}

', + options: { allowEval: true }, data: { name: 'fred' }, result: '

hello fred

' };}, @@ -109,6 +111,7 @@ var tests = [ function() { return { name: 'ifFalse', template: '

hello ${name}

', + options: { allowEval: true }, data: { name: 'jim' }, result: '' };}, @@ -116,6 +119,7 @@ var tests = [ function() { return { name: 'simpleLoop', template: '

${index}

', + options: { allowEval: true }, data: {}, result: '

1

2

3

' };}, @@ -127,6 +131,7 @@ var tests = [ result: '123' };}, + // Bug 692028: DOMTemplate memory leak with asynchronous arrays // Bug 692031: DOMTemplate async loops do not drop the loop element function() { return { name: 'asyncLoopElement', @@ -150,6 +155,7 @@ var tests = [ function() { return { name: 'useElement', template: '

${adjust(__element)}

', + options: { allowEval: true }, data: { adjust: function(element) { is('pass9', element.id, 'useElement adjust'); @@ -167,6 +173,7 @@ var tests = [ later: 'inline' };}, + // Bug 692028: DOMTemplate memory leak with asynchronous arrays function() { return { name: 'asyncArray', template: '

${i}

', @@ -183,6 +190,7 @@ var tests = [ later: '

4

5

6

' };}, + // Bug 692028: DOMTemplate memory leak with asynchronous arrays function() { return { name: 'asyncBoth', template: '

${i}

', @@ -195,6 +203,38 @@ var tests = [ }, result: '', later: '

4

5

6

' + };}, + + // Bug 701762: DOMTemplate fails when ${foo()} returns undefined + function() { return { + name: 'functionReturningUndefiend', + template: '

${foo()}

', + options: { allowEval: true }, + data: { + foo: function() {} + }, + result: '

undefined

' + };}, + + // Bug 702642: DOMTemplate is relatively slow when evaluating JS ${} + function() { return { + name: 'propertySimple', + template: '

${a.b.c}

', + data: { a: { b: { c: 'hello' } } }, + result: '

hello

' + };}, + + function() { return { + name: 'propertyPass', + template: '

${Math.max(1, 2)}

', + options: { allowEval: true }, + result: '

2

' + };}, + + function() { return { + name: 'propertyFail', + template: '

${Math.max(1, 2)}

', + result: '

${Math.max(1, 2)}

' };} ]; diff --git a/browser/devtools/styleinspector/CssHtmlTree.jsm b/browser/devtools/styleinspector/CssHtmlTree.jsm index a49efa275d2..3cdae6bdb58 100644 --- a/browser/devtools/styleinspector/CssHtmlTree.jsm +++ b/browser/devtools/styleinspector/CssHtmlTree.jsm @@ -214,7 +214,10 @@ CssHtmlTree.processTemplate = function CssHtmlTree_processTemplate(aTemplate, // All the templater does is to populate a given DOM tree with the given // values, so we need to clone the template first. let duplicated = aTemplate.cloneNode(true); - new Templater().processNode(duplicated, aData); + + // See https://github.com/mozilla/domtemplate/blob/master/README.md + // for docs on the template() function + template(duplicated, aData, { allowEval: true }); while (duplicated.firstChild) { aDestination.appendChild(duplicated.firstChild); } diff --git a/browser/devtools/webconsole/GcliCommands.jsm b/browser/devtools/webconsole/GcliCommands.jsm index e5c3d17b6e5..227b0db5d26 100644 --- a/browser/devtools/webconsole/GcliCommands.jsm +++ b/browser/devtools/webconsole/GcliCommands.jsm @@ -62,40 +62,6 @@ gcli.addCommand({ }); -let canon = gcli._internal.require("gcli/canon"); - -/** - * 'help' command - */ -gcli.addCommand({ - name: "help", - returnType: "html", - description: gcli.lookup("helpDesc"), - exec: function Command_help(args, context) { - let output = []; - - output.push("" + gcli.lookup("helpAvailable") + ":
"); - - let commandNames = canon.getCommandNames(); - commandNames.sort(); - - output.push(""); - for (let i = 0; i < commandNames.length; i++) { - let command = canon.getCommand(commandNames[i]); - if (!command.hidden && command.description) { - output.push(""); - output.push('"); - output.push(""); - output.push(""); - } - } - output.push("
' + command.name + "→ " + command.description + "
"); - - return output.join(""); - } -}); - - /** * 'console' command */ diff --git a/browser/devtools/webconsole/HUDService.jsm b/browser/devtools/webconsole/HUDService.jsm index 7a95438dc09..6768f67a641 100644 --- a/browser/devtools/webconsole/HUDService.jsm +++ b/browser/devtools/webconsole/HUDService.jsm @@ -92,6 +92,12 @@ XPCOMUtils.defineLazyGetter(this, "NetUtil", function () { return obj.NetUtil; }); +XPCOMUtils.defineLazyGetter(this, "template", function () { + var obj = {}; + Cu.import("resource:///modules/devtools/Templater.jsm", obj); + return obj.template; +}); + XPCOMUtils.defineLazyGetter(this, "PropertyPanel", function () { var obj = {}; try { @@ -6854,14 +6860,38 @@ GcliTerm.prototype = { let output = aEvent.output.output; if (aEvent.output.command.returnType == "html" && typeof output == "string") { - let frag = this.document.createRange().createContextualFragment( + output = this.document.createRange().createContextualFragment( '
' + - output + '
'); - - output = this.document.createElementNS(HTML_NS, "div"); - output.appendChild(frag); + output + '').firstChild; } - this.writeOutput(output); + + // See https://github.com/mozilla/domtemplate/blob/master/README.md + // for docs on the template() function + let element = this.document.createRange().createContextualFragment( + '' + + ' ').firstChild; + + let hud = HUDService.getHudReferenceById(this.hudId); + let timestamp = ConsoleUtils.timestamp(); + template(element, { + iconContainerStyle: "margin-left=" + (hud.groupDepth * GROUP_INDENT) + "px", + output: output, + timestamp: timestamp, + timestampString: ConsoleUtils.timestampString(timestamp), + clipboardText: output.innerText, + id: "console-msg-" + HUDService.sequenceId() + }); + + ConsoleUtils.setMessageType(element, CATEGORY_OUTPUT, SEVERITY_LOG); + ConsoleUtils.outputMessageNode(element, this.hudId); }, /** diff --git a/browser/devtools/webconsole/gcli.jsm b/browser/devtools/webconsole/gcli.jsm index cf57f6abd5a..014cd4a1da8 100644 --- a/browser/devtools/webconsole/gcli.jsm +++ b/browser/devtools/webconsole/gcli.jsm @@ -686,7 +686,7 @@ var mozl10n = {}; })(mozl10n); -define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/ui/display'], function(require, exports, module) { +define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types/basic', 'gcli/types/javascript', 'gcli/types/node', 'gcli/cli', 'gcli/commands/help', 'gcli/ui/console'], function(require, exports, module) { // The API for use by command authors exports.addCommand = require('gcli/canon').addCommand; @@ -699,9 +699,10 @@ define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types require('gcli/types/javascript').startup(); require('gcli/types/node').startup(); require('gcli/cli').startup(); + require('gcli/commands/help').startup(); var Requisition = require('gcli/cli').Requisition; - var Display = require('gcli/ui/display').Display; + var Console = require('gcli/ui/console').Console; var cli = require('gcli/cli'); var jstype = require('gcli/types/javascript'); @@ -739,15 +740,15 @@ define('gcli/index', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/types opts.requisition = new Requisition(opts.environment, opts.chromeDocument); } - opts.display = new Display(opts); + opts.console = new Console(opts); }, /** * Undo the effects of createView() to prevent memory leaks */ removeView: function(opts) { - opts.display.destroy(); - delete opts.display; + opts.console.destroy(); + delete opts.console; opts.requisition.destroy(); delete opts.requisition; @@ -1029,7 +1030,8 @@ canon.removeCommand = function removeCommand(commandOrName) { * @param name The name of the command to retrieve */ canon.getCommand = function getCommand(name) { - return commands[name]; + // '|| undefined' is to silence 'reference to undefined property' warnings + return commands[name] || undefined; }; /** @@ -1190,8 +1192,16 @@ exports.createEvent = function(name) { var dom = {}; +/** + * XHTML namespace + */ dom.NS_XHTML = 'http://www.w3.org/1999/xhtml'; +/** + * XUL namespace + */ +dom.NS_XUL = 'http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul'; + /** * Create an HTML or XHTML element depending on whether the document is HTML * or XML based. Where HTML/XHTML elements are distinguished by whether they @@ -1246,12 +1256,19 @@ dom.importCss = function(cssText, doc) { */ dom.setInnerHtml = function(elem, html) { if (dom.isXmlDocument(elem.ownerDocument)) { - dom.clearElement(elem); - html = '
' + html + '
'; - var range = elem.ownerDocument.createRange(); - var child = range.createContextualFragment(html).childNodes[0]; - while (child.hasChildNodes()) { - elem.appendChild(child.firstChild); + try { + dom.clearElement(elem); + html = '
' + html + '
'; + var range = elem.ownerDocument.createRange(); + var child = range.createContextualFragment(html).firstChild; + while (child.hasChildNodes()) { + elem.appendChild(child.firstChild); + } + } + catch (ex) { + console.error('Bad XHTML', ex); + console.trace(); + throw ex; } } else { @@ -1260,10 +1277,9 @@ dom.setInnerHtml = function(elem, html) { }; /** - * How to detect if we're in an XUL document (and therefore should create - * elements in an XHTML namespace) - * In a Mozilla XUL document, document.xmlVersion = null, however in Chrome - * document.contentType = undefined. + * How to detect if we're in an XML document. + * In a Mozilla we check that document.xmlVersion = null, however in Chrome + * we use document.contentType = undefined. * @param doc The document element to work from (defaulted to the global * 'document' if missing */ @@ -1479,6 +1495,13 @@ exports.lookup = function(key) { } }; +/** @see propertyLookup in lib/gcli/l10n.js */ +exports.propertyLookup = Proxy.create({ + get: function(rcvr, name) { + return exports.lookup(name); + } +}); + /** @see lookupFormat in lib/gcli/l10n.js */ exports.lookupFormat = function(key, swaps) { try { @@ -3462,6 +3485,14 @@ exports.unsetDocument = function() { doc = undefined; }; +/** + * Getter for the document that contains the nodes we're matching + * Most for changing things back to how they were for unit testing + */ +exports.getDocument = function() { + return doc; +}; + /** * A CSS expression that refers to a single node @@ -4042,7 +4073,15 @@ UnassignedAssignment.prototype.setUnassigned = function(args) { */ function Requisition(environment, doc) { this.environment = environment; - this.document = doc || document; + this.document = doc; + if (this.document == null) { + try { + this.document = document; + } + catch (ex) { + // Ignore + } + } // The command that we are about to execute. // @see setCommandConversion() @@ -4508,7 +4547,8 @@ Requisition.prototype.exec = function(input) { var outputObject = { command: command, args: args, - typed: this.toCanonicalString(), + typed: this.toString(), + canonical: this.toCanonicalString(), completed: false, start: new Date() }; @@ -4527,7 +4567,7 @@ Requisition.prototype.exec = function(input) { }).bind(this); try { - var context = new ExecutionContext(this.environment, this.document); + var context = new ExecutionContext(this); var reply = command.exec(args, context); if (reply != null && reply.isPromise) { @@ -5012,9 +5052,10 @@ exports.Requisition = Requisition; /** * Functions and data related to the execution of a command */ -function ExecutionContext(environment, document) { - this.environment = environment; - this.document = document; +function ExecutionContext(requisition) { + this.requisition = requisition; + this.environment = requisition.environment; + this.document = requisition.document; } ExecutionContext.prototype.createPromise = function() { @@ -5041,7 +5082,275 @@ define('gcli/promise', ['require', 'exports', 'module' ], function(require, expo * http://opensource.org/licenses/BSD-3-Clause */ -define('gcli/ui/display', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) { +define('gcli/commands/help', ['require', 'exports', 'module' , 'gcli/canon', 'gcli/util', 'gcli/l10n', 'gcli/ui/domtemplate', 'text!gcli/commands/help.css', 'text!gcli/commands/help_intro.html', 'text!gcli/commands/help_list.html', 'text!gcli/commands/help_man.html'], function(require, exports, module) { +var help = exports; + + +var canon = require('gcli/canon'); +var util = require('gcli/util'); +var l10n = require('gcli/l10n'); +var domtemplate = require('gcli/ui/domtemplate'); + +var helpCss = require('text!gcli/commands/help.css'); +var helpStyle = undefined; +var helpIntroHtml = require('text!gcli/commands/help_intro.html'); +var helpIntroTemplate = undefined; +var helpListHtml = require('text!gcli/commands/help_list.html'); +var helpListTemplate = undefined; +var helpManHtml = require('text!gcli/commands/help_man.html'); +var helpManTemplate = undefined; + +/** + * 'help' command + * We delay definition of helpCommandSpec until help.startup() to ensure that + * the l10n strings have been loaded + */ +var helpCommandSpec; + +/** + * Registration and de-registration. + */ +help.startup = function() { + + helpCommandSpec = { + name: 'help', + description: l10n.lookup('helpDesc'), + manual: l10n.lookup('helpManual'), + params: [ + { + name: 'search', + type: 'string', + description: l10n.lookup('helpSearchDesc'), + manual: l10n.lookup('helpSearchManual'), + defaultValue: null + } + ], + returnType: 'html', + + exec: function(args, context) { + help.onFirstUseStartup(context.document); + + var match = canon.getCommand(args.search); + if (match) { + var clone = helpManTemplate.cloneNode(true); + domtemplate.template(clone, getManTemplateData(match, context), + { allowEval: true, stack: 'help_man.html' }); + return clone; + } + + var parent = util.dom.createElement(context.document, 'div'); + if (!args.search) { + parent.appendChild(helpIntroTemplate.cloneNode(true)); + } + parent.appendChild(helpListTemplate.cloneNode(true)); + domtemplate.template(parent, getListTemplateData(args, context), + { allowEval: true, stack: 'help_intro.html | help_list.html' }); + return parent; + } + }; + + canon.addCommand(helpCommandSpec); +}; + +help.shutdown = function() { + canon.removeCommand(helpCommandSpec); + + helpListTemplate = undefined; + helpStyle.parentElement.removeChild(helpStyle); + helpStyle = undefined; +}; + +/** + * Called when the command is executed + */ +help.onFirstUseStartup = function(document) { + if (!helpIntroTemplate) { + helpIntroTemplate = util.dom.createElement(document, 'div'); + util.dom.setInnerHtml(helpIntroTemplate, helpIntroHtml); + } + if (!helpListTemplate) { + helpListTemplate = util.dom.createElement(document, 'div'); + util.dom.setInnerHtml(helpListTemplate, helpListHtml); + } + if (!helpManTemplate) { + helpManTemplate = util.dom.createElement(document, 'div'); + util.dom.setInnerHtml(helpManTemplate, helpManHtml); + } + if (!helpStyle && helpCss != null) { + helpStyle = util.dom.importCss(helpCss, document); + } +}; + +/** + * Find an element within the passed element with the class gcli-help-command + * and update the requisition to contain this text. + */ +function updateCommand(element, context) { + context.requisition.update({ + typed: element.querySelector('.gcli-help-command').textContent + }); +} + +/** + * Find an element within the passed element with the class gcli-help-command + * and execute this text. + */ +function executeCommand(element, context) { + context.requisition.exec({ + visible: true, + typed: element.querySelector('.gcli-help-command').textContent + }); +} + +/** + * Create a block of data suitable to be passed to the help_list.html template + */ +function getListTemplateData(args, context) { + return { + l10n: l10n.propertyLookup, + lang: context.document.defaultView.navigator.language, + + onclick: function(ev) { + updateCommand(ev.currentTarget, context); + }, + + ondblclick: function(ev) { + executeCommand(ev.currentTarget, context); + }, + + getHeading: function() { + return args.search == null ? + 'Available Commands:' : + 'Commands starting with \'' + args.search + '\':'; + }, + + getMatchingCommands: function() { + var matching = canon.getCommands().filter(function(command) { + if (args.search && command.name.indexOf(args.search) !== 0) { + // Filtered out because they don't match the search + return false; + } + if (!args.search && command.name.indexOf(' ') != -1) { + // We don't show sub commands with plain 'help' + return false; + } + return true; + }); + matching.sort(); + return matching; + } + }; +} + +/** + * Create a block of data suitable to be passed to the help_man.html template + */ +function getManTemplateData(command, context) { + return { + l10n: l10n.propertyLookup, + lang: context.document.defaultView.navigator.language, + + command: command, + + onclick: function(ev) { + updateCommand(ev.currentTarget, context); + }, + + getTypeDescription: function(param) { + var input = ''; + if (param.defaultValue === undefined) { + input = 'required'; + } + else if (param.defaultValue === null) { + input = 'optional'; + } + else { + input = param.defaultValue; + } + return '(' + param.type.name + ', ' + input + ')'; + } + }; +} + +}); +/* + * Copyright 2009-2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE.txt or: + * http://opensource.org/licenses/BSD-3-Clause + */ + +define('gcli/ui/domtemplate', ['require', 'exports', 'module' ], function(require, exports, module) { + + var obj = {}; + Components.utils.import('resource:///modules/devtools/Templater.jsm', obj); + exports.template = obj.template; + +}); +define("text!gcli/commands/help.css", [], void 0); +define("text!gcli/commands/help_intro.html", [], "\n" + + "

${l10n.introHeader}

\n" + + "\n" + + "

\n" + + " \n" + + " ${l10n.introBody}\n" + + " \n" + + "

\n" + + ""); + +define("text!gcli/commands/help_list.html", [], "\n" + + "

${getHeading()}

\n" + + "\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "
${command.name}\n" + + " ${command.description}\n" + + " help ${command.name}\n" + + "
\n" + + ""); + +define("text!gcli/commands/help_man.html", [], "\n" + + "

${command.name}

\n" + + "\n" + + "

\n" + + " ${l10n.helpManSynopsis}:\n" + + " \n" + + " ${command.name}\n" + + " \n" + + " ${param.defaultValue !== undefined ? '[' + param.name + ']' : param.name}\n" + + " \n" + + " \n" + + "

\n" + + "\n" + + "

${l10n.helpManDescription}:

\n" + + "\n" + + "

\n" + + " ${command.manual || command.description}\n" + + "

\n" + + "\n" + + "

${l10n.helpManParameters}:

\n" + + "\n" + + "\n" + + ""); + +/* + * Copyright 2009-2011 Mozilla Foundation and contributors + * Licensed under the New BSD license. See LICENSE.txt or: + * http://opensource.org/licenses/BSD-3-Clause + */ + +define('gcli/ui/console', ['require', 'exports', 'module' , 'gcli/ui/inputter', 'gcli/ui/arg_fetch', 'gcli/ui/menu', 'gcli/ui/focus'], function(require, exports, module) { var Inputter = require('gcli/ui/inputter').Inputter; var ArgFetcher = require('gcli/ui/arg_fetch').ArgFetcher; @@ -5049,10 +5358,10 @@ var CommandMenu = require('gcli/ui/menu').CommandMenu; var FocusManager = require('gcli/ui/focus').FocusManager; /** - * Display is responsible for generating the UI for GCLI, this implementation + * Console is responsible for generating the UI for GCLI, this implementation * is a special case for use inside Firefox */ -function Display(options) { +function Console(options) { this.hintElement = options.hintElement; this.gcliTerm = options.gcliTerm; this.consoleWrap = options.consoleWrap; @@ -5097,7 +5406,7 @@ function Display(options) { /** * Avoid memory leaks */ -Display.prototype.destroy = function() { +Console.prototype.destroy = function() { this.chromeWindow.removeEventListener('resize', this.resizer, false); delete this.resizer; delete this.chromeWindow; @@ -5122,10 +5431,17 @@ Display.prototype.destroy = function() { /** * Called on chrome window resize, or on divider slide */ -Display.prototype.resizer = function() { +Console.prototype.resizer = function() { + // Bug 705109: There are several numbers hard-coded in this function. + // This is simpler than calculating them, but error-prone when the UI setup, + // the styling or display settings change. + var parentRect = this.consoleWrap.getBoundingClientRect(); + // Magic number: 64 is the size of the toolbar above the output area var parentHeight = parentRect.bottom - parentRect.top - 64; + // Magic number: 100 is the size at which we decide the hints are too small + // to be useful, so we hide them if (parentHeight < 100) { this.hintElement.classList.add('gcliterm-hint-nospace'); } @@ -5136,20 +5452,14 @@ Display.prototype.resizer = function() { if (isMenuVisible) { this.menu.setMaxHeight(parentHeight); - // Magic numbers. We have 2 options - lots of complex dom math to derive - // the height of a menu item (19 pixels) and the vertical padding - // (22 pixels), or we could just hard-code. The former is *slightly* more - // resilient to refactoring (but still breaks with dom structure changes), - // the latter is simpler, faster and easier. + // Magic numbers: 19 = height of a menu item, 22 = total vertical padding + // of container var idealMenuHeight = (19 * this.menu.items.length) + 22; - if (idealMenuHeight > parentHeight) { - this.hintElement.style.overflowY = 'scroll'; - this.hintElement.style.borderBottomColor = 'threedshadow'; + this.hintElement.classList.add('gcliterm-hint-scroll'); } else { - this.hintElement.style.overflowY = null; - this.hintElement.style.borderBottomColor = 'white'; + this.hintElement.classList.remove('gcliterm-hint-scroll'); } } else { @@ -5161,7 +5471,7 @@ Display.prototype.resizer = function() { } }; -exports.Display = Display; +exports.Console = Console; }); /* @@ -5582,8 +5892,9 @@ cliView.Inputter = Inputter; * - document (required) DOM document to be used in creating elements * - requisition (required) A GCLI Requisition object whose state is monitored * - completeElement (optional) An element to use - * - completionPrompt (optional) The prompt to show before a completion. - * Defaults to '»' (double greater-than, a.k.a right guillemet). + * - completionPrompt (optional) The prompt - defaults to '\u00bb' + * (double greater-than, a.k.a right guillemet). The prompt is used directly + * in a TextNode, so HTML entities are not allowed. */ function Completer(options) { this.document = options.document || document; @@ -5606,7 +5917,7 @@ function Completer(options) { this.completionPrompt = typeof options.completionPrompt === 'string' ? options.completionPrompt - : '»'; + : '\u00bb'; if (options.inputBackgroundElement) { this.backgroundElement = options.inputBackgroundElement; @@ -5714,50 +6025,85 @@ Completer.prototype.update = function(input) { var current = this.requisition.getAssignmentAt(input.cursor.start); var predictions = current.getPredictions(); - var completion = '' + this.completionPrompt + ' '; + dom.clearElement(this.element); + + // All this DOM manipulation is equivalent to the HTML below. + // It's not a template because it's very simple except appendMarkupStatus() + // which is complex due to a need to merge spans. + // Bug 707131 questions if we couldn't simplify this to use a template. + // + // ${completionPrompt} + // ${appendMarkupStatus()} + // ${prefix} + // ${contents} + // } + + var document = this.element.ownerDocument; + var prompt = document.createElement('span'); + prompt.classList.add('gcli-prompt'); + prompt.appendChild(document.createTextNode(this.completionPrompt + ' ')); + this.element.appendChild(prompt); + if (input.typed.length > 0) { var scores = this.requisition.getInputStatusMarkup(input.cursor.start); - completion += this.markupStatusScore(scores, input); + this.appendMarkupStatus(this.element, scores, input); } if (input.typed.length > 0 && predictions.length > 0) { var tab = predictions[0].name; var existing = current.getArg().text; - if (isStrictCompletion(existing, tab) && input.cursor.start === input.typed.length) { - // Display the suffix of the prediction as the completion. + + var contents; + var prefix = null; + + if (isStrictCompletion(existing, tab) && + input.cursor.start === input.typed.length) { + // Display the suffix of the prediction as the completion var numLeadingSpaces = existing.match(/^(\s*)/)[0].length; - var suffix = tab.slice(existing.length - numLeadingSpaces); - completion += '' + suffix + ''; + contents = tab.slice(existing.length - numLeadingSpaces); } else { // Display the '-> prediction' at the end of the completer element - completion += '  ⇥ ' + - tab + ''; + prefix = ' \u00a0'; // aka   + contents = '\u21E5 ' + tab; // aka → the right arrow } + + if (prefix != null) { + this.element.appendChild(document.createTextNode(prefix)); + } + + var suffix = document.createElement('span'); + suffix.classList.add('gcli-in-ontab'); + suffix.appendChild(document.createTextNode(contents)); + this.element.appendChild(suffix); } - // A hack to add a grey '}' to the end of the command line when we've opened + // Add a grey '}' to the end of the command line when we've opened // with a { but haven't closed it var command = this.requisition.commandAssignment.getValue(); - if (command && command.name === '{') { - if (this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1) { - completion += '}'; - } + var unclosedJs = command && command.name === '{' && + this.requisition.getAssignment(0).getArg().suffix.indexOf('}') === -1; + if (unclosedJs) { + var close = document.createElement('span'); + close.classList.add('gcli-in-closebrace'); + close.appendChild(document.createTextNode('}')); + this.element.appendChild(close); } - - dom.setInnerHtml(this.element, completion); }; /** * Mark-up an array of Status values with spans */ -Completer.prototype.markupStatusScore = function(scores, input) { - var completion = ''; +Completer.prototype.appendMarkupStatus = function(element, scores, input) { if (scores.length === 0) { - return completion; + return; } + var document = element.ownerDocument; var i = 0; var lastStatus = -1; + var span; + var contents = ''; + while (true) { if (lastStatus !== scores[i]) { var state = scores[i]; @@ -5765,25 +6111,27 @@ Completer.prototype.markupStatusScore = function(scores, input) { console.error('No state at i=' + i + '. scores.len=' + scores.length); state = Status.VALID; } - completion += ''; + span = document.createElement('span'); + span.classList.add('gcli-in-' + state.toString().toLowerCase()); lastStatus = scores[i]; } var char = input.typed[i]; if (char === ' ') { - char = ' '; + char = '\u00a0'; } - completion += char; + contents += char; i++; if (i === input.typed.length) { - completion += ''; + span.appendChild(document.createTextNode(contents)); + this.element.appendChild(span); break; } if (lastStatus !== scores[i]) { - completion += ''; + span.appendChild(document.createTextNode(contents)); + this.element.appendChild(span); + contents = ''; } } - - return completion; }; cliView.Completer = Completer; @@ -5867,7 +6215,7 @@ var dom = require('gcli/util').dom; var Status = require('gcli/types').Status; var getField = require('gcli/ui/field').getField; -var Templater = require('gcli/ui/domtemplate').Templater; +var domtemplate = require('gcli/ui/domtemplate'); var editorCss = require('text!gcli/ui/arg_fetch.css'); var argFetchHtml = require('text!gcli/ui/arg_fetch.html'); @@ -5896,7 +6244,6 @@ function ArgFetcher(options) { // We cache the fields we create so we can destroy them later this.fields = []; - this.tmpl = new Templater(); // Populated by template this.okElement = null; @@ -5953,7 +6300,8 @@ ArgFetcher.prototype.onCommandChange = function(ev) { this.fields = []; var reqEle = this.reqTempl.cloneNode(true); - this.tmpl.processNode(reqEle, this); + domtemplate.template(reqEle, this, + { allowEval: true, stack: 'arg_fetch.html' }); dom.clearElement(this.element); this.element.appendChild(reqEle); @@ -6008,7 +6356,7 @@ ArgFetcher.prototype.getInputFor = function(assignment) { return newField.element; } catch (ex) { - // This is called from within Templater which can make tracing errors hard + // This is called from within template() which can make tracing errors hard // so we log here if anything goes wrong console.error(ex); return ''; @@ -6252,7 +6600,7 @@ function StringField(type, options) { this.element = dom.createElement(this.document, 'input'); this.element.type = 'text'; - this.element.className = 'gcli-field'; + this.element.classList.add('gcli-field'); this.onInputChange = this.onInputChange.bind(this); this.element.addEventListener('keyup', this.onInputChange, false); @@ -6412,7 +6760,7 @@ function SelectionField(type, options) { this.items = []; this.element = dom.createElement(this.document, 'select'); - this.element.className = 'gcli-field'; + this.element.classList.add('gcli-field'); this._addOption({ name: l10n.lookupFormat('fieldSelectionSelect', [ options.name ]) }); @@ -6487,8 +6835,8 @@ function JavascriptField(type, options) { this.input = dom.createElement(this.document, 'input'); this.input.type = 'text'; this.input.addEventListener('keyup', this.onInputChange, false); - this.input.style.marginBottom = '0'; - this.input.className = 'gcli-field'; + this.input.classList.add('gcli-field'); + this.input.classList.add('gcli-field-javascript'); this.element.appendChild(this.input); this.menu = new Menu({ document: this.document, field: true }); @@ -6680,18 +7028,18 @@ function ArrayField(type, options) { //
this.element = dom.createElement(this.document, 'div'); - this.element.className = 'gcliArrayParent'; + this.element.classList.add('gcli-array-parent'); //